diff --git a/orchestrator/config.py b/orchestrator/config.py index 0aa6bfd..6147123 100644 --- a/orchestrator/config.py +++ b/orchestrator/config.py @@ -15,6 +15,13 @@ from pathlib import Path from dataclasses import dataclass, field from typing import Optional, Any +try: + from .utils import get_logger +except ImportError: + from utils import get_logger + +logger = get_logger("orchestrator.config") + def load_env(): """Carga variables desde .env si existe.""" @@ -202,7 +209,7 @@ class Config: with open(self.config_path) as f: return yaml.safe_load(f) or {} except ImportError: - print("AVISO: PyYAML no instalado. pip install pyyaml") + logger.warning("PyYAML no instalado", suggestion="pip install pyyaml") return {} def _parse_settings(self) -> Settings: diff --git a/orchestrator/utils.py b/orchestrator/utils.py index 4be1eb8..47d3109 100644 --- a/orchestrator/utils.py +++ b/orchestrator/utils.py @@ -7,8 +7,178 @@ por múltiples componentes del sistema. """ import asyncio +import json +import logging +import sys import time from collections import deque +from datetime import datetime +from pathlib import Path +from typing import Optional, Any + + +# ============================================================================= +# LOGGING ESTRUCTURADO +# ============================================================================= + +class JSONFormatter(logging.Formatter): + """Formateador que produce logs en formato JSON estructurado.""" + + def __init__(self, service: str = "orchestrator"): + super().__init__() + self.service = service + + def format(self, record: logging.LogRecord) -> str: + log_data = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "level": record.levelname, + "service": self.service, + "logger": record.name, + "message": record.getMessage(), + } + + # Agregar información de ubicación en DEBUG + if record.levelno <= logging.DEBUG: + log_data["location"] = { + "file": record.filename, + "line": record.lineno, + "function": record.funcName, + } + + # Agregar excepción si existe + if record.exc_info: + log_data["exception"] = { + "type": record.exc_info[0].__name__ if record.exc_info[0] else None, + "message": str(record.exc_info[1]) if record.exc_info[1] else None, + } + + # Agregar campos extra + if hasattr(record, "extra_fields"): + log_data["context"] = record.extra_fields + + return json.dumps(log_data, default=str) + + +class StructuredLogger: + """ + Logger estructurado con soporte para contexto adicional. + + Ejemplo: + logger = get_logger("architect-app") + logger.info("Request recibido", agent="architect", action="chat") + logger.error("Error de conexión", error=str(e), retry=3) + """ + + def __init__(self, logger: logging.Logger): + self._logger = logger + + def _log(self, level: int, message: str, **kwargs): + """Log con campos extra.""" + record = self._logger.makeRecord( + self._logger.name, + level, + "(unknown)", + 0, + message, + (), + None, + ) + if kwargs: + record.extra_fields = kwargs + self._logger.handle(record) + + def debug(self, message: str, **kwargs): + self._log(logging.DEBUG, message, **kwargs) + + def info(self, message: str, **kwargs): + self._log(logging.INFO, message, **kwargs) + + def warning(self, message: str, **kwargs): + self._log(logging.WARNING, message, **kwargs) + + def error(self, message: str, exc_info: bool = False, **kwargs): + if exc_info: + self._logger.error(message, exc_info=True, extra={"extra_fields": kwargs} if kwargs else {}) + else: + self._log(logging.ERROR, message, **kwargs) + + def critical(self, message: str, **kwargs): + self._log(logging.CRITICAL, message, **kwargs) + + +def setup_logging( + service: str = "orchestrator", + level: str = "INFO", + log_file: Optional[Path] = None, + json_format: bool = True +) -> StructuredLogger: + """ + Configura el sistema de logging. + + Args: + service: Nombre del servicio (aparece en los logs) + level: Nivel de logging (DEBUG, INFO, WARNING, ERROR) + log_file: Archivo opcional para escribir logs + json_format: Si True, usa formato JSON; si False, formato legible + + Returns: + StructuredLogger configurado + """ + logger = logging.getLogger(service) + logger.setLevel(getattr(logging, level.upper(), logging.INFO)) + logger.handlers.clear() + + if json_format: + formatter = JSONFormatter(service=service) + else: + formatter = logging.Formatter( + "%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S" + ) + + # Handler para consola (stderr) + console_handler = logging.StreamHandler(sys.stderr) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + # Handler para archivo (opcional) + if log_file: + log_file.parent.mkdir(parents=True, exist_ok=True) + file_handler = logging.FileHandler(log_file) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + return StructuredLogger(logger) + + +# Cache de loggers +_loggers: dict[str, StructuredLogger] = {} + + +def get_logger( + name: str = "orchestrator", + level: str = "INFO", + log_file: Optional[Path] = None +) -> StructuredLogger: + """ + Obtiene un logger estructurado (cached). + + Args: + name: Nombre del logger/servicio + level: Nivel de logging + log_file: Archivo opcional para logs + + Returns: + StructuredLogger + """ + if name not in _loggers: + _loggers[name] = setup_logging( + service=name, + level=level, + log_file=log_file, + json_format=True + ) + return _loggers[name] class RateLimiter: