Skip to content

Commit 5de8934

Browse files
committed
Add Prometheus custom labels support for FastAPI and FastStream
1 parent 395fd74 commit 5de8934

File tree

7 files changed

+103
-6
lines changed

7 files changed

+103
-6
lines changed

microbootstrap/bootstrappers/fastapi.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from fastapi.middleware.cors import CORSMiddleware
66
from fastapi_offline_docs import enable_offline_docs
77
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
8-
from prometheus_fastapi_instrumentator import Instrumentator
8+
from prometheus_fastapi_instrumentator import Instrumentator, metrics
99

1010
from microbootstrap.bootstrappers.base import ApplicationBootstrapper
1111
from microbootstrap.config.fastapi import FastApiConfig
@@ -113,7 +113,11 @@ def bootstrap_after(self, application: ApplicationT) -> ApplicationT:
113113
@FastApiBootstrapper.use_instrument()
114114
class FastApiPrometheusInstrument(PrometheusInstrument[FastApiPrometheusConfig]):
115115
def bootstrap_after(self, application: ApplicationT) -> ApplicationT:
116-
Instrumentator(**self.instrument_config.prometheus_instrumentator_params).instrument(
116+
Instrumentator(**self.instrument_config.prometheus_instrumentator_params).add(
117+
metrics.default(
118+
custom_labels=self.instrument_config.prometheus_custom_labels,
119+
),
120+
).instrument(
117121
application,
118122
**self.instrument_config.prometheus_instrument_params,
119123
).expose(

microbootstrap/bootstrappers/faststream.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,10 @@ def bootstrap_before(self) -> dict[str, typing.Any]:
9191
def bootstrap_after(self, application: AsgiFastStream) -> AsgiFastStream: # type: ignore[override]
9292
if self.instrument_config.prometheus_middleware_cls and application.broker:
9393
application.broker.add_middleware(
94-
self.instrument_config.prometheus_middleware_cls(registry=prometheus_client.REGISTRY),
94+
self.instrument_config.prometheus_middleware_cls(
95+
registry=prometheus_client.REGISTRY,
96+
custom_labels=self.instrument_config.prometheus_custom_labels,
97+
),
9598
)
9699
return application
97100

microbootstrap/instruments/prometheus_instrument.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class FastApiPrometheusConfig(BasePrometheusConfig):
3030
prometheus_instrumentator_params: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
3131
prometheus_instrument_params: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
3232
prometheus_expose_params: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
33+
prometheus_custom_labels: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
3334

3435

3536
@typing.runtime_checkable
@@ -41,11 +42,13 @@ def __init__(
4142
app_name: str = ...,
4243
metrics_prefix: str = "faststream",
4344
received_messages_size_buckets: typing.Sequence[float] | None = None,
45+
custom_labels: dict[str, str | typing.Callable[[typing.Any], str]] | None = None,
4446
) -> None: ...
4547

4648

4749
class FastStreamPrometheusConfig(BasePrometheusConfig):
4850
prometheus_middleware_cls: type[FastStreamPrometheusMiddlewareProtocol] | None = None
51+
prometheus_custom_labels: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
4952

5053

5154
class PrometheusInstrument(Instrument[PrometheusConfigT]):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ litestar = [
7272
"prometheus-client>=0.20",
7373
]
7474
granian = ["granian[reload]>=1"]
75-
faststream = ["faststream~=0.5", "prometheus-client>=0.20"]
75+
faststream = ["faststream~=0.6.2", "prometheus-client>=0.20"]
7676

7777
[dependency-groups]
7878
dev = [

tests/conftest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55

66
import litestar
77
import pytest
8+
from prometheus_client import REGISTRY
89
from sentry_sdk.transport import Transport as SentryTransport
910

1011
import microbootstrap.settings
1112
from microbootstrap import (
1213
FastApiPrometheusConfig,
14+
FastStreamPrometheusConfig,
1315
LitestarPrometheusConfig,
1416
LoggingConfig,
1517
OpentelemetryConfig,
@@ -74,6 +76,11 @@ def minimal_litestar_prometheus_config() -> LitestarPrometheusConfig:
7476
return LitestarPrometheusConfig()
7577

7678

79+
@pytest.fixture
80+
def minimal_faststream_prometheus_config() -> FastStreamPrometheusConfig:
81+
return FastStreamPrometheusConfig()
82+
83+
7784
@pytest.fixture
7885
def minimal_swagger_config() -> SwaggerConfig:
7986
return SwaggerConfig()
@@ -132,3 +139,9 @@ def reset_reloaded_settings_module() -> typing.Iterator[None]:
132139
@pytest.fixture(autouse=True)
133140
def patch_out_entry_points(monkeypatch: pytest.MonkeyPatch) -> None:
134141
monkeypatch.setattr(opentelemetry_instrument, "entry_points", MagicMock(retrun_value=[]))
142+
143+
144+
@pytest.fixture(autouse=True)
145+
def clean_prometheus_registry() -> None:
146+
REGISTRY._names_to_collectors.clear() # noqa: SLF001
147+
REGISTRY._collector_to_names.clear() # noqa: SLF001

tests/instruments/test_prometheus.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,23 @@
33
import fastapi
44
import litestar
55
from fastapi.testclient import TestClient as FastAPITestClient
6+
from faststream.redis import RedisBroker, TestRedisBroker
7+
from faststream.redis.prometheus import RedisPrometheusMiddleware
68
from litestar import status_codes
79
from litestar.middleware.base import DefineMiddleware
810
from litestar.testing import TestClient as LitestarTestClient
911

10-
from microbootstrap import FastApiPrometheusConfig, LitestarPrometheusConfig
12+
from microbootstrap import FastApiPrometheusConfig, FastStreamSettings, LitestarPrometheusConfig
1113
from microbootstrap.bootstrappers.fastapi import FastApiPrometheusInstrument
14+
from microbootstrap.bootstrappers.faststream import FastStreamBootstrapper
1215
from microbootstrap.bootstrappers.litestar import LitestarPrometheusInstrument
13-
from microbootstrap.instruments.prometheus_instrument import BasePrometheusConfig, PrometheusInstrument
16+
from microbootstrap.config.faststream import FastStreamConfig
17+
from microbootstrap.instruments.prometheus_instrument import (
18+
BasePrometheusConfig,
19+
FastStreamPrometheusConfig,
20+
PrometheusInstrument,
21+
)
22+
from tests.utils import check_is_metrics_has_labels, create_test_redis_subscriber
1423

1524

1625
def test_prometheus_is_ready(minimal_base_prometheus_config: BasePrometheusConfig) -> None:
@@ -85,3 +94,42 @@ def test_fastapi_prometheus_bootstrap_working(minimal_fastapi_prometheus_config:
8594
)
8695
assert response.status_code == status_codes.HTTP_200_OK
8796
assert response.text
97+
98+
99+
def test_fastapi_prometheus_custom_labels(
100+
minimal_fastapi_prometheus_config: FastApiPrometheusConfig,
101+
) -> None:
102+
custom_labels = {"test_label": "test_value"}
103+
minimal_fastapi_prometheus_config.prometheus_custom_labels = custom_labels
104+
prometheus_instrument: typing.Final = FastApiPrometheusInstrument(minimal_fastapi_prometheus_config)
105+
106+
fastapi_application = fastapi.FastAPI()
107+
fastapi_application = prometheus_instrument.bootstrap_after(fastapi_application)
108+
109+
response: typing.Final = FastAPITestClient(app=fastapi_application).get(
110+
minimal_fastapi_prometheus_config.prometheus_metrics_path
111+
)
112+
113+
assert response.status_code == status_codes.HTTP_200_OK
114+
assert check_is_metrics_has_labels(custom_labels=custom_labels)
115+
116+
117+
async def test_faststream_prometheus_custom_labels(
118+
minimal_faststream_prometheus_config: FastStreamPrometheusConfig,
119+
) -> None:
120+
custom_labels = {"test_label": "test_value"}
121+
minimal_faststream_prometheus_config.prometheus_custom_labels = custom_labels
122+
minimal_faststream_prometheus_config.prometheus_middleware_cls = RedisPrometheusMiddleware # type: ignore[assignment]
123+
124+
broker: typing.Final = RedisBroker()
125+
(
126+
FastStreamBootstrapper(FastStreamSettings())
127+
.configure_application(FastStreamConfig(broker=broker))
128+
.configure_instrument(minimal_faststream_prometheus_config)
129+
.bootstrap()
130+
)
131+
create_test_redis_subscriber(broker, topic="test-topic")
132+
133+
async with TestRedisBroker(broker) as tb:
134+
await tb.publish({"foo": "bar"}, "test-topic")
135+
assert check_is_metrics_has_labels(custom_labels)

tests/utils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import typing
2+
3+
from faststream.redis import RedisBroker
4+
from prometheus_client import REGISTRY
5+
6+
7+
def check_is_metrics_has_labels(custom_labels: dict[str, str]) -> bool:
8+
expected_keys = set(custom_labels.keys())
9+
10+
for metric in REGISTRY.collect():
11+
for sample in metric.samples:
12+
label_keys = set(sample.labels.keys())
13+
if expected_keys & label_keys:
14+
return True
15+
return False
16+
17+
18+
def create_test_redis_subscriber(
19+
broker: RedisBroker,
20+
topic: str,
21+
) -> typing.Callable[[dict[str, str]], typing.Coroutine[typing.Any, typing.Any, None]]:
22+
@broker.subscriber(topic)
23+
async def test_subscriber(payload: dict[str, str]) -> None:
24+
pass
25+
26+
return test_subscriber

0 commit comments

Comments
 (0)