Skip to content

fix: mount FastStream metrics for an injected collector registry - #238

Merged
lesnik512 merged 2 commits into
mainfrom
fix/229-faststream-metrics-registry
Sep 20, 2026
Merged

lesnik512 merged 2 commits into
mainfrom
fix/229-faststream-metrics-registry

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Closes #229.

FastStreamConfig accepts prometheus_collector_registry, and __post_init__ prefers an injected
registry over a fresh one. But is_configured required prometheus_middleware_cls, so a service
that injected a registry holding its own collectors and did not use the broker middleware got no
endpoint to scrape them from. The registry was accepted and did nothing, and the skip was silent
because config-level skips are deliberately quiet.

The gate now accepts either signal:

return super().is_configured(bootstrap_config) and (
    bootstrap_config.prometheus_middleware_cls is not None
    or bootstrap_config.prometheus_collector_registry is not None
)

Why option 1, and not the others

The issue listed four possibilities and called option 1 "slightly odd as a rule to explain". Reading
the sibling bootstrappers made it not odd at all. The rule is mount the endpoint when something
will populate the registry
: the broker middleware populates it, an injected registry arrives
already populated, and with neither there is nothing to scrape.

Three facts settle it, none of which are in the issue:

  1. FastStream is the only Prometheus instrument that overrides is_configured. FastAPI, Litestar
    and FastMCP all mount on the default path without asking. (The override at
    litestar_bootstrapper.py:290 is the Swagger instrument, not Prometheus.)
  2. FastStream already mounts HTTP by default. health_checks_enabled defaults to True, so
    /health/ is exposed unasked. The issue's worry about changing the HTTP surface is weaker than it
    looks.
  3. But FastStream genuinely differs from its siblings. FastMCP serves
    prometheus_client.REGISTRY literally; FastAPI's Instrumentator and Litestar's
    PrometheusController both fall back to it. Only FastStream builds a private registry. So
    deleting the override for full symmetry with FastStream skips the whole OpenTelemetry instrument when opentelemetry_middleware_cls is unset #226 would mount an endpoint serving an empty
    registry for every app that uses neither the middleware nor an injection.

Fact 3 is why the asymmetry survives, and it turns a special case into a consequence of the
private-registry design. It also disposes of the rest:

Tests

Red-to-green, verified by reverting only faststream_bootstrapper.py to main: two tests go red,
and the middleware case stays green.

case before after
injected registry, no middleware 404, instrument skipped 200, serves the injected registry
middleware set mounted unchanged
neither not mounted unchanged, still skipped

The injected-registry test is parametrized over middleware_cls rather than duplicated, and asserts
the counter's name appears in the response body, so it proves the mount serves the injected
registry rather than a fresh one. The third row is pinned by its own test: it is the behaviour the
issue was protecting, and it guards against a later drift into deleting the override.

That skip test also pins not_configured_reason, which is user-visible through the documented
skipped_instruments API and build_summary(). That assertion independently fails on main, so the
string is covered rather than assumed:

FastStreamPrometheusInstrument: prometheus_metrics_path is empty or not valid, or neither
prometheus_middleware_cls nor prometheus_collector_registry is set

Review changes

  • is not None rather than truthiness, matching __post_init__ two lines above. Identical behaviour
    today, since CollectorRegistry defines no __bool__ or __len__, but a subclass defining
    __len__ would have silently un-configured the instrument.
  • A comma in the reason string, whose or chain was ambiguous once a second clause was appended.
  • The docs no longer say the middleware is "added to the broker" unconditionally; bootstrap()
    guards on the application having one.

No ADR

One-line gate, so it fails "hard to reverse", and the "surprising without context" test is answered
by the configuration docs, which now state the rule and the private-registry reason behind it. Only
"a real trade-off" passes, and the practice here wants all three.

Verification

ruff format, ruff check, ty, 309 tests (up from 307) and mkdocs build --strict, all clean.

@lesnik512
lesnik512 merged commit 2a9fd06 into main Sep 20, 2026
13 checks passed
@lesnik512
lesnik512 deleted the fix/229-faststream-metrics-registry branch September 20, 2026 11:58
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 mounts no /metrics endpoint when prometheus_middleware_cls is unset, even with an injected registry

1 participant