Skip to content
Warlock.js v4

Attach request / trace context

When a request fans out across services and helpers, you want every line it produces to carry the same requestId (and maybe userId, traceId) so you can filter your logs down to one request. The logger’s fourth argument — the context object — is exactly this; the trick is threading the same context through every call without retyping it.

Every level method takes structured context as its fourth argument. File and JSON channels persist it alongside the entry:

await log.info("orders", "checkout", "Order placed", {
requestId: "req_9f2a",
userId: 101,
amount: 4999,
});

That works, but repeating { requestId, userId } on every call is tedious and easy to forget. Wrap it once.

Build a small object at the start of each request that closes over its context and exposes the same level methods:

src/request-logger.ts
import { log } from "@warlock.js/logger";
/**
* Bind a context object to every log call for the life of one request.
* Returns the same level methods, each pre-filled with `base` context
* (merged with any per-call context you pass).
*/
export function requestLogger(base: Record<string, unknown>) {
const withBase = (context?: Record<string, unknown>) => ({
...base,
...context,
});
return {
debug: (module: string, action: string, message: unknown, context?: Record<string, unknown>) =>
log.debug(module, action, message as string, withBase(context)),
info: (module: string, action: string, message: unknown, context?: Record<string, unknown>) =>
log.info(module, action, message as string, withBase(context)),
warn: (module: string, action: string, message: unknown, context?: Record<string, unknown>) =>
log.warn(module, action, message as string, withBase(context)),
error: (module: string, action: string, message: unknown, context?: Record<string, unknown>) =>
log.error(module, action, message as string, withBase(context)),
success: (module: string, action: string, message: unknown, context?: Record<string, unknown>) =>
log.success(module, action, message as string, withBase(context)),
};
}

Use it inside a request:

src/middleware/logging.ts
import { randomUUID } from "node:crypto";
import { requestLogger } from "../request-logger";
export function handleRequest(request, response) {
const requestLog = requestLogger({
requestId: request.headers["x-request-id"] ?? randomUUID(),
userId: request.user?.id,
method: request.method,
path: request.url,
});
requestLog.info("http", "request", "Incoming request");
// Pass `requestLog` down into your services. Every line they emit now
// carries requestId + userId automatically.
return runHandler(request, response, requestLog);
}

Now a single requestId filter in your log viewer surfaces the entire request.

log.timer pairs naturally here — it returns an end-function that logs the elapsed duration. Merge the request context into the final entry:

const end = log.timer("http", `${request.method} ${request.url}`);
try {
return await runHandler(request, response);
} finally {
end({ requestId, status: response.statusCode });
// → info "http" "GET /orders" "completed in 37ms" { durationMs: 37, requestId, status: 200 }
}

Once you’re funneling request data into context, some of it is sensitive (auth headers, tokens). Set a redaction floor so those paths are scrubbed before any channel sees them:

log.configure({
redact: {
paths: ["context.headers.authorization", "context.token", "context.*.password"],
},
});

See Redaction for the path syntax.

  • Your first log — the context argument and the timer helper
  • Redaction — keep secrets out of the context you log