Files
mcma-backend/app/core/logging.py
T
2026-06-01 18:47:59 +03:00

53 lines
1.8 KiB
Python

"""Structured logging via structlog.
Emits key=value (dev) or JSON (prod) with a per-request/task ``correlation_id``
bound through a contextvar. Call :func:`configure_logging` once at startup.
"""
import logging
from contextvars import ContextVar
from typing import cast
import structlog
from structlog.typing import EventDict, FilteringBoundLogger, Processor, WrappedLogger
# Bound by middleware (HTTP) and worker entrypoints (tasks) so every log line
# downstream carries the same id without explicit passing.
correlation_id: ContextVar[str | None] = ContextVar("correlation_id", default=None)
def _add_correlation_id(
_logger: WrappedLogger, _method: str, event_dict: EventDict
) -> EventDict:
cid = correlation_id.get()
if cid is not None:
event_dict["correlation_id"] = cid
return event_dict
def configure_logging(*, level: str = "INFO", json: bool = False) -> None:
shared: list[Processor] = [
structlog.contextvars.merge_contextvars,
_add_correlation_id,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso", utc=True),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
]
renderer = (
structlog.processors.JSONRenderer() if json else structlog.dev.ConsoleRenderer(colors=True)
)
structlog.configure(
processors=[*shared, renderer],
wrapper_class=structlog.make_filtering_bound_logger(logging.getLevelName(level.upper())),
logger_factory=structlog.PrintLoggerFactory(),
cache_logger_on_first_use=True,
)
# Route stdlib logging (uvicorn, sqlalchemy) through structlog formatting.
logging.basicConfig(level=level.upper(), format="%(message)s")
def get_logger(name: str | None = None) -> FilteringBoundLogger:
return cast(FilteringBoundLogger, structlog.get_logger(name))