Product Management
Đăng nhập
ESC

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

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

Bài 49 — Logging Best Practices

Đã đọc 0/14 mục · còn khoảng 12 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

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

Hãy tưởng tượng 2 giờ sáng, điện thoại bạn rung liên hồi. Cổng thanh toán của một sàn thương mại điện tử lớn tại TP.HCM đang lỗi, khách hàng không đặt được hàng trong giờ flash sale. Bạn mở terminal, SSH vào server, chạy tail -f trên file log và nhận về một biển chữ: hàng chục nghìn dòng text lẫn lộn, không có timestamp chuẩn, không biết request nào của khách nào, không biết lỗi bắt đầu từ service nào. Bạn mất 40 phút chỉ để tìm ra dòng log đúng — trong khi mỗi phút downtime là hàng chục triệu đồng doanh thu bốc hơi.

Đây là lý do logging không phải là thứ "viết cho có". Trong một hệ thống phân tán hiện đại với hàng chục microservice, log chính là hộp đen của máy bay — thứ duy nhất kể lại chính xác chuyện gì đã xảy ra khi mọi thứ sụp đổ. Một hệ thống logging tốt giúp bạn giảm thời gian điều tra sự cố (MTTR) từ hàng giờ xuống còn vài phút. Một hệ thống logging tệ thì ngược lại: nó đốt tiền lưu trữ, làm chậm ứng dụng, và tệ nhất là khiến bạn "mù" đúng vào lúc bạn cần nhìn rõ nhất.

Trong bài này, chúng ta tập trung vào best practices — tức là làm log đúng cách: viết cái gì, viết như thế nào, và tránh những cạm bẫy khiến log trở thành gánh nặng thay vì tài sản. Đây là phần thực hành gắn với công việc hằng ngày của một DevOps engineer, không phải lý thuyết suông.

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

Structured logging — nền tảng của mọi thứ

Điều quan trọng nhất bạn cần nắm: log phải có cấu trúc (structured), không phải văn xuôi (free-text). Thay vì viết:

Payment failed for user 8321 amount 250000 at 10:23

Bạn viết dưới dạng JSON có các trường rõ ràng:

{
  "ts": "2026-05-11T10:23:45.123Z",
  "level": "error",
  "service": "payment-api",
  "version": "1.2.7",
  "trace_id": "a3f9c2e1b7d84f60",
  "user_id": 8321,
  "event": "payment_failed",
  "amount": 250000,
  "currency": "VND",
  "gateway": "vnpay",
  "error": "gateway_timeout",
  "duration_ms": 3021
}

Sự khác biệt là một trời một vực. Với dòng free-text, muốn tìm "tất cả payment thất bại qua VNPay hôm nay có amount trên 200 nghìn" bạn phải viết regex phức tạp và cầu nguyện định dạng không đổi. Với JSON, đó chỉ là một câu truy vấn đơn giản: event="payment_failed" AND gateway="vnpay" AND amount > 200000. Máy đọc được, con người cũng đọc được, và công cụ như Loki, Elasticsearch hay CloudWatch có thể index và tìm kiếm trong mili-giây.

Log levels — dùng đúng mức độ

Mỗi dòng log cần một level để phân loại mức độ nghiêm trọng. Đừng lạm dụng — hãy hiểu đúng ý nghĩa:

  • DEBUG: chi tiết kỹ thuật để debug, chỉ bật khi phát triển hoặc điều tra sâu. Không bật trên production mặc định.
  • INFO: sự kiện bình thường đáng ghi lại — "user đăng nhập", "đơn hàng được tạo", "job chạy xong".
  • WARN: có điều bất thường nhưng hệ thống vẫn hoạt động — "retry lần 2", "cache miss cao bất thường", "config dùng giá trị mặc định".
  • ERROR: một thao tác thất bại, cần con người chú ý — "không ghi được vào DB", "gọi API bên thứ ba lỗi".
  • FATAL/CRITICAL: hệ thống không thể tiếp tục, thường kèm việc process thoát.
Nguyên tắc vàng: level là công cụ lọc. Nếu bạn log mọi thứ ở mức ERROR, thì khi có sự cố thật, ERROR trở nên vô nghĩa vì nó lẫn trong hàng nghìn "ERROR giả".

Correlation ID / Trace ID — sợi dây xuyên suốt

Trong hệ thống microservice, một request của khách hàng đi qua nhiều service: api-gateway → order-service → payment-api → inventory-service. Nếu mỗi service log riêng rẽ, bạn không cách nào ghép lại được hành trình của một request cụ thể.

Giải pháp là correlation ID (hay trace_id, request_id): sinh một mã duy nhất ở điểm vào (thường là API gateway), rồi truyền qua header (X-Request-Id) tới mọi service phía sau. Mỗi service ghi mã này vào mọi dòng log của nó. Khi điều tra, bạn chỉ cần lọc theo một trace_id và thấy toàn bộ câu chuyện xuyên suốt các service.

Ba nguyên tắc phụ trợ

  • Log ra stdout/stderr, không ghi file (đặc biệt trong container). Đây là triết lý của The Twelve-Factor App: ứng dụng coi log như một luồng sự kiện và "phun" ra stdout; hạ tầng (Docker, Kubernetes, agent như Fluent Bit) chịu trách nhiệm thu thập, chuyển tiếp, lưu trữ. Ứng dụng không nên quan tâm chuyện xoay file, nén, xóa.
  • Log phải bất đồng bộ (async). Việc ghi log không được chặn luồng xử lý chính. Dùng buffer/queue để log không làm chậm request của người dùng.
  • Không log dữ liệu nhạy cảm. Mật khẩu, số thẻ tín dụng, CCCD, token — tuyệt đối không đưa vào log. Đây vừa là vấn đề bảo mật, vừa là vấn đề tuân thủ pháp luật (Nghị định 13/2023 về bảo vệ dữ liệu cá nhân tại Việt Nam).

Tình huống thực tế

Ví dụ 1: Tiki và cơn ác mộng free-text log

Một đội kỹ thuật tại một sàn TMĐT lớn (bối cảnh tương tự Tiki, Shopee) ban đầu để mỗi lập trình viên tự viết log theo ý mình. Kết quả: cùng một sự kiện "thanh toán thất bại" mà service A ghi "payment error", service B ghi "TXN FAILED", service C ghi "Lỗi thanh toán - mã 502". Khi đội SRE muốn dựng dashboard đếm tỷ lệ thanh toán lỗi theo cổng, họ phải viết hàng chục regex khác nhau, và cứ mỗi lần deploy mới lại có định dạng mới làm hỏng dashboard.

Đội chuyển sang structured logging chuẩn hóa: định nghĩa một schema chung — mọi sự kiện thanh toán phải có event, gateway, amount, status, trace_id. Họ đóng gói thành một thư viện logging nội bộ để mọi service dùng chung. Sau ba tháng, thời gian dựng một dashboard mới giảm từ 2 ngày xuống 2 giờ, và MTTR cho sự cố thanh toán giảm khoảng 60%.

Bài học: structured logging chỉ phát huy sức mạnh khi được chuẩn hóa toàn tổ chức. Một schema chung + thư viện dùng chung quan trọng hơn việc từng cá nhân log "đẹp".

Ví dụ 2: Startup fintech và hóa đơn log 8.000 USD/tháng

Một startup fintech ở Singapore phục vụ thị trường Đông Nam Á dùng dịch vụ logging đám mây (kiểu Datadog Logs). Ban đầu chi phí không đáng kể. Nhưng khi lượng người dùng tăng, họ bật DEBUG trên production để "cho chắc", và mỗi request HTTP tạo ra 15-20 dòng log. Cuối tháng, hóa đơn logging nhảy lên hơn 8.000 USD — cao hơn cả tiền compute.

Họ điều tra và phát hiện: 85% khối lượng log là DEBUG vô dụng (log toàn bộ request/response body của mọi API call, kể cả health check chạy mỗi 5 giây). Giải pháp gồm ba phần: (1) đưa production về mức INFO, chỉ bật DEBUG động khi cần điều tra; (2) sampling — với log ở đường "happy path" tần suất cao, chỉ giữ lại 10%; (3) loại bỏ log của health check và các endpoint nội bộ. Kết quả: hóa đơn giảm còn khoảng 1.200 USD/tháng, mà khả năng điều tra sự cố không hề suy giảm.

Bài học: log không miễn phí. Mỗi dòng log tốn tiền lưu trữ, index và mạng. "Log ít mà đúng" luôn thắng "log nhiều cho chắc".

Ví dụ 3: Rò rỉ dữ liệu qua log tại một công ty ví điện tử

Một công ty ví điện tử tại Việt Nam gặp sự cố khi đội security audit phát hiện file log chứa số CCCD và số điện thoại đầy đủ của khách hàng ở dạng rõ (plaintext). Nguyên nhân: một lập trình viên log toàn bộ payload của request KYC để tiện debug, và log này được đẩy lên hệ thống lưu trữ tập trung mà nhiều nhân viên có quyền truy cập.

Đây không chỉ là lỗi kỹ thuật mà là rủi ro pháp lý nghiêm trọng theo Nghị định 13/2023. Đội xử lý bằng cách: thêm một lớp redaction (che dữ liệu) ngay trong thư viện logging — tự động thay các trường nhạy cảm (password, cccd, card_number, otp) bằng *; thiết lập kiểm tra tự động trong CI để chặn merge nếu phát hiện log các trường trong danh sách cấm.

Bài học: giả định rằng bất kỳ ai trong công ty đều có thể đọc log của bạn. Redaction phải là mặc định của hệ thống, không phải là kỷ luật cá nhân của từng dev.

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

Đây là cách xây dựng logging đúng chuẩn cho một service mới:

Bước 1 — Chọn thư viện structured logging. Đừng dùng print hay console.log. Mỗi ngôn ngữ có thư viện chuẩn: zap/zerolog (Go), pino (Node.js), structlog (Python), logback + logstash encoder (Java). Chúng xuất JSON sẵn và hiệu năng cao.

Bước 2 — Định nghĩa schema chung. Thống nhất các trường bắt buộc trong mọi dòng log: ts (timestamp ISO-8601 kèm UTC), level, service, version, trace_id, env. Đây là "hợp đồng" giữa các team.

Bước 3 — Cấu hình logger ở điểm khởi tạo (bootstrap). Gắn sẵn các trường chung (service name, version, environment) một lần khi khởi động, để không phải lặp lại ở mỗi lời gọi log.

Bước 4 — Triển khai correlation ID. Ở API gateway hoặc middleware đầu vào: nếu request đã có header X-Request-Id thì dùng lại, nếu chưa thì sinh mới (UUID). Truyền qua context và gắn vào mọi dòng log. Khi gọi service khác, đính kèm header này.

Bước 5 — Thêm lớp redaction. Định nghĩa danh sách trường nhạy cảm và tự động che chúng ngay trong logger. Không bao giờ log nguyên request/response body ở production.

Bước 6 — Log ra stdout. Trong container, để ứng dụng phun JSON ra stdout. Cấu hình agent thu thập (Fluent Bit, Vector, hoặc agent của cloud) đọc từ stdout và chuyển về hệ thống trung tâm.

Bước 7 — Thiết lập retention và sampling. Quyết định giữ log bao lâu (ví dụ ERROR giữ 90 ngày, INFO giữ 14 ngày), và sampling cho log tần suất cao. Điều này gắn chặt với ngân sách.

Bước 8 — Kiểm thử. Tạo lỗi giả và xác nhận log xuất hiện đúng, có trace_id, không lộ dữ liệu nhạy cảm, và tìm được trên hệ thống trung tâm.

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

Log quá nhiều (log spam). Log trong vòng lặp nóng, log mỗi bước của một thuật toán, log health check. Kết quả là "chôn vùi" tín hiệu quan trọng trong nhiễu và đốt tiền. Mẹo: trước khi thêm một dòng log, tự hỏi "khi đọc dòng này lúc 2 giờ sáng, nó có giúp tôi ra quyết định không?".

Log quá ít. Ngược lại, một số service chỉ log khi crash, khiến bạn không tái hiện được sự cố. Hãy log các ranh giới quan trọng: request vào/ra, gọi service ngoài, thay đổi trạng thái quan trọng.

Nuốt lỗi rồi log sai chỗ. catch (e) { log.info("something happened") } — mất luôn stack trace và giáng lỗi thật xuống INFO. Luôn log exception ở level ERROR kèm đầy đủ stack trace và context.

Timestamp không có timezone hoặc dùng giờ local. Server ở Singapore, dev ở Hà Nội, log ở giờ máy chủ — khi ghép log giữa các region sẽ loạn. Luôn dùng UTC với định dạng ISO-8601 (2026-05-11T10:23:45.123Z).

Log message động không nhất quán. Viết log.error("Cannot connect to db " + host) khiến mỗi host thành một message khác nhau, không group được. Thay vào đó để host là một trường riêng: log.error("db_connect_failed", {host: host}).

Không phân biệt log và metric/trace. Đừng đếm số lượng bằng cách grep log. Log kể chuyện gì đã xảy ra; số liệu tổng hợp thuộc về metrics (đã học ở phần monitoring). Dùng đúng công cụ cho đúng việc.

Mẹo — dynamic log level. Thiết kế để có thể đổi log level runtime (qua env var hoặc endpoint quản trị) mà không cần deploy lại. Khi điều tra sự cố, bật DEBUG cho một service trong 15 phút rồi tắt.

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

  • Chuyển đổi log. Lấy một đoạn code bất kỳ đang dùng print/console.log/free-text và chuyển sang structured logging với thư viện phù hợp ngôn ngữ của bạn. Đảm bảo output là JSON hợp lệ có đủ ts, level, service.
  • Correlation ID xuyên service. Dựng hai service nhỏ (A gọi B). Ở service A sinh trace_id, truyền qua header sang B. Xác nhận cả hai đều log cùng trace_id, rồi thử lọc theo mã đó để thấy toàn bộ hành trình.
  • Redaction. Viết một hàm bao quanh logger tự động thay các trường password, otp, card_number bằng *. Test với một object chứa các trường này và xác nhận chúng bị che.
  • Tính chi phí. Giả sử service của bạn nhận 500 request/giây, mỗi request tạo 8 dòng log, mỗi dòng trung bình 1 KB. Hãy tính khối lượng log mỗi ngày (GB). Sau đó tính lại nếu bạn sampling happy-path còn 10%. So sánh con số để cảm nhận tác động của sampling lên ngân sách.
  • Thiết kế schema. Viết ra tài liệu một trang định nghĩa schema log chung cho đội bạn: các trường bắt buộc, các level và ý nghĩa, danh sách trường cấm log. Đây chính là thứ bạn sẽ dùng thật khi đi làm.

Tóm tắt

Logging tốt là khoản đầu tư có lợi nhuận cao nhất mà một DevOps engineer có thể làm cho khả năng vận hành hệ thống. Những điểm cốt lõi cần nhớ:

  • Structured logging (JSON) là nền tảng — máy đọc được, tìm kiếm được, group được. Chuẩn hóa một schema chung toàn tổ chức quan trọng hơn việc từng người log đẹp.
  • Dùng log level đúng để log trở thành công cụ lọc, không phải bãi rác.
  • Correlation ID là sợi dây xuyên suốt giúp bạn tái hiện hành trình một request qua nhiều service.
  • Log ra stdout, bất đồng bộ, để hạ tầng lo việc thu thập — theo triết lý Twelve-Factor.
  • Không bao giờ log dữ liệu nhạy cảm; redaction phải là mặc định của hệ thống, vừa vì bảo mật vừa vì tuân thủ pháp luật.
  • Log không miễn phí: dùng sampling và retention hợp lý để kiểm soát chi phí — như bài học từ hóa đơn 8.000 USD.
Khi bạn viết một dòng log, hãy tưởng tượng chính bạn của 2 giờ sáng sẽ đọc nó. Nếu dòng đó giúp "bạn tương lai" ra quyết định nhanh hơn, đó là một dòng log tốt. Nếu không, đừng viết nó.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi