// ======================================== // FICHIER: lib/ErrorReporting.js - SYSTÈME DE LOGGING SOURCEFINDER // Description: Système de logging Pino avec traçage hiérarchique et WebSocket // ======================================== const fs = require('fs').promises; const path = require('path'); const pino = require('pino'); const pretty = require('pino-pretty'); const { PassThrough } = require('stream'); const WebSocket = require('ws'); // Import du traçage (injection différée pour éviter références circulaires) const { setLogger } = require('./trace'); // WebSocket server for real-time logs let wsServer; const wsClients = new Set(); // Configuration Pino avec fichiers datés const now = new Date(); const timestamp = now.toISOString().slice(0, 10) + '_' + now.toLocaleTimeString('fr-FR').replace(/:/g, '-'); const logFile = path.join(__dirname, '..', 'logs', `sourcefinder-${timestamp}.log`); const prettyStream = pretty({ colorize: true, translateTime: 'HH:MM:ss.l', ignore: 'pid,hostname', messageFormat: '{msg}', customPrettifiers: { level: (logLevel) => { const levels = { 10: '🔍 DEBUG', 20: '📝 INFO', 25: '🤖 PROMPT', 26: '⚡ LLM', 30: '⚠️ WARN', 40: '❌ ERROR', 50: '💀 FATAL', 5: '👁️ TRACE' }; return levels[logLevel] || logLevel; } } }); const tee = new PassThrough(); let consolePipeInitialized = false; // File destination with dated filename const fileDest = pino.destination({ dest: logFile, mkdir: true, sync: false, minLength: 0 }); tee.pipe(fileDest); // Niveaux personnalisés pour SourceFinder const customLevels = { trace: 5, // Traçage hiérarchique détaillé debug: 10, // Debug standard info: 20, // Informations importantes prompt: 25, // Requêtes vers LLMs llm: 26, // Réponses LLM warn: 30, // Avertissements error: 40, // Erreurs fatal: 50 // Erreurs fatales }; // Logger Pino principal const logger = pino( { level: (process.env.LOG_LEVEL || 'info').toLowerCase(), base: undefined, timestamp: pino.stdTimeFunctions.isoTime, customLevels: customLevels, useOnlyCustomLevels: true }, tee ); // Initialiser WebSocket server si activé function initWebSocketServer() { if (!wsServer && process.env.ENABLE_LOG_WS === 'true') { try { const logPort = process.env.LOG_WS_PORT || 8082; wsServer = new WebSocket.Server({ port: logPort }); wsServer.on('connection', (ws) => { wsClients.add(ws); logger.info('Client connecté au WebSocket des logs'); ws.on('close', () => { wsClients.delete(ws); logger.info('Client WebSocket déconnecté'); }); ws.on('error', (error) => { logger.error('Erreur WebSocket:', error.message); wsClients.delete(ws); }); }); wsServer.on('error', (error) => { if (error.code === 'EADDRINUSE') { logger.warn(`Port WebSocket ${logPort} déjà utilisé`); wsServer = null; } else { logger.error('Erreur serveur WebSocket:', error.message); } }); logger.info(`Serveur WebSocket des logs démarré sur le port ${logPort}`); } catch (error) { logger.warn(`Échec démarrage serveur WebSocket: ${error.message}`); wsServer = null; } } } // Diffusion vers clients WebSocket function broadcastLog(message, level) { const logData = { timestamp: new Date().toISOString(), level: level.toUpperCase(), message: message, service: 'SourceFinder' }; wsClients.forEach(ws => { if (ws.readyState === WebSocket.OPEN) { try { ws.send(JSON.stringify(logData)); } catch (error) { logger.error('Échec envoi log vers client WebSocket:', error.message); wsClients.delete(ws); } } }); } // Fonction principale de logging SourceFinder async function logSh(message, level = 'INFO') { // Initialiser WebSocket si demandé if (!wsServer) { initWebSocketServer(); } // Initialiser sortie console si demandée if (!consolePipeInitialized && (process.env.ENABLE_CONSOLE_LOG === 'true' || process.env.NODE_ENV === 'development')) { tee.pipe(prettyStream).pipe(process.stdout); consolePipeInitialized = true; } const pinoLevel = level.toLowerCase(); // Métadonnées de traçage pour logging hiérarchique const traceData = {}; if (message.includes('▶') || message.includes('✔') || message.includes('✖') || message.includes('•')) { traceData.trace = true; traceData.service = 'SourceFinder'; traceData.evt = message.includes('▶') ? 'span.start' : message.includes('✔') ? 'span.end' : message.includes('✖') ? 'span.error' : 'span.event'; } // Ajouter contexte SourceFinder traceData.service = 'SourceFinder'; traceData.timestamp = new Date().toISOString(); // Logger avec Pino switch (pinoLevel) { case 'error': logger.error(traceData, message); break; case 'warning': case 'warn': logger.warn(traceData, message); break; case 'debug': logger.debug(traceData, message); break; case 'trace': logger.trace(traceData, message); break; case 'prompt': logger.prompt(traceData, message); break; case 'llm': logger.llm(traceData, message); break; case 'fatal': logger.fatal(traceData, message); break; default: logger.info(traceData, message); } // Diffuser vers clients WebSocket broadcastLog(message, level); // Force flush pour affichage temps réel logger.flush(); } // Méthodes de logging spécialisées SourceFinder const sourceFinderLogger = { // Recherche de news newsSearch: (message, metadata = {}) => { logSh(`🔍 [NEWS_SEARCH] ${message}`, 'INFO'); if (Object.keys(metadata).length > 0) { logSh(` Métadonnées: ${JSON.stringify(metadata)}`, 'DEBUG'); } }, // Interactions LLM llmRequest: (message, metadata = {}) => { logSh(`🤖 [LLM_REQUEST] ${message}`, 'PROMPT'); if (metadata.tokens) { logSh(` Tokens: ${metadata.tokens}`, 'DEBUG'); } }, llmResponse: (message, metadata = {}) => { logSh(`⚡ [LLM_RESPONSE] ${message}`, 'LLM'); if (metadata.duration) { logSh(` Durée: ${metadata.duration}ms`, 'DEBUG'); } }, // Opérations de stock stockOperation: (message, operation, count = 0, metadata = {}) => { logSh(`📦 [STOCK_${operation.toUpperCase()}] ${message}`, 'INFO'); if (count > 0) { logSh(` Articles traités: ${count}`, 'DEBUG'); } if (Object.keys(metadata).length > 0) { logSh(` Détails: ${JSON.stringify(metadata)}`, 'DEBUG'); } }, // Scoring d'articles scoringOperation: (message, score = null, metadata = {}) => { const scoreStr = score !== null ? ` [Score: ${score}]` : ''; logSh(`🎯 [SCORING]${scoreStr} ${message}`, 'INFO'); if (Object.keys(metadata).length > 0) { logSh(` Métadonnées: ${JSON.stringify(metadata)}`, 'DEBUG'); } }, // Erreurs spécifiques antiInjectionAlert: (message, metadata = {}) => { logSh(`🛡️ [ANTI_INJECTION] ${message}`, 'WARN'); if (Object.keys(metadata).length > 0) { logSh(` Contexte: ${JSON.stringify(metadata)}`, 'WARN'); } }, // Performance et métriques performance: (message, duration, metadata = {}) => { logSh(`⏱️ [PERFORMANCE] ${message} (${duration}ms)`, 'DEBUG'); if (Object.keys(metadata).length > 0) { logSh(` Métriques: ${JSON.stringify(metadata)}`, 'DEBUG'); } } }; // Nettoyer logs anciens async function cleanLocalLogs() { try { const logsDir = path.join(__dirname, '../logs'); try { const files = await fs.readdir(logsDir); const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - 7); // Garder 7 jours for (const file of files) { if (file.endsWith('.log') && file.startsWith('sourcefinder-')) { const filePath = path.join(logsDir, file); const stats = await fs.stat(filePath); if (stats.mtime < cutoffDate) { await fs.unlink(filePath); logSh(`🗑️ Log ancien supprimé: ${file}`, 'INFO'); } } } } catch (error) { // Répertoire pourrait ne pas exister } } catch (error) { // Échec silencieux } } // Fonction de nettoyage générale async function cleanLogSheet() { try { logSh('🧹 Nettoyage logs SourceFinder...', 'INFO'); await cleanLocalLogs(); logSh('✅ Nettoyage logs terminé', 'INFO'); } catch (error) { logSh('Erreur nettoyage logs: ' + error.message, 'ERROR'); } } // Injecter logSh dans le système de traçage setLogger(logSh); // Exports pour SourceFinder module.exports = { logSh, ...sourceFinderLogger, cleanLogSheet, initWebSocketServer, // Import du traçage setupTracer: require('./trace').setupTracer, tracer: require('./trace').tracer };