Files
descarga_xml_sunat/logger_manager.py
T

146 lines
5.0 KiB
Python
Raw Normal View History

import logging
import re
import contextvars
import os
import sys
import time
from contextlib import contextmanager
from logging.handlers import TimedRotatingFileHandler
from pathlib import Path
LOG_DIR = Path(__file__).parent / "logs"
LOG_FILE = LOG_DIR / "sunat.log"
_LOG_CONFIGURED = False
_ANSI_RE = re.compile(r"\033\[[0-9;]*m")
_stack: contextvars.ContextVar[list] = contextvars.ContextVar("log_stack", default=[])
# ============================================================================
class TreeFormatter(logging.Formatter):
def format(self, record):
return f"{self.formatTime(record)} ─── {record.getMessage()}"
def formatTime(self, record, datefmt=None):
import datetime
return datetime.datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S")
# ============================================================================
class PlainFileFormatter(TreeFormatter):
"""Igual que TreeFormatter pero sin códigos ANSI de color, para el archivo."""
def format(self, record):
return _ANSI_RE.sub("", super().format(record))
# ============================================================================
def _build_prefix(stack, is_last):
if not stack:
return ""
parts = []
for sib_is_last in stack[:-1]:
parts.append(" " if sib_is_last else "│ ")
parts.append("└── " if is_last else "├── ")
return "".join(parts)
# ============================================================================
class _TaskContext:
def __init__(self, logger, msg, emoji, is_last):
self._logger = logger
self._msg = msg
self._emoji = emoji
self._is_last = is_last
def __enter__(self):
stack = _stack.get()
prefix = _build_prefix(stack, self._is_last)
emoji_str = f"{self._emoji} " if self._emoji else ""
self._logger._logger.info(f"{prefix}{emoji_str}{self._msg}")
_stack.set(stack + [self._is_last])
return TaskLogger(self._logger._logger)
def __exit__(self, exc_type, exc_val, exc_tb):
_stack.set(_stack.get()[:-1])
# ============================================================================
class TaskLogger:
def __init__(self, logger):
self._logger = logger
@contextmanager
def task(self, msg, emoji=""):
with _TaskContext(self, msg, emoji, is_last=False) as sub_logger:
yield sub_logger
@contextmanager
def last_task(self, msg, emoji=""):
with _TaskContext(self, msg, emoji, is_last=True) as sub_logger:
yield sub_logger
def info(self, msg, *args, _last=False, **kwargs):
self._log(logging.INFO, msg, args, kwargs, _last)
def warning(self, msg, *args, _last=False, **kwargs):
self._log(logging.WARNING, msg, args, kwargs, _last)
def error(self, msg, *args, _last=False, **kwargs):
self._log(logging.ERROR, msg, args, kwargs, _last)
def _log(self, level, msg, args, kwargs, is_last):
stack = _stack.get()
prefix = _build_prefix(stack, is_last)
self._logger.log(level, f"{prefix}{msg}", *args, **kwargs)
def _rotated_name(default_name):
"""TimedRotatingFileHandler entrega 'sunat.log.2026-08-30'; lo convertimos
a 'sunat_2026-08-30.log' para que sea más legible como nombre de archivo."""
date_part = default_name.rsplit(".", 1)[-1]
return str(LOG_FILE.with_name(f"{LOG_FILE.stem}_{date_part}{LOG_FILE.suffix}"))
# ============================================================================
def limpiar_logs_antiguos(dias=7):
"""Borra los logs rotados (sunat_YYYY-MM-DD.log) con más de 'dias' de antigüedad."""
limite = time.time() - dias * 86400
count = 0
for p in LOG_DIR.glob(f"{LOG_FILE.stem}_*{LOG_FILE.suffix}"):
try:
if p.stat().st_mtime < limite:
p.unlink()
count += 1
except OSError:
pass
return count
# ============================================================================
def setup_logging(level=None, console=True):
global _LOG_CONFIGURED
if _LOG_CONFIGURED:
return
if level is None:
level_name = os.getenv("LOG_LEVEL", "INFO").upper()
level = getattr(logging, level_name, logging.INFO)
LOG_DIR.mkdir(exist_ok=True)
file_handler = TimedRotatingFileHandler(
LOG_FILE,
when="midnight",
interval=1,
backupCount=0, # la retención la maneja limpiar_logs_antiguos() con el namer de abajo
encoding="utf-8",
)
file_handler.namer = _rotated_name
file_handler.setFormatter(PlainFileFormatter())
handlers = [file_handler]
if console:
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(TreeFormatter())
handlers.append(console_handler)
logging.basicConfig(level=level, handlers=handlers, force=True)
_LOG_CONFIGURED = True
# ============================================================================
def get_logger(name="sunat"):
setup_logging()
return TaskLogger(logging.getLogger(name))