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 #
| Tipe | Karakteristik | Contoh |
|---|---|---|
| Counter | Monoton naik; reset saat restart | http_requests_total, errors_total |
| Gauge | Naik turun, nilai instan | active_connections, memory_used_bytes |
| Timer/Histogram | Distribusi nilai | http_request_duration_ms (p50, p95, p99) |
| Distribution Summary | Distribusi non-waktu | payload_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:
// 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:
=== 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 menitMari kita bedah hasil di atas:
- Counter:
http_requests_totalnilainya 155.0 setelah 150 increment + 1 increment dengan amount=5. Counter tidak pernah berkurang — bila request gagal, tetap increment (dengan tagstatus=500). - Gauge:
http_connections_activemengikuti perubahanactiveConns(12 → 8). Gauge membaca nilai viasuppliersetiap snapshot — cocok untuk nilai yang berubah-ubah. - 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). - Histogram: bucket
0.005ada 0 sampel,0.01ada 1,0.025ada 2, dst. Histogram bucket memungkinkan Prometheus menghitung p99 across instance — tidak mungkin dilakukan timer quantile biasa. - 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):
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-prometheusPrometheus 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:
| Panel | Query PromQL | Tujuan |
|---|---|---|
| Request rate | rate(http_requests_total[1m]) | Trafik per detik |
| Error rate | rate(http_requests_total{status=~"5.."}[1m]) / rate(http_requests_total[1m]) | Persentase error |
| Latency p99 | histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) | p99 latency |
| Active conn | http_connections_active | Koneksi aktif |
| Heap used | jvm_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.