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 ClassPrometheus TypeWhen to UseUpdate Mechanism
CounterMetricCounterMonotonically increasing events (e.g. user signups, orders placed, failed logins).MyCounter.increment(amount=1)
StoredMetricGaugeValues that fluctuate over time (e.g. active users, worker threads, cache size).MyGauge.set(value)
CalculatedMetricGauge / ComputedValues computed on-demand when /metrics is scraped (e.g. database query counts).Dynamic inside get_value(*args)
AverageMetricSummary / AverageRunning arithmetic mean updated per request (e.g. average basket value, cart size).MyAverage.add_value(val)
ObservedMetricHistogramLatency 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:

myapp/metrics.py
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:

myapp/middleware.py
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:

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:

myapp/services.py
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:

myapp/signals.py
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

  1. 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).
  2. Cache Heavy Calculated Queries: Because Prometheus scrapes /metrics at regular intervals (typically every 10–30s), a CalculatedMetric that performs full table scans or un-indexed queries can impact database performance. Cache query results with cache.get_or_set(..., timeout=15) to avoid duplicate computation across concurrent scrapes.
  3. Use the Standard Python Client When Labels are Required: When you need multi-dimensional labels (such as {service="...", status_code="..."}), use prometheus_client.Counter or prometheus_client.Gauge directly. Hurricane automatically includes all collectors registered in the default prometheus_client.REGISTRY.
  4. Learn More: