
Cloudflare WorkersのspanContext()でtraceIdをレスポンスに入れる:setStatus・updateNameとあわせて挙動を確かめる
どうも!オペ部の西村祐二です!
Cloudflare Workersで500エラーを返したとき、「このリクエストのトレースはどれか」をすぐに引けると調査が楽になります。2026年10月26日のCloudflare Changelogで、Workersのカスタムspanに spanContext()、setStatus()、updateName() の3つのメソッドが追加されました。spanContext() を使うと、Workerのコードから traceId と spanId を読めます。
この記事では、3つのメソッドを、Wranglerの wrangler dev によるローカル環境と、Workers FreeプランのアカウントにデプロイしたWorkerの両方で動かした結果をまとめます。あわせて、本番のWorkers Observabilityのダッシュボードと、ローカルのLocal Explorerで、何が見えて何が見えないかも確かめました。
今回の変更点
- 日付: 2026年10月26日
- 対象: カスタムspanのAPI(
import { tracing } from "cloudflare:workers"またはctx.tracing)が返すSpan - 追加されたメソッド:
| メソッド | できること | 主な使いどころ |
|---|---|---|
span.spanContext() |
traceId(16進32桁)、spanId(16進16桁)、traceFlags を返す |
ログやエラーレスポンスにtraceIdを入れ、トレースと突き合わせる |
span.setStatus(status) |
spanのstatusを "unset" / "ok" / "error" にする。"error" のときはmessageも付けられる |
例外を投げずに失敗を返した処理を、トレース上で失敗として扱う |
span.updateName(name) |
開始後のspanの名前を変える。ルートspanも変えられる | ルーティングが決まってから GET /users/:id のような名前にする |
ChangelogとCustom spansのドキュメントによると、statusはOpenTelemetryの送信先へexportするトレースに含まれます。ただし、Workers Observabilityのダッシュボードにはまだ表示されず、statusで絞り込むこともできません。
試してみる
環境
- macOS 26.6.1、Node.js v24.22.1
- wrangler 4.153.0(workerd 1.20261023.1)。ローカルとデプロイの両方で同じバージョンを使いました
compatibility_dateは2026-10-27
カスタムspanを使うには、トレースを有効にしておく必要があります。
{
"name": "span-context-demo",
"main": "src/index.js",
"compatibility_date": "2026-10-27",
"observability": { "traces": { "enabled": true } }
}
検証に使ったWorker
Changelogのサンプルをもとに、ルートspanの名前をルーティングで決まったパターン名に変え、失敗時はtraceIdをレスポンスに入れるWorkerを用意しました。/inspect と /status-only は確認用のエンドポイントです。/inspect は各メソッドの戻り値をJSONで返し、/status-only はダッシュボードとLocal Explorerでの見え方を確かめるためのspanを作ります。
import { tracing } from "cloudflare:workers";
const routes = [
{ pattern: "/users/:id", re: /^\/users\/[^/]+$/ },
{ pattern: "/fail", re: /^\/fail$/ },
];
export default {
async fetch(request) {
const url = new URL(request.url);
const root = tracing.getActiveSpan();
const route = routes.find((r) => r.re.test(url.pathname));
// ルーティングが決まってからルートspanの名前を変える
root?.updateName(`${request.method} ${route?.pattern ?? "unknown"}`);
if (url.pathname === "/status-only") {
// setStatusだけを呼び、例外は記録しないspan
tracing.enterSpan("status-error-no-exception", (span) => {
span.setStatus({ code: "error", message: "set by setStatus only" });
});
// コールバックの中で名前を変えるspan
tracing.enterSpan("rename-me", (span) => {
span.updateName("renamed-inside-callback");
});
return new Response("ok");
}
if (url.pathname === "/inspect") {
const out = {};
out.root = root?.spanContext();
const s = tracing.startSpan("probe");
out.probe = s.spanContext();
try {
s.setStatus({ code: "OK" });
out.upperOK = "no error";
} catch (e) {
out.upperOK = `${e.name}: ${e.message}`;
}
try {
s.setStatus({ code: 1 });
out.numericOK = "no error";
} catch (e) {
out.numericOK = `${e.name}: ${e.message}`;
}
s.setStatus({ code: "ok" });
s.setStatus({ code: "error", message: "after ok" });
s.updateName("probe renamed");
s.end();
s.updateName("ignored after end");
out.probeAfterEnd = s.spanContext();
out.isTracedAfterEnd = s.isTraced;
return Response.json(out);
}
return tracing.startActiveSpan("handle", async (span) => {
try {
if (route?.pattern === "/fail") throw new Error("boom");
span.setStatus({ code: "ok" });
return Response.json({ ok: true, traceId: root?.spanContext().traceId });
} catch (err) {
span.recordException(err);
span.setStatus({ code: "error", message: "Route handler failed" });
return Response.json(
{ error: "Internal error", traceId: root?.spanContext().traceId },
{ status: 500 },
);
} finally {
span.end();
}
});
},
};
1. spanContext()でtraceIdを取り出す
まず npx wrangler deploy でデプロイし、/inspect、/users/42、/fail、/status-only にリクエストを送りました。<subdomain> には自分のworkers.devサブドメインが入ります。
npx wrangler deploy
curl -s https://span-context-demo.<subdomain>.workers.dev/inspect
curl -s https://span-context-demo.<subdomain>.workers.dev/users/42
curl -s -w '\nstatus=%{http_code}\n' https://span-context-demo.<subdomain>.workers.dev/fail
curl -s https://span-context-demo.<subdomain>.workers.dev/status-only
デプロイしたWorkerの /inspect のレスポンス(見やすく整形しています)は次の通りです。
{
"root": { "traceId": "6e0da3b4d885342c176779762cd8ad62", "spanId": "5d7c853b01467569", "traceFlags": 0 },
"probe": { "traceId": "6e0da3b4d885342c176779762cd8ad62", "spanId": "9563fb68e394c262", "traceFlags": 0 },
"upperOK": "TypeError: Span status code must be 'unset', 'ok', or 'error'.",
"numericOK": "TypeError: Span status code must be 'unset', 'ok', or 'error'.",
"probeAfterEnd": { "traceId": "6e0da3b4d885342c176779762cd8ad62", "spanId": "9563fb68e394c262", "traceFlags": 0 },
"isTracedAfterEnd": false
}
/fail はステータスコード500で、本文にtraceIdが入りました。
{"error":"Internal error","traceId":"bd70802ea981dc3218b669ff4cf6599d"}
- ルートspanと、そこから作ったspanは同じtraceIdを返し、spanIdは別の値でした
span.end()の後もspanContext()は同じIDを返しました。一方で、spanが記録の対象かを表すisTracedはfalseになりましたtraceFlagsはデプロイ先でもローカル(2で後述)でも0でした。Custom spansのドキュメントによると、現時点のtraceFlagsはサンプリングの有無を表しません。そのため、下流サービスへ渡すtraceparentヘッダーのflagsに、この値をそのまま使うことはできません
upperOK と numericOK は、3で説明します。
2. 返したIDがトレースと一致するかを確かめる
デプロイしたWorkerの「Observability」タブを開き、表示を「Traces」に切り替えました。以下の画像では、メールアドレス、アカウントID、workers.devのサブドメインを伏せています。
/fail のトレースを開くと、画面上部のトレースIDが、500レスポンスに入れた bd70802ea981dc3218b669ff4cf6599d と一致しました。

/inspect のトレースも、トレースIDがレスポンスの 6e0da3b4d885342c176779762cd8ad62 と同じでした。子spanの詳細を開くと、スパンIDは probe の spanId(9563fb68e394c262)、親スパンIDはルートspanの spanId(5d7c853b01467569)と一致しました。

ローカルの wrangler dev でも、同じようにIDを照合しました。wrangler dev を起動するとLocal Explorerが使えます。今回は既定の8787番ではなく8799番で起動しました。
npx wrangler dev --port 8799
curl -s http://localhost:8799/inspect
ローカルの /inspect は、上のデプロイ先とは別のIDを返しました。
{
"root": { "traceId": "80b4bb1f3a5dbea3e0e51fe7e441cbcb", "spanId": "0656b932b4d27cbe", "traceFlags": 0 },
"probe": { "traceId": "80b4bb1f3a5dbea3e0e51fe7e441cbcb", "spanId": "2363150df2c77110", "traceFlags": 0 }
}
このtraceIdでLocal ExplorerのAPIからspanを取得しました。
curl -s -X POST http://localhost:8799/cdn-cgi/local/explorer/api/local/observability/query \
-H 'Content-Type: application/json' \
-d '{"sql":"SELECT trace_id, span_id, parent_id, name FROM spans WHERE trace_id = '\''80b4bb1f3a5dbea3e0e51fe7e441cbcb'\''"}'
["80b4bb1f3a5dbea3e0e51fe7e441cbcb","0656b932b4d27cbe",null,"GET"]
["80b4bb1f3a5dbea3e0e51fe7e441cbcb","2363150df2c77110","0656b932b4d27cbe","probe"]
レスポンスのspanIdは、Local Explorerに記録された span_id と同じ値でした。probe の parent_id も、ルートspanのspanIdと同じ値です。
ローカルで /fail を送ったときも、500レスポンスに入っていたtraceIdを使って、Local Explorerの画面から該当のトレースを開けました。

3. setStatus()の指定値とトレースでの見え方
code は小文字の文字列で指定します。大文字の setStatus({ code: "OK" }) も、数値の setStatus({ code: 1 }) も、デプロイ先とローカルの両方で TypeError になりました(1の /inspect のレスポンスの upperOK と numericOK)。OpenTelemetry JSの SpanStatusCode.OK は数値の 1 なので、既存のOpenTelemetryのコードはそのままでは動きません。
statusの見え方は、/status-only で確かめました。status-error-no-exception spanでは、setStatus({ code: "error" }) だけを呼んでいます。本番のダッシュボードでは、このspanはエラー表示にならず、詳細でも「完了」と表示されました。属性の一覧にもstatusを表す項目はありませんでした。


一方、recordException() も呼んだ /fail の handle spanは、詳細で「エラー」と表示され、例外のメッセージ boom が出ました。トレース一覧のErrors列にも boom と表示されていました。

ローカルのLocal Explorerでも同じ傾向でした。
| spanで呼んだメソッド | ダッシュボードの表示 | Local Explorerのoutcome | Local Explorerのerror列 |
|---|---|---|---|
setStatus({ code: "error" }) のみ |
完了 | ok |
なし |
recordException(err) と setStatus({ code: "error" }) |
エラー(boom) |
error |
Error: boom |
2の最後に載せたLocal Explorerの画像ではルートspanも赤く表示されていますが、APIで取得したルートspanのoutcomeは ok でした。画面がどの条件でルートspanを赤くするかは確かめていません。本番のダッシュボードでは、ルートspanはエラー表示になりませんでした。
2026年10月28日時点では、ダッシュボード(トレースの一覧とspanの詳細の属性)にも、Local Explorer(画面とAPIで取得したspan)にも、statusを表す項目は見当たりませんでした。ダッシュボードについては、Changelogに「まだ表示されない」とあるため、今後表示されるようになる可能性があります。Local Explorerについては、statusに対応していないのか、今回の確認方法では見えていないだけなのかは切り分けていません。確かめられたのは「例外にならずに呼べること」までです。statusを確認するには、OpenTelemetryへのexportを設定し、送信先で見る必要があります。
Custom spansのドキュメントには、次の挙動も書かれています。
"ok"を設定した後のsetStatusは無視される"unset"を設定しても、先に設定した"error"は消えないmessageはcodeが"error"のときだけ残る- statusを設定してもspanは終わらず、例外も記録されない
今回は値を観測する手段がなかったため、"ok" の後に "error" を設定しても例外にならないことだけを確かめました。
4. updateName()の反映範囲
updateName() は、本番のダッシュボードとローカルのLocal Explorerで結果が分かれました。
本番のダッシュボードでは、変えた名前が反映されました。
- ルートspan: Traces一覧のOperation列が
GET /users/:id、GET /fail、GET unknownになりました。画面では名前の後ろにリクエストのURLが付いて表示されます startSpan("probe")の後にupdateName("probe renamed")を呼んだspanは、probe renamedと表示されました。end()の後に呼んだupdateName("ignored after end")は、ドキュメントの記述どおり反映されていません(上の/inspectの画像)enterSpan("rename-me", ...)のコールバック内で変えた名前も、renamed-inside-callbackと表示されました(上の/status-onlyの画像)

一方、wrangler 4.153.0のLocal Explorerでは、画面とAPIの name 列を見た範囲では、updateName() で変えた名前は記録されていませんでした。
- ルートspanの名前を
GET /failに変えた後も、記録された名前はGETのままでした。画面ではURLが付いてGET http://localhost:8799/failと表示されます startSpan("probe")の後にupdateName("probe renamed")を呼んでも、記録された名前はprobeでした/status-onlyのenterSpan("rename-me", ...)のコールバック内で名前を変えても、rename-meと記録されました
同じコードで本番のダッシュボードには反映されたので、ローカルで名前が変わらなくても、コードの誤りとは限りません。Local Explorerが updateName() に対応していないのか、今回の確認方法に足りないところがあるのかは切り分けていません。今後のwranglerのアップデートで変わる可能性もあります。ドキュメントでは、変更後の名前はexportされたトレースにも反映され、64バイトで切り詰められると説明されています。
検証結果の考察
表は、検証で確かめた範囲(確認済み)と、ドキュメントの記述によるものを分けています。
| メソッド | Workerのレスポンス | Local Explorer | ダッシュボード | OpenTelemetry export |
|---|---|---|---|---|
spanContext() |
確認済み(デプロイ先・ローカル) | IDの一致を確認済み | IDの一致を確認済み | 未確認 |
setStatus() |
不正な code の TypeError を確認済み |
見当たらなかった(4.153.0) | 表示されなかった(10月28日時点) | 含まれる(Changelog・ドキュメント) |
updateName() |
- | 反映されなかった(4.153.0) | 反映された(確認済み) | 反映される(ドキュメント) |
「見当たらなかった」「表示されなかった」「反映されなかった」は、2026年10月28日時点で、今回の方法で確認した結果です。今後のアップデートで変わる可能性があります。
- ドキュメントによると、
spanContext()は、トレースが実際に保存されたかどうかに関係なくIDを返します- トレースが無効な場合や、サンプリングで記録の対象から外れたリクエストでも、Workers Logsが有効なら0以外のIDが返ります。このIDはログに付くtraceIdと同じですが、spanは保存されません。そのため、レスポンスに入れたtraceIdでダッシュボードのTracesを探しても、トレースは見つかりません。この場合は、同じtraceIdでWorkers Logsを探すことになります
- observability自体が無効な場合は、すべて0のIDが返ります
- 今回はトレースを有効にし、サンプリングの設定も既定のまま、少数のリクエストしか送っていません。この2つのケースは実機では確かめておらず、ドキュメントの記述をもとに書いています
- 既存のOpenTelemetryのコードから
SpanStatusCodeの数値を渡している箇所は、文字列の"ok"/"error"に書き換えます - ダッシュボードでエラーとして表示させるには、
recordException()も呼びます。今回確かめたのはsetStatus({ code: "error" })と併用した場合だけです
まとめ
Cloudflare Workersのカスタムspanに追加された spanContext() を使うと、traceIdをレスポンスやログに入れて、リクエストとトレースを結び付けられます。返したIDは、本番のWorkers Observabilityダッシュボードでも、ローカルのLocal Explorerでも、トレースのIDと一致しました。
updateName() で変えた名前は、本番のダッシュボードには反映されましたが、wrangler 4.153.0のLocal Explorerには反映されませんでした。setStatus() で設定したstatusは、2026年10月28日時点ではどちらの画面にも見当たりませんでした。statusを確認するなら、OpenTelemetryの送信先で見ることになります。ダッシュボードでエラーとして見るには、recordException() も呼びます。
誰かの参考になれば幸いです。
参考リンク:







