第28章:ログ設計② requestIdで一本道にする🧵🚶♀️✨
「ログ見ても、どの行が同じリクエストの話なのか分からない〜😭」ってなるやつ、requestIdで一発解決しにいこっ💪😊 この章は「1回のリクエスト(あるいは1つのユーザー操作)を、ログ上で一本の線にする」のがテーマだよ〜🧵✨
1) requestIdってなに?なんで“一本道”になるの?🛣️🔎
✅ requestIdの正体
- 1つのリクエストに付けるユニークな番号だよ🆔✨
- ログ1行1行に 同じrequestId を付けると、あとで検索したときに「この1件の流れ」がズラーッと出る📚🔍
✅ 具体的に嬉しいこと😍
- 例:ログが100万行あっても、
requestId=abc123で絞ればその件だけ見れる✨ - 障害調査が「当てずっぽう」から「一本道の追跡」になる🧵🚶♀️
![追跡の糸:迷宮の中で一本の道しるべを辿る[(./picture/err_model_ts_study_028_trace_thread.png)
2) requestId / correlationId / traceId の違い(超やさしく)🧠🌸
ここ、ちょい用語が似てて混乱しがちなので、最短で整理するね😊
🆔 requestId(今回の主役)
- 1回のHTTPリクエストに付けるIDのことが多いよ
- たとえば
GET /api/orders1回に1つ
🧵 correlationId(“取引全体”のID)
- 「ユーザーの1操作」が裏で複数のリクエスト/ジョブを生むとき、全部を束ねる親IDとして使うことが多いよ
- ただ、最初は requestId = correlationId として運用しても全然OK🙆♀️✨(成長したら分ければいい)
🧭 traceId(分散トレーシングのID)
- 複数サービスをまたいで追跡する標準の世界では、W3C Trace Contextの
traceparentで trace-id を運ぶのが王道だよ📦✨ (W3C) - Google Cloudなども
traceparent/tracestateによる伝播を説明してるよ🌐 (Google Cloud Documentation)
まとめると: まずはrequestIdで一本道 → 慣れたら traceId(traceparent) もログに出す、が気持ちいい流れ😊✨ (OpenTelemetry)
3) requestId設計の基本ルール(ここだけ守れば強い)🧱✨
ルールA:入口で作る🚪
-
**最初に受け取る場所(APIの入口)**で作るのが基本!
-
もしクライアントやゲートウェイが
X-Request-IDを付けてくれてたら、それを採用してもOK(ただし検証はしてね)- 例:Herokuは
X-Request-IDを付けてアプリに渡す仕組みを説明してるよ🧾 (Heroku Dev Center) X-Request-IDは非公式だけど「追跡用によく使う」ヘッダとして紹介されてるよ🔍 (HTTP.DEV)
- 例:Herokuは
ルールB:レスポンスにも返す📤
-
X-Request-IDをレスポンスヘッダに返すと、- ユーザーから「このエラー出た😭」って来たときに requestId を聞ける📞✨
-
pino-http の例でも
X-Request-Idをレスポンスにセットしてるよ🌲 (GitHub)
ルールC:下流にも渡す(伝播)📨
- 自分のサーバ → 外部API → DB → キュー…みたいに流れるなら 次の呼び出しにも requestId を付けて渡すと追跡が途切れない🧵✨
ルールD:ログは“構造化”して常に同じキー名で🧾✨
requestIdというキーで統一(reqIdと混ぜない)- 検索しやすさが命🔥
4) TypeScriptで実装:最小で強いパターン🧪💪✨
AsyncLocalStorageで「どこでもrequestIdが取れる」ようにする🧵
Nodeの AsyncLocalStorage を使うと、関数引数で requestId を渡し回さなくても 「今の処理のrequestId」を取り出せるようになるよ✨ Node公式も、リクエストごとにIDを付ける例を出してて、AsyncLocalStorageを推奨してるよ😊 (Node.js)
4-1) requestContext(保存場所)を作る🧺🧵
// src/requestContext.ts
import { AsyncLocalStorage } from "node:async_hooks";
export type RequestContext = {
requestId: string;
};
const als = new AsyncLocalStorage<RequestContext>();
export function runWithRequestContext(ctx: RequestContext, fn: () => void) {
als.run(ctx, fn);
}
export function getRequestContext(): RequestContext | undefined {
return als.getStore();
}
export function getRequestId(): string | undefined {
return als.getStore()?.requestId;
}
4-2) どこでも使える logger(自作の超軽量版)🪶🧾
// src/logger.ts
import { getRequestId } from "./requestContext";
type Level = "debug" | "info" | "warn" | "error";
export function log(level: Level, message: string, extra: Record<string, unknown> = {}) {
const requestId = getRequestId();
// JSONログ(構造化ログ)
const line = {
ts: new Date().toISOString(),
level,
message,
requestId: requestId ?? null,
...extra,
};
// ここは本番なら pino 等に差し替えやすい形✨
// とりあえずconsoleでもOK!
console.log(JSON.stringify(line));
}
4-3) Expressで「入口で発行→ALSに入れる→レスポンスに返す」🚪📤✨
// src/server.ts
import express from "express";
import { randomUUID } from "node:crypto";
import { runWithRequestContext } from "./requestContext";
import { log } from "./logger";
const app = express();
app.use((req, res, next) => {
// 既存のX-Request-IDがあれば採用(なければ生成)
const incoming = req.header("x-request-id");
const requestId = (typeof incoming === "string" && incoming.length > 0) ? incoming : randomUUID();
// レスポンスにも返す
res.setHeader("X-Request-ID", requestId);
// ここがポイント:このリクエストの“文脈”として保存🧵
runWithRequestContext({ requestId }, () => next());
});
app.get("/api/hello", async (_req, res) => {
log("info", "handler start");
// 何か処理した気分
await new Promise(r => setTimeout(r, 50));
log("info", "handler end");
res.json({ ok: true });
});
app.listen(3000, () => {
console.log("http://localhost:3000");
});
これで /api/hello を叩くと、ログがこうなるイメージだよ👇😍
{"ts":"...","level":"info","message":"handler start","requestId":"b3c..."}
{"ts":"...","level":"info","message":"handler end","requestId":"b3c..."}
5) フレームワークの“標準機能”も使えるよ(Fastify / pino)🌲⚡
Fastifyは「デフォでrequest id」を持ってる🆔✨
Fastifyは リクエストごとにIDを付けるのが標準で、request-id ヘッダがあればそれを使う、なければ生成…みたいな挙動が説明されてるよ😊 (Fastify)
pino-httpは genReqId で x-request-id を拾って返せる🌲
pino-http のREADME例でも x-request-id を拾って、なければUUIDを作って X-Request-Id をレスポンスに返してるよ✨ (GitHub)
「自作で理解」→「pino/fastifyで実戦投入」って流れがめっちゃ学びやすい😊💗
6) 外部APIを呼ぶとき:requestIdを“渡して続きの線”を作る📨🧵
fetchラッパーを作る(超おすすめ)🪄✨
// src/fetchWithRequestId.ts
import { getRequestId } from "./requestContext";
export async function fetchWithRequestId(input: RequestInfo | URL, init: RequestInit = {}) {
const requestId = getRequestId();
const headers = new Headers(init.headers);
if (requestId) headers.set("X-Request-ID", requestId);
return fetch(input, { ...init, headers });
}
これで「外部API側のログ」も X-Request-ID で繋がる可能性が上がるよ🧵✨
(相手が対応してれば最強💪)
7) “標準の世界”に寄せたい人へ:traceparent(W3C Trace Context)🧭✨
サービスが増えてくると、requestIdだけじゃなくて 分散トレースも欲しくなるよね😊
そのときの標準が W3C Trace Contextで、traceparent / tracestate が定義されてるよ🌐 (W3C)
OpenTelemetryのJSドキュメントでも、Nodeでは AsyncLocalStorage や async_hooks でコンテキスト伝播する話が出てるよ🧵 (OpenTelemetry)
(つまり:ログに traceId を出すと、ログ↔トレースの相互参照がしやすくなる😍)
⚠️ちょい最新メモ(2026)
OpenTelemetry JSは Nodeのセキュリティ話題(async_hooks周り)について声明を出してるよ。計測系を入れてる場合は、追従アップデート意識すると安心だよ〜🛡️ (OpenTelemetry)
8) requestIdログ設計のチェックリスト✅🧾✨
✅ ログに入れる最低限(この章のコア)
requestIdmessagelevelts(ISO文字列)event(任意:"http.in","db.query","http.out"みたいに分類できると神✨)
✅ エラー時はこう入れる(Errorモデリングと接続)💥🧠
requestIdは 絶対入れるerror.name,error.message(stackは環境に応じて)- もしアプリ標準エラー(domain/infra/bug)に正規化してるなら、
error.type/error.codeも入れると最強💪✨
9) ミニ演習📝:ログを“一本道”にしてみよう🧵🚶♀️
お題🎀
「プロフィール更新API」を想像してね😊 流れはこんな感じ:
PATCH /api/profileを受ける(入口)🚪- DB更新する(成功/失敗あり)🗃️
- 外部の通知APIを叩く(失敗することがある)📨
- どこで失敗しても、同じrequestIdで追えるようにログを設計する🧵✨
やること✅
- (1) 入口で
X-Request-IDを発行してレスポンスに返す - (2) DB処理ログに
requestIdを付ける - (3) 外部API呼び出しに
X-Request-IDを付ける - (4) 失敗時ログにも
requestIdを付ける(これが一番大事!)
ゴール🌈
障害が起きても requestId で検索したら、
「入口→DB→外部API→失敗地点」まで 1本の線で出ること🧵😍
10) AI活用🤖✨(Copilot / Codex想定)
そのままコピって使えるプロンプト集だよ〜💗
🤖 ① ミドルウェア生成
- 「Expressで
X-Request-IDを受け取り、なければUUIDを生成してレスポンスにも返すミドルウェアをTypeScriptで書いて。AsyncLocalStorageで requestId を保持して。」
🤖 ② ログ項目の監査👮♀️
- 「このログ設計で“追跡できない穴”がないかレビューして。特に非同期や外部API呼び出しで requestId が途切れないか確認して。」
🤖 ③ 事故シナリオ作り😱➡️🧵
- 「外部通知APIがタイムアウトするケースで、調査担当が requestId で追えるログの例を10行くらい作って。」
🤖 ④ 命名統一✨
- 「requestId / correlationId / traceId の命名をチームで混乱しないように、命名規約案を3つ出してメリデメ比較して。」
今日のまとめ🎀✨
- requestIdは「ログを一本道にする」ための最強アイテム🧵😍
- 入口で発行 → レスポンスに返す → 下流に伝播 → 全ログに付ける が黄金ルール✨
- Nodeなら AsyncLocalStorage で requestId を迷子にしない設計が作れるよ🧠🧵 (Node.js)
- 将来は
traceparent(W3C Trace Context)で traceId まで出すと、分散でも追いやすくなるよ🌐 (W3C)
次の章(第29章)は、タイムアウト/キャンセル/リトライで「落ちる現実」に強くなるやつだよ⏳🛑🔁✨