Volume log aplikasi production bisa mencapai jutaan baris per jam, sementara operator hanya punya menit untuk menemukan akar masalah. Log level menyaring sinyal dari kebisingan itu: setiap pesan diberi kelas keparahan, lalu satu ambang menentukan kelas mana yang benar-benar ditulis. Pada chapter ini kita menetapkan ambang dari variabel lingkungan, mengubahnya saat runtime, dan mengurai anti-pola yang membuat log error kehilangan nilainya.
Empat Level dan Maknanya #
slog mengenal empat level bawaan: LevelDebug (-4), LevelInfo (0), LevelWarn (4), dan LevelError (8). Handler menulis pesan hanya jika level pesan ≥ ambang. Tabel berikut menjadi kontrak tim tentang arti tiap level:
| Level | Arti | Contoh pesan yang tepat | Perilaku umum di production |
|---|---|---|---|
Debug | detail internal untuk pengembang | detail validasi field alamat | dibuang (ambang Info) |
Info | peristiwa normal alur bisnis | pesanan diterima id=42 | dicatat |
Warn | kondisi aneh namun pulih, perlu diperhatikan | stok menipis sisa=3, retry ke-3 dari 5 | dicatat, kandidat alarm |
Error | operasi gagal, perlu tindakan | pembayaran gagal id=42 | dicatat, memicu pager |
Aturan praktisnya: Debug untuk data yang hanya berarti saat membaca kode, Info untuk peristiwa yang membentuk narasi sistem, Warn untuk penyimpangan yang masih ditangani otomatis, dan Error untuk kegagalan yang tidak selesai sendiri. Pesan Error yang pulih sendiri lima menit kemudian seharusnya turun kelas menjadi Warn.
Ambang Statis dari Variabel Lingkungan #
Ambang paling umum ditetapkan sekali saat startup dan dibaca dari variabel lingkungan LOG_LEVEL — pola twelve-factor yang juga dipakai di chapter secure configuration
. Fungsi parseLevel di program demo menerjemahkan teks menjadi slog.Level, dengan Info sebagai default. Aplikasi yang belum memanggil slog.SetDefault memakai logger bawaan yang menulis lewat package log ke stderr; verbositasnya diatur terpisah:
slog.SetLogLoggerLevel(slog.LevelWarn)Fungsi tersebut hanya mengatur logger default, bukan handler yang kita bangun sendiri.
Ambang Dinamis dengan LevelVar #
Saat insiden berlangsung, kita ingin menaikkan verbositas tanpa memulai ulang proses. slog.LevelVar adalah level yang bisa diubah kapan pun lewat Set; logger yang memegang pointer-nya langsung mengikuti nilai terbaru — dasar fitur “ubah level dari admin panel”. Program demo membangun satu logger dengan LevelVar, memancarkan empat level saat ambang Debug, lalu menaikkan ambang ke Error dan memancarkan lagi.
Program Demo #
Berkas level-demo.go merangkai tiga skenario: ambang statis LOG_LEVEL=warn, ambang dinamis LevelVar, dan anti-pola pencatatan ganda.
package main
import (
"fmt"
"log/slog"
"os"
"strings"
)
// buangWaktu membekukan atribut time agar output demo identik di setiap
// eksekusi; ambang level diteruskan dari pemanggil.
func buangWaktu(level slog.Leveler) *slog.HandlerOptions {
return &slog.HandlerOptions{
Level: level,
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
},
}
}
// parseLevel menerjemahkan isi LOG_LEVEL menjadi slog.Level.
func parseLevel(teks string) (slog.Level, error) {
switch strings.ToLower(strings.TrimSpace(teks)) {
case "debug":
return slog.LevelDebug, nil
case "info":
return slog.LevelInfo, nil
case "warn", "warning":
return slog.LevelWarn, nil
case "error":
return slog.LevelError, nil
default:
return slog.LevelInfo, fmt.Errorf("level tidak dikenal: %q", teks)
}
}
// levelDariEnv membaca LOG_LEVEL; nilai kosong berarti default Info.
func levelDariEnv() (slog.Level, error) {
teks := os.Getenv("LOG_LEVEL")
if teks == "" {
return slog.LevelInfo, nil
}
return parseLevel(teks)
}
// catatSemuaLevel memancarkan satu pesan di setiap level.
func catatSemuaLevel(logger *slog.Logger) {
logger.Debug("detail validasi", "field", "alamat")
logger.Info("pesanan diterima", "id", 42)
logger.Warn("stok menipis", "sisa", 3)
logger.Error("pembayaran gagal", "id", 42)
}
// simpanPesanan berisi ANTI-POLA: mencatat error lalu mengembalikan error
// yang sama, sehingga pemanggil cenderung mencatatnya ulang.
func simpanPesanan(logger *slog.Logger, id int) error {
err := fmt.Errorf("koneksi database timeout")
logger.Error("gagal menyimpan pesanan", "id", id, "err", err)
return err
}
func main() {
// Skenario 1: ambang statis dari variabel lingkungan.
os.Setenv("LOG_LEVEL", "warn")
ambang, err := levelDariEnv()
if err != nil {
panic(err)
}
logger := slog.New(slog.NewTextHandler(os.Stdout, buangWaktu(ambang)))
fmt.Println("--- ambang statis LOG_LEVEL=warn ---")
catatSemuaLevel(logger)
// Skenario 2: ambang dinamis dengan slog.LevelVar.
dinamis := &slog.LevelVar{}
dinamis.Set(slog.LevelDebug)
peneliti := slog.New(slog.NewTextHandler(os.Stdout, buangWaktu(dinamis)))
fmt.Println("--- LevelVar di Debug ---")
catatSemuaLevel(peneliti)
dinamis.Set(slog.LevelError)
fmt.Println("--- LevelVar dinaikkan ke Error ---")
catatSemuaLevel(peneliti)
// Skenario 3: anti-pola log + return error.
fmt.Println("--- anti-pola: error yang sama dicatat dua kali ---")
if err := simpanPesanan(logger, 42); err != nil {
logger.Error("gagal menyimpan pesanan", "id", 42, "err", err)
}
}Output yang Dihasilkan:
--- ambang statis LOG_LEVEL=warn ---
time=2026-01-15T08:00:00Z level=WARN msg="stok menipis" sisa=3
time=2026-01-15T08:00:00Z level=ERROR msg="pembayaran gagal" id=42
--- LevelVar di Debug ---
time=2026-01-15T08:00:00Z level=DEBUG msg="detail validasi" field=alamat
time=2026-01-15T08:00:00Z level=INFO msg="pesanan diterima" id=42
time=2026-01-15T08:00:00Z level=WARN msg="stok menipis" sisa=3
time=2026-01-15T08:00:00Z level=ERROR msg="pembayaran gagal" id=42
--- LevelVar dinaikkan ke Error ---
time=2026-01-15T08:00:00Z level=ERROR msg="pembayaran gagal" id=42
--- anti-pola: error yang sama dicatat dua kali ---
time=2026-01-15T08:00:00Z level=ERROR msg="gagal menyimpan pesanan" id=42 err="koneksi database timeout"
time=2026-01-15T08:00:00Z level=ERROR msg="gagal menyimpan pesanan" id=42 err="koneksi database timeout"Mari kita telaah baris demi baris program di atas:
- Baris 12-22 (
buangWaktu): opsi handler menerimaslog.Leveler— statis maupun*slog.LevelVardinamis — sambil membekukan atributtimedemi output deterministik. - Baris 25-38 (
parseLevel):strings.ToLowerdanTrimSpacemembuatWARN,warn, danwarndiperlakukan sama; cabangdefaultmengembalikan error agar salah ketik terdengar saat startup, bukan diam-diam menurunkan verbositas. - Baris 41-47 (
levelDariEnv):os.Getenvmengembalikan string kosong saat variabel tak ada — diubah jadi defaultInfoeksplisit. - Baris 50-55 (
catatSemuaLevel): satu pesan per level supaya perbedaan ambang terlihat langsung. - Baris 67-72:
os.Setenv("LOG_LEVEL", "warn")dipanggil di dalam program agar skenario terkendali; logger dibangun dengan ambang hasil parsing. - Baris 74-75: pada ambang
warn, hanyalevel=WARNdanlevel=ERRORyang muncul — dua baris pertama daricatatSemuaLeveldibuang tanpa sampai ke handler. - Baris 78-87:
dinamisadalah*slog.LevelVaryang dipegang handler; dua pemanggilanSetmengubah ambang logger yang sama — saatDebugsemua level tampil, saatErrorhanya satu baris tersisa, tanpa membangun ulang logger. - Baris 59-63 + 91-93:
simpanPesananmencatat error lalu mengembalikannya; pemanggil mencatat ulang di baris 92 — output memperlihatkan dua barisERRORidentik untuk satu kejadian.
Anti-Pola: Mencatat lalu Mengembalikan Error #
Dua baris error identik di akhir output bukan sekadar pemborosan. Ketika satu kejadian menghasilkan banyak baris, penghitung “jumlah error” di dasbor tidak lagi sebanding dengan jumlah kejadian nyata, dan operator kehilangan rasa skala masalah. Pola yang dianjurkan: fungsi mengembalikan error begitu saja; pencatatan dilakukan sekali di lapisan teratas yang memegang konteks lengkap — dibahas di section error handling .
Level yang disiplin membuat log satu layanan mudah dibaca. Namun begitu satu request menyeberangi banyak handler dan layanan, kita butuh benang merah yang menautkan seluruh baris log itu — tugas request ID pada chapter berikutnya.
Referensi #
- Dokumentasi package log/slog
— spesifikasi
Level,Leveler, danLevelVar. - Package log
— perilaku
log.Fatalyang berbeda dari level. - Structured logging with slog (Go Blog) — desain level.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
level-demo.go
Unduh berkaspackage main
import (
"fmt"
"log/slog"
"os"
"strings"
)
// buangWaktu membekukan atribut time agar output demo identik di setiap
// eksekusi; ambang level diteruskan dari pemanggil.
func buangWaktu(level slog.Leveler) *slog.HandlerOptions {
return &slog.HandlerOptions{
Level: level,
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
},
}
}
// parseLevel menerjemahkan isi LOG_LEVEL menjadi slog.Level.
func parseLevel(teks string) (slog.Level, error) {
switch strings.ToLower(strings.TrimSpace(teks)) {
case "debug":
return slog.LevelDebug, nil
case "info":
return slog.LevelInfo, nil
case "warn", "warning":
return slog.LevelWarn, nil
case "error":
return slog.LevelError, nil
default:
return slog.LevelInfo, fmt.Errorf("level tidak dikenal: %q", teks)
}
}
// levelDariEnv membaca LOG_LEVEL; nilai kosong berarti default Info.
func levelDariEnv() (slog.Level, error) {
teks := os.Getenv("LOG_LEVEL")
if teks == "" {
return slog.LevelInfo, nil
}
return parseLevel(teks)
}
// catatSemuaLevel memancarkan satu pesan di setiap level.
func catatSemuaLevel(logger *slog.Logger) {
logger.Debug("detail validasi", "field", "alamat")
logger.Info("pesanan diterima", "id", 42)
logger.Warn("stok menipis", "sisa", 3)
logger.Error("pembayaran gagal", "id", 42)
}
// simpanPesanan berisi ANTI-POLA: mencatat error lalu mengembalikan error
// yang sama, sehingga pemanggil cenderung mencatatnya ulang.
func simpanPesanan(logger *slog.Logger, id int) error {
err := fmt.Errorf("koneksi database timeout")
logger.Error("gagal menyimpan pesanan", "id", id, "err", err)
return err
}
func main() {
// Skenario 1: ambang statis dari variabel lingkungan.
os.Setenv("LOG_LEVEL", "warn")
ambang, err := levelDariEnv()
if err != nil {
panic(err)
}
logger := slog.New(slog.NewTextHandler(os.Stdout, buangWaktu(ambang)))
fmt.Println("--- ambang statis LOG_LEVEL=warn ---")
catatSemuaLevel(logger)
// Skenario 2: ambang dinamis dengan slog.LevelVar.
dinamis := &slog.LevelVar{}
dinamis.Set(slog.LevelDebug)
peneliti := slog.New(slog.NewTextHandler(os.Stdout, buangWaktu(dinamis)))
fmt.Println("--- LevelVar di Debug ---")
catatSemuaLevel(peneliti)
dinamis.Set(slog.LevelError)
fmt.Println("--- LevelVar dinaikkan ke Error ---")
catatSemuaLevel(peneliti)
// Skenario 3: anti-pola log + return error.
fmt.Println("--- anti-pola: error yang sama dicatat dua kali ---")
if err := simpanPesanan(logger, 42); err != nil {
logger.Error("gagal menyimpan pesanan", "id", 42, "err", err)
}
}