Log menjawab “apa yang terjadi”, tetapi pertanyaan operasional seperti “berapa request per detik sekarang?” atau “berapa persen yang lambat?” menuntut jawaban berupa angka yang dapat dijumlahkan. Metrics memindahkan pengamatan dari level peristiwa ke level agregat: aplikasi memelihara angka-angka kecil di memori, angka tersebut diambil berkala, lalu digambar sebagai deret waktu. Pada chapter ini kita membangun registry metrik mini di memori dengan tiga jenis metrik yang menjadi standar industri — counter, gauge, dan histogram — sebagai bekal menuju format Prometheus pada chapter berikutnya.
Tiga Jenis Metrik #
Pilihan jenis metrik menentukan pertanyaan apa yang bisa dijawab. Tabel berikut memetakan ketiganya ke contoh nyata:
| Jenis | Perilaku nilai | Contoh nyata | Pertanyaan yang dijawab |
|---|---|---|---|
| Counter | hanya naik, reset saat restart | http_requests_total, error_total | berapa banyak sejak awal? |
| Gauge | bebas naik-turun | antrian_pesanan, koneksi_aktif | berapa nilainya sekarang? |
| Histogram | merekam distribusi observasi | durasi_request_ms | berapa lamanya p50/p90/p99? |
Counter cocok untuk jumlah peristiwa karena selisih dua sampel waktu menghasilkan laju — requests(15:00) - requests(14:59) adalah request per menit. Gauge dipakai untuk nilai sesaat yang tidak bisa dijumlahkan: ukuran antrian naik saat pesanan masuk dan turun saat pesanan diproses. Histogram merekam setiap observasi satu per satu sehingga kuantil dapat dihitung: p50 (median) menggambarkan pengalaman biasa, p90 dan p99 menggambarkan ekor yang dirasakan pengguna paling lambat.
Label: Satu Metrik, Banyak Dimensi #
Label memecah satu metrik menjadi deret angka per kombinasi dimensi. http_requests_total dengan label metode dan kode menghasilkan baris terpisah untuk metode="GET",kode="200" dan metode="GET",kode="500" — keduanya tetap satu nama metrik, sehingga dasbor dapat menampilkan total sekaligus membedah per kode. Aturan pentingnya: label untuk kardinalitas rendah dan tetap (metode, kode status, nama layanan); memakai label user_id atau email akan meledakkan jumlah baris menjadi jutaan.
Pull dan Push #
Dua model pengambilan data metrik beredar di industri. Model pull — dipopulerkan Prometheus — berarti aplikasi mengekspos nilai metrik di sebuah endpoint HTTP, lalu sistem pemantau mengambil (scrape) endpoint tersebut secara berkala. Model push berarti aplikasi mengirim angka ke server agregasi, misalnya Pushgateway untuk batch job yang hidupnya sebentar. Pull unggul untuk layanan berumur panjang: satu sumber kebenaran, mudah di-debug dengan curl, dan target yang mati terlihat dari scrape yang gagal.
Program Demo: Registry Mini #
Berkas metrics-demo.go mengimplementasikan registry in-memory utuh: tiga jenis metrik dilindungi sync.Mutex, kontrak Metrik menyatukan cara render, dan urutan cetak dijaga lewat slices.Sorted(maps.Keys(...)) karena iterasi map Go tidak berurutan.
package main
import (
"fmt"
"maps"
"slices"
"strings"
"sync"
)
// Metrik adalah kontrak render: setiap jenis metrik tahu cara mencetak
// dirinya sendiri menjadi baris "nama nilai".
type Metrik interface {
Baris(nama string) []string
}
// Counter adalah metrik monoton: nilainya hanya naik, tidak pernah turun.
type Counter struct {
mu sync.Mutex
nilai map[string]float64
}
// Inc menaikkan counter satu poin untuk satu kombinasi label.
func (c *Counter) Inc(label string) {
c.mu.Lock()
defer c.mu.Unlock()
if c.nilai == nil {
c.nilai = map[string]float64{}
}
c.nilai[label]++
}
// Baris mencetak setiap label terurut supaya output deterministik.
func (c *Counter) Baris(nama string) []string {
c.mu.Lock()
defer c.mu.Unlock()
var hasil []string
for _, k := range slices.Sorted(maps.Keys(c.nilai)) {
hasil = append(hasil, fmt.Sprintf("%s{%s} %g", nama, k, c.nilai[k]))
}
return hasil
}
// Gauge adalah metrik bebas: nilainya bisa naik maupun turun.
type Gauge struct {
mu sync.Mutex
nilai float64
}
// Set menimpa nilai gauge dengan nilai sesaat.
func (g *Gauge) Set(v float64) {
g.mu.Lock()
defer g.mu.Unlock()
g.nilai = v
}
func (g *Gauge) Baris(nama string) []string {
g.mu.Lock()
defer g.mu.Unlock()
return []string{fmt.Sprintf("%s %g", nama, g.nilai)}
}
// Histogram merekam distribusi nilai; kuantil dihitung saat render.
type Histogram struct {
mu sync.Mutex
data []float64
}
// Observe menambahkan satu observasi ke histogram.
func (h *Histogram) Observe(v float64) {
h.mu.Lock()
defer h.mu.Unlock()
h.data = append(h.data, v)
}
func (h *Histogram) Baris(nama string) []string {
h.mu.Lock()
data := slices.Clone(h.data)
h.mu.Unlock()
slices.Sort(data)
return []string{
fmt.Sprintf("%s_jumlah %d", nama, len(data)),
fmt.Sprintf("%s_p50 %.1f", nama, kuantil(data, 0.50)),
fmt.Sprintf("%s_p90 %.1f", nama, kuantil(data, 0.90)),
fmt.Sprintf("%s_p99 %.1f", nama, kuantil(data, 0.99)),
}
}
// kuantil menghitung kuantil q dari data terurut dengan interpolasi linear.
func kuantil(data []float64, q float64) float64 {
if len(data) == 0 {
return 0
}
pos := q * float64(len(data)-1)
bawah := int(pos)
atas := bawah + 1
if atas >= len(data) {
return data[len(data)-1]
}
frac := pos - float64(bawah)
return data[bawah] + frac*(data[atas]-data[bawah])
}
// Registry menyimpan metrik in-memory dan mencetaknya terurut nama.
type Registry struct {
mu sync.Mutex
metrik map[string]Metrik
}
// Daftarkan mendaftarkan satu metrik di bawah sebuah nama.
func (r *Registry) Daftarkan(nama string, m Metrik) {
r.mu.Lock()
defer r.mu.Unlock()
if r.metrik == nil {
r.metrik = map[string]Metrik{}
}
r.metrik[nama] = m
}
// Render mencetak seluruh metrik terurut nama.
func (r *Registry) Render() {
for _, nama := range slices.Sorted(maps.Keys(r.metrik)) {
for _, b := range r.metrik[nama].Baris(nama) {
fmt.Println(b)
}
}
}
// labelKunci merangkai pasangan kunci-nilai menjadi kunci label tunggal.
func labelKunci(pasangan ...string) string {
var sb strings.Builder
for i := 0; i+1 < len(pasangan); i += 2 {
fmt.Fprintf(&sb, "%s=%q", pasangan[i], pasangan[i+1])
if i+2 < len(pasangan) {
sb.WriteByte(',')
}
}
return sb.String()
}
func main() {
reg := &Registry{}
// Counter: jumlah request per kombinasi metode dan kode status.
requests := &Counter{}
reg.Daftarkan("http_requests_total", requests)
for range 3 {
requests.Inc(labelKunci("metode", "GET", "kode", "200"))
}
requests.Inc(labelKunci("metode", "GET", "kode", "500"))
for range 2 {
requests.Inc(labelKunci("metode", "POST", "kode", "201"))
}
fmt.Println("--- counter http_requests_total ---")
for _, b := range requests.Baris("http_requests_total") {
fmt.Println(b)
}
// Gauge: ukuran antrian pesanan yang berubah setiap saat.
antrian := &Gauge{}
reg.Daftarkan("antrian_pesanan", antrian)
fmt.Println("--- gauge antrian_pesanan ---")
antrian.Set(5)
for _, b := range antrian.Baris("antrian_pesanan") {
fmt.Println("nilai awal:", b)
}
antrian.Set(3) // dua pesanan selesai diproses
for _, b := range antrian.Baris("antrian_pesanan") {
fmt.Println("setelah 2 diproses:", b)
}
// Histogram: durasi request dalam milidetik.
durasi := &Histogram{}
reg.Daftarkan("durasi_request_ms", durasi)
for _, v := range []float64{42, 1180, 65, 230, 87, 95, 110, 61, 78, 99} {
durasi.Observe(v)
}
fmt.Println("--- registry lengkap (terurut nama) ---")
reg.Render()
}Output yang Dihasilkan:
--- counter http_requests_total ---
http_requests_total{metode="GET",kode="200"} 3
http_requests_total{metode="GET",kode="500"} 1
http_requests_total{metode="POST",kode="201"} 2
--- gauge antrian_pesanan ---
nilai awal: antrian_pesanan 5
setelah 2 diproses: antrian_pesanan 3
--- registry lengkap (terurut nama) ---
antrian_pesanan 3
durasi_request_ms_jumlah 10
durasi_request_ms_p50 91.0
durasi_request_ms_p90 325.0
durasi_request_ms_p99 1094.5
http_requests_total{metode="GET",kode="200"} 3
http_requests_total{metode="GET",kode="500"} 1
http_requests_total{metode="POST",kode="201"} 2Mari kita telaah baris demi baris program di atas:
- Baris 13-15 (
Metrik): interface satu methodBaris(nama string) []string— setiap jenis metrik bertanggung jawab mencetak dirinya, registry tidak perlu tahu tipenya. - Baris 17-42 (
Counter):Incmenaikkan nilai map per label;Barismencetak label terurut lewatslices.Sorted(maps.Keys(...))sehingga urutan baris selalu sama. - Baris 44-61 (
Gauge):Setmenimpa nilai mutlak — tidak ada penjumlahan; nilai terakhir selalu menang. - Baris 63-87 (
Histogram):Observemengumpulkan observasi mentah;Barismengurutkan salinan data (slices.Cloneagar lock tidak ditahan lama) lalu menghitung kuantil. - Baris 89-102 (
kuantil): interpolasi linear pada posisiq*(n-1)— p50 dari 10 observasi jatuh di antara nilai ke-5 dan ke-6. - Baris 104-127 (
Registry): map nama→MetrikdenganDaftarkandanRenderterurut nama;sync.Mutexmenjaga akses dari banyak goroutine. - Baris 129-139 (
labelKunci): merangkai pasangan kunci-nilai variadik menjadi kunci labelmetode="GET",kode="200". - Baris 147-153: simulasi enam request — tiga GET 200, satu GET 500, dua POST 201 — masuk ke counter melalui
Inc. - Baris 162-170: gauge dicatat dua kali (5 lalu 3) untuk memperlihatkan perilaku naik-turunnya secara kontras dengan counter.
- Baris 172-180: sepuluh observasi durasi tetap masuk ke histogram, lalu
reg.Render()mencetak seluruh registry terurut nama — kuantil p50=91.0, p90=325.0, p99=1094.5 berasal dari data tersebut.
Registry mini kita tinggal di memori proses. Langkah berikutnya adalah mengeksposnya melalui endpoint HTTP dengan format baku yang dipahami pemantau — tugas chapter Prometheus .
Referensi #
- Prometheus: Metric Types — definisi resmi counter, gauge, histogram, dan summary.
- Package sync
—
Mutexyang melindungi metrik dari akses bersamaan.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
metrics-demo.go
Unduh berkaspackage main
import (
"fmt"
"maps"
"slices"
"strings"
"sync"
)
// Metrik adalah kontrak render: setiap jenis metrik tahu cara mencetak
// dirinya sendiri menjadi baris "nama nilai".
type Metrik interface {
Baris(nama string) []string
}
// Counter adalah metrik monoton: nilainya hanya naik, tidak pernah turun.
type Counter struct {
mu sync.Mutex
nilai map[string]float64
}
// Inc menaikkan counter satu poin untuk satu kombinasi label.
func (c *Counter) Inc(label string) {
c.mu.Lock()
defer c.mu.Unlock()
if c.nilai == nil {
c.nilai = map[string]float64{}
}
c.nilai[label]++
}
// Baris mencetak setiap label terurut supaya output deterministik.
func (c *Counter) Baris(nama string) []string {
c.mu.Lock()
defer c.mu.Unlock()
var hasil []string
for _, k := range slices.Sorted(maps.Keys(c.nilai)) {
hasil = append(hasil, fmt.Sprintf("%s{%s} %g", nama, k, c.nilai[k]))
}
return hasil
}
// Gauge adalah metrik bebas: nilainya bisa naik maupun turun.
type Gauge struct {
mu sync.Mutex
nilai float64
}
// Set menimpa nilai gauge dengan nilai sesaat.
func (g *Gauge) Set(v float64) {
g.mu.Lock()
defer g.mu.Unlock()
g.nilai = v
}
func (g *Gauge) Baris(nama string) []string {
g.mu.Lock()
defer g.mu.Unlock()
return []string{fmt.Sprintf("%s %g", nama, g.nilai)}
}
// Histogram merekam distribusi nilai; kuantil dihitung saat render.
type Histogram struct {
mu sync.Mutex
data []float64
}
// Observe menambahkan satu observasi ke histogram.
func (h *Histogram) Observe(v float64) {
h.mu.Lock()
defer h.mu.Unlock()
h.data = append(h.data, v)
}
func (h *Histogram) Baris(nama string) []string {
h.mu.Lock()
data := slices.Clone(h.data)
h.mu.Unlock()
slices.Sort(data)
return []string{
fmt.Sprintf("%s_jumlah %d", nama, len(data)),
fmt.Sprintf("%s_p50 %.1f", nama, kuantil(data, 0.50)),
fmt.Sprintf("%s_p90 %.1f", nama, kuantil(data, 0.90)),
fmt.Sprintf("%s_p99 %.1f", nama, kuantil(data, 0.99)),
}
}
// kuantil menghitung kuantil q dari data terurut dengan interpolasi linear.
func kuantil(data []float64, q float64) float64 {
if len(data) == 0 {
return 0
}
pos := q * float64(len(data)-1)
bawah := int(pos)
atas := bawah + 1
if atas >= len(data) {
return data[len(data)-1]
}
frac := pos - float64(bawah)
return data[bawah] + frac*(data[atas]-data[bawah])
}
// Registry menyimpan metrik in-memory dan mencetaknya terurut nama.
type Registry struct {
mu sync.Mutex
metrik map[string]Metrik
}
// Daftarkan mendaftarkan satu metrik di bawah sebuah nama.
func (r *Registry) Daftarkan(nama string, m Metrik) {
r.mu.Lock()
defer r.mu.Unlock()
if r.metrik == nil {
r.metrik = map[string]Metrik{}
}
r.metrik[nama] = m
}
// Render mencetak seluruh metrik terurut nama.
func (r *Registry) Render() {
for _, nama := range slices.Sorted(maps.Keys(r.metrik)) {
for _, b := range r.metrik[nama].Baris(nama) {
fmt.Println(b)
}
}
}
// labelKunci merangkai pasangan kunci-nilai menjadi kunci label tunggal.
func labelKunci(pasangan ...string) string {
var sb strings.Builder
for i := 0; i+1 < len(pasangan); i += 2 {
fmt.Fprintf(&sb, "%s=%q", pasangan[i], pasangan[i+1])
if i+2 < len(pasangan) {
sb.WriteByte(',')
}
}
return sb.String()
}
func main() {
reg := &Registry{}
// Counter: jumlah request per kombinasi metode dan kode status.
requests := &Counter{}
reg.Daftarkan("http_requests_total", requests)
for range 3 {
requests.Inc(labelKunci("metode", "GET", "kode", "200"))
}
requests.Inc(labelKunci("metode", "GET", "kode", "500"))
for range 2 {
requests.Inc(labelKunci("metode", "POST", "kode", "201"))
}
fmt.Println("--- counter http_requests_total ---")
for _, b := range requests.Baris("http_requests_total") {
fmt.Println(b)
}
// Gauge: ukuran antrian pesanan yang berubah setiap saat.
antrian := &Gauge{}
reg.Daftarkan("antrian_pesanan", antrian)
fmt.Println("--- gauge antrian_pesanan ---")
antrian.Set(5)
for _, b := range antrian.Baris("antrian_pesanan") {
fmt.Println("nilai awal:", b)
}
antrian.Set(3) // dua pesanan selesai diproses
for _, b := range antrian.Baris("antrian_pesanan") {
fmt.Println("setelah 2 diproses:", b)
}
// Histogram: durasi request dalam milidetik.
durasi := &Histogram{}
reg.Daftarkan("durasi_request_ms", durasi)
for _, v := range []float64{42, 1180, 65, 230, 87, 95, 110, 61, 78, 99} {
durasi.Observe(v)
}
fmt.Println("--- registry lengkap (terurut nama) ---")
reg.Render()
}