Agregar logging estructurado a utils.py

- JSONFormatter, StructuredLogger, setup_logging(), get_logger()
- Soporte para logs JSON con contexto
- Usar logger en config.py para warnings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
ARCHITECT
2025-12-24 10:31:06 +00:00
parent a99e58e809
commit e77cbb8f58
2 changed files with 178 additions and 1 deletions

View File

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

View File

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