Cloudflare WorkersのエラーをSlackに送信する

Cloudflare WorkersのエラーをSlackに送信する

Cloudflare Workersで複雑な処理を書くようになったので、Tail Workersを使ってSlackへのエラー通知を実装してみました。その手順を紹介します。
2026.10.08

こんばんは、情報システム室の夏目です。

最近個人開発でCloudflare Workersを使っているのですが、シンプルな処理しかさせていなかったのでエラー通知をさせていませんでした。

今作っているもので複雑な処理を書く必要が出てきたので、さすがにエラー通知をする必要があるかなと思いました。

そのため、今回はCloudflare WorkersでSlackへのエラー通知を実装します。

1. アーキテクチャ

エラー通知をするにあたって、Tail Workersという機能を使います。

https://developers.cloudflare.com/workers/observability/logs/tail-workers/

Tail WorkersではあるWorker (Producer)の実行におけるHTTPステータスや console.log() に渡されたデータ、補足されなかった例外などを、別のWorker (Consumer)に渡して処理させることができます。

原則として、Producer Workerの実行完了後にイベントをキャプチャして、Consumer Workerに渡され起動します。

2. 試してみる

Tail Workerを使ってエラー通知をしてみます。
Producer Workerの名前を trial-tail-worker-producer, Consumer Workerを trial-tail-worker-consumer とします。

通知にはSlackのincoming webhookを使用します。

2-1. 二つのWorkerの雛型を作成する

trial-tail-worker-producerとtrial-tail-worker-consumerの雛型を作成します。

選択肢は両方とも次のように選択します。

  • Which package manager do you want to use?: npm
$ npx cf init trial-tail-worker-producer
$ npx cf init trial-tail-worker-consumer

2-2. trial-tail-worker-consumerの設定を行う

trial-tail-worker-consumerのcloudflare.config.tsを編集します。

trial-tail-worker-consumer/cloudflare.config.ts
 import { bindings, defineConfig } from "cf/config";

 import * as entrypoint from "./src/index.ts" with { type: "cf-worker" };

 export default defineConfig({
   worker: {
     name: "trial-tail-worker-consumer",
     compatibilityDate: "2026-10-06",
     entrypoint,
+    workersDev: false,
+    previewUrls: false,
     env: {
-      WORLD: bindings.text("World"),
+      INCOMING_WEBHOOK_URL: bindings.secret(),
+      CLOUDFLARE_ACCOUNT_ID: bindings.secret(),
     },
   },
 });

Tail Workerを使用するにあたって、Consumer Worker側の設定で特別なことを行う必要がありません。

今回はConsumer WorkerではHTTPアクセスが必要ないので workersDev, previewUrls の値をfalseとし、Slackのincoming webhookのURLとCloudflareのアカウントIDをシークレットとしてenvに追加しています。

cloudflare.config.tsを変更したので、型ファイルを再生成します。

$ cd trial-tail-worker-consumer
$ npx cf workers types
🍊☁  cf · v1.0.0-beta.13
─────────────────────────
{
  "path": ".cloudflare/types/index.d.ts"
}

2-3. trial-tail-worker-consumerのコードを実装する

型ファイルの再生成をしたのでtrial-tail-worker-consumerのindex.tsを実装します。

trial-tail-worker-consumer/src/index.ts
import { env } from "cloudflare:workers";

/**
 * Workerの実行が正常終了したかを取得する
 */
function isSucceeded(event: TraceItem): boolean {
  return event.outcome === "ok";
}

/**
 * ダッシュボードのobservabilityページ用にQueryStringを組み立てる
 */
function generateSearchParams(event: TraceItem): URLSearchParams {
  // 開始日時も含まれないようなので1秒戻す
  const unixtimeMsStart = (event.eventTimestamp as number) - 1000;

  // WorkersのPaid Planだと最大5分なので、あと開始日時を1秒戻しているから一秒足す
  // 5 (min) * 60 (sec/min) * 1000 (ms/sec) = 300000 (ms)
  const unixtimeMsEnd = unixtimeMsStart + 300000 + 1000;

  const dtStart = new Date(unixtimeMsStart);
  const dtEnd = new Date(unixtimeMsEnd);

  const params = new URLSearchParams();
  params.append("timeframe", `${dtStart.toISOString()}/${dtEnd.toISOString()}`);

  return params;
}

/**
 * ダッシュボードのobservabilityページ用のURL
 */
function generateObservabilityPageUrl(workerName: string, params: URLSearchParams): string {
  return [
    "https://dash.cloudflare.com/",
    env.CLOUDFLARE_ACCOUNT_ID,
    "/workers/services/view/",
    workerName,
    "/production/observability/events?",
    params.toString(),
  ].join("");
}

/**
 * Incoming Webhookに送信するメッセージを作成する
 */
function generateMessageBody(workerName: string, observabilityPageUrl: string): string {
  const data = {
    text: `error occurred in ${workerName}: <${observabilityPageUrl}|link>`,
  };
  return JSON.stringify(data);
}

/**
 * Incoming Webhookにメッセージを送信する
 */
async function sendMessage(body: string) {
  await fetch(env.INCOMING_WEBHOOK_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body,
  });
}

export default {
  async tail(allEvents: TraceItem[]) {
    for (const event of allEvents) {
      if (isSucceeded(event)) {
        continue;
      }

      const workerName = event.scriptName as string;

      const params = generateSearchParams(event);
      const observabilityPageUrl = generateObservabilityPageUrl(workerName, params);

      const body = generateMessageBody(workerName, observabilityPageUrl);

      await sendMessage(body);
    }
  },
} satisfies ExportedHandler<Env>;

Tail WorkerのComsumer Workerでは tail()というハンドラーを実装します。

TailItems の型定義は次のページにあります。
(tsの型定義と一部一致しなかった)

https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/

allEventsのサンプルは次のページにあります。

https://developers.cloudflare.com/workers/observability/logs/tail-workers/

今回はProducer Workerが正常終了しなかったときにSlack通知するようにしています。

実際にはTrailItemsのlogsを見て、ログレベルがエラーだった場合も通知するようにした方がいいと思います。

2-4. trial-tail-worker-consumerのデプロイをする

trial-tail-worker-consumerをデプロイするのですが、シークレットの値が必要なのでJSONファイルに記載します。
試すときには実際の値を記載してください。

trial-tail-worker-consumer/secrets.json
{
  "INCOMING_WEBHOOK_URL": "<incoming webhook url>",
  "CLOUDFLARE_ACCOUNT_ID": "<Cloudflare Account ID>"
}

cfコマンドでデプロイをします。

$ npx cf deploy --secrets-file secrets.json
✓ built in 14ms
◆  Build complete
│
├  Deploy
│  Total Upload: 1.79 KiB / gzip: 0.92 KiB
│  Worker Startup Time: 2 ms
│  Your Worker has access to the following bindings:
│  Binding                                     Resource                  
│  env.INCOMING_WEBHOOK_URL ("(hidden)")       Environment Variable      
│  env.CLOUDFLARE_ACCOUNT_ID ("(hidden)")      Environment Variable      
│
│  Uploaded trial-tail-worker-consumer (1.53 sec)
│  No targets deployed for trial-tail-worker-consumer (0.89 sec)
│  Current Version ID: 079b7676-f787-4d7f-af8f-da4ae626e22f
│
◆  Deploy complete

trial-tail-worker-consumerのデプロイが完了しました。

2-5. trial-tail-worker-producerの設定を行う

trial-tail-worker-producerの設定を行います。

trial-tail-worker-producer
-import { bindings, defineConfig } from "cf/config";
+import { defineConfig } from "cf/config";

 import * as entrypoint from "./src/index.ts" with { type: "cf-worker" };

 export default defineConfig({
   worker: {
     name: "trial-tail-worker-producer",
     compatibilityDate: "2026-10-06",
     entrypoint,
-    env: {
-      WORLD: bindings.text("World"),
+    observability: {
+      enabled: true,
+      logs: {
+        enabled: true,
+      },
     },
+    tailConsumers: [{ worker: "trial-tail-worker-consumer" }],
   },
 });

Tail WorkerではProducer Workerの設定ファイルに、Consumer Workerの名前を記載して設定します。
Consumer WorkerはProducer Workerに対して複数設定できます。

またobservabilityなどを enabled: true にして、ログなどを保存するようにしています。

2-6. trial-tail-worker-producerのコードを実装する

trial-tail-worker-producerのコードを実装します。

trial-tail-worker-producer/src/index.ts
export default {
  fetch() {
    console.error("throw error");
    throw new Error("test");
  },
} satisfies ExportedHandler;

今回Consumer WorkerではProducer Workerが正常終了しなかったときにSlackに通知をするようにしているので、例外をスローします。
また、エラーログも投げるようにしています。

2-7. trial-tail-worker-producerをデプロイする

cfコマンドでデプロイします。

$ npx cf deploy
✓ built in 9ms
◆  Build complete
│
├  Deploy
│  Total Upload: 0.18 KiB / gzip: 0.14 KiB
│  Worker Startup Time: 1 ms
│  Your Worker is sending Tail events to the following Workers:
│  - trial-tail-worker-consumer
│  Uploaded trial-tail-worker-producer (1.39 sec)
│  Deployed trial-tail-worker-producer triggers (0.73 sec)
│    https://trial-tail-worker-producer.luciferous.workers.dev
│  Current Version ID: cb48931a-1c6f-4c72-8d4a-878ae970ce11
│
◆  Deploy complete

2-7. 確認する

Producer WorkerのURLを叩いて、エラー通知がSlackに来るか確認します。

$ curl https://trial-tail-worker-producer.luciferous.workers.dev
error code: 1101

error code: 1101 は「WorkerでJavaScriptの例外が発生しました」というエラーコードです。

Slackを確認すると通知が来ていました。

ダッシュボードへのリンクもきちんと動いています。

3. まとめ

以上、Cloudflare WorkersにTail Workerでエラー通知を実装する話でした。

今回はWorkerが正常終了しないときに通知をしましたが、実際にはログの中身を見て通知するのも追加した方がいいかと思います。

何かのお役に立てたら幸いです。

この記事をシェアする

関連記事