← Semua pembelajaran / Go Nol → Enterprise
Fase 5 · Arsitektur Aplikasi

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.

Sumber asli pkg.go.dev Resmi Rangkuman ~7 menit baca

Intisari

  • slog ada 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, ...), bukan slog.Info — hanya varian context yang bisa membawa trace dan request ID.
  • Handler kustom yang menarik atribut dari context adalah cara membuat request_id muncul 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

LevelUntukDi produksi
DebugNilai antara saat mengembangkanMati — sering membocorkan data
InfoPeristiwa bisnis: pesanan dibuat, pembayaran lunasNyala
WarnPulih sendiri: retry berhasil, cache gagal lalu jatuh ke DBNyala
ErrorPermintaan gagal, pekerjaan tidak selesaiNyala — 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

JanganSebagai gantinya
Sandi, token, kunci APITipe Rahasia dengan LogValue()
Seluruh body permintaanBeberapa field yang relevan saja
Nomor kartu, NIK, data pribadiID internal yang bisa ditelusuri lewat sistem lain
Stack trace untuk error 4xxSatu baris Info; 4xx adalah perilaku normal
Satu baris log per iterasi loopSatu 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?

slogzap / zerolog
DependensiNolSatu
Alokasi per barisRendah dengan atribut bertipeSedikit lebih rendah
Integrasi ekosistemStandar — pustaka lain memakainyaPerlu adaptor
Kapan menangHampir semua aplikasiJutaan 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.