Skip to content

Commit 9d62515

Browse files
committed
Add support OpenMetrics format in default endpoints
1 parent 146f2eb commit 9d62515

File tree

6 files changed

+123
-6
lines changed

6 files changed

+123
-6
lines changed

src/asgi_monitor/integrations/fastapi.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ class MetricsConfig(BaseMetricsConfig):
3737
include_metrics_endpoint: bool = field(default=True)
3838
"""Whether to include a /metrics endpoint."""
3939

40+
openmetrics_format: bool = field(default=False)
41+
"""A flag indicating whether to generate metrics in OpenMetrics format."""
42+
4043

4144
@dataclass(slots=True, frozen=True)
4245
class TracingConfig(BaseTracingConfig):
@@ -92,6 +95,7 @@ def setup_metrics(app: FastAPI, config: MetricsConfig) -> None:
9295
)
9396
if config.include_metrics_endpoint:
9497
app.state.metrics_registry = config.registry
98+
app.state.openmetrics_format = config.openmetrics_format
9599
app.add_route(
96100
path="/metrics",
97101
route=get_metrics,

src/asgi_monitor/integrations/litestar.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
154154
@get(path="/metrics", summary="Get Prometheus metrics", include_in_schema=True)
155155
async def get_metrics(request: Request) -> Response:
156156
registry = request.app.state.metrics_registry
157-
response = get_latest_metrics(registry, openmetrics_format=False)
157+
openmetrics_format = request.app.state.openmetrics_format
158+
response = get_latest_metrics(registry, openmetrics_format=openmetrics_format)
158159
return Response(
159160
content=response.payload,
160161
status_code=response.status_code,
@@ -195,14 +196,16 @@ def build_metrics_middleware(config: MetricsConfig) -> DefineMiddleware:
195196
)
196197

197198

198-
def add_metrics_endpoint(app: Litestar, registry: CollectorRegistry) -> None:
199+
def add_metrics_endpoint(app: Litestar, registry: CollectorRegistry, *, openmetrics_format: bool = False) -> None:
199200
"""
200201
Add CollectorRegistry in state and register /metrics endpoint.
201202
202203
:param Litestar app: The Litestar application instance.
203204
:param CollectorRegistry registry: The registry for the metrics.
205+
:param bool openmetrics_format: A flag indicating whether to generate metrics in OpenMetrics format.
204206
:returns: None
205207
"""
206208

207209
app.state.metrics_registry = registry
210+
app.state.openmetrics_format = openmetrics_format
208211
app.register(get_metrics)

src/asgi_monitor/integrations/starlette.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ class MetricsConfig(BaseMetricsConfig):
9898
include_metrics_endpoint: bool = field(default=True)
9999
"""Whether to include a /metrics endpoint."""
100100

101+
openmetrics_format: bool = field(default=False)
102+
"""A flag indicating whether to generate metrics in OpenMetrics format."""
103+
101104

102105
class TracingMiddleware:
103106
__slots__ = ("app", "open_telemetry_middleware")
@@ -179,7 +182,8 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -
179182

180183
async def get_metrics(request: Request) -> Response:
181184
registry = request.app.state.metrics_registry
182-
response = get_latest_metrics(registry, openmetrics_format=False)
185+
openmetrics_format = request.app.state.openmetrics_format
186+
response = get_latest_metrics(registry, openmetrics_format=openmetrics_format)
183187
return Response(
184188
content=response.payload,
185189
status_code=response.status_code,
@@ -220,6 +224,7 @@ def setup_metrics(app: Starlette, config: MetricsConfig) -> None:
220224
)
221225
if config.include_metrics_endpoint:
222226
app.state.metrics_registry = config.registry
227+
app.state.openmetrics_format = config.openmetrics_format
223228
app.add_route(
224229
path="/metrics",
225230
route=get_metrics,

tests/integration/fastapi/test_middlewares.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ async def index() -> dict:
1919

2020
async def test_metrics() -> None:
2121
# Arrange
22+
expected_content_type = "text/plain; version=0.0.4; charset=utf-8"
2223
app = FastAPI()
23-
metrics_config = MetricsConfig(app_name="test", include_metrics_endpoint=True, include_trace_exemplar=False)
24+
metrics_config = MetricsConfig(
25+
app_name="test",
26+
include_metrics_endpoint=True,
27+
include_trace_exemplar=False,
28+
openmetrics_format=False,
29+
)
2430
setup_metrics(app=app, config=metrics_config)
2531

2632
# Act
@@ -29,6 +35,7 @@ async def test_metrics() -> None:
2935

3036
# Assert
3137
assert response.status_code == 200
38+
assert response.headers["content-type"] == expected_content_type
3239
assert_that(response.content.decode()).contains(
3340
'fastapi_app_info{app_name="test"} 1.0',
3441
'fastapi_requests_total{app_name="test",method="GET",path="/metrics"} 1.0',
@@ -87,6 +94,39 @@ async def test_metrics_with_tracing() -> None:
8794
assert_that(metrics.payload.decode()).contains('fastapi_app_info{app_name="test"} 1.0')
8895

8996

97+
async def test_metrics_openmetrics_with_tracing() -> None:
98+
# Arrange
99+
expected_content_type = "application/openmetrics-text; version=1.0.0; charset=utf-8"
100+
app = FastAPI()
101+
app.include_router(router)
102+
trace_config, _ = build_fastapi_tracing_config()
103+
metrics_config = MetricsConfig(
104+
app_name="test",
105+
include_metrics_endpoint=True,
106+
include_trace_exemplar=True,
107+
openmetrics_format=True,
108+
)
109+
110+
setup_metrics(app=app, config=metrics_config)
111+
setup_tracing(app=app, config=trace_config)
112+
113+
# Act
114+
async with fastapi_app(app) as client:
115+
response = client.get("/")
116+
metrics = client.get("/metrics")
117+
118+
# Assert
119+
assert response.status_code == 200
120+
assert metrics.status_code == 200
121+
assert metrics.headers["content-type"] == expected_content_type
122+
pattern = (
123+
r"fastapi_request_duration_seconds_bucket\{"
124+
r'app_name="test",le="([\d.]+)",method="GET",path="\/"}\ 1.0 # \{TraceID="(\w+)"\} (\d+\.\d+) (\d+\.\d+)'
125+
)
126+
assert_that(metrics.content.decode()).matches(pattern)
127+
assert_that(metrics.content.decode()).contains('fastapi_app_info{app_name="test"} 1.0')
128+
129+
90130
async def test_metrics_get_path() -> None:
91131
# Arrange
92132
app = FastAPI()

tests/integration/litestar/test_middlewares.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,18 @@ async def test_tracing() -> None:
6767

6868
async def test_metrics() -> None:
6969
# Arrange
70+
expected_content_type = "text/plain; version=0.0.4; charset=utf-8"
7071
metrics_config = MetricsConfig(app_name="test", include_trace_exemplar=False)
7172
app = Litestar(middleware=[build_metrics_middleware(metrics_config)])
72-
add_metrics_endpoint(app, metrics_config.registry)
73+
add_metrics_endpoint(app, metrics_config.registry, openmetrics_format=False)
7374

7475
# Act
7576
async with litestar_app(app) as client:
7677
response = client.get("/metrics")
7778

7879
# Assert
7980
assert response.status_code == 200
81+
assert response.headers["content-type"] == expected_content_type
8082
assert_that(response.content.decode()).contains(
8183
'litestar_app_info{app_name="test"} 1.0',
8284
'litestar_requests_total{app_name="test",method="GET",path="/metrics"} 1.0',
@@ -129,3 +131,28 @@ async def test_metrics_with_tracing() -> None:
129131
r'app_name="test",le="([\d.]+)",method="GET",path="\/"}\ 1.0 # \{TraceID="(\w+)"\} (\d+\.\d+) (\d+\.\d+)'
130132
)
131133
assert_that(metrics.payload.decode()).matches(pattern)
134+
135+
136+
async def test_metrics_openmetrics_with_tracing() -> None:
137+
# Arrange
138+
expected_content_type = "application/openmetrics-text; version=1.0.0; charset=utf-8"
139+
trace_config, _ = build_litestar_tracing_config()
140+
metrics_config = MetricsConfig(app_name="test", include_trace_exemplar=True)
141+
middlewares = [build_tracing_middleware(trace_config), build_metrics_middleware(metrics_config)]
142+
app = Litestar([index], middleware=middlewares)
143+
add_metrics_endpoint(app, metrics_config.registry, openmetrics_format=True)
144+
145+
# Act
146+
async with litestar_app(app) as client:
147+
response = client.get("/")
148+
metrics = client.get("/metrics")
149+
150+
# Assert
151+
assert response.status_code == 200
152+
assert metrics.status_code == 200
153+
assert metrics.headers["content-type"] == expected_content_type
154+
pattern = (
155+
r"litestar_request_duration_seconds_bucket\{"
156+
r'app_name="test",le="([\d.]+)",method="GET",path="\/"}\ 1.0 # \{TraceID="(\w+)"\} (\d+\.\d+) (\d+\.\d+)'
157+
)
158+
assert_that(metrics.content.decode()).matches(pattern)

tests/integration/starlette/test_middlewares.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,14 @@ async def test_tracing_partial_match() -> None:
135135

136136
async def test_metrics() -> None:
137137
# Arrange
138+
expected_content_type = "text/plain; version=0.0.4; charset=utf-8"
138139
app = Starlette()
139-
metrics_config = MetricsConfig(app_name="test", include_metrics_endpoint=True, include_trace_exemplar=False)
140+
metrics_config = MetricsConfig(
141+
app_name="test",
142+
include_metrics_endpoint=True,
143+
include_trace_exemplar=False,
144+
openmetrics_format=False,
145+
)
140146
setup_metrics(app=app, config=metrics_config)
141147

142148
# Act
@@ -145,6 +151,7 @@ async def test_metrics() -> None:
145151

146152
# Assert
147153
assert response.status_code == 200
154+
assert response.headers["content-type"] == expected_content_type
148155
assert_that(response.content.decode()).contains(
149156
'starlette_app_info{app_name="test"} 1.0',
150157
'starlette_requests_total{app_name="test",method="GET",path="/metrics"} 1.0',
@@ -201,3 +208,34 @@ async def test_metrics_with_tracing() -> None:
201208
r'app_name="test",le="([\d.]+)",method="GET",path="\/"}\ 1.0 # \{TraceID="(\w+)"\} (\d+\.\d+) (\d+\.\d+)'
202209
)
203210
assert_that(metrics.payload.decode()).matches(pattern)
211+
212+
213+
async def test_metrics_openmetrics_with_tracing() -> None:
214+
# Arrange
215+
expected_content_type = "application/openmetrics-text; version=1.0.0; charset=utf-8"
216+
trace_config, _ = build_starlette_tracing_config()
217+
metrics_config = MetricsConfig(
218+
app_name="test",
219+
include_metrics_endpoint=True,
220+
include_trace_exemplar=True,
221+
openmetrics_format=True,
222+
)
223+
app = Starlette(routes=[Route("/", endpoint=index, methods=["GET"])])
224+
225+
setup_metrics(app=app, config=metrics_config)
226+
setup_tracing(app=app, config=trace_config)
227+
228+
# Act
229+
async with starlette_app(app) as client:
230+
response = client.get("/")
231+
metrics = client.get("/metrics")
232+
233+
# Assert
234+
assert response.status_code == 200
235+
assert metrics.status_code == 200
236+
assert metrics.headers["content-type"] == expected_content_type
237+
pattern = (
238+
r"starlette_request_duration_seconds_bucket\{"
239+
r'app_name="test",le="([\d.]+)",method="GET",path="\/"}\ 1.0 # \{TraceID="(\w+)"\} (\d+\.\d+) (\d+\.\d+)'
240+
)
241+
assert_that(metrics.content.decode()).matches(pattern)

0 commit comments

Comments
 (0)