Custom Prometheus Metrics
Django Hurricane includes an integrated Prometheus metrics exporter running directly inside its gateway and probe application. Beyond the built-in system metrics (request counts, latency averages, and queue lengths), Hurricane provides a declarative framework for defining and exporting custom application, operational, and business metrics.
Hurricane integrates seamlessly with the official prometheus_client Python library (documentation), allowing you to combine Hurricane's high-level metric primitives with low-level labeled Prometheus collectors.
The 5 Metric Primitives
Hurricane provides five base metric primitives in hurricane.metrics:
| Metric Class | Prometheus Type | When to Use | Update Mechanism |
|---|---|---|---|
CounterMetric | Counter | Monotonically increasing events (e.g. user signups, orders placed, failed logins). | MyCounter.increment(amount=1) |
StoredMetric | Gauge | Values that fluctuate over time (e.g. active users, worker threads, cache size). | MyGauge.set(value) |
CalculatedMetric | Gauge / Computed | Values computed on-demand when /metrics is scraped (e.g. database query counts). | Dynamic inside get_value(*args) |
AverageMetric | Summary / Average | Running arithmetic mean updated per request (e.g. average basket value, cart size). | MyAverage.add_value(val) |
ObservedMetric | Histogram | Latency distributions and percentile buckets (e.g. external payment gateway response times). | MyHistogram.observe(val) |
Developing Business & Operational Metrics: Code Examples
Create a dedicated metrics.py inside your Django application:
from django.core.cache import cache
from prometheus_client import Counter, Histogram
from hurricane.metrics import (
AverageMetric,
CalculatedMetric,
CounterMetric,
ObservedMetric,
StoredMetric,
)
from hurricane.metrics.registry import registry
from myapp.models import Order, UserProfile
# ==============================================================================
# 1. Business Metrics: Open Orders & Financial Pipeline
# ==============================================================================
class OpenOrdersMetric(CalculatedMetric):
"""
Computes total open orders requiring fulfillment.
Demonstrates caching pattern to avoid hammering database during frequent Prometheus scrapes.
"""
code = "ecommerce_open_orders_total"
description = "Current number of open orders requiring warehouse fulfillment"
@classmethod
def get_value(cls, *args):
# Cache count for 15 seconds so multiple scraper pods or rapid scrapes don't overload DB
def fetch_count():
return Order.objects.filter(status__in=["pending", "processing"]).count()
return cache.get_or_set("metric_open_orders_count", fetch_count, timeout=15)
class HighValueOrdersMetric(CalculatedMetric):
"""Orders with total value exceeding $1,000."""
code = "ecommerce_high_value_orders_pending"
description = "Number of pending high-priority orders above $1,000"
@classmethod
def get_value(cls, *args):
return Order.objects.filter(status="processing", total_amount__gte=1000).count()
# ==============================================================================
# 2. Business Metrics: User Segmentation & Account Attributes
# ==============================================================================
class EnterpriseUsersMetric(CalculatedMetric):
"""Tracks count of active enterprise-tier subscribers."""
code = "app_users_enterprise_tier_count"
description = "Active accounts on the enterprise subscription tier"
@classmethod
def get_value(cls, *args):
return UserProfile.objects.filter(is_active=True, tier="enterprise").count()
class PendingVerificationUsersMetric(CalculatedMetric):
"""Tracks onboarding friction by counting users pending KYC or email verification."""
code = "app_users_pending_verification_count"
description = "Accounts awaiting KYC or identity verification"
@classmethod
def get_value(cls, *args):
return UserProfile.objects.filter(is_active=True, kyc_verified=False).count()
# ==============================================================================
# 3. Operational Metrics: Multi-Dimensional API Status Counters (200 vs 400 vs 500)
# ==============================================================================
# Using the official prometheus_client Counter with multidimensional labels
# Docs: https://prometheus.github.io/client_python/instrumenting/labels/
EXTERNAL_API_REQUESTS = Counter(
"external_api_http_requests_total",
"Total outgoing HTTP requests to external partner APIs partitioned by target and HTTP status",
labelnames=["service", "method", "status_class"],
)
DJANGO_HTTP_REQUESTS = Counter(
"django_http_requests_total",
"Total HTTP requests handled by Django grouped by endpoint, method, and HTTP status code",
labelnames=["endpoint", "method", "status_code"],
)
# ==============================================================================
# 4. Latency Distribution & Moving Averages
# ==============================================================================
class PaymentGatewayLatencyMetric(ObservedMetric):
code = "ecommerce_payment_latency_seconds"
description = "Response time distribution for third-party payment processing (Stripe/PayPal)"
prometheus = Histogram(
"ecommerce_payment_latency_seconds",
"Payment gateway latency in seconds",
buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0),
)
class AverageOrderValueMetric(AverageMetric):
code = "ecommerce_order_value_average_dollars"
description = "Running average dollar amount per completed order"
# Register Hurricane metrics
registry.register(OpenOrdersMetric)
registry.register(HighValueOrdersMetric)
registry.register(EnterpriseUsersMetric)
registry.register(PendingVerificationUsersMetric)
registry.register(PaymentGatewayLatencyMetric)
registry.register(AverageOrderValueMetric)
Tracking API Status Codes via Middleware
To record all incoming HTTP traffic and classify responses by status code (200, 400, 500, etc.), implement a lightweight Django middleware:
import time
from myapp.metrics import DJANGO_HTTP_REQUESTS
class PrometheusApiMetricsMiddleware:
"""
Middleware that records HTTP status codes and endpoint metrics
into Prometheus counters with dimensional labels.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# Sanitize path to prevent high-cardinality label explosion with raw UUIDs/IDs
# E.g. /api/orders/123 -> /api/orders
url_name = getattr(request.resolver_match, "url_name", None) if request.resolver_match else None
endpoint = url_name or request.path[:32]
DJANGO_HTTP_REQUESTS.labels(
endpoint=endpoint,
method=request.method,
status_code=str(response.status_code),
).inc()
return response
Add the middleware to settings.py:
MIDDLEWARE = [
# Add near the top to capture all responses
"myapp.middleware.PrometheusApiMetricsMiddleware",
# ... other middlewares
]
Instrumenting Outbound API Service Calls (2xx vs. 4xx vs. 5xx)
When calling external microservices or third-party APIs (Stripe, Twilio, SendGrid), record the result using status class labels:
import time
import requests
from myapp.metrics import EXTERNAL_API_REQUESTS, PaymentGatewayLatencyMetric
def process_external_payment(amount: float, customer_id: str) -> dict:
url = "https://api.payments.example.com/v1/charges"
start_time = time.perf_counter()
try:
response = requests.post(url, json={"amount": amount, "customer": customer_id}, timeout=3.0)
status_class = f"{response.status_code // 100}xx"
status_code = response.status_code
data = response.json()
except requests.exceptions.Timeout:
status_class = "timeout"
data = {"error": "Payment service timed out"}
except requests.exceptions.RequestException:
status_class = "network_error"
data = {"error": "Payment network connection failed"}
finally:
duration = time.perf_counter() - start_time
# Record metrics
EXTERNAL_API_REQUESTS.labels(
service="payments_gateway",
method="POST",
status_class=status_class,
).inc()
PaymentGatewayLatencyMetric.observe(duration)
return data
Updating Metrics via Django Signals
Use Django signals to reactively adjust gauges and counters when models change:
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import Order
from myapp.metrics import AverageOrderValueMetric
@receiver(post_save, sender=Order)
def on_order_created(sender, instance, created, **kwargs):
if created and instance.status == "completed":
# Update running transaction average
AverageOrderValueMetric.add_value(float(instance.total_amount))
Scrape Output Example
Once registered, all custom and standard Prometheus client metrics are exported on /metrics (probe port 8001):
curl -s http://127.0.0.1:8001/metrics | grep -E "ecommerce|app_users|django_http|external_api"
Exposition Output:
# HELP ecommerce_open_orders_total Current number of open orders requiring warehouse fulfillment
# TYPE ecommerce_open_orders_total gauge
ecommerce_open_orders_total 14.0
# HELP ecommerce_high_value_orders_pending Number of pending high-priority orders above $1,000
# TYPE ecommerce_high_value_orders_pending gauge
ecommerce_high_value_orders_pending 3.0
# HELP app_users_enterprise_tier_count Active accounts on the enterprise subscription tier
# TYPE app_users_enterprise_tier_count gauge
app_users_enterprise_tier_count 142.0
# HELP app_users_pending_verification_count Accounts awaiting KYC or identity verification
# TYPE app_users_pending_verification_count gauge
app_users_pending_verification_count 27.0
# HELP django_http_requests_total Total HTTP requests handled by Django grouped by endpoint, method, and HTTP status code
# TYPE django_http_requests_total counter
django_http_requests_total{endpoint="order-create",method="POST",status_code="201"} 320.0
django_http_requests_total{endpoint="order-create",method="POST",status_code="400"} 12.0
django_http_requests_total{endpoint="checkout-view",method="POST",status_code="500"} 1.0
django_http_requests_total{endpoint="api-user-detail",method="GET",status_code="200"} 1845.0
django_http_requests_total{endpoint="api-user-detail",method="GET",status_code="404"} 43.0
# HELP external_api_http_requests_total Total outgoing HTTP requests to external partner APIs
# TYPE external_api_http_requests_total counter
external_api_http_requests_total{method="POST",service="payments_gateway",status_class="2xx"} 318.0
external_api_http_requests_total{method="POST",service="payments_gateway",status_class="4xx"} 9.0
external_api_http_requests_total{method="POST",service="payments_gateway",status_class="5xx"} 2.0
# HELP ecommerce_payment_latency_seconds Payment gateway latency in seconds
# TYPE ecommerce_payment_latency_seconds histogram
ecommerce_payment_latency_seconds_bucket{le="0.1"} 42.0
ecommerce_payment_latency_seconds_bucket{le="0.5"} 310.0
ecommerce_payment_latency_seconds_bucket{le="1.0"} 325.0
ecommerce_payment_latency_seconds_bucket{le="+Inf"} 329.0
ecommerce_payment_latency_seconds_count 329.0
ecommerce_payment_latency_seconds_sum 42.18
Best Practices for Custom Prometheus Metrics
- Beware of Label Cardinality:
Never use unbounded high-cardinality values (such as raw UUIDs, email addresses, or un-sanitized URLs) as label values in Prometheus counters or histograms. High cardinality causes memory explosion in both the application process and Prometheus TSDB. Always group or sanitize endpoints (e.g.
request.resolver_match.url_name). - Cache Heavy Calculated Queries:
Because Prometheus scrapes
/metricsat regular intervals (typically every 10–30s), aCalculatedMetricthat performs full table scans or un-indexed queries can impact database performance. Cache query results withcache.get_or_set(..., timeout=15)to avoid duplicate computation across concurrent scrapes. - Use the Standard Python Client When Labels are Required:
When you need multi-dimensional labels (such as
{service="...", status_code="..."}), useprometheus_client.Counterorprometheus_client.Gaugedirectly. Hurricane automatically includes all collectors registered in the defaultprometheus_client.REGISTRY. - Learn More:
- Official Python Library: prometheus_client on GitHub
- Official Documentation: prometheus_client Documentation
- Prometheus Guidelines: Metric and Label Naming Best Practices
