❓ Can I monitor Django apps with Prometheus + Grafana without rewriting my view code? Yes — you can expose metrics automatically, but configuring the scrape pipeline and visualisation still requires careful setup.
To monitor Django apps with Prometheus + Grafana you need three components: a metrics exporter inside the Django process, a Prometheus server that scrapes that endpoint, and a Grafana dashboard that queries Prometheus. Wiring them together in a way that scales and remains secure is the key challenge.
📑 Table of Contents
- ❓ Can I monitor Django apps with Prometheus + Grafana without rewriting my view code? Yes — you can expose metrics automatically, but configuring the scrape pipeline and visualisation still requires careful setup.
- 🛠️ Instrumentation — How to Expose Metrics
- 🔧 Adding the Middleware
- 📦 Exporter Configuration — Why Use an Exporter
- 📊 Grafana Dashboard — Building a Dashboard
- 🔧 Production Deployment — Securing Scrape Targets
- 🟩 Final Thoughts
- ❓ Frequently Asked Questions
- Do I need to modify every view to expose metrics?
- Can I use the
django-prometheuspackage instead of the generic client? - How do I secure the
/metrics/endpoint in production? - 📚 References & Further Reading
🛠️ Instrumentation — How to Expose Metrics
Instrumentation means exposing internal counters and gauges as HTTP endpoints that Prometheus can read — that is the purpose of the client library.
First, add the official prometheus_client library to your project:
$ pip install prometheus-client
Collecting prometheus-client Downloading prometheus_client-0.19.0-py2.py3-none-any.whl (70 kB)
Installing collected packages: prometheus-client
Successfully installed prometheus-client-0.19.0
Next, create a reusable module that registers Django‑specific metrics. The module lives in myproject/metrics.py:
# myproject/metrics.py
import time
from prometheus_client import Counter, Histogram, generate_latest
from django.http import HttpResponse
from django.utils.deprecation import MiddlewareMixin # Counter increments in O(1) per request
REQUEST_COUNT = Counter( "django_http_requests_total", "Total HTTP requests processed by Django", ["method", "endpoint", "status"]
) # Histogram stores observations in configurable buckets; queryable via quantiles
REQUEST_LATENCY = Histogram( "django_http_request_duration_seconds", "Latency of HTTP requests in seconds", ["method", "endpoint"]
) class PrometheusMiddleware(MiddlewareMixin): def process_view(self, request, view_func, view_args, view_kwargs): request._prometheus_start = time.time() def process_response(self, request, response): latency = time.time() - getattr(request, "_prometheus_start", time.time()) REQUEST_COUNT.labels( method=request.method, endpoint=request.path, status=response.status_code ).inc() REQUEST_LATENCY.labels( method=request.method, endpoint=request.path ).observe(latency) return response def prometheus_metrics_view(request): return HttpResponse(generate_latest(), content_type="text/plain")
What this does:
-
REQUEST_COUNT: a
Counterthat increments per request, labeled by method, endpoint, and HTTP status. -
REQUEST_LATENCY: a
Histogramthat records request duration, enabling percentile queries. - PrometheusMiddleware: hooks into Django’s request lifecycle to capture start time and update metrics on response.
- prometheus_metrics_view: returns the raw exposition format that Prometheus scrapes.
Why this, not a manual view decorator? The middleware applies automatically to every request, guaranteeing coverage without touching individual view functions.
🔧 Adding the Middleware
Update settings.py to insert the middleware and expose the endpoint:
# myproject/settings.py
MIDDLEWARE = [ # ... other middleware ... "myproject.metrics.PrometheusMiddleware",
] ROOT_URLCONF = "myproject.urls"
Then add a URL pattern:
# myproject/urls.py
from django.urls import path
from myproject.metrics import prometheus_metrics_view urlpatterns = [ # ... existing routes ... path("metrics/", prometheus_metrics_view, name="metrics"),
]
Key point: the /metrics/ endpoint now serves live metric data without extra code in each view.
Key point: Adding a dedicated middleware centralizes metric collection, reducing duplication and ensuring consistent labeling across the entire Django stack. (Also read: 🐍 Azure App Service vs AKS for Django deployment — which one should you use?)
📦 Exporter Configuration — Why Use an Exporter
Exporter configuration tells Prometheus how to reach the Django metrics endpoint and which scrape parameters to apply — that is the purpose of the scrape_config block.
According to the official Prometheus documentation, a scrape job defines the target URLs, the interval, and optional relabeling rules.
Create prometheus.yml with the following content:
# prometheus.yml
global: scrape_interval: 15s # default polling frequency evaluation_interval: 15s scrape_configs: - job_name: "django" static_configs: - targets: ["localhost:8000"] metrics_path: "/metrics/" relabel_configs: - source_labels: [__address__] regex: "(.*):8000" replacement: "$1:8000" target_label: instance
What this does:
- global.scrape_interval: sets the default polling frequency for all jobs.
- job_name: identifies the scrape job; here it is “django”.
- targets: points to the Django service listening on port 8000.
-
metrics_path: overrides the default
/metricspath to match our endpoint. -
relabel_configs: normalizes the
instancelabel for Grafana templating.
Why this, not a naked curl script? Prometheus handles retries, timeouts, and service discovery automatically, providing a robust collection pipeline. (Also read: ⚙️ Deploying a dockerized Django Rest Framework with Postgres for production)
“A well‑configured scrape job is the backbone of reliable observability.”
Key point: The exporter config bridges the Django process and Prometheus, enabling pull‑based monitoring without modifying application code.
📊 Grafana Dashboard — Building a Dashboard
Dashboard creation defines panels that query Prometheus for the metrics you exposed — that is the visual layer for developers.
Save the following JSON snippet as django-dashboard.json. Grafana can import it via the UI or the HTTP API.
{ "dashboard": { "title": "Django Application Overview", "panels": [ { "type": "graph", "title": "HTTP Requests Total", "targets": [ { "expr": "sum(django_http_requests_total) by (status)", "legendFormat": "{{status}}" } ], "datasource": "Prometheus" }, { "type": "graph", "title": "Request Latency (seconds)", "targets": [ { "expr": "histogram_quantile(0.95, sum(rate(django_http_request_duration_seconds_bucket[5m])) by (le))", "legendFormat": "95th percentile" } ], "datasource": "Prometheus" } ], "schemaVersion": 30, "version": 1 }, "overwrite": true
}
What this does:
- panels[0]: a time‑series graph showing total requests, grouped by HTTP status.
-
panels[1]: a latency graph using
histogram_quantileto compute the 95th percentile over a 5‑minute window. - datasource: points to the Prometheus data source configured in Grafana.
Why this, not a raw Prometheus UI query? Grafana adds templating, alerting, and sharing capabilities that the raw UI lacks.
Import the dashboard:
$ curl -X POST -H "Content-Type: application/json" \ -d @django-dashboard.json \ http://localhost:3000/api/dashboards/db
{"message":"Dashboard django-dashboard imported","status":"success"}
Key point: The dashboard visualizes the same metrics collected by Prometheus, giving immediate insight into request volume and latency trends. (More onPythonTPoint tutorials)
🔧 Production Deployment — Securing Scrape Targets
Production deployment packages Django, Prometheus, and Grafana into containers and applies network policies — that is the operational layer.
Below is a minimal docker-compose.yml that runs all three services. It builds the Django app with the instrumentation added earlier.
# docker-compose.yml
version: "3.8"
services: web: build: . ports: - "8000:8000" environment: - DJANGO_SETTINGS_MODULE=myproject.settings depends_on: - db prometheus: image: prom/prometheus:latest volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro ports: - "9090:9090" grafana: image: grafana/grafana:latest ports: - "3000:3000" environment: - GF_SECURITY_ADMIN_PASSWORD=admin depends_on: - prometheus db: image: postgres:13 environment: - POSTGRES_PASSWORD=example
What this does:
- web: builds the Django image from the current directory, exposing port 8000.
-
prometheus: mounts the custom
prometheus.ymland runs the Prometheus server. - grafana: runs Grafana with a default admin password; it depends on Prometheus to ensure the data source is available.
- db: provides a PostgreSQL instance for Django.
Why this, not a single monolithic container? Separating concerns allows each component to be scaled independently and secured with network policies.
Build and start the stack:
$ docker compose up -d
Creating network "myproject_default" with the default driver
Creating myproject_db_1 ... done
Creating myproject_web_1 ... done
Creating myproject_prometheus_1 ... done
Creating myproject_grafana_1 ... done
Verify that Prometheus can scrape the Django endpoint:
$ curl http://localhost:8000/metrics/
# HELP django_http_requests_total Total HTTP requests processed by Django
# TYPE django_http_requests_total counter
django_http_requests_total{method="GET",endpoint="/",status="200"} 42
# HELP django_http_request_duration_seconds Request latency in seconds
# TYPE django_http_request_duration_seconds histogram
django_http_request_duration_seconds_bucket{le="0.005"} 30
django_http_request_duration_seconds_bucket{le="0.01"} 40
django_http_request_duration_seconds_bucket{le="+Inf"} 42
Key point: The compose file illustrates a clean separation of scrape target (Django) and collector (Prometheus), while Grafana consumes the same metrics for dashboards.
🟩 Final Thoughts
Implementing a full observability stack for Django does not require invasive code changes; a small middleware and a single endpoint are enough to feed Prometheus. The remaining effort focuses on wiring scrape jobs, securing the network path, and designing Grafana panels that surface the most relevant signals.
Developers can add performance monitoring to any existing Django project with a predictable deployment footprint, and the stack can evolve—adding alerts, service‑level objectives, or additional exporters—without revisiting application logic.
❓ Frequently Asked Questions
Do I need to modify every view to expose metrics?
No. The middleware automatically records request counts and latency for all incoming HTTP requests, so individual view changes are unnecessary.
Can I use the django-prometheus package instead of the generic client?
Yes, django-prometheus provides ready‑made collectors, but the generic prometheus_client gives finer control over custom metrics and reduces external dependencies.
How do I secure the /metrics/ endpoint in production?
Common approaches include restricting access to the Prometheus IP range using firewall rules, placing the endpoint behind an authentication proxy, or exposing it only on an internal network.
💡 Want to practise this hands-on? DigitalOcean gives new accounts $200 free credit for 60 days — enough to spin up a full Linux/Docker/Kubernetes environment at no cost.
📚 Recommended reading: Best DevOps & cloud books on Amazon — from Linux fundamentals to Kubernetes in production, curated for working engineers.
📚 References & Further Reading
- Official Prometheus client for Python — how to define counters and histograms: prometheus.io
- Prometheus configuration reference — scrape job definitions and relabeling: prometheus.io
- Grafana dashboard provisioning guide — import JSON dashboards programmatically: grafana.com

Top comments (0)