Skip to content

Commit 5dd2777

Browse files
authored
Merge pull request #193 from asherf/labels2
Extensibility model for requests/response metrics.
2 parents 0c41883 + b5160d4 commit 5dd2777

File tree

3 files changed

+193
-25
lines changed

3 files changed

+193
-25
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,16 @@ that will export the metrics (replace myapp by your project name).
204204

205205
Then we inject the wrapper in settings:
206206

207-
```python
207+
```python
208208
ROOT_URLCONF = "graphite.urls_prometheus_wrapper"
209209
```
210+
211+
## Adding custom labels to middleware (request/response) metrics
212+
213+
You can add application specific labels to metrics reported by the django-prometheus middleware.
214+
This involves extending the classes defined in middleware.py.
215+
216+
* Extend the Metrics class and override the `register_metric` method to add the application specific labels.
217+
* Extend middleware classes, set the metrics_cls class attribute to the the extended metric class and override the label_metric method to attach custom metrics.
218+
219+
See implementation example in [the test app](django_prometheus/tests/end2end/testapp/test_middleware_custom_labels.py#L19-L46)

django_prometheus/middleware.py

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
)
2626

2727

28-
class Metrics:
28+
class Metrics(object):
2929
_instance = None
3030

3131
@classmethod
@@ -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: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from prometheus_client import REGISTRY
2+
from prometheus_client.metrics import MetricWrapperBase
3+
4+
from django.test import SimpleTestCase, override_settings
5+
from django_prometheus.middleware import (
6+
Metrics,
7+
PrometheusAfterMiddleware,
8+
PrometheusBeforeMiddleware,
9+
)
10+
from django_prometheus.testutils import PrometheusTestCaseMixin
11+
from testapp.helpers import get_middleware
12+
from testapp.test_middleware import M, T
13+
14+
EXTENDED_METRICS = [
15+
M("requests_latency_seconds_by_view_method"),
16+
M("responses_total_by_status_view_method"),
17+
]
18+
19+
20+
class CustomMetrics(Metrics):
21+
def register_metric(
22+
self, metric_cls, name, documentation, labelnames=tuple(), **kwargs
23+
):
24+
if name in EXTENDED_METRICS:
25+
labelnames.extend(("view_type", "user_agent_type"))
26+
return super(CustomMetrics, self).register_metric(
27+
metric_cls, name, documentation, labelnames=labelnames, **kwargs
28+
)
29+
30+
31+
class AppMetricsBeforeMiddleware(PrometheusBeforeMiddleware):
32+
metrics_cls = CustomMetrics
33+
34+
35+
class AppMetricsAfterMiddleware(PrometheusAfterMiddleware):
36+
metrics_cls = CustomMetrics
37+
38+
def label_metric(self, metric, request, response=None, **labels):
39+
new_labels = labels
40+
if metric._name in EXTENDED_METRICS:
41+
new_labels = {"view_type": "foo", "user_agent_type": "browser"}
42+
new_labels.update(labels)
43+
return super(AppMetricsAfterMiddleware, self).label_metric(
44+
metric, request, response=response, **new_labels
45+
)
46+
47+
48+
@override_settings(
49+
MIDDLEWARE=get_middleware(
50+
"testapp.test_middleware_custom_labels.AppMetricsBeforeMiddleware",
51+
"testapp.test_middleware_custom_labels.AppMetricsAfterMiddleware",
52+
)
53+
)
54+
class TestMiddlewareMetricsWithCustomLabels(PrometheusTestCaseMixin, SimpleTestCase):
55+
@classmethod
56+
def setUpClass(cls):
57+
super(TestMiddlewareMetricsWithCustomLabels, cls).setUpClass()
58+
# Allow CustomMetrics to be used
59+
for metric in Metrics._instance.__dict__.values():
60+
if isinstance(metric, MetricWrapperBase):
61+
REGISTRY.unregister(metric)
62+
Metrics._instance = None
63+
64+
def test_request_counters(self):
65+
registry = self.saveRegistry()
66+
self.client.get("/")
67+
self.client.get("/")
68+
self.client.get("/help")
69+
self.client.post("/", {"test": "data"})
70+
71+
self.assertMetricDiff(registry, 4, M("requests_before_middlewares_total"))
72+
self.assertMetricDiff(registry, 4, M("responses_before_middlewares_total"))
73+
self.assertMetricDiff(registry, 3, T("requests_total_by_method"), method="GET")
74+
self.assertMetricDiff(registry, 1, T("requests_total_by_method"), method="POST")
75+
self.assertMetricDiff(
76+
registry, 4, T("requests_total_by_transport"), transport="http"
77+
)
78+
self.assertMetricDiff(
79+
registry,
80+
2,
81+
T("requests_total_by_view_transport_method"),
82+
view="testapp.views.index",
83+
transport="http",
84+
method="GET",
85+
)
86+
self.assertMetricDiff(
87+
registry,
88+
1,
89+
T("requests_total_by_view_transport_method"),
90+
view="testapp.views.help",
91+
transport="http",
92+
method="GET",
93+
)
94+
self.assertMetricDiff(
95+
registry,
96+
1,
97+
T("requests_total_by_view_transport_method"),
98+
view="testapp.views.index",
99+
transport="http",
100+
method="POST",
101+
)
102+
self.assertMetricDiff(
103+
registry,
104+
2.0,
105+
T("responses_total_by_status_view_method"),
106+
status="200",
107+
view="testapp.views.index",
108+
method="GET",
109+
view_type="foo",
110+
user_agent_type="browser",
111+
)
112+
self.assertMetricDiff(
113+
registry,
114+
1.0,
115+
T("responses_total_by_status_view_method"),
116+
status="200",
117+
view="testapp.views.help",
118+
method="GET",
119+
view_type="foo",
120+
user_agent_type="browser",
121+
)

0 commit comments

Comments
 (0)