Add request trace IDs to Winston logs independent of OTEL
Problem
Currently, request trace IDs only appear in Winston log output when the OpenTelemetry SDK is active (i.e., OTEL_EXPORTER_OTLP_ENDPOINT is set or OTEL_FILE_EXPORT_ENABLED=true). The WinstonInstrumentation in src/tracing.ts injects trace_id and span_id into log entries via OTEL's log correlation, but for operators who don't run an OTEL collector, there's no way to correlate log lines to a specific HTTP request.
This makes debugging production issues significantly harder — when multiple concurrent requests are being served, it's difficult to trace which log lines belong to which request without grepping for data IDs or other indirect identifiers.
Context
Current tracing setup:
src/tracing.ts configures OTEL with WinstonInstrumentation for log correlation (trace_id/span_id injection)
- Route handlers in
src/routes/data/handlers.ts and src/routes/chunk/handlers.ts create OTEL spans per request
- Other route handlers (
graphql, arns, root, ar-io, datasets, x402, rate-limit) do not create spans
- When OTEL SDK is not started (no endpoint, no file export), the tracer is effectively no-op and no trace IDs appear in logs
- There is no Express middleware that assigns a request ID independent of OTEL
Requirements
Must Have
- Every HTTP request gets a unique request ID assigned at the Express middleware level, independent of OTEL configuration
- The request ID is included in all Winston log entries emitted during that request's lifecycle
- The request ID is returned in a response header (e.g.,
X-Request-Id) so clients can reference it in bug reports
- If an incoming request already carries an
X-Request-Id header, honor it (useful for tracing across reverse proxies / CDN layers)
Should Have
- The request ID should use a compact, fast-to-generate format (e.g.,
nanoid or crypto.randomUUID())
- Minimal performance overhead — this runs on every request
- The field name in logs should be
requestId (or similar) to distinguish from OTEL's trace_id
Nice to Have
- When OTEL is active, the OTEL
trace_id continues to be injected alongside the requestId (both are useful — trace_id for distributed tracing, requestId for local log grep)
Technical Notes
Approach
The typical pattern is an Express middleware early in the chain that:
- Reads
X-Request-Id from the incoming request headers (if present) or generates a new one
- Stores it on the request object (e.g.,
req.requestId)
- Sets the
X-Request-Id response header
- Uses a continuation-local storage mechanism (e.g.,
AsyncLocalStorage) to make the ID available to Winston without explicit passing
Winston supports custom formats and defaultMeta — a log format that reads from AsyncLocalStorage can inject the requestId automatically.
Files likely to modify
| File |
Change |
New: src/middleware/request-id.ts |
Express middleware to assign/propagate request IDs |
src/log.ts (or equivalent) |
Winston format/transport to inject requestId from AsyncLocalStorage |
src/app.ts or route registration |
Register the middleware early in the Express chain |
Considerations
AsyncLocalStorage is stable in Node.js 16+ and has negligible overhead for this use case
- This should be orthogonal to OTEL — both can coexist (OTEL injects
trace_id/span_id, this injects requestId)
- The middleware should be registered before any route handlers so all downstream logs pick up the ID
Add request trace IDs to Winston logs independent of OTEL
Problem
Currently, request trace IDs only appear in Winston log output when the OpenTelemetry SDK is active (i.e.,
OTEL_EXPORTER_OTLP_ENDPOINTis set orOTEL_FILE_EXPORT_ENABLED=true). TheWinstonInstrumentationinsrc/tracing.tsinjectstrace_idandspan_idinto log entries via OTEL's log correlation, but for operators who don't run an OTEL collector, there's no way to correlate log lines to a specific HTTP request.This makes debugging production issues significantly harder — when multiple concurrent requests are being served, it's difficult to trace which log lines belong to which request without grepping for data IDs or other indirect identifiers.
Context
Current tracing setup:
src/tracing.tsconfigures OTEL withWinstonInstrumentationfor log correlation (trace_id/span_idinjection)src/routes/data/handlers.tsandsrc/routes/chunk/handlers.tscreate OTEL spans per requestgraphql,arns,root,ar-io,datasets,x402,rate-limit) do not create spansRequirements
Must Have
X-Request-Id) so clients can reference it in bug reportsX-Request-Idheader, honor it (useful for tracing across reverse proxies / CDN layers)Should Have
nanoidorcrypto.randomUUID())requestId(or similar) to distinguish from OTEL'strace_idNice to Have
trace_idcontinues to be injected alongside therequestId(both are useful —trace_idfor distributed tracing,requestIdfor local log grep)Technical Notes
Approach
The typical pattern is an Express middleware early in the chain that:
X-Request-Idfrom the incoming request headers (if present) or generates a new onereq.requestId)X-Request-Idresponse headerAsyncLocalStorage) to make the ID available to Winston without explicit passingWinston supports custom formats and
defaultMeta— a log format that reads fromAsyncLocalStoragecan inject therequestIdautomatically.Files likely to modify
src/middleware/request-id.tssrc/log.ts(or equivalent)requestIdfromAsyncLocalStoragesrc/app.tsor route registrationConsiderations
AsyncLocalStorageis stable in Node.js 16+ and has negligible overhead for this use casetrace_id/span_id, this injectsrequestId)