karawaci.kode

← Semua snippet

Python Menengah Performance

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/12345 literal, 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() atau increase() 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 /metrics endpoint 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