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
6 changes: 4 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ per-framework guides, and Sentry's word for its own `Integration` objects.
**Configured**:
An instrument is configured when the user's config asks for it: `is_configured(config)`, a
classmethod evaluated before anything is instantiated. Unconfigured is the normal case, not a fault:
an empty `sentry_dsn` means the user did not want Sentry.
an empty `sentry_dsn` means the user did not want Sentry. Prometheus at its default metrics path
is also unconfigured when `prometheus_client` is missing, treating an absent extra as an opt-out.
_Avoid_: enabled. `logging_enabled` and `health_checks_enabled` are two config fields among the
several inputs `is_configured` reads; most instruments have no enabled flag at all.

Expand All @@ -45,7 +46,8 @@ An instrument the bootstrapper decided not to instantiate. Two paths, deliberate
signals: not configured is silent and lands in `skipped_instruments`, so it shows in
`build_summary()`; configured but with its optional package missing is a deployment surprise, so it
raises `InstrumentDependencyMissingWarning` plus a `logger.warning` and lands in neither list. The
quiet skip is the one you have to go looking for; the loud one finds you.
quiet skip includes Prometheus at its default path without `prometheus_client`; a custom path
with the same missing dependency takes the warning path.

**Health probe**:
One check of something the service depends on, run each time the health check route is requested. A
Expand Down
12 changes: 9 additions & 3 deletions docs/introduction/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,15 @@ the option [here](https://docs.sentry.io/platforms/python/data-management/data-c

Prometheus is the cheapest of the three non-logging instruments; see [Performance](performance.md).

Prometheus is on by default when its extra is installed, serving metrics at `prometheus_metrics_path`
(default: `/metrics`). Set `prometheus_metrics_path` to an empty string to disable it. FastStream is
the exception: its default is empty; see [Prometheus FastStream](#prometheus-faststream).
On FastAPI, Litestar and FastMCP, Prometheus is on by default when its extra is installed,
serving metrics at `prometheus_metrics_path` (default: `/metrics`). Set the path to an empty
string to disable it. Without `prometheus_client`, the default path is a silent opt-out;
a custom path still emits a missing-dependency warning. Explicitly setting `/metrics`
is indistinguishable from leaving the default and is also silent.

FastStream is opt-in: its default path is empty because broker metrics require a
broker-specific middleware class that lite-bootstrap cannot choose automatically.
See [Prometheus FastStream](#prometheus-faststream).

Additional parameters:

Expand Down
5 changes: 4 additions & 1 deletion lite_bootstrap/bootstrappers/fastapi_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,10 @@ class FastAPIPrometheusInstrument(PrometheusInstrument[FastAPIConfig]):

@staticmethod
def dependencies_installed() -> bool:
return import_checker.is_prometheus_fastapi_instrumentator_installed
return (
import_checker.is_prometheus_client_installed
and import_checker.is_prometheus_fastapi_instrumentator_installed
)

def bootstrap(self) -> None:
config = self.bootstrap_config
Expand Down
9 changes: 7 additions & 2 deletions lite_bootstrap/instruments/prometheus_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re
import typing

from lite_bootstrap import import_checker
from lite_bootstrap.exceptions import ConfigurationError
from lite_bootstrap.helpers.path import is_valid_path
from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument
Expand Down Expand Up @@ -38,11 +39,15 @@ def is_prometheus_http_metrics_on(self, *, opentelemetry_records_http_metrics: b

@dataclasses.dataclass(kw_only=True, slots=True)
class PrometheusInstrument(BaseInstrument[PrometheusConfigT]):
not_configured_reason = "prometheus_metrics_path is empty"
not_configured_reason = "prometheus_metrics_path is empty or uses its default without prometheus_client"

@classmethod
def is_configured(cls, bootstrap_config: "PrometheusConfig") -> bool:
return bool(bootstrap_config.prometheus_metrics_path)
path = bootstrap_config.prometheus_metrics_path
default_path = next(
field.default for field in dataclasses.fields(bootstrap_config) if field.name == "prometheus_metrics_path"
)
return bool(path) and (path != default_path or import_checker.is_prometheus_client_installed)

def infrastructure_excluded_patterns(self) -> list[str]:
config = self.bootstrap_config
Expand Down
5 changes: 4 additions & 1 deletion tests/instruments/test_prometheus_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ def test_prometheus_instrument_configured_with_default_path() -> None:
def test_prometheus_instrument_not_configured_with_empty_path() -> None:
config = PrometheusConfig(prometheus_metrics_path="")
assert not PrometheusInstrument.is_configured(config)
assert PrometheusInstrument.not_configured_reason == "prometheus_metrics_path is empty"
assert (
PrometheusInstrument.not_configured_reason
== "prometheus_metrics_path is empty or uses its default without prometheus_client"
)


def test_prometheus_instrument_configured_with_custom_valid_path() -> None:
Expand Down
28 changes: 28 additions & 0 deletions tests/test_fastapi_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,34 @@ def test_fastapi_bootstrapper_with_missing_instrument_dependency(
FastAPIBootstrapper(bootstrap_config=fastapi_config)


@pytest.mark.parametrize("metrics_path", [None, "/custom"])
def test_fastapi_missing_prometheus_is_quiet_only_for_the_default_path(
metrics_path: str | None, caplog: pytest.LogCaptureFixture
) -> None:
config = FastAPIConfig() if metrics_path is None else FastAPIConfig(prometheus_metrics_path=metrics_path)
with emulate_package_missing("prometheus_client"), warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
bootstrapper = FastAPIBootstrapper(bootstrap_config=config)
try:
assert not any(
isinstance(instrument, fastapi_bootstrapper.FastAPIPrometheusInstrument)
for instrument in bootstrapper.instruments
)
if metrics_path is None:
assert not caught
assert not caplog.records
assert any(
instrument is fastapi_bootstrapper.FastAPIPrometheusInstrument
for instrument, _reason in bootstrapper.skipped_instruments
)
else:
assert [warning.category for warning in caught] == [InstrumentDependencyMissingWarning]
assert "prometheus" in str(caught[0].message)
assert "prometheus" in caplog.text
finally:
bootstrapper.teardown()


def test_fastapi_bootstrapper_runs_without_sentry_sdk(fastapi_config: FastAPIConfig) -> None:
with emulate_package_missing_with_module_reload(
"sentry_sdk",
Expand Down
6 changes: 4 additions & 2 deletions tests/test_fastmcp_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ def test_fastmcp_logging_config_warns_nothing(overrides: dict[str, bool]) -> Non
[
("sentry_sdk", {"sentry_dsn": "https://testdsn@localhost/1"}),
("structlog", {}),
("prometheus_client", {}),
("prometheus_client", {"prometheus_metrics_path": "/custom"}),
],
)
def test_fastmcp_bootstrapper_with_missing_instrument_dependency(
Expand Down Expand Up @@ -372,8 +372,10 @@ def test_fastmcp_bootstrap_without_prometheus_client() -> None:
"prometheus_client",
["lite_bootstrap.bootstrappers.fastmcp_bootstrapper"],
):
with pytest.warns(UserWarning, match="prometheus_client"):
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
bootstrapper = FastMcpBootstrapper(bootstrap_config=FastMcpConfig())
assert not caught
bootstrapper.bootstrap()
bootstrapper.teardown()

Expand Down
2 changes: 1 addition & 1 deletion tests/test_faststream_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ def test_faststream_prometheus_is_skipped_without_metrics_path(broker: RedisBrok
)
bootstrapper = FastStreamBootstrapper(bootstrap_config=bootstrap_config)
skipped = dict(bootstrapper.skipped_instruments)
assert skipped[FastStreamPrometheusInstrument] == "prometheus_metrics_path is empty"
assert skipped[FastStreamPrometheusInstrument] == FastStreamPrometheusInstrument.not_configured_reason


async def test_faststream_prometheus_serves_the_default_registry(broker: RedisBroker) -> None:
Expand Down
Loading