Health check memberi sinyal cepat naik/turun, tetapi tidak cukup untuk memahami tren performance dari waktu ke waktu. Berapa ratus request per detik? Apa latency p99? Berapa error rate dalam 5 menit terakhir? Pertanyaan ini dijawab oleh metrics — angka numerik yang dikumpul secara periodik dan disimpan di time-series database. Empat tipe metric dasar: counter (monoton naik), gauge (nilai instan), timer/histogram (distribusi), dan distribution summary (distribusi non-waktu). Pada chapter ini kita akan membangun MeterRegistry sederhana yang mendukung keempat tipe, mendemokan format exposition Prometheus, serta menjelaskan konfigurasi Micrometer + Grafana untuk monitoring production.

Empat Tipe Metric #

TipeKarakteristikContoh
CounterMonoton naik; reset saat restarthttp_requests_total, errors_total
GaugeNaik turun, nilai instanactive_connections, memory_used_bytes
Timer/HistogramDistribusi nilaihttp_request_duration_ms (p50, p95, p99)
Distribution SummaryDistribusi non-waktupayload_size_bytes, batch_size

Counter cocok untuk count yang hanya bertambah. Gauge untuk nilai yang bisa naik-turun (mis. koneksi aktif). Timer untuk distribusi latency — penting karena rata-rata bisa menyesatkan (p99 lebih akurat merefleksikan pengalaman pengguna). Histogram bucket memungkinkan agregasi across instance (mis. total p99 dari 10 pod).

Demo MeterRegistry #

Cuplikan berikut mengimplementasikan registry dengan counter, gauge, timer, histogram, dan output format Prometheus:

kotlin
// metrics_dan_monitoring.kt — Demo meter Micrometer-style: counter, gauge, timer, histogram

import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.DoubleAdder

// === Meter types ===
interface Meter {
    val name: String
    val type: String
    fun snapshot(): Map<String, Any>
}

// === Counter: monoton naik ===
class Counter(override val name: String, private val tags: Map<String, String> = emptyMap()) : Meter {
    override val type = "counter"
    private val value = DoubleAdder()

    fun increment(amount: Double = 1.0) {
        value.add(amount)
    }

    override fun snapshot(): Map<String, Any> = mapOf(
        "type" to type, "name" to name, "value" to value.sum(), "tags" to tags
    )
}

// === Gauge: nilai instan ===
class Gauge(override val name: String, private val supplier: () -> Double) : Meter {
    override val type = "gauge"
    override fun snapshot(): Map<String, Any> = mapOf(
        "type" to type, "name" to name, "value" to supplier()
    )
}

// === Timer: distribusi durasi ===
class Timer(override val name: String) : Meter {
    override val type = "timer"
    private val samples = mutableListOf<Double>()
    private val count = AtomicLong(0)
    private val totalMs = DoubleAdder()

    fun record(durationMs: Double) {
        synchronized(samples) {
            if (samples.size < 1000) samples.add(durationMs)
        }
        count.incrementAndGet()
        totalMs.add(durationMs)
    }

    fun percentile(p: Double): Double {
        val sorted = synchronized(samples) { samples.sorted() }
        if (sorted.isEmpty()) return 0.0
        val idx = ((p / 100.0) * (sorted.size - 1)).toInt().coerceIn(0, sorted.size - 1)
        return sorted[idx]
    }

    override fun snapshot(): Map<String, Any> = mapOf(
        "type" to type, "name" to name, "count" to count.get(),
        "meanMs" to (if (count.get() > 0) totalMs.sum() / count.get() else 0.0),
        "p50Ms" to percentile(50.0),
        "p95Ms" to percentile(95.0),
        "p99Ms" to percentile(99.0)
    )
}

// === Histogram: bucket count ===
class Histogram(override val name: String, private val buckets: List<Double>) : Meter {
    override val type = "histogram"
    private val counts = buckets.associateWith { 0L }.toMutableMap()

    fun observe(value: Double) {
        for (b in buckets) {
            if (value <= b) {
                counts[b] = counts[b]!! + 1
                return
            }
        }
    }

    override fun snapshot(): Map<String, Any> = mapOf(
        "type" to type, "name" to name, "buckets" to counts.toMap()
    )
}

// === Registry ===
class MeterRegistry {
    private val meters = mutableListOf<Meter>()

    fun counter(name: String, tags: Map<String, String> = emptyMap()): Counter {
        val c = Counter(name, tags); meters.add(c); return c
    }

    fun gauge(name: String, supplier: () -> Double): Gauge {
        val g = Gauge(name, supplier); meters.add(g); return g
    }

    fun timer(name: String): Timer {
        val t = Timer(name); meters.add(t); return t
    }

    fun histogram(name: String, buckets: List<Double>): Histogram {
        val h = Histogram(name, buckets); meters.add(h); return h
    }

    fun snapshot(): List<Map<String, Any>> = meters.map { it.snapshot() }

    // Format Prometheus text exposition
    fun prometheusFormat(): String {
        val sb = StringBuilder()
        meters.forEach { m ->
            val snap = m.snapshot()
            sb.append("# TYPE ${m.name} ${m.type}\n")
            when (m.type) {
                "counter" -> sb.append("${m.name} ${snap["value"]}\n")
                "gauge" -> sb.append("${m.name} ${snap["value"]}\n")
                "timer" -> {
                    sb.append("${m.name}_count ${snap["count"]}\n")
                    sb.append("${m.name}_mean_ms ${snap["meanMs"]}\n")
                    sb.append("${m.name}{quantile=\"0.5\"} ${snap["p50Ms"]}\n")
                    sb.append("${m.name}{quantile=\"0.95\"} ${snap["p95Ms"]}\n")
                    sb.append("${m.name}{quantile=\"0.99\"} ${snap["p99Ms"]}\n")
                }
                "histogram" -> {
                    @Suppress("UNCHECKED_CAST")
                    val buckets = snap["buckets"] as Map<Double, Long>
                    buckets.forEach { (le, c) ->
                        sb.append("${m.name}_bucket{le=\"$le\"} $c\n")
                    }
                }
            }
        }
        return sb.toString()
    }
}

fun main() {
    println("=== Metrics dan Monitoring ===")
    println()
    println("Counter, Gauge, Timer, Histogram -> Micrometer + Prometheus.")
    println()

    val registry = MeterRegistry()

    // === 1. Counter: request count ===
    println("=== 1. Counter (request count) ===")
    val reqCounter = registry.counter("http_requests_total", mapOf("method" to "GET", "path" to "/users"))
    repeat(150) { reqCounter.increment() }
    reqCounter.increment(5.0)
    println("  ${reqCounter.snapshot()}")
    println()

    // === 2. Gauge: active connections ===
    println("=== 2. Gauge (active connections) ===")
    var activeConns = 0
    val connGauge = registry.gauge("http_connections_active") { activeConns.toDouble() }
    activeConns = 12
    println("  After +12: ${connGauge.snapshot()}")
    activeConns = 8
    println("  After -4:  ${connGauge.snapshot()}")
    println()

    // === 3. Timer: request latency ===
    println("=== 3. Timer (request latency) ===")
    val reqTimer = registry.timer("http_request_duration_ms")
    // Sample data deterministik
    val samples = listOf(5.0, 8.0, 12.0, 15.0, 20.0, 25.0, 30.0, 50.0, 80.0, 120.0)
    samples.forEach { reqTimer.record(it) }
    val snap = reqTimer.snapshot()
    println("  count=${snap["count"]}, meanMs=${snap["meanMs"]}")
    println("  p50Ms=${snap["p50Ms"]}, p95Ms=${snap["p95Ms"]}, p99Ms=${snap["p99Ms"]}")
    println()

    // === 4. Histogram: distribution ===
    println("=== 4. Histogram (latency buckets) ===")
    val hist = registry.histogram("http_latency_seconds",
        buckets = listOf(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0))
    val secondsSamples = listOf(0.008, 0.012, 0.020, 0.035, 0.045, 0.080, 0.150, 0.220, 0.450, 0.700)
    secondsSamples.forEach { hist.observe(it) }
    println("  ${hist.snapshot()}")
    println()

    // === 5. Prometheus format ===
    println("=== 5. Prometheus Format ===")
    println(registry.prometheusFormat())

    // === 6. Metric types ===
    println("=== 6. Empat Tipe Metric ===")
    val types = listOf(
        Triple("Counter", "Monoton naik, reset saat restart", "request count, error count"),
        Triple("Gauge", "Naik turun, nilai instan", "active conn, queue size, memory"),
        Triple("Timer/Histogram", "Distribusi nilai", "request latency, response size"),
        Triple("Distribution Summary", "Distribusi nilai non-waktu", "payload size, batch size")
    )
    types.forEach { (nama, deskripsi, contoh) ->
        println("  " + nama.padEnd(22) + " | " + deskripsi.padEnd(38) + " | " + contoh)
    }
    println()

    // === 7. Aturan praktis ===
    println("=== 7. Aturan Praktis ===")
    val rules = listOf(
        "1. Counter untuk count; Gauge untuk nilai instan",
        "2. Histogram untuk latency (bukan counter)",
        "3. Tag/label untuk dimensi: method, status, path",
        "4. Hindari label high-cardinality (userId, sessionId)",
        "5. Scrape interval 15-60 detik (default Prometheus 15s)",
        "6. SLO: p99 < 500ms, error rate < 0.1%",
        "7. Alert: latency p99 > 1s selama 5 menit"
    )
    rules.forEach { println("  " + it) }
}

Output yang Dihasilkan:

text
=== Metrics dan Monitoring ===

Counter, Gauge, Timer, Histogram -> Micrometer + Prometheus.

=== 1. Counter (request count) ===
  {type=counter, name=http_requests_total, value=155.0, tags={method=GET, path=/users}}

=== 2. Gauge (active connections) ===
  After +12: {type=gauge, name=http_connections_active, value=12.0}
  After -4:  {type=gauge, name=http_connections_active, value=8.0}

=== 3. Timer (request latency) ===
  count=10, meanMs=36.5
  p50Ms=20.0, p95Ms=80.0, p99Ms=80.0

=== 4. Histogram (latency buckets) ===
  {type=histogram, name=http_latency_seconds, buckets={0.005=0, 0.01=1, 0.025=2, 0.05=2, 0.1=1, 0.25=2, 0.5=1, 1.0=1}}

=== 5. Prometheus Format ===
# TYPE http_requests_total counter
http_requests_total 155.0
# TYPE http_connections_active gauge
http_connections_active 8.0
# TYPE http_request_duration_ms timer
http_request_duration_ms_count 10
http_request_duration_ms_mean_ms 36.5
http_request_duration_ms{quantile="0.5"} 20.0
http_request_duration_ms{quantile="0.95"} 80.0
http_request_duration_ms{quantile="0.99"} 80.0
# TYPE http_latency_seconds histogram
http_latency_seconds_bucket{le="0.005"} 0
http_latency_seconds_bucket{le="0.01"} 1
http_latency_seconds_bucket{le="0.025"} 2
http_latency_seconds_bucket{le="0.05"} 2
http_latency_seconds_bucket{le="0.1"} 1
http_latency_seconds_bucket{le="0.25"} 2
http_latency_seconds_bucket{le="0.5"} 1
http_latency_seconds_bucket{le="1.0"} 1

=== 6. Empat Tipe Metric ===
  Counter                | Monoton naik, reset saat restart       | request count, error count
  Gauge                  | Naik turun, nilai instan               | active conn, queue size, memory
  Timer/Histogram        | Distribusi nilai                       | request latency, response size
  Distribution Summary   | Distribusi nilai non-waktu             | payload size, batch size

=== 7. Aturan Praktis ===
  1. Counter untuk count; Gauge untuk nilai instan
  2. Histogram untuk latency (bukan counter)
  3. Tag/label untuk dimensi: method, status, path
  4. Hindari label high-cardinality (userId, sessionId)
  5. Scrape interval 15-60 detik (default Prometheus 15s)
  6. SLO: p99 < 500ms, error rate < 0.1%
  7. Alert: latency p99 > 1s selama 5 menit

Mari kita bedah hasil di atas:

  1. Counter: http_requests_total nilainya 155.0 setelah 150 increment + 1 increment dengan amount=5. Counter tidak pernah berkurang — bila request gagal, tetap increment (dengan tag status=500).
  2. Gauge: http_connections_active mengikuti perubahan activeConns (12 → 8). Gauge membaca nilai via supplier setiap snapshot — cocok untuk nilai yang berubah-ubah.
  3. Timer: 10 sampel latency. mean = 36.5 ms, p50 = 20.0 ms, p95 = 80.0 ms, p99 = 80.0 ms. p99 merefleksikan pengalaman pengguna paling lambat — lebih akurat daripada rata-rata yang bisa menyesatkan (satu request 120 ms tidak terlihat di mean).
  4. Histogram: bucket 0.005 ada 0 sampel, 0.01 ada 1, 0.025 ada 2, dst. Histogram bucket memungkinkan Prometheus menghitung p99 across instance — tidak mungkin dilakukan timer quantile biasa.
  5. Prometheus Format: format exposition standar yang dipahami Prometheus. Counter dan Gauge ditampilkan sebagai angka tunggal. Timer sebagai multiple series dengan label quantile. Histogram sebagai _bucket{le="..."}.

Micrometer dan Prometheus #

Micrometer adalah facade metric yang mendukung banyak backend (Prometheus, Datadog, NewRelic, CloudWatch). Library ini menyediakan API yang serupa dengan demo di atas, tetapi dengan fitur production-grade (async publish, cardinally-limited labels, JIT-aware):

kotlin Micrometer setup
val registry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
val counter = registry.counter("http_requests_total", "method", "GET", "path", "/users")
counter.increment()

// Spring Boot Actuator otomatis expose /actuator/prometheus
// Tidak perlu kode tambahan bila sudah pakai Spring Boot Actuator + micrometer-registry-prometheus

Prometheus scrape endpoint /actuator/prometheus setiap 15 detik. Data disimpan di time-series database Prometheus, lalu divisualisasikan di Grafana.

Dashboard Grafana #

Dashboard standar untuk service Kotlin/JVM:

PanelQuery PromQLTujuan
Request raterate(http_requests_total[1m])Trafik per detik
Error raterate(http_requests_total{status=~"5.."}[1m]) / rate(http_requests_total[1m])Persentase error
Latency p99histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))p99 latency
Active connhttp_connections_activeKoneksi aktif
Heap usedjvm_memory_used_bytes{area="heap"}Memori JVM

SLO dan Alerting #

Service Level Objective (SLO) mendefinisikan target performance. Alert berdasarkan SLO, bukan ambang batas arbitrer:

  • SLO: p99 < 500ms untuk 99.9% request dalam 30 hari
  • Alert: histogram_quantile(0.99, ...) > 0.5 selama 5 menit → page on-call
  • Burn rate: bila konsumsi budget error > 14x normal selama 1 jam → multi-page

Alert bawaas latency p99 > 1 detik selama 5 menit adalah rule minimum. Untuk service kritis, tambahkan alert error rate > 0.1% dan 5xx spike detection.

Referensi Lanjutan #

  • Micrometer — dokumentasi Micrometer untuk pengumpulan metrik aplikasi Kotlin
  • OpenTelemetry — dokumentasi OpenTelemetry untuk eksport metrik terpadu

Selanjutnya #

Metrics memberi visibilitas tren, tetapi untuk deployment yang aman di lingkungan terisolasi, kita perlu container. Bab berikutnya membahas Docker fundamentals — Dockerfile, multi-stage build, dan layer caching — topik pada chapter Docker Fundamentals.