Prometheus for Application Metrics¶
Einordnung ins Overtime-Projekt
Neben Logs braucht Overtime auch Echtzeit-Metriken – etwa zur Zahl aktiver WebSocket-Verbindungen oder gleichzeitiger Video-Calls –, um Kapazität und Verfügbarkeit zu überwachen.
1. Kontext & Problemstellung (Deutsch)¶
Während Logs die Frage beantworten "Was ist wann passiert?", beantworten Metriken die Frage "Was macht das System genau jetzt und im Zeitverlauf?". Für eine Echtzeit-Kommunikationsplattform wie Overtime müssen zentrale operative Signale – wie aktive WebSocket-Verbindungen, gleichzeitige Video-Calls, Anfrage-Latenzen und Fehlerraten – überwacht werden können, ohne dafür Log-Streams aufwendig parsen zu müssen.
Ein Pull-basiertes Metrik-System, das direkt im Anwendungsprozess läuft, wird gegenüber einem Push-Ansatz (bei dem die Anwendung Metriken aktiv an einen externen Dienst sendet) bevorzugt. Ein Push-Ansatz würde eine zusätzliche Netzwerk-Abhängigkeit im kritischen Schreibpfad (Write-Path) der Applikation einführen.
2. Architecture Decision Record (English)¶
ADR-012: Prometheus for Application Metrics¶
Status¶
Accepted
Context and Problem Statement¶
Logs answer "what happened and when." Metrics answer "what is the system doing right now and over time." For a real-time messaging and video-calling application, key operational signals — active WebSocket connections, concurrent video calls, request latency, error rate — must be observable without parsing log streams. A pull-based metrics system that runs in-process is preferable to pushing metrics from application code to an external service, which would introduce a write-path dependency.
Decision Drivers¶
- Actionable Alerting: Metrics must be queryable for alerting (e.g., concurrent calls or WebSocket connections exceeding a safe threshold).
- Zero Log Interference: The exposure mechanism must not interfere with application logging (scrape requests must not generate access log noise).
- Accurate Multiprocessing: The multi-worker Gunicorn setup requires metrics to be correctly aggregated across all workers — naive per-worker reporting produces misleading totals.
- Internal Accessibility: The
/metricsendpoint must be reachable by an in-cluster Prometheus scraper without authentication, but must remain unexposed externally.
Considered Options¶
- StatsD + Push Gateway — Application pushes counters to a sidecar. Requires maintaining a sidecar and a push gateway; metrics can be lost if the sidecar is temporarily unavailable.
- OpenTelemetry SDK — Vendor-neutral but introduces a significantly larger dependency surface and requires an OTEL collector in the cluster. Too much setup overhead for a focused prototype.
- **
django-prometheus+prometheus_client**— Pull-based. The/metricsendpoint is served directly from the app process, Prometheus scrapes on its own schedule, and Gunicorn multiprocess support is natively built-in.
Decision Outcome¶
Chosen: django-prometheus for Django-level automatic instrumentation (request counts, latency histograms, DB query counts) combined with prometheus_client for custom business metrics.
Gunicorn Multiprocess Mode¶
Gunicorn forks multiple worker processes. Without coordination, each worker tracks its own counter values (e.g., a Prometheus scrape of worker A sees only worker A's counts). prometheus_client resolves this via a shared memory directory:
- The
PROMETHEUS_MULTIPROC_DIRenvironment variable points to/app/prometheus_multiproc(created during the builder stage of the Dockerfile). - Each worker writes its metric values to a file in this directory.
- At scrape time, the
prometheus_clientcollector seamlessly aggregates across all worker files.
Log Noise Suppression & Metric Naming¶
Prometheus scrapes /metrics every few seconds. Without filtering, each scrape generates a Django access log line, resulting in thousands of low-value logs obscuring real events. A NoiseFilter explicitly excludes /metrics from the django.request logger. The endpoint remains fully functional for scraping without polluting standard output.
All custom metrics use the overtime namespace prefix (e.g., overtime_concurrent_calls). The _total suffix on counters is a Prometheus convention automatically appended by prometheus_client.
Consequences & Trade-offs¶
Positive Consequences¶
- Zero-Code Baselines:
django-prometheusauto-instruments request duration histograms, status code counts, and database query counts with zero application code changes. - Simplified Architecture: Prometheus utilizes a pull model. The application has no knowledge of whether it is being scraped, removing outbound network dependencies.
- Centralized Filtering: The
NoiseFilteris shared between the metrics scrape path and the Kubernetes healthcheck path — a single mechanism suppresses all operational-polling noise.
Negative Consequences / Trade-offs¶
- Strict Deployment Invariants:
PROMETHEUS_MULTIPROC_DIRmust exist and be writable before Gunicorn workers start. A missing or non-writable path causes a runtime crash. - Manual Mode Declaration:
prometheus_clientdoes not support Gaugemultiprocess_mode="livesum"by default. Using the wrong mode produces silently incorrect aggregations; developers must declare this mode explicitly for every new Gauge. - Unauthenticated Endpoint:
/metricsis unauthenticated. This is acceptable for an in-cluster scrape, but the ingress controller must be strictly configured to never expose this path externally.
3. Implementation & Code Snippets (English)¶
The implementation utilizes the prometheus_client to define custom metrics at the module level. They are updated directly at the relevant call sites without needing to pass a metrics client object through function signatures.
Custom Metrics Definition (api/views/demo.py)¶
The following snippet demonstrates the two primary metric types used in the Overtime backend: a Gauge for tracking active state (which goes up and down) and a Counter for tracking cumulative events (which only goes up).
from prometheus_client import Counter, Gauge
# Gauge: multiprocess_mode="livesum" sums the value across all Gunicorn workers so
# /metrics always shows the real total across the pod, not just one worker's slice.
concurrent_calls = Gauge(
"concurrent_calls",
"Number of calls currently being processed",
namespace="overtime",
multiprocess_mode="livesum",
)
# Usage when a video call starts:
# concurrent_calls.inc()
# Usage when a video call ends:
# concurrent_calls.dec()
# Counter: Each worker accumulates its own count; prometheus_client sums them
# together automatically at scrape time (default behavior for Counters).
finished_calls = Counter(
"finished_calls_total",
"Total number of finished calls",
namespace="overtime",
)
# Usage (Counters never decrease. Prometheus calculates rates over time itself):
# finished_calls.inc()
4. Compliance Einordnung (Deutsch)¶
Das detaillierte Mapping auf BSI IT-Grundschutz, CRA und CIS Benchmarks wird später ergänzt.