Kodeloka
  1. Kodeloka
  2. Kelas Prometheus
  3. Materi

Modul 4 · Modul 4 dari 15

Instrumentasi Aplikasi: Go, Python, Node.js & Metode RED

Menambahkan metrik ke aplikasi Anda sendiri dengan benar, dan memilih metrik apa yang benar-benar layak dipantau.

Metrik Apa yang Layak Dibuat?

Godaan terbesar saat instrumentasi adalah mengukur segalanya. Hasilnya: ribuan metrik yang tidak pernah dilihat, kardinalitas membengkak, dan tidak ada yang tahu metrik mana yang penting saat insiden terjadi.

Gunakan dua kerangka baku berikut sebagai titik awal.

Metode RED — untuk Layanan (request-driven)

HurufArtiMetrik
RateBerapa request per detikrate(http_requests_total[5m])
ErrorsBerapa yang gagalrate(http_requests_total{status=~"5.."}[5m])
DurationBerapa lamahistogram_quantile(0.95, ...)

Metode USE — untuk Sumber Daya (CPU, disk, memori)

HurufArtiContoh
UtilizationBerapa persen terpakaiCPU 78%
SaturationBerapa banyak yang mengantreLoad average, panjang run queue
ErrorsBerapa kegagalanDisk I/O error, paket jaringan drop
💡 Titik awal yang sudah cukup untuk 90% layanan

Tiga metrik ini saja sudah bisa menjawab sebagian besar pertanyaan operasional:

  • http_requests_total{method, route, status} — Counter
  • http_request_duration_seconds{method, route} — Histogram
  • http_requests_in_flight{route} — Gauge

Tambahkan metrik bisnis (pesanan_dibuat_total, pembayaran_gagal_total) setelah tiga di atas berjalan.

Go

main.gogo
package main

import (
    "net/http"
    "time"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    requestsTotal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Jumlah total request HTTP.",
        },
        []string{"method", "route", "status"},   // label berkardinalitas RENDAH
    )

    requestDuration = promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "Durasi request HTTP dalam detik.",
            Buckets: []float64{.05, .1, .2, .3, .5, .75, 1, 2, 5},  // sesuai SLO
        },
        []string{"method", "route"},
    )

    inFlight = promauto.NewGauge(prometheus.GaugeOpts{
        Name: "http_requests_in_flight",
        Help: "Jumlah request yang sedang diproses.",
    })
)

func instrumentasi(route string, next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        inFlight.Inc()
        defer inFlight.Dec()

        mulai := time.Now()
        rec := &statusRecorder{ResponseWriter: w, status: 200}
        next(rec, r)

        // PENTING: pakai 'route' (pola), bukan r.URL.Path (nilai sebenarnya)
        requestsTotal.WithLabelValues(r.Method, route, itoa(rec.status)).Inc()
        requestDuration.WithLabelValues(r.Method, route).Observe(time.Since(mulai).Seconds())
    }
}

func main() {
    http.Handle("/metrics", promhttp.Handler())
    http.HandleFunc("/api/pesanan", instrumentasi("/api/pesanan", handlerPesanan))
    http.ListenAndServe(":8080", nil)
}

Python (FastAPI / Flask)

app.pypython
from prometheus_client import Counter, Histogram, Gauge, make_asgi_app
from fastapi import FastAPI, Request
import time

REQUESTS = Counter(
    "http_requests_total", "Jumlah total request HTTP.",
    ["method", "route", "status"],
)
DURATION = Histogram(
    "http_request_duration_seconds", "Durasi request HTTP.",
    ["method", "route"],
    buckets=(.05, .1, .2, .3, .5, .75, 1, 2, 5),
)
IN_FLIGHT = Gauge("http_requests_in_flight", "Request yang sedang diproses.")

app = FastAPI()
app.mount("/metrics", make_asgi_app())     # endpoint /metrics

@app.middleware("http")
async def ukur(request: Request, call_next):
    # Pakai pola route, BUKAN path sebenarnya -> mencegah cardinality explosion
    route = request.scope.get("route").path if request.scope.get("route") else "unknown"
    IN_FLIGHT.inc()
    mulai = time.perf_counter()
    try:
        response = await call_next(request)
        status = response.status_code
        return response
    except Exception:
        status = 500
        raise
    finally:
        IN_FLIGHT.dec()
        REQUESTS.labels(request.method, route, str(status)).inc()
        DURATION.labels(request.method, route).observe(time.perf_counter() - mulai)
⚠️ Python multi-worker (gunicorn/uvicorn) butuh perlakuan khusus

Setiap worker adalah proses terpisah dengan registry metriknya sendiri. Prometheus akan men-scrape worker yang berbeda-beda secara acak, sehingga angkanya melompat-lompat.

Solusi: aktifkan mode multiprocess dengan variabel lingkungan PROMETHEUS_MULTIPROC_DIR yang menunjuk ke direktori bersama, dan gunakan MultiProcessCollector.

Node.js

metrics.jsjavascript
const client = require('prom-client');

// Metrik bawaan Node: event loop lag, heap, GC — sangat berguna
client.collectDefaultMetrics({ prefix: 'node_' });

const requests = new client.Counter({
  name: 'http_requests_total',
  help: 'Jumlah total request HTTP.',
  labelNames: ['method', 'route', 'status'],
});

const duration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Durasi request HTTP.',
  labelNames: ['method', 'route'],
  buckets: [0.05, 0.1, 0.2, 0.3, 0.5, 0.75, 1, 2, 5],
});

app.use((req, res, next) => {
  const selesai = duration.startTimer({ method: req.method });
  res.on('finish', () => {
    // req.route.path = pola seperti '/api/user/:id', bukan '/api/user/48213'
    const route = (req.route && req.route.path) || 'unknown';
    selesai({ route });
    requests.inc({ method: req.method, route, status: res.statusCode });
  });
  next();
});

app.get('/metrics', async (req, res) => {
  res.set('Content-Type', client.register.contentType);
  res.end(await client.register.metrics());
});