Log adalah catatan yang dihasilkan program selama berjalan agar perilakunya dapat ditinjau kemudian, baik oleh manusia maupun oleh alat pengumpul log. Topik ini membahas logging terstruktur (structured logging) dengan paket log/slog beserta taksonomi error yang menentukan kapan error dikembalikan ke pemanggil dan kapan error dicatat. Tiga program lengkap dieksekusi nyata pada topik ini, dan kunci waktu pada keluarannya dihapus agar hasil setiap percobaan dapat dibandingkan.
Logging terstruktur dengan slog#
Log terstruktur tidak merangkai peristiwa menjadi satu teks, melainkan menyimpan pesan beserta pasangan atributnya sebagai data yang terpisah. Bentuk ini membuat baris log tetap dapat disaring, diurutkan, dan dihitung oleh pengumpul log tanpa penguraian teks yang rapuh. Paket log/slog menyediakan logger sebagai objek perekam dan handler sebagai penentu bentuk keluarannya. Program berikut membuat logger dengan handler teks, lalu mencatat tiga peristiwa pada tiga tingkat berbeda:
package main
import (
"log/slog"
"os"
)
func main() {
// Atribut waktu dihapus agar keluaran contoh tetap deterministik.
hapusWaktu := func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
}
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: hapusWaktu,
}))
logger.Info("layanan dimulai", "port", 8080, "mode", "pengujian")
logger.Warn("ambang kuota hampir terlampaui", "terpakai", 92, "batas", 100)
logger.Error("gagal memuat pengaturan", "berkas", "pengaturan.json")
}level=INFO msg="layanan dimulai" port=8080 mode=pengujian
level=WARN msg="ambang kuota hampir terlampaui" terpakai=92 batas=100
level=ERROR msg="gagal memuat pengaturan" berkas=pengaturan.jsonSetiap baris berawalan level dan msg, lalu diikuti atribut yang ditulis sebagai pasangan kunci dan nilai. Tingkat INFO, WARN, dan ERROR menyatakan keparahan peristiwa, sedangkan atribut seperti port=8080 tetap berupa data yang utuh, bukan potongan teks yang tercabut dari konteksnya. Nilai yang berisi spasi, misalnya nama berkas, dikutip otomatis oleh handler. Handler teks memang ditujukan bagi mata manusia, sedangkan pengumpul log biasanya meminta bentuk yang dapat diurai mesin.
Tingkat log dan handler JSON#
Empat tingkat bawaan slog diurutkan dari yang paling rinci sampai paling parah, yaitu Debug, Info, Warn, dan Error. Handler dapat diberi ambang tingkat, yaitu batas bawah yang menyaring setiap catatan yang lebih rinci daripada ambangnya, sehingga verbositas keluaran disetel tanpa menyunting kode pencatatnya. Handler JSON menulis satu objek per baris dengan kunci yang tetap, bentuk yang paling mudah dibaca oleh alat pengumpul log. Kedua handler menerima opsi yang sama, sehingga perpindahan dari bentuk teks ke JSON cukup mengganti handler pada satu tempat pembuatan logger tanpa menyentuh kode pencatatnya. Program berikut menyetel ambang pada LevelWarn, sehingga catatan Info tidak pernah ditulis:
package main
import (
"log/slog"
"os"
)
func main() {
// Atribut waktu dihapus agar keluaran contoh tetap deterministik.
hapusWaktu := func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: hapusWaktu,
Level: slog.LevelWarn,
}))
logger.Info("permintaan diproses", "durasi_ms", 12)
logger.Warn("tenggat waktu hampir habis", "sisa_ms", 40)
logger.Error("percobaan masuk gagal", "pengguna", "dewi")
}{"level":"WARN","msg":"tenggat waktu hampir habis","sisa_ms":40}
{"level":"ERROR","msg":"percobaan masuk gagal","pengguna":"dewi"}Catatan Info tentang permintaan yang diproses hilang sama sekali, sedangkan Warn dan Error tetap tercetak dalam bentuk objek JSON dengan kunci level, msg, beserta atribut-atributnya. Satu objek per baris membuat setiap catatan tetap dapat diurai secara terpisah oleh pengumpul log.
Keluaran contoh di atas tidak memuat kunci waktu. Handler pada ketiga program topik ini memakai opsi ReplaceAttr untuk menghapus atribut time, sehingga baris keluarannya identik pada setiap percobaan dan dapat dibandingkan dengan ketat. Program produksi membiarkan waktu bawaan, karena penanda waktu (timestamp) justru menjadi salah satu nilai terpenting pada sebuah baris log.
Taksonomi error#
Error di Go adalah nilai biasa yang dikembalikan bersama hasil kerja, sehingga penanganannya menuntut keputusan yang tetap, bukan improvisasi per berkas. Taksonomi yang lazim membaginya menjadi tiga kelas. Error sentinel adalah nilai errors.New yang dideklarasikan di paket sumbernya dan diperiksa dengan errors.Is, sehingga pemanggil menguji penyebab error tanpa menguraikan teks pesannya. Error bungkusan dibentuk dengan fmt.Errorf dan %w, yaitu pembungkusan yang menyimpan konteks tambahan tanpa menghilangkan error asalnya.
Kelas ketiga adalah tipe error kustom yang diperiksa dengan errors.As, yaitu pilihan bila penyebab error perlu membawa data tambahan. Pola pemeriksaannya tetap sama, yaitu menguji sekali di batas yang menangani. Keputusan pencatatan melengkapi kelas-kelas nilai error itu, yaitu error ditangani satu kali pada batas program dan dicatat di sana, bukan di setiap lapisan yang dilewatinya. Program berikut menggabungkan error sentinel, error bungkusan, dan keputusan pencatatan itu sekaligus:
package main
import (
"errors"
"fmt"
"log/slog"
"os"
)
var errNamaKosong = errors.New("nama tidak boleh kosong")
var errSkorDiLuarRentang = errors.New("skor di luar rentang")
type peserta struct {
nama string
skor int
}
func periksa(p peserta) error {
if p.nama == "" {
return errNamaKosong
}
if p.skor < 0 || p.skor > 100 {
return fmt.Errorf("skor %d untuk %s: %w", p.skor, p.nama, errSkorDiLuarRentang)
}
return nil
}
func main() {
// Atribut waktu dihapus agar keluaran contoh tetap deterministik.
hapusWaktu := func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
}
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: hapusWaktu,
}))
daftar := []peserta{
{nama: "citra", skor: 88},
{nama: "", skor: 70},
{nama: "budi", skor: 104},
}
for _, p := range daftar {
err := periksa(p)
switch {
case errors.Is(err, errNamaKosong):
// keadaan yang diharapkan dicatat dengan tingkat WARN
logger.Warn("baris tanpa nama dilewati", "skor", p.skor)
case err != nil:
// error dicatat sekali pada batas program, bukan di tempat terjadinya
logger.Error("data peserta ditolak", "error", err)
}
}
}level=WARN msg="baris tanpa nama dilewati" skor=70
level=ERROR msg="data peserta ditolak" error="skor 104 untuk budi: skor di luar rentang"Fungsi periksa mengembalikan error sentinel bila nama kosong, atau error bungkusan bila skor keluar rentang, dan keduanya tidak mencatat apa pun. Pencatatan hanya terjadi di main, yaitu batas program, dengan errors.Is sebagai pembeda kelasnya. Baris tanpa nama adalah keadaan yang memang diharapkan pada data masukan, sehingga cukup dicatat dengan Warn beserta skor yang tersisa. Skor di luar rentang disebut ketidakwajaran dan dicatat dengan Error berikut seluruh teks error bungkusannya. Nama citra tidak memunculkan baris apa pun, karena keberhasilan bukanlah peristiwa yang perlu dicatat.
Kesalahan umum#
Tiga kekeliruan berikut paling sering muncul saat error dan log saling dipertemukan.
- Mencatat error lalu mengembalikannya juga. Satu kejadian tercatat di setiap lapisan yang dilewati, dan laporan pengumpul log menghitungnya berkali-kali. Error dicatat satu kali pada batas yang memutus penanganannya, sedangkan lapisan di atasnya cukup meneruskan atau membungkusnya.
- Memakai fmt.Println sebagai log. Barisnya tanpa tingkat, tanpa atribut, dan tanpa penanda waktu yang dapat disaring, sehingga pengumpul log tidak dapat memperlakukannya sebagai log.
log/slogdipakai sejak baris pertama proyek. - Menandai semua catatan sebagai Error. Tingkat keparahan kehilangan maknanya, karena keadaan yang diharapkan berbaur dengan ketidakwajaran. Tingkat dipilih menurut akibatnya, yaitu Debug untuk rincian, Info untuk peristiwa normal, Warn untuk yang patut dicermati, dan Error untuk yang menuntut tindakan.
Ringkasan#
- log/slog mencatat pesan beserta atribut berupa pasangan kunci dan nilai, sehingga baris log tetap data yang dapat disaring dan dihitung.
- Tingkat bawaan Debug, Info, Warn, dan Error diurutkan dari yang paling rinci, dan handler menyaring catatan di bawah ambangnya.
- Handler teks ditujukan bagi manusia, sedangkan handler JSON menulis satu objek per baris untuk pengumpul log.
- Error sentinel diperiksa dengan
errors.Is, error bungkusan%wmenyimpan konteksnya, dan pencatatan dilakukan satu kali di batas program.
Referensi#
- log/slog package — pkg.go.dev — Dokumentasi resmi paket log/slog, termasuk handler teks dan JSON.
- Structured Logging with slog — The Go Blog — Penjelasan resmi desain logging terstruktur Go beserta alasannya.