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:

LevelArtiContoh pesan yang tepatPerilaku umum di production
Debugdetail internal untuk pengembangdetail validasi field alamatdibuang (ambang Info)
Infoperistiwa normal alur bisnispesanan diterima id=42dicatat
Warnkondisi aneh namun pulih, perlu diperhatikanstok menipis sisa=3, retry ke-3 dari 5dicatat, kandidat alarm
Erroroperasi gagal, perlu tindakanpembayaran gagal id=42dicatat, 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:

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

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

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

  1. Baris 12-22 (buangWaktu): opsi handler menerima slog.Leveler — statis maupun *slog.LevelVar dinamis — sambil membekukan atribut time demi output deterministik.
  2. Baris 25-38 (parseLevel): strings.ToLower dan TrimSpace membuat WARN, warn, dan warn diperlakukan sama; cabang default mengembalikan error agar salah ketik terdengar saat startup, bukan diam-diam menurunkan verbositas.
  3. Baris 41-47 (levelDariEnv): os.Getenv mengembalikan string kosong saat variabel tak ada — diubah jadi default Info eksplisit.
  4. Baris 50-55 (catatSemuaLevel): satu pesan per level supaya perbedaan ambang terlihat langsung.
  5. Baris 67-72: os.Setenv("LOG_LEVEL", "warn") dipanggil di dalam program agar skenario terkendali; logger dibangun dengan ambang hasil parsing.
  6. Baris 74-75: pada ambang warn, hanya level=WARN dan level=ERROR yang muncul — dua baris pertama dari catatSemuaLevel dibuang tanpa sampai ke handler.
  7. Baris 78-87: dinamis adalah *slog.LevelVar yang dipegang handler; dua pemanggilan Set mengubah ambang logger yang sama — saat Debug semua level tampil, saat Error hanya satu baris tersisa, tanpa membangun ulang logger.
  8. Baris 59-63 + 91-93: simpanPesanan mencatat error lalu mengembalikannya; pemanggil mencatat ulang di baris 92 — output memperlihatkan dua baris ERROR identik 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 #

Berkas Sumber

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

level-demo.go

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