Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/integrations/faststream.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ application = bootstrapper.bootstrap()

Read more about available configuration options [here](../introduction/configuration.md).

## Health and metrics routes

When configured, FastStream health and Prometheus metrics routes accept requests with or without
the trailing slash. For example, both `/health` and `/health/` reach the same health check, and a
metrics path configured as `/metrics` also serves `/metrics/`. If the two configured paths differ only by a trailing slash, each
route keeps its explicitly configured path.

## Multiple brokers

Tracing, metrics and logging cover every broker the application has when `bootstrap()` runs. Add
Expand Down
20 changes: 16 additions & 4 deletions lite_bootstrap/bootstrappers/faststream_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ def _middleware_targets(
return targets


def _route_path_variants(path: str) -> tuple[str, ...]:
alternate_path = path.removesuffix("/") if path.endswith("/") else f"{path}/"
return (path, alternate_path) if alternate_path else (path,)


def _make_asgi_faststream() -> "AsgiFastStream":
return AsgiFastStream()

Expand Down Expand Up @@ -154,7 +159,14 @@ async def check_health(_: object) -> "AsgiResponse":
if config.opentelemetry_generate_health_check_spans and import_checker.is_opentelemetry_installed:
check_health = tracer.start_as_current_span(f"GET {config.health_checks_path}")(check_health)

config.application.mount(config.health_checks_path, check_health)
for path in _route_path_variants(config.health_checks_path):
if (
path != config.health_checks_path
and path == config.prometheus_metrics_path
and FastStreamPrometheusInstrument.dependencies_installed()
):
continue
config.application.mount(path, check_health)

async def _define_framework_health_status(self) -> bool:
config = self.bootstrap_config
Expand Down Expand Up @@ -236,9 +248,9 @@ def dependencies_installed() -> bool:

def bootstrap(self) -> None:
config = self.bootstrap_config
config.application.mount(
config.prometheus_metrics_path, prometheus_client.make_asgi_app(self.collector_registry)
)
metrics_app = prometheus_client.make_asgi_app(self.collector_registry)
for path in _route_path_variants(config.prometheus_metrics_path):
config.application.mount(path, metrics_app)
if config.prometheus_middleware_cls and config.application.brokers:
for broker, middleware_cls in _middleware_targets(
config.application.brokers, config.prometheus_middleware_cls, "prometheus_middleware_cls"
Expand Down
31 changes: 31 additions & 0 deletions tests/test_faststream_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,37 @@ async def test_faststream_bootstrap(broker: RedisBroker) -> None:
assert not bootstrapper.is_bootstrapped


@pytest.mark.parametrize("path", ["/health", "/health/", "/metrics", "/metrics/"])
async def test_faststream_route_paths_accept_both_slash_forms(broker: RedisBroker, path: str) -> None:
config = dataclasses.replace(
build_faststream_config(broker=broker), health_checks_path="/health/", prometheus_metrics_path="/metrics"
)
application = FastStreamBootstrapper(bootstrap_config=config).bootstrap()

with TestClient(app=application) as test_client:
async with TestRedisBroker(broker):
response = test_client.get(path, follow_redirects=False)

assert response.status_code == status.HTTP_200_OK


async def test_faststream_configured_metrics_path_takes_precedence_over_health_alias(broker: RedisBroker) -> None:
config = dataclasses.replace(
build_faststream_config(broker=broker), health_checks_path="/health/", prometheus_metrics_path="/health"
)
application = FastStreamBootstrapper(bootstrap_config=config).bootstrap()

with TestClient(app=application) as test_client:
async with TestRedisBroker(broker):
response = test_client.get("/health")
health_response = test_client.get("/health/")

assert health_response.status_code == status.HTTP_200_OK
assert health_response.json()["health_status"] is True
assert response.status_code == status.HTTP_200_OK
assert response.headers["content-type"].startswith("text/plain")


async def test_faststream_sentry_context_is_isolated_per_message(broker: RedisBroker) -> None:
transport = SentryTestTransport()
config = FastStreamConfig(
Expand Down
Loading