"""Structured logging setup with request and trace correlation.""" import logging from typing import Any import structlog from opentelemetry import trace from opentelemetry.trace import SpanContext def configure_structured_logging(log_level: str) -> None: resolved_level = _resolve_log_level(log_level) logging.basicConfig(level=resolved_level, format="%(message)s") structlog.configure( processors=[ structlog.contextvars.merge_contextvars, structlog.stdlib.add_log_level, add_trace_context, structlog.processors.TimeStamper(fmt="iso", utc=True), structlog.processors.JSONRenderer(), ], logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.make_filtering_bound_logger(resolved_level), cache_logger_on_first_use=True, ) def add_trace_context( _: Any, __: str, event_dict: dict[str, Any], ) -> dict[str, Any]: span = trace.get_current_span() span_context = span.get_span_context() if _is_valid_span_context(span_context): event_dict["trace_id"] = format(span_context.trace_id, "032x") event_dict["span_id"] = format(span_context.span_id, "016x") return event_dict def _resolve_log_level(log_level: str) -> int: candidate = log_level.strip().upper() return logging.getLevelNamesMapping().get(candidate, logging.INFO) def _is_valid_span_context(span_context: SpanContext) -> bool: return bool(span_context.is_valid and span_context.trace_id and span_context.span_id)