Skip to content

Commit 92c0047

Browse files
authored
Merge pull request #126 from m1khalchenko/prometheus-custom-labels
Add Prometheus custom labels support for FastAPI and FastStream
2 parents 395fd74 + adef635 commit 92c0047

File tree

6 files changed

+114
-6
lines changed

6 files changed

+114
-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: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,34 @@
22

33
import fastapi
44
import litestar
5+
import pytest
56
from fastapi.testclient import TestClient as FastAPITestClient
7+
from faststream.redis import RedisBroker, TestRedisBroker
8+
from faststream.redis.prometheus import RedisPrometheusMiddleware
69
from litestar import status_codes
710
from litestar.middleware.base import DefineMiddleware
811
from litestar.testing import TestClient as LitestarTestClient
12+
from prometheus_client import REGISTRY
913

10-
from microbootstrap import FastApiPrometheusConfig, LitestarPrometheusConfig
14+
from microbootstrap import FastApiPrometheusConfig, FastStreamSettings, LitestarPrometheusConfig
1115
from microbootstrap.bootstrappers.fastapi import FastApiPrometheusInstrument
16+
from microbootstrap.bootstrappers.faststream import FastStreamBootstrapper
1217
from microbootstrap.bootstrappers.litestar import LitestarPrometheusInstrument
13-
from microbootstrap.instruments.prometheus_instrument import BasePrometheusConfig, PrometheusInstrument
18+
from microbootstrap.config.faststream import FastStreamConfig
19+
from microbootstrap.instruments.prometheus_instrument import (
20+
BasePrometheusConfig,
21+
FastStreamPrometheusConfig,
22+
PrometheusInstrument,
23+
)
24+
25+
26+
def check_is_metrics_has_labels(custom_labels_keys: set[str]) -> bool:
27+
for metric in REGISTRY.collect():
28+
for sample in metric.samples:
29+
label_keys = set(sample.labels.keys())
30+
if custom_labels_keys & label_keys:
31+
return True
32+
return False
1433

1534

1635
def test_prometheus_is_ready(minimal_base_prometheus_config: BasePrometheusConfig) -> None:
@@ -85,3 +104,69 @@ def test_fastapi_prometheus_bootstrap_working(minimal_fastapi_prometheus_config:
85104
)
86105
assert response.status_code == status_codes.HTTP_200_OK
87106
assert response.text
107+
108+
109+
@pytest.mark.parametrize(
110+
("custom_labels", "expected_label_keys"),
111+
[
112+
({"test_label": "test_value"}, {"test_label"}),
113+
({}, {"method", "handler", "status"}),
114+
],
115+
)
116+
def test_fastapi_prometheus_custom_labels(
117+
minimal_fastapi_prometheus_config: FastApiPrometheusConfig,
118+
custom_labels: dict[str, str],
119+
expected_label_keys: set[str],
120+
) -> None:
121+
minimal_fastapi_prometheus_config.prometheus_custom_labels = custom_labels
122+
prometheus_instrument: typing.Final = FastApiPrometheusInstrument(minimal_fastapi_prometheus_config)
123+
124+
fastapi_application = fastapi.FastAPI()
125+
fastapi_application = prometheus_instrument.bootstrap_after(fastapi_application)
126+
127+
response: typing.Final = FastAPITestClient(app=fastapi_application).get(
128+
minimal_fastapi_prometheus_config.prometheus_metrics_path
129+
)
130+
131+
assert response.status_code == status_codes.HTTP_200_OK
132+
assert check_is_metrics_has_labels(expected_label_keys)
133+
134+
135+
@pytest.mark.parametrize(
136+
("custom_labels", "expected_label_keys"),
137+
[
138+
({"test_label": "test_value"}, {"test_label"}),
139+
({}, {"app_name", "broker", "handler"}),
140+
],
141+
)
142+
async def test_faststream_prometheus_custom_labels(
143+
minimal_faststream_prometheus_config: FastStreamPrometheusConfig,
144+
custom_labels: dict[str, str],
145+
expected_label_keys: set[str],
146+
) -> None:
147+
minimal_faststream_prometheus_config.prometheus_custom_labels = custom_labels
148+
minimal_faststream_prometheus_config.prometheus_middleware_cls = RedisPrometheusMiddleware # type: ignore[assignment]
149+
150+
broker: typing.Final = RedisBroker()
151+
(
152+
FastStreamBootstrapper(FastStreamSettings())
153+
.configure_application(FastStreamConfig(broker=broker))
154+
.configure_instrument(minimal_faststream_prometheus_config)
155+
.bootstrap()
156+
)
157+
158+
def create_test_redis_subscriber(
159+
broker: RedisBroker,
160+
topic: str,
161+
) -> typing.Callable[[dict[str, str]], typing.Coroutine[typing.Any, typing.Any, None]]:
162+
@broker.subscriber(topic)
163+
async def test_subscriber(payload: dict[str, str]) -> None:
164+
pass
165+
166+
return test_subscriber
167+
168+
create_test_redis_subscriber(broker, topic="test-topic")
169+
170+
async with TestRedisBroker(broker) as tb:
171+
await tb.publish({"foo": "bar"}, "test-topic")
172+
assert check_is_metrics_has_labels(expected_label_keys)

0 commit comments

Comments
 (0)