Log cơ bản và uptime monitoring
Bài 2: Log cơ bản và uptime monitoring
Bài trước bạn đã tách dev/prod và đưa secrets ra khỏi code. Giờ app đã chạy trên prod với biến môi trường đúng, nhưng bạn vẫn đang "mù": nếu app chết lúc 2 giờ sáng, ai báo cho bạn biết? Bài này là cặp mắt và cái chuông báo cháy của bạn.
Sau 30 phút này, bạn sẽ:
- Chọn được mức log tối thiểu cho production
- Biết log gì, và tuyệt đối không log gì
- Tìm được lỗi trong log theo một trình tự cố định, không đọc lan man
- Dựng một
/healthendpoint và một kênh cảnh báo uptime có test thật
Log level: bật vừa đủ, không bật hết
Log level là mức độ nghiêm trọng của một dòng log. Hầu hết thư viện đều có 5 mức này:
| Mức | Dùng khi nào ở prod |
|---|---|
error |
Việc đã hỏng thật, cần người xem |
warn |
Bất thường nhưng app vẫn chạy, ví dụ retry lần 2 |
info |
Sự kiện chính: app start, deploy xong, đăng nhập thất bại |
debug |
Chi tiết để gỡ lỗi, chỉ nên bật ở local |
trace |
Cực chi tiết, gần như không bao giờ bật ở prod |
Ở production, mức mặc định nên là info. Bật debug ở prod nghĩa là log phình to, tốn tiền lưu trữ, và dễ vô tình ghi cả dữ liệu người dùng vào đó. Khi cần điều tra, bật debug tạm 10-15 phút, xem xong thì tắt lại ngay.
Cách làm gọn nhất là đọc từ biến môi trường, đúng tinh thần bài trước:
const LOG_LEVEL =
process.env.LOG_LEVEL ||
(process.env.NODE_ENV === "production" ? "info" : "debug");
Nếu app chưa có thư viện log, cứ dùng console.log với JSON. Hầu hết hosting hiện nay (Railway, Render, Fly, Vercel) tự thu stdout và cho bạn xem lại trong dashboard.
Tip: Đặt
LOG_LEVEL=infotrong biến môi trường prod, không hard-code. Khi cần điều tra chỉ đổi biến và restart, không phải sửa code rồi deploy lại.
Bài tập 1: App của bạn đang ghi mỗi request một dòng debug kèm toàn bộ body. Ở prod, 2 ngày log đã 400MB. Bạn sửa gì trước?
Gợi ý đáp án
Đổi LOG_LEVEL sang info trước (nhanh nhất, không cần deploy code). Sau đó mới xem lại chỗ ghi body request, vì body có thể chứa email, số điện thoại, token. Chỉ log những trường thật sự cần: method, path, status, thời gian xử lý.
Log gì và tuyệt đối không log gì
Một dòng log tốt ở prod trả lời được 3 câu: chuyện gì xảy ra, khi nào, và liên quan tới request nào. Vì vậy hãy log có cấu trúc:
console.log(JSON.stringify({
level: "info",
event: "deploy_started",
version: "v1.2.0",
at: new Date().toISOString(),
}));
Vì sao dùng JSON thay vì câu văn? Vì sau này bạn lọc được theo trường: tìm mọi dòng có level: "error", hay mọi dòng của một request_id. Đọc bằng mắt thì thô, nhưng tìm thì cực nhanh.
Danh sách tuyệt đối không log: password, API key, token, cookie, số thẻ, toàn bộ header Authorization, và dữ liệu cá nhân không cần thiết. Log là thứ được lưu hàng tháng và đôi khi bị chia sẻ khi nhờ người khác debug. Lộ log là lộ secret, và bạn sẽ phải rotate toàn bộ key.
Nên log: app start/stop, deploy (version + thời điểm), lỗi error kèm stack, và các sự kiện bảo mật như đăng nhập thất bại.
Bài tập 2: Trong 4 dòng sau, dòng nào phải xoá khỏi code?
1. log.info({ event: "login_failed", email_masked: "a***@gmail.com" })
2. log.info({ event: "payment", card_number: req.body.card })
3. log.error({ event: "db_error", message: err.message })
4. log.info({ event: "deploy_done", version: "v1.3.0" })
Gợi ý đáp án
Dòng 2, tuyệt đối không log số thẻ. Dòng 1 ổn vì đã che email. Dòng 3 ổn, nhưng nhớ đừng log nguyên err.stack nếu stack chứa connection string có mật khẩu. Dòng 4 là mẫu log deploy nên có.
Tìm lỗi trong log trong 2 phút
Khi có sự cố, đừng cuộn log từ trên xuống. Làm theo thứ tự này:
- Xác định mốc thời gian: sự cố bắt đầu lúc mấy giờ (giờ UTC để khỏi lệch múi giờ)?
- Lọc
level: "error"trong khoảng 15 phút quanh mốc đó. - Đọc dòng đầu tiên, không phải dòng cuối cùng. Lỗi gốc thường ở đầu chuỗi, các dòng sau chỉ là hệ quả.
- Đối chiếu với log deploy: có deploy nào rơi đúng mốc đó không? 80% sự cố của tôi là do deploy.
- Ghi lại kết luận thành 1 câu vào một file
incident.md. Lần sau gặp lại, bạn tiết kiệm 30 phút.
Warning: Đừng sửa code trước khi đọc log. Phản xạ sửa mò lúc 2 giờ sáng thường làm app hỏng thêm một tầng nữa.
Bài tập 3: Log của bạn có 3 dòng liên tiếp:
10:02:11 info deploy_started version=v1.4.0
10:02:40 error db_error: relation "orders" does not exist
10:02:41 error request_failed path=/api/orders status=500
Lỗi gốc là gì và bạn sửa ở đâu?
Gợi ý đáp án
Lỗi gốc là migration chưa chạy trên prod: bảng orders không tồn tại. Dòng thứ 3 chỉ là hệ quả. Sửa bằng cách chạy migration trong bước deploy, không sửa code xử lý request.
Uptime monitoring: người gác cổng lúc 2 giờ sáng
Uptime monitoring là một dịch vụ bên ngoài app, tự động gọi vào một URL của bạn theo chu kỳ. Nếu gọi thất bại vài lần liên tiếp, nó bắn cảnh báo cho bạn. Điểm mấu chốt: phải là bên ngoài. Nếu bạn tự viết cron kiểm tra trên chính server đó, khi hosting sập thì cái cron cũng sập theo và bạn không nhận được gì.
Bước 1, tạo endpoint kiểm tra sức khỏe:
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok", uptime: process.uptime() });
});
Bước 2, chọn dịch vụ (UptimeRobot, Better Stack, Healthchecks.io, hoặc Pingdom), trỏ nó vào https://your-domain.com/health, đặt chu kỳ 1 đến 5 phút, timeout khoảng 10 giây.
Bước 3, đặt ngưỡng cảnh báo là 2 lần thất bại liên tiếp. Một lần timeout đơn lẻ là chuyện bình thường, báo ngay sẽ khiến bạn thức dậy vô ích vài lần rồi tắt luôn thông báo (người ta gọi là alert fatigue).
Bước 4, chọn kênh bạn thực sự nhìn thấy: Telegram, Slack, SMS. Đừng chỉ dùng email, email dễ chìm.
Bước 5, bắt buộc: bấm nút test alert. Và lần sau, thử tắt app thật 5 phút xem chuông có kêu không. Một kênh cảnh báo chưa test thì coi như chưa có.
Bài tập 4: Thiết lập của bạn: chu kỳ 1 phút, ngưỡng 1 lần thất bại, cảnh báo qua email. Tuần đầu bạn nhận 40 email, tuần sau bạn chặn luôn địa chỉ đó. Sai ở đâu?
Gợi ý đáp án
Sai ở cả 3 chỗ: ngưỡng quá nhạy (đổi sang 2-3 lần liên tiếp), kênh quá yếu (thêm Telegram hoặc SMS), và thiếu phân loại. Chỉ email cho sự cố thật, còn log lại để xem sau. Mục tiêu là mỗi cảnh báo bạn nhận được đều đáng để mở điện thoại.
Ba cái bẫy khiến log và cảnh báo thành vô dụng
- Log không có timestamp: không timestamp thì không ghép được với sự kiện nào. Luôn dùng ISO 8601 UTC.
- Cảnh báo gửi vào nơi không ai đọc: kênh cảnh báo phải là kênh bạn mở trong 5 phút. Test ngay trong ngày hôm nay.
- Quên rằng monitor cũng có thể sai: khi nhận cảnh báo, việc đầu tiên là mở
/healthbằng trình duyệt để xác nhận app thật sự chết, không phải monitor báo nhầm.
Một mẹo nhỏ tôi ước mình biết sớm: viết sẵn một dòng log.info({ event: "deploy_done", version }) ở cuối mỗi lần deploy. Sau này nhìn log là biết ngay sự cố có trùng với deploy hay không.
Chốt lại và bước tiếp theo
Điền nhanh 5 câu để tự kiểm tra:
- Ở production, mức log mặc định nên là _____.
- Tuyệt đối không ghi _____ hoặc API key vào log.
- Uptime monitoring phải chạy từ _____ app.
- Endpoint kiểm tra sức khỏe thường có đường dẫn _____.
- Cảnh báo chỉ nên bắn sau _____ lần kiểm tra thất bại liên tiếp.
Bạn vừa có hai thứ: log đủ để hiểu chuyện gì đã xảy ra, và một cái chuông báo khi app chết. Nhưng biết app chết rồi thì làm gì? Bài tiếp theo, Backup dữ liệu và rollback khi lỗi, sẽ cho bạn đường lùi: khôi phục dữ liệu và quay về bản deploy trước đó trong vài phút.
Front: Ở production, mức log mặc định nên là gì? Back: info, chỉ log các sự kiện chính; debug chỉ bật tạm khi điều tra.
Front: Vì sao không được log password hay API key? Back: Log được lưu lâu và dễ bị chia sẻ, lộ log là lộ secret và phải rotate toàn bộ key.
Front: Log có cấu trúc (JSON) hơn log text thường ở điểm nào? Back: Lọc và tìm được theo trường như level, event, request_id thay vì đọc bằng mắt.
Front: Timestamp trong log nên ở dạng nào? Back: ISO 8601 theo UTC, để so sánh được giữa các server và múi giờ khác nhau.
Front: Uptime monitoring là gì? Back: Dịch vụ bên ngoài tự gọi vào app theo chu kỳ để biết app còn sống hay không.
Front: Vì sao không tự kiểm tra uptime bằng cron trên chính server đó? Back: Khi server hoặc hosting sập thì tiến trình kiểm tra cũng sập theo, không ai báo bạn.
Front: Endpoint /health dùng để làm gì? Back: Trả về trạng thái sống của app để monitor và load balancer kiểm tra.
Front: Chu kỳ kiểm tra uptime hợp lý cho app nhỏ? Back: 1 đến 5 phút một lần, đủ nhanh để biết sự cố mà không tốn quota.
Front: Vì sao đặt ngưỡng cảnh báo 2 lần thất bại liên tiếp? Back: Tránh báo động giả do một lần timeout mạng đơn lẻ.
Front: Alert fatigue là gì? Back: Quá nhiều cảnh báo khiến bạn bỏ qua hoặc tắt luôn cả cảnh báo thật.
Front: Bước đầu tiên khi nhận cảnh báo app down? Back: Mở /health để xác nhận, rồi xem log và log deploy gần nhất trước khi sửa gì.
Front: Trong một chuỗi log lỗi, nên đọc dòng nào trước? Back: Dòng lỗi đầu tiên, vì đó thường là lỗi gốc; các dòng sau chỉ là hệ quả.
Front: Kênh cảnh báo uptime nên đặt ở đâu? Back: Nơi bạn thực sự nhìn thấy trong vài phút: Telegram, Slack hoặc SMS, và phải test một lần.