feat(observability): wire OTel tracing end-to-end with Tempo (post-MVP Phase 0)#52
Merged
Merged
Conversation
…P Phase 0) Tracing was a silent no-op for three stacked reasons: - the ingestor image never installed the tracing extra, so OTel packages were missing in containers -> add --extra tracing to the uv sync - no OTLP collector existed in any compose file -> add Grafana Tempo (monitoring profile), OTLP gRPC :4317, config in infra/monitoring/tempo.yml - setup_tracing() ran in the lifespan, after Starlette had already built the middleware stack, so FastAPIInstrumentor never entered the request path (only SQLAlchemy connect spans emitted) -> move the call to module level after the last add_middleware, making the OTel middleware outermost Correlation both directions in Grafana: Tempo datasource with tracesToLogsV2 -> Loki (service.name maps to the container label), and a Loki derived field extracting trace_id -> Tempo. Compose defaults OTEL_ENABLED=true; OTEL_SERVICE_NAME matches the container name so the tag mapping resolves. The except clause at libs/platform/tracing.py:120 turned out to be valid Python 3.14 (PEP 758), not the suspected Python-2 syntax bug; it is parenthesized anyway for readability and cross-version portability. New regression test asserts get_trace_id() returns the active span's 32-hex trace id and degrades to None outside a span. Verified live: GET /api/v1/observations trace_id in ingestor JSON logs resolves in Tempo to the HTTP server span; same lines queryable in Loki. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Post-MVP NFR roadmap Phase 0: make the already-coded OTel tracing actually work. Tracing was a silent no-op for three stacked root causes — none of them the one the roadmap suspected:
Dockerfilehad--extra aibut not--extra tracing, sosetup_tracing()degraded to a no-op in every container. → added--extra tracingto theuv sync.monitoringprofile (infra/monitoring/tempo.yml, OTLP gRPC:4317, loopback-exposed for host-run dev).setup_tracing()ran in the lifespan — too late. Starlette builds its middleware stack when the first ASGI scope (the lifespan event) arrives, soFastAPIInstrumentor's middleware never entered the request path: Tempo received only SQLAlchemyconnectspans, zero HTTP spans, and logs had notrace_id. → moved the call to module level after the lastadd_middleware, making the OTel span outermost sorequest_start/request_endlogs run inside it.Premise correction: the
except ImportError, AttributeError, ...clause atlibs/platform/tracing.py:120is valid Python ≥3.14 (PEP 758 re-legalized unparenthesized multi-exception tuples) — it was never syntax-dead on this repo's pinned runtime. Parenthesized anyway for readability and cross-version portability.Changes
grafana/tempo:2.7.2) indocker-compose.ymlmonitoring profile +just up-monitoringtracesToLogsV2→ Loki (service.namemaps to the promtailcontainerlabel;OTEL_SERVICE_NAME=api-obs-ingestorkeeps them identical), and a Loki derived field extractingtrace_id→ TempoOTEL_ENABLED=true(overridable via.env; export failures non-fatal when monitoring is down);.env.example+ config descriptions updated Jaeger → Tempoget_trace_id()returns the active span's 32-hex trace ID, degrades toNoneoutside a span (pytest.importorskippattern, consistent with theaiextra tests)Verification (live, docker compose)
otel_tracing_initializedlogged withendpoint=http://tempo:4317GET /api/v1/observations→request_start/request_endJSON logs carrytrace_idGET /api/v1/observationsHTTP server span (+ SQLAlchemyconnectspan)loki,tempouids pinned)Notes
docs/.plans/post-mvp-nfr-roadmap.mdis gitignored, so the Phase 0 status/premise corrections live on local disk only — the substance is recorded here and in the commit message.🤖 Generated with Claude Code