Skip to content

fix: build a TracerProvider on FastStream - #228

Merged
lesnik512 merged 1 commit into
mainfrom
fix/faststream-tracer-provider
Sep 20, 2026
Merged

lesnik512 merged 1 commit into
mainfrom
fix/faststream-tracer-provider

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Closes #223.

FastStreamOpenTelemetryInstrument.bootstrap() overrode the base without calling super(), and it is
the only OpenTelemetry instrument in FastStreamBootstrapper.instruments_types. So on FastStream
lite-bootstrap never constructed a TracerProvider at all: the middleware was wired to whatever
get_tracer_provider() returned, which in a process where nothing else installed one is a
ProxyTracerProvider handing out NonRecordingSpans. Every OpenTelemetry field on FastStreamConfig
was accepted and did nothing.

One line fixes it:

def bootstrap(self) -> None:
    super().bootstrap()
    config = self.bootstrap_config
    if config.opentelemetry_middleware_cls and config.application.broker:
        ...

Why option 1 of the two the issue offered

The issue left open whether the old behaviour was deliberate, a FastStream service consuming a provider
that something else owns. It cannot have been. is_configured is
bool(opentelemetry_endpoint or opentelemetry_log_traces) and bool(opentelemetry_middleware_cls), so
the only way to turn FastStream tracing on at all was to set an endpoint the instrument then ignored.
The gate demanded an exporter config it proceeded to throw away. Documenting that (option 2) would
first have required changing is_configured to stop asking for the endpoint, a larger behaviour change
than just honouring it.

Why the middleware still reads get_tracer_provider()

Deliberately not self._tracer_provider, matching FastAPIOpenTelemetryInstrument and
LitestarOpenTelemetryInstrument. The SDK enforces set_tracer_provider as set-once, so an
application that installed its own provider before bootstrap still wins and the middleware still binds
to theirs. That is unchanged behaviour for exactly the users the issue worried about breaking. What
changes is the case the issue is about: nobody installed one, so lite-bootstrap's now exists.

This does mean a service that loses the set-once race is left with an orphaned provider carrying a live
exporter thread. That hazard is not new and not FastStream-specific, it is identical in FastAPI and
Litestar today, so this change makes FastStream share it rather than invent it. Filed separately as
#227.

Falls out for free: _apply_instrumentors now runs, so opentelemetry_instrumentors works and
teardown()'s uninstrument() stops being unpaired; _silence_otel_loggers() now runs, so OTel's
"Attempting to uninstrument while already uninstrumented" is suppressed as it is everywhere else. That
warning was visible on stderr before this change and is gone after.

No ADR

Against the three tests in the domain-modeling skill's ADR-FORMAT.md, despite the issue text saying
"either way it wants an ADR". That is right for option 2 and wrong for option 1. This change removes
a deviation: afterwards FastStream does what the other three bootstrappers do, so there is nothing
surprising left for a future reader to wonder about. Option 2 would have been a deliberate deviation
from the obvious path and would have needed one.

Tests

Two, both red on main and green with the one line (verified by stashing the source hunk and
re-running, not only before writing it):

  • test_faststream_bootstrap_builds_its_own_tracer_provider — the instrument holds a real SDK
    TracerProvider carrying service.name. Was None.
  • test_faststream_bootstrap_applies_opentelemetry_instrumentors — an instrumentor in
    opentelemetry_instrumentors is handed that provider. Was never called.

Both assert on the instrument's own _tracer_provider rather than the process global, following
test_free_bootstrap_passes_sampler_to_tracer_provider, because the global is set-once and belongs to
whichever test bootstraps first. Ran the FastStream file both first and last relative to the FastAPI
and Free files to confirm order independence.

A third test asserting the middleware receives get_tracer_provider() was written and then dropped: it
passed identically with and without the fix, so it pinned nothing, and the obvious stronger assertion
(get_tracer_provider() is instrument._tracer_provider) passes alone but fails once another file
bootstraps first, which would have made the suite order-dependent.

Out of scope

@lesnik512
lesnik512 merged commit d6a7ad1 into main Sep 20, 2026
13 checks passed
@lesnik512
lesnik512 deleted the fix/faststream-tracer-provider branch September 20, 2026 08:29
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.

FastStream never builds a TracerProvider, so most of OpenTelemetryConfig is inert there

1 participant