@vizydrop/correlation-id
v7.0.1
Published
Vizydrop correlation id helper based on async hooks
Readme
Correlation Id
Correlation ID propagation across async contexts using AsyncLocalStorage. Provides middleware for Express/Koa and enhancers for HTTP clients (got, request, http-proxy) to automatically attach correlation IDs and baggage headers to all outbound requests.
Written in TypeScript (versions > 6.0).
Installation
yarn add @vizydrop/correlation-idHow to use
Create correlation id instance
const {createCorrelationId} = require('@vizydrop/correlation-id');
const correlationId = createCorrelationId();Integrate with @vizydrop/logger
const {createLogger} = require(`@vizydrop/logger`);
const logger = createLogger({
correlationId: {
enabled: true,
getCorrelationId: () => correlationId.correlator.getId(),
emptyValue: 'nocorrelation',
},
baggage: {
enabled: true,
getBaggage: () => correlationId.correlator.getParsedBaggage(),
},
});Register middleware
Support for koa and express:
// express
app.use(correlationId.expressMiddleware);
// koa
app.use(correlationId.koaMiddleware);Enhance got
Each request will automatically contain the correlation ID and baggage headers:
import got from 'got';
const correlatedGot = correlationId.enhanceGot(got);
correlatedGot.get('http://anotherservice:10020/data');Enhance request
const request = require('request');
const correlatedRequest = correlationId.enhanceRequest(request);
correlatedRequest.get('http://anotherservice:10020/data');Enhance axios
axios has no way to derive a new instance that inherits interceptors, so the instance is
mutated in place and returned. Prefer a dedicated instance over the shared default export:
import axios from 'axios';
const http = correlationId.enhanceAxios(axios.create());
http.get('http://anotherservice:10020/data');In NestJS, enhance the axios instance behind HttpService:
@Injectable()
export class HttpCorrelation implements OnModuleInit {
constructor(private readonly http: HttpService) {}
onModuleInit(): void {
correlationId.enhanceAxios(this.http.axiosRef);
}
}Enhance http-proxy
Each proxied request will contain the correlation ID and baggage headers:
import httpProxy from 'http-proxy';
const proxy = httpProxy.createProxyServer({target: 'http://anotherservice:10020/'});
correlationId.enhanceHttpProxy(proxy);Run background jobs
Jobs usually do not go through express/koa middleware so correlation ID should be generated manually:
function jobTask() { /* ... */ }
function runJob() {
correlationId.correlator.withId(correlationId.correlator.generateId(), () => {
jobTask();
});
}Framework compatibility
The package declares no framework dependencies. Request, response, proxy and HTTP client types are described structurally, so the same build works with:
| Library | Verified versions |
|---------|-------------------|
| express | 4.x and 5.x |
| koa | 2.x and 3.x |
| got | 11.x (CommonJS) through 16.x (ESM) |
| axios | 1.x (including @nestjs/axios HttpService.axiosRef) |
| request | 2.48.x |
| http-proxy | 1.18.x |
| @nestjs/platform-express | 11.x (both app.use() and a NestMiddleware wrapper) |
The only runtime dependencies are @opentelemetry/api and @opentelemetry/core.
Correlation IDs are generated with Node's built-in crypto.randomUUID(), so no uuid
peer dependency is required.
How the table is verified
Two majors of one package cannot be installed under a single name, so the older ones come
from aliased devDependencies (express4, types-express4, types-koa2, got11):
| Check | What it covers |
|-------|----------------|
| yarn test:types | compiles test/types/current-majors.test-d.ts against the current majors — express 5, koa 3, got 16, axios 1, http-proxy 1.18 |
| yarn test:compat | compiles the same src against the previous majors — @types/express 4, @types/koa 2, got 11 — through test/compat/legacy-majors.ts. This is also the only check that resolves the package the way a CommonJS consumer does |
| yarn test | runtime integration against real servers and clients in test/integration/: express 4 and 5, koa 3, fastify 5, got 11 and 16, axios 1, http-proxy |
NestJS
@Injectable()
export class CorrelationMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction): void {
correlationId.expressMiddleware(req, res, next);
}
}Fastify
There is no dedicated Fastify middleware — the correlator is used directly from an
onRequest hook, and the context survives into route handlers:
app.addHook('onRequest', (req, reply, done) => {
correlationId.correlator.withId(req.headers['x-correlationid'] as string | undefined, () => {
const id = correlationId.correlator.getId();
if (id) reply.header('x-correlationid', id);
const inbound = req.headers.baggage as string | undefined;
if (inbound) correlationId.correlator.setBaggage(inbound);
done();
});
});Baggage Propagation
Baggage allows attaching key-value metadata to a request context and propagating it across services via the Baggage HTTP header (W3C standard).
Automatic propagation
Both Express and Koa middleware propagate inbound Baggage headers — if a request arrives with a Baggage header, it is captured and forwarded on outbound requests (via enhanceGot, enhanceRequest, enhanceHttpProxy).
However, only the Express middleware supports creating baggage from request data via the extractBaggage callback. Koa middleware only forwards existing baggage headers.
Custom baggage extraction (Express only)
Extract baggage from request data (e.g. JWT tokens, custom headers). Note that when
extractBaggage is provided it replaces inbound baggage handling — the incoming
Baggage header is not merged into the result, and if every extracted value is falsy
(null, undefined, '', 0) no baggage is set at all, including the inbound one:
const correlationId = createCorrelationId({
extractBaggage: (req) => ({
userId: req.headers['x-user-id'],
accountId: req.headers['x-account-id'],
}),
});To access framework-specific request properties, widen the request type:
import type {Request} from 'express';
const correlationId = createCorrelationId<Request>({
extractBaggage: (req) => ({userId: req.user?.id, path: req.path}),
});W3C size limits
Baggage is serialized through OpenTelemetry's W3CBaggagePropagator, which enforces the
limits from the W3C specification. Values that exceed them are dropped rather than
truncated:
| Limit | Behaviour when exceeded |
|-------|-------------------------|
| 4096 characters per key=value pair | that entry is dropped; if it was the only entry, no Baggage header is sent |
| 180 entries | entries past the 180th are dropped |
| 8192 characters in total | the header is cut off at the limit |
Keep baggage small — it travels with every outbound request.
Manual baggage management
correlationId.correlator.withId('my-id', () => {
// Set baggage
correlationId.correlator.setBaggage('key1=value1,key2=value2');
// Get raw baggage string
correlationId.correlator.getBaggage(); // 'key1=value1,key2=value2'
// Get parsed baggage as object
correlationId.correlator.getParsedBaggage(); // {key1: 'value1', key2: 'value2'}
});Custom baggage header name
const correlationId = createCorrelationId({
baggageHeaderName: 'x-custom-baggage',
});Settings
Custom settings can be passed as an object to createCorrelationId:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| generateDefaultId | () => string | crypto.randomUUID() | Function that returns a new correlation ID |
| httpHeaderParamName | string | 'x-correlationid' | HTTP header name for correlation ID |
| baggageHeaderName | string | 'Baggage' | HTTP header name for baggage propagation |
| extractBaggage | (req) => Record \| null | — | Custom function to extract baggage from request |
API Reference
createCorrelationId(opts?)
Returns a CorrelationIdApi object with:
| Property | Description |
|----------|-------------|
| correlator | Core correlator instance |
| expressMiddleware | Express middleware |
| koaMiddleware | Koa middleware |
| enhanceGot(got) | Returns enhanced got instance with correlation headers |
| enhanceAxios(instance) | Registers a request interceptor that adds correlation headers, returns the same instance |
| enhanceRequest(request) | Returns enhanced request instance with correlation headers |
| enhanceHttpProxy(proxy) | Registers proxyReq listener that adds correlation headers |
correlator
| Method | Description |
|--------|-------------|
| getId() | Returns current correlation ID (or undefined outside context) |
| withId(id, fn) | Run function within a correlation ID context |
| withIdAndReturn(id, fn) | Same as withId but returns the function's return value |
| generateId() | Generate a new correlation ID |
| setBaggage(value) | Set baggage string for current context |
| getBaggage() | Get raw baggage string |
| getParsedBaggage() | Get baggage as parsed key-value object |
Development
yarn install # Install dependencies
yarn build # Compile TypeScript to dist/
yarn test # Run tests
yarn lint # Run ESLintKnown issues
When the middleware runs inside an already correlated context (registered twice, wrapped in an outer
withId, a request replayed from a job), it passes the request through without setting the response headers — the ID is present in the context and in the logs, but the client does not seex-correlationidon the response.enhanceHttpProxylooks the inbound header up underhttpHeaderParamNameverbatim, while Node lowercases inbound header names. With a non-lowercase name ('X-Request-Id') the inbound ID is not found outside of a correlation context and an empty header is sent instead. KeephttpHeaderParamNamelowercase.Does not work well with
bluebird.promisifyAll. Alternative solution is to explicitly promisify using native promise:
const redis = require('redis');
const util = require('util');
const client = redis.createClient();
client.setAsync = util.promisify(client.set).bind(client);
client.getAsync = util.promisify(client.get).bind(client);- Does not work well with
mongoosecallbacks. Alternative solution is to use promisified functions:
const mongoose = require('mongoose');
mongoose.Promise = global.Promise;
EntityModel.find({name: 'name'}).then((value) => {
// do something
});