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
- 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.
- 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.
FastStreamOpenTelemetryInstrument.bootstrap()overrides the base without callingsuper().bootstrap():It is the only OpenTelemetry instrument in
FastStreamBootstrapper.instruments_types, so on FastStreamlite-bootstrap never constructs a
TracerProviderat all. The middleware is wired to whateveropentelemetry.trace.get_tracer_provider()returns, which in a process where nothing else installed oneis a
ProxyTracerProviderhanding outNonRecordingSpans.What is inert
FastStreamConfiginheritsOpenTelemetryConfig, so all of these are accepted and silently do nothing:opentelemetry_endpoint,opentelemetry_exporter_protocol,opentelemetry_insecure— no exporter is builtopentelemetry_service_name,opentelemetry_namespace,opentelemetry_container_name— noResourceis builtopentelemetry_log_traces— no console span processoropentelemetry_instrumentors—_apply_instrumentorslives in the basebootstrap()opentelemetry_sampler(OpenTelemetry sampler is not configurable #184, feat: make the OpenTelemetry sampler configurable, and compress the ADRs #222) — joins the listis_configuredstill returns True for a config carryingopentelemetry_endpointandopentelemetry_middleware_cls, so the instrument reports as active inbuild_summary()while exportingnothing. The docs reinforce it:
docs/introduction/configuration.mddocuments these fields in oneOpenTelemetry 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()callsuninstrument()on everyentry in
opentelemetry_instrumentorsthatbootstrap()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
super().bootstrap()and pass the instrument's own provider to the middleware. Makes everydocumented 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 enforcesset-once per process, so a service doing both would find one of the two silently ignored.
externally-installed provider, and make the inert fields say so — at minimum in the docs, possibly as a
warn_at_callerwhen a FastStream config carriesopentelemetry_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.