Skip to content

Add Basic OpenTelemetry Tracing - #1536

Open
genematx wants to merge 16 commits into
bluesky:mainfrom
genematx:otlp-jaeger
Open

genematx wants to merge 16 commits into
bluesky:mainfrom
genematx:otlp-jaeger

Conversation

@genematx

@genematx genematx commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

This adds minimal tracing capabilities with OpenTelemetry.

The traces are exported over OTLP, which is enabled by setting OTEL_EXPORTER_OTLP_ENDPOINT. Health checks and metrics scrapes can be excluded via OTEL_PYTHON_FASTAPI_EXCLUDED_URLS.

The example monitoring stack (compose.monitoring.yml) gains an OpenTelemetry Collector and two trace backends, Jaeger and Grafana Tempo. The Collector receives traces and fans them out to both backends so their capabilities can be compared: Jaeger has its own UI, while Tempo is added as a Grafana datasource so traces can be explored in Grafana with TraceQL. The Collector also scrapes and re-exposes Tiled's Prometheus metrics.

A new "Distributed Tracing" user-guide page documents how to enable tracing and try the example stack, including a diagram of the telemetry flow.

Screenshot 2026-09-24 at 5 29 47 PM

Related Issue: #1000

Checklist

  • Add a Changelog entry
  • Add the ticket number which this PR closes to the comment section

Instrument the server with OpenTelemetry tracing, exported over OTLP and
disabled unless OTEL_EXPORTER_OTLP_ENDPOINT is set. Health checks and
metrics scrapes can be excluded via OTEL_PYTHON_FASTAPI_EXCLUDED_URLS.

Add an OpenTelemetry Collector and Jaeger to the example monitoring
stack. The Collector receives traces and forwards them to Jaeger, and
also scrapes and re-exposes Tiled's Prometheus metrics.

Add a 'Distributed Tracing' user-guide page.
Send traces to both Jaeger and Grafana Tempo: the OpenTelemetry Collector
now fans traces out to a Tempo service in addition to Jaeger, and Tempo is
added as a Grafana datasource so traces can be explored in Grafana with
TraceQL. Bump Grafana to a version that supports TraceQL, and add the
required apiVersion to the datasource provisioning files.

Illustrate the telemetry flow in the tracing docs with a diagram.
Wrap record_timing in an OpenTelemetry span so the phases it already times
(access control, read, tokenize, pack) appear as child spans in a request's
trace, giving a per-request breakdown of where time is spent. The span is a
no-op when OpenTelemetry is not installed or no tracer provider is
configured.
@danielballan
danielballan requested a lite review from Copilot September 24, 2026 07:21
@danielballan

Copy link
Copy Markdown
Member

Ah, the Copilot review request was an errant click. Oh well.

@danielballan

Copy link
Copy Markdown
Member

@ZohebShaikh @dylanmcreynolds Feel no obligation, of course, but if you have a moment to give feedback on this (or suggest other reviewers from your institutions) we'd interested in any thoughts.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved issues affect Compose behavior, image compatibility, the advertised Tempo/Grafana setup, and tracing test coverage.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 5 Medium severity

Open (5)
What changed in this PR

Adds OpenTelemetry tracing support, OTLP Collector/Jaeger monitoring configuration, and user documentation.

Changes:

  • Adds conditional FastAPI instrumentation and tracing dependencies.
  • Adds Collector, Jaeger, and metrics configuration.
  • Adds tracing documentation, navigation, and changelog updates.
File Description
tiled/​server/​app.py Configures conditional OpenTelemetry tracing.
pyproject.toml Adds OpenTelemetry dependencies.
monitoring_example/​prometheus/​prometheus.yml Documents Collector metrics handling.
monitoring_example/​otel-collector/​otel-collector.yml Defines trace and metrics pipelines.
docs/​source/​user-guide/​tracing.md Documents tracing setup and usage.
docs/​source/​_toc.yml Adds tracing documentation navigation.
compose.yml Adds tracing environment variables.
compose.monitoring.yml Adds Collector and Jaeger services.
compose.dev.yml Adds tracing environment variables for development.
CHANGELOG.md Records the tracing feature.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread compose.dev.yml Outdated
Comment thread compose.monitoring.yml Outdated
Comment thread compose.yml Outdated
Comment thread docs/source/user-guide/tracing.md Outdated
Comment thread tiled/server/app.py
record_timing opened a phase span unconditionally, so requests that are
excluded from tracing (health checks, metrics scrapes) produced orphaned
single-span traces that cluttered the trace UI. Only open a phase span
when there is an active recording span.
FastAPI >=0.142 ships built-in OpenTelemetry support that, when
OTEL_EXPORTER_OTLP_ENDPOINT is set, registers its own OTLP export
pipeline on the global tracer provider. Combined with the tracing
pipeline Tiled configures, this exported every span twice. Pass
telemetry={"auto_configure": False} to FastAPI() so Tiled remains the
sole exporter.
Use an in-memory span exporter to verify, without a running collector or
backend: a traced request emits the FastAPI server span and child phase
spans (single trace, no duplicate span IDs); excluded endpoints emit no
spans; tracing stays off when OTEL_EXPORTER_OTLP_ENDPOINT is unset; and
FastAPI's built-in telemetry does not register a second export pipeline.
FastAPI >=0.142 ships its own OpenTelemetry integration that emits request
spans on any globally installed tracer provider when the app is not
instrumented by opentelemetry-instrumentation-fastapi. The in-memory
provider the tests install made test_tracing_disabled_by_default capture
those spans and fail on CI. Assert instead that the tracing hook did not
instrument the app (its documented off-by-default behavior). Also use
single backticks in comments/docstrings.
The base compose.yml and compose.dev.yml set OTEL_EXPORTER_OTLP_ENDPOINT
pointing at otel-collector, which is only defined in compose.monitoring.yml.
Running the base files on their own therefore enabled tracing against an
unreachable host, causing continuous export failures. Move the OTEL_*
variables into compose.monitoring.yml, next to the collector they target, so
tracing is off unless that overlay is used.

Also update the tracing user guide to launch the example with compose.dev.yml
(which builds the image from this checkout) instead of compose.yml (whose
pinned published image may not include tracing yet).
@ZohebShaikh

Copy link
Copy Markdown
Contributor

@dan-fernandes From our side Will have a look at this PR. He has worked on this tech before so will be a good person for this

The FastAPI auto_configure opt-out entry was lost while resolving a
CHANGELOG conflict when merging main.
@danielballan

Copy link
Copy Markdown
Member

That would be excellent! @genematx has some experience with this stack from previous jobs, as does @dylanmcreynolds I believe, but it's brand new to NSLS-II. Feedback from @dan-fernadandes would be very appreciated!

@danielballan

Copy link
Copy Markdown
Member

I think it's slick that this slots right into the record_timing utility that we use to populate the Server-Timing header. It's nice.

@danielballan danielballan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great. I have two implementation nit-picks.

Comment thread tiled/server/utils.py Outdated
Comment thread tiled/server/app.py Outdated
`opentelemetry` is a namespace package shared by all `opentelemetry-*`
distributions, so `find_spec("opentelemetry")` succeeds even when the
API package (which provides `opentelemetry.trace`) is not installed, and
importing `tiled.server.utils` then fails. Check for `opentelemetry.trace`
itself.

Import `importlib.metadata` explicitly: `import importlib` does not load
the submodule, and `app.py` only worked because another import loaded it.
@genematx genematx mentioned this pull request Oct 8, 2026
1 of 2 tasks

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants