|
| 1 | +import os |
| 2 | +from typing import Any |
| 3 | + |
| 4 | +from django.conf import settings |
| 5 | +from django.http import HttpRequest, HttpResponse |
| 6 | +from prometheus_client import ( |
| 7 | + CONTENT_TYPE_LATEST, |
| 8 | + Counter, |
| 9 | + Gauge, |
| 10 | + Histogram, |
| 11 | + generate_latest, |
| 12 | +) |
| 13 | + |
| 14 | +from studio.middleware import get_db_pool_stats |
| 15 | + |
| 16 | +DB_POOL_ENABLED = Gauge( |
| 17 | + "django_db_pool_enabled", |
| 18 | + "Whether Django database connection pooling is enabled.", |
| 19 | + ["alias", "pod", "pid", "pool_id"], |
| 20 | +) |
| 21 | +DB_POOL_OPENED = Gauge( |
| 22 | + "django_db_pool_opened", |
| 23 | + "Whether the Django database connection pool has been opened in this process.", |
| 24 | + ["alias", "pod", "pid", "pool_id"], |
| 25 | +) |
| 26 | +DB_POOL_SIZE = Gauge( |
| 27 | + "django_db_pool_size", |
| 28 | + "Current number of connections in the Django database connection pool.", |
| 29 | + ["alias", "pod", "pid", "pool_id"], |
| 30 | +) |
| 31 | +DB_POOL_AVAILABLE = Gauge( |
| 32 | + "django_db_pool_available", |
| 33 | + "Current number of available connections in the Django database connection pool.", |
| 34 | + ["alias", "pod", "pid", "pool_id"], |
| 35 | +) |
| 36 | +DB_POOL_MIN = Gauge( |
| 37 | + "django_db_pool_min", |
| 38 | + "Configured minimum number of connections in the Django database connection pool.", |
| 39 | + ["alias", "pod", "pid", "pool_id"], |
| 40 | +) |
| 41 | +DB_POOL_MAX = Gauge( |
| 42 | + "django_db_pool_max", |
| 43 | + "Configured maximum number of connections in the Django database connection pool.", |
| 44 | + ["alias", "pod", "pid", "pool_id"], |
| 45 | +) |
| 46 | +DB_POOL_REQUESTS_WAITING = Gauge( |
| 47 | + "django_db_pool_requests_waiting", |
| 48 | + "Current number of requests waiting for a Django database connection pool slot.", |
| 49 | + ["alias", "pod", "pid", "pool_id"], |
| 50 | +) |
| 51 | +DB_POOL_REQUESTS_NUM = Gauge( |
| 52 | + "django_db_pool_requests_total", |
| 53 | + "Total number of requests served by the Django database connection pool in this process.", |
| 54 | + ["alias", "pod", "pid", "pool_id"], |
| 55 | +) |
| 56 | +DB_POOL_REQUESTS_ERRORS = Gauge( |
| 57 | + "django_db_pool_requests_errors_total", |
| 58 | + "Total number of Django database connection pool request errors in this process.", |
| 59 | + ["alias", "pod", "pid", "pool_id"], |
| 60 | +) |
| 61 | +HTTP_REQUESTS_TOTAL = Counter( |
| 62 | + "django_http_requests_total", |
| 63 | + "Total number of Django HTTP requests handled by this process.", |
| 64 | + ["method", "route", "status_code", "pod"], |
| 65 | +) |
| 66 | +HTTP_REQUEST_DURATION_SECONDS = Histogram( |
| 67 | + "django_http_request_duration_seconds", |
| 68 | + "Django HTTP request duration in seconds for this process.", |
| 69 | + ["method", "route", "status_code", "pod"], |
| 70 | +) |
| 71 | +HTTP_REQUEST_EXCEPTIONS_TOTAL = Counter( |
| 72 | + "django_http_request_exceptions_total", |
| 73 | + "Total number of Django HTTP requests that raised an exception in this process.", |
| 74 | + ["method", "route", "exception", "pod"], |
| 75 | +) |
| 76 | + |
| 77 | + |
| 78 | +def _metric_value(stats: dict[str, Any], key: str) -> float: |
| 79 | + value = stats.get(key) |
| 80 | + if isinstance(value, bool): |
| 81 | + return 1.0 if value else 0.0 |
| 82 | + if isinstance(value, int | float): |
| 83 | + return float(value) |
| 84 | + return 0.0 |
| 85 | + |
| 86 | + |
| 87 | +def update_db_pool_metrics() -> None: |
| 88 | + stats = get_db_pool_stats() |
| 89 | + labels = { |
| 90 | + "alias": str(stats.get("alias", "")), |
| 91 | + "pod": str(stats.get("pod", "")), |
| 92 | + "pid": str(stats.get("pid", "")), |
| 93 | + "pool_id": str(stats.get("pool_id", "")), |
| 94 | + } |
| 95 | + |
| 96 | + DB_POOL_ENABLED.labels(**labels).set(_metric_value(stats, "enabled")) |
| 97 | + DB_POOL_OPENED.labels(**labels).set(_metric_value(stats, "opened")) |
| 98 | + DB_POOL_SIZE.labels(**labels).set(_metric_value(stats, "pool_size")) |
| 99 | + DB_POOL_AVAILABLE.labels(**labels).set(_metric_value(stats, "pool_available")) |
| 100 | + DB_POOL_MIN.labels(**labels).set(_metric_value(stats, "pool_min")) |
| 101 | + DB_POOL_MAX.labels(**labels).set(_metric_value(stats, "pool_max")) |
| 102 | + DB_POOL_REQUESTS_WAITING.labels(**labels).set(_metric_value(stats, "requests_waiting")) |
| 103 | + DB_POOL_REQUESTS_NUM.labels(**labels).set(_metric_value(stats, "requests_num")) |
| 104 | + DB_POOL_REQUESTS_ERRORS.labels(**labels).set(_metric_value(stats, "requests_errors")) |
| 105 | + |
| 106 | + |
| 107 | +def get_http_route_label(request: HttpRequest) -> str: |
| 108 | + resolver_match = getattr(request, "resolver_match", None) |
| 109 | + if resolver_match is None: |
| 110 | + return "unknown" |
| 111 | + view_name = getattr(resolver_match, "view_name", "") |
| 112 | + if isinstance(view_name, str) and view_name: |
| 113 | + return view_name |
| 114 | + route = getattr(resolver_match, "route", "") |
| 115 | + if isinstance(route, str) and route: |
| 116 | + return route |
| 117 | + return "unknown" |
| 118 | + |
| 119 | + |
| 120 | +def _pod_label() -> str: |
| 121 | + return os.environ.get("HOSTNAME", "") |
| 122 | + |
| 123 | + |
| 124 | +def record_http_request_metrics(request: HttpRequest, response: HttpResponse, duration_seconds: float) -> None: |
| 125 | + labels = { |
| 126 | + "method": request.method, |
| 127 | + "route": get_http_route_label(request), |
| 128 | + "status_code": str(response.status_code), |
| 129 | + "pod": _pod_label(), |
| 130 | + } |
| 131 | + HTTP_REQUESTS_TOTAL.labels(**labels).inc() |
| 132 | + HTTP_REQUEST_DURATION_SECONDS.labels(**labels).observe(duration_seconds) |
| 133 | + |
| 134 | + |
| 135 | +def record_http_exception_metrics(request: HttpRequest, exception: Exception, duration_seconds: float) -> None: |
| 136 | + HTTP_REQUEST_EXCEPTIONS_TOTAL.labels( |
| 137 | + method=request.method, |
| 138 | + route=get_http_route_label(request), |
| 139 | + exception=exception.__class__.__name__, |
| 140 | + pod=_pod_label(), |
| 141 | + ).inc() |
| 142 | + labels = { |
| 143 | + "method": request.method, |
| 144 | + "route": get_http_route_label(request), |
| 145 | + "status_code": "exception", |
| 146 | + "pod": _pod_label(), |
| 147 | + } |
| 148 | + HTTP_REQUEST_DURATION_SECONDS.labels(**labels).observe(duration_seconds) |
| 149 | + |
| 150 | + |
| 151 | +def metrics_view(request: HttpRequest) -> HttpResponse: |
| 152 | + if not settings.PROMETHEUS_METRICS_ENABLED: |
| 153 | + return HttpResponse(status=404) |
| 154 | + |
| 155 | + update_db_pool_metrics() |
| 156 | + return HttpResponse(generate_latest(), content_type=CONTENT_TYPE_LATEST) |
0 commit comments