0f923303d3
Task O.1: AsyncLocalStorage trace context (currentTraceId/runWithTrace/toTraceparent) read by singleton services with no DI churn; Express traceMiddleware derives a trace id per request (x-trace-id → traceparent → generated), echoes x-trace-id on the response, runs the request in-context, and logs a structured http.request line on finish. Minimal JSON structured logger stamps traceId on every line. Wired via app.use in main.ts. 4 tests: scope, concurrency isolation, traceparent shape/parse. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
44 lines
1.6 KiB
TypeScript
44 lines
1.6 KiB
TypeScript
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
|
|
/**
|
|
* Request-scoped trace context (P9 observability). A module-level AsyncLocalStorage
|
|
* so singleton services can read the current request's trace id via `currentTraceId()`
|
|
* WITHOUT threading it through every method or constructor. The middleware runs each
|
|
* request inside `runWithTrace(...)`; async projectors run outside a request and
|
|
* simply see `undefined` (their events already carry the trace from the emitting call).
|
|
*/
|
|
export interface TraceStore {
|
|
traceId: string;
|
|
correlationId?: string;
|
|
}
|
|
|
|
const als = new AsyncLocalStorage<TraceStore>();
|
|
|
|
export function runWithTrace<T>(traceId: string, fn: () => T): T {
|
|
return als.run({ traceId }, fn);
|
|
}
|
|
|
|
export function currentTraceId(): string | undefined {
|
|
return als.getStore()?.traceId;
|
|
}
|
|
|
|
/** A fresh trace id (used when no inbound trace context is present). */
|
|
export function newTraceId(): string {
|
|
return randomUUID();
|
|
}
|
|
|
|
/** W3C traceparent for a trace id: 00-<32 hex>-<16 hex span>-01. */
|
|
export function toTraceparent(traceId: string): string {
|
|
const trace = traceId.replace(/-/g, '').padEnd(32, '0').slice(0, 32);
|
|
const span = randomBytes(8).toString('hex');
|
|
return `00-${trace}-${span}-01`;
|
|
}
|
|
|
|
/** Extract a trace id from an inbound traceparent header (the 2nd field), else undefined. */
|
|
export function traceIdFromTraceparent(traceparent?: string): string | undefined {
|
|
if (!traceparent) return undefined;
|
|
const parts = traceparent.split('-');
|
|
return parts.length >= 3 && /^[0-9a-f]{32}$/i.test(parts[1]) ? parts[1] : undefined;
|
|
}
|