0

Bài 4: Log Levels và Phân nhóm dữ liệu (Attributes & Groups)

Trong một hệ thống đang chạy trên môi trường Production, việc in ra mọi chi tiết nhỏ nhặt của luồng xử lý sẽ làm phình to ổ cứng và tốn chi phí lưu trữ trên các nền tảng như Elasticsearch. Ngược lại, nếu in quá ít, khi có lỗi bạn sẽ không đủ dữ liệu để debug.

Đây là lúc chúng ta cần phân loại mức độ quan trọng bằng Log Levels, kết hợp với việc tổ chức các Key-Value thành từng nhóm (Group) có tính định kiểu (Strongly-typed) nhằm tăng hiệu năng.

1. Quản lý Log Levels (Cấp độ Log)

Package slog định nghĩa sẵn 4 cấp độ tiêu chuẩn (tương ứng với các hằng số nguyên):

  • slog.LevelDebug (-4): Dành cho lập trình viên khi đang dev hoặc fix bug (vd: in ra câu query SQL chi tiết, payload thô của request).

  • slog.LevelInfo (0): Thông tin hoạt động bình thường của hệ thống (vd: "Server started", "User logged in"). Đây là mức mặc định.

  • slog.LevelWarn (4): Các tình huống bất thường nhưng chưa làm gián đoạn chương trình (vd: "API bên thứ 3 phản hồi chậm", "Dùng lại cache cũ").

  • slog.LevelError (8): Lỗi nghiêm trọng khiến tác vụ thất bại (vd: "Không thể lưu vào database", "Mất kết nối Redis").

Để cấu hình mức log tối thiểu được phép ghi, chúng ta sử dụng slog.HandlerOptions.

Go

package main

import (
	"log/slog"
	"os"
)

func main() {
	// Khởi tạo tùy chọn: Chỉ cho phép in từ mức WARN trở lên.
	// Bỏ qua tất cả log ở mức DEBUG và INFO.
	opts := &slog.HandlerOptions{
		Level: slog.LevelWarn,
	}

	handler := slog.NewJSONHandler(os.Stdout, opts)
	logger := slog.New(handler)
	slog.SetDefault(logger)

	// Các dòng log này sẽ BỊ BỎ QUA, không xuất ra Terminal
	slog.Debug("Đang thực hiện vòng lặp thứ 100")
	slog.Info("Người dùng 123 truy cập trang chủ")

	// Các dòng log này SẼ ĐƯỢC IN RA
	slog.Warn("Dung lượng ổ cứng chỉ còn 5%")
	slog.Error("Crash ứng dụng do Null Pointer")
}

Việc cấu hình Level thông qua biến môi trường (Environment Variable) là cực kỳ phổ biến. Bạn có thể set Level là INFO cho Production, nhưng khi có sự cố, chỉ cần đổi biến môi trường thành DEBUG và khởi động lại container để xem chi tiết mọi luồng đi.

2. Tối ưu hiệu năng với Strongly-typed Attributes

Trong bài trước, chúng ta truyền Key-Value trực tiếp vào hàm log: slog.Info("msg", "user_id", 123). Bên dưới nền, slog nhận các tham số này dưới dạng any (interface rỗng). Điều này ép Go phải sử dụng Reflection lúc runtime để tự đoán kiểu dữ liệu, gây tốn tài nguyên (CPU và RAM) đáng kể nếu bạn ghi hàng chục ngàn dòng log mỗi giây.

Để hệ thống chạy nhanh nhất có thể, hãy dùng các hàm khai báo thuộc tính (Attributes) định kiểu rõ ràng:

Go

// Thay vì viết:
slog.Info("Tạo đơn hàng", "order_id", 99, "price", 500.5, "is_paid", false)

// HÃY VIẾT (Tối ưu hiệu năng):
slog.Info("Tạo đơn hàng",
	slog.Int("order_id", 99),
	slog.Float64("price", 500.5),
	slog.Bool("is_paid", false),
	slog.String("status", "pending"),
)

3. Phân nhóm dữ liệu (Log Grouping)

Khi log của bạn có quá nhiều trường (fields), việc để chúng cùng nằm trên một mặt phẳng JSON sẽ gây hỗn loạn. Ví dụ: bạn có user_id, user_email, request_ip, request_method, request_url.

Tính năng slog.Group cho phép bạn gom các Key-Value liên quan vào chung một JSON Object (nested JSON).

Go

package main

import (
	"log/slog"
	"os"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

	logger.Info("Phân tích API Request",
		slog.Group("user",
			slog.Int("id", 1024),
			slog.String("email", "admin@domain.com"),
		),
		slog.Group("request",
			slog.String("method", "POST"),
			slog.String("path", "/api/checkout"),
			slog.String("ip", "192.168.1.55"),
		),
	)
}

Kết quả JSON trả ra sẽ có cấu trúc cực kỳ sạch sẽ và dễ query trên Elasticsearch (vd: request.method = "POST"):

JSON

{
  "time": "2026-09-18T21:20:00Z",
  "level": "INFO",
  "msg": "Phân tích API Request",
  "user": {
    "id": 1024,
    "email": "admin@domain.com"
  },
  "request": {
    "method": "POST",
    "path": "/api/checkout",
    "ip": "192.168.1.55"
  }
}

4. Logger Kế thừa (Contextual Logger)

Trong mô hình Microservices hoặc Clean Architecture, một Request thường đi qua nhiều tầng (từ Controller/Handler -> UseCase/Service -> Repository). Nếu ở tầng nào bạn cũng phải truyền lại thông tin user_id hay request_id để ghi log thì code sẽ rất rườm rà.

slog giải quyết vấn đề này bằng hàm .With(). Hàm này tạo ra một instance Logger con (child logger), kế thừa toàn bộ cấu hình của Logger cha, nhưng gắn cố định thêm một số thuộc tính vào mọi dòng log mà nó phát ra.

Go

package main

import (
	"log/slog"
	"os"
)

// Mô phỏng một module Database
func connectDB(logger *slog.Logger) {
	// Mọi log từ hàm này sẽ luôn có tự động gắn key module="database"
	logger.Info("Đang khởi tạo connection pool")
	logger.Warn("Thời gian phản hồi DB hơi chậm")
}

func main() {
	baseLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

	// Tạo một logger con chuyên dùng cho module thanh toán
	paymentLogger := baseLogger.With(
		slog.String("module", "payment_gateway"),
		slog.String("provider", "ZaloPay"),
	)

	// Tạo một logger con khác chuyên cho Database
	dbLogger := baseLogger.With(
		slog.String("module", "database"),
		slog.String("db_type", "PostgreSQL"),
	)

	// Gọi log từ các module
	paymentLogger.Info("Khởi tạo thành công") 
	connectDB(dbLogger)
}

Nhờ .With(), bạn hoàn toàn cách ly bối cảnh (context) của từng thành phần, đảm bảo mọi log liên quan đến ZaloPay đều tự động được "đóng dấu" đầy đủ mà không cần lập trình viên phải nhớ chèn tay thủ công.

Tuy nhiên, có một rủi ro lớn trong thực tế: Nếu bạn vô tình log luôn cả dữ liệu Request Body chứa Mật khẩu hay Thẻ tín dụng thì sao? Bài tiếp theo sẽ hướng dẫn cách can thiệp vào tầng Handler để bưng bít (masking) các thông tin nhạy cảm này trước khi ghi ra file.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.