| name | correlation-id-tracking |
| description | Manages correlation-id in Node.js/TypeScript applications using AsyncLocalStorage for async context isolation. Use when implementing correlation-id tracking, HTTP request/response correlation, logging integration, or when working with distributed tracing in Node.js applications. |
Correlation-ID Tracking for Node.js
This skill helps you implement correlation-id tracking in Node.js/TypeScript applications using AsyncLocalStorage for async context preservation.
Installation
npm install async-local-storage
yarn add async-local-storage
Quick Setup
Express.js - Zero Configuration
Minimal setup:
import express from 'express';
import { correlationIdMiddleware } from './middleware/correlation-id';
const app = express();
app.use(correlationIdMiddleware);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/api/test', (req, res) => {
const correlationId = getCorrelationId();
res.json({ correlationId });
});
app.listen(3000);
Correlation-ID Middleware
import { Request, Response, NextFunction } from 'express';
import { AsyncLocalStorage } from 'async_hooks';
const correlationStorage = new AsyncLocalStorage<string>();
export function getCorrelationId(): string {
return correlationStorage.getStore() || generateCorrelationId();
}
export function setCorrelationId(id: string): void {
correlationStorage.enterWith(id);
}
function generateCorrelationId(): string {
return crypto.randomUUID().replace(/-/g, '');
}
export function correlationIdMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
const correlationId =
req.headers['x-correlation-id'] as string || generateCorrelationId();
correlationStorage.run(correlationId, () => {
res.setHeader('X-Correlation-Id', correlationId);
next();
});
}
Using Correlation-ID in Code
In Controllers
import { Request, Response } from 'express';
import { getCorrelationId } from '../middleware/correlation-id';
export async function getOrder(req: Request, res: Response) {
const correlationId = getCorrelationId();
try {
const order = await orderService.getById(req.params.id);
res.json({ order, correlationId });
} catch (error) {
res.status(500).json({ error: 'Failed to get order', correlationId });
}
}
In Services
import { getCorrelationId } from '../middleware/correlation-id';
import logger from '../logger';
export class OrderService {
async getById(id: string) {
const correlationId = getCorrelationId();
logger.info('Getting order', {
orderId: id,
correlationId
});
return order;
}
}
HTTP Client Integration
Axios Interceptor
import axios from 'axios';
import { getCorrelationId } from './middleware/correlation-id';
axios.interceptors.request.use((config) => {
const correlationId = getCorrelationId();
if (correlationId) {
config.headers['X-Correlation-Id'] = correlationId;
}
return config;
});
Fetch Wrapper
import { getCorrelationId } from './middleware/correlation-id';
export async function fetchWithCorrelationId(
url: string,
options?: RequestInit
): Promise<Response> {
const correlationId = getCorrelationId();
const headers = {
...options?.headers,
'X-Correlation-Id': correlationId || '',
};
return fetch(url, { ...options, headers });
}
Logging Integration
Winston Logger
import winston from 'winston';
import { getCorrelationId } from './middleware/correlation-id';
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: {
get correlationId() {
return getCorrelationId();
},
},
transports: [
new winston.transports.Console({
format: winston.format.simple(),
}),
],
});
logger.info('Processing order', { orderId: '123' });
Pino Logger
import pino from 'pino';
import { getCorrelationId } from './middleware/correlation-id';
const logger = pino({
mixin() {
return {
correlationId: getCorrelationId(),
};
},
});
logger.info({ orderId: '123' }, 'Processing order');
Logging Best Practices
Log Levels: When to Use Each
DEBUG - Development and troubleshooting only:
- Detailed execution flow
- Variable values and intermediate states
- Step-by-step process information
- Not visible in production (typically filtered out)
INFO - Production-ready, important events:
- Request start/completion
- Business operations (create, update, delete)
- External API calls (start/end)
- Important state changes
- Visible in production
WARNING - Potential issues that don't break functionality:
- Retry attempts
- Fallback to default values
- Deprecated API usage
- Performance degradation
ERROR - Failures that need attention:
- Exceptions and errors
- Failed operations
- External service failures
What to Log (and What NOT to Log)
✅ DO Log:
- Request identifiers (correlation-ID, user ID, request ID)
- Business operations (what happened)
- External service calls (start, end, duration)
- Important state changes
- Errors with context (correlation-ID, user, operation)
❌ DON'T Log:
- Sensitive data (passwords, tokens, PII, credit cards)
- Large payloads (use summaries instead)
- Every iteration in loops (log once per operation)
- Redundant information (correlation-ID is already in context)
- Excessive detail in production (use DEBUG for that)
Structured Logging with Correlation-ID
Always include correlation-ID in logs for traceability:
logger.info('Processing order', {
orderId: orderId,
userId: userId,
correlationId: getCorrelationId(),
});
logger.info('External API call completed', {
endpoint: endpoint,
duration: duration,
statusCode: statusCode,
correlationId: getCorrelationId(),
});
logger.info(`Processing order ${orderId}`);
logger.info(`User password: ${password}`);
Logging Patterns for Debugging
Pattern 1: Request Lifecycle
export async function getOrder(req: Request, res: Response) {
const correlationId = getCorrelationId();
logger.info('Getting order', {
orderId: req.params.id,
correlationId
});
try {
logger.debug('Querying database for order', {
orderId: req.params.id,
correlationId
});
const order = await orderRepository.findById(req.params.id);
if (!order) {
logger.warn('Order not found', {
orderId: req.params.id,
correlationId
});
return res.status(404).json({ error: 'Order not found' });
}
logger.info('Order retrieved successfully', {
orderId: req.params.id,
correlationId
});
res.json(order);
} catch (error) {
logger.error(error, 'Error retrieving order', {
orderId: req.params.id,
correlationId
});
res.status(500).json({ error: 'Internal server error' });
}
}
Pattern 2: External Service Calls
export async function processPayment(request: PaymentRequest) {
const correlationId = getCorrelationId();
logger.info('Calling payment service', {
amount: request.amount,
currency: request.currency,
correlationId,
});
const startTime = Date.now();
try {
const response = await paymentClient.process(request);
const duration = Date.now() - startTime;
logger.info('Payment service call completed', {
status: response.status,
duration,
correlationId,
});
return response;
} catch (error) {
const duration = Date.now() - startTime;
logger.error(error, 'Payment service call failed', {
duration,
correlationId,
});
throw error;
}
}
Pattern 3: Conditional Debug Logging
export async function processItems(items: Item[]) {
const correlationId = getCorrelationId();
logger.info('Processing items', {
count: items.length,
correlationId,
});
for (let i = 0; i < items.length; i++) {
logger.debug('Processing item', {
index: i + 1,
total: items.length,
itemId: items[i].id,
correlationId,
});
await processItem(items[i]);
}
logger.info('Processed items successfully', {
count: items.length,
correlationId,
});
}
Key Features
- AsyncLocalStorage: Uses Node.js
async_hooks AsyncLocalStorage for context preservation
- Automatic Propagation: HTTP clients automatically include correlation-ID in headers
- Logging Integration: Works with Winston, Pino, and other loggers
- Framework Support: Works with Express, Fastify, NestJS, and other frameworks
- Header-Based: Uses
X-Correlation-Id header for HTTP propagation
Important Notes
- UUID Format: Correlation-IDs are typically 32-character UUIDs without hyphens
- Never Overwrites: If correlation-ID exists (from headers), it's preserved
- Independent from OpenTelemetry: Correlation-ID is separate from trace context - both can coexist
- Thread-Safe: AsyncLocalStorage is safe for concurrent async operations
Common Patterns
Pattern 1: Reading Correlation-ID from Request
The middleware automatically:
- Reads
X-Correlation-Id from incoming request headers
- If present, uses that value
- If missing, generates new UUID (32 chars, no hyphens)
- Sets AsyncLocalStorage context
- Adds to response headers
Pattern 2: Preserving Across Async Operations
const correlationId = getCorrelationId();
await someAsyncMethod();
const sameId = getCorrelationId();
Pattern 3: Manual Propagation
setCorrelationId(externalCorrelationId);
await processRequest();
Key Principles
- Correlation-ID Always: Every log should include correlation-ID
- Structured Properties: Use structured logging with named properties, not string interpolation
- Context Matters: Include relevant context (user ID, operation, IDs) but not sensitive data
- Level Appropriately: Use DEBUG for detailed troubleshooting, INFO for production visibility
- Performance Aware: Don't log in tight loops; summarize instead
- Error Context: Always include correlation-ID and relevant context in error logs