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 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

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.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

View File

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

View File

@ -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()`.