import json import logging import os import re import traceback import uuid from copy import deepcopy from datetime import datetime from logging.handlers import RotatingFileHandler from paths import BASE_DIR, LOGS_DIR as LOG_DIR, ERROR_LOG_PATH MAX_STRING_LENGTH = 4000 MAX_COLLECTION_ITEMS = 50 SENSITIVE_KEYS = { "authorization", "api_token", "token", "access_token", "refresh_token", "cookie", "set-cookie", "password", "secret", } BEARER_RE = re.compile(r"Bearer\s+[A-Za-z0-9._\-]+", re.IGNORECASE) def _ensure_logger(): os.makedirs(LOG_DIR, exist_ok=True) logger = logging.getLogger("mp_manager.errors") if logger.handlers: return logger logger.setLevel(logging.ERROR) logger.propagate = False handler = RotatingFileHandler( ERROR_LOG_PATH, maxBytes=5 * 1024 * 1024, backupCount=5, encoding="utf-8", ) handler.setFormatter(logging.Formatter("%(message)s")) logger.addHandler(handler) return logger def new_error_id(): return str(uuid.uuid4()) def sanitize(value, depth=0): if depth > 6: return "" if isinstance(value, dict): clean = {} for key, item in list(value.items())[:MAX_COLLECTION_ITEMS]: key_text = str(key) if key_text.lower() in SENSITIVE_KEYS or "token" in key_text.lower(): clean[key_text] = "" else: clean[key_text] = sanitize(item, depth + 1) return clean if isinstance(value, (list, tuple, set)): return [sanitize(item, depth + 1) for item in list(value)[:MAX_COLLECTION_ITEMS]] if isinstance(value, bytes): value = value.decode("utf-8", errors="replace") if isinstance(value, str): text = BEARER_RE.sub("Bearer ", value) if len(text) > MAX_STRING_LENGTH: return text[:MAX_STRING_LENGTH] + "..." return text return value def format_exception(exc): if exc is None: return None return { "type": type(exc).__name__, "message": sanitize(str(exc)), "traceback": sanitize("".join(traceback.format_exception(type(exc), exc, exc.__traceback__))), } def log_error(event, exc=None, context=None, *, error_id=None): """ Registra errores técnicos en JSONL y, si la DB está disponible, también en SQLite. Devuelve error_id para poder correlacionarlo con la terminal o respuesta HTTP. """ error_id = error_id or new_error_id() context = sanitize(deepcopy(context or {})) exception_data = format_exception(exc) record = { "error_id": error_id, "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "event": event, "context": context, "exception": exception_data, } logger = _ensure_logger() logger.error(json.dumps(record, ensure_ascii=False, default=str)) try: import db db.insert_error_log( error_id=error_id, event=event, exception_type=exception_data.get("type") if exception_data else None, exception_message=exception_data.get("message") if exception_data else None, context=record, ) except Exception: # El archivo JSONL es la fuente primaria si SQLite no está disponible. pass return error_id