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