chore: Remove obsolete build and GUI files, and introduce a new Docker Compose setup for microservices architecture. This change streamlines the project structure by eliminating legacy scripts and integrating a PostgreSQL database service alongside extraction and cleaning services.
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
"""Configuración de logging en formato JSON y loggers dedicados.
|
||||
|
||||
Crea handlers rotativos para:
|
||||
- logs de aplicación (`logs/ips_extractor.log`),
|
||||
- logs de normalización LLM (`logs/llm.log`),
|
||||
- logs de llamadas a APIs (`logs/api_calls.log`),
|
||||
además de un stream a consola para WARNING+.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from typing import Callable, Any
|
||||
import coloredlogs
|
||||
|
||||
|
||||
class JsonFormatter(logging.Formatter):
|
||||
"""Formatter que serializa registros a JSON con extras."""
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
base = {
|
||||
"ts": datetime.utcfromtimestamp(record.created).isoformat() + "Z",
|
||||
"level": record.levelname,
|
||||
"logger": record.name,
|
||||
"msg": record.getMessage(),
|
||||
}
|
||||
# incluir extras no estándar
|
||||
extras = {}
|
||||
for k, v in record.__dict__.items():
|
||||
if k in (
|
||||
"name", "msg", "args", "levelname", "levelno", "pathname", "filename",
|
||||
"module", "exc_info", "exc_text", "stack_info", "lineno", "funcName",
|
||||
"created", "msecs", "relativeCreated", "thread", "threadName", "processName",
|
||||
"process", "message", "asctime",
|
||||
):
|
||||
continue
|
||||
# evitar objetos no serializables
|
||||
try:
|
||||
json.dumps(v)
|
||||
extras[k] = v
|
||||
except Exception:
|
||||
extras[k] = str(v)
|
||||
if extras:
|
||||
base.update(extras)
|
||||
return json.dumps(base, ensure_ascii=False)
|
||||
|
||||
|
||||
_configured = False
|
||||
|
||||
|
||||
def log_execution_time(logger: logging.Logger | None = None, level: int = logging.DEBUG) -> Callable:
|
||||
"""Decorador para registrar el tiempo de ejecución de una función."""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
# Si no se pasa logger, intentar obtenerlo del primer argumento (self) si es una clase
|
||||
# o usar el logger del módulo de la función.
|
||||
_logger = logger
|
||||
if _logger is None:
|
||||
if args and hasattr(args[0], '__class__'):
|
||||
# Intenta buscar un logger en la instancia
|
||||
_logger = getattr(args[0], 'logger', None)
|
||||
|
||||
if _logger is None:
|
||||
_logger = logging.getLogger(func.__module__)
|
||||
|
||||
start_time = time.time()
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
return result
|
||||
except Exception as e:
|
||||
duration_ms = (time.time() - start_time) * 1000
|
||||
if _logger:
|
||||
_logger.exception(
|
||||
f"Error en ejecución de {func.__name__}",
|
||||
extra={"duration_ms": duration_ms, "function": func.__name__, "error": str(e)}
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
duration_ms = (time.time() - start_time) * 1000
|
||||
if _logger:
|
||||
_logger.log(
|
||||
level,
|
||||
f"Ejecución de {func.__name__} completada",
|
||||
extra={"duration_ms": duration_ms, "function": func.__name__}
|
||||
)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def configure_logging(level: int = logging.INFO) -> None:
|
||||
"""Configura logging global de forma idempotente.
|
||||
|
||||
- Crea archivos rotativos en `logs/` con formato JSON.
|
||||
- Configura salida a consola con colores y formato legible.
|
||||
- Define loggers dedicados para normalización LLM y llamadas a APIs.
|
||||
"""
|
||||
global _configured
|
||||
if _configured:
|
||||
return
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
|
||||
root = logging.getLogger()
|
||||
# idempotencia: limpiar handlers existentes solo si fueron nuestros json
|
||||
# si ya hay handlers, no duplicar
|
||||
if not root.handlers:
|
||||
root.setLevel(level)
|
||||
|
||||
# Handler de archivo principal (JSON)
|
||||
app_handler = logging.handlers.RotatingFileHandler(
|
||||
filename=os.path.join("logs", "ips_extractor.log"),
|
||||
maxBytes=10 * 1024 * 1024,
|
||||
backupCount=5,
|
||||
encoding="utf-8",
|
||||
)
|
||||
app_handler.setFormatter(JsonFormatter())
|
||||
app_handler.setLevel(logging.INFO)
|
||||
root.addHandler(app_handler)
|
||||
|
||||
# Handler de consola (Colores y legible para humanos)
|
||||
console = logging.StreamHandler()
|
||||
console.setLevel(level) # Usar el nivel configurado (INFO por defecto)
|
||||
|
||||
# Formato de consola: Tiempo | Nivel | Logger | Mensaje
|
||||
console_fmt = "%(asctime)s %(levelname)-8s %(name)s %(message)s"
|
||||
console_formatter = coloredlogs.ColoredFormatter(
|
||||
fmt=console_fmt,
|
||||
datefmt='%H:%M:%S',
|
||||
level_styles={
|
||||
'debug': {'color': 'black', 'bright': True},
|
||||
'info': {'color': 'green'},
|
||||
'warning': {'color': 'yellow'},
|
||||
'error': {'color': 'red'},
|
||||
'critical': {'color': 'red', 'bold': True},
|
||||
},
|
||||
field_styles={
|
||||
'asctime': {'color': 'cyan'},
|
||||
'levelname': {'bold': True},
|
||||
'name': {'color': 'blue'},
|
||||
}
|
||||
)
|
||||
console.setFormatter(console_formatter)
|
||||
root.addHandler(console)
|
||||
|
||||
# Logger dedicado para normalizador (LLM)
|
||||
llm_logger = logging.getLogger("domain.normalizer")
|
||||
# evitar duplicados si ya está configurado
|
||||
has_file = any(isinstance(h, logging.handlers.RotatingFileHandler) for h in llm_logger.handlers)
|
||||
if not has_file:
|
||||
llm_handler = logging.handlers.RotatingFileHandler(
|
||||
filename=os.path.join("logs", "llm.log"),
|
||||
maxBytes=10 * 1024 * 1024,
|
||||
backupCount=5,
|
||||
encoding="utf-8",
|
||||
)
|
||||
llm_handler.setFormatter(JsonFormatter())
|
||||
llm_handler.setLevel(logging.INFO)
|
||||
llm_logger.addHandler(llm_handler)
|
||||
llm_logger.setLevel(logging.INFO)
|
||||
llm_logger.propagate = False # No propagar al root para no duplicar en consola/main log si no se desea
|
||||
|
||||
# Logger dedicado para llamadas a APIs (peticiones y tokens)
|
||||
api_calls_logger = logging.getLogger("domain.api_calls")
|
||||
has_api_file = any(isinstance(h, logging.handlers.RotatingFileHandler) for h in api_calls_logger.handlers)
|
||||
if not has_api_file:
|
||||
api_handler = logging.handlers.RotatingFileHandler(
|
||||
filename=os.path.join("logs", "api_calls.log"),
|
||||
maxBytes=10 * 1024 * 1024,
|
||||
backupCount=5,
|
||||
encoding="utf-8",
|
||||
)
|
||||
api_handler.setFormatter(JsonFormatter())
|
||||
api_handler.setLevel(logging.INFO)
|
||||
api_calls_logger.addHandler(api_handler)
|
||||
api_calls_logger.setLevel(logging.INFO)
|
||||
api_calls_logger.propagate = False
|
||||
|
||||
_configured = True
|
||||
Reference in New Issue
Block a user