Bài 3: Cuộc cách mạng log/slog và Structured Logging
Khi hệ thống backend của bạn phát triển từ một server đơn lẻ thành kiến trúc Microservices xử lý hàng ngàn request mỗi giây, việc sử dụng các hàm Printf để nối chuỗi văn bản (Plain Text) sẽ bộc lộ một điểm yếu nghiêm trọng: Khả năng truy vấn.
Giả sử bạn cần tìm tất cả các lỗi thanh toán của một User ID cụ thể trong file log 50GB. Việc dùng các công cụ tìm kiếm văn bản (như grep) hoặc Regex là cực kỳ chậm và kém chính xác. Các hệ thống giám sát log tập trung (Centralized Logging) như Elasticsearch (ELK Stack), Datadog hay Grafana Loki cần dữ liệu được cấu trúc rõ ràng (thường là JSON) để có thể index và query tốc độ cao.
Đó là lý do Structured Logging (Ghi log có cấu trúc) ra đời. Thay vì viết: [ERROR] User 123 failed to buy item 456 due to timeout Chúng ta sẽ tách biệt nội dung tĩnh và dữ liệu động thành các cặp Key-Value: {"level":"ERROR", "msg":"Payment failed", "user_id":123, "item_id":456, "reason":"timeout"}
Từ phiên bản Go 1.21, thư viện chuẩn chính thức tích hợp package log/slog, mang lại chuẩn mực Structured Logging mạnh mẽ mà trước đây các lập trình viên phải dựa vào thư viện ngoài như zap hay logrus.
1. Cách hoạt động của slog
Kiến trúc của slog tách biệt rõ ràng giữa phần Frontend (API bạn gọi để ghi log) và Backend (Handler chịu trách nhiệm định dạng và xuất dữ liệu ra ngoài).
Package này cung cấp sẵn hai Handler cốt lõi:
-
slog.TextHandler: In log dưới dạng các cặpkey=value. Dễ đọc bằng mắt người, phù hợp khi chạy local. -
slog.JSONHandler: In log dưới dạng chuẩn JSON. Tối ưu cho máy móc (ELK, Datadog) xử lý, là lựa chọn bắt buộc cho môi trường Production.
2. Khởi tạo và sử dụng TextHandler
Trong môi trường dev local, bạn cần log có cấu trúc nhưng vẫn muốn dễ dàng đọc bằng mắt trên Terminal. TextHandler là lựa chọn hoàn hảo.
Go
package main
import (
"log/slog"
"os"
)
func main() {
// 1. Khởi tạo TextHandler trỏ output ra Terminal (os.Stdout)
textHandler := slog.NewTextHandler(os.Stdout, nil)
// 2. Tạo một Logger mới từ Handler đó
logger := slog.New(textHandler)
// 3. Ghi log với cú pháp: Thông điệp (msg), theo sau là các cặp Key-Value
logger.Info("Người dùng đăng nhập thành công",
"user_id", 1024,
"ip_address", "192.168.1.10",
"role", "admin",
)
}
Kết quả ở Terminal:
Plaintext
time=2026-09-18T21:19:32.000Z level=INFO msg="Người dùng đăng nhập thành công" user_id=1024 ip_address=192.168.1.10 role=admin
3. Triển khai JSONHandler cho Production
Chỉ với việc thay đổi hàm khởi tạo Handler, toàn bộ output của bạn sẽ lập tức chuyển thành chuỗi JSON tiêu chuẩn mà không cần sửa đổi bất kỳ dòng code gọi log nào bên dưới.
Go
package main
import (
"log/slog"
"os"
)
func main() {
// Mở file để lưu log JSON
file, _ := os.OpenFile("production.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
defer file.Close()
// Sử dụng JSONHandler trỏ vào file
jsonHandler := slog.NewJSONHandler(file, nil)
logger := slog.New(jsonHandler)
// Ghi log lỗi thanh toán
logger.Error("Thanh toán thất bại",
"order_id", "ORD-999",
"amount", 500000,
"error_cause", "timeout_gateway",
)
}
Kết quả lưu trong file production.json:
JSON
{"time":"2026-09-18T21:19:32Z","level":"ERROR","msg":"Thanh toán thất bại","order_id":"ORD-999","amount":500000,"error_cause":"timeout_gateway"}
Nhờ định dạng JSON này, khi đẩy file log lên Elasticsearch, bạn có thể dễ dàng viết câu query: Lọc tất cả log có level = ERROR VÀ error_cause = timeout_gateway VÀ amount > 200000 chỉ trong vài mili-giây.
4. Chiếm quyền kiểm soát (Set Default)
Trong một dự án lớn, việc phải truyền (pass) biến logger đi khắp hàng chục package khác nhau là rất cồng kềnh. slog cho phép bạn ghi đè cấu hình Logger toàn cục để bạn có thể gọi trực tiếp slog.Info() ở bất cứ đâu.
Tuyệt vời hơn, khi bạn Set Default cho slog, toàn bộ các lệnh ghi log cũ kỹ từ package log gốc (như log.Printf, log.Println) cũng sẽ tự động được chuyển đổi format theo Handler của slog. Tính năng tương thích ngược này giúp bạn nâng cấp hệ thống cũ lên Structured Logging cực kỳ nhẹ nhàng.
Go
package main
import (
"log" // Package log truyền thống
"log/slog" // Package slog mới
"os"
)
func init() {
// Khởi tạo JSONHandler
jsonHandler := slog.NewJSONHandler(os.Stdout, nil)
logger := slog.New(jsonHandler)
// Đặt logger này làm mặc định cho toàn bộ chương trình
slog.SetDefault(logger)
}
func main() {
// Giờ đây bạn có thể gọi trực tiếp ở cấp package
slog.Info("Chương trình bắt đầu", "version", "1.0.0")
// Lệnh log cũ cũng tự động biến thành JSON!
log.Println("Cảnh báo: Bộ nhớ RAM đang ở mức cao")
}
Kết quả:
JSON
{"time":"2026-09-18T21:19:32Z","level":"INFO","msg":"Chương trình bắt đầu","version":"1.0.0"}
{"time":"2026-09-18T21:19:32Z","level":"INFO","msg":"Cảnh báo: Bộ nhớ RAM đang ở mức cao"}
Với cấu trúc Key-Value rõ ràng, câu hỏi tiếp theo là làm sao để tổ chức các Key này cho khoa học (thay vì vứt lung tung) và kiểm soát được những log nào được phép in ra dựa trên môi trường Dev hay Prod.
All rights reserved