Observability cho Worker: Logs, Tail Workers, Analytics

4 tầng observability Cloudflare: Workers Logs (retention 3 ngày), Tail Workers (realtime), Logpush (batch tới R2/SIEM), Analytics Engine. Structured logging, alert, debug prod.

· 9 phút đọc · Read in English
4 tầng observability của Worker: Workers Logs (dashboard tích hợp 3 ngày), Tail Workers (stream realtime), Logpush (xuất batch tới R2/SIEM), Analytics Engine (sự kiện tuỳ biến + SQL) cho alert và debug production

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 tail hoặ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

Stack observability: Workers Logs (dashboard, 3-day), Tail Workers (realtime stream), Logpush (R2 / SIEM), Analytics Engine (custom event), Dashboard built-in (metrics CPU/errors), Alerts (email/Slack/PagerDuty). Worker ở center, forward data ra tất cả 4 layer tùy need.

Khi nào dùng cái nào

Tình huốngTầ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 SentryTail Worker tuỳ biến
Tuân thủ — giữ mọi request log 1 nămLogpush → 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.

Flow Analytics Engine: Worker writeDataPoint (blobs + doubles), lưu time-series 90 ngày với column store, query qua SQL API (count, quantileWeighted, groupBy), dashboard Grafana hoặc custom Worker admin page.

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: true trong wrangler.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.


Tham khảo