121 lines
3.5 KiB
Python
121 lines
3.5 KiB
Python
"""Centralized runtime logging bootstrap."""
|
|
|
|
import json
|
|
import logging
|
|
import sys
|
|
from collections.abc import Iterable
|
|
from typing import Any, TextIO
|
|
|
|
import structlog
|
|
from opentelemetry import trace
|
|
|
|
|
|
_UVICORN_LOGGER_NAMES = ("uvicorn", "uvicorn.error", "uvicorn.access")
|
|
|
|
|
|
def configure_logging(
|
|
*,
|
|
log_level: int = logging.INFO,
|
|
stream: TextIO | None = None,
|
|
) -> None:
|
|
resolved_stream = sys.stdout if stream is None else stream
|
|
formatter = structlog.stdlib.ProcessorFormatter(
|
|
foreign_pre_chain=[_add_open_telemetry_context, *_shared_processors()],
|
|
processors=[
|
|
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
structlog.processors.JSONRenderer(sort_keys=True, ensure_ascii=False),
|
|
],
|
|
)
|
|
handler = logging.StreamHandler(resolved_stream)
|
|
handler.setLevel(log_level)
|
|
handler.setFormatter(formatter)
|
|
|
|
structlog.reset_defaults()
|
|
structlog.configure(
|
|
processors=[
|
|
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
_add_open_telemetry_context,
|
|
_render_context_in_event,
|
|
*_shared_processors(),
|
|
structlog.processors.StackInfoRenderer(),
|
|
structlog.processors.format_exc_info,
|
|
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
],
|
|
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
wrapper_class=structlog.stdlib.BoundLogger,
|
|
cache_logger_on_first_use=True,
|
|
)
|
|
|
|
_configure_logger(logging.getLogger(), handler=handler, log_level=log_level)
|
|
for logger_name in _UVICORN_LOGGER_NAMES:
|
|
_configure_logger(
|
|
logging.getLogger(logger_name),
|
|
handler=handler,
|
|
log_level=log_level,
|
|
propagate=False,
|
|
)
|
|
|
|
|
|
def _shared_processors() -> tuple[structlog.types.Processor, ...]:
|
|
return (
|
|
structlog.stdlib.add_logger_name,
|
|
structlog.stdlib.add_log_level,
|
|
structlog.processors.TimeStamper(fmt="iso", utc=True),
|
|
)
|
|
|
|
|
|
def _add_open_telemetry_context(
|
|
_logger: Any,
|
|
_method_name: str,
|
|
event_dict: structlog.types.EventDict,
|
|
) -> structlog.types.EventDict:
|
|
span_context = trace.get_current_span().get_span_context()
|
|
if span_context.is_valid:
|
|
event_dict["trace_id"] = format(span_context.trace_id, "032x")
|
|
event_dict["span_id"] = format(span_context.span_id, "016x")
|
|
event_dict["trace_flags"] = int(span_context.trace_flags)
|
|
return event_dict
|
|
|
|
|
|
def _render_context_in_event(
|
|
_logger: Any,
|
|
_method_name: str,
|
|
event_dict: structlog.types.EventDict,
|
|
) -> structlog.types.EventDict:
|
|
event = str(event_dict.get("event", ""))
|
|
context = " ".join(
|
|
f"{key}={_serialize_log_value(value)}"
|
|
for key, value in sorted(event_dict.items())
|
|
if key not in {"event", "exc_info", "stack_info"}
|
|
)
|
|
event_dict["event"] = f"{event} {context}" if context else event
|
|
return event_dict
|
|
|
|
|
|
def _serialize_log_value(value: object) -> str:
|
|
return json.dumps(
|
|
value,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
default=str,
|
|
)
|
|
|
|
|
|
def _configure_logger(
|
|
logger: logging.Logger,
|
|
*,
|
|
handler: logging.Handler,
|
|
log_level: int,
|
|
propagate: bool = True,
|
|
) -> None:
|
|
_clear_handlers(logger.handlers)
|
|
logger.setLevel(log_level)
|
|
logger.handlers = [handler]
|
|
logger.propagate = propagate
|
|
|
|
|
|
def _clear_handlers(handlers: Iterable[logging.Handler]) -> None:
|
|
for existing_handler in list(handlers):
|
|
existing_handler.close()
|