Skip to content

fix: trace FastStream without a broker telemetry middleware - #230

Merged
lesnik512 merged 1 commit into
mainfrom
fix/faststream-otel-gate
Sep 20, 2026
Merged

lesnik512 merged 1 commit into
mainfrom
fix/faststream-otel-gate

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Closes #226. Follow-up to #223 (merged as d6a7ad1), which made FastStream build a TracerProvider but
left this remainder of its headline claim, "all of these are accepted and silently do nothing".

FastStreamOpenTelemetryInstrument gated itself on a second field:

not_configured_reason = OpenTelemetryInstrument.not_configured_reason + " or opentelemetry_middleware_cls is empty"

@classmethod
def is_configured(cls, bootstrap_config: "FastStreamConfig") -> bool:  # ty: ignore[invalid-method-override]
    return super().is_configured(bootstrap_config) and bool(bootstrap_config.opentelemetry_middleware_cls)

Both overrides are deleted. The middleware stays conditional on the guard already inside bootstrap().

That gate was sound before #223, when adding the broker middleware was the only thing the instrument
did: no middleware class meant genuinely nothing to do. Since #223 the instrument does three things
and still refused all three over the absence of one, so FastStreamConfig(opentelemetry_endpoint=...)
without a middleware class built no provider at all. Config-level skips are deliberately silent
(base.py, "Config-level skip first: silent (no warning)"), so nothing was reported above INFO.

Behaviour change

A FastStream service setting opentelemetry_endpoint or opentelemetry_log_traces without
opentelemetry_middleware_cls previously got nothing. It now builds a provider, starts an exporter
and claims the process-global set_tracer_provider slot. That is what the config asked for, but it is
a change in behaviour, not only a fix, which is why it is its own release rather than riding along
with #223.

What it buys, neither of which involves the broker:

  • Health-check spans. FastStreamHealthChecksInstrument wraps its handler in
    tracer.start_as_current_span(...). That module-level tracer is a ProxyTracer resolving on span
    entry, so with no provider installed the spans were NonRecordingSpans. Verified by hand that they
    now record: "name": "GET /custom-health/" reaches the exporter with opentelemetry_middleware_cls=None.
  • opentelemetry_instrumentors. _apply_instrumentors lives in the base bootstrap() that never
    ran. An HTTP-client or SQLAlchemy instrumentor on a FastStream service has nothing to do with the
    broker middleware, and was dropped.

Broker messages stay untraced without a middleware class. That is unavoidable, and the docs now say so
explicitly rather than implying the field gates all of OpenTelemetry.

Vocabulary

CONTEXT.md defines an instrument as configured when the user's config asks for it. An
opentelemetry_endpoint is the user asking. The deleted override made an asked-for instrument
disappear into skipped_instruments, which is the quiet skip "you have to go looking for". The
deletion brings the code back in line with the definition.

Tests

The #223 instrumentor test is parametrized over [RedisTelemetryMiddleware, None] rather than copied.
without_middleware fails on main (assert len(instruments) == 1 → 0, the instrument is not even
instantiated) and passes here; with_middleware passes either way, since that is the already-fixed
#223 regression.

RecordingInstrumentor stays defined inside the test function on purpose: BaseInstrumentor is a
singleton per class, so a module-level one would find itself already instrumented on the second
parametrized run and silently skip.

Not tested, deliberately: an assertion that the health-check span records. It is the most
user-visible consequence, but the span resolves through the global provider, which is set-once and
owned by whichever test bootstraps first. An in-memory exporter attached to this instrument's provider
would see the span when the file runs alone and miss it in a full run. That is the same
order-dependence that got a test dropped from #228.

Type checking

Deleting this is_configured removes one # ty: ignore[invalid-method-override]. AGENTS.md names
that suppression as recurring and correct, and it still is: two sites remain, in
litestar_bootstrapper.py and in FastStreamPrometheusInstrument just below. No AGENTS.md edit
needed.

Out of scope

FastStreamPrometheusInstrument keeps the analogous prometheus_middleware_cls gate while its
bootstrap() mounts the metrics path unconditionally, so an injected prometheus_collector_registry
with no middleware class yields no endpoint. The #226 fix does not transfer there, because
prometheus_metrics_path defaults to /metrics and deleting the override would change every
FastStream service's HTTP surface. Filed with options as #229.

@lesnik512
lesnik512 merged commit f28547b into main Sep 20, 2026
13 checks passed
@lesnik512
lesnik512 deleted the fix/faststream-otel-gate branch September 20, 2026 08:42
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 skips the whole OpenTelemetry instrument when opentelemetry_middleware_cls is unset

1 participant