log/slog — log terstruktur
Log JSON yang membawa request_id di setiap baris mengubah investigasi insiden dari membaca ribuan baris jadi satu kueri filter. slog memberi itu tanpa dependensi apa pun.
Intisari
slogada di pustaka standar sejak Go 1.21 — tidak perlu zap atau zerolog untuk sebagian besar aplikasi.- JSON di produksi, teks berwarna di lokal. Satu baris perbedaan di
main. slog.InfoContext(ctx, ...), bukanslog.Info— hanya varian context yang bisa membawa trace dan request ID.- Handler kustom yang menarik atribut dari
contextadalah cara membuatrequest_idmuncul di semua log tanpa mengoper logger ke mana-mana. - Jangan pernah mencatat sandi, token, atau seluruh body permintaan.
Menyiapkan
func buatLogger(cfg config.Config) *slog.Logger {
opsi := &slog.HandlerOptions{
Level: cfg.LevelLog,
AddSource: cfg.Env != "production", // nomor baris: berguna, tapi mahal
}
var h slog.Handler
if cfg.Env == "production" {
h = slog.NewJSONHandler(os.Stdout, opsi) // CloudWatch bisa mem-parse
} else {
h = slog.NewTextHandler(os.Stdout, opsi) // terbaca manusia
}
// Atribut yang menempel di SETIAP baris log.
log := slog.New(h).With(
"layanan", "toko-api",
"versi", versiBuild,
)
slog.SetDefault(log) // supaya pustaka yang memakai slog global ikut rapi
return log
}
Tulis ke stdout, bukan ke berkas. Di container, log adalah aliran keluaran; ECS
mengumpulkannya lewat driver awslogs dan mengirimkannya ke CloudWatch. Menulis ke berkas di
dalam container berarti log yang hilang saat task berganti, dan disk yang penuh diam-diam.
Menulis log yang berguna
// Pasangan kunci-nilai, bukan string yang disambung
log.InfoContext(ctx, "pesanan dibuat",
"pesanan_id", p.ID,
"pengguna_id", p.PenggunaID,
"total", p.Total,
"jumlah_item", len(p.Item))
// Versi bertipe: lebih cepat, tanpa alokasi konversi
log.InfoContext(ctx, "pesanan dibuat",
slog.Int64("pesanan_id", p.ID),
slog.Int64("pengguna_id", p.PenggunaID),
slog.Duration("durasi", waktu))
// Kelompok
log.InfoContext(ctx, "panggilan keluar",
slog.Group("http",
slog.String("metode", "POST"),
slog.String("host", "api.pembayaran.id"),
slog.Int("status", 502)))
Pesan log adalah label, bukan kalimat. Tulis "pesanan dibuat", bukan
fmt.Sprintf("Pesanan %d untuk pengguna %d dibuat", ...). Pesan yang tetap bisa dijadikan
filter dan dihitung; pesan yang setiap barisnya unik tidak bisa diapa-apakan selain dibaca satu per
satu.
Membuat request_id muncul di semua baris
// Handler pembungkus yang menarik atribut dari context.
type handlerKonteks struct{ slog.Handler }
func (h handlerKonteks) Handle(ctx context.Context, r slog.Record) error {
if id := RequestID(ctx); id != "" {
r.AddAttrs(slog.String("request_id", id))
}
if u := PenggunaID(ctx); u != 0 {
r.AddAttrs(slog.Int64("pengguna_id", u))
}
if sc := trace.SpanContextFromContext(ctx); sc.IsValid() {
r.AddAttrs(
slog.String("trace_id", sc.TraceID().String()),
slog.String("span_id", sc.SpanID().String()))
}
return h.Handler.Handle(ctx, r)
}
func (h handlerKonteks) WithAttrs(a []slog.Attr) slog.Handler {
return handlerKonteks{h.Handler.WithAttrs(a)}
}
func (h handlerKonteks) WithGroup(n string) slog.Handler {
return handlerKonteks{h.Handler.WithGroup(n)}
}
log := slog.New(handlerKonteks{slog.NewJSONHandler(os.Stdout, opsi)})
Inilah alasan InfoContext penting. Varian tanpa context (log.Info)
meneruskan context.Background(), sehingga handler di atas tidak menemukan apa pun — dan
baris log itu jadi yatim, tidak bisa dihubungkan ke permintaan mana pun. Buat aturan tim: selalu
varian Context, dan pasang linter sloglint untuk menegakkannya.
Level dan artinya
| Level | Untuk | Di produksi |
|---|---|---|
Debug | Nilai antara saat mengembangkan | Mati — sering membocorkan data |
Info | Peristiwa bisnis: pesanan dibuat, pembayaran lunas | Nyala |
Warn | Pulih sendiri: retry berhasil, cache gagal lalu jatuh ke DB | Nyala |
Error | Permintaan gagal, pekerjaan tidak selesai | Nyala — ini yang jadi alarm |
// Level bisa diubah saat runtime tanpa restart
var levelAktif = new(slog.LevelVar)
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: levelAktif})
// endpoint admin: naikkan verbositas selama insiden, lalu turunkan lagi
mux.HandleFunc("POST /admin/log-level", func(w http.ResponseWriter, r *http.Request) {
levelAktif.Set(slog.LevelDebug)
})
Yang tidak boleh masuk log
| Jangan | Sebagai gantinya |
|---|---|
| Sandi, token, kunci API | Tipe Rahasia dengan LogValue() |
| Seluruh body permintaan | Beberapa field yang relevan saja |
| Nomor kartu, NIK, data pribadi | ID internal yang bisa ditelusuri lewat sistem lain |
| Stack trace untuk error 4xx | Satu baris Info; 4xx adalah perilaku normal |
| Satu baris log per iterasi loop | Satu ringkasan setelah loop selesai |
// Tipe apa pun bisa menyensor dirinya sendiri
type Pengguna struct {
ID int64
Email string
Hash string
}
func (u Pengguna) LogValue() slog.Value {
return slog.GroupValue(
slog.Int64("id", u.ID),
slog.String("email", samarkanEmail(u.Email)), // a***@contoh.com
)
}
Volume log adalah biaya yang nyata. CloudWatch menagih per GB yang masuk dan per GB yang
disimpan. Satu baris Debug per kueri database pada aplikasi dengan 500 permintaan/detik
menghasilkan puluhan gigabita per hari — dan tagihannya bisa melampaui biaya container yang
menghasilkannya. Debug mati di produksi bukan cuma soal keamanan.
Perlukah zap atau zerolog?
| slog | zap / zerolog | |
|---|---|---|
| Dependensi | Nol | Satu |
| Alokasi per baris | Rendah dengan atribut bertipe | Sedikit lebih rendah |
| Integrasi ekosistem | Standar — pustaka lain memakainya | Perlu adaptor |
| Kapan menang | Hampir semua aplikasi | Jutaan baris log per detik |
Go 1.26 juga menambahkan slog.NewMultiHandler, yang mengirim satu baris ke beberapa handler
sekaligus — misalnya JSON ke stdout dan salinan ringkas ke pengumpul error.
Latihan: pasang handler kustom yang menambahkan request_id dari context, lalu
buat middleware yang membangkitkan ID itu. Panggil satu endpoint dan pastikan semua baris log
dari permintaan itu — termasuk yang ditulis di lapisan repository — membawa ID yang sama. Lalu ganti satu
panggilan jadi slog.Info tanpa context dan lihat barisnya kehilangan ID.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.