Emit JSON logs through structlog, including uvicorn and stdlib loggers
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7ddb614ded
commit
7be54a6ff9
@@ -1,12 +1,64 @@
|
||||
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 configure_logging(service_name: str) -> logging.Logger:
|
||||
"""Plain-text, human-prose logging -- the deliberately weak Phase 0
|
||||
baseline. Per phase-0-base-project-spec.md §9: no JSON, no key-value, no
|
||||
structured fields. Do not "improve" this; later phases replace it."""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||
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,
|
||||
)
|
||||
return logging.getLogger(service_name)
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user