Challenge 3: Add Request Correlation — Possible Solution ==================================================================== interface LogFields { [key: string]: string | number | boolean | undefined; } interface Logger { debug(event: string, fields?: LogFields): void; info(event: string, fields?: LogFields): void; warn(event: string, fields?: LogFields): void; error(event: string, fields?: LogFields): void; } interface RequestContext { requestId: string; userId?: string; } function logWithContext( logger: Logger, ctx: RequestContext, level: "debug" | "info" | "warn" | "error", event: string, fields: LogFields = {} ): void { const enriched: LogFields = { requestId: ctx.requestId, userId: ctx.userId, ...fields, }; logger[level](event, enriched); } // --- Usage across a simulated request lifecycle --- class ConsoleLogger implements Logger { debug(event: string, fields?: LogFields) { console.log(JSON.stringify({ level: "debug", event, ...fields })); } info(event: string, fields?: LogFields) { console.log(JSON.stringify({ level: "info", event, ...fields })); } warn(event: string, fields?: LogFields) { console.log(JSON.stringify({ level: "warn", event, ...fields })); } error(event: string, fields?: LogFields) { console.log(JSON.stringify({ level: "error", event, ...fields })); } } const logger = new ConsoleLogger(); const ctx: RequestContext = { requestId: "req-8f3a", userId: "u42" }; logWithContext(logger, ctx, "info", "request.received", { path: "/orders" }); // {"level":"info","event":"request.received","requestId":"req-8f3a","userId":"u42","path":"/orders"} logWithContext(logger, ctx, "info", "order.placed", { orderId: "o1", total: 49.99 }); // {"level":"info","event":"order.placed","requestId":"req-8f3a","userId":"u42","orderId":"o1","total":49.99} logWithContext(logger, ctx, "info", "request.completed", { statusCode: 200, durationMs: 42 }); // {"level":"info","event":"request.completed","requestId":"req-8f3a","userId":"u42","statusCode":200,"durationMs":42} // A log aggregator query for requestId:"req-8f3a" now returns all three // lines above, in order, even if they were logged from different modules // or (in a real distributed system) different services. WHY THIS WORKS -------------- - ctx fields are spread in BEFORE the caller's own fields, so a caller can still override requestId/userId in an edge case, but in normal use every event automatically carries the same requestId without each call site having to remember to add it. - Accepting `level` as a parameter and indexing into `logger[level](...)` avoids writing four nearly-identical wrapper functions (logInfoWithContext, logErrorWithContext, ...) for each log level. - Because RequestContext is created once per incoming request (typically in middleware) and threaded through every downstream call, every log line produced while handling that request shares the same requestId — the simplest form of tracing, with no external library required.