Custom Prometheus counter + histogram di Python
Instrument FastAPI / Flask dengan Prometheus counter + histogram. Track business metric (order, payment) bukan cuma RED metric.
Dipublikasikan 29 Juni 2026
RED metric (Rate-Error-Duration) penting, tapi business metric lebih berharga buat product team. Berapa order per menit? Distribusi nilai checkout? Snippet ini wrap FastAPI dengan middleware Prometheus + custom counter untuk order Tokopedia.
Kode
# metrics.py
from prometheus_client import (
Counter,
Histogram,
Gauge,
CollectorRegistry,
generate_latest,
CONTENT_TYPE_LATEST,
)
# Registry custom — supaya bisa multiple registry kalau perlu
registry = CollectorRegistry()
# RED METRICS (technical)
http_requests_total = Counter(
"http_requests_total",
"Total HTTP request",
labelnames=["method", "route", "status"],
registry=registry,
)
http_request_duration_seconds = Histogram(
"http_request_duration_seconds",
"Durasi HTTP request dalam detik",
labelnames=["method", "route"],
buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
registry=registry,
)
http_requests_in_progress = Gauge(
"http_requests_in_progress",
"Jumlah HTTP request yang sedang diproses",
labelnames=["method", "route"],
registry=registry,
)
# BUSINESS METRICS
order_created_total = Counter(
"order_created_total",
"Total order dibuat",
labelnames=["status", "payment_method", "kota"],
registry=registry,
)
order_amount_rupiah = Histogram(
"order_amount_rupiah",
"Distribusi nilai order dalam rupiah",
buckets=(50_000, 100_000, 250_000, 500_000, 1_000_000,
2_500_000, 5_000_000, 10_000_000, 25_000_000),
labelnames=["payment_method"],
registry=registry,
)
stok_low_warning_total = Counter(
"stok_low_warning_total",
"Berapa kali produk hit threshold stok rendah",
labelnames=["produk_sku"], # CAREFUL: SKU bisa cardinal tinggi — limit produk yang track
registry=registry,
)
def render_metrics() -> tuple[bytes, str]:
"""Return (body, content_type) untuk endpoint /metrics."""
return generate_latest(registry), CONTENT_TYPE_LATEST
# middleware.py — FastAPI middleware untuk auto-track RED
import time
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
from metrics import (
http_requests_total,
http_request_duration_seconds,
http_requests_in_progress,
)
def normalize_route(request: Request) -> str:
"""Ganti path dengan template, supaya gak cardinal tinggi.
/api/produk/12345 → /api/produk/{id}
"""
route = request.scope.get("route")
return route.path if route else request.url.path
class PrometheusMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
method = request.method
route = normalize_route(request)
http_requests_in_progress.labels(method=method, route=route).inc()
start = time.perf_counter()
try:
response: Response = await call_next(request)
status = str(response.status_code)
except Exception:
status = "500"
http_requests_total.labels(method=method, route=route, status=status).inc()
raise
finally:
http_requests_in_progress.labels(method=method, route=route).dec()
duration = time.perf_counter() - start
http_request_duration_seconds.labels(method=method, route=route).observe(duration)
http_requests_total.labels(method=method, route=route, status=status).inc()
return response
# main.py
from fastapi import FastAPI, Response
from middleware import PrometheusMiddleware
from metrics import (
render_metrics,
order_created_total,
order_amount_rupiah,
stok_low_warning_total,
)
app = FastAPI()
app.add_middleware(PrometheusMiddleware)
@app.get("/metrics")
async def metrics() -> Response:
body, content_type = render_metrics()
return Response(content=body, media_type=content_type)
@app.post("/api/order")
async def create_order(order: dict):
# ... proses order ...
# Track business metric
order_created_total.labels(
status=order["status"],
payment_method=order["payment_method"], # 'qris', 'gopay', 'bca_va'
kota=order["kota"], # 'jakarta', 'surabaya', dst
).inc()
order_amount_rupiah.labels(
payment_method=order["payment_method"]
).observe(order["total"])
# Cek stok rendah
if order["produk_stok_sisa"] < 10:
stok_low_warning_total.labels(produk_sku=order["produk_sku"]).inc()
return {"ok": True, "order_id": order["id"]}
Pemakaian
# Scrape endpoint /metrics
curl http://localhost:8000/metrics | head -30
# Output sample:
# HELP http_requests_total Total HTTP request
# TYPE http_requests_total counter
# http_requests_total{method="POST",route="/api/order",status="201"} 1248.0
# http_requests_total{method="GET",route="/api/produk/{id}",status="200"} 5239.0
#
# HELP order_created_total Total order dibuat
# TYPE order_created_total counter
# order_created_total{kota="jakarta",payment_method="qris",status="paid"} 423.0
# order_created_total{kota="surabaya",payment_method="gopay",status="paid"} 215.0
# prometheus.yml — scrape config
scrape_configs:
- job_name: api-tokopedia
scrape_interval: 15s
static_configs:
- targets: ['api:8000']
# Query Grafana — rate order per menit per kota
sum by (kota) (rate(order_created_total{status="paid"}[5m])) * 60
# P95 nilai order
histogram_quantile(0.95, sum by (le) (rate(order_amount_rupiah_bucket[10m])))
# Latency p99 per route
histogram_quantile(0.99,
sum by (le, route) (rate(http_request_duration_seconds_bucket[5m]))
)
# Error rate per route (>1% = warning)
sum by (route) (rate(http_requests_total{status=~"5.."}[5m]))
/ sum by (route) (rate(http_requests_total[5m]))
Kapan dipakai
- API service production yang butuh SLO monitoring.
- Business dashboard untuk product team (order, revenue, GMV).
- Alerting rule (PagerDuty, Slack) — fire kalau error rate > threshold.
- Performance regression detection antar deploy.
Catatan
- Cardinality discipline — label produk_sku untuk warning OK kalau cuma puluhan SKU dimonitor. Untuk seluruh katalog 100rb produk, ini cardinality bomb. Pakai topK + sample.
- Route normalize wajib — kalau pakai
/api/produk/12345literal, setiap ID jadi series. Pakai route template Starlette. - Histogram bucket — pilih sesuai distribution real. Default 0.005-10 detik bagus untuk HTTP. Untuk amount, sesuaikan range Indonesia (50rb - 25jt).
- Counter naik terus — query selalu pakai
rate()atauincrease()di Prometheus. Raw value gak useful. - Gauge untuk current state — queue depth, connection pool active. Naik turun OK.
- multiprocess mode — Gunicorn / uvicorn multi-worker butuh
multiprocess.MultiProcessCollector. Tanpa itu, tiap worker pegang state sendiri.
Prometheus
/metricsendpoint adalah attack surface. Production: limit access ke internal network atau pasang auth. Banyak business detail di metric.
# tags
prometheusmetricsfastapiobservabilitypython
Ditulis oleh Asti Larasati · 29 Juni 2026