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:
Julien Froidefond
2025-09-26 11:42:18 +02:00
parent 350dbe6479
commit bd7ede412e
3 changed files with 273 additions and 20 deletions

View File

@@ -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
View 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);

View File

@@ -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