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.
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ùngtrace_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_numberbằ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.