Ketika aplikasi berjalan di production, log sering menjadi satu-satunya saksi kejadian yang tersedia: siapa pemanggil, rute apa yang diakses, dan kesalahan apa yang muncul. Masalahnya, log gaya lama berbentuk kalimat bebas seperti request gagal dengan status 500 dan durasi 1180ms, sehingga pencarian harus mengandalkan teks persis dan agregasi angka menjadi mustahil. Structured logging mengubah cara pandang kita: setiap baris log adalah data berupa pasangan kunci-nilai, bukan kalimat untuk manusia saja. Pada chapter ini kita akan membandingkan kedua gaya tersebut dalam satu program yang sama, lalu memetakan posisi logging di antara dua pilar observability lainnya.
Log sebagai Data, Bukan Kalimat #
Sebuah entri log terstruktur terdiri dari field tetap — waktu, level, pesan — ditambah atribut kunci-nilai yang kita definisikan sendiri. Package log/slog dari standard library mendukung gaya ini sejak Go 1.21 melalui pemanggilan seperti berikut:
slog.Info("request selesai",
"metode", "GET",
"rute", "/api/pesanan",
"status", 200,
"durasi_ms", 42)Argumen setelah pesan berselang-seling: kunci string, lalu nilai bertipe apa pun; JSONHandler mengubah pemanggilan tersebut menjadi satu objek JSON per baris. Karena setiap atribut punya kunci yang stabil, alat pencari seperti grep dapat mencari pola "status":500, sedangkan jq bahkan dapat memfilter berdasarkan nilai:
jq 'select(.status == 500)' aplikasi.logDengan log teks bebas, pola pencarian berubah-ubah mengikuti format fmt.Printf yang dipilih programmer, dan angka terjebak di dalam kalimat sehingga tidak bisa diagregasi.
Program Demo: Teks Bebas vs Terstruktur #
Berkas structured-demo.go di bawah halaman mencatat tiga peristiwa request yang sama dengan dua gaya: fmt.Printf gaya lama dan slog dengan JSONHandler. Setelah itu, program menyaring baris JSON yang memuat "status":500 — simulasi sederhana dari apa yang dilakukan grep pada berkas log sungguhan.
package main
import (
"bytes"
"fmt"
"log/slog"
"strings"
)
// buangWaktu membekukan atribut time pada handler agar output contoh ini
// identik pada setiap eksekusi (timestamp nyata berubah terus).
func buangWaktu() *slog.HandlerOptions {
return &slog.HandlerOptions{
Level: slog.LevelInfo,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && len(groups) == 0 {
return slog.String(slog.TimeKey, "2026-01-15T08:00:00Z")
}
return a
},
}
}
// peristiwaRequest memodelkan satu request HTTP yang selesai ditangani.
type peristiwaRequest struct {
Metode string
Rute string
Status int
Durasi int // dalam milidetik
}
func main() {
peristiwa := []peristiwaRequest{
{"GET", "/api/pesanan", 200, 42},
{"GET", "/api/pesanan", 500, 1180},
{"POST", "/api/pesanan", 201, 65},
}
fmt.Println("--- gaya lama: log teks bebas ---")
for _, p := range peristiwa {
status := "selesai"
if p.Status >= 500 {
status = "gagal"
}
fmt.Printf("2026-01-15 08:00:00 request %s %s %s status=%d durasi=%dms\n",
p.Metode, p.Rute, status, p.Status, p.Durasi)
}
fmt.Println("--- gaya baru: log terstruktur (JSON) ---")
var buf bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&buf, buangWaktu()))
for _, p := range peristiwa {
aksi, pesan := logger.Info, "request selesai"
if p.Status >= 500 {
aksi, pesan = logger.Error, "request gagal"
}
aksi(pesan,
"metode", p.Metode,
"rute", p.Rute,
"status", p.Status,
"durasi_ms", p.Durasi)
}
garis := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
for _, g := range garis {
fmt.Println(g)
}
fmt.Println("--- saringan baris dengan status 500 ---")
cocok := 0
for _, g := range garis {
if strings.Contains(g, `"status":500`) {
cocok++
fmt.Println(g)
}
}
fmt.Println("jumlah kecocokan:", cocok)
}Output yang Dihasilkan:
--- gaya lama: log teks bebas ---
2026-01-15 08:00:00 request GET /api/pesanan selesai status=200 durasi=42ms
2026-01-15 08:00:00 request GET /api/pesanan gagal status=500 durasi=1180ms
2026-01-15 08:00:00 request POST /api/pesanan selesai status=201 durasi=65ms
--- gaya baru: log terstruktur (JSON) ---
{"time":"2026-01-15T08:00:00Z","level":"INFO","msg":"request selesai","metode":"GET","rute":"/api/pesanan","status":200,"durasi_ms":42}
{"time":"2026-01-15T08:00:00Z","level":"ERROR","msg":"request gagal","metode":"GET","rute":"/api/pesanan","status":500,"durasi_ms":1180}
{"time":"2026-01-15T08:00:00Z","level":"INFO","msg":"request selesai","metode":"POST","rute":"/api/pesanan","status":201,"durasi_ms":65}
--- saringan baris dengan status 500 ---
{"time":"2026-01-15T08:00:00Z","level":"ERROR","msg":"request gagal","metode":"GET","rute":"/api/pesanan","status":500,"durasi_ms":1180}
jumlah kecocokan: 1Mari kita telaah baris demi baris program di atas:
- Baris 12-22 (
buangWaktu): membangunslog.HandlerOptionsyang mengganti atributtimedengan string tetap lewat fungsiReplaceAttr, membuat output demo deterministik. - Baris 25-30 (
peristiwaRequest): struct sederhana yang memodelkan satu request: metode HTTP, rute, kode status, dan durasi dalam milidetik. - Baris 33-37: tiga peristiwa uji — dua sukses (200 dan 201) dan satu kegagalan (500) — dicatat dengan kedua gaya agar perbandingannya adil.
- Baris 39-47: gaya lama merangkai kalimat lewat
fmt.Printf; posisi tiap nilai hanya diketahui pembuat formatnya. - Baris 50-51:
slog.NewJSONHandlermenulis kebytes.Buffer— bukan langsung ke terminal — sehingga program dapat memproses balik lognya sendiri seperti layaknya alat analisis. - Baris 52-62: pemanggilan
logger.Info/logger.Errormenerima pasangan kunci-nilai; level dipilih berdasarkan kode status, danaksiberisi method value sehingga satu pemanggilanaksi(pesan, ...)cukup untuk kedua cabang. - Baris 64-67: isi buffer dipecah per baris lalu dicetak ulang;
strings.TrimRightmembuang baris kosong di akhir buffer. - Baris 69-77: saringan
strings.Contains(g, "\"status\":500\")menirukangrep— hanya baris dengan fieldstatusbernilai 500 yang lolos, dan penghitungcocokmembuktikan tepat satu baris yang ditemukan.
Perhatikan bahwa saringan menemukan kegagalan tanpa menebak bentuk kalimatnya. Pada log gaya lama, kata gagal bisa saja ditulis error atau failed oleh programmer lain, dan pola pencarian tunggal akan melewatkan variasi itu.
Menjaga Konsistensi Field #
Nilai structured logging baru terasa bila seluruh tim memakai kosakata field yang sama. Tabel berikut merangkum praktik yang menjaga skema log tetap sehat:
| Praktik | Contoh baik | Contoh buruk |
|---|---|---|
| Kunci stabil dan lowercase | status, rute, durasi_ms | StatusCode, route url, Durasi |
| Satuan masuk nama field | durasi_ms, ukuran_byte | durasi (detik? milidetik?) |
| Nilai primitif, bukan kalimat | "status": 500 | "status": "terjadi kesalahan lima ratus" |
| Kunci sama untuk event sejenis | selalu request_id | req_id, id_request, trace bergantian mood |
Tiga Pilar Observability #
Logging hanyalah satu dari tiga pilar observability. Ketiganya menjawab pertanyaan yang berbeda dan saling melengkapi:
| Pilar | Pertanyaan utama | Contoh data | Karakteristik |
|---|---|---|---|
| Logging | Apa yang terjadi pada momen tertentu? | baris JSON request gagal status=500 | peristiwa diskrit, konteks kaya |
| Metrics | Bagaimana perilaku sistem dari waktu ke waktu? | angka error_rate = 0.02 | deret angka, murah diagregasi, ideal untuk alarm |
| Tracing | Bagaimana satu request mengalir lintas komponen? | pohon span API → service pesanan → DB | kausalitas dan durasi antar tahap |
Chapter log dan slog di section Standard Library sudah memperlihatkan anatomi handler text dan JSON. Section ini melanjutkannya ke arah operasional: level yang disiplin, request ID untuk korelasi, metrics format Prometheus, hingga distributed tracing dengan OpenTelemetry.
Setiap baris log kita kini berupa data yang dapat dicari. Pertanyaan berikutnya: bagaimana memilih Handler, menyuntikkan field tetap, dan mengendalikan biaya pencatatan itu? Chapter log/slog membedah API-nya satu per satu.
Referensi #
- Dokumentasi package log/slog — spesifikasi resmi Logger, Handler, dan atribut.
- Structured logging with slog (Go Blog) — desain dan alasan API slog.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
structured-demo.go
Unduh berkaspackage main
import (
"bytes"
"fmt"
"log/slog"
"strings"
)
// buangWaktu membekukan atribut time pada handler agar output contoh ini
// identik pada setiap eksekusi (timestamp nyata berubah terus).
func buangWaktu() *slog.HandlerOptions {
return &slog.HandlerOptions{
Level: slog.LevelInfo,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && len(groups) == 0 {
return slog.String(slog.TimeKey, "2026-01-15T08:00:00Z")
}
return a
},
}
}
// peristiwaRequest memodelkan satu request HTTP yang selesai ditangani.
type peristiwaRequest struct {
Metode string
Rute string
Status int
Durasi int // dalam milidetik
}
func main() {
peristiwa := []peristiwaRequest{
{"GET", "/api/pesanan", 200, 42},
{"GET", "/api/pesanan", 500, 1180},
{"POST", "/api/pesanan", 201, 65},
}
fmt.Println("--- gaya lama: log teks bebas ---")
for _, p := range peristiwa {
status := "selesai"
if p.Status >= 500 {
status = "gagal"
}
fmt.Printf("2026-01-15 08:00:00 request %s %s %s status=%d durasi=%dms\n",
p.Metode, p.Rute, status, p.Status, p.Durasi)
}
fmt.Println("--- gaya baru: log terstruktur (JSON) ---")
var buf bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&buf, buangWaktu()))
for _, p := range peristiwa {
aksi, pesan := logger.Info, "request selesai"
if p.Status >= 500 {
aksi, pesan = logger.Error, "request gagal"
}
aksi(pesan,
"metode", p.Metode,
"rute", p.Rute,
"status", p.Status,
"durasi_ms", p.Durasi)
}
garis := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
for _, g := range garis {
fmt.Println(g)
}
fmt.Println("--- saringan baris dengan status 500 ---")
cocok := 0
for _, g := range garis {
if strings.Contains(g, `"status":500`) {
cocok++
fmt.Println(g)
}
}
fmt.Println("jumlah kecocokan:", cocok)
}