feat: add cache monitoring scripts and enhance JiraAnalyticsCache
- Introduced `cache-monitor.ts` for real-time cache monitoring, providing stats and actions for managing Jira analytics cache. - Updated `package.json` with new cache-related scripts for easy access. - Enhanced `JiraAnalyticsCacheService` to support TTL for cache entries, automatic cleanup of expired entries, and improved logging for cache operations. - Added methods for calculating time until expiry and formatting TTL for better visibility.
This commit is contained in:
@@ -13,7 +13,11 @@
|
||||
"backup:config": "npx tsx scripts/backup-manager.ts config",
|
||||
"backup:start": "npx tsx scripts/backup-manager.ts scheduler-start",
|
||||
"backup:stop": "npx tsx scripts/backup-manager.ts scheduler-stop",
|
||||
"backup:status": "npx tsx scripts/backup-manager.ts scheduler-status"
|
||||
"backup:status": "npx tsx scripts/backup-manager.ts scheduler-status",
|
||||
"cache:monitor": "npx tsx scripts/cache-monitor.ts",
|
||||
"cache:stats": "npx tsx scripts/cache-monitor.ts stats",
|
||||
"cache:cleanup": "npx tsx scripts/cache-monitor.ts cleanup",
|
||||
"cache:clear": "npx tsx scripts/cache-monitor.ts clear"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
|
||||
137
scripts/cache-monitor.ts
Normal file
137
scripts/cache-monitor.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* Script de monitoring du cache Jira Analytics
|
||||
* Usage: npm run cache:monitor
|
||||
*/
|
||||
|
||||
import { jiraAnalyticsCache } from '../src/services/integrations/jira/analytics-cache';
|
||||
import * as readline from 'readline';
|
||||
|
||||
function displayCacheStats() {
|
||||
console.log('\n📊 === STATISTIQUES DU CACHE JIRA ANALYTICS ===');
|
||||
|
||||
const stats = jiraAnalyticsCache.getStats();
|
||||
|
||||
console.log(`\n📈 Total des entrées: ${stats.totalEntries}`);
|
||||
|
||||
if (stats.projects.length === 0) {
|
||||
console.log('📭 Aucune donnée en cache');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\n📋 Projets en cache:');
|
||||
stats.projects.forEach(project => {
|
||||
const status = project.isExpired ? '❌ EXPIRÉ' : '✅ VALIDE';
|
||||
console.log(` • ${project.projectKey}:`);
|
||||
console.log(` - Âge: ${project.age}`);
|
||||
console.log(` - TTL: ${project.ttl}`);
|
||||
console.log(` - Expire dans: ${project.expiresIn}`);
|
||||
console.log(` - Taille: ${Math.round(project.size / 1024)}KB`);
|
||||
console.log(` - Statut: ${status}`);
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
||||
function displayCacheActions() {
|
||||
console.log('\n🔧 === ACTIONS DISPONIBLES ===');
|
||||
console.log('1. Afficher les statistiques');
|
||||
console.log('2. Forcer le nettoyage');
|
||||
console.log('3. Invalider tout le cache');
|
||||
console.log('4. Surveiller en temps réel (Ctrl+C pour arrêter)');
|
||||
console.log('5. Quitter');
|
||||
}
|
||||
|
||||
async function monitorRealtime() {
|
||||
console.log('\n👀 Surveillance en temps réel (Ctrl+C pour arrêter)...');
|
||||
|
||||
const interval = setInterval(() => {
|
||||
console.clear();
|
||||
displayCacheStats();
|
||||
console.log('\n⏰ Mise à jour toutes les 5 secondes...');
|
||||
}, 5000);
|
||||
|
||||
// Gérer l'arrêt propre
|
||||
process.on('SIGINT', () => {
|
||||
clearInterval(interval);
|
||||
console.log('\n\n👋 Surveillance arrêtée');
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 Cache Monitor Jira Analytics');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
switch (command) {
|
||||
case 'stats':
|
||||
displayCacheStats();
|
||||
break;
|
||||
|
||||
case 'cleanup':
|
||||
console.log('\n🧹 Nettoyage forcé du cache...');
|
||||
const cleaned = jiraAnalyticsCache.forceCleanup();
|
||||
console.log(`✅ ${cleaned} entrées supprimées`);
|
||||
break;
|
||||
|
||||
case 'clear':
|
||||
console.log('\n🗑️ Invalidation de tout le cache...');
|
||||
jiraAnalyticsCache.invalidateAll();
|
||||
console.log('✅ Cache vidé');
|
||||
break;
|
||||
|
||||
case 'monitor':
|
||||
await monitorRealtime();
|
||||
break;
|
||||
|
||||
default:
|
||||
displayCacheStats();
|
||||
displayCacheActions();
|
||||
|
||||
// Interface interactive simple
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
const askAction = () => {
|
||||
rl.question('\nChoisissez une action (1-5): ', async (answer: string) => {
|
||||
switch (answer.trim()) {
|
||||
case '1':
|
||||
displayCacheStats();
|
||||
askAction();
|
||||
break;
|
||||
case '2':
|
||||
const cleaned = jiraAnalyticsCache.forceCleanup();
|
||||
console.log(`✅ ${cleaned} entrées supprimées`);
|
||||
askAction();
|
||||
break;
|
||||
case '3':
|
||||
jiraAnalyticsCache.invalidateAll();
|
||||
console.log('✅ Cache vidé');
|
||||
askAction();
|
||||
break;
|
||||
case '4':
|
||||
rl.close();
|
||||
await monitorRealtime();
|
||||
break;
|
||||
case '5':
|
||||
console.log('👋 Au revoir !');
|
||||
rl.close();
|
||||
process.exit(0);
|
||||
break;
|
||||
default:
|
||||
console.log('❌ Action invalide');
|
||||
askAction();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
askAction();
|
||||
}
|
||||
}
|
||||
|
||||
// Exécution du script
|
||||
main().catch(console.error);
|
||||
@@ -1,20 +1,24 @@
|
||||
/**
|
||||
* Service de cache pour les analytics Jira
|
||||
* Cache en mémoire avec invalidation manuelle
|
||||
* Cache en mémoire avec invalidation manuelle et TTL
|
||||
*/
|
||||
|
||||
import { JiraAnalytics } from '@/lib/types';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
interface CacheEntry {
|
||||
data: JiraAnalytics;
|
||||
timestamp: number;
|
||||
projectKey: string;
|
||||
configHash: string; // Hash de la config Jira pour détecter les changements
|
||||
ttl: number; // Time To Live en millisecondes
|
||||
}
|
||||
|
||||
class JiraAnalyticsCacheService {
|
||||
private cache = new Map<string, CacheEntry>();
|
||||
private readonly CACHE_KEY_PREFIX = 'jira-analytics:';
|
||||
private readonly DEFAULT_TTL = 30 * 60 * 1000; // 30 minutes par défaut
|
||||
private cleanupInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
/**
|
||||
* Génère une clé de cache basée sur la config Jira
|
||||
@@ -24,22 +28,15 @@ class JiraAnalyticsCacheService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un hash de la configuration Jira pour détecter les changements
|
||||
* Génère un hash sécurisé de la configuration Jira pour détecter les changements
|
||||
*/
|
||||
private generateConfigHash(config: { baseUrl: string; email: string; apiToken: string; projectKey: string }): string {
|
||||
const configString = `${config.baseUrl}|${config.email}|${config.apiToken}|${config.projectKey}`;
|
||||
// Simple hash (pour production, utiliser crypto.createHash)
|
||||
let hash = 0;
|
||||
for (let i = 0; i < configString.length; i++) {
|
||||
const char = configString.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return hash.toString();
|
||||
return createHash('sha256').update(configString).digest('hex').substring(0, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les analytics depuis le cache si disponible
|
||||
* Récupère les analytics depuis le cache si disponible et non expirées
|
||||
*/
|
||||
get(config: { baseUrl: string; email: string; apiToken: string; projectKey: string }): JiraAnalytics | null {
|
||||
const configHash = this.generateConfigHash(config);
|
||||
@@ -59,14 +56,24 @@ class JiraAnalyticsCacheService {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`✅ Cache HIT pour projet ${config.projectKey} (${this.getAgeDescription(entry.timestamp)})`);
|
||||
// Vérifier si l'entrée a expiré
|
||||
const now = Date.now();
|
||||
const isExpired = (now - entry.timestamp) > entry.ttl;
|
||||
|
||||
if (isExpired) {
|
||||
console.log(`⏰ Cache EXPIRÉ pour projet ${config.projectKey} (${this.getAgeDescription(entry.timestamp)})`);
|
||||
this.cache.delete(cacheKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`✅ Cache HIT pour projet ${config.projectKey} (${this.getAgeDescription(entry.timestamp)}, expire dans ${this.getTimeUntilExpiry(entry.timestamp, entry.ttl)})`);
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stocke les analytics dans le cache
|
||||
* Stocke les analytics dans le cache avec TTL personnalisé
|
||||
*/
|
||||
set(config: { baseUrl: string; email: string; apiToken: string; projectKey: string }, data: JiraAnalytics): void {
|
||||
set(config: { baseUrl: string; email: string; apiToken: string; projectKey: string }, data: JiraAnalytics, ttl?: number): void {
|
||||
const configHash = this.generateConfigHash(config);
|
||||
const cacheKey = this.getCacheKey(config.projectKey, configHash);
|
||||
|
||||
@@ -74,11 +81,15 @@ class JiraAnalyticsCacheService {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
projectKey: config.projectKey,
|
||||
configHash
|
||||
configHash,
|
||||
ttl: ttl || this.DEFAULT_TTL
|
||||
};
|
||||
|
||||
this.cache.set(cacheKey, entry);
|
||||
console.log(`💾 Analytics mises en cache pour projet ${config.projectKey}`);
|
||||
console.log(`💾 Analytics mises en cache pour projet ${config.projectKey} (TTL: ${this.formatTTL(entry.ttl)})`);
|
||||
|
||||
// Démarrer le nettoyage automatique si pas déjà fait
|
||||
this.startCleanupInterval();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,16 +117,27 @@ class JiraAnalyticsCacheService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les statistiques du cache
|
||||
* Retourne les statistiques du cache avec informations TTL
|
||||
*/
|
||||
getStats(): {
|
||||
totalEntries: number;
|
||||
projects: Array<{ projectKey: string; age: string; size: number }>;
|
||||
projects: Array<{
|
||||
projectKey: string;
|
||||
age: string;
|
||||
size: number;
|
||||
ttl: string;
|
||||
expiresIn: string;
|
||||
isExpired: boolean;
|
||||
}>;
|
||||
} {
|
||||
const now = Date.now();
|
||||
const projects = Array.from(this.cache.entries()).map(([, entry]) => ({
|
||||
projectKey: entry.projectKey,
|
||||
age: this.getAgeDescription(entry.timestamp),
|
||||
size: JSON.stringify(entry.data).length
|
||||
size: JSON.stringify(entry.data).length,
|
||||
ttl: this.formatTTL(entry.ttl),
|
||||
expiresIn: this.getTimeUntilExpiry(entry.timestamp, entry.ttl),
|
||||
isExpired: (now - entry.timestamp) > entry.ttl
|
||||
}));
|
||||
|
||||
return {
|
||||
@@ -141,6 +163,72 @@ class JiraAnalyticsCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le temps restant avant expiration
|
||||
*/
|
||||
private getTimeUntilExpiry(timestamp: number, ttl: number): string {
|
||||
const now = Date.now();
|
||||
const remainingMs = ttl - (now - timestamp);
|
||||
|
||||
if (remainingMs <= 0) return 'expiré';
|
||||
|
||||
const remainingMinutes = Math.floor(remainingMs / (1000 * 60));
|
||||
const remainingHours = Math.floor(remainingMinutes / 60);
|
||||
|
||||
if (remainingHours > 0) {
|
||||
return `${remainingHours}h${remainingMinutes % 60}m`;
|
||||
} else {
|
||||
return `${remainingMinutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate un TTL en texte lisible
|
||||
*/
|
||||
private formatTTL(ttl: number): string {
|
||||
const minutes = Math.floor(ttl / (1000 * 60));
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h${minutes % 60}m`;
|
||||
} else {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Démarre l'intervalle de nettoyage automatique
|
||||
*/
|
||||
private startCleanupInterval(): void {
|
||||
if (this.cleanupInterval) return; // Déjà démarré
|
||||
|
||||
// Nettoyer toutes les 5 minutes
|
||||
this.cleanupInterval = setInterval(() => {
|
||||
this.cleanupExpiredEntries();
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
console.log('🧹 Nettoyage automatique du cache démarré (toutes les 5 minutes)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoie les entrées expirées du cache
|
||||
*/
|
||||
private cleanupExpiredEntries(): void {
|
||||
const now = Date.now();
|
||||
let cleanedCount = 0;
|
||||
|
||||
for (const [key, entry] of this.cache.entries()) {
|
||||
if ((now - entry.timestamp) > entry.ttl) {
|
||||
this.cache.delete(key);
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCount > 0) {
|
||||
console.log(`🧹 Nettoyage automatique: ${cleanedCount} entrées expirées supprimées`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une entrée existe pour un projet
|
||||
*/
|
||||
@@ -149,6 +237,30 @@ class JiraAnalyticsCacheService {
|
||||
const cacheKey = this.getCacheKey(config.projectKey, configHash);
|
||||
return this.cache.has(cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Arrête le nettoyage automatique
|
||||
*/
|
||||
stopCleanupInterval(): void {
|
||||
if (this.cleanupInterval) {
|
||||
clearInterval(this.cleanupInterval);
|
||||
this.cleanupInterval = null;
|
||||
console.log('🛑 Nettoyage automatique du cache arrêté');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force un nettoyage immédiat des entrées expirées
|
||||
*/
|
||||
forceCleanup(): number {
|
||||
const beforeCount = this.cache.size;
|
||||
this.cleanupExpiredEntries();
|
||||
const afterCount = this.cache.size;
|
||||
const cleanedCount = beforeCount - afterCount;
|
||||
|
||||
console.log(`🧹 Nettoyage forcé: ${cleanedCount} entrées supprimées`);
|
||||
return cleanedCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Instance singleton
|
||||
|
||||
Reference in New Issue
Block a user