diff --git a/docs/introduction/configuration.md b/docs/introduction/configuration.md index 1ea9e2d..95e3189 100644 --- a/docs/introduction/configuration.md +++ b/docs/introduction/configuration.md @@ -73,9 +73,18 @@ Additional parameters for Litestar integration: Prometheus's integration for FastStream requires `prometheus_client` package. -To bootstrap Prometheus for FastStream, you must provide additionally: - -- `prometheus_middleware_cls`. +To bootstrap Prometheus for FastStream, you must provide at least one of: + +- `prometheus_middleware_cls` - the broker metrics middleware, e.g. + `faststream.redis.prometheus.RedisPrometheusMiddleware`. It is constructed with the instrument's + registry and added to the broker, when the application has one. +- `prometheus_collector_registry` - a `prometheus_client.CollectorRegistry` of your own, used in + place of the fresh one the instrument would otherwise build. + +With neither, nothing would populate the registry, so the metrics endpoint is not mounted and the +instrument is skipped. Unlike the other frameworks, FastStream serves a private registry rather than +`prometheus_client.REGISTRY`, so an endpoint with no middleware and no injected registry would have +nothing to report. ### Prometheus FastAPI diff --git a/lite_bootstrap/bootstrappers/faststream_bootstrapper.py b/lite_bootstrap/bootstrappers/faststream_bootstrapper.py index 5b7d52d..5dc7655 100644 --- a/lite_bootstrap/bootstrappers/faststream_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/faststream_bootstrapper.py @@ -161,7 +161,10 @@ def _make_collector_registry() -> "prometheus_client.CollectorRegistry": class FastStreamPrometheusInstrument(PrometheusInstrument): bootstrap_config: FastStreamConfig collector_registry: "prometheus_client.CollectorRegistry" = dataclasses.field(init=False) - not_configured_reason = PrometheusInstrument.not_configured_reason + " or prometheus_middleware_cls is missing" + not_configured_reason = ( + PrometheusInstrument.not_configured_reason + + ", or neither prometheus_middleware_cls nor prometheus_collector_registry is set" + ) missing_dependency_message = "prometheus_client is not installed" def __post_init__(self) -> None: @@ -170,7 +173,10 @@ def __post_init__(self) -> None: @classmethod def is_configured(cls, bootstrap_config: "FastStreamConfig") -> bool: # ty: ignore[invalid-method-override] - return super().is_configured(bootstrap_config) and bool(bootstrap_config.prometheus_middleware_cls) + return super().is_configured(bootstrap_config) and ( + bootstrap_config.prometheus_middleware_cls is not None + or bootstrap_config.prometheus_collector_registry is not None + ) @staticmethod def dependencies_installed() -> bool: diff --git a/tests/test_faststream_bootstrap.py b/tests/test_faststream_bootstrap.py index 4f3ac39..96562b6 100644 --- a/tests/test_faststream_bootstrap.py +++ b/tests/test_faststream_bootstrap.py @@ -23,6 +23,7 @@ from lite_bootstrap.bootstrappers.faststream_bootstrapper import ( FastStreamLoggingInstrument, FastStreamOpenTelemetryInstrument, + FastStreamPrometheusInstrument, ) from lite_bootstrap.exceptions import ConfigurationError from tests.conftest import ( @@ -254,7 +255,10 @@ def test_faststream_build_excluded_urls_covers_prometheus_and_health_paths(broke assert config_with_health_spans.health_checks_path not in excluded_with # kept when health spans are on -async def test_faststream_prometheus_uses_injected_registry(broker: RedisBroker) -> None: +@pytest.mark.parametrize( + "middleware_cls", [RedisPrometheusMiddleware, None], ids=["with_middleware", "without_middleware"] +) +async def test_faststream_prometheus_uses_injected_registry(broker: RedisBroker, middleware_cls: type | None) -> None: custom_registry = prometheus_client.CollectorRegistry() counter_name = f"injected_counter_{uuid.uuid4().hex}_total" counter = prometheus_client.Counter(counter_name, "Injected registry counter", registry=custom_registry) @@ -262,6 +266,7 @@ async def test_faststream_prometheus_uses_injected_registry(broker: RedisBroker) bootstrap_config = dataclasses.replace( build_faststream_config(broker=broker), + prometheus_middleware_cls=middleware_cls, prometheus_collector_registry=custom_registry, ) bootstrapper = FastStreamBootstrapper(bootstrap_config=bootstrap_config) @@ -276,6 +281,27 @@ async def test_faststream_prometheus_uses_injected_registry(broker: RedisBroker) bootstrapper.teardown() +async def test_faststream_prometheus_is_skipped_without_middleware_or_registry(broker: RedisBroker) -> None: + bootstrap_config = dataclasses.replace( + build_faststream_config(broker=broker), + prometheus_middleware_cls=None, + prometheus_collector_registry=None, + ) + bootstrapper = FastStreamBootstrapper(bootstrap_config=bootstrap_config) + skipped = dict(bootstrapper.skipped_instruments) + assert FastStreamPrometheusInstrument in skipped + assert "prometheus_collector_registry" in skipped[FastStreamPrometheusInstrument] + + application = bootstrapper.bootstrap() + try: + with TestClient(app=application) as test_client: + async with TestRedisBroker(broker): + response = test_client.get(bootstrap_config.prometheus_metrics_path) + assert response.status_code == status.HTTP_404_NOT_FOUND + finally: + bootstrapper.teardown() + + def test_faststream_logging_teardown_runs_super_when_broker_write_raises(broker: RedisBroker) -> None: bootstrap_config = build_faststream_config(broker=broker) instrument = FastStreamLoggingInstrument(bootstrap_config=bootstrap_config)