Log gì và không log gì
Mở đầu: dòng log bạn không muốn thấy lúc 3 giờ sáng
Hai bài trước đã cho bạn hai viên gạch: log có cấu trúc với các field cố định, và log level để chọn đúng mức ồn. Bài này trả lời câu hỏi còn lại, cái khó nhất: cụ thể thì ghi cái gì vào đó?
Chuyện thật: 3 giờ sáng, alert kêu. Bạn mở log, thấy 400 dòng info: success, đúng một dòng error: something went wrong, và không có một manh mối nào về user nào, đơn nào, hay bước nào. Bài này dạy cách để bạn không bao giờ ở trong tình huống đó.
Sau bài này, bạn có thể:
- Liệt kê được 4 nhóm sự kiện đáng ghi log trong một app nhỏ.
- Nhận ra PII và secret trong một dòng log và biết cách xử lý trước khi ghi.
- Chỉ ra những dòng log chỉ tạo nhiễu và cắt chúng đi.
- Áp dụng quy tắc allowlist thay vì "log hết rồi tính sau".
Ba câu hỏi lọc trước khi viết một dòng log
Trước khi gõ logger.info(...), hỏi ba câu. Cả ba đều "có" thì mới viết:
- Sự kiện này có giúp debug hoặc điều tra sự cố không? Nếu không, nó là nhiễu.
- Có ai thực sự đọc dòng này không? Không ai đọc thì đừng tốn tiền lưu.
- Dòng này có chứa dữ liệu của người dùng hoặc bí mật hệ thống không? Nếu có, phải che trước khi ghi.
Đây là bộ lọc dạng sơ đồ, dùng lại được cho mọi field:
flowchart TD
A["📝 Field muốn log"] --> B{"Là secret?"}
B -->|Có| X["🚫 Bỏ hẳn"]
B -->|Không| C{"Là PII?"}
C -->|Có| D["🎭 Che hoặc hash"]
C -->|Không| E{"Giúp debug?"}
E -->|Không| X
E -->|Có| F["✅ Log kèm context"]
Những sự kiện đáng ghi log
Với một app nhỏ đã deploy, bốn nhóm này là đủ:
| Nhóm | Ví dụ field | Vì sao đáng log |
|---|---|---|
| Request và response | route, method, status, duration_ms, request_id |
Biết chậm ở đâu, lỗi ở đâu |
| Lỗi và exception | error_type, error_code, stack (đã lọc), request_id |
Có manh mối để lần lại |
| Thay đổi state nghiệp vụ | order_id, action, from_status, to_status |
Trả lời "đơn này đã đi qua bước nào" |
| Sự kiện hệ thống | job bắt đầu và kết thúc, deploy, đổi config, chạm rate limit | Giải thích vì sao hành vi đổi đột ngột |
Với nhóm auth, nguyên tắc là: log kết quả (đăng nhập thành công hay thất bại, lý do thất bại dạng mã như invalid_password), tuyệt đối không log thông tin đăng nhập.
Tip: Một sự kiện, một dòng log, một
request_id. Khi có sự cố, bạn chỉ cần grep một id là ra toàn bộ câu chuyện.
Bài tập 1 (2 phút). Đánh dấu log hay không log: (a) user mở trang chủ thành công, (b) thanh toán thất bại với mã card_declined, (c) nội dung body của form đăng ký, (d) job đồng bộ chạy xong sau 12 giây, (e) giá trị header Authorization, (f) đổi trạng thái đơn từ pending sang paid.
Xem gợi ý đáp án
- (a) Không nên log từng lần ở mức
info, hãy đếm bằng metric. Log sẽ ngập. - (b) Có, kèm
order_id,error_code,request_id. - (c) Không, body form có thể chứa PII. Chỉ log
form_namevàvalidation_error. - (d) Có, kèm
job_name,duration_ms,items_processed. - (e) Không, đây là secret. Nếu buộc phải phân biệt, log
auth_schemehoặc một phần đã hash. - (f) Có, kèm
order_id,from_status,to_status.
PII và secret: thứ không bao giờ được vào log
Đây là phần dễ gây tai nạn nhất. Log thường được gửi tới dịch vụ bên thứ ba, giữ lại hàng tháng, và rất nhiều người đọc được. Một dòng log lỡ tay là một bản sao dữ liệu người dùng nằm ngoài tầm kiểm soát của bạn.
Secret, không bao giờ log: password, API key, token, cookie phiên, private key, chuỗi kết nối database, header Authorization.
PII, phải che hoặc bỏ: email, số điện thoại, địa chỉ, giấy tờ định danh, số thẻ, ngày sinh. Địa chỉ IP tùy ngữ cảnh, cứ coi là nhạy cảm và hạn chế.
Cách làm đúng là allowlist: định nghĩa sẵn danh sách field được phép ghi, mọi thứ khác bị bỏ. Cách làm sai là blocklist, liệt kê field nguy hiểm rồi quên một cái là thủng.
- logger.info("checkout", { body: req.body, headers: req.headers });
+ logger.info("checkout.failed", {
+ request_id: req.id,
+ order_ref: order.public_ref,
+ reason: "card_declined",
+ user_ref: hash(user.id),
+ });
Vài quy tắc cụ thể:
- Khi cần tương quan log giữa các request mà vẫn giữ riêng tư, dùng
hash(user.id)làmuser_ref, không dùng id gốc hay email. - Đừng log nguyên
req.body,req.headers, hayreq.query. Query string và URL path cũng hay dính token. - Đặt phần che dữ liệu ở một chỗ duy nhất trong lớp logging, đừng rải
ifkhắp nơi. - Ví dụ trong tài liệu và test chỉ dùng giá trị giả, không bao giờ dán dữ liệu thật của người dùng.
Warning: Một khi PII hoặc secret đã vào log, nó nằm trong nhiều bản sao ở nhiều hệ thống. Xóa đi rất khó và rất đắt. Che trước khi ghi là cách duy nhất rẻ tiền.
Bài tập 2 (3 phút). Dòng log sau vi phạm gì, sửa lại thế nào?
{"level":"info","msg":"login ok","email":"[email protected]","password":"hunter2","ip":"203.0.113.9","headers":{"cookie":"session=abc123"}}
Xem gợi ý đáp án
Vi phạm: password và cookie là secret, email và ip là PII, và headers bị log nguyên khối. Sửa lại:
{"level":"info","msg":"auth.login","result":"success","user_ref":"u_8f21c9","request_id":"req_7712"}
Bỏ hẳn password và cookie là bắt buộc. email thay bằng user_ref đã hash. Nếu thật sự cần IP cho mục đích chống lạm dụng, hãy lưu riêng, có thời hạn, và ghi rõ lý do.
Những dòng log chỉ tạo nhiễu
Nhiễu không phải là vô hại. Nó làm bạn chậm đi đúng vào lúc bạn cần nhanh nhất. Các thủ phạm quen thuộc:
- Log bên trong vòng lặp, mỗi item một dòng.
- Log kiểu "đang vào hàm X", "đã xong bước 2" cho luồng bình thường.
- Cùng một lỗi được log ở ba tầng: controller, service, repository. Chọn một tầng.
- Log cả object lớn trong khi chỉ cần ba field.
- Bật
debugở production rồi quên tắt. - Message rỗng nghĩa:
"something went wrong","error","failed". Không có dữ kiện thì không ai debug được.
Note: Mẹo đọc lại: nếu một dòng log không giúp bạn trả lời "chuyện gì đã xảy ra, với cái gì, khi nào", thì nó chưa xứng đáng được ghi.
Bài tập 3 (2 phút). Rút 5 dòng sau thành 1 dòng duy nhất, vẫn đủ để debug:
info: vào hàm syncOrders
info: lấy được 120 đơn
info: bắt đầu xử lý
error: lỗi rồi
info: kết thúc
Xem gợi ý đáp án
{"level":"error","msg":"job.sync_orders.failed","job_name":"sync_orders","items_total":120,"items_processed":87,"error_type":"TimeoutError","duration_ms":12400}
Nguyên tắc: một dòng, có tên sự kiện dạng noun.verb, có số liệu, có loại lỗi, có thời lượng.
Kiểm tra nhanh
Nguyên tắc an toàn nhất khi log dữ liệu là dùng _____ các field được phép ghi, thay vì liệt kê các field nguy hiểm. Password, API key, token và cookie phiên phải được _____ trước khi ghi log. Khi cần tương quan log mà vẫn giữ riêng tư, ta log _____ của user id thay vì giá trị gốc. Một lỗi chỉ nên được log ở _____ để tránh trùng lặp. Phần che dữ liệu nhạy cảm nên được đặt ở _____ trong lớp logging.
Mẹo của người trực đêm
- Review log như review code. Đọc to dòng log bạn vừa viết và tự hỏi: nếu dòng này lọt ra ngoài thì sao?
- Grep thử trước khi merge. Chạy app ở dev, bắn một request, grep
errorvàpasswordtrong file log. Không thấy gì là tốt. - Allowlist mặc định. Tạo hàm
logEvent(name, fields)chỉ nhận các field trong danh sách trắng. Muốn thêm field phải sửa danh sách, tức là có chủ đích. - Che ở nguồn, không che ở nơi đọc. Nếu phải che lúc query thì đã quá muộn.
- Đặt tên sự kiện theo mẫu
noun.verb:order.created,auth.login,job.sync_orders.failed. Dễ grep, dễ đếm. - Nghi ngờ thì bỏ. Không chắc một field có nhạy cảm không, hãy bỏ nó đi. Debug thiếu một field dễ chịu hơn nhiều so với rò rỉ dữ liệu.
Mang gì sang bài checkpoint
Tóm lại ba điều: log sự kiện, không log luồng chạy; log định danh tham chiếu (request_id, order_ref, user_ref đã hash), không log dữ liệu gốc; và mỗi sự kiện một dòng có cấu trúc, không phải một chuỗi câu.
Bài tiếp theo là checkpoint của module: bạn sẽ gặp lại structured logging, log level và bài học hôm nay trong một loạt tình huống ngắn để tự kiểm tra xem phản xạ "log gì, bỏ gì" đã thành phản xạ chưa.
Front: Bốn nhóm sự kiện đáng ghi log trong một app nhỏ? Back: Request và response, lỗi và exception, thay đổi state nghiệp vụ, sự kiện hệ thống như job và deploy.
Front: Secret tuyệt đối không được vào log gồm những gì? Back: Password, API key, token, cookie phiên, private key, chuỗi kết nối database.
Front: Vì sao không log nguyên req.body hay req.headers? Back: Chúng có thể chứa PII và secret, và log đã ghi thì rất khó xóa.
Front: Allowlist khác blocklist ở điểm nào khi log? Back: Allowlist chỉ ghi các field được liệt kê sẵn, an toàn hơn blocklist vì không sợ quên một field nguy hiểm.
Front: Làm sao tương quan log giữa các request mà không lộ danh tính? Back: Dùng hash của user id làm user_ref thay vì id gốc hay email.
Front: Header nào phải bỏ khỏi log? Back: Authorization và Cookie.
Front: Ba dấu hiệu của log nhiễu? Back: Log trong vòng lặp, log từng bước bình thường, log cùng một lỗi ở nhiều tầng.
Front: Một sự kiện nên chiếm bao nhiêu dòng log? Back: Đúng một dòng, có cấu trúc, kèm request_id.
Front: Vì sao message 'something went wrong' là log vô dụng? Back: Nó không có dữ kiện nào để lần lại: không tên sự kiện, không mã lỗi, không id.
Front: Phần che dữ liệu nhạy cảm nên đặt ở đâu? Back: Một chỗ duy nhất trong lớp logging, không rải rác khắp code.
Front: Địa chỉ IP có phải PII không? Back: Tùy ngữ cảnh, nên coi là nhạy cảm và hạn chế log.
Front: Trước khi merge một dòng log mới, nên làm gì? Back: Chạy ở dev, grep thử file log để chắc không có secret hay PII lọt ra.
Front: Log level debug ở production nên xử lý thế nào? Back: Tắt mặc định, chỉ bật tạm thời có kiểm soát khi đang điều tra.