/**
* リクエスト相関 ID の格納。
*
* `proxy.ts` は別バンドルになる可能性があり、公式ガイドは Proxy から
* 共有モジュールやグローバルへ依存することを禁じている。
* そのため ID の発行は Proxy、格納はルート側の AsyncLocalStorage に分け、
* pino の mixin() が同じストアを読んで全ログへ request_id を自動付与する。
*/
import { AsyncLocalStorage } from "node:async_hooks";
interface RequestLogStore {
requestId: string;
}
const requestLogStorage = new AsyncLocalStorage<RequestLogStore>();
/**
* 指定した request_id を後続の非同期処理に伝播させる。
* ルート先頭でラップし、その内側の logger 呼び出しが同一 ID を持てるようにする。
*/
export function runWithRequestId<T>(requestId: string, fn: () => T): T {
return requestLogStorage.run({ requestId }, fn);
}
/**
* 現在の request_id。ストア未設定時は undefined。
* mixin 側では欠落を pino がキーごと省略しないよう "-" に落とす。
*/
export function getRequestId(): string | undefined {
return requestLogStorage.getStore()?.requestId;
}
/** 相関IDヘッダ名(docs/logging-design.md §10 で確定済み) */
export const REQUEST_ID_HEADER = "x-request-id";
/**
* ヘッダ由来の値をそのまま使わず形を検査する。
*
* `proxy.ts`(ステップ7)が入るまではクライアントが直接送れる位置にあり、
* 長大な値や制御文字を入れられるとログが読めなくなる。
* 形が合わない値は捨てて生成し直す。
*/
const REQUEST_ID_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
/**
* ヘッダ値として妥当な相関IDだけを返す。妥当でなければ undefined。
*
* `instrumentation.ts` の `onRequestError()` はルートの AsyncLocalStorage の
* 外側で走るため、ヘッダから同じ規則で ID を取り直す必要がある。
* 検査規則を1箇所に置くためこの関数を共有する。
*/
export function normalizeRequestId(
value: string | string[] | undefined | null
): string | undefined {
const raw = Array.isArray(value) ? value[0] : value;
if (!raw || !REQUEST_ID_PATTERN.test(raw)) return undefined;
return raw;
}
/**
* リクエストから request_id を決めてハンドラを包む。
*
* `x-request-id` があればそれを使い、無ければ生成する。
* 設計(§1)では `proxy.ts` が発行する前提だが、ステップ7 が保留中でも
* 相関IDが欠けないようにこの形にしてある。`proxy.ts` が入ると
* ヘッダが必ず存在するようになり、この関数はそのまま動く(手戻りしない)。
*
* ★全APIルートに同じラップ処理を書き写さないための共通ヘルパー★
*/
export function withRequestId<T>(request: Request, fn: () => T): T {
const fromHeader = normalizeRequestId(request.headers.get(REQUEST_ID_HEADER));
return runWithRequestId(fromHeader ?? crypto.randomUUID(), fn);
}