🎚️ Log level: chọn đúng mức để không ngập log

By Better Wise

Bài trước bạn đã dựng log có cấu trúc với các field như timestamp, level, service, request_id. Bài này trả lời câu hỏi ngay sau đó: field level nên mang giá trị nào, và ai quyết định dòng log nào thực sự được ghi ra.

Bạn sẽ làm được gì sau bài này

  • Phân biệt được debug, info, warn, error bằng tiêu chí rõ ràng, không bằng cảm giác.
  • Gán đúng mức cho một tình huống bất kỳ trong app của bạn.
  • Đặt ngưỡng log phù hợp cho dev, staging, production.
  • Nhận ra dấu hiệu ngưỡng đặt sai trước khi đồng đội phàn nàn.

Bốn mức log và cái ngưỡng lọc chúng

3 giờ sáng, bạn mở log của service thanh toán. Bốn mươi nghìn dòng. Trong đó có đúng một dòng nói database timeout, còn 39.999 dòng là "Calling getUserById". Bạn cuộn mỏi tay. Đó là lúc log level chứng minh giá trị của nó.

Mỗi dòng log có một mức. Logger so sánh mức đó với ngưỡng (threshold) đang cấu hình: chỉ dòng có mức _____ ngưỡng mới được ghi ra. Thứ tự từ thấp đến cao là debug, info, warn, error.

Level Dùng khi nào Ví dụ Ở production
debug Chi tiết chỉ dev cần đọc payload request, câu query, giá trị biến Tắt
info Sự kiện nghiệp vụ bình thường, đọc lại thấy có nghĩa đơn hàng tạo xong, user đăng nhập, job chạy xong Bật
warn Bất thường nhưng app vẫn tự xử lý được gọi API đối tác chậm, retry lần 1, cache miss tăng Bật
error Thao tác thất bại và cần có người xem mất kết nối DB, unhandled exception, thanh toán fail Bật, kèm alert

Mức debug chỉ dev cần đọc và thường bị tắt ở production: _____. Mức warn dành cho bất thường mà app vẫn xử lý được: _____.

Bài tập 1. Chọn level cho bốn dòng sau: (a) user đăng nhập thành công, (b) gọi API vận chuyển timeout, đã retry và thành công ở lần 2, (c) không kết nối được Redis, (d) in ra req.body để debug.

Xem gợi ý đáp án

Sign in to view the full content of this lesson. Sign in

(a) info, đây là sự kiện nghiệp vụ bình thường. (b) warn, có bất thường nhưng hệ thống tự xử lý xong. (c) error, app không tự xử lý được và cần người xem. (d) debug, chỉ phục vụ việc phát triển.

Gán mức cho tình huống thật

Quy tắc gọn nhất tôi dùng: error là "cần ai đó làm gì đó", không phải "có exception". Rất nhiều app log error cho mọi thứ bắt được trong try/catch, kể cả khi đã fallback thành công. Kết quả là error rate trên dashboard luôn đỏ, alert kêu suốt, và đến lúc có sự cố thật thì không ai còn tin.

Vài ranh giới dễ nhầm:

  • Lỗi do người dùng nhập sai (validation, 400, sai mật khẩu): log ở mức _____ (hoặc warn), không phải error. Đây là vận hành bình thường của app.
  • warn là "tôi xử lý được nhưng bạn nên biết": retry, fallback, chậm bất thường.
  • info không được nằm trong vòng lặp. Log một dòng info cho cả lô 500 item, kèm count, thay vì 500 dòng.
  • Một dòng log, một level. Đừng nhét stack trace vào info rồi tự nhủ là "cho gọn".
flowchart TD
    Start["Có một tình huống cần log"] --> Q1{"Việc chính có thất bại?"}
    Q1 -->|"Có"| Q2{"Do người dùng nhập sai?"}
    Q2 -->|"Có"| Info["ℹ️<br/>info hoặc warn"]
    Q2 -->|"Không"| Err["🚨<br/>error"]
    Q1 -->|"Không"| Q3{"Chỉ dev cần đọc?"}
    Q3 -->|"Có"| Dbg["🔍<br/>debug"]
    Q3 -->|"Không"| Q4{"Bất thường nhưng app tự xử lý được?"}
    Q4 -->|"Có"| Warn["⚠️<br/>warn"]
    Q4 -->|"Không"| Info

Bài tập 2. Một job đồng bộ chạy mỗi 5 phút. Với 500 bản ghi, 3 bản ghi lỗi và được đẩy vào hàng đợi thử lại. Bạn sẽ log mấy dòng, ở mức nào?

Xem gợi ý đáp án

Hai dòng. Một dòng info cho cả lô: {"msg":"sync_batch_done","total":500,"failed":3,"duration_ms":4200}. Một dòng warn cho phần thử lại: {"msg":"sync_records_retry_queued","count":3,"reason":"validation"}. Không log 3 dòng error riêng lẻ, vì job vẫn xử lý được và chưa cần ai can thiệp.

Đặt ngưỡng theo từng môi trường

Cùng một codebase, ba ngưỡng khác nhau. Điều này nên lấy từ biến môi trường, đừng hard-code.

const LOG_LEVEL =
  process.env.LOG_LEVEL ??
  (process.env.NODE_ENV === "production" ? "info" : "debug");
  • Dev: debug. Bạn muốn thấy mọi thứ, kể cả payload.
  • Staging: debug hoặc info, tùy nhu cầu kiểm thử.
  • Production: _____. Chỉ khi điều tra sự cố mới bật debug tạm thời, có thời hạn, rồi tắt lại.

Dấu hiệu bạn đặt ngưỡng quá thấp ở production: log ra quá nhiều dòng mỗi phút, chi phí lưu trữ tăng, tìm một dòng lỗi mất vài phút, và alert cứ kêu vì error xuất hiện liên tục.

Bài tập 3. Service production của bạn đang ghi khoảng 20.000 dòng log mỗi phút, chủ yếu là "Calling getUserById". Xử lý thế nào trong 10 phút?

Xem gợi ý đáp án

Đổi LOG_LEVEL sang info (nhiều khả năng các dòng kia đang là debug nhưng ngưỡng bị hạ), deploy lại hoặc dùng dynamic log level nếu có. Sau đó kiểm tra lại lượng log mỗi phút và chỉ giữ những dòng info thật sự có nghĩa khi đọc lại.

Mẹo của người trực đêm

  • Không chắc nên đặt mức nào? Hỏi: "3 giờ sáng, dòng này có đáng đánh thức tôi không?" Có thì error, không thì cùng lắm là warn.
  • Lỗi lặp lại trong vòng lặp: log lần đầu kèm số lần đã gặp, đừng log từng lần.
  • Đừng log error cho thứ đã catch và xử lý xong. Đó là cách nhanh nhất để alert mất uy tín.
  • Nếu warn xuất hiện dày đặc, vấn đề nằm ở ngưỡng hoặc ở chỗ bạn gán mức, không phải ở hệ thống.
  • Bật debug trên production thì đặt lịch tắt. Debug quên tắt là sự cố của tháng sau.

Chốt lại và đi tiếp

Bốn mức, một ngưỡng, ba môi trường. error dành cho việc cần người xem, warn cho bất thường đã xử lý được, info cho sự kiện nghiệp vụ, debug cho chi tiết của dev và tắt ở production. Thứ tự level từ thấp đến cao là debug, info, warn, _____. Đặt ngưỡng qua biến môi trường để mỗi nơi chạy một mức phù hợp.

Biết gán mức rồi, bài tiếp theo mới là phần khó hơn: log gì và không log gì, tức là nội dung nào được phép bước vào dòng log đó.

Front: Log level là gì? Back: Nhãn gắn cho mỗi dòng log, dùng để lọc dòng nào được ghi ra và dòng nào bị bỏ qua.

Front: Thứ tự các mức log từ thấp đến cao? Back: debug, info, warn, error.

Front: Ngưỡng log (threshold) hoạt động thế nào? Back: Logger chỉ ghi các dòng có level lớn hơn hoặc bằng ngưỡng đang cấu hình.

Front: Khi nào dùng debug? Back: Khi cần chi tiết phục vụ phát triển: payload, câu query, giá trị biến.

Front: Khi nào dùng info? Back: Khi ghi sự kiện nghiệp vụ bình thường mà đọc lại vẫn thấy có nghĩa.

Front: Khi nào dùng warn? Back: Khi có bất thường nhưng app vẫn tự xử lý được, chưa cần ai can thiệp.

Front: Khi nào dùng error? Back: Khi thao tác thất bại và cần có người xem, không chỉ vì có exception.

Front: Ngưỡng log thường đặt ở production là gì? Back: info.

Front: Ngưỡng log thường đặt ở môi trường dev là gì? Back: debug.

Front: Lỗi validation do người dùng nhập sai nên log mức nào? Back: info hoặc warn, không phải error.

Front: Vì sao không log error cho việc đã catch và xử lý xong? Back: Vì tạo ra error giả, làm alert nhiễu và làm mòn niềm tin vào log.

Front: Bật debug tạm thời trên production bằng cách nào? Back: Đổi ngưỡng qua biến môi trường hoặc dynamic log level, có thời hạn rồi tắt lại.

Front: Dấu hiệu ngưỡng log đặt quá thấp ở production? Back: Quá nhiều dòng log mỗi phút, khó tìm lỗi thật, tốn chi phí lưu trữ và alert nhiễu.