Chapter log dan slog sudah memperkenalkan pemakaian dasar slog. Kini kita masuk ke sisi yang menentukan kualitas log di production: bagaimana Logger dan Handler bekerja sama, bagaimana nilai khusus bisa tersamarkan otomatis, dan bagaimana biaya pencatatan dikendalikan agar hot path tidak melambat. Seluruh mekanisme tersebut hidup di tiga komponen inti: Logger, Handler, dan antarmuka slog.LogValuer.

Arsitektur Logger dan Handler #

Logger hanyalah pemegang referensi ke sebuah Handler; seluruh keputusan format dan tujuan output berada di handler. Pemanggilan logger.Info(...) pertama-tama menanyakan handler.Enabled(level); hanya jika jawabannya true, record dibangun dan diteruskan ke handler.Handle. Ambang level diatur lewat slog.HandlerOptions.Level — slog.LevelInfo berarti semua panggilan Debug dibuang tanpa membangun record.

Dua handler bawaan menutupi mayoritas kebutuhan:

HandlerFormatKapan dipakai
slog.NewTextHandlerkunci=nilai satu baristerminal manusia, development
slog.NewJSONHandlersatu objek JSON per barisagregator log, production

HandlerOptions membuka dua fitur berharga: AddSource untuk mencatat lokasi pemanggilan, dan ReplaceAttr untuk mengubah atribut sebelum dicetak — demo kita memakai ReplaceAttr membekukan time dan memendekkan path sumber agar output dapat direproduksi.

Field yang Melekat dengan With dan Group #

logger.With("layanan", "pesanan") mengembalikan logger baru yang setiap baris turunannya otomatis memuat field tersebut — pola yang pas untuk lingkup satu request:

go
reqLogger := logger.With("layanan", "pesanan", "request_id", "req-001")
reqLogger.Info("validasi selesai")
// turunan ini tetap mewarisi request_id tanpa menulisnya ulang

logger.WithGroup("http") memberi awalan pada atribut turunan sehingga field seperti http.metode terpisah rapi dari field domain. Contoh lengkap pola With dan WithGroup ada di chapter log dan slog .

Nilai Khusus dengan LogValuer #

Interface slog.LogValuer memungkinkan tipe kita mengendalikan cara nilainya dirender:

go
type LogValuer interface {
	LogValue() slog.Value
}

Dua kegunaan praktisnya: penyamaran dan penundaan. Tipe Kredensial di program demo mengembalikan slog.GroupValue berisi pengguna dan sandi yang sudah diganti "***" — sandi asli tidak pernah sampai ke handler. Pendekatan serupa dipakai untuk token pada chapter secure logging .

Biaya Pencatatan: Evaluasi Lazy #

Hot path aplikasi sibuk bisa memanggil pencatatan ribuan kali per detik. slog menjawabnya dengan evaluasi lazy: nilai yang mengimplementasikan LogValuer hanya dievaluasi ketika baris benar-benar ditulis. Program demo memakai Statistik penghitung evaluasi — Debug di bawah ambang Info tidak memicu LogValue sama sekali, penghitung hanya naik satu. Untuk jalur ekstrem, Logger.LogAttrs menerima []slog.Attr siap pakai dan menghindari alokasi slice variadik.

Program Demo #

Berkas slog-obs-demo.go memadatkan empat mekanisme di atas: AddSource dengan normalisasi path, penyamaran Kredensial lewat GroupValue, bukti evaluasi lazy, dan pemasangan slog.SetDefault.

go
package main

import (
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"strconv"
)

// opsiDemo membekukan atribut time dan menormalisasi path sumber menjadi
// nama berkas saja, sehingga output contoh identik di setiap eksekusi dan
// tidak bergantung pada lokasi direktori proyek.
func opsiDemo() *slog.HandlerOptions {
	return &slog.HandlerOptions{
		Level:     slog.LevelInfo,
		AddSource: true,
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			if len(groups) > 0 {
				return a
			}
			switch a.Key {
			case slog.TimeKey:
				return slog.String(slog.TimeKey, "2026-01-15T08:00:00Z")
			case slog.SourceKey:
				if s, ok := a.Value.Any().(*slog.Source); ok {
					return slog.String(slog.SourceKey,
						filepath.Base(s.File)+":"+strconv.Itoa(s.Line))
				}
			}
			return a
		},
	}
}

// Kredensial mengimplementasikan slog.LogValuer: handler memanggil LogValue
// saat mencetak, sehingga sandi selalu tersamarkan di log.
type Kredensial struct {
	Pengguna string
	Sandi    string
}

func (k Kredensial) LogValue() slog.Value {
	return slog.GroupValue(
		slog.String("pengguna", k.Pengguna),
		slog.String("sandi", "***"),
	)
}

// Statistik membuktikan evaluasi lazy: LogValue hanya dipanggil ketika
// baris log benar-benar ditulis handler (level aktif).
type Statistik struct {
	hitung *int
}

func (s Statistik) LogValue() slog.Value {
	*s.hitung++
	return slog.IntValue(*s.hitung)
}

func main() {
	logger := slog.New(slog.NewTextHandler(os.Stdout, opsiDemo()))

	fmt.Println("--- atribut sumber pemanggilan (AddSource) ---")
	logger.Info("server siap", "port", 8080)
	logger.Warn("cache miss", "kunci", "pengguna:42")

	fmt.Println("--- LogValuer: kredensial tersamarkan ---")
	logger.Info("login berhasil", "kredensial", Kredensial{Pengguna: "budi", Sandi: "rahasia-123"})

	fmt.Println("--- evaluasi lazy pada level Debug yang dinonaktifkan ---")
	eval := 0
	logger.Debug("mode diagnosa", "statistik", Statistik{hitung: &eval})
	logger.Info("mode normal", "statistik", Statistik{hitung: &eval})
	fmt.Println("LogValue dipanggil:", eval, "kali")

	fmt.Println("--- slog.SetDefault ---")
	slog.SetDefault(logger)
	slog.Info("dari logger default")
}

Output yang Dihasilkan:

text
--- atribut sumber pemanggilan (AddSource) ---
time=2026-01-15T08:00:00Z level=INFO source=slog-obs-demo.go:65 msg="server siap" port=8080
time=2026-01-15T08:00:00Z level=WARN source=slog-obs-demo.go:66 msg="cache miss" kunci=pengguna:42
--- LogValuer: kredensial tersamarkan ---
time=2026-01-15T08:00:00Z level=INFO source=slog-obs-demo.go:69 msg="login berhasil" kredensial.pengguna=budi kredensial.sandi=***
--- evaluasi lazy pada level Debug yang dinonaktifkan ---
time=2026-01-15T08:00:00Z level=INFO source=slog-obs-demo.go:74 msg="mode normal" statistik=1
LogValue dipanggil: 1 kali
--- slog.SetDefault ---
time=2026-01-15T08:00:00Z level=INFO source=slog-obs-demo.go:79 msg="dari logger default"

Mari kita telaah baris demi baris program di atas:

  1. Baris 14-34 (opsiDemo): membangun opsi handler dengan ambang LevelInfo, AddSource, dan ReplaceAttr dua tugas — mengganti atribut time dengan string tetap serta memangkas path sumber menjadi nama berkas plus nomor baris lewat filepath.Base.
  2. Baris 38-48 (Kredensial + LogValue): method LogValue mengembalikan slog.GroupValue sehingga handler mencetak dua field berawalan kredensial.; nilai Sandi tidak pernah dikirim ke handler.
  3. Baris 52-59 (Statistik): setiap evaluasi LogValue menaikkan *s.hitung — penghitung inilah bukti nyata kapan evaluasi terjadi.
  4. Baris 62: satu handler TextHandler ke os.Stdout dibungkus slog.New; seluruh pemanggilan berikutnya memakai logger ini.
  5. Baris 65-66: dua log biasa memperlihatkan atribut source — perhatikan nomor baris yang menunjuk persis ke lokasi pemanggilan Info dan Warn.
  6. Baris 69: field kredensial dirender menjadi kredensial.pengguna=budi kredensial.sandi=*** — perilaku group dari GroupValue bekerja sama seperti WithGroup.
  7. Baris 72-75: eval dimulai dari nol; Debug pada baris 73 dibuang karena di bawah ambang, Info pada baris 74 dievaluasi sekali, dan baris 75 mencetak LogValue dipanggil: 1 kali.
  8. Baris 78-79: slog.SetDefault menjadikan logger ini tujuan seluruh pemanggilan paket-level seperti slog.Info — penting agar library pihak ketiga yang memakai slog default ikut menulis ke handler kita.

Handler Buatan Sendiri #

Ketika format bawaan tidak cukup — misalnya menulis ke Elasticsearch bulk API atau format OpenTelemetry log — kita dapat mengimplementasikan antarmuka slog.Handler sendiri:

go
type Handler interface {
	Enabled(ctx context.Context, level slog.Level) bool
	Handle(ctx context.Context, r slog.Record) error
	WithAttrs(attrs []slog.Attr) Handler
	WithGroup(name string) Handler
}

Cara termudah merangkai perilaku khusus adalah membungkus handler bawaan di dalam Handle milik kita, bukan menulis formatter dari nol.

Dengan handler, field menempel, dan nilai khusus tersamarkan, fondasi teknis kita sudah berdiri. Pilar disiplin berikutnya berada di tangan penulis kode: memilih level yang tepat untuk setiap peristiwa — tema chapter Log Level .

Referensi #

Berkas Sumber

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

slog-obs-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"strconv"
)

// opsiDemo membekukan atribut time dan menormalisasi path sumber menjadi
// nama berkas saja, sehingga output contoh identik di setiap eksekusi dan
// tidak bergantung pada lokasi direktori proyek.
func opsiDemo() *slog.HandlerOptions {
	return &slog.HandlerOptions{
		Level:     slog.LevelInfo,
		AddSource: true,
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			if len(groups) > 0 {
				return a
			}
			switch a.Key {
			case slog.TimeKey:
				return slog.String(slog.TimeKey, "2026-01-15T08:00:00Z")
			case slog.SourceKey:
				if s, ok := a.Value.Any().(*slog.Source); ok {
					return slog.String(slog.SourceKey,
						filepath.Base(s.File)+":"+strconv.Itoa(s.Line))
				}
			}
			return a
		},
	}
}

// Kredensial mengimplementasikan slog.LogValuer: handler memanggil LogValue
// saat mencetak, sehingga sandi selalu tersamarkan di log.
type Kredensial struct {
	Pengguna string
	Sandi    string
}

func (k Kredensial) LogValue() slog.Value {
	return slog.GroupValue(
		slog.String("pengguna", k.Pengguna),
		slog.String("sandi", "***"),
	)
}

// Statistik membuktikan evaluasi lazy: LogValue hanya dipanggil ketika
// baris log benar-benar ditulis handler (level aktif).
type Statistik struct {
	hitung *int
}

func (s Statistik) LogValue() slog.Value {
	*s.hitung++
	return slog.IntValue(*s.hitung)
}

func main() {
	logger := slog.New(slog.NewTextHandler(os.Stdout, opsiDemo()))

	fmt.Println("--- atribut sumber pemanggilan (AddSource) ---")
	logger.Info("server siap", "port", 8080)
	logger.Warn("cache miss", "kunci", "pengguna:42")

	fmt.Println("--- LogValuer: kredensial tersamarkan ---")
	logger.Info("login berhasil", "kredensial", Kredensial{Pengguna: "budi", Sandi: "rahasia-123"})

	fmt.Println("--- evaluasi lazy pada level Debug yang dinonaktifkan ---")
	eval := 0
	logger.Debug("mode diagnosa", "statistik", Statistik{hitung: &eval})
	logger.Info("mode normal", "statistik", Statistik{hitung: &eval})
	fmt.Println("LogValue dipanggil:", eval, "kali")

	fmt.Println("--- slog.SetDefault ---")
	slog.SetDefault(logger)
	slog.Info("dari logger default")
}