OpenTelemetry SDK setup, OTLP exporter wiring, structured-JSON logging with trace correlation, and span helpers built around the OTel GenAI semantic conventions. Wired once from src.api.main.lifespan so the rest of the codebase calls into a configured tracer / logger without orchestrating startup.
tracing.setup_tracing()— creates the globalTracerProvider, attaches aBatchSpanProcessorwith the OTLP gRPC exporter (orConsoleSpanExporterwhenOTEL_EXPORTER=console). ReadsOTEL_SERVICE_NAME(defaultharness-python-react) andOTEL_EXPORTER_OTLP_ENDPOINT(defaulthttp://localhost:4317).tracing.instrument_fastapi(app)+tracing.instrument_httpx()— auto-instrumentation hooks. Each FastAPI request and outbound httpx call gets a span with the standard semantic-convention attributes.tracing.get_tracer(name)— retrieve a tracer by module name when manual instrumentation is needed.logging.setup_logging(level=None)— configures stdlibloggingto emit single-line JSON via_JSONFormatter. ReadsLOG_LEVELenv when no argument is passed. Loops inLoggingInstrumentorso theotelTraceID/otelSpanIDfields land on every record.logging._JSONFormatter— the formatter; tested directly intests/test_observability.pyfor the trace-correlation contract.spans.agent_span(name, attributes=None)— context manager that opens a span, sets initial attributes from aMapping, yields the live span for further mutation. Use this rather than callingtracer.start_as_current_spandirectly so the attribute-shape stays consistent.spans.set_span_attributes(span, **kwargs)— set multiple attributes;Nonevalues are silently skipped.spans.GENAI_*/DB_*constants — exported attribute-key constants for the OTel GenAI + database semantic conventions. Use these instead of raw strings so a typo is aNameError, not a silently-different attribute.
- Semconv keys only. The constants at the top of
spans.pyare the single source of attribute names. Adding a customagent.fooattribute is a hard no — extend the semconv list (or wait for upstream OTel to bless one). - Provider-agnostic exporter.
OTEL_EXPORTER_OTLP_ENDPOINTis the standard variable;docker-compose.ymlsets it tohttp://jaeger:4317in the compose network. - Lifespan-only setup. Don't call
setup_tracing()from anywhere exceptsrc.api.main.lifespan— duplicate calls trigger OTel's "Overriding of current TracerProvider" warning, and tests rely on the test-fixture variant that attaches an in-memory exporter to whatever provider exists.