autotel
Version:
Write Once, Observe Anywhere
625 lines (623 loc) • 18.2 kB
JavaScript
import { t as requireModule } from "./node-require-vROmTeJ8.js";
import { SpanStatusCode, context, trace } from "@opentelemetry/api";
//#region src/trace-helpers.ts
/**
* Trace context helpers - Core primitives for trace correlation
*
* These are the building blocks that allow users to bring their own logger
* (bunyan, log4js, custom, etc.) and add trace correlation.
*
* @example Using with bunyan
* ```typescript
* import bunyan from 'bunyan';
* import { enrichWithTraceContext } from 'autotel/trace-helpers';
*
* const bunyanLogger = bunyan.createLogger({ name: 'myapp' });
*
* const logger = {
* info: (msg: string, extra?: object) => {
* bunyanLogger.info(enrichWithTraceContext(extra || {}), msg);
* }
* };
* ```
*
* @example Using with log4js
* ```typescript
* import log4js from 'log4js';
* import { getTraceContext } from 'autotel/trace-helpers';
*
* const log4jsLogger = log4js.getLogger();
*
* function logWithTrace(level: string, msg: string, extra?: object) {
* const context = getTraceContext();
* log4jsLogger[level](msg, { ...extra, ...context });
* }
* ```
*/
/**
* WeakMap to store span names for active spans
* This allows us to retrieve the span name even though OpenTelemetry
* doesn't expose it through the public API
*/
const spanNameMap = /* @__PURE__ */ new WeakMap();
/**
* Store span name for a given span
* Called internally when spans are created
*/
function setSpanName(span, name) {
spanNameMap.set(span, name);
}
/**
* Convert hex string to decimal string representation
* Handles 64-bit unsigned integers for Datadog correlation
*
* @param hex - Hex string (up to 16 characters for 64-bit)
* @returns Decimal string representation
*/
function hexToDecimal(hex) {
return BigInt("0x" + hex).toString(10);
}
/**
* Get current trace context from active span
*
* Returns null if no span is active (e.g., outside of trace operation)
*
* Includes both OpenTelemetry standard fields (hex) and Datadog-specific
* fields (decimal) for maximum compatibility.
*
* @returns Trace context with traceId, spanId, correlationId, and Datadog decimal IDs, or null
*
* @example
* ```typescript
* import { getTraceContext } from 'autotel/trace-helpers';
*
* const context = getTraceContext();
* if (context) {
* console.log('Current trace:', context.traceId);
* // Current trace: 4bf92f3577b34da6a3ce929d0e0e4736
* console.log('Datadog trace ID:', context['dd.trace_id']);
* // Datadog trace ID: 12007117331170166582 (decimal for log correlation)
* }
* ```
*/
function getTraceContext() {
const span = trace.getActiveSpan();
if (!span) return null;
const spanContext = span.spanContext();
const traceId = spanContext.traceId;
const spanId = spanContext.spanId;
const spanName = spanNameMap.get(span);
const ddTraceId = hexToDecimal(traceId.slice(-16));
const ddSpanId = hexToDecimal(spanId);
return {
traceId,
spanId,
correlationId: traceId.slice(0, 16),
...spanName && { "code.function": spanName },
"dd.trace_id": ddTraceId,
"dd.span_id": ddSpanId
};
}
/**
* Enrich object with trace context (traceId, spanId, correlationId, and Datadog fields)
*
* If no span is active, returns the object unchanged.
* This prevents "undefined" or "null" values in logs.
*
* Automatically adds both OpenTelemetry standard fields (hex) and Datadog-specific
* fields (decimal) for maximum compatibility with observability backends.
*
* @param obj - Object to enrich (e.g., log metadata)
* @returns Object with trace context merged in, or unchanged if no active span
*
* @example
* ```typescript
* import { enrichWithTraceContext } from 'autotel/trace-helpers';
*
* // Inside a trace operation:
* const enriched = enrichWithTraceContext({ userId: '123' });
* // {
* // userId: '123',
* // traceId: '4bf92f3577b34da6a3ce929d0e0e4736',
* // spanId: '00f067aa0ba902b7',
* // correlationId: '4bf92f3577b34da6',
* // 'dd.trace_id': '12007117331170166582', // Datadog decimal format
* // 'dd.span_id': '67667974448284583' // Datadog decimal format
* // }
*
* // Outside trace operation:
* const unchanged = enrichWithTraceContext({ userId: '123' });
* // { userId: '123' } - no trace fields added
* ```
*/
function enrichWithTraceContext(obj) {
const context = getTraceContext();
return context ? {
...obj,
...context
} : obj;
}
/**
* Check if currently in a trace context
*
* Useful for conditional logic based on trace presence
*
* @returns true if active span exists, false otherwise
*
* @example
* ```typescript
* import { isTracing } from 'autotel/trace-helpers';
*
* if (isTracing()) {
* // Add expensive debug metadata only when tracing
* logger.debug('Detailed context', expensiveDebugData());
* }
* ```
*/
function isTracing() {
return trace.getActiveSpan() !== void 0;
}
/**
* Get a tracer instance for creating custom spans
*
* Use this when you need low-level control over span lifecycle.
* For most use cases, prefer trace(), span(), or instrument() instead.
*
* @param name - Tracer name (usually your service or module name)
* @param version - Optional version string
* @returns OpenTelemetry Tracer instance
*
* @example Basic usage
* ```typescript
* import { getTracer } from 'autotel';
*
* const tracer = getTracer('my-service');
* const span = tracer.startSpan('custom.operation');
* try {
* // Your logic
* span.setAttribute('key', 'value');
* } finally {
* span.end();
* }
* ```
*
* @example With AI SDK
* ```typescript
* import { getTracer } from 'autotel';
* import { generateText } from 'ai';
*
* const tracer = getTracer('ai-agent');
* const result = await generateText({
* model: myModel,
* prompt: 'Hello',
* experimental_telemetry: {
* isEnabled: true,
* tracer,
* },
* });
* ```
*/
function getTracer(name, version) {
return trace.getTracer(name, version);
}
/**
* Get the currently active span
*
* Returns undefined if no span is currently active.
* Useful for adding attributes to the current span.
*
* @returns Active span or undefined
*
* @example Adding attributes to active span
* ```typescript
* import { getActiveSpan } from 'autotel';
*
* const span = getActiveSpan();
* if (span) {
* span.setAttribute('user.id', userId);
* span.setAttribute('user.action', 'click');
* }
* ```
*
* @example Checking span status
* ```typescript
* import { getActiveSpan, SpanStatusCode } from 'autotel';
*
* const span = getActiveSpan();
* if (span?.isRecording()) {
* span.setStatus({ code: SpanStatusCode.OK });
* }
* ```
*/
function getActiveSpan() {
return trace.getActiveSpan();
}
/**
* Get the currently active OpenTelemetry context
*
* The context contains the active span and any baggage.
* Useful for context propagation and custom instrumentation.
*
* @returns Current active context
*
* @example Propagating context
* ```typescript
* import { getActiveContext } from 'autotel';
*
* const currentContext = getActiveContext();
* // Pass context to another function or service
* ```
*
* @example With context injection
* ```typescript
* import { getActiveContext, injectTraceContext } from 'autotel';
*
* const headers = {};
* injectTraceContext(headers);
* // Headers now contain trace propagation data
* ```
*/
function getActiveContext() {
try {
const { getActiveContextWithBaggage } = requireModule("./trace-context");
return getActiveContextWithBaggage();
} catch {
return context.active();
}
}
/**
* Run a function with a specific span set as active
*
* This is a convenience wrapper around the two-step process of
* setting a span in context and running code within that context.
*
* @param span - The span to set as active
* @param fn - Function to execute with the span active
* @returns The return value of the function
*
* @example Running code with a custom span
* ```typescript
* import { getTracer, runWithSpan } from 'autotel';
*
* const tracer = getTracer('my-service');
* const span = tracer.startSpan('background.job');
*
* try {
* const result = await runWithSpan(span, async () => {
* // Any spans created here will be children of 'background.job'
* await processData();
* return { success: true };
* });
* console.log(result);
* } finally {
* span.end();
* }
* ```
*
* @example Testing with custom spans
* ```typescript
* import { runWithSpan, otelTrace } from 'autotel';
*
* const tracer = otelTrace.getTracer('test');
* const span = tracer.startSpan('test.operation');
*
* const result = runWithSpan(span, () => {
* // Code under test runs with this span as active
* return myFunction();
* });
*
* span.end();
* ```
*/
function runWithSpan(span, fn) {
const ctx = trace.setSpan(context.active(), span);
return context.with(ctx, fn);
}
/**
* Finalize a span with appropriate status and optional error recording
*
* This is a convenience function that:
* - Records exceptions if an error is provided
* - Sets span status to ERROR if error exists, OK otherwise
* - Ends the span
*
* @param span - The span to finalize
* @param error - Optional error to record
*
* @example Without error (success case)
* ```typescript
* import { getTracer, finalizeSpan } from 'autotel';
*
* const tracer = getTracer('my-service');
* const span = tracer.startSpan('operation');
*
* try {
* await doWork();
* finalizeSpan(span);
* } catch (error) {
* finalizeSpan(span, error);
* throw error;
* }
* ```
*
* @example With error
* ```typescript
* import { getTracer, finalizeSpan } from 'autotel';
*
* const tracer = getTracer('my-service');
* const span = tracer.startSpan('operation');
*
* try {
* await riskyOperation();
* finalizeSpan(span);
* } catch (error) {
* finalizeSpan(span, error); // Records exception and sets ERROR status
* throw error;
* }
* ```
*
* @example In instrumentation
* ```typescript
* import { getTracer, runWithSpan, finalizeSpan } from 'autotel';
*
* function instrumentedQuery(query: string) {
* const tracer = getTracer('db');
* const span = tracer.startSpan('db.query');
*
* return runWithSpan(span, () => {
* try {
* const result = executeQuery(query);
* finalizeSpan(span);
* return result;
* } catch (error) {
* finalizeSpan(span, error);
* throw error;
* }
* });
* }
* ```
*/
function finalizeSpan(span, error) {
if (error) {
if (error instanceof Error) span.recordException(error);
else span.recordException(new Error(String(error)));
span.setStatus({ code: SpanStatusCode.ERROR });
} else span.setStatus({ code: SpanStatusCode.OK });
span.end();
}
/**
* Creates a deterministic trace ID from a seed string.
*
* Generates a consistent 128-bit trace ID (32 hex characters) from an input seed
* using SHA-256 hashing. Useful for correlating external system IDs (request IDs,
* order IDs, session IDs) with OpenTelemetry trace IDs.
*
* **Use Cases:**
* - Correlate external request IDs with traces
* - Link customer support tickets to trace data
* - Associate business entities (orders, sessions) with observability data
* - Debug specific user flows by deterministic trace lookup
*
* **Important:** Only use this when you need deterministic trace IDs for correlation.
* For normal tracing, let OpenTelemetry generate random trace IDs automatically.
*
* **Runtime Support:**
* - Node.js 15+ (native crypto.subtle)
* - All modern browsers
* - Edge runtimes (Cloudflare Workers, Deno, etc.)
*
* @param seed - Input string to generate trace ID from (e.g., request ID, order ID)
* @returns Promise resolving to a 32-character hex trace ID (128 bits)
*
* @example Correlate external request ID with trace
* ```typescript
* import { createDeterministicTraceId } from 'autotel/trace-helpers'
* import { trace, context } from '@opentelemetry/api'
*
* // In middleware or request handler
* const requestId = req.headers['x-request-id']
* const traceId = await createDeterministicTraceId(requestId)
*
* // Use with manual span creation (advanced - not needed with trace/span functions)
* const tracer = trace.getTracer('my-service')
* const spanContext = {
* traceId,
* spanId: '0123456789abcdef', // Still random
* traceFlags: 1
* }
* ```
*
* @example Link customer support tickets to traces
* ```typescript
* import { createDeterministicTraceId } from 'autotel/trace-helpers'
*
* // Support dashboard integration
* const ticketId = 'TICKET-12345'
* const traceId = await createDeterministicTraceId(ticketId)
*
* // Generate direct link to traces in observability backend
* const traceUrl = `https://your-otel-backend.com/traces/${traceId}`
* console.log(`View related traces: ${traceUrl}`)
* ```
*
* @example Session-based correlation
* ```typescript
* import { createDeterministicTraceId } from 'autotel/trace-helpers'
*
* // Track all operations for a user session
* const sessionId = req.session.id
* const traceId = await createDeterministicTraceId(sessionId)
*
* // All operations in this session share the same trace ID
* // Makes it easy to find all activity for a specific session
* ```
*
* @public
*/
async function createDeterministicTraceId(seed) {
const data = new TextEncoder().encode(seed);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
return [...new Uint8Array(hashBuffer)].map((byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, 32);
}
/**
* Flattens nested metadata objects into dot-notation span attributes.
*
* Converts complex nested objects into flat key-value pairs suitable for
* OpenTelemetry span attributes. Non-string values are JSON serialized.
* Handles serialization failures gracefully with a fallback value.
*
* **Use Cases:**
* - Structured metadata with nested objects
* - User context with multiple properties
* - Request/response metadata
* - Business entity attributes
*
* **Note:** Filters out null/undefined values automatically to keep spans clean.
*
* @param metadata - Nested metadata object to flatten
* @param prefix - Prefix for all attribute keys (default: 'metadata')
* @returns Flattened attributes as { [key: string]: string }
*
* @example Basic metadata flattening
* ```typescript
* import { flattenMetadata } from 'autotel/trace-helpers'
* import { trace } from 'autotel'
*
* export const processOrder = trace(ctx => async (orderId: string) => {
* const order = await getOrder(orderId)
*
* // Flatten complex order metadata
* const flattened = flattenMetadata({
* user: { id: order.userId, tier: 'premium' },
* payment: { method: 'card', processor: 'stripe' },
* items: order.items.length
* })
*
* ctx.setAttributes(flattened)
* // Results in:
* // {
* // 'metadata.user.id': 'user-123',
* // 'metadata.user.tier': 'premium',
* // 'metadata.payment.method': 'card',
* // 'metadata.payment.processor': 'stripe',
* // 'metadata.items': '5'
* // }
* })
* ```
*
* @example Custom prefix for semantic conventions
* ```typescript
* import { flattenMetadata } from 'autotel/trace-helpers'
* import { trace } from 'autotel'
*
* export const fetchUser = trace(ctx => async (userId: string) => {
* const user = await db.users.findOne({ id: userId })
*
* // Use semantic convention prefix
* const userAttrs = flattenMetadata(
* {
* id: user.id,
* email: user.email,
* plan: user.subscription.plan
* },
* 'user' // Custom prefix
* )
*
* ctx.setAttributes(userAttrs)
* // Results in:
* // {
* // 'user.id': 'user-123',
* // 'user.email': 'user@example.com',
* // 'user.plan': 'enterprise'
* // }
* })
* ```
*
* @example With complex objects (auto-serialized)
* ```typescript
* import { flattenMetadata } from 'autotel/trace-helpers'
* import { trace } from 'autotel'
*
* export const analyzeRequest = trace(ctx => async (req: Request) => {
* const metadata = flattenMetadata({
* headers: req.headers, // Object - will be JSON serialized
* query: req.query, // Object - will be JSON serialized
* timestamp: new Date() // Non-string - will be JSON serialized
* })
*
* ctx.setAttributes(metadata)
* // Results in:
* // {
* // 'metadata.headers': '{"accept":"application/json",...}',
* // 'metadata.query': '{"page":"1","limit":"10"}',
* // 'metadata.timestamp': '"2024-01-15T12:00:00.000Z"'
* // }
* })
* ```
*
* @example Error handling
* ```typescript
* import { flattenMetadata } from 'autotel/trace-helpers'
*
* // Objects with circular references are handled gracefully
* const circular: any = { a: 1 }
* circular.self = circular
*
* const flattened = flattenMetadata({ data: circular })
* // Results in:
* // { 'metadata.data': '<serialization-failed>' }
* ```
*
* @public
*/
/**
* Resolve a trace URL from a template string and trace ID.
*
* Templates use `{traceId}` as placeholder. Falls back to `OTEL_TRACE_URL_TEMPLATE` env var.
*
* @example
* resolveTraceUrl('https://grafana.example.com/explore?traceId={traceId}', 'abc123')
* // => 'https://grafana.example.com/explore?traceId=abc123'
*/
function resolveTraceUrl(template, traceId) {
const t = template ?? process.env.OTEL_TRACE_URL_TEMPLATE;
if (!t) return void 0;
return t.replace(/\{traceId\}/g, traceId);
}
function flattenMetadata(metadata, prefix = "metadata") {
const flattened = {};
const seen = /* @__PURE__ */ new WeakSet();
function flatten(obj, currentPrefix) {
for (const [key, value] of Object.entries(obj)) {
if (value == null) continue;
const attributeKey = `${currentPrefix}.${key}`;
if (typeof value === "string") {
flattened[attributeKey] = value;
continue;
}
if (typeof value === "number" || typeof value === "boolean") {
flattened[attributeKey] = String(value);
continue;
}
if (typeof value === "object" && value !== null && value.constructor === Object) {
if (seen.has(value)) {
flattened[attributeKey] = "<circular-reference>";
continue;
}
seen.add(value);
flatten(value, attributeKey);
continue;
}
try {
flattened[attributeKey] = JSON.stringify(value);
} catch {
flattened[attributeKey] = "<serialization-failed>";
}
}
}
flatten(metadata, prefix);
return flattened;
}
//#endregion
export { createDeterministicTraceId, enrichWithTraceContext, finalizeSpan, flattenMetadata, getActiveContext, getActiveSpan, getTraceContext, getTracer, isTracing, resolveTraceUrl, runWithSpan, setSpanName };
//# sourceMappingURL=trace-helpers.js.map