Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 20 — Logging Best Practices và Cấu trúc Log

Operations Management for Tech Bài 20/60

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng lúc 2 giờ sáng, hệ thống thanh toán của bạn bắt đầu lỗi. Khách hàng phàn nàn, doanh thu đang chảy máu từng phút. Bạn mở log lên và thấy hàng nghìn dòng như thế này: Error processing request. Chỉ vậy thôi. Không có ID giao dịch, không biết user nào, không rõ tầng nào lỗi. Bạn phải đọc từng dòng, đoán mò, và mỗi phút trôi qua là tiền mất đi.

Đây là kịch bản mà mọi kỹ sư vận hành đều sợ. Và trớ trêu là nó hoàn toàn có thể tránh được — chỉ bằng cách viết log cho tử tế.

Log không phải là thứ bạn viết cho vui hay để "cho có". Log là con mắt của bạn nhìn vào hệ thống khi mọi thứ đang chạy trong bóng tối của production. Trong các bài trước, chúng ta đã nói về observability như một khái niệm tổng thể và ba trụ cột của nó (logs, metrics, traces). Bài này đi sâu vào trụ cột đầu tiên và cũng là nền tảng nhất: log — cụ thể là cách viết log sao cho máy đọc được, người truy vấn được, và đội ngũ debug nhanh được. Chúng ta sẽ không bàn về việc gom log tập trung (đó là Bài 49 về Log Aggregation) hay về metrics — chỉ tập trung vào một câu hỏi: mỗi dòng log nên trông như thế nào?

Đây là kỹ năng nền, tưởng nhỏ mà quyết định. Một hệ thống có log tốt thì thời gian điều tra sự cố (MTTR) giảm từ hàng giờ xuống còn vài phút. Một hệ thống log tệ thì dù bạn có công cụ đắt tiền cỡ nào cũng bó tay.

Khái niệm cốt lõi

Structured logging — trái tim của bài học

Điểm mấu chốt cần nhớ: log nên là dữ liệu, không phải là câu văn.

Cách viết log truyền thống (unstructured / plain text) trông như thế này:

log("user 123 login from VN")

Nhìn thì con người đọc được. Nhưng máy tính thì "mù chữ" trước nó. Muốn tìm tất cả lần đăng nhập từ Việt Nam, bạn phải viết biểu thức regex phức tạp để bóc chuỗi "from VN" ra khỏi câu — và chỉ cần lập trình viên khác viết "login from Vietnam" hay "đăng nhập từ VN" là regex của bạn sập ngay.

Structured logging làm khác hẳn. Thay vì một câu văn, bạn ghi một object có cấu trúc (thường là JSON):

{
  "event": "login",
  "user_id": 123,
  "country": "VN",
  "timestamp": "2026-06-27T02:15:33Z",
  "level": "info"
}

Bây giờ máy đọc được. Bạn muốn tìm mọi lần login từ Việt Nam trong đêm qua? Chỉ cần query:

event = "login" AND country = "VN"

Không regex, không đoán mò. Đây chính là khác biệt giữa "một đống văn bản" và "một cơ sở dữ liệu có thể truy vấn". Structured logging biến log từ thứ để đọc thành thứ để phân tích.

Các trường bắt buộc trong mỗi dòng log

Một dòng log structured tốt nên luôn có một bộ trường (field) chuẩn:

  • timestamp: thời điểm chính xác, nên dùng chuẩn ISO 8601 và múi giờ UTC. Đừng dùng giờ local server — khi bạn có server ở Singapore và ở Frankfurt, giờ local sẽ khiến bạn loạn khi ghép timeline.
  • level: mức độ nghiêm trọng (debug, info, warn, error, fatal). Đây là bộ lọc đầu tiên khi có sự cố.
  • message: mô tả ngắn gọn, con người đọc được. Vẫn cần, nhưng đi kèm các trường có cấu trúc.
  • service / component: dịch vụ nào phát ra log này (ví dụ payment-service).
  • trace_id / request_id: định danh để lần theo một request đi xuyên qua nhiều dịch vụ. Đây là trường quý giá nhất trong kiến trúc microservices.

Log level — dùng cho đúng

Rất nhiều đội dùng level lung tung, dẫn tới log nhiễu hoặc bỏ sót. Hãy phân biệt rõ:

  • DEBUG: chi tiết cho lập trình viên khi phát triển. Thường tắt ở production.
  • INFO: sự kiện bình thường đáng ghi nhận — user đăng nhập, đơn hàng tạo thành công.
  • WARN: có gì đó bất thường nhưng hệ thống vẫn chạy — ví dụ retry lần 2, cache miss cao.
  • ERROR: một thao tác thất bại, cần người nhìn vào — thanh toán lỗi, không kết nối được database.
  • FATAL: hệ thống không thể tiếp tục, sắp crash.
Quy tắc mentor hay dặn học trò: nếu mọi thứ đều là ERROR thì chẳng có gì là ERROR cả. Level chỉ có giá trị khi bạn dùng nó kỷ luật.

Correlation ID — sợi chỉ đỏ xuyên hệ thống

Trong hệ thống nhiều dịch vụ, một request của khách hàng có thể đi qua API gateway → order service → payment service → inventory service. Nếu mỗi service log riêng lẻ, bạn không cách nào ghép chúng lại. Giải pháp: sinh một trace_id (hay request_id) ở điểm vào đầu tiên, rồi truyền nó xuống tất cả các dịch vụ phía sau. Khi có lỗi, bạn chỉ cần query theo trace_id đó là thấy toàn bộ hành trình của request. Đây là kỹ thuật đơn giản nhưng cứu bạn vô số giờ điều tra.

Đừng log những gì không được phép log

Log cũng là nơi rò rỉ dữ liệu nhạy cảm số một. Tuyệt đối không ghi vào log: mật khẩu, số thẻ tín dụng đầy đủ, CMND/CCCD, token xác thực, OTP. Ở Việt Nam, Nghị định 13/2023 về bảo vệ dữ liệu cá nhân khiến chuyện này không còn là "nên tránh" mà là nghĩa vụ pháp lý. Hãy masking (che) hoặc redact (loại bỏ) trước khi ghi.

Tình huống thực tế

Ví dụ 1: Sàn thương mại điện tử và đêm sale 12/12

Một sàn TMĐT lớn ở Đông Nam Á (giả định gọi là ShopViet) chạy chương trình sale 12/12. Đúng giờ vàng, tỷ lệ đơn hàng thất bại tăng vọt lên 8%. Đội SRE lao vào log.

Vấn đề: log của service thanh toán viết kiểu cũ — "Payment failed for order". Không có order_id, không có mã lỗi từ cổng thanh toán, không có user_id. Đội phải grep hàng chục nghìn dòng, đối chiếu thủ công với database. Mất 47 phút mới xác định được nguyên nhân: một cổng thanh toán nội địa bị timeout khi tải cao.

Sau sự cố, họ chuyển sang structured logging. Mỗi lần thanh toán lỗi giờ ghi:

{
  "event": "payment_failed",
  "order_id": "OD-8842213",
  "user_id": 55021,
  "gateway": "vnpay",
  "error_code": "GATEWAY_TIMEOUT",
  "latency_ms": 30011,
  "amount": 1290000
}

Ba tháng sau, sự cố tương tự lặp lại. Lần này đội chỉ cần một query: event = "payment_failed" AND gateway = "vnpay" — rồi vẽ biểu đồ theo error_code. Thời gian xác định nguyên nhân: 90 giây. Bài học: cùng một sự cố, log tốt biến 47 phút thành 1,5 phút.

Ví dụ 2: Startup fintech và cú rò rỉ suýt chết người

Một startup fintech ở TP.HCM có tính năng đăng nhập bằng OTP. Một lập trình viên junior, để tiện debug, đã thêm dòng log: log.info("OTP sent: " + otpCode + " to " + phoneNumber). Code chạy tốt, qua review sơ sài, lên production.

Vấn đề: toàn bộ OTP và số điện thoại khách hàng giờ nằm trong log, và log được đẩy lên một hệ thống tập trung mà cả chục nhân viên truy cập được. Bất kỳ ai xem log đều có thể đọc OTP của khách để chiếm tài khoản. May mắn là một kỹ sư senior phát hiện trong lúc review dashboard và báo động ngay.

Họ phải: xóa toàn bộ log lịch sử chứa OTP, buộc invalidate mọi OTP đang hoạt động, và thêm một bước kiểm tra tự động trong CI để chặn mọi commit có chứa từ khóa nhạy cảm (otp, password, token) trong lời gọi log. Bài học: log là một mặt tấn công (attack surface). Điều bạn ghi ra hôm nay có thể là lỗ hổng của ngày mai. Cấu trúc log phải đi kèm chính sách về dữ liệu nhạy cảm.

Ví dụ 3: Grab và bài toán correlation trong microservices

Với một nền tảng gọi xe quy mô như Grab, một cuốc xe đi qua rất nhiều dịch vụ: định vị, tính giá, ghép tài xế, thanh toán, thông báo. Giả sử một khách báo "app tính sai giá". Nếu mỗi service log độc lập, việc lần lại một cuốc xe cụ thể trong biển log là bất khả thi.

Cách làm chuẩn: ngay khi request vào, hệ thống sinh một trip_id và một trace_id, rồi gắn vào mọi log của mọi service liên quan đến cuốc xe đó. Khi cần điều tra, kỹ sư chỉ query trace_id = "abc-123" và thấy đầy đủ: định vị nhận điểm đón lúc nào, service tính giá trả ra giá bao nhiêu với hệ số surge nào, thanh toán trừ bao nhiêu. Toàn bộ câu chuyện của một request hiện ra theo đúng thứ tự thời gian. Bài học: trong hệ phân tán, một trường trace_id nhất quán còn giá trị hơn mười trang mô tả.

Hướng dẫn từng bước

Đây là lộ trình áp dụng structured logging cho một dịch vụ thực tế:

  • Chọn một thư viện logging có hỗ trợ structured output. Ví dụ: zap hoặc logrus (Go), structlog (Python), winston hoặc pino (Node.js), logback với encoder JSON (Java). Đừng tự viết bằng cách nối chuỗi.
  • Định nghĩa schema log chung cho toàn tổ chức. Thống nhất tên trường: dùng user_id hay userId? error_code hay err? Quyết định một lần, ghi thành tài liệu, và bắt mọi service tuân theo. Log không nhất quán về tên trường thì không query xuyên service được.
  • Chuẩn hóa các trường bắt buộc. Mọi log phải có tối thiểu: timestamp (UTC, ISO 8601), level, service, message, và trace_id nếu có. Cấu hình logger sinh các trường này tự động.
  • Triển khai correlation ID. Ở tầng vào (middleware của API gateway hoặc HTTP framework), sinh trace_id nếu request chưa có, rồi truyền nó qua header xuống các service phía sau và nhét vào context của logger.
  • Log ở đúng chỗ, đúng level. Log tại ranh giới quan trọng: khi nhận request, khi gọi service ngoài, khi thao tác thất bại. Đừng log trong vòng lặp chạy triệu lần.
  • Áp dụng redaction cho dữ liệu nhạy cảm. Thiết lập một lớp lọc tự động che các trường như password, card_number, otp trước khi ghi. Đừng phụ thuộc vào việc lập trình viên "nhớ" không log.
  • Kiểm thử log như kiểm thử code. Thêm bước trong CI quét các pattern nguy hiểm. Review log output như review một tính năng.

Lỗi thường gặp & mẹo

Lỗi: Log quá nhiều (log noise). Ghi mọi thứ ở level INFO khiến log phình to, tốn tiền lưu trữ và làm loãng tín hiệu quan trọng. Mẹo: hỏi "dòng log này giúp gì cho việc debug?" Nếu không trả lời được, đừng ghi.

Lỗi: Log message động không có cấu trúc. Viết log("Failed for user " + name) khiến mỗi dòng là một chuỗi duy nhất, không gom nhóm được. Mẹo: để phần biến vào trường riêng, giữ message tĩnh: message="user operation failed", user=name.

Lỗi: Không có timestamp chuẩn hoặc dùng giờ local. Khi ghép log từ nhiều server khác múi giờ, timeline loạn hết. Mẹo: luôn UTC, luôn ISO 8601.

Lỗi: Nuốt lỗi (swallow exception). Bắt exception rồi log "error occurred" mà không ghi stack trace hay error code. Mẹo: luôn log đủ ngữ cảnh để tái hiện — error type, message, stack trace, và các tham số liên quan.

Lỗi: Dùng print thay cho logger. print/console.log không có level, không có timestamp, không đẩy vào hệ thống tập trung được. Mẹo: cấm print ở production bằng linter.

Mẹo vàng: Hãy tưởng tượng bạn là chính mình lúc 3 giờ sáng, mắt nhắm mắt mở, đang debug sự cố. Dòng log bạn viết hôm nay có đủ để "bạn tương lai" hiểu chuyện gì đang xảy ra không? Nếu có, bạn đã viết log đúng.

Bài tập thực hành

  • Chuyển đổi log. Lấy đoạn log unstructured sau và viết lại thành JSON có cấu trúc, tự đề xuất các trường phù hợp:
"Order 4471 for user Minh could not be shipped because warehouse HN-02 is out of stock"

  • Thiết kế schema. Định nghĩa một schema log chuẩn cho service đăng nhập của một app ngân hàng Việt Nam. Liệt kê các trường bắt buộc, chỉ rõ trường nào tuyệt đối không được ghi và vì sao.
  • Viết query. Giả sử bạn có log structured với các trường event, country, error_code, latency_ms. Viết (bằng ngôn ngữ tự nhiên hoặc pseudo-query) truy vấn để tìm: mọi request lỗi từ Việt Nam có latency trên 5 giây trong 1 giờ qua.
  • Săn lỗi bảo mật. Rà soát một service bạn từng viết (hoặc code mẫu bất kỳ) và tìm mọi chỗ có nguy cơ log dữ liệu nhạy cảm. Đề xuất cách redact cho từng chỗ.

Tóm tắt

Log tốt là khác biệt giữa một sự cố kéo dài 47 phút và một sự cố xử lý trong 90 giây. Những điều cốt lõi cần khắc ghi:

  • Structured logging là bắt buộc: log là dữ liệu để query, không phải câu văn để đọc. Dùng JSON với các trường rõ ràng thay vì nối chuỗi.
  • Chuẩn hóa các trường bắt buộc: timestamp (UTC, ISO 8601), level, service, message, trace_id. Thống nhất tên trường trên toàn tổ chức.
  • Dùng log level có kỷ luật: DEBUG/INFO/WARN/ERROR/FATAL đúng ngữ nghĩa, đừng để mọi thứ thành ERROR.
  • Correlation ID là sợi chỉ đỏ: trong microservices, trace_id nhất quán giúp lần theo một request xuyên nhiều dịch vụ.
  • Log là mặt tấn công: không bao giờ ghi mật khẩu, OTP, thẻ, dữ liệu cá nhân. Redact tự động, đừng trông chờ vào trí nhớ con người. Nhớ Nghị định 13/2023.
  • Viết log cho "bạn của lúc 3 giờ sáng": đủ ngữ cảnh để hiểu và hành động ngay khi sự cố xảy ra.
Log chỉ là bước khởi đầu. Ở các bài sau, chúng ta sẽ học cách gom log tập trung để tìm kiếm ở quy mô lớn (Bài 49) và cách kết hợp log với metrics, traces để có bức tranh observability hoàn chỉnh. Nhưng nền móng thì bắt đầu ngay tại đây: mỗi dòng log bạn viết hôm nay.