UNPKG

autotel

Version:
535 lines (531 loc) 16.6 kB
import { Span, Context, Tracer } from '@opentelemetry/api'; /** * 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 }); * } * ``` */ /** * Store span name for a given span * Called internally when spans are created */ declare function setSpanName(span: Span, name: string): void; /** * Trace context extracted from active span */ interface TraceContext { /** Full 32-character hex trace ID */ traceId: string; /** 16-character hex span ID */ spanId: string; /** First 16 characters of trace ID (for log grouping/correlation) */ correlationId: string; /** Function/operation name (OpenTelemetry semantic convention: code.function) */ 'code.function'?: string; /** Datadog trace ID in decimal format (lower 64 bits) for log-trace correlation */ 'dd.trace_id'?: string; /** Datadog span ID in decimal format for log-trace correlation */ 'dd.span_id'?: string; } /** * 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) * } * ``` */ declare function getTraceContext(): TraceContext | null; /** * 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 * ``` */ declare function enrichWithTraceContext<T extends Record<string, unknown>>(obj: T): T; /** * 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()); * } * ``` */ declare function isTracing(): boolean; /** * 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, * }, * }); * ``` */ declare function getTracer(name: string, version?: string): Tracer; /** * Get the currently active span * * Returns undefined if no span is currently active. * Useful for adding attributes or events 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.addEvent('User action', { action: 'click' }); * } * ``` * * @example Checking span status * ```typescript * import { getActiveSpan, SpanStatusCode } from 'autotel'; * * const span = getActiveSpan(); * if (span?.isRecording()) { * span.setStatus({ code: SpanStatusCode.OK }); * } * ``` */ declare function getActiveSpan(): Span | undefined; /** * 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 * ``` */ declare function getActiveContext(): Context; /** * 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(); * ``` */ declare function runWithSpan<T>(span: Span, fn: () => T): T; /** * 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; * } * }); * } * ``` */ declare function finalizeSpan(span: Span, error?: unknown): void; /** * 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 */ declare function createDeterministicTraceId(seed: string): Promise<string>; /** * 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' */ declare function resolveTraceUrl(template: string | undefined, traceId: string): string | undefined; declare function flattenMetadata(metadata: Record<string, unknown>, prefix?: string): Record<string, string>; export { type TraceContext, createDeterministicTraceId, enrichWithTraceContext, finalizeSpan, flattenMetadata, getActiveContext, getActiveSpan, getTraceContext, getTracer, isTracing, resolveTraceUrl, runWithSpan, setSpanName };