configure_logging now builds one processor chain shared by structlog calls and foreign stdlib records (uvicorn, the consumers' getLogger), rendered as one JSON object per line on stdout. Tracebacks stay inside their line instead of splitting into one Docker log entry per frame. uvicorn.access is deliberately left untouched: --no-access-log works by disabling its propagation, and re-enabling it would bring access logs back (and inflate the healthz/lookup line counts the tests rely on). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
65 lines
2.2 KiB
Python
65 lines
2.2 KiB
Python
import logging
|
|
import sys
|
|
|
|
import structlog
|
|
|
|
# Loggers that install their own handlers (uvicorn's dictConfig runs before the
|
|
# app is imported). Their handlers are stripped so records propagate to the
|
|
# root handler below and come out as JSON like everything else.
|
|
# uvicorn.access is left alone on purpose: --no-access-log works by setting its
|
|
# propagate=False, and re-enabling propagation would turn access logs back on.
|
|
_FOREIGN_LOGGERS = ("uvicorn", "uvicorn.error")
|
|
|
|
|
|
def _add_service(service_name: str):
|
|
def processor(logger, method_name, event_dict):
|
|
event_dict["service"] = service_name
|
|
return event_dict
|
|
|
|
return processor
|
|
|
|
|
|
def configure_logging(service_name: str) -> structlog.stdlib.BoundLogger:
|
|
"""One JSON object per line on stdout, for structlog and stdlib loggers alike.
|
|
|
|
`shared` runs for every record, whichever API emitted it: structlog calls
|
|
pass through it via structlog.configure(), stdlib records (uvicorn, the
|
|
consumers' logging.getLogger(...)) via the formatter's foreign_pre_chain.
|
|
"""
|
|
shared = [
|
|
structlog.contextvars.merge_contextvars,
|
|
structlog.stdlib.add_log_level,
|
|
structlog.stdlib.add_logger_name,
|
|
structlog.processors.TimeStamper(fmt="iso", utc=True),
|
|
_add_service(service_name),
|
|
]
|
|
|
|
structlog.configure(
|
|
processors=[*shared, structlog.stdlib.ProcessorFormatter.wrap_for_formatter],
|
|
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
wrapper_class=structlog.stdlib.BoundLogger,
|
|
cache_logger_on_first_use=True,
|
|
)
|
|
|
|
formatter = structlog.stdlib.ProcessorFormatter(
|
|
foreign_pre_chain=shared,
|
|
processors=[
|
|
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
structlog.processors.format_exc_info,
|
|
structlog.processors.JSONRenderer(),
|
|
],
|
|
)
|
|
handler = logging.StreamHandler(sys.stdout)
|
|
handler.setFormatter(formatter)
|
|
|
|
root = logging.getLogger()
|
|
root.handlers = [handler]
|
|
root.setLevel(logging.INFO)
|
|
|
|
for name in _FOREIGN_LOGGERS:
|
|
foreign = logging.getLogger(name)
|
|
foreign.handlers = []
|
|
foreign.propagate = True
|
|
|
|
return structlog.get_logger(service_name)
|