From 455b0970b109ce1393cf0a692ceb2443cf56484c Mon Sep 17 00:00:00 2001 From: Movses Aslanyan Date: Tue, 13 Oct 2026 20:31:44 +0330 Subject: [PATCH] Stamp trace_id and span_id on every log line inside a span A processor in the shared chain reads the active span context and adds its IDs as lowercase hex, so both structlog and stdlib records carry them. The lowercase `trace_id` name is what the provisioned Loki derived field's case-sensitive regex matches; outside a span nothing is added. Co-Authored-By: Claude Opus 5.5 --- common/logging_config.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/common/logging_config.py b/common/logging_config.py index 95b3f1d..415144e 100644 --- a/common/logging_config.py +++ b/common/logging_config.py @@ -2,6 +2,7 @@ import logging import sys import structlog +from opentelemetry import trace # Loggers that install their own handlers (uvicorn's dictConfig runs before the # app is imported). Their handlers are stripped so records propagate to the @@ -19,6 +20,22 @@ def _add_service(service_name: str): return processor +def _add_trace_context(logger, method_name, event_dict): + """Stamp the active span's IDs on the record, as lowercase hex. + + The name `trace_id` is load-bearing: Grafana's Loki derived field matches + the case-sensitive regex trace[_-]?id["=: ]+([a-fA-F0-9]+), so OTel's own + `otelTraceID` would never become a link. Outside a span (startup, the + consumers' idle loop) the context is invalid and nothing is added, rather + than an all-zeros ID that points at no trace. + """ + ctx = trace.get_current_span().get_span_context() + if ctx.is_valid: + event_dict["trace_id"] = trace.format_trace_id(ctx.trace_id) + event_dict["span_id"] = trace.format_span_id(ctx.span_id) + return event_dict + + def configure_logging(service_name: str) -> structlog.stdlib.BoundLogger: """One JSON object per line on stdout, for structlog and stdlib loggers alike. @@ -32,6 +49,7 @@ def configure_logging(service_name: str) -> structlog.stdlib.BoundLogger: structlog.stdlib.add_logger_name, structlog.processors.TimeStamper(fmt="iso", utc=True), _add_service(service_name), + _add_trace_context, ] structlog.configure(