Panduan Lengkap Structured Logging di Go Menggunakan log/slog
Panduan praktis memakai log/slog di backend Go: JSON handler, level dinamis, trace ID, masking data sensitif, dan middleware HTTP.
Saat menulis kode Go, salah satu cara paling umum untuk mencatat informasi proses adalah memakai log.Println atau log.Printf. Output-nya sederhana: sebaris teks biasa, misalnya User 123 gagal checkout: saldo tidak cukup.
Untuk kebutuhan pengembangan, format seperti ini sudah enak dibaca. Masalahnya mulai terasa ketika aplikasi berjalan di production dan menghasilkan ribuan baris log. Sistem hanya melihatnya sebagai teks; ia tidak tahu mana ID pengguna, mana jenis transaksi, dan mana pesan error.
Akibatnya, mencari kasus tertentu jadi lebih merepotkan. Misalnya, saat ingin melihat semua transaksi gagal untuk User 123, kita harus mengandalkan pencarian teks dan berharap format pesannya selalu sama.
Structured logging menyelesaikan masalah tersebut dengan mencatat data sebagai pasangan key-value, biasanya dalam bentuk JSON. Jadi, informasi seperti user_id, event, dan error mempunyai label sendiri. Log lebih mudah difilter, dikelompokkan, dan dipakai untuk mencari masalah.

Daftar Isi
- Mengapa Memilih log/slog?
- Arsitektur Inti: Logger vs Handler
- Bekerja dengan Structured Attributes
- Membawa Trace ID lewat context
- Keamanan Data: Masking Sensitif dengan LogValuer
- Merapikan Output dengan Handler
- Middleware HTTP untuk Mencatat Request
- Dari os.Stdout ke Log Aggregator
- Berbagi Logger dengan Library Lain
- Tiga Jebakan yang Sering Terlewat
- Penutup
- Bacaan Lanjutan
Mengapa Memilih log/slog?
Sebelum adanya Go 1.21, saat kita ingin membuat log terstruktur di Go, pilihannya biasanya hanya ke beberapa library populer berikut ini:
- Logrus: Dulu sangat populer karena mudah dipakai. Saat ini proyeknya berstatus maintenance mode. Jika performa logging menjadi pertimbangan utama, sebaiknya bandingkan lewat benchmark yang mendekati beban aplikasi sendiri.
- Zap & Zerolog: Dirancang untuk performa tinggi dan penggunaan alokasi yang rendah. Keduanya tetap library pihak ketiga. Saat memakai beragam modul open-source, masing-masing modul juga bisa membawa dependensi logger yang berbeda sehingga format log menjadi tidak seragam.
Kehadiran log/slog menjadi alternatif pilihan standar resmi untuk ekosistem Go:
- Bawaan standard library: Tidak menambah dependensi eksternal di
go.moddan mengikuti Go 1 compatibility promise yang berlaku untuk API standard library, dengan pengecualian umum dalam kebijakan kompatibilitas Go. - Interface Bersama: Pembuat library (client database, message broker, HTTP router) bisa menerima
*slog.Loggersebagai kontrak standar tanpa memaksa pengguna menginstal pustaka logging tertentu. - Pilihan Performa: Menyediakan API info standar (
slog.Info) untuk alur bisnis umum, sekaligus API strongly-typed (slog.Attr,LogAttrs) saat bekerja di jalur kritis (hot path) yang membutuhkan minim alokasi heap.
Arsitektur Inti: Logger vs Handler
log/slog memisahkan tanggung jawab logging menjadi dua komponen utama:
[Aplikasi Go] ---> [slog.Logger] (Frontend API: Info, Warn, Error)
|
v (meneruskan Record)
[slog.Handler] (Backend Formatter & Writer)
|
+------------+------------+
| |
[TextHandler] [JSONHandler]
(Terminal/Dev) (Production/Stdout)slog.Logger: Adalah fungsi/method yang dipanggil di kode aplikasi (slog.Info(),slog.Error(), dll). Tugasnya mengemas pesan dan atribut ke dalam struktur dataslog.Record.slog.Handler: Bagian pemrosesan (backend). Handler menentukan apakah level log diizinkan, bagaimana data diformat (teks biasa atau JSON), dan ke mana log ditulis (io.Writer).
TextHandler vs JSONHandler
Go menyediakan dua handler bawaan yang bisa langsung kita gunakan:
slog.NewTextHandler: Menghasilkan format tekskey=value. Format ini lebih nyaman dibaca langsung di terminal pada saat pengembangan.slog.NewJSONHandler: Menghasilkan baris log dalam format JSON. Ini adalah format standar yang biasanya digunakan di zona production agar sistem agregasi log bisa memproses datanya secara otomatis.
Contoh perbandingannya:
package main
import (
"log/slog"
"os"
)
func main() {
// TextHandler untuk development lokal
textHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
devLogger := slog.New(textHandler)
devLogger.Info("Aplikasi berjalan di mode development", "env", "local", "port", 8080)
// JSONHandler untuk production
jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
prodLogger := slog.New(jsonHandler)
prodLogger.Info("Aplikasi berjalan di mode production", "env", "production", "port", 8080)
}Output di terminal:
time=2026-03-14T08:30:00.000+07:00 level=INFO msg="Aplikasi berjalan di mode development" env=local port=8080
{"time":"2026-03-14T08:30:00.000+07:00","level":"INFO","msg":"Aplikasi berjalan di mode production","env":"production","port":8080}Leveling Log dan Dynamic LevelVar
log/slog membagi prioritas log ke dalam empat tingkatan default:
| Level | Nilai Integer | Kapan Digunakan |
|---|---|---|
LevelDebug |
-4 |
Diagnostik mendalam, payload detail, atau tracing internal saat debugging |
LevelInfo |
0 |
Aktivitas normal sistem (service start, proses transaksi selesai) |
LevelWarn |
4 |
Kejadian tak terduga yang tidak menghentikan sistem (retry koneksi, disk mendekati batas) |
LevelError |
8 |
Kegagalan proses (koneksi payment timeout, query database gagal total) |
Pada zona production, level log biasanya diatur pada LevelInfo agar kapasitas penyimpanan tidak cepat habis oleh pesan-pesan diagnostik. Masalahnya, saat muncul kendala atau bug tertentu di server, kita sering kali butuh memunculkan log Debug tanpa harus mematikan atau me-restart aplikasi.
Untuk kebutuhan ini, kita bisa memanfaatkan slog.LevelVar. Fitur ini memungkinkan kita mengubah level log aplikasi secara dinamis saat sistem sedang berjalan (runtime), contohnya pada endpoint HTTP khusus admin:
package main
import (
"log/slog"
"os"
)
func main() {
// LevelVar dinamis (default: Info)
programLevel := new(slog.LevelVar)
programLevel.Set(slog.LevelInfo)
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: programLevel,
})
logger := slog.New(handler)
// Log Debug ini diabaikan karena level aktif masih INFO
logger.Debug("Pesan diagnostik tersembunyi")
// Ubah level log ke DEBUG saat runtime (misal dipicu HTTP admin endpoint)
programLevel.Set(slog.LevelDebug)
// Sekarang log Debug langsung tercetak tanpa restart server
logger.Debug("Pesan diagnostik muncul seketika setelah level diubah!")
}Bekerja dengan Structured Attributes
Loosely-Typed vs Strongly-Typed (Minim Alokasi)
Saat menambahkan atribut ke log, slog menyediakan dua pendekatan:
1. Loosely-Typed (Key-Value Bergantian) Pendekatan ini paling sederhana. Kita tinggal memasukkan nama key (string) dan nilainya (any) secara bergantian:
slog.Info("Pembayaran berhasil", "order_id", "ORD-1234", "amount", 150000, "is_guest", false)2. Strongly-Typed (`slog.Attr`) Pada fungsi yang dieksekusi ratusan ribu kali per detik (hot path), passing tipe any dapat menyebabkan alokasi memori ke heap (escape analysis). Untuk skenario dimana performa harus diutamakan, maka pendekatan loosely ini menjadi kurang tepat. Go menyediakan konstruktor strongly-typed seperti slog.String(), slog.Int(), slog.Duration(), yang dipanggil melalui method logger.LogAttrs():
logger.LogAttrs(ctx, slog.LevelInfo, "Pembayaran berhasil",
slog.String("order_id", "ORD-1234"),
slog.Int("amount", 150000),
slog.Bool("is_guest", false),
slog.Duration("processing_time", 42*time.Millisecond),
)Menurut saya, untuk sebagian besar kode bisnis umum, gaya loosely-typedslog.Info("...", "key", val)sudah sangat memadai. Keterbacaan kode lebih berharga ketimbang optimasi dini. Gunakanslog.AttrdanLogAttrs()jika profiling memori memang menunjukkan alokasi logging menjadi bottleneck.
Grouping dan Scoping Atribut
Sub-Logger dengan logger.With() . Jika ada atribut yang harus selalu muncul di setiap baris log pada suatu modul (misalnya nama service atau versi), maka kita bisa menambahkan sub-logger dengan With():
orderLogger := logger.With(
slog.String("service", "order-service"),
slog.String("version", "v2.1.0"),
)
// Log otomatis menyertakan service="order-service" dan version="v2.1.0"
orderLogger.Info("Worker pemrosesan order berhasil diinisialisasi")Pengelompokan dengan slog.Group . Bila log aggregator mendukung format JSON bertingkat (nested JSON), atribut terkait bisa dikelompokkan:
logger.Info("Aktivitas login terdeteksi",
slog.Group("user",
slog.Int("id", 101),
slog.String("email", "[email protected]"),
slog.String("role", "admin"),
),
slog.Group("network",
slog.String("ip", "203.0.113.195"),
slog.String("user_agent", "Mozilla/5.0"),
),
)Hasil JSON-nya akan lebih terstruktur rapi:
{
"time": "2026-03-14T08:35:10.123Z",
"level": "INFO",
"msg": "Aktivitas login terdeteksi",
"user": {
"id": 101,
"email": "[email protected]",
"role": "admin"
},
"network": {
"ip": "203.0.113.195",
"user_agent": "Mozilla/5.0"
}
}Membawa Trace ID lewat context
Pada backend yang menangani banyak request bersamaan atau terdiri dari beberapa service, menelusuri alur satu request biasanya membutuhkan penanda khusus yang unique , biasanya disebut _trace_id_ atau _request_id_.
Go sudah menyediakan fungsi log berbasis context tersebut, seperti slog.InfoContext(ctx, ...) dan slog.ErrorContext(ctx, ...). Masalahnya, jika developer harus menyisipkan trace_id secara manual di setiap baris kode pencatatan log, maka cara ini sangat mungkin akan terlewat.
Solusi yang lebih rapi adalah dengan membuat wrapper custom pada slog.Handler. Handler ini bertugas memeriksa context.Context, mengekstrak nilai _trace_id_, lalu memasukkannya secara otomatis ke baris log sebelum dicetak:
package main
import (
"context"
"log/slog"
"os"
)
type contextKey string
const (
TraceIDKey contextKey = "trace_id"
RequestIDKey contextKey = "request_id"
)
// ContextHandler membungkus handler bawaan dan menginjeksi metadata dari context
type ContextHandler struct {
slog.Handler
}
func (h ContextHandler) Handle(ctx context.Context, r slog.Record) error {
if ctx != nil {
if traceID, ok := ctx.Value(TraceIDKey).(string); ok && traceID != "" {
r.AddAttrs(slog.String("trace_id", traceID))
}
if reqID, ok := ctx.Value(RequestIDKey).(string); ok && reqID != "" {
r.AddAttrs(slog.String("request_id", reqID))
}
}
return h.Handler.Handle(ctx, r)
}Dengan cara ini, maka pemakaian di kode aplikasi menjadi lebih praktis. Kita cukup memanggil InfoContext dengan context yang aktif:
func main() {
baseJSONHandler := slog.NewJSONHandler(os.Stdout, nil)
customHandler := ContextHandler{Handler: baseJSONHandler}
logger := slog.New(customHandler)
// Context membawa Trace ID (misal dari OpenTelemetry atau API Gateway)
ctx := context.WithValue(context.Background(), TraceIDKey, "c4b9-4f81-9b7e-9023")
// Trace ID otomatis tersemat di output log
logger.InfoContext(ctx, "Query database selesai", "duration_ms", 12)
}Output log JSON yang dihasilkan:
{
"time": "2026-03-14T08:40:00.000Z",
"level": "INFO",
"msg": "Query database selesai",
"duration_ms": 12,
"trace_id": "c4b9-4f81-9b7e-9023"
}
Keamanan Data: Masking Sensitif dengan LogValuer
Kebocoran data rahasia sering kali terjadi karena hal sederhana: saat mencari penyebab masalah, kita mencetak seluruh struct data pengguna ke log, sehingga data seperti password plaintext, nomor token, atau API key ikut terkirim ke sistem pencatat log.
log/slog telah mengantisipasi masalah ini melalui fitur interface slog.LogValuer:

type LogValuer interface {
LogValue() slog.Value
}Ketika sebuah struct mengimplementasikan method LogValue(), handler slog tidak akan mencetak nilai asli dari struct tersebut, melainkan mencetak data yang sudah kita amankan melalui method itu.
Contoh implementasinya:
package main
import (
"log/slog"
"os"
)
type UserProfile struct {
ID int
Username string
Password string
APIKey string
}
// LogValue memastikan password dan API key tersanitasi sebelum dicetak
func (u UserProfile) LogValue() slog.Value {
return slog.GroupValue(
slog.Int("id", u.ID),
slog.String("username", u.Username),
slog.String("password", "[REDACTED]"),
slog.String("api_key", maskSecret(u.APIKey)),
)
}
func maskSecret(s string) string {
if len(s) <= 4 {
return "****"
}
return s[:2] + "****" + s[len(s)-2:]
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
user := UserProfile{
ID: 42,
Username: "budi_santoso",
Password: "RahasiaSuperAman123!",
APIKey: "sk_live_9981273918239018",
}
// Walaupun passing struct user utuh, nilainya otomatis tersanitasi
logger.Info("Pendaftaran akun pengguna baru berhasil", slog.Any("user", user))
}Output log JSON yang aman:
{
"time": "2026-03-14T08:42:00.000Z",
"level": "INFO",
"msg": "Pendaftaran akun pengguna baru berhasil",
"user": {
"id": 42,
"username": "budi_santoso",
"password": "[REDACTED]",
"api_key": "sk****18"
}
}Merapikan Output dengan Handler
slog.HandlerOptions menyediakan opsi penyesuaian format output:
type HandlerOptions struct {
AddSource bool
Level Leveler
ReplaceAttr func(groups []string, a Attr) Attr
}Menambahkan Caller Source (AddSource)
Dengan mengaktifkan AddSource: true, handler akan otomatis menyertakan fungsi, nama file, dan baris kode tempat log dipanggil:
opts := &slog.HandlerOptions{
AddSource: true,
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, opts))
logger.Error("Koneksi ke database PostgreSQL terputus")Output:
{
"time": "2026-03-14T08:45:00.000Z",
"level": "ERROR",
"source": {
"function": "main.main",
"file": "/app/cmd/server/main.go",
"line": 48
},
"msg": "Koneksi ke database PostgreSQL terputus"
}Transformasi Atribut dengan ReplaceAttr
Fungsi ReplaceAttr dieksekusi pada setiap atribut sebelum dicetak. Fitur ini berguna untuk:
- Menyeragamkan Key: Mengubah
msgmenjadimessage(standar Elasticsearch ECS / Datadog), ataulevelmenjadiseverity(Google Cloud Logging). - Format Timestamp: Mengunci format waktu ke UTC RFC 3339.
- Memotong File Path: Mengubah path absolut menjadi nama file relatif agar log lebih ringkas dan tidak mengekspos direktori mesin server.
Contoh konfigurasi logger production:
package main
import (
"log/slog"
"os"
"path/filepath"
"time"
)
func setupProductionLogger() *slog.Logger {
opts := &slog.HandlerOptions{
Level: slog.LevelInfo,
AddSource: true,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Format waktu menjadi UTC RFC 3339
if a.Key == slog.TimeKey {
return slog.String("timestamp", a.Value.Time().UTC().Format(time.RFC3339))
}
// Sesuaikan key "msg" menjadi "message"
if a.Key == slog.MessageKey {
return slog.Attr{Key: "message", Value: a.Value}
}
// Ubah "level" menjadi "severity"
if a.Key == slog.LevelKey {
return slog.Attr{Key: "severity", Value: a.Value}
}
// Potong path absolut agar hanya menampilkan nama file
if a.Key == slog.SourceKey {
source, ok := a.Value.Any().(*slog.Source)
if ok && source != nil {
source.File = filepath.Base(source.File)
}
}
return a
},
}
return slog.New(slog.NewJSONHandler(os.Stdout, opts))
}Output JSON menjadi rapi dan lebih mudah disesuaikan dengan skema ingestion yang dipakai:
{
"timestamp": "2026-03-14T01:45:00Z",
"severity": "INFO",
"source": {
"function": "main.main",
"file": "main.go",
"line": 25
},
"message": "Worker pool diinisialisasi"
}Middleware HTTP untuk Mencatat Request
Berikut ini adalah contoh implementasi dari teori di atas, yaitu dalam bentuk HTTP Logging Middleware standar (net/http) yang bisa digunakan langsung:
- Membaca
X-Request-IDdari header request atau membuat ID baru jika kosong. - Menyimpan Request ID ke dalam context.
- Menggunakan wrapper
ResponseWriteruntuk menangkap HTTP status code. - Menghitung latency pemrosesan.
- Menyesuaikan level log berdasarkan status HTTP (5xx jadi Error, 4xx jadi Warn, lainnya Info).
package main
import (
"context"
"crypto/rand"
"encoding/hex"
"log/slog"
"net/http"
"os"
"time"
)
type contextKey string
const RequestIDKey contextKey = "request_id"
type responseWriterInterceptor struct {
http.ResponseWriter
statusCode int
}
func (w *responseWriterInterceptor) WriteHeader(statusCode int) {
w.statusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
func generateRequestID() string {
bytes := make([]byte, 8)
_, _ = rand.Read(bytes)
return hex.EncodeToString(bytes)
}
func HTTPLoggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = generateRequestID()
}
ctx := context.WithValue(r.Context(), RequestIDKey, reqID)
r = r.WithContext(ctx)
interceptor := &responseWriterInterceptor{
ResponseWriter: w,
statusCode: http.StatusOK,
}
next.ServeHTTP(interceptor, r)
duration := time.Since(start)
level := slog.LevelInfo
if interceptor.statusCode >= 500 {
level = slog.LevelError
} else if interceptor.statusCode >= 400 {
level = slog.LevelWarn
}
logger.LogAttrs(ctx, level, "HTTP Request Selesai",
slog.String("request_id", reqID),
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.Int("status", interceptor.statusCode),
slog.Duration("latency", duration),
slog.String("ip", r.RemoteAddr),
slog.String("user_agent", r.UserAgent()),
)
})
}
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"UP"}`))
})
loggedMux := HTTPLoggingMiddleware(logger)(mux)
logger.Info("Server API berjalan pada port :8080")
_ = http.ListenAndServe(":8080", loggedMux)
}Ketika endpoint diakses, log JSON dengan latency dan status code tercatat:
{
"time": "2026-03-14T08:50:15.821Z",
"level": "INFO",
"msg": "HTTP Request Selesai",
"request_id": "8f3b20c91a457de1",
"method": "GET",
"path": "/api/v1/health",
"status": 200,
"latency": 45120,
"ip": "127.0.0.1:52134",
"user_agent": "curl/8.7.1"
}
Dari os.Stdout ke Log Aggregator
Pada seluruh contoh konfigurasi di atas, handler selalu diarahkan ke os.Stdout:
handler := slog.NewJSONHandler(os.Stdout, opts)Mungkin timbul pertanyaan praktis: mengapa log dicetak ke os.Stdout (layar terminal) dan bukan disimpan ke file log lokal di server? Pada deployment berbasis container seperti Docker atau Kubernetes, menyimpan file lokal sebagai satu-satunya sumber log biasanya membawa beberapa masalah:
- Pod Bersifat Ephemeral: Container bisa di-restart, dipindahkan antar-node, atau di-scale down kapan saja. Saat container musnah, file log lokal ikut hilang.
- Sulit Ditelusuri saat Scale Out: Jika aplikasi berjalan di 20 pod, memeriksa file lokal satu per satu di tiap pod jelas tidak praktis.
- Risiko Disk Penuh: Berkas log yang menumpuk di layer container bisa memenuhi disk node host (disk pressure).
Oleh karena itu, praktik yang umum digunakan (mengikuti prinsip Twelve-Factor App) adalah membiarkan aplikasi Go mencetak stream log ke os.Stdout secepat mungkin. Urusan mengumpulkan, menyaring, dan menyimpan log diserahkan ke agen pengumpul log (log shipper):
[ Pod 1 (Go slog) ] --(stdout)--> \
[ Pod 2 (Go slog) ] --(stdout)--> [ Container Runtime (containerd) ]
[ Pod 3 (Go slog) ] --(stdout)--> Menulis stream ke /var/log/pods/ di Node Host
|
v
[ Log Shipper: Fluent Bit / Promtail ]
(Berjalan sebagai DaemonSet per Node)
- Membaca berkas log host
- Menginjeksi metadata k8s (pod, namespace)
|
v (Kirim batch via HTTP/gRPC)
[ Centralized Log Aggregator ]
(Grafana Loki / Elasticsearch / Datadog)
|
Disimpan di Object Storage (S3 / MinIO)
|
v
[ Dashboard Observabilitas ]
(Grafana / Kibana untuk query terpusat)
Aplikasi Go kita cukup fokus memformat log ke JSON dan mengalirkannya ke stdout. Agen log shipper (seperti Promtail atau Fluent Bit yang berjalan sebagai DaemonSet pada tiap node server) akan membaca berkas log runtime, menambahkan metadata (seperti nama pod dan namespace), lalu mengirimkannya secara efisien ke sistem agregasi sentral (seperti Grafana Loki atau Elasticsearch).
Karena log sudah berformat JSON dan memiliki label yang jelas, pencarian masalah di dashboard menjadi sangat mudah. Contohnya pada Grafana Loki menggunakan query LogQL:
{app="order-service", env="production"} | json | trace_id = "c4b9-4f81-9b7e-9023"Atau memfilter request yang lambat dan error:
{app="order-service"} | json | latency > 500ms and status >= 500Berbagi Logger dengan Library Lain
Menjadikan slog Sebagai Default Logger
Jika pada codebase lama kita sudah terlanjur banyak memanggil fungsi global slog.Info(...) tanpa menyuntikkan instance logger secara eksplisit, kita bisa menetapkan logger kustom sebagai default global di fungsi main():
slog.SetDefault(logger)Sementara untuk library eksternal yang masih memakai package log standar bawaan Go, kita bisa menjembataninya (bridge) menggunakan slog.NewLogLogger agar output-nya tetap seragam:
legacyLogger := slog.NewLogLogger(logger.Handler(), slog.LevelWarn)
httpServer := &http.Server{
Addr: ":8080",
ErrorLog: legacyLogger,
}Komparasi: log/slog vs Zap vs Zerolog
| Fitur / Karakteristik | log/slog (Standard Library) |
uber-go/zap |
rs/zerolog |
|---|---|---|---|
| Status Dependensi | Standard Library (Go 1.21+) | Pustaka Eksternal (go get) |
Pustaka Eksternal (go get) |
| Jaminan Stabilitas | Garansi Kompatibilitas Go 1 | Bergantung maintainer open source | Bergantung maintainer open source |
| Alokasi Memori | Rendah (dengan LogAttrs) |
Sangat Rendah (Zero-allocation) | Sangat Rendah (Zero-allocation) |
| Integrasi Context | Bawaan (*Context + Custom Handler) |
Memerlukan wrapper zapctx |
Memerlukan helper terpisah |
| Data Masking | Bawaan (slog.LogValuer) |
Kustom Encoder | Kustom Hook |
| Rotasi Berkas | Tidak Ada (Gunakan logrotate / lumberjack) | Tidak Ada | Tidak Ada |
Menurut pengalaman saya di backend service biasa (REST/gRPC API), performalog/slogsudah lebih dari cukup untuk menangani throughput produksi. Keuntungan tidak menambah dependensi eksternal digo.modsering kali lebih bernilai praktis dibanding selisih mikrodetik alokasi, kecuali pada sistem ultra-low latency atau HFT yang memang menuntut alokasi memori nol secara ketat.
Tiga Jebakan yang Sering Terlewat
1. Evaluasi Dini (Eager Evaluation) pada Parameter Log
Parameter fungsi di Go selalu dievaluasi saat baris fungsi dipanggil (eager evaluation):
// JEBAKAN: serializePayload() akan SELALU dieksekusi meskipun log level saat ini adalah INFO!
logger.Debug("Detail payload request", "data", serializePayload(massivePayload))Jika serializePayload melakukan serialisasi berat, resource CPU terbuang percuma untuk menyiapkan parameter yang pada akhirnya langsung dibuang oleh logger.
Solusi: Cek apakah log level aktif terlebih dahulu dengan logger.Enabled():
if logger.Enabled(ctx, slog.LevelDebug) {
logger.Debug("Detail payload request", "data", serializePayload(massivePayload))
}2. Caller Loss Saat Membuat Helper Function
Jika kita membuat fungsi pembungkus (helper function):
// JEBAKAN: AddSource akan mencatat helper.go sebagai sumber baris kode, bukan lokasi pemanggil sebenarnya!
func LogError(logger *slog.Logger, msg string, err error) {
logger.Error(msg, "error", err)
}Dengan AddSource: true, logger akan selalu mencatat file helper.go. Informasi baris kode asli tempat error terjadi pun hilang.
Solusi: Jika perlu membuat wrapper logging sendiri, gunakan runtime.Callers dan konstruksi slog.NewRecord secara manual agar kedalaman call stack terhitung dengan benar.
3. Error Stack Trace
Secara default, memasukkan err ke slog.Any("error", err) hanya akan mencetak pesan string dari method err.Error(). Jika membutuhkan stack trace lengkap, kita bisa membungkus error dengan library tracing error (atau wrapper kustom), lalu mengekstrak frame stack-nya melalui ReplaceAttr atau implementasi slog.LogValuer.
Penutup
Catatan ini saya tulis sebagai pengingat saat mengerjakan backend Go dan mulai berhadapan dengan log yang semakin ramai. log/slog sendiri punya cukup banyak bagian untuk dipelajari, dan mungkin ada pendekatan lain yang lebih cocok untuk kebutuhan proyek masing-masing.
Semoga contoh dan catatan di atas bisa menjadi titik awal kalau sedang mencoba merapikan logging di aplikasi Go. Terima kasih sudah membaca, selamat mencoba, dan semoga catatan kecil ini bermanfaat.
Bacaan Lanjutan




