Closes #19965: Expand Prometheus metrics (#19966)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run

This commit is contained in:
Jeremy Stretch 2025-07-31 16:27:50 -04:00 committed by GitHub
parent 9a2fab1d48
commit ae55eed98f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 81 additions and 3 deletions

View File

@ -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 model insert, update, and delete counters
- Per view request counters - Per view request counters
- Per view request latency histograms - Per view request latency histograms
- REST API requests (by endpoint & method)
- GraphQL API requests
- Request body size histograms - Request body size histograms
- Response body size histograms - Response body size histograms
- Response code counters - Response code counters

40
netbox/netbox/metrics.py Normal file
View File

@ -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,
)

View File

@ -8,16 +8,20 @@ from django.core.exceptions import ImproperlyConfigured
from django.db import connection, ProgrammingError from django.db import connection, ProgrammingError
from django.db.utils import InternalError from django.db.utils import InternalError
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django_prometheus import middleware
from netbox.config import clear_config, get_config from netbox.config import clear_config, get_config
from netbox.metrics import Metrics
from netbox.views import handler_500 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.error_handlers import handle_rest_api_exception
from utilities.request import apply_request_processors from utilities.request import apply_request_processors
__all__ = ( __all__ = (
'CoreMiddleware', 'CoreMiddleware',
'MaintenanceModeMiddleware', 'MaintenanceModeMiddleware',
'PrometheusAfterMiddleware',
'PrometheusBeforeMiddleware',
'RemoteUserMiddleware', 'RemoteUserMiddleware',
) )
@ -180,6 +184,30 @@ class RemoteUserMiddleware(RemoteUserMiddleware_):
return groups 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: class MaintenanceModeMiddleware:
""" """
Middleware that checks if the application is in maintenance mode Middleware that checks if the application is in maintenance mode

View File

@ -472,9 +472,9 @@ if DEBUG:
if METRICS_ENABLED: if METRICS_ENABLED:
# If metrics are enabled, add the before & after Prometheus middleware # If metrics are enabled, add the before & after Prometheus middleware
MIDDLEWARE = [ MIDDLEWARE = [
'django_prometheus.middleware.PrometheusBeforeMiddleware', 'netbox.middleware.PrometheusBeforeMiddleware',
*MIDDLEWARE, *MIDDLEWARE,
'django_prometheus.middleware.PrometheusAfterMiddleware', 'netbox.middleware.PrometheusAfterMiddleware',
] ]
# URLs # URLs

View File

@ -23,6 +23,7 @@ __all__ = (
'get_serializer_for_model', 'get_serializer_for_model',
'get_view_name', 'get_view_name',
'is_api_request', '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 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): 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()`. Derive the view name from its associated model, if it has one. Fall back to DRF's built-in `get_view_name()`.