From ae55eed98feeb726d3fe090e0b382a7e2080336d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 31 Jul 2025 16:27:50 -0400 Subject: [PATCH] Closes #19965: Expand Prometheus metrics (#19966) --- docs/integrations/prometheus-metrics.md | 2 ++ netbox/netbox/metrics.py | 40 +++++++++++++++++++++++++ netbox/netbox/middleware.py | 30 ++++++++++++++++++- netbox/netbox/settings.py | 4 +-- netbox/utilities/api.py | 8 +++++ 5 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 netbox/netbox/metrics.py diff --git a/docs/integrations/prometheus-metrics.md b/docs/integrations/prometheus-metrics.md index 006ff16a4..3f0e0ebda 100644 --- a/docs/integrations/prometheus-metrics.md +++ b/docs/integrations/prometheus-metrics.md @@ -11,6 +11,8 @@ NetBox makes use of the [django-prometheus](https://github.com/korfuri/django-pr - Per model insert, update, and delete counters - Per view request counters - Per view request latency histograms +- REST API requests (by endpoint & method) +- GraphQL API requests - Request body size histograms - Response body size histograms - Response code counters diff --git a/netbox/netbox/metrics.py b/netbox/netbox/metrics.py new file mode 100644 index 000000000..59619d34a --- /dev/null +++ b/netbox/netbox/metrics.py @@ -0,0 +1,40 @@ +from django_prometheus.conf import NAMESPACE +from django_prometheus import middleware +from prometheus_client import Counter + +__all__ = ( + 'Metrics', +) + + +class Metrics(middleware.Metrics): + """ + Expand the stock Metrics class from django_prometheus to add our own counters. + """ + + def register(self): + super().register() + + # REST API metrics + self.rest_api_requests = self.register_metric( + Counter, + "rest_api_requests_total_by_method", + "Count of total REST API requests by method", + ["method"], + namespace=NAMESPACE, + ) + self.rest_api_requests_by_view_method = self.register_metric( + Counter, + "rest_api_requests_total_by_view_method", + "Count of REST API requests by view & method", + ["view", "method"], + namespace=NAMESPACE, + ) + + # GraphQL API metrics + self.graphql_api_requests = self.register_metric( + Counter, + "graphql_api_requests_total", + "Count of total GraphQL API requests", + namespace=NAMESPACE, + ) diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index 145141615..66c980778 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -8,16 +8,20 @@ from django.core.exceptions import ImproperlyConfigured from django.db import connection, ProgrammingError from django.db.utils import InternalError from django.http import Http404, HttpResponseRedirect +from django_prometheus import middleware from netbox.config import clear_config, get_config +from netbox.metrics import Metrics from netbox.views import handler_500 -from utilities.api import is_api_request +from utilities.api import is_api_request, is_graphql_request from utilities.error_handlers import handle_rest_api_exception from utilities.request import apply_request_processors __all__ = ( 'CoreMiddleware', 'MaintenanceModeMiddleware', + 'PrometheusAfterMiddleware', + 'PrometheusBeforeMiddleware', 'RemoteUserMiddleware', ) @@ -180,6 +184,30 @@ class RemoteUserMiddleware(RemoteUserMiddleware_): return groups +class PrometheusBeforeMiddleware(middleware.PrometheusBeforeMiddleware): + metrics_cls = Metrics + + +class PrometheusAfterMiddleware(middleware.PrometheusAfterMiddleware): + metrics_cls = Metrics + + def process_response(self, request, response): + response = super().process_response(request, response) + + # Increment REST API request counters + if is_api_request(request): + method = self._method(request) + name = self._get_view_name(request) + self.label_metric(self.metrics.rest_api_requests, request, method=method).inc() + self.label_metric(self.metrics.rest_api_requests_by_view_method, request, method=method, view=name).inc() + + # Increment GraphQL API request counters + elif is_graphql_request(request): + self.metrics.graphql_api_requests.inc() + + return response + + class MaintenanceModeMiddleware: """ Middleware that checks if the application is in maintenance mode diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 35900f60e..c9eed75e1 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -472,9 +472,9 @@ if DEBUG: if METRICS_ENABLED: # If metrics are enabled, add the before & after Prometheus middleware MIDDLEWARE = [ - 'django_prometheus.middleware.PrometheusBeforeMiddleware', + 'netbox.middleware.PrometheusBeforeMiddleware', *MIDDLEWARE, - 'django_prometheus.middleware.PrometheusAfterMiddleware', + 'netbox.middleware.PrometheusAfterMiddleware', ] # URLs diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 6793c0526..cc13e1b13 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -23,6 +23,7 @@ __all__ = ( 'get_serializer_for_model', 'get_view_name', 'is_api_request', + 'is_graphql_request', ) @@ -60,6 +61,13 @@ def is_api_request(request): return request.path_info.startswith(api_path) and request.content_type == HTTP_CONTENT_TYPE_JSON +def is_graphql_request(request): + """ + Return True of the request is being made via the GraphQL API. + """ + return request.path_info == reverse('graphql') and request.content_type == HTTP_CONTENT_TYPE_JSON + + def get_view_name(view): """ Derive the view name from its associated model, if it has one. Fall back to DRF's built-in `get_view_name()`.