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:
| Handler | Format | Kapan dipakai |
|---|---|---|
slog.NewTextHandler | kunci=nilai satu baris | terminal manusia, development |
slog.NewJSONHandler | satu objek JSON per baris | agregator 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:
reqLogger := logger.With("layanan", "pesanan", "request_id", "req-001")
reqLogger.Info("validasi selesai")
// turunan ini tetap mewarisi request_id tanpa menulisnya ulanglogger.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:
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.
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:
--- 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:
- Baris 14-34 (
opsiDemo): membangun opsi handler dengan ambangLevelInfo,AddSource, danReplaceAttrdua tugas — mengganti atributtimedengan string tetap serta memangkas path sumber menjadi nama berkas plus nomor baris lewatfilepath.Base. - Baris 38-48 (
Kredensial+LogValue): methodLogValuemengembalikanslog.GroupValuesehingga handler mencetak dua field berawalankredensial.; nilaiSanditidak pernah dikirim ke handler. - Baris 52-59 (
Statistik): setiap evaluasiLogValuemenaikkan*s.hitung— penghitung inilah bukti nyata kapan evaluasi terjadi. - Baris 62: satu handler
TextHandlerkeos.Stdoutdibungkusslog.New; seluruh pemanggilan berikutnya memakai logger ini. - Baris 65-66: dua log biasa memperlihatkan atribut
source— perhatikan nomor baris yang menunjuk persis ke lokasi pemanggilanInfodanWarn. - Baris 69: field
kredensialdirender menjadikredensial.pengguna=budi kredensial.sandi=***— perilaku group dariGroupValuebekerja sama sepertiWithGroup. - Baris 72-75:
evaldimulai dari nol;Debugpada baris 73 dibuang karena di bawah ambang,Infopada baris 74 dievaluasi sekali, dan baris 75 mencetakLogValue dipanggil: 1 kali. - Baris 78-79:
slog.SetDefaultmenjadikan logger ini tujuan seluruh pemanggilan paket-level sepertislog.Info— penting agar library pihak ketiga yang memakaislogdefault 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:
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 #
- Dokumentasi package log/slog
— spesifikasi
Logger,Handler,LogValuer, danHandlerOptions. - Structured logging with slog (Go Blog) — latar desain API, termasuk evaluasi lazy.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
slog-obs-demo.go
Unduh berkaspackage 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")
}