Menu

slog và log trong Golang: ghi log có cấu trúc trong Go

Cách ghi log trong Go: package log truyền thống với các cờ và log.Fatal, và log/slog (Go 1.21) cho log có cấu trúc với các mức log, thuộc tính khóa giá trị, handler dạng văn bản và JSON, cùng logger mang theo ngữ cảnh nhờ With.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

slog qua một ví dụ

log/slog (Go 1.21) ghi các bản ghi có cấu trúc: một thông điệp, một mức log và các thuộc tính khóa giá trị.

Mỗi dòng xuất ra có dạng time=... level=INFO msg="user logged in" user=ada attempts=1. Vì mỗi giá trị là một trường riêng, hệ thống thu thập log (Loki, Elasticsearch, CloudWatch, Datadog) có thể lọc theo user=ada hoặc level=ERROR mà không cần biểu thức chính quy trên văn bản tự do.

Các ví dụ trên trang này ghi ra os.Stdout để đầu ra hiện đúng thứ tự. Trong một service thật, log thường đi ra os.Stderr, cũng là nơi logger mặc định ghi vào.

Mức log

MứcGiá trịDùng cho
slog.LevelDebug-4chi tiết cho lập trình viên, tắt trên production
slog.LevelInfo0sự kiện bình thường: đã khởi động, đã xử lý request, công việc đã xong
slog.LevelWarn4điều gì đó bất thường mà chương trình đã xử lý được
slog.LevelError8một thao tác thất bại

Handler bỏ đi các bản ghi dưới mức tối thiểu của nó, và mức tối thiểu mặc định là Info. Đó là lý do slog.Debug(...) không in ra gì cho đến khi bạn cấu hình một handler với Level: slog.LevelDebug. Khoảng cách giữa các giá trị chừa chỗ cho các mức tự định nghĩa như slog.Level(2).

Để đổi mức log lúc chạy (từ một cờ dòng lệnh, một endpoint quản trị hay một tín hiệu), đặt một slog.LevelVar vào các tùy chọn và gọi Set trên nó sau đó:

var level slog.LevelVar // zero value: Info
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: &level}))
level.Set(slog.LevelDebug) // from now on, debug records are written

Văn bản hay JSON

slog.NewTextHandler ghi các cặp key=value, dễ đọc trong terminal. slog.NewJSONHandler ghi mỗi dòng một object JSON, định dạng mà phần lớn pipeline log mong đợi. Các lời gọi ghi log giữ nguyên; chỉ handler thay đổi.

Các giá trị giữ nguyên kiểu: status là một số trong đầu ra JSON còn retry là boolean, một time.Duration in ra là 42ms ở dạng văn bản và là nano giây trong JSON, còn một error in ra thông điệp của nó. ReplaceAttr là điểm móc để viết lại hoặc xóa thuộc tính, ở đây dùng để bỏ dấu thời gian, còn trong thực tế thì để đổi tên khóa (msg thành message) hoặc che giá trị.

Thuộc tính

Dạng không chặt kiểu xen kẽ khóa và giá trị: "user", "ada", "attempts", 3. Nó ngắn gọn, và chỉ có một kiểu lỗi: số đối số lẻ. Giá trị bị thừa được ghi dưới khóa !BADKEY. go vet bắt được lỗi này:

./main.go:14:2: call to slog.Info missing a final value

Để an toàn kiểu và bớt cấp phát bộ nhớ một chút, hãy dùng các hàm tạo thuộc tính, và LogAttrs khi ghi log trên đường chạy nóng:

logger.Info("order placed",
	slog.Int("order_id", 1017),
	slog.String("currency", "EUR"),
	slog.Float64("total", 59.90),
	slog.Duration("took", elapsed),
)

logger.LogAttrs(ctx, slog.LevelInfo, "order placed", slog.Int("order_id", 1017))

Hãy dùng một quy ước đặt tên khóa thống nhất trong toàn bộ codebase (user_id ở mọi nơi, không phải userID ở package này và uid ở package khác). Các truy vấn trong hệ thống log phụ thuộc vào điều đó.

With: logger mang theo ngữ cảnh

logger.With(attrs...) trả về một logger mới thêm các thuộc tính đó vào mọi bản ghi. Hãy tạo một logger cho mỗi request hoặc mỗi công việc, và mọi dòng nó ghi đều có thể liên kết với nhau:

Mỗi dòng đều mang service, version, request_id và user mà không cần lặp lại ở mỗi lời gọi. slog.Group lồng các thuộc tính vào nhau, handler JSON ghi chúng thành một object lồng ("payment":{"amount":25,"currency":"USD"}) còn handler văn bản ghi thành khóa có dấu chấm (payment.amount=25). logger.WithGroup("db") đặt mọi thuộc tính sau đó của logger ấy vào trong một group.

Hãy truyền logger gắn với request xuống dưới dạng tham số hoặc trường của struct. Lưu nó trong context.Context cũng được nhưng sẽ che giấu sự phụ thuộc; các phương thức InfoContext(ctx, ...) của slog truyền context cho handler, và một handler tự viết có thể dùng nó để lấy trace ID ra.

Ẩn bí mật với LogValuer

Một kiểu có thể quyết định cách nó được ghi log bằng cách triển khai slog.LogValuer. Nhờ vậy mật khẩu và token không lọt vào log, bất kể ai ghi log giá trị đó:

User chỉ ghi ID và email, còn một Token được ghi log riêng sẽ in ra REDACTED. Handler chỉ gọi LogValue khi bản ghi thực sự được ghi ra, nên cách này cũng hợp với những giá trị tốn kém để tính.

Package log truyền thống

log có trước slog và vẫn ổn cho các chương trình nhỏ và script. Nó ghi các dòng ra standard error với tiền tố ngày giờ:

CờThêm vào
log.LstdFlags (mặc định)ngày giờ 2009/11/10 23:00:00
log.Lmicrosecondsmicro giây trong thời gian
log.LUTCthời gian theo UTC
log.Lshortfile / log.Llongfilemain.go:14 / đường dẫn đầy đủ
log.Lmsgprefixđặt tiền tố ngay trước thông điệp thay vì ở đầu dòng

Ba nhóm hàm thoát chương trình hoặc gây panic, và sự khác nhau rất quan trọng:

  • log.Fatal, log.Fatalf, log.Fatalln in ra rồi gọi os.Exit(1). Các lời gọi defer không chạy. Hãy dùng chúng trong main cho lỗi lúc khởi động, đừng bao giờ dùng trong code thư viện hay handler của request.
  • log.Panic và các hàm cùng nhóm in ra rồi gây panic, nên các lời gọi defer vẫn chạy và panic có thể được recover.
  • Mọi hàm khác chỉ ghi một dòng.

Để ghi log ra tệp, hãy mở tệp rồi truyền nó cho log.New hoặc log.SetOutput; io.MultiWriter(os.Stderr, f) ghi ra cả hai.

Dùng log và slog cùng nhau

slog.SetDefault(logger) đặt logger làm mặc định cho các hàm cấp cao nhất như slog.Info, đồng thời chuyển đầu ra của package log qua nó. Khi đó, các lời gọi log.Printf có sẵn trong code của bạn hoặc trong thư viện phụ thuộc sẽ xuất ra thành bản ghi có cấu trúc ở mức Info:

slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil)))
log.Printf("legacy message") // {"time":"...","level":"INFO","msg":"legacy message"}

Trước khi gọi SetDefault, logger slog mặc định ghi qua package log, vì vậy một slog.Info("hi") đơn lẻ in ra 2026/09/23 14:30:00 INFO hi.

Quy tắc thực tế

  • Ghi log hoặc trả về lỗi, đừng làm cả hai. Một hàm vừa ghi log lỗi vừa trả nó về sẽ khiến cùng một lỗi được ghi ở mọi tầng của call stack. Hãy trả lỗi lên trên kèm ngữ cảnh, và ghi log một lần ở nơi xử lý nó.
  • Đặt dữ liệu thay đổi vào thuộc tính, không đặt vào thông điệp. logger.Info("user created", "user_id", id) gom nhóm tốt trong hệ thống log; logger.Info(fmt.Sprintf("user %d created", id)) tạo ra một thông điệp khác nhau cho mỗi người dùng.
  • Đừng bao giờ ghi log bí mật hay toàn bộ body của request. Hãy dùng LogValuer hoặc ReplaceAttr để che đi.
  • Dùng JSON trên production, văn bản khi phát triển. Chọn handler lúc khởi động dựa vào một cờ hoặc một biến môi trường.
  • Chọn mức log có chủ đích. Nếu mọi thứ đều được ghi ở mức Error, cảnh báo về lỗi sẽ trở thành nhiễu.

Câu hỏi thường gặp

slog trong Go là gì?

log/slog là package ghi log có cấu trúc được thêm vào thư viện chuẩn từ Go 1.21. Thay vì chuỗi đã định dạng, mỗi bản ghi có một thông điệp, một mức (Debug, Info, Warn, Error) và các thuộc tính khóa giá trị, và một handler ghi nó ra dưới dạng văn bản key=value hoặc JSON: slog.Info("login", "user", "ada", "attempts", 3).

Làm sao bật log debug trong slog?

Mức tối thiểu mặc định là Info, nên slog.Debug không in ra gì. Hãy tạo một handler với mức thấp hơn và đặt nó làm mặc định: slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))). Dùng slog.LevelVar thay cho một hằng số nếu bạn muốn đổi mức log trong lúc chương trình chạy.

log và slog trong Go khác nhau thế nào?

log ghi các dòng tự do với tiền tố thời gian tùy chọn và không có mức log. slog ghi các bản ghi có mức và thuộc tính khóa giá trị có kiểu, mà các hệ thống thu thập log có thể phân tích và lọc. Cả hai đều nằm trong thư viện chuẩn; slog.SetDefault còn chuyển đầu ra của package log qua handler của slog.

log.Fatal có chạy các hàm defer không?

Không. log.Fatal và log.Fatalf in thông điệp rồi gọi os.Exit(1), lệnh này bỏ qua mọi lời gọi defer. Chỉ dùng chúng trong main hoặc code khởi tạo, nơi không có gì cần dọn dẹp. log.Panic thì gây panic, nên các lời gọi defer vẫn chạy.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu