TL;DR
Worker không có SSH, không có /var/log. Observability nằm ở 4 tầng:
- Workers Logs — dashboard tích hợp sẵn, retention 3 ngày, không cần cấu hình, $0.60/1M lượt gọi. Đủ cho 90% debug hằng ngày.
- Tail Workers — stream realtime qua
wrangler tailhoặc Tail Worker tuỳ biến chuyển tiếp tới Sentry/Datadog. - Logpush — xuất log request theo batch sang R2, S3, Splunk, Elastic. Dành cho doanh nghiệp, thường để tuân thủ.
- Analytics Engine — nơi lưu sự kiện tuỳ biến, ghi từ Worker, truy vấn qua SQL API. Retention 90 ngày. Dành cho metric tuỳ biến theo app.
Luận điểm chính:
Log của Worker không giống log Linux. Không có file, không SSH. Debug production nghĩa là structured log + request ID + stream Tail Worker + metric Analytics Engine. Thiết lập đúng từ đầu = 80% sự cố xử lý trong 5 phút thay vì 5 giờ.
Bài này đi qua: 4 tầng kèm code thực tế, pattern structured logging, schema Analytics Engine + truy vấn SQL, cảnh báo email/Slack/PagerDuty, tích hợp Sentry, và debug sự cố thực tế.
Bài này mở Block 5 (Production). Part 18 sẽ vào Security.
Dành cho ai
- Dev vừa triển khai Worker production và muốn biết nó chạy ra sao.
- Nhóm debug sự cố: 5xx tăng đột biến, độ trễ tăng, thiếu dữ liệu.
- Ai cần metric tuỳ biến (mức dùng tính năng, phễu chuyển đổi) nhưng không muốn thiết lập Prometheus + Grafana.
Nên đọc trước: Part 2 (runtime), Part 12 (CI/CD).
Sau bài này bạn sẽ:
- Cài đặt structured logging với request ID.
- Thiết lập Tail Worker chuyển tiếp tới Sentry trong < 30 phút.
- Ghi metric tuỳ biến qua Analytics Engine + truy vấn SQL.
- Cảnh báo khi tỉ lệ lỗi > 1% hoặc p95 độ trễ > 500ms.
Bài này không nói về gì
- APM đầy đủ như Datadog, New Relic: có tích hợp nhưng không phải sẵn có từ Cloudflare. Bài tập trung vào stack sẵn có + cầu Tail Worker.
- Chính sách retention log cho tuân thủ: cần nghiêm túc thì Logpush → R2 và policy ở đó. Bài không đi chi tiết GDPR/HIPAA.
- Distributed tracing phức tạp (Jaeger, Zipkin): Worker là single-hop stateless, tracing không phải first-class. Pattern request ID thay thế tốt cho edge function.
4 layer tổng quan
Khi nào dùng cái nào
| Tình huống | Tầng |
|---|---|
| Debug “tại sao request này 500?” | Workers Logs |
| Stream log realtime khi xử lý sự cố | Tail Worker / wrangler tail |
| Chuyển tiếp mọi lỗi tới Sentry | Tail Worker tuỳ biến |
| Tuân thủ — giữ mọi request log 1 năm | Logpush → R2 |
| Metric tuỳ biến (mức dùng tính năng, chuyển đổi) | Analytics Engine |
| Cảnh báo khi lỗi > 1% | Cloudflare Notifications + Analytics Engine |
Không cần cả 4. Phần lớn nhóm bắt đầu với 2 tầng (Workers Logs + Analytics Engine), thêm Logpush khi cần tuân thủ.
Layer 1: Workers Logs
console.log/warn/error trong Worker được tự động bắt và xem trong dashboard.
Truy cập dashboard
Dashboard → Workers & Pages → Select Worker → Logs tab
Bộ lọc:
- Khoảng thời gian (15 phút, 1h, 6h, 24h, 3 ngày gần nhất).
- Mã trạng thái (2xx, 4xx, 5xx).
- Mức log (info, warn, error).
- Tìm chuỗi con trong message.
Bật trong wrangler.jsonc
Observability mặc định tắt với gói free, bật từ gói Paid. Bật lên:
{
"observability": {
"enabled": true,
"head_sampling_rate": 1.0 // 100% request được log
}
}
head_sampling_rate: 0.1 = 10% request được log (giảm chi phí cho site có lưu lượng cao).
Structured logging
console.log("user 123 logged in") khó truy vấn. Dùng JSON:
function log(level: string, message: string, context: Record<string, unknown> = {}) {
console.log(JSON.stringify({
level,
message,
timestamp: new Date().toISOString(),
...context,
}));
}
// Dùng
log("info", "user logged in", { userId: "abc-123", method: "oidc" });
log("error", "payment failed", { userId: "abc-123", orderId: "ord-1", reason: "card_declined" });
Dashboard có lọc theo trường JSON (nếu dùng Workers Logs v2). Tìm “userId:abc-123” để thấy mọi log của user đó.
Pattern request ID
Mỗi request gán một ID, đưa vào mọi log và header phản hồi.
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const requestId = crypto.randomUUID();
// Wrap log để tự include requestId
const log = (level: string, msg: string, ctx: Record<string, unknown> = {}) =>
console.log(JSON.stringify({ level, requestId, msg, ...ctx, ts: Date.now() }));
log("info", "request start", { path: new URL(request.url).pathname });
try {
const response = await handleRequest(request, env, log);
response.headers.set("x-request-id", requestId);
log("info", "request done", { status: response.status });
return response;
} catch (err) {
log("error", "request failed", { error: err.message, stack: err.stack });
return new Response("Internal error", {
status: 500,
headers: { "x-request-id": requestId },
});
}
},
};
User thấy x-request-id: abc-123 trong header phản hồi. Support ticket kèm ID → debug nhanh.
Giá
Workers Logs: $0.60/1M lượt gọi log vượt mức free tier. Site lưu lượng cao 1B req/tháng × lấy mẫu 10% = 100M log × $0.60/1M = $60/tháng. Tỉ lệ lấy mẫu quan trọng.
Tầng 2: Tail Workers
Stream log realtime khi đang debug trực tiếp.
wrangler tail
npx wrangler tail my-worker
Stream mọi log đầu ra theo thời gian thực. Lọc:
npx wrangler tail my-worker --status=error
npx wrangler tail my-worker --search="user-123"
npx wrangler tail my-worker --sampling-rate=0.1
Dùng khi đang xử lý sự cố trực tiếp. Không lưu — Ctrl+C là mất.
Tail Worker tuỳ biến
Tail Worker là Worker đặc biệt nhận event từ Worker production. Chuyển tiếp log đi đâu tuỳ bạn.
my-logger/src/index.ts:
export default {
async tail(events: TraceItem[], env: Env): Promise<void> {
for (const event of events) {
// event.scriptName, event.outcome, event.logs, event.exceptions
if (event.outcome === "exception" || event.exceptions.length > 0) {
await sendToSentry(event, env);
}
// Forward tất cả log level="error" tới Datadog
for (const log of event.logs) {
if (log.level === "error") {
await sendToDatadog(log, env);
}
}
}
},
} satisfies ExportedHandler<Env>;
async function sendToSentry(event: TraceItem, env: Env) {
await fetch(env.SENTRY_DSN, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: event.exceptions[0]?.message,
request: event.event,
tags: { worker: event.scriptName },
}),
});
}
my-logger/wrangler.jsonc:
{
"name": "my-logger",
"main": "src/index.ts",
"compatibility_date": "2026-05-01"
}
Triển khai:
cd my-logger && npx wrangler deploy
Gắn vào Worker production (my-app/wrangler.jsonc):
{
"name": "my-app",
"tail_consumers": [
{ "service": "my-logger" }
]
}
Triển khai my-app. Giờ mỗi request của my-app phát event cho my-logger, chuyển tiếp tới Sentry.
Tích hợp Sentry
Thư viện toucan-js tối ưu cho Worker:
import Toucan from "toucan-js";
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const sentry = new Toucan({
dsn: env.SENTRY_DSN,
context: ctx,
request,
environment: env.ENVIRONMENT,
});
try {
return await handleRequest(request, env);
} catch (err) {
sentry.captureException(err);
return new Response("Internal error", { status: 500 });
}
},
};
Inline capture khác Tail Worker: inline chặn request cho tới khi Sentry xác nhận. Tail Worker bất đồng bộ, không ảnh hưởng độ trễ production.
Khuyến nghị: Tail Worker cho production, toucan-js chỉ khi cần stack trace chi tiết theo từng request.
Tầng 3: Logpush
Xuất log request theo batch sang R2, S3, Splunk, Elastic, Datadog.
Thiết lập (tính năng Enterprise)
Dashboard → Analytics & Logs → Logpush → Create job.
Cấu hình:
- Dataset: HTTP requests, Workers traces, Spectrum, DNS firewall, v.v.
- Đích nhận: R2 bucket, S3 bucket, endpoint HTTP.
- Trường: ClientIP, Datetime, EdgeResponseStatus, v.v.
- Lấy mẫu: 0.01 = 1% request.
- Định dạng: JSON, NDJSON, CSV.
Đích nhận R2:
{
"destination_conf": "r2://my-bucket?account-id=xxx&access-key-id=xxx&secret-access-key=xxx",
"dataset": "workers_trace_events",
"fields": "Event,EventTimestampMs,Outcome,ScriptName,Logs,Exceptions",
"kind": "instant-logs"
}
Tình huống sử dụng
- Tuân thủ: giữ log 1 năm (PCI, HIPAA, SOC2).
- Phân tích bảo mật: WAF log → SIEM Splunk/Elastic.
- Xu hướng dài hạn: metric > 90 ngày (giới hạn Analytics Engine).
- Tương quan giữa hệ thống: log Worker + log AWS cùng trong Datadog.
Chi phí
Logpush là tính năng Enterprise. Liên hệ sales. Cân nhắc phương án khác:
- Workers Logs (3 ngày) + Analytics Engine (90 ngày) cho 99% trường hợp.
- Tail Worker tuỳ biến → R2 khi ngân sách eo hẹp.
Tự xây Logpush nghèo
// Tail Worker write event vào R2
export default {
async tail(events: TraceItem[], env: Env): Promise<void> {
const ndjson = events.map((e) => JSON.stringify(e)).join("\n");
const key = `logs/${new Date().toISOString().slice(0, 13)}/${crypto.randomUUID()}.ndjson`;
await env.R2.put(key, ndjson);
},
};
Mỗi giờ một prefix, mỗi batch một file. Scheduled Worker hằng ngày gộp các file nhỏ thành file lớn.
Chi phí: R2 storage $0.015/GB. 100M event × ~500 byte = 50GB = $0.75/tháng. Rẻ hơn Logpush nhiều.
Tầng 4: Analytics Engine
Nơi lưu sự kiện tuỳ biến. Worker ghi datapoint, truy vấn qua SQL API.
Thiết lập
wrangler.jsonc:
{
"analytics_engine_datasets": [
{ "binding": "AE", "dataset": "my_app_events" }
]
}
Ghi
env.AE.writeDataPoint({
indexes: ["user:abc-123"],
blobs: [
"/api/search", // blob1: path
"vn", // blob2: country
"claude-3.5", // blob3: model used
],
doubles: [
250.5, // double1: duration ms
1024, // double2: response size bytes
],
});
Schema:
- indexes: tối đa 1, chuỗi, trường lọc high-cardinality.
- blobs: tối đa 20, chuỗi, trường lọc low-cardinality + groupBy.
- doubles: tối đa 20, số, trường tổng hợp.
Worker không tính phí CPU cho thao tác ghi. Fire-and-forget.
Truy vấn qua SQL API
POST tới https://api.cloudflare.com/client/v4/accounts/<account-id>/analytics_engine/sql:
SELECT
blob1 AS path,
count() AS hits,
quantileWeighted(0.5, double1) AS p50,
quantileWeighted(0.95, double1) AS p95,
quantileWeighted(0.99, double1) AS p99
FROM my_app_events
WHERE timestamp > NOW() - INTERVAL '1' HOUR
GROUP BY path
ORDER BY hits DESC
LIMIT 20
Auth: Authorization: Bearer <scoped-api-token>.
Ví dụ thực tế: bài viết được xem nhiều
// Worker: log mỗi pageview
async function fetch(request: Request, env: Env) {
const response = await handleRequest(request, env);
if (response.ok && request.url.includes("/blog/")) {
env.AE.writeDataPoint({
indexes: [request.cf?.country ?? "unknown"],
blobs: [
new URL(request.url).pathname,
request.headers.get("user-agent") ?? "",
request.headers.get("referer") ?? "",
],
doubles: [1], // placeholder, count() dùng thay
});
}
return response;
}
Truy vấn top post tuần qua:
async function getPopularPosts(env: Env): Promise<PopularPost[]> {
const sql = `
SELECT blob1 AS path, count() AS views
FROM my_app_events
WHERE timestamp > NOW() - INTERVAL '7' DAY
AND blob1 LIKE '/blog/%'
GROUP BY blob1
ORDER BY views DESC
LIMIT 10
`;
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/analytics_engine/sql`,
{
method: "POST",
headers: {
Authorization: `Bearer ${env.AE_API_TOKEN}`,
"Content-Type": "application/json",
},
body: sql,
}
);
const { data } = await response.json();
return data;
}
Endpoint /api/popular của blog dùng pattern này.
Giá
- ~25M data point/tháng ở gói free.
- $0.25 cho mỗi 1M data point sau đó.
- Truy vấn SQL: miễn phí (mức dùng hợp lý).
Ví dụ: 1M lượt xem trang × 2 lần ghi mỗi lượt (trang + api) = 2M data point/tháng = miễn phí.
Lấy mẫu phía máy chủ
Dataset lớn (>1B) → Cloudflare tự động lấy mẫu. Trường _sample_interval trong mỗi dòng cho biết dòng đó đại diện cho bao nhiêu event thật.
SELECT sum(_sample_interval) AS real_count
FROM my_dataset
count() trả số dòng (có lấy mẫu). sum(_sample_interval) trả ước lượng số event thật.
Thiết lập cảnh báo
Cloudflare Notifications
Dashboard → Notifications → Add. Loại thông báo:
- Worker Errors: tỉ lệ lỗi vượt ngưỡng.
- Worker CPU: thời gian CPU vượt ngưỡng.
- HTTP 5xx rate: cấp zone.
- Billing: chi phí > $X.
Đích nhận: Email, Webhook, PagerDuty, Slack.
Ngưỡng đơn giản. Không có tổng hợp phức tạp. Đủ cho 80% nhu cầu.
Cảnh báo với Analytics Engine + Scheduled Worker
Phức tạp hơn: Scheduled Worker truy vấn AE mỗi 5 phút, gửi Slack khi vượt ngưỡng.
// scheduled handler
export default {
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
const result = await querySQL(env, `
SELECT
countIf(double1 >= 500) AS errors,
count() AS total
FROM my_app_events
WHERE timestamp > NOW() - INTERVAL '5' MINUTE
`);
const { errors, total } = result.data[0];
const errorRate = errors / total;
if (errorRate > 0.01) { // > 1%
await fetch(env.SLACK_WEBHOOK, {
method: "POST",
body: JSON.stringify({
text: `🚨 Error rate ${(errorRate * 100).toFixed(2)}% (${errors}/${total})`,
}),
});
}
},
};
wrangler.jsonc:
{
"triggers": {
"crons": ["*/5 * * * *"]
}
}
Chạy mỗi 5 phút. Cảnh báo chi tiết hơn loại sẵn có.
Debug sự cố: playbook
Tình huống thật: user báo “5xx khi đăng ký newsletter”. 10:30 sáng, đang trong cuộc họp.
Phút 1: Workers Logs
Dashboard → Worker my-app → Logs. Lọc status: 5xx, 30 phút gần nhất.
Thấy 15 log error, tất cả trong 10:20-10:28. Mọi log có:
{
"level": "error",
"msg": "request failed",
"requestId": "...",
"error": "D1_ERROR: too many requests"
}
Nguyên nhân gốc: D1 rate limit.
Phút 2: Kiểm tra metric D1
Dashboard → D1 → Metrics. Số truy vấn: tăng đột biến 10:15-10:28 từ 50/s lên 300/s. Ai đó gọi /api/subscribe nhiều.
Phút 3: Tail Worker kiểm tra lạm dụng
wrangler tail my-app --search="/api/subscribe"
Thấy 200 request/phút từ cùng IP. Bot tấn công.
Phút 4: Giảm thiểu
Triển khai rule rate limit:
// Add to Worker
const subscribeLimiter = env.RATE_LIMITER.get(env.RATE_LIMITER.idFromName(`ip:${clientIP}`));
const { allowed } = await subscribeLimiter.fetch(...).json();
if (!allowed) return new Response("Too many", { status: 429 });
Push → CI → triển khai.
Phút 5: Xác minh
wrangler tail thấy 429 trả về cho bot. Số truy vấn D1 giảm về baseline.
Sau sự cố
Truy vấn Analytics Engine:
SELECT blob1 AS ip, count() AS req
FROM subscribe_events
WHERE timestamp > NOW() - INTERVAL '1' HOUR
GROUP BY ip
ORDER BY req DESC
LIMIT 20
Xác định phạm vi tấn công, báo lạm dụng. Rule vĩnh viễn qua WAF.
Tổng thời gian: 5 phút từ lúc báo → giảm thiểu. Nhờ 4 tầng observability sẵn sàng.
Các lỗi hay gặp
① console.log không định dạng trong console trình duyệt
console.log(obj) trong dashboard Worker hiển thị [object Object]. Dùng console.log(JSON.stringify(obj)). Hoặc Workers Logs v2 tự động phân tích JSON.
② waitUntil cho log bất đồng bộ
Log gọi ra ngoài (Sentry, Datadog) không await → request trả về trước khi log được gửi. Dùng ctx.waitUntil():
ctx.waitUntil(sendToSentry(error));
return response;
③ Analytics Engine _sample_interval dễ quên
Dataset >1B datapoint bị lấy mẫu. Truy vấn count() sẽ báo thiếu. Luôn dùng sum(_sample_interval) để lấy tổng:
-- Wrong: count() chỉ đếm row
SELECT blob1, count() FROM ae GROUP BY blob1
-- Right: scale với _sample_interval
SELECT blob1, sum(_sample_interval) FROM ae GROUP BY blob1
④ Log request làm chi phí phình to
1B request/tháng × Workers Logs $0.60/1M = $600/tháng chỉ riêng log. Lấy mẫu 10% giảm còn $60. Đặt head_sampling_rate từ đầu.
⑤ Tail Worker bị lặp
Tail Worker log = mỗi request Worker production = 1 log. Tail Worker log chính nó = vòng log vô hạn. Không log bên trong Tail Worker trừ khi cần.
⑥ Dữ liệu nhạy cảm trong log
console.log(request.headers) đổ cả token Authorization. PII nguy hiểm. Che đi:
function redact(headers: Headers): Record<string, string> {
const obj = Object.fromEntries(headers);
delete obj.authorization;
delete obj.cookie;
if (obj["x-api-key"]) obj["x-api-key"] = "***";
return obj;
}
⑦ Giới hạn buffer log
Worker tối đa 128 log entry/request trong dashboard. Dịch vụ lưu lượng cao, log nhiều = xoay vòng qua Tail Worker → R2.
⑧ Múi giờ
Timestamp log Cloudflare là UTC. Dashboard có thể chuyển sang local, API trả UTC. Ghi rõ trong tài liệu cho nhóm.
Thiết lập từ đầu: 30 phút
Phút 0-5: bật observability trong wrangler.jsonc, triển khai. Logs có ngay trong dashboard.
Phút 5-15: hàm helper structured logging + request ID. Mỗi log = JSON có requestId.
Phút 15-25: dataset Analytics Engine + writeDataPoint cho mỗi request. Metric chính: path, duration, status.
Phút 25-30: Cảnh báo Cloudflare Notifications cho tỉ lệ lỗi + webhook Slack.
30 phút = stack observability đầy đủ cho app nhỏ/vừa.
Danh sách kiểm tra production
-
observability.enabled: truetrongwrangler.jsonc. - Tỉ lệ lấy mẫu phù hợp lưu lượng (1.0 cho thấp, 0.1 cho cao).
- Structured logging JSON có level, requestId, timestamp.
- Request ID trong header phản hồi cho support.
- Che các trường nhạy cảm (token, PII) trước khi log.
- Tail Worker chuyển tiếp lỗi tới Sentry hoặc hệ thống giám sát bên ngoài.
- Dataset Analytics Engine cho metric nghiệp vụ (lượt xem trang, chuyển đổi, độ trễ).
- Truy vấn SQL tái sử dụng cho dashboard /
/api/metrics. - Cảnh báo tỉ lệ lỗi, p95 độ trễ, ngân sách chi phí.
- Scheduled Worker cho cảnh báo phức tạp (nếu Notifications sẵn có không đủ).
-
ctx.waitUntil()cho lời gọi log bất đồng bộ. - Logpush sang R2 nếu cần tuân thủ / lưu dài hạn.
Kết
Observability không phải tuỳ chọn. Worker production không có SSH — không log là không biết gì. 4 tầng Cloudflare bao phủ: debug hằng ngày (Workers Logs), stream khi sự cố (Tail Worker), tuân thủ (Logpush), metric tuỳ biến (Analytics Engine).
Thiết lập đúng 30 phút = tiết kiệm hàng chục giờ debug. Không thiết lập = mù trong production.
Part 18: Security — quản lý secret, CSP header, Bot Management, Turnstile, Cloudflare Access, pattern signed cookie, và phòng thủ theo chiều sâu cho Worker.