Skip to content

Commit 80bc4e4

Browse files
committed
Extensibility model for requests/response metrics.
This allows applications to add labels to the metrics reported by middlewares. this is done in a few steps. 1. Create an application class that inherits from the Metric class and overrides the register_metric mehtod to register metrics with the application sepcific labels. 2. Create an application class that inherits from the middleware class and overrides the label_metric method and attach the application specific labels to the relevant metrics.
1 parent 0c41883 commit 80bc4e4

File tree

2 files changed

+170
-23
lines changed

2 files changed

+170
-23
lines changed

django_prometheus/middleware.py

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -216,15 +216,22 @@ def _method(self, request):
216216
return "<invalid method>"
217217
return m
218218

219+
def label_metric(self, metric, request, response=None, **labels):
220+
return metric.labels(**labels) if labels else metric
221+
219222
def process_request(self, request):
220223
transport = self._transport(request)
221224
method = self._method(request)
222-
self.metrics.requests_by_method.labels(method=method).inc()
223-
self.metrics.requests_by_transport.labels(transport=transport).inc()
225+
self.label_metric(self.metrics.requests_by_method, request, method=method).inc()
226+
self.label_metric(
227+
self.metrics.requests_by_transport, request, transport=transport
228+
).inc()
224229
if request.is_ajax():
225-
self.metrics.requests_ajax.inc()
230+
self.label_metric(self.metrics.requests_ajax, request).inc()
226231
content_length = int(request.META.get("CONTENT_LENGTH") or 0)
227-
self.metrics.requests_body_bytes.observe(content_length)
232+
self.label_metric(self.metrics.requests_body_bytes, request).observe(
233+
content_length
234+
)
228235
request.prometheus_after_middleware_event = Time()
229236

230237
def _get_view_name(self, request):
@@ -240,49 +247,79 @@ def process_view(self, request, view_func, *view_args, **view_kwargs):
240247
method = self._method(request)
241248
if hasattr(request, "resolver_match"):
242249
name = request.resolver_match.view_name or "<unnamed view>"
243-
self.metrics.requests_by_view_transport_method.labels(
244-
view=name, transport=transport, method=method
250+
self.label_metric(
251+
self.metrics.requests_by_view_transport_method,
252+
request,
253+
view=name,
254+
transport=transport,
255+
method=method,
245256
).inc()
246257

247258
def process_template_response(self, request, response):
248259
if hasattr(response, "template_name"):
249-
self.metrics.responses_by_templatename.labels(
250-
templatename=str(response.template_name)
260+
self.label_metric(
261+
self.metrics.responses_by_templatename,
262+
request,
263+
response=response,
264+
templatename=str(response.template_name),
251265
).inc()
252266
return response
253267

254268
def process_response(self, request, response):
255269
method = self._method(request)
256270
name = self._get_view_name(request)
257271
status = str(response.status_code)
258-
self.metrics.responses_by_status.labels(status=status).inc()
259-
self.metrics.responses_by_status_view_method.labels(
260-
status=status, view=name, method=method
272+
self.label_metric(
273+
self.metrics.responses_by_status, request, response, status=status
274+
).inc()
275+
self.label_metric(
276+
self.metrics.responses_by_status_view_method,
277+
request,
278+
response,
279+
status=status,
280+
view=name,
281+
method=method,
261282
).inc()
262283
if hasattr(response, "charset"):
263-
self.metrics.responses_by_charset.labels(
264-
charset=str(response.charset)
284+
self.label_metric(
285+
self.metrics.responses_by_charset,
286+
request,
287+
response,
288+
charset=str(response.charset),
265289
).inc()
266290
if hasattr(response, "streaming") and response.streaming:
267-
self.metrics.responses_streaming.inc()
291+
self.label_metric(self.metrics.responses_streaming, request, response).inc()
268292
if hasattr(response, "content"):
269-
self.metrics.responses_body_bytes.observe(len(response.content))
293+
self.label_metric(
294+
self.metrics.responses_body_bytes, request, response
295+
).observe(len(response.content))
270296
if hasattr(request, "prometheus_after_middleware_event"):
271-
self.metrics.requests_latency_by_view_method.labels(
272-
view=self._get_view_name(request), method=request.method
297+
self.label_metric(
298+
self.metrics.requests_latency_by_view_method,
299+
request,
300+
response,
301+
view=self._get_view_name(request),
302+
method=request.method,
273303
).observe(TimeSince(request.prometheus_after_middleware_event))
274304
else:
275-
self.metrics.requests_unknown_latency.inc()
305+
self.label_metric(
306+
self.metrics.requests_unknown_latency, request, response
307+
).inc()
276308
return response
277309

278310
def process_exception(self, request, exception):
279-
self.metrics.exceptions_by_type.labels(type=type(exception).__name__).inc()
311+
self.label_metric(
312+
self.metrics.exceptions_by_type, request, type=type(exception).__name__
313+
).inc()
280314
if hasattr(request, "resolver_match"):
281315
name = request.resolver_match.view_name or "<unnamed view>"
282-
self.metrics.exceptions_by_view.labels(view=name).inc()
316+
self.label_metric(self.metrics.exceptions_by_view, request, view=name).inc()
283317
if hasattr(request, "prometheus_after_middleware_event"):
284-
self.metrics.requests_latency_by_view_method.labels(
285-
view=self._get_view_name(request), method=request.method
318+
self.label_metric(
319+
self.metrics.requests_latency_by_view_method,
320+
request,
321+
view=self._get_view_name(request),
322+
method=request.method,
286323
).observe(TimeSince(request.prometheus_after_middleware_event))
287324
else:
288-
self.metrics.requests_unknown_latency.inc()
325+
self.label_metric(self.metrics.requests_unknown_latency, request).inc()
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from django.test import SimpleTestCase, override_settings
2+
from django_prometheus.middleware import (
3+
Metrics,
4+
PrometheusAfterMiddleware,
5+
PrometheusBeforeMiddleware,
6+
)
7+
from django_prometheus.testutils import PrometheusTestCaseMixin
8+
from testapp.helpers import get_middleware
9+
from testapp.test_middleware import M, T
10+
11+
EXTENDED_METRICS = [
12+
"django_http_requests_latency_seconds_by_view_method",
13+
"django_http_responses_total_by_status_view_method",
14+
]
15+
16+
17+
class CustomMetrics(Metrics):
18+
def register_metric(
19+
self, metric_cls, name, documentation, labelnames=tuple(), **kwargs
20+
):
21+
if name in EXTENDED_METRICS:
22+
labelnames = labelnames + ("view_type", "user_agent_type")
23+
return super(CustomMetrics, self).register_metric(
24+
metric_cls, name, documentation, labelnames=labelnames, **kwargs
25+
)
26+
27+
28+
class AppMetricsBeforeMiddleware(PrometheusBeforeMiddleware):
29+
metrics_cls = CustomMetrics
30+
31+
32+
class AppMetricsAfterMiddleware(PrometheusAfterMiddleware):
33+
metrics_cls = CustomMetrics
34+
35+
def label_metric(self, metric, request, response=None, **labels):
36+
if metric._name in EXTENDED_METRICS:
37+
labels.update({"view_type": "foo", "user_agent_type": "browser"})
38+
return super(AppMetricsAfterMiddleware, self).label_metric(
39+
metric, request, response=response, **labels
40+
)
41+
42+
43+
@override_settings(
44+
MIDDLEWARE_X=get_middleware(
45+
"testapp.test_middleware_custom_labels.AppMetricsBeforeMiddleware",
46+
"testapp.test_middleware_custom_labels.AppMetricsAfterMiddleware",
47+
)
48+
)
49+
class TestMiddlewareMetricsWithCustomLabels(PrometheusTestCaseMixin, SimpleTestCase):
50+
"""Test django_prometheus.middleware.
51+
52+
Note that counters related to exceptions can't be tested as
53+
Django's test Client only simulates requests and the exception
54+
handling flow is very different in that simulation.
55+
"""
56+
57+
def test_request_counters(self):
58+
registry = self.saveRegistry()
59+
self.client.get("/")
60+
self.client.get("/")
61+
self.client.get("/help")
62+
self.client.post("/", {"test": "data"})
63+
64+
self.assertMetricDiff(registry, 4, M("requests_before_middlewares_total"))
65+
self.assertMetricDiff(registry, 4, M("responses_before_middlewares_total"))
66+
self.assertMetricDiff(registry, 3, T("requests_total_by_method"), method="GET")
67+
self.assertMetricDiff(registry, 1, T("requests_total_by_method"), method="POST")
68+
self.assertMetricDiff(
69+
registry, 4, T("requests_total_by_transport"), transport="http"
70+
)
71+
self.assertMetricDiff(
72+
registry,
73+
2,
74+
T("requests_total_by_view_transport_method"),
75+
view="testapp.views.index",
76+
transport="http",
77+
method="GET",
78+
)
79+
self.assertMetricDiff(
80+
registry,
81+
1,
82+
T("requests_total_by_view_transport_method"),
83+
view="testapp.views.help",
84+
transport="http",
85+
method="GET",
86+
)
87+
self.assertMetricDiff(
88+
registry,
89+
1,
90+
T("requests_total_by_view_transport_method"),
91+
view="testapp.views.index",
92+
transport="http",
93+
method="POST",
94+
)
95+
self.assertMetricDiff(
96+
registry,
97+
2.0,
98+
T("responses_total_by_status_view_method"),
99+
status="200",
100+
view="testapp.views.index",
101+
method="GET",
102+
)
103+
self.assertMetricDiff(
104+
registry,
105+
1.0,
106+
T("responses_total_by_status_view_method"),
107+
status="200",
108+
view="testapp.views.help",
109+
method="GET",
110+
)

0 commit comments

Comments
 (0)