Files

150 lines
5.0 KiB
Python
Raw Permalink Normal View History

import logging
import contextvars
import os
import sys
from contextlib import contextmanager
from logging.handlers import TimedRotatingFileHandler
from pathlib import Path
LOG_DIR = Path(__file__).parent / "logs"
LOG_FILE = LOG_DIR / "descarga_factura_transporte.log"
_LOG_CONFIGURED = False
_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")
# ============================================================================
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:
yield sub
@contextmanager
def last_task(self, msg, emoji=""):
with _TaskContext(self, msg, emoji, is_last=True) as sub:
yield sub
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)
class _StreamToLogger:
"""Redirige escrituras de un stream (stdout/stderr) al logger, línea por línea."""
def __init__(self, logger: logging.Logger, level: int) -> None:
self._logger = logger
self._level = level
self._buffer = ""
def write(self, message: str) -> None:
self._buffer += message
while "\n" in self._buffer:
line, self._buffer = self._buffer.split("\n", 1)
if line:
self._logger.log(self._level, line)
def flush(self) -> None:
pass
# ============================================================================
def setup_logging(level=None, console=True, capture_print=True):
"""
Configura el logging del proceso: archivo rotado a medianoche (30 días
de historial) y, opcionalmente, consola. Si ``capture_print`` es True
(por defecto), también redirige ``print()`` (stdout/stderr) al log, de
forma que cualquier salida existente en el proyecto quede registrada
en archivo sin tener que migrar cada ``print`` a ``logger.info``.
"""
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)
formatter = TreeFormatter()
file_handler = TimedRotatingFileHandler(
LOG_FILE,
when="midnight",
interval=1,
backupCount=30,
encoding="utf-8",
)
file_handler.setFormatter(formatter)
handlers: list[logging.Handler] = [file_handler]
if console:
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
handlers.append(console_handler)
logging.basicConfig(level=level, handlers=handlers, force=True)
if capture_print:
stdout_logger = logging.getLogger("stdout")
stderr_logger = logging.getLogger("stderr")
sys.stdout = _StreamToLogger(stdout_logger, logging.INFO)
sys.stderr = _StreamToLogger(stderr_logger, logging.ERROR)
_LOG_CONFIGURED = True
# ============================================================================
def get_logger(name="descarga_factura_transporte"):
setup_logging()
return TaskLogger(logging.getLogger(name))