# ============================================ # Projet : API-KA # Fichier : src/utils/logger.py # Node : m3u96b # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Date : 2026-08-16 # ============================================ """Logging structuré JSON avec rotation quotidienne + alertes (logs/alerts.log).""" from __future__ import annotations import datetime import json import logging import socket from logging.handlers import TimedRotatingFileHandler from src.config import get_settings _RESERVED_ATTRS = { "name", "msg", "args", "levelname", "levelno", "pathname", "filename", "module", "exc_info", "exc_text", "stack_info", "lineno", "funcName", "created", "msecs", "relativeCreated", "thread", "threadName", "processName", "process", "taskName", "message", "asctime", } class JsonFormatter(logging.Formatter): """Formate chaque enregistrement de log en une ligne JSON.""" def format(self, record: logging.LogRecord) -> str: entry: dict[str, object] = { "timestamp": datetime.datetime.fromtimestamp( record.created, tz=datetime.UTC ).isoformat(), "level": record.levelname, "logger": record.name, "message": record.getMessage(), "node": socket.gethostname(), } for key, value in record.__dict__.items(): if key not in _RESERVED_ATTRS and not key.startswith("_"): entry[key] = value if record.exc_info: entry["exception"] = self.formatException(record.exc_info) return json.dumps(entry, ensure_ascii=False, default=str) def get_logger(name: str = "apika") -> logging.Logger: """Retourne un logger JSON avec rotation quotidienne (logs/apika.log).""" logger = logging.getLogger(name) if logger.handlers: return logger settings = get_settings() logger.setLevel(logging.INFO) logger.propagate = False formatter = JsonFormatter() file_handler = TimedRotatingFileHandler( settings.logs_dir / "apika.log", when="midnight", backupCount=90, encoding="utf-8", ) file_handler.setFormatter(formatter) logger.addHandler(file_handler) stream_handler = logging.StreamHandler() stream_handler.setFormatter(formatter) logger.addHandler(stream_handler) return logger def alert(message: str) -> None: """Écrit une alerte horodatée dans logs/alerts.log et la logge en ERROR.""" settings = get_settings() timestamp = datetime.datetime.now(tz=datetime.UTC).isoformat() with open(settings.logs_dir / "alerts.log", "a", encoding="utf-8") as fh: fh.write(f"{timestamp} [ALERT] {message}\n") get_logger().error(message, extra={"alert": True})