import json import logging import os import sys from datetime import datetime, timezone STANDARD_LOG_RECORD_FIELDS = { "args", "asctime", "created", "exc_info", "exc_text", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "msg", "name", "pathname", "process", "processName", "relativeCreated", "stack_info", "thread", "threadName", "taskName", } class JsonFormatter(logging.Formatter): def __init__(self, service_name: str): super().__init__() self.service_name = service_name def format(self, record: logging.LogRecord) -> str: log_payload = { "timestamp": datetime.fromtimestamp( record.created, tz=timezone.utc ).isoformat(), "level": record.levelname, "service": self.service_name, "logger": record.name, "message": record.getMessage(), "module": record.module, "function": record.funcName, "line": record.lineno, } extra_fields = self._collect_extra_fields(record) if extra_fields: log_payload["extra"] = extra_fields if record.exc_info: log_payload["exception"] = self.formatException(record.exc_info) return json.dumps(log_payload, ensure_ascii=False) def _collect_extra_fields(self, record: logging.LogRecord) -> dict: extra_fields = {} for key, value in record.__dict__.items(): if key in STANDARD_LOG_RECORD_FIELDS or key.startswith("_"): continue if isinstance(value, (str, int, float, bool)) or value is None: extra_fields[key] = value else: extra_fields[key] = repr(value) return extra_fields def setup_logging(service_name: str) -> None: log_level_name = os.getenv("LOG_LEVEL", "INFO").upper() log_level = getattr(logging, log_level_name, logging.INFO) root_logger = logging.getLogger() root_logger.handlers.clear() root_logger.setLevel(log_level) handler = logging.StreamHandler(sys.stdout) handler.setFormatter(JsonFormatter(service_name=service_name)) root_logger.addHandler(handler) for logger_name in ( "uvicorn", "uvicorn.error", "uvicorn.access", "aiogram", "aiohttp", "asyncio", ): logger = logging.getLogger(logger_name) logger.handlers.clear() logger.propagate = True logging.captureWarnings(True)