""" Système de logging centralisé pour RPA Vision V3 Ce module fournit un logging unifié pour tous les composants avec: - Format structuré et cohérent - Filtrage des codes ANSI - Nettoyage des noms de composants - Rotation des fichiers - Intégration Prometheus """ import logging import logging.handlers import json import os import re import sys from datetime import datetime from typing import Dict, Any, Optional, List from dataclasses import dataclass, field from pathlib import Path # Import des métriques Prometheus try: from core.monitoring.metrics import increment_log_entry METRICS_ENABLED = True except ImportError: METRICS_ENABLED = False # ============================================================================= # Configuration # ============================================================================= # Répertoire des logs LOG_DIR = Path(__file__).parent.parent.parent / "logs" LOG_DIR.mkdir(parents=True, exist_ok=True) # Format unifié LOG_FORMAT = "%(asctime)s | %(levelname)-7s | %(name)-20s | %(message)s" LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" # Rotation MAX_LOG_SIZE = 10 * 1024 * 1024 # 10 MB BACKUP_COUNT = 5 # Garder 5 fichiers de backup # Pattern pour supprimer les codes ANSI ANSI_ESCAPE_PATTERN = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') # ============================================================================= # Filtres et Formatters # ============================================================================= class ANSIStripFilter(logging.Filter): """Filtre qui supprime les codes ANSI des messages.""" def filter(self, record: logging.LogRecord) -> bool: if hasattr(record, 'msg') and isinstance(record.msg, str): record.msg = ANSI_ESCAPE_PATTERN.sub('', record.msg) return True class CleanFormatter(logging.Formatter): """Formatter qui nettoie les messages et assure un format cohérent.""" def format(self, record: logging.LogRecord) -> str: # Nettoyer les codes ANSI du message if hasattr(record, 'msg') and isinstance(record.msg, str): record.msg = ANSI_ESCAPE_PATTERN.sub('', record.msg) # Formater return super().format(record) # ============================================================================= # LogEntry Dataclass # ============================================================================= @dataclass class LogEntry: """Entrée de log structurée.""" timestamp: datetime level: str component: str message: str workflow_id: Optional[str] = None node_id: Optional[str] = None metadata: Dict[str, Any] = field(default_factory=dict) def to_dict(self) -> Dict[str, Any]: """Convertit l'entrée en dictionnaire.""" return { 'timestamp': self.timestamp.isoformat(), 'level': self.level, 'component': self.component, 'message': self.message, 'workflow_id': self.workflow_id, 'node_id': self.node_id, 'metadata': self.metadata } def to_json(self) -> str: """Convertit l'entrée en JSON.""" return json.dumps(self.to_dict()) # ============================================================================= # RPALogger Class # ============================================================================= class RPALogger: """Logger centralisé pour le système RPA.""" # Liste des composants valides (whitelist) VALID_COMPONENTS = { 'api', 'workflow', 'execution', 'detection', 'healing', 'monitoring', 'dashboard', 'automation_scheduler', 'chain_manager', 'trigger_manager', 'vwb_backend', 'vwb_frontend', 'main_backend', 'log_exporter', 'analytics', 'security', 'embedding', 'faiss', 'coaching', 'correction_packs', 'system', 'backup', 'version', 'test', 'test_component' } @staticmethod def _clean_component_name(component: str) -> str: """ Nettoie le nom du composant pour éviter les caractères corrompus. Args: component: Nom du composant brut Returns: Nom nettoyé sans caractères problématiques """ if not component: return "unknown" # Convertir en minuscules et supprimer les espaces component = component.lower().strip() # Ne garder que les caractères alphanumériques et underscores cleaned = "" for char in component: if char.isalnum() or char == '_': cleaned += char elif char in ['-', '.', ' ']: cleaned += '_' # Limiter la longueur cleaned = cleaned[:30].strip('_') # Si le résultat est vide ou trop court, utiliser "unknown" if not cleaned or len(cleaned) < 2: return "unknown" return cleaned def __init__(self, component: str, log_file: Optional[str] = None): """ Initialise le logger. Args: component: Nom du composant log_file: Chemin du fichier de log (optionnel) """ # Nettoyer le nom du composant self.component = self._clean_component_name(component) # Chemin du fichier de log if log_file: self.log_file = Path(log_file) else: self.log_file = LOG_DIR / f"{self.component}.log" # Créer le répertoire parent si nécessaire self.log_file.parent.mkdir(parents=True, exist_ok=True) # Stocker les entrées en mémoire (limité) self.entries: List[LogEntry] = [] self._max_entries = 1000 # Configuration du logger Python standard self.logger = logging.getLogger(f"rpa.{self.component}") self.logger.setLevel(logging.DEBUG) # Éviter les handlers dupliqués if not self.logger.handlers: self._setup_handlers() def _setup_handlers(self): """Configure les handlers de logging.""" # Formatter commun formatter = CleanFormatter(LOG_FORMAT, LOG_DATE_FORMAT) # Handler fichier avec rotation try: file_handler = logging.handlers.RotatingFileHandler( self.log_file, maxBytes=MAX_LOG_SIZE, backupCount=BACKUP_COUNT, encoding='utf-8' ) file_handler.setFormatter(formatter) file_handler.setLevel(logging.DEBUG) file_handler.addFilter(ANSIStripFilter()) self.logger.addHandler(file_handler) except Exception as e: print(f"Warning: Could not create file handler for {self.component}: {e}") # Handler console (seulement si pas en mode silencieux) if os.getenv('RPA_LOG_CONSOLE', 'false').lower() == 'true': console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(formatter) console_handler.setLevel(logging.INFO) console_handler.addFilter(ANSIStripFilter()) self.logger.addHandler(console_handler) def _log(self, level: str, message: str, workflow_id: Optional[str] = None, node_id: Optional[str] = None, **metadata): """ Log interne avec métriques. Args: level: Niveau de log (INFO, WARNING, ERROR, DEBUG) message: Message de log workflow_id: ID du workflow (optionnel) node_id: ID du nœud (optionnel) **metadata: Métadonnées additionnelles """ # Nettoyer le message des codes ANSI clean_message = ANSI_ESCAPE_PATTERN.sub('', str(message)) # Créer l'entrée entry = LogEntry( timestamp=datetime.now(), level=level, component=self.component, message=clean_message, workflow_id=workflow_id, node_id=node_id, metadata=metadata ) # Stocker en mémoire (avec limite) self.entries.append(entry) if len(self.entries) > self._max_entries: self.entries = self.entries[-self._max_entries:] # Incrémenter le compteur Prometheus if METRICS_ENABLED: try: increment_log_entry(level, self.component) except Exception: pass # Construire le message formaté log_msg = clean_message if workflow_id: log_msg += f" | workflow={workflow_id}" if node_id: log_msg += f" | node={node_id}" if metadata: # Formater les métadonnées de manière compacte meta_str = " | ".join(f"{k}={v}" for k, v in metadata.items()) log_msg += f" | {meta_str}" # Log via le logger Python log_method = getattr(self.logger, level.lower(), self.logger.info) log_method(log_msg) def info(self, message: str, workflow_id: Optional[str] = None, node_id: Optional[str] = None, **metadata): """Log niveau INFO.""" self._log('INFO', message, workflow_id, node_id, **metadata) def warning(self, message: str, workflow_id: Optional[str] = None, node_id: Optional[str] = None, **metadata): """Log niveau WARNING.""" self._log('WARNING', message, workflow_id, node_id, **metadata) def error(self, message: str, workflow_id: Optional[str] = None, node_id: Optional[str] = None, **metadata): """Log niveau ERROR.""" self._log('ERROR', message, workflow_id, node_id, **metadata) def debug(self, message: str, workflow_id: Optional[str] = None, node_id: Optional[str] = None, **metadata): """Log niveau DEBUG.""" self._log('DEBUG', message, workflow_id, node_id, **metadata) def critical(self, message: str, workflow_id: Optional[str] = None, node_id: Optional[str] = None, **metadata): """Log niveau CRITICAL.""" self._log('CRITICAL', message, workflow_id, node_id, **metadata) def workflow_start(self, workflow_id: str, **metadata): """Log démarrage de workflow.""" self.info("Workflow started", workflow_id=workflow_id, **metadata) def workflow_end(self, workflow_id: str, success: bool, duration: float, **metadata): """Log fin de workflow.""" status = 'success' if success else 'failure' self.info( f"Workflow completed: {status}", workflow_id=workflow_id, success=success, duration_s=round(duration, 3), **metadata ) def action_executed(self, workflow_id: str, node_id: str, action_type: str, success: bool, confidence: float = 0.0, **metadata): """Log exécution d'une action.""" level = 'INFO' if success else 'WARNING' self._log( level, f"Action executed: {action_type}", workflow_id=workflow_id, node_id=node_id, success=success, confidence=round(confidence, 3), **metadata ) def get_recent_logs(self, limit: int = 100) -> List[LogEntry]: """ Récupère les logs récents. Args: limit: Nombre maximum de logs à retourner Returns: Liste des entrées de log récentes """ return self.entries[-limit:] def export_logs(self, start_time: Optional[datetime] = None, end_time: Optional[datetime] = None, format: str = 'json') -> str: """ Exporte les logs. Args: start_time: Timestamp de début (optionnel) end_time: Timestamp de fin (optionnel) format: Format d'export ('json' ou 'text') Returns: Logs exportés """ filtered_logs = self.entries if start_time: filtered_logs = [log for log in filtered_logs if log.timestamp >= start_time] if end_time: filtered_logs = [log for log in filtered_logs if log.timestamp <= end_time] if format == 'json': return json.dumps([log.to_dict() for log in filtered_logs], indent=2) else: lines = [] for log in filtered_logs: line = f"{log.timestamp.strftime(LOG_DATE_FORMAT)} | {log.level:7} | {log.component:20} | {log.message}" lines.append(line) return "\n".join(lines) # ============================================================================= # Global Logger Management # ============================================================================= # Instance globale pour chaque composant _loggers: Dict[str, RPALogger] = {} def get_logger(component: str) -> RPALogger: """ Récupère ou crée un logger pour un composant. Args: component: Nom du composant Returns: Logger pour le composant """ # Nettoyer le nom avant de chercher clean_name = RPALogger._clean_component_name(component) if clean_name not in _loggers: _loggers[clean_name] = RPALogger(component) return _loggers[clean_name] def configure_root_logger(): """ Configure le logger racine pour capturer tous les logs Python. Appelé une seule fois au démarrage. """ root_logger = logging.getLogger() root_logger.setLevel(logging.INFO) # Formatter formatter = CleanFormatter(LOG_FORMAT, LOG_DATE_FORMAT) # Handler fichier principal main_log = LOG_DIR / "main.log" try: handler = logging.handlers.RotatingFileHandler( main_log, maxBytes=MAX_LOG_SIZE, backupCount=BACKUP_COUNT, encoding='utf-8' ) handler.setFormatter(formatter) handler.addFilter(ANSIStripFilter()) root_logger.addHandler(handler) except Exception: pass # Réduire le bruit des bibliothèques externes logging.getLogger('urllib3').setLevel(logging.WARNING) logging.getLogger('werkzeug').setLevel(logging.WARNING) logging.getLogger('socketio').setLevel(logging.WARNING) logging.getLogger('engineio').setLevel(logging.WARNING) # ============================================================================= # Pre-configured Loggers # ============================================================================= # Loggers pré-configurés pour les composants principaux workflow_logger = get_logger('workflow') execution_logger = get_logger('execution') detection_logger = get_logger('detection') api_logger = get_logger('api') monitoring_logger = get_logger('monitoring') security_logger = get_logger('security')