Files
Dom 758e671732 fix(logging): Corriger système de logging avec format unifié et filtrage ANSI
- Réécrire logger.py avec format cohérent: "timestamp | level | component | message"
- Ajouter ANSIStripFilter pour nettoyer les codes couleur des logs
- Implémenter _clean_component_name() pour éviter les noms de fichiers corrompus
- Configurer rotation des fichiers (10MB, 5 backups)
- Rendre imports Prometheus optionnels dans __init__.py
- Réduire bruit des librairies externes (werkzeug, urllib3, etc.)

Corrige les problèmes de fichiers logs corrompus (!0.log, #0.log, %0.log, etc.)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 15:34:34 +01:00

440 lines
14 KiB
Python

"""
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')