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:
@@ -15,6 +15,13 @@ from pathlib import Path
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Optional, Any
|
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():
|
def load_env():
|
||||||
"""Carga variables desde .env si existe."""
|
"""Carga variables desde .env si existe."""
|
||||||
@@ -202,7 +209,7 @@ class Config:
|
|||||||
with open(self.config_path) as f:
|
with open(self.config_path) as f:
|
||||||
return yaml.safe_load(f) or {}
|
return yaml.safe_load(f) or {}
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("AVISO: PyYAML no instalado. pip install pyyaml")
|
logger.warning("PyYAML no instalado", suggestion="pip install pyyaml")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _parse_settings(self) -> Settings:
|
def _parse_settings(self) -> Settings:
|
||||||
|
|||||||
@@ -7,8 +7,178 @@ por múltiples componentes del sistema.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from collections import deque
|
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:
|
class RateLimiter:
|
||||||
|
|||||||
Reference in New Issue
Block a user