Newer
Older
client_dwatch / lib / logger / context.ts
/**
 * リクエスト相関 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);
}