Skip to content

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

Description

@lesnik512

FastStreamOpenTelemetryInstrument.bootstrap() overrides the base without calling
super().bootstrap():

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

It is the only OpenTelemetry instrument in FastStreamBootstrapper.instruments_types, so on FastStream
lite-bootstrap never constructs a TracerProvider at all. The middleware is wired to whatever
opentelemetry.trace.get_tracer_provider() returns, which in a process where nothing else installed one
is a ProxyTracerProvider handing out NonRecordingSpans.

What is inert

FastStreamConfig inherits OpenTelemetryConfig, so all of these are accepted and silently do nothing:

is_configured still returns True for a config carrying opentelemetry_endpoint and
opentelemetry_middleware_cls, so the instrument reports as active in build_summary() while exporting
nothing. The docs reinforce it: docs/introduction/configuration.md documents these fields in one
OpenTelemetry section and then adds "For FastStream you must provide additionally:
opentelemetry_middleware_cls", which reads as "the above, plus one more".

Teardown is asymmetric for the same reason: the inherited teardown() calls uninstrument() on every
entry in opentelemetry_instrumentors that bootstrap() never instrumented, and the base's
_silence_otel_loggers() never ran, so OTel's "Attempting to uninstrument while already uninstrumented"
warning is not suppressed.

Why it is not obvious

Nothing pins the current behaviour — no test asserts that FastStream uses the ambient provider, and no ADR
records it as a decision. It may well be deliberate (a FastStream service is often bootstrapped alongside
something else that owns the provider), but if so it is undocumented, and the config surface contradicts it.

Options

  1. Call super().bootstrap() and pass the instrument's own provider to the middleware. Makes every
    documented field work. It is a behaviour change: lite-bootstrap would start constructing and
    set_tracer_provider-ing for FastStream services that today supply their own, and the SDK enforces
    set-once per process, so a service doing both would find one of the two silently ignored.
  2. Keep the ambient provider and narrow the surface. Document that FastStream consumes an
    externally-installed provider, and make the inert fields say so — at minimum in the docs, possibly as a
    warn_at_caller when a FastStream config carries opentelemetry_endpoint.

Either way it wants an ADR, since the choice is exactly the kind of thing docs/adr/ records.

Found while implementing #184, whose "This applies to every bootstrapper, since they share
OpenTelemetryInstrument" holds for FastAPI, Litestar and Free but not for FastStream.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions