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:

JenisPerilaku nilaiContoh nyataPertanyaan yang dijawab
Counterhanya naik, reset saat restarthttp_requests_total, error_totalberapa banyak sejak awal?
Gaugebebas naik-turunantrian_pesanan, koneksi_aktifberapa nilainya sekarang?
Histogrammerekam distribusi observasidurasi_request_msberapa 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.

go
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:

text
--- 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"} 2

Mari kita telaah baris demi baris program di atas:

  1. Baris 13-15 (Metrik): interface satu method Baris(nama string) []string — setiap jenis metrik bertanggung jawab mencetak dirinya, registry tidak perlu tahu tipenya.
  2. Baris 17-42 (Counter): Inc menaikkan nilai map per label; Baris mencetak label terurut lewat slices.Sorted(maps.Keys(...)) sehingga urutan baris selalu sama.
  3. Baris 44-61 (Gauge): Set menimpa nilai mutlak — tidak ada penjumlahan; nilai terakhir selalu menang.
  4. Baris 63-87 (Histogram): Observe mengumpulkan observasi mentah; Baris mengurutkan salinan data (slices.Clone agar lock tidak ditahan lama) lalu menghitung kuantil.
  5. Baris 89-102 (kuantil): interpolasi linear pada posisi q*(n-1) — p50 dari 10 observasi jatuh di antara nilai ke-5 dan ke-6.
  6. Baris 104-127 (Registry): map nama→Metrik dengan Daftarkan dan Render terurut nama; sync.Mutex menjaga akses dari banyak goroutine.
  7. Baris 129-139 (labelKunci): merangkai pasangan kunci-nilai variadik menjadi kunci label metode="GET",kode="200".
  8. Baris 147-153: simulasi enam request — tiga GET 200, satu GET 500, dua POST 201 — masuk ke counter melalui Inc.
  9. Baris 162-170: gauge dicatat dua kali (5 lalu 3) untuk memperlihatkan perilaku naik-turunnya secara kontras dengan counter.
  10. 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

metrics-demo.go

Unduh berkas
go
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()
}