Log Slog #
Logging adalah kebutuhan yang hampir universal di setiap aplikasi produksi — tapi bukan sekadar mencetak teks ke konsol. Di sistem terdistribusi, log harus bisa dicari, difilter, dan dianalisis secara efisien: artinya log harus terstruktur, punya level yang konsisten, dan membawa konteks yang cukup untuk menelusuri masalah. Package log/slog hadir di Go 1.21 sebagai solusi resmi untuk kebutuhan ini. Sebelum slog, developer harus memilih antara package log bawaan yang sangat sederhana, atau library eksternal seperti zap atau zerolog. Sekarang, slog menyediakan structured logging yang performan langsung dari standard library — dengan API yang bersih, dukungan level, output JSON atau teks, dan kemampuan kustomisasi penuh melalui interface Handler.
Gambaran Besar Package log/slog #
flowchart TD
App["Kode Aplikasi"] --> Logger["slog.Logger"]
Logger --> Level["Level Filter\nDebug / Info / Warn / Error"]
Level --> Handler["slog.Handler"]
Handler --> TH["TextHandler\noutput teks manusia"]
Handler --> JH["JSONHandler\noutput JSON mesin"]
Handler --> CH["Custom Handler\nimplementasi sendiri"]
TH --> Stdout["os.Stdout\nos.Stderr"]
JH --> Stdout
CH --> Any["File, Network,\nCloud Logging, dll"]
Logger --> Attr["Atribut\nKey-Value pairs"]
Logger --> Group["Group\nnamespace atribut"]
Logger --> Ctx["Context\nrequest-scoped attrs"]
style App fill:#4f86c6,color:#fff
style Logger fill:#e8f5e9
style Handler fill:#e3f2fd
style TH fill:#fff3e0
style JH fill:#fff3e0
style CH fill:#f3e5f5Mulai Cepat — Default Logger #
Package slog menyediakan fungsi-fungsi top-level yang menggunakan default logger — mudah digunakan tanpa konfigurasi apapun:
package main
import (
"log/slog"
"os"
)
func main() {
// Fungsi top-level — menggunakan default logger
slog.Info("aplikasi dimulai")
slog.Debug("ini tidak muncul secara default — level minimum adalah Info")
slog.Warn("penggunaan memori tinggi", "persen", 85)
slog.Error("koneksi database gagal", "host", "localhost", "port", 5432)
// Output (TextHandler, format teks):
// time=2024-03-15T14:30:00.000Z level=INFO msg="aplikasi dimulai"
// time=2024-03-15T14:30:00.001Z level=WARN msg="penggunaan memori tinggi" persen=85
// time=2024-03-15T14:30:00.002Z level=ERROR msg="koneksi database gagal" host=localhost port=5432
}
Default logger menggunakan TextHandler dengan output ke os.Stderr dan level minimum Info. Untuk aplikasi produksi, selalu buat logger sendiri dengan konfigurasi yang eksplisit.
Handler — TextHandler dan JSONHandler #
Handler menentukan bagaimana log ditulis — ke mana dan dalam format apa. Go menyediakan dua handler bawaan:
flowchart LR
subgraph Text["TextHandler"]
T1["Format teks yang mudah dibaca manusia"]
T2["time=... level=... msg=... key=val"]
T3["Ideal untuk development\ndan CLI tools"]
end
subgraph JSON["JSONHandler"]
J1["Format JSON yang mudah diproses mesin"]
J2["{time:..., level:..., msg:..., key:val}"]
J3["Ideal untuk production\ndan cloud logging"]
end
subgraph Config["HandlerOptions"]
C1["Level — filter minimum level"]
C2["AddSource — sertakan file:baris"]
C3["ReplaceAttr — ubah atribut"]
end
Config --> Text
Config --> JSONimport (
"log/slog"
"os"
)
// TextHandler — untuk development
func buatLoggerDev() *slog.Logger {
handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug, // tampilkan semua level
AddSource: true, // sertakan nama file dan nomor baris
})
return slog.New(handler)
}
// JSONHandler — untuk produksi
func buatLoggerProd() *slog.Logger {
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo, // hanya Info ke atas
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Ganti nama atribut waktu dari "time" ke "timestamp"
if a.Key == slog.TimeKey && len(groups) == 0 {
return slog.Attr{Key: "timestamp", Value: a.Value}
}
// Ganti nama level dari "level" ke "severity" (konvensi GCP)
if a.Key == slog.LevelKey && len(groups) == 0 {
return slog.Attr{Key: "severity", Value: a.Value}
}
return a
},
})
return slog.New(handler)
}
func main() {
// Set sebagai default logger global
logger := buatLoggerProd()
slog.SetDefault(logger)
// Sekarang slog.Info(), dll menggunakan logger yang baru
slog.Info("server dimulai", "port", 8080, "env", "production")
// Output JSON:
// {"timestamp":"2024-03-15T14:30:00Z","severity":"INFO","msg":"server dimulai","port":8080,"env":"production"}
}
Level Logging #
slog mendukung empat level bawaan, dan level kustom bisa ditambahkan:
flowchart LR
Debug["Debug\n-4\nInformasi detail\nuntuk debugging"] --> Info["Info\n0\nEvent normal\n(default minimum)"]
Info --> Warn["Warn\n4\nKondisi tidak normal\ntapi masih berjalan"]
Warn --> Error["Error\n8\nKegagalan yang perlu\nperhatian segera"]
style Debug fill:#e3f2fd
style Info fill:#e8f5e9
style Warn fill:#fff3e0
style Error fill:#fce4eclogger := slog.Default()
// Empat level standar
logger.Debug("memuat konfigurasi", "path", "/etc/app.yaml")
logger.Info("server berjalan", "addr", ":8080")
logger.Warn("disk hampir penuh", "persen_terpakai", 92)
logger.Error("gagal simpan data", "error", err)
// Cek apakah level aktif sebelum membuat pesan mahal
if logger.Enabled(context.Background(), slog.LevelDebug) {
// Operasi mahal ini hanya dijalankan jika Debug aktif
state := ambilStateDetailed() // mahal untuk dihitung
logger.Debug("state detail", "state", state)
}
// Level kustom — int di antara level standar
const LevelTrace = slog.Level(-8) // lebih rendah dari Debug
const LevelNotice = slog.Level(2) // antara Info dan Warn
const LevelFatal = slog.Level(12) // lebih tinggi dari Error
logger.Log(context.Background(), LevelTrace, "trace sangat detail")
logger.Log(context.Background(), LevelFatal, "error fatal, aplikasi berhenti")
// Mengubah level secara dinamis (tanpa restart)
var levelVar slog.LevelVar // zero value = LevelInfo
levelVar.Set(slog.LevelDebug) // ubah ke Debug saat runtime
handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: &levelVar, // pointer ke LevelVar
})
dynamicLogger := slog.New(handler)
dynamicLogger.Debug("ini sekarang muncul")
levelVar.Set(slog.LevelWarn) // ubah lagi
dynamicLogger.Info("ini sekarang tidak muncul")
Atribut — Key-Value yang Terstruktur #
Kekuatan utama slog adalah kemampuannya menyertakan atribut terstruktur bersama setiap pesan log. Ada beberapa cara untuk menambahkan atribut:
logger := slog.Default()
// Cara 1: alternating key-value (paling ringkas)
logger.Info("pesanan dibuat",
"id", 42,
"total", 150000.50,
"item_count", 3,
)
// Cara 2: slog.Attr (lebih eksplisit, lebih cepat)
logger.Info("pesanan dibuat",
slog.Int("id", 42),
slog.Float64("total", 150000.50),
slog.Int("item_count", 3),
)
// Cara 3: slog.Any untuk tipe apapun
logger.Info("pengguna login",
slog.Any("user", pengguna),
slog.Any("ip", net.ParseIP("192.168.1.1")),
)
// Tipe atribut yang tersedia
slog.String("nama", "Budi")
slog.Int("umur", 30)
slog.Int64("id", int64(12345678901))
slog.Uint64("bytes", uint64(1024))
slog.Float64("skor", 98.5)
slog.Bool("aktif", true)
slog.Time("dibuat", time.Now())
slog.Duration("elapsed", 250*time.Millisecond)
slog.Any("error", err)
slog.Any("data", map[string]any{"key": "val"})
// Error — konvensi: gunakan key "error" atau "err"
if err != nil {
logger.Error("operasi gagal",
"error", err, // akan memanggil err.Error() otomatis
"operasi", "simpanProduk",
"id", produkID,
)
}
Group — Mengelompokkan Atribut #
Group membuat namespace untuk atribut yang terkait, menghasilkan struktur yang lebih bersih di output JSON:
// Tanpa group — atribut datar
logger.Info("request masuk",
"method", "POST",
"path", "/api/produk",
"ip", "192.168.1.1",
"user_agent", "Mozilla/5.0",
"user_id", 42,
"user_nama", "Budi",
)
// Output JSON: {"msg":"request masuk","method":"POST","path":"/api/produk",...}
// Dengan group — atribut terstruktur
logger.Info("request masuk",
slog.Group("http",
slog.String("method", "POST"),
slog.String("path", "/api/produk"),
slog.String("ip", "192.168.1.1"),
slog.String("user_agent", "Mozilla/5.0"),
),
slog.Group("user",
slog.Int("id", 42),
slog.String("nama", "Budi"),
),
)
// Output JSON:
// {
// "msg": "request masuk",
// "http": {"method":"POST","path":"/api/produk","ip":"...","user_agent":"..."},
// "user": {"id":42,"nama":"Budi"}
// }
Logger dengan Atribut Tetap — With #
logger.With() membuat logger baru yang selalu menyertakan atribut tertentu di setiap log — berguna untuk menambahkan konteks yang konsisten seperti nama service, versi, atau request ID:
// Logger dasar
base := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// Logger dengan konteks service — atribut ini muncul di SETIAP log
serviceLogger := base.With(
slog.String("service", "order-service"),
slog.String("version", "1.2.3"),
slog.String("env", "production"),
)
serviceLogger.Info("server dimulai", "port", 8080)
// {"service":"order-service","version":"1.2.3","env":"production","msg":"server dimulai","port":8080}
serviceLogger.Error("koneksi DB gagal", "error", err)
// {"service":"order-service","version":"1.2.3","env":"production","msg":"koneksi DB gagal","error":"..."}
// Logger per request — tambahkan request ID
func handlerAPI(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = generateRequestID()
}
// Logger khusus request ini — mewarisi atribut dari serviceLogger
log := serviceLogger.With(
slog.String("request_id", requestID),
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
)
log.Info("request diterima")
hasil, err := prosesRequest(r)
if err != nil {
log.Error("request gagal", "error", err)
http.Error(w, "internal error", 500)
return
}
log.Info("request berhasil", "status", 200)
json.NewEncoder(w).Encode(hasil)
}
WithGroup — Namespace Tetap #
// Semua atribut berikutnya dimasukkan ke dalam group "db"
dbLogger := base.WithGroup("db")
dbLogger.Info("query dijalankan",
slog.String("query", "SELECT * FROM users"),
slog.Duration("duration", 45*time.Millisecond),
slog.Int("rows", 10),
)
// {"msg":"query dijalankan","db":{"query":"SELECT...","duration":"45ms","rows":10}}
Logging dengan Context #
slog mendukung logging yang terintegrasi dengan context.Context — memungkinkan middleware menyimpan atribut di context dan semua log berikutnya otomatis menyertakannya:
sequenceDiagram
participant MW as Middleware
participant Handler as HTTP Handler
participant Service as Service
participant Repo as Repository
MW->>MW: Buat logger dengan request_id, user_id
MW->>MW: Simpan logger di context
MW->>Handler: ctx dengan logger
Handler->>Handler: log := LoggerFromCtx(ctx)
Handler->>Handler: log.Info("proses request")
Handler->>Service: Service(ctx, ...)
Service->>Service: log := LoggerFromCtx(ctx)
Service->>Service: log.Info("validasi input")
Service->>Repo: Repo(ctx, ...)
Repo->>Repo: log := LoggerFromCtx(ctx)
Repo->>Repo: log.Info("eksekusi query")
Note over Handler,Repo: Semua log otomatis punya\nrequest_id dan user_id yang samatype contextKey string
const keyLogger contextKey = "logger"
// Simpan logger di context
func ContextDenganLogger(ctx context.Context, logger *slog.Logger) context.Context {
return context.WithValue(ctx, keyLogger, logger)
}
// Ambil logger dari context — fallback ke default jika tidak ada
func LoggerDariCtx(ctx context.Context) *slog.Logger {
if logger, ok := ctx.Value(keyLogger).(*slog.Logger); ok {
return logger
}
return slog.Default()
}
// Middleware: tambahkan request-scoped logger ke context
func middlewareSlog(base *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = generateRequestID()
}
// Logger dengan atribut request
log := base.With(
slog.String("request_id", requestID),
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.String("remote_addr", r.RemoteAddr),
)
// Simpan di context
ctx := ContextDenganLogger(r.Context(), log)
mulai := time.Now()
rw := &responseWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(rw, r.WithContext(ctx))
// Log setelah request selesai
log.Info("request selesai",
slog.Int("status", rw.statusCode),
slog.Duration("duration", time.Since(mulai)),
)
})
}
}
// Penggunaan di service — tidak perlu oper logger sebagai parameter
func serviceCariBuku(ctx context.Context, id int) (*Buku, error) {
log := LoggerDariCtx(ctx)
log.Debug("mencari buku", slog.Int("id", id))
buku, err := repoCariBuku(ctx, id)
if err != nil {
log.Error("gagal cari buku", slog.Int("id", id), slog.Any("error", err))
return nil, err
}
log.Debug("buku ditemukan", slog.String("judul", buku.Judul))
return buku, nil
}
Custom Handler #
Untuk kebutuhan logging yang tidak bisa dipenuhi oleh TextHandler atau JSONHandler, implementasikan interface slog.Handler:
type Handler interface {
Enabled(context.Context, Level) bool
Handle(context.Context, Record) error
WithAttrs(attrs []Attr) Handler
WithGroup(name string) Handler
}
Contoh: Multi-Handler #
// Handler yang mengirim log ke beberapa tujuan sekaligus
type MultiHandler struct {
handlers []slog.Handler
}
func NewMultiHandler(handlers ...slog.Handler) *MultiHandler {
return &MultiHandler{handlers: handlers}
}
func (h *MultiHandler) Enabled(ctx context.Context, level slog.Level) bool {
for _, handler := range h.handlers {
if handler.Enabled(ctx, level) {
return true
}
}
return false
}
func (h *MultiHandler) Handle(ctx context.Context, r slog.Record) error {
var errs []error
for _, handler := range h.handlers {
if handler.Enabled(ctx, r.Level) {
if err := handler.Handle(ctx, r.Clone()); err != nil {
errs = append(errs, err)
}
}
}
return errors.Join(errs...)
}
func (h *MultiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
handlers := make([]slog.Handler, len(h.handlers))
for i, handler := range h.handlers {
handlers[i] = handler.WithAttrs(attrs)
}
return &MultiHandler{handlers: handlers}
}
func (h *MultiHandler) WithGroup(name string) slog.Handler {
handlers := make([]slog.Handler, len(h.handlers))
for i, handler := range h.handlers {
handlers[i] = handler.WithGroup(name)
}
return &MultiHandler{handlers: handlers}
}
// Penggunaan: log ke stdout (teks) DAN file (JSON)
func buatMultiLogger() *slog.Logger {
logFile, _ := os.OpenFile("app.log",
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
textHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
jsonHandler := slog.NewJSONHandler(logFile, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
return slog.New(NewMultiHandler(textHandler, jsonHandler))
}
Contoh: Handler dengan Sampling #
// Handler yang hanya log sebagian pesan Debug untuk mengurangi volume
type SamplingHandler struct {
handler slog.Handler
rate int // log 1 dari N pesan Debug
counter atomic.Int64
}
func (h *SamplingHandler) Enabled(ctx context.Context, level slog.Level) bool {
if level > slog.LevelDebug {
return h.handler.Enabled(ctx, level)
}
// Untuk Debug: hanya enabled 1 dari N kali
return h.counter.Add(1)%int64(h.rate) == 0
}
func (h *SamplingHandler) Handle(ctx context.Context, r slog.Record) error {
return h.handler.Handle(ctx, r)
}
func (h *SamplingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &SamplingHandler{
handler: h.handler.WithAttrs(attrs),
rate: h.rate,
}
}
func (h *SamplingHandler) WithGroup(name string) slog.Handler {
return &SamplingHandler{
handler: h.handler.WithGroup(name),
rate: h.rate,
}
}
Migrasi dari log ke log/slog #
Jika kamu punya kode yang menggunakan package log lama, migrasi ke slog mudah dilakukan secara bertahap:
import (
"log"
"log/slog"
"os"
)
// Package log lama — tidak terstruktur
log.Printf("server dimulai di port %d", 8080)
log.Printf("error: %v", err)
// slog — terstruktur
slog.Info("server dimulai", "port", 8080)
slog.Error("operasi gagal", "error", err)
// Redirect output log lama ke slog (untuk migrasi bertahap)
// Semua log.Printf akan lewat slog sebagai level Info
slogHandler := slog.NewJSONHandler(os.Stdout, nil)
slogLogger := slog.New(slogHandler)
slog.SetDefault(slogLogger)
// log.Default() sekarang menulis ke slog
log.SetOutput(slog.NewLogLogger(slogLogger.Handler(), slog.LevelInfo).Writer())
Pola Penggunaan di Produksi #
Setup Logger Produksi yang Lengkap #
package main
import (
"log/slog"
"os"
)
func setupLogger(env, versi string) *slog.Logger {
var level slog.Level
switch env {
case "production":
level = slog.LevelInfo
case "staging":
level = slog.LevelDebug
default: // development
level = slog.LevelDebug
}
opts := &slog.HandlerOptions{
Level: level,
AddSource: env != "production", // source hanya di non-prod
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Format waktu sebagai Unix timestamp untuk efisiensi parsing
if a.Key == slog.TimeKey && len(groups) == 0 {
return slog.Int64("ts", a.Value.Time().Unix())
}
return a
},
}
var handler slog.Handler
if env == "production" || env == "staging" {
handler = slog.NewJSONHandler(os.Stdout, opts)
} else {
handler = slog.NewTextHandler(os.Stdout, opts)
}
return slog.New(handler).With(
slog.String("service", "myapp"),
slog.String("version", versi),
slog.String("env", env),
)
}
func main() {
env := os.Getenv("APP_ENV")
if env == "" {
env = "development"
}
logger := setupLogger(env, "1.2.3")
slog.SetDefault(logger)
logger.Info("aplikasi dimulai",
slog.String("go_version", runtime.Version()),
slog.Int("pid", os.Getpid()),
)
}
Logging Performa Operasi #
// Decorator untuk mengukur dan log durasi operasi
func denganLog(ctx context.Context, operasi string, fn func() error) error {
log := LoggerDariCtx(ctx)
log.Debug("mulai " + operasi)
mulai := time.Now()
err := fn()
durasi := time.Since(mulai)
if err != nil {
log.Error("gagal "+operasi,
slog.Duration("duration", durasi),
slog.Any("error", err),
)
return err
}
log.Info("selesai "+operasi,
slog.Duration("duration", durasi),
)
return nil
}
// Penggunaan
func serviceProsesPembayaran(ctx context.Context, p Pembayaran) error {
return denganLog(ctx, "proses pembayaran", func() error {
if err := validasiPembayaran(ctx, p); err != nil {
return err
}
return simpanPembayaran(ctx, p)
})
}
Structured Error Logging #
// Helper untuk log error dengan konteks yang kaya
func logError(ctx context.Context, msg string, err error, attrs ...slog.Attr) {
log := LoggerDariCtx(ctx)
// Kumpulkan atribut error
allAttrs := []slog.Attr{slog.Any("error", err)}
// Tambahkan stack trace jika error mendukungnya
type stackTracer interface {
StackTrace() []string
}
if st, ok := err.(stackTracer); ok {
allAttrs = append(allAttrs,
slog.Any("stack_trace", st.StackTrace()))
}
allAttrs = append(allAttrs, attrs...)
args := make([]any, len(allAttrs))
for i, a := range allAttrs {
args[i] = a
}
log.Error(msg, args...)
}
// Penggunaan
func handlerBuatPesanan(w http.ResponseWriter, r *http.Request) {
pesanan, err := serviceBuatPesanan(r.Context(), input)
if err != nil {
logError(r.Context(), "gagal buat pesanan", err,
slog.Int("user_id", userID),
slog.String("produk", input.ProdukID),
)
http.Error(w, "gagal", 500)
return
}
LoggerDariCtx(r.Context()).Info("pesanan berhasil dibuat",
slog.Int("pesanan_id", pesanan.ID),
slog.Float64("total", pesanan.Total),
)
}
Kapan Beralih ke Alternatif #
Tetap gunakan log/slog jika:
✓ Structured logging dengan level untuk semua aplikasi Go 1.21+
✓ Output JSON untuk cloud logging (GCP, AWS CloudWatch, ELK)
✓ Kustomisasi via custom Handler
✓ Integrasi dengan context untuk request-scoped logging
✓ Ingin zero external dependency
Pertimbangkan package log lama jika:
✗ Menggunakan Go < 1.21 dan tidak bisa upgrade
✗ Logging sangat sederhana tanpa kebutuhan struktur
Pertimbangkan library eksternal jika:
✗ Performa sangat kritikal dengan volume log sangat tinggi
→ zap (uber-go/zap) — zero-allocation, sangat cepat
→ zerolog (rs/zerolog) — zero-allocation, API berantai
✗ Fitur yang belum ada di slog:
→ Log rotation → lumberjack
→ Sampling built-in → zap
→ Hooks untuk kirim ke Sentry/Datadog → logrus (tapi lebih lambat)
✗ Tim sudah familiar dan ekosistem sudah terikat ke zap/zerolog
Ringkasan #
slogadalah pilihan logging default untuk semua project Go 1.21+ — menggantikanlog,logrus, danzapuntuk kasus umum tanpa dependensi eksternal.JSONHandleruntuk produksi,TextHandleruntuk development — JSON mudah diproses oleh sistem log aggregation, teks mudah dibaca manusia di terminal.logger.With()untuk konteks tetap — buat logger dengan atribut yang selalu muncul (service name, version, request ID) alih-alih menambahkannya secara manual di setiap log.- Simpan logger di context untuk request-scoped logging — middleware menambahkan request ID dan user info ke logger, dan semua layer bawah menggunakannya tanpa oper logger sebagai parameter.
slog.Attrlebih cepat dari alternating key-value —slog.String("key", val)menghindari alokasi refleksi dibanding"key", val. GunakanAttrdi hot path.logger.Enabled(ctx, level)sebelum operasi logging yang mahal — hindari membuat pesan debug yang kompleks jika level Debug tidak aktif.LevelVaruntuk mengubah level tanpa restart — ekspos endpoint HTTP untuk mengubah log level di runtime saat debugging di produksi.ReplaceAttruntuk normalisasi — standardisasi nama field (misaltime→timestamp) agar kompatibel dengan format yang diharapkan sistem log aggregation.slog.SetDefault(logger)agarslog.Info()dll menggunakan logger yang sudah dikonfigurasi — jangan andalkan default logger di aplikasi produksi.