feat(p9): request-scoped trace context + middleware + structured logger
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>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user