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

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