53 lines
1.8 KiB
Python
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))
|