autotel
Version:
Write Once, Observe Anywhere
207 lines (191 loc) • 5.9 kB
text/typescript
/**
* Correlation ID utilities for event-driven observability
*
* Provides a stable join key across events, logs, and spans even when traces fragment.
* Format: 16 hex chars (64 bits), crypto-random, URL-safe.
*
* Lifecycle:
* 1. Generated at boundary root (HTTP server span, message process span, cron job span)
* 2. Reused within context (nested work shares it via AsyncLocalStorage)
* 3. Propagated via baggage (optional, default OFF to avoid header bloat)
*
* @example Basic usage
* ```typescript
* import { generateCorrelationId, getCorrelationId } from 'autotel/correlation-id';
*
* // Generate a new correlation ID
* const id = generateCorrelationId();
* // Returns: 'a1b2c3d4e5f67890'
*
* // Get current correlation ID from context
* const currentId = getCorrelationId();
* ```
*/
import { trace, propagation, context } from '@opentelemetry/api';
import { AsyncLocalStorage } from 'node:async_hooks';
import { enterOrRun } from './trace-context';
type CorrelationStore = {
value: string;
};
/**
* AsyncLocalStorage for storing correlation ID
* This allows correlation IDs to persist across async boundaries
*/
const correlationStorage = new AsyncLocalStorage<CorrelationStore>();
/**
* Baggage key for correlation ID propagation
*/
export const CORRELATION_ID_BAGGAGE_KEY = 'autotel.correlation_id';
/**
* Generate a new correlation ID
*
* Format: 16 hex chars (64 bits), crypto-random, URL-safe
*
* @returns A new correlation ID
*
* @example
* ```typescript
* const id = generateCorrelationId();
* // Returns: 'a1b2c3d4e5f67890'
* ```
*/
export function generateCorrelationId(): string {
// Use crypto.getRandomValues for secure random bytes
const bytes = new Uint8Array(8); // 64 bits
crypto.getRandomValues(bytes);
// Convert to hex string
return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
}
/**
* Get the current correlation ID from context
*
* Resolution order:
* 1. AsyncLocalStorage (from explicit setCorrelationId or runWithCorrelationId)
* 2. Baggage (if propagated from upstream)
* 3. Active span's trace ID (first 16 chars as fallback)
* 4. undefined (if not in any context)
*
* @returns Current correlation ID or undefined
*
* @example
* ```typescript
* const id = getCorrelationId();
* if (id) {
* console.log('Correlation ID:', id);
* }
* ```
*/
export function getCorrelationId(): string | undefined {
// 1. Check AsyncLocalStorage first (explicit correlation ID)
const storedId = correlationStorage.getStore()?.value;
if (storedId) {
return storedId;
}
// 2. Check baggage (propagated from upstream)
const activeContext = context.active();
const baggage = propagation.getBaggage(activeContext);
const baggageEntry = baggage?.getEntry(CORRELATION_ID_BAGGAGE_KEY);
if (baggageEntry?.value) {
return baggageEntry.value;
}
// 3. Fall back to active span's trace ID (first 16 chars)
const span = trace.getActiveSpan();
if (span) {
const spanContext = span.spanContext();
return spanContext.traceId.slice(0, 16);
}
// 4. No context available
return undefined;
}
/**
* Get or create a correlation ID
*
* If a correlation ID exists in the current context, returns it.
* Otherwise, generates a new one.
*
* @returns Existing or new correlation ID
*
* @example
* ```typescript
* const id = getOrCreateCorrelationId();
* // Always returns a valid correlation ID
* ```
*/
export function getOrCreateCorrelationId(): string {
return getCorrelationId() ?? generateCorrelationId();
}
/**
* Run a function with a specific correlation ID in context
*
* The correlation ID will be available via getCorrelationId() throughout
* the execution of the function and any async operations it spawns.
*
* @param correlationId - Correlation ID to use
* @param fn - Function to execute
* @returns The return value of the function
*
* @example
* ```typescript
* await runWithCorrelationId('abc123', async () => {
* // getCorrelationId() returns 'abc123' here
* await processRequest();
* });
* ```
*/
export function runWithCorrelationId<T>(correlationId: string, fn: () => T): T {
return correlationStorage.run({ value: correlationId }, fn);
}
/**
* Set correlation ID in the current context (mutates context)
*
* Note: This updates the AsyncLocalStorage context. For proper scoping
* across async boundaries, prefer runWithCorrelationId() instead.
*
* @param correlationId - Correlation ID to set
*
* @example
* ```typescript
* setCorrelationId('abc123');
* // Now getCorrelationId() returns 'abc123'
* ```
*/
export function setCorrelationId(correlationId: string): void {
enterOrRun(correlationStorage, correlationId);
}
/**
* Set correlation ID in baggage for propagation
*
* This adds the correlation ID to the W3C baggage header, allowing it
* to be propagated to downstream services.
*
* Note: Only use this when you explicitly want cross-service propagation.
* Default is OFF to avoid header bloat.
*
* @param correlationId - Correlation ID to propagate
* @returns New context with baggage set
*
* @example
* ```typescript
* const newContext = setCorrelationIdInBaggage('abc123');
* context.with(newContext, () => {
* // Baggage will be propagated in outgoing requests
* });
* ```
*/
export function setCorrelationIdInBaggage(
correlationId: string,
): import('@opentelemetry/api').Context {
const activeContext = context.active();
let baggage =
propagation.getBaggage(activeContext) ?? propagation.createBaggage();
baggage = baggage.setEntry(CORRELATION_ID_BAGGAGE_KEY, {
value: correlationId,
});
return propagation.setBaggage(activeContext, baggage);
}
/**
* Get the correlation storage instance (for internal use in init/shutdown)
*/
export function getCorrelationStorage(): AsyncLocalStorage<CorrelationStore> {
return correlationStorage;
}