Correlation IDs provide end-to-end request tracing across all systems: APIs, webhooks, background jobs, and databases. A single correlation ID ties together all work that belongs to one logical operation, making debugging and incident response tractable.
Every request gets a correlation context containing:
{
correlationId: "uuid-v4", // Primary identifier for the request
traceId: "uuid-v4", // End-to-end trace ID (usually same as correlationId)
operationId: "uuid-v4", // Unique ID for this operation
requestId: "uuid-v4", // Original HTTP request ID
parentCorrelationId: "uuid-v4" // If this is async work spawned from a request
}HTTP Request
↓ (middleware sets correlationId)
↓
Logger (automatic inclusion in all logs)
Request Processing
├─→ Database queries (context available)
├─→ Stellar operations (context available)
├─→ Webhook delivery (headers sent)
└─→ Background jobs (context inherited)
↓ (async context carries parentCorrelationId)
↓
Webhook sent with X-Correlation-ID header
Log lines include correlationId
Database records audited with correlationId
Correlation context is automatically initialized in the request middleware:
// src/middleware/requestId.js
const correlationUtils = require('../utils/correlation');
// Creates correlation context from inbound headers or generates new IDs
context = correlationUtils.createCorrelationContext({
requestId,
correlationId: inboundHeaders.correlationId || undefined,
traceId: inboundHeaders.traceId || undefined,
operationType: 'http_request',
metadata: { method, path, userAgent, ip, initiatedAt }
});
correlationUtils.setCorrelationContext(context);Inbound Headers:
X-Correlation-ID: If present, reused; otherwise generatedX-Trace-ID: If present, reused; otherwise set to correlationIdX-Operation-ID: New operation ID always generated
Outbound Response Headers:
X-Request-ID: HTTP request IDX-Correlation-ID: Correlation IDX-Trace-ID: Trace ID
All log lines automatically include correlation fields via log.setContext():
// Automatically called in requestId middleware
log.setContext({
requestId,
correlationId: context.correlationId,
traceId: context.traceId,
route: req.path
});
// Usage: correlation fields are auto-included
log.info('Service', 'Processing donation', { amount: 100 });
// Output: {..., "correlationId": "abc-123", "traceId": "abc-123", ...}When sending webhooks, correlation headers are automatically included:
// src/services/WebhookService.js
const correlationHeaders = generateCorrelationHeaders();
// Headers added to every webhook POST request
const options = {
headers: {
'Content-Type': 'application/json',
'X-Signature': `sha256=${signature}`,
...correlationHeaders // ← Automatically includes X-Correlation-ID, X-Trace-ID, X-Operation-ID
}
};
// Payload also includes correlation context for reference
const body = JSON.stringify({
event,
data: payload,
correlationContext: {
correlationId: correlationHeaders['X-Correlation-ID'],
traceId: correlationHeaders['X-Trace-ID'],
operationId: correlationHeaders['X-Operation-ID']
}
});Webhook Headers Sent:
X-Correlation-ID: Correlation ID from originating requestX-Trace-ID: Trace ID from originating requestX-Operation-ID: Operation ID from webhook deliveryX-Signature: Webhook signatureX-Signature-Timestamp: Signature timestamp
Background tasks spawned from a request inherit the parent's correlation ID:
const { createAsyncContext, withAsyncContext } = require('./utils/correlation');
// In request handler:
async function handleDonation(req, res) {
// ... process donation ...
// Fire-and-forget webhook delivery with inherited correlation
withAsyncContext('webhook_delivery', async () => {
await sendWebhook(webhookData);
}, {
parentRequestId: req.correlationContext.requestId
}).catch(() => {});
}Context Inheritance:
- Parent
correlationId→ ChildparentCorrelationId - Parent
traceId→ ChildtraceId(inherited) - Parent
requestId→ ChildrequestId(inherited) - New
operationIdgenerated for this async operation
Log Output:
{
"correlationId": "parent-123", // Same as parent
"traceId": "parent-123",
"parentCorrelationId": "parent-123",
"operationId": "op-456" // New operation in async context
}Correlation IDs are available to database services for audit logging:
const { getCorrelationContext } = require('./utils/correlation');
async function recordDonation(amount) {
const { correlationId } = getCorrelationContext();
await Database.run(
`INSERT INTO donations (amount, correlation_id) VALUES (?, ?)`,
[amount, correlationId]
);
}When interacting with Stellar network:
const { getCorrelationContext } = require('./utils/correlation');
class StellarService {
async submitTransaction(xdr) {
const { correlationId, operationId } = getCorrelationContext();
log.info('STELLAR', 'Submitting transaction', {
correlationId,
operationId,
txSize: xdr.length
});
// Include correlation ID in request if possible
const result = await this.horizon.submitTransaction(xdr);
log.info('STELLAR', 'Transaction submitted', {
correlationId,
operationId,
txHash: result.hash
});
}
}const { getCorrelationContext } = require('../utils/correlation');
async function createDonation(req, res) {
const { correlationId } = getCorrelationContext();
log.info('DONATION', 'Creating donation', {
donationId: req.body.id,
correlationId // Auto-included but shown for clarity
});
// Process donation...
}class DonationService {
async process(donation) {
const { correlationId, operationId } = getCorrelationContext();
log.info('DONATION_SERVICE', 'Processing', {
donationId: donation.id,
correlationId,
operationId
});
// Service logic...
}
}const { withBackgroundContext } = require('./utils/correlation');
// Create background task with isolated context
withBackgroundContext('webhook_processor', async () => {
const { correlationId, parentCorrelationId } = getCorrelationContext();
log.info('WEBHOOK_PROCESSOR', 'Processing', {
correlationId,
parentCorrelationId // Links back to originating request
});
// Process webhooks...
}, {
taskType: 'webhook_processor'
}).catch(err => {
log.error('WEBHOOK_PROCESSOR', 'Failed', { error: err.message });
});// Parse incoming correlation headers to link to originating system
const { parseCorrelationHeaders, setCorrelationContext } = require('./utils/correlation');
app.post('/webhooks/stripe', (req, res) => {
const inboundCorrelation = parseCorrelationHeaders(req.headers);
// Set correlation context for all subsequent logs
setCorrelationContext({
correlationId: inboundCorrelation.correlationId || generateUUID(),
traceId: inboundCorrelation.traceId,
operationId: inboundCorrelation.operationId
});
log.info('WEBHOOK_RECEIVER', 'Received webhook', {
event: req.body.type
});
// Process webhook...
res.json({ received: true });
});| Header | Meaning | Example |
|---|---|---|
X-Correlation-ID |
Primary request identifier | 550e8400-e29b-41d4-a716-446655440000 |
X-Trace-ID |
End-to-end trace identifier | 550e8400-e29b-41d4-a716-446655440000 |
X-Operation-ID |
Unique operation identifier | f47ac10b-58cc-4372-a567-0e02b2c3d479 |
X-Request-ID |
HTTP request identifier | req-abc123def456 |
| Header | Meaning | Example |
|---|---|---|
X-Signature |
HMAC-SHA256 signature | sha256=abcd1234... |
X-Signature-Timestamp |
Signature timestamp | 2024-07-24T12:34:56.789Z |
X-Webhook-Signature |
Alternative signature header | sha256=abcd1234... |
X-Webhook-Timestamp |
Alternative timestamp header | 2024-07-24T12:34:56.789Z |
-
Check middleware is installed and runs before your route:
app.use(requestIdMiddleware);
-
Verify
log.setContext()is being called:// Should be in requestId middleware if (log.setContext) { log.setContext({ requestId, correlationId: context.correlationId, traceId: context.traceId }); }
-
Ensure you're using the unified logger, not
console.log:const log = require('./utils/log'); log.info('Service', 'Message'); // ✓ Includes correlationId console.log('Message'); // ✗ No correlationId
-
Verify
generateCorrelationHeaders()is called in webhook sender:const correlationHeaders = generateCorrelationHeaders();
-
Ensure headers are merged into request options:
const options = { headers: { 'Content-Type': 'application/json', ...correlationHeaders // ← Must be spread here } };
-
Use
withAsyncContext()when spawning async work:// ✓ Correct: Correlation inherited withAsyncContext('webhook_delivery', async () => { log.info('WEBHOOK', 'Sending'); // correlationId included }); // ✗ Wrong: Correlation lost setTimeout(() => { log.info('WEBHOOK', 'Sending'); // correlationId NOT included }, 1000);
-
For promises, ensure context is preserved:
// ✓ Correct: Context preserved const { withCorrelationContext, getCorrelationContext } = require('./utils/correlation'); const ctx = getCorrelationContext(); promise.then(() => { withCorrelationContext(ctx, () => { log.info('Service', 'After promise'); }); });
const { setCorrelationContext, getCorrelationContext } = require('../utils/correlation');
describe('DonationService', () => {
it('should log with correlation ID', () => {
const correlationId = '550e8400-e29b-41d4-a716-446655440000';
setCorrelationContext({ correlationId });
// ... call service ...
const ctx = getCorrelationContext();
expect(ctx.correlationId).toBe(correlationId);
});
});const request = require('supertest');
const app = require('../src/app');
describe('Correlation headers', () => {
it('should return correlation ID in response', async () => {
const correlationId = '550e8400-e29b-41d4-a716-446655440000';
const res = await request(app)
.post('/api/donation/create')
.set('X-Correlation-ID', correlationId)
.send({ amount: 100 });
expect(res.headers['x-correlation-id']).toBe(correlationId);
});
});