Skip to content

Commit db422bf

Browse files
authored
Merge pull request #15 from modern-python/12-feature-add-prometheus-instrument
add prometheus instrument
2 parents d2bf874 + 80e5bcc commit db422bf

File tree

7 files changed

+108
-1
lines changed

7 files changed

+108
-1
lines changed

lite_bootstrap/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
FastAPIHealthChecksInstrument,
44
FastAPILoggingInstrument,
55
FastAPIOpenTelemetryInstrument,
6+
FastAPIPrometheusInstrument,
67
FastAPISentryInstrument,
78
)
89
from lite_bootstrap.bootstrappers.free_bootstrapper import FreeBootstrapper
@@ -11,6 +12,7 @@
1112
LitestarHealthChecksInstrument,
1213
LitestarLoggingInstrument,
1314
LitestarOpenTelemetryInstrument,
15+
LitestarPrometheusInstrument,
1416
LitestarSentryInstrument,
1517
)
1618
from lite_bootstrap.instruments.healthchecks_instrument import HealthChecksInstrument
@@ -25,13 +27,15 @@
2527
"FastAPIHealthChecksInstrument",
2628
"FastAPILoggingInstrument",
2729
"FastAPIOpenTelemetryInstrument",
30+
"FastAPIPrometheusInstrument",
2831
"FastAPISentryInstrument",
2932
"FreeBootstrapper",
3033
"HealthChecksInstrument",
3134
"LitestarBootstrapper",
3235
"LitestarHealthChecksInstrument",
3336
"LitestarLoggingInstrument",
3437
"LitestarOpenTelemetryInstrument",
38+
"LitestarPrometheusInstrument",
3539
"LitestarSentryInstrument",
3640
"LoggingInstrument",
3741
"OpenTelemetryInstrument",

lite_bootstrap/bootstrappers/fastapi_bootstrapper.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
import dataclasses
33
import typing
44

5+
from prometheus_fastapi_instrumentator import Instrumentator
6+
57
from lite_bootstrap.bootstrappers.base import BaseBootstrapper
68
from lite_bootstrap.instruments.healthchecks_instrument import HealthChecksInstrument, HealthCheckTypedDict
79
from lite_bootstrap.instruments.logging_instrument import LoggingInstrument
810
from lite_bootstrap.instruments.opentelemetry_instrument import OpenTelemetryInstrument
11+
from lite_bootstrap.instruments.prometheus_instrument import PrometheusInstrument
912
from lite_bootstrap.instruments.sentry_instrument import SentryInstrument
1013
from lite_bootstrap.service_config import ServiceConfig
1114

@@ -65,6 +68,27 @@ def teardown(self, application: fastapi.FastAPI | None = None) -> None:
6568
class FastAPISentryInstrument(SentryInstrument): ...
6669

6770

71+
@dataclasses.dataclass(kw_only=True, frozen=True)
72+
class FastAPIPrometheusInstrument(PrometheusInstrument):
73+
metrics_path: str = "/metrics"
74+
metrics_include_in_schema: bool = False
75+
instrumentator_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
76+
instrument_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
77+
expose_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
78+
79+
def bootstrap(self, _: ServiceConfig, application: fastapi.FastAPI | None = None) -> None:
80+
if application:
81+
Instrumentator(**self.instrumentator_params).instrument(
82+
application,
83+
**self.instrument_params,
84+
).expose(
85+
application,
86+
endpoint=self.metrics_path,
87+
include_in_schema=self.metrics_include_in_schema,
88+
**self.expose_params,
89+
)
90+
91+
6892
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
6993
class FastAPIBootstrapper(BaseBootstrapper[fastapi.FastAPI, fastapi.FastAPI]):
7094
bootstrap_object: fastapi.FastAPI
@@ -73,6 +97,7 @@ class FastAPIBootstrapper(BaseBootstrapper[fastapi.FastAPI, fastapi.FastAPI]):
7397
| FastAPISentryInstrument
7498
| FastAPIHealthChecksInstrument
7599
| FastAPILoggingInstrument
100+
| FastAPIPrometheusInstrument
76101
]
77102
service_config: ServiceConfig
78103

lite_bootstrap/bootstrappers/litestar_bootstrapper.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
import dataclasses
33
import typing
44

5+
from litestar.plugins.prometheus import PrometheusConfig, PrometheusController
6+
57
from lite_bootstrap.bootstrappers.base import BaseBootstrapper
68
from lite_bootstrap.instruments.healthchecks_instrument import HealthChecksInstrument, HealthCheckTypedDict
79
from lite_bootstrap.instruments.logging_instrument import LoggingInstrument
810
from lite_bootstrap.instruments.opentelemetry_instrument import OpenTelemetryInstrument
11+
from lite_bootstrap.instruments.prometheus_instrument import PrometheusInstrument
912
from lite_bootstrap.instruments.sentry_instrument import SentryInstrument
1013
from lite_bootstrap.service_config import ServiceConfig
1114

@@ -63,6 +66,26 @@ def bootstrap(self, service_config: ServiceConfig, app_config: AppConfig | None
6366
class LitestarSentryInstrument(SentryInstrument): ...
6467

6568

69+
@dataclasses.dataclass(kw_only=True, frozen=True)
70+
class LitestarPrometheusInstrument(PrometheusInstrument):
71+
additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
72+
73+
def bootstrap(self, service_config: ServiceConfig, app_config: AppConfig | None = None) -> None:
74+
class LitestarPrometheusController(PrometheusController):
75+
path = self.metrics_path
76+
include_in_schema = self.metrics_include_in_schema
77+
openmetrics_format = True
78+
79+
litestar_prometheus_config = PrometheusConfig(
80+
app_name=service_config.service_name,
81+
**self.additional_params,
82+
)
83+
84+
if app_config:
85+
app_config.route_handlers.append(LitestarPrometheusController)
86+
app_config.middleware.append(litestar_prometheus_config.middleware)
87+
88+
6689
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
6790
class LitestarBootstrapper(BaseBootstrapper[AppConfig, litestar.Litestar]):
6891
bootstrap_object: AppConfig
@@ -71,6 +94,7 @@ class LitestarBootstrapper(BaseBootstrapper[AppConfig, litestar.Litestar]):
7194
| LitestarSentryInstrument
7295
| LitestarHealthChecksInstrument
7396
| LitestarLoggingInstrument
97+
| LitestarPrometheusInstrument
7498
]
7599
service_config: ServiceConfig
76100

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import dataclasses
2+
import re
3+
import typing
4+
5+
from lite_bootstrap.instruments.base import BaseInstrument
6+
from lite_bootstrap.service_config import ServiceConfig
7+
8+
9+
VALID_PATH_PATTERN: typing.Final = re.compile(r"^(/[a-zA-Z0-9_-]+)+/?$")
10+
11+
12+
def _is_valid_path(maybe_path: str) -> bool:
13+
return bool(re.fullmatch(VALID_PATH_PATTERN, maybe_path))
14+
15+
16+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
17+
class PrometheusInstrument(BaseInstrument):
18+
metrics_path: str = "/metrics"
19+
metrics_include_in_schema: bool = False
20+
21+
def is_ready(self, _: ServiceConfig) -> bool:
22+
return bool(self.metrics_path) and _is_valid_path(self.metrics_path)

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,11 @@ fastapi = [
5252
fastapi-otl = [
5353
"opentelemetry-instrumentation-fastapi",
5454
]
55+
fastapi-metrics = [
56+
"prometheus-fastapi-instrumentator>=6.1",
57+
]
5558
fastapi-all = [
56-
"lite-bootstrap[sentry,otl,logging,fastapi,fastapi-otl]"
59+
"lite-bootstrap[sentry,otl,logging,fastapi,fastapi-otl,fastapi-metrics]"
5760
]
5861
litestar = [
5962
"litestar>=2.9",

tests/test_fastapi_bootstrap.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
FastAPIHealthChecksInstrument,
1010
FastAPILoggingInstrument,
1111
FastAPIOpenTelemetryInstrument,
12+
FastAPIPrometheusInstrument,
1213
FastAPISentryInstrument,
1314
ServiceConfig,
1415
)
@@ -35,6 +36,7 @@ def test_fastapi_bootstrap(fastapi_app: FastAPI, service_config: ServiceConfig)
3536
path="/health/",
3637
),
3738
FastAPILoggingInstrument(logging_buffer_capacity=0),
39+
FastAPIPrometheusInstrument(),
3840
],
3941
)
4042
bootstrapper.bootstrap()
@@ -46,3 +48,12 @@ def test_fastapi_bootstrap(fastapi_app: FastAPI, service_config: ServiceConfig)
4648
assert response.json() == {"health_status": True, "service_name": "microservice", "service_version": "2.0.0"}
4749
finally:
4850
bootstrapper.teardown()
51+
52+
53+
def test_fastapi_prometheus_instrument(fastapi_app: FastAPI, service_config: ServiceConfig) -> None:
54+
prometheus_instrument = FastAPIPrometheusInstrument(metrics_path="/custom-metrics-path")
55+
prometheus_instrument.bootstrap(service_config, fastapi_app)
56+
57+
response = TestClient(fastapi_app).get(prometheus_instrument.metrics_path)
58+
assert response.status_code == status.HTTP_200_OK
59+
assert response.text

tests/test_litestar_bootstrap.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
LitestarHealthChecksInstrument,
1010
LitestarLoggingInstrument,
1111
LitestarOpenTelemetryInstrument,
12+
LitestarPrometheusInstrument,
1213
LitestarSentryInstrument,
1314
ServiceConfig,
1415
)
@@ -36,6 +37,7 @@ def test_litestar_bootstrap(service_config: ServiceConfig) -> None:
3637
path="/health/",
3738
),
3839
LitestarLoggingInstrument(logging_buffer_capacity=0),
40+
LitestarPrometheusInstrument(),
3941
],
4042
)
4143
application = bootstrapper.bootstrap()
@@ -52,3 +54,19 @@ def test_litestar_bootstrap(service_config: ServiceConfig) -> None:
5254
}
5355
finally:
5456
bootstrapper.teardown()
57+
58+
59+
def test_litestar_prometheus_bootstrap(service_config: ServiceConfig) -> None:
60+
app_config = AppConfig()
61+
prometheus_instrument = LitestarPrometheusInstrument(metrics_path="/custom-metrics-path")
62+
bootstrapper = LitestarBootstrapper(
63+
bootstrap_object=app_config,
64+
service_config=service_config,
65+
instruments=[prometheus_instrument],
66+
)
67+
application = bootstrapper.bootstrap()
68+
69+
with TestClient(app=application) as test_client:
70+
response = test_client.get(prometheus_instrument.metrics_path)
71+
assert response.status_code == status_codes.HTTP_200_OK
72+
assert response.text

0 commit comments

Comments
 (0)