UNPKG

autotel

Version:
1 lines 36.3 kB
{"version":3,"file":"sampling.cjs","names":["TraceFlags"],"sources":["../src/sampling.ts"],"sourcesContent":["/**\n * Sampling Strategies\n *\n * Provides intelligent sampling beyond simple random rates.\n * Helps reduce telemetry costs while capturing critical data.\n *\n * Key strategies:\n * - Always trace errors and slow requests (critical for debugging)\n * - Sample by user ID for consistent request tracing\n * - Adaptive sampling based on load\n * - Sample by feature flags for A/B testing correlation\n *\n * @example\n * ```typescript\n * import { AlwaysOnErrorSampler, UserIdSampler } from './sampling'\n *\n * @Instrumented({\n * serviceName: 'user',\n * sampler: new AlwaysOnErrorSampler(0.1) // 10% baseline, 100% on errors\n * })\n * class UserService { }\n * ```\n */\n\nimport type { Link, Attributes } from '@opentelemetry/api';\nimport { TraceFlags } from '@opentelemetry/api';\nimport { type Logger } from './logger';\n\n/**\n * Tail sampling attribute keys (autotel-internal, not OTel semconv)\n */\nexport const AUTOTEL_SAMPLING_TAIL_KEEP = 'autotel.sampling.tail.keep';\nexport const AUTOTEL_SAMPLING_TAIL_EVALUATED =\n 'autotel.sampling.tail.evaluated';\n\n/**\n * How many events each kept event stands for, expressed as \"1 in N\".\n *\n * A query that counts sampled spans undercounts the population. Multiplying\n * each kept event by this rate restores the estimate. Autotel records the\n * attribute only when N exceeds 1, so fully captured spans stay clean.\n */\nexport const AUTOTEL_SAMPLING_RATE = 'autotel.sampling.rate';\n\n/**\n * Convert a keep probability into the \"1 in N\" rate reported on spans.\n *\n * Keeping 10% of traces means each survivor stands for 10, so the two numbers\n * are reciprocals and easy to publish the wrong way round.\n */\nfunction toSampleRate(probability: number): number {\n return probability > 0 ? 1 / probability : 0;\n}\n\n/**\n * Map a string to a stable, evenly spread position in the unit interval.\n *\n * Two processes that hash the same key reach the same number, which is what\n * lets independent services agree on one trace's sampling decision.\n *\n * The spread matters as much as the stability. Real sampling keys share long\n * prefixes: `user_1000`, `user_1001`, `checkout-trace-0001`. A plain\n * multiply-and-add hash lets that shared prefix dominate the high bits, so a\n * whole key family lands in one narrow band and a rate of 0.1 keeps all of\n * them or none of them. FNV-1a followed by the murmur3 finalizer mixes the\n * low bits back through the word, so keys that differ in one character land\n * far apart.\n */\nexport function hashUnitInterval(value: string): number {\n let hash = 0x81_1c_9d_c5; // FNV-1a 32-bit offset basis\n for (let i = 0; i < value.length; i++) {\n hash ^= value.codePointAt(i) ?? 0;\n hash = Math.imul(hash, 0x01_00_01_93); // FNV prime\n }\n\n // murmur3 fmix32 avalanche\n hash ^= hash >>> 16;\n hash = Math.imul(hash, 0x85_eb_ca_6b);\n hash ^= hash >>> 13;\n hash = Math.imul(hash, 0xc2_b2_ae_35);\n hash ^= hash >>> 16;\n\n return (hash >>> 0) / 4_294_967_296;\n}\n\n/**\n * Sampler interface - return true to trace, false to skip\n */\nexport interface Sampler {\n /**\n * Decide whether to trace this operation\n *\n * @param context - Sampling context\n * @returns true to trace, false to skip\n */\n shouldSample(context: SamplingContext): boolean;\n\n /**\n * Whether this sampler needs tail sampling (post-execution decision)\n * If true, spans are always created and shouldKeepTrace() is called after execution\n *\n * @returns true if this sampler needs to evaluate after operation completes\n */\n needsTailSampling?(): boolean;\n\n /**\n * Re-evaluate sampling decision after operation completes (tail sampling)\n * Only called if needsTailSampling() returns true\n *\n * @param context - Sampling context\n * @param result - Operation result\n * @returns true if this trace should be kept, false to drop it\n */\n shouldKeepTrace?(context: SamplingContext, result: OperationResult): boolean;\n\n /**\n * How many events a kept event represents, as \"1 in N\".\n *\n * Autotel writes the result to {@link AUTOTEL_SAMPLING_RATE} so an analyst\n * can reweight counts. Return 1 when the sampler keeps everything.\n *\n * @param context - Sampling context\n * @returns Events represented per kept event\n */\n sampleRate?(context: SamplingContext): number;\n}\n\n/**\n * Context information for sampling decisions\n */\nexport interface SamplingContext {\n /** Operation name */\n operationName: string;\n /** Method arguments (for extracting user IDs, etc.) */\n args: unknown[];\n /** Optional metadata (e.g., feature flags, request headers) */\n metadata?: Record<string, unknown>;\n /** Optional span links for links-based sampling */\n links?: Link[];\n}\n\n/**\n * Result of a trace operation (for post-execution sampling)\n */\nexport interface OperationResult {\n /** Whether the operation succeeded */\n success: boolean;\n /** Duration in milliseconds */\n duration: number;\n /** Error if operation failed */\n error?: Error;\n}\n\n/**\n * Simple random sampler\n *\n * @example\n * ```typescript\n * new RandomSampler(0.1) // Sample 10% of requests\n * ```\n */\nexport class RandomSampler implements Sampler {\n constructor(private readonly rate: number) {\n if (rate < 0 || rate > 1) {\n throw new Error('Sample rate must be between 0 and 1');\n }\n }\n\n shouldSample(_context: SamplingContext): boolean {\n return Math.random() < this.rate;\n }\n\n sampleRate(): number {\n return toSampleRate(this.rate);\n }\n}\n\n/**\n * Always sample (100% tracing)\n */\nexport class AlwaysSampler implements Sampler {\n shouldSample(_context: SamplingContext): boolean {\n return true;\n }\n}\n\n/**\n * Never sample (0% tracing)\n */\nexport class NeverSampler implements Sampler {\n shouldSample(_context: SamplingContext): boolean {\n return false;\n }\n}\n\n/**\n * Adaptive sampler that always traces errors and slow requests\n *\n * This is the recommended sampler for production use.\n * It ensures you never miss critical issues while keeping costs down.\n *\n * Strategy:\n * - Always trace errors (critical for debugging)\n * - Always trace slow requests (performance issues)\n * - Use baseline sample rate for successful fast requests\n *\n * **IMPORTANT - Tail Sampling Requirement:**\n * This sampler uses tail sampling (makes decisions AFTER execution).\n * You MUST use TailSamplingSpanProcessor for it to work correctly:\n *\n * - If using initInstrumentation(): TailSamplingSpanProcessor is auto-configured\n * - If using custom TracerProvider: You MUST manually register TailSamplingSpanProcessor\n *\n * Without TailSamplingSpanProcessor, ALL spans are exported (defeating the cost savings).\n *\n * @see TailSamplingSpanProcessor\n * @see README.md \"Tail Sampling with Custom Providers\" section\n *\n * @example\n * ```typescript\n * new AdaptiveSampler({\n * baselineSampleRate: 0.1, // 10% of normal requests\n * slowThresholdMs: 1000, // Requests > 1s are \"slow\"\n * alwaysSampleErrors: true, // Always trace errors\n * alwaysSampleSlow: true // Always trace slow requests\n * })\n * ```\n */\nexport class AdaptiveSampler implements Sampler {\n private baselineSampleRate: number;\n private slowThresholdMs: number;\n private alwaysSampleErrors: boolean;\n private alwaysSampleSlow: boolean;\n private linksBased: boolean;\n private linksRate: number;\n private logger?: Logger;\n\n // Track whether we should sample this request\n private readonly samplingDecisions = new WeakMap<unknown[], boolean>();\n // Track operation results to enable post-execution decision\n private readonly operationResults = new WeakMap<unknown[], OperationResult>();\n\n constructor(\n options: {\n baselineSampleRate?: number;\n slowThresholdMs?: number;\n alwaysSampleErrors?: boolean;\n alwaysSampleSlow?: boolean;\n /** Enable links-based sampling for event-driven architectures */\n linksBased?: boolean;\n /** Sampling rate for spans linked to sampled spans (0.0-1.0) */\n linksRate?: number;\n logger?: Logger;\n } = {},\n ) {\n this.baselineSampleRate = options.baselineSampleRate ?? 0.1;\n this.slowThresholdMs = options.slowThresholdMs ?? 1000;\n this.alwaysSampleErrors = options.alwaysSampleErrors ?? true;\n this.alwaysSampleSlow = options.alwaysSampleSlow ?? true;\n this.linksBased = options.linksBased ?? false;\n this.linksRate = options.linksRate ?? 1;\n this.logger = options.logger;\n\n if (this.baselineSampleRate < 0 || this.baselineSampleRate > 1) {\n throw new Error('Baseline sample rate must be between 0 and 1');\n }\n if (this.linksRate < 0 || this.linksRate > 1) {\n throw new Error('Links rate must be between 0 and 1');\n }\n }\n\n needsTailSampling(): boolean {\n // AdaptiveSampler ALWAYS needs tail sampling to implement error/slow capture\n return true;\n }\n\n shouldSample(context: SamplingContext): boolean {\n // For tail sampling, we optimistically create spans for all requests\n // The real decision happens in shouldKeepTrace() after execution\n // We still store the baseline decision for shouldKeepTrace() to use\n const baselineDecision = Math.random() < this.baselineSampleRate;\n this.samplingDecisions.set(context.args, baselineDecision);\n\n // Always return true to create the span (tail sampling will decide if we keep it)\n return true;\n }\n\n /**\n * Check if any links point to sampled spans.\n *\n * A span is considered linked to a sampled span if any of its links\n * have trace_flags with the sampled bit set (0x01).\n *\n * @param links - Array of span links to check\n * @returns true if any linked span is sampled, false otherwise\n */\n hasSampledLink(links: Link[]): boolean {\n if (!links || links.length === 0) {\n return false;\n }\n return links.some(\n (link) =>\n link.context && (link.context.traceFlags & TraceFlags.SAMPLED) !== 0,\n );\n }\n\n /**\n * Re-evaluate sampling decision after operation completes\n *\n * This allows us to always capture errors and slow requests,\n * even if they weren't initially sampled.\n *\n * @param context - Sampling context\n * @param result - Operation result\n * @returns true if this operation should be kept (not discarded)\n */\n shouldKeepTrace(context: SamplingContext, result: OperationResult): boolean {\n const baselineDecision = this.samplingDecisions.get(context.args) ?? false;\n\n // Always keep errors\n if (this.alwaysSampleErrors && !result.success) {\n if (!baselineDecision) {\n this.logger?.debug(\n {\n operation: context.operationName,\n error: result.error?.message,\n },\n 'Adaptive sampling: Keeping error trace',\n );\n }\n return true;\n }\n\n // Always keep slow requests\n if (this.alwaysSampleSlow && result.duration >= this.slowThresholdMs) {\n if (!baselineDecision) {\n this.logger?.debug(\n {\n operation: context.operationName,\n duration: result.duration,\n },\n 'Adaptive sampling: Keeping slow trace',\n );\n }\n return true;\n }\n\n // Check for sampled links (links-based sampling for event-driven systems)\n if (\n this.linksBased &&\n context.links &&\n this.hasSampledLink(context.links)\n ) {\n // Use linksRate to decide whether to keep the linked span\n const keepLinked = Math.random() < this.linksRate;\n if (keepLinked && !baselineDecision) {\n this.logger?.debug(\n {\n operation: context.operationName,\n linkCount: context.links.length,\n },\n 'Adaptive sampling: Keeping trace due to sampled link',\n );\n }\n return keepLinked;\n }\n\n // Otherwise, use baseline decision\n return baselineDecision;\n }\n}\n\n/**\n * User-based sampler for consistent tracing\n *\n * Always samples requests from specific user IDs.\n * Useful for debugging specific user issues or monitoring VIP users.\n *\n * @example\n * ```typescript\n * new UserIdSampler({\n * baselineSampleRate: 0.01, // 1% of normal users\n * alwaysSampleUsers: ['vip_123'], // Always trace VIP users\n * extractUserId: (args) => args[0]?.userId // Extract user ID from first arg\n * })\n * ```\n */\nexport class UserIdSampler implements Sampler {\n private baselineSampleRate: number;\n private alwaysSampleUsers: Set<string>;\n private extractUserId: (args: unknown[]) => string | undefined;\n private logger?: Logger;\n\n constructor(options: {\n baselineSampleRate?: number;\n alwaysSampleUsers?: string[];\n extractUserId: (args: unknown[]) => string | undefined;\n logger?: Logger;\n }) {\n this.baselineSampleRate = options.baselineSampleRate ?? 0.1;\n this.alwaysSampleUsers = new Set(options.alwaysSampleUsers || []);\n this.extractUserId = options.extractUserId;\n this.logger = options.logger;\n }\n\n shouldSample(context: SamplingContext): boolean {\n const userId = this.extractUserId(context.args);\n\n // Always sample specific users\n if (userId && this.alwaysSampleUsers.has(userId)) {\n this.logger?.debug(\n {\n operation: context.operationName,\n userId,\n },\n 'Sampling user request',\n );\n return true;\n }\n\n // For consistent per-user sampling, hash the user ID\n if (userId) {\n const hash = this.hashString(userId);\n return hash < this.baselineSampleRate;\n }\n\n // Fallback to random sampling if no user ID\n return Math.random() < this.baselineSampleRate;\n }\n\n /**\n * Add user IDs to always-sample list\n */\n addAlwaysSampleUsers(...userIds: string[]): void {\n for (const userId of userIds) {\n this.alwaysSampleUsers.add(userId);\n }\n }\n\n /**\n * Remove user IDs from always-sample list\n */\n removeAlwaysSampleUsers(...userIds: string[]): void {\n for (const userId of userIds) {\n this.alwaysSampleUsers.delete(userId);\n }\n }\n\n /**\n * Simple hash function for consistent user sampling\n */\n private hashString(str: string): number {\n return hashUnitInterval(str);\n }\n}\n\n/**\n * Consistent sampler: every service reaches the same verdict for one trace.\n *\n * `RandomSampler` rolls the dice per process, so an upstream service can keep\n * a trace that its downstream drops, leaving a waterfall with holes in it.\n * Hashing a key that travels with the request removes the disagreement. Pass\n * the trace id, or any identifier every hop already shares.\n *\n * @example\n * ```typescript\n * new DeterministicSampler({\n * sampleRate: 0.1,\n * key: (context) => trace.getActiveSpan()?.spanContext().traceId,\n * })\n * ```\n */\nexport class DeterministicSampler implements Sampler {\n private readonly rate: number;\n private readonly key: (context: SamplingContext) => string | undefined;\n\n constructor(options: {\n /** Fraction of traces to keep, 0-1. */\n sampleRate: number;\n /** Identifier shared by every hop of the trace. */\n key: (context: SamplingContext) => string | undefined;\n }) {\n if (options.sampleRate < 0 || options.sampleRate > 1) {\n throw new Error('Sample rate must be between 0 and 1');\n }\n this.rate = options.sampleRate;\n this.key = options.key;\n }\n\n shouldSample(context: SamplingContext): boolean {\n const key = this.key(context);\n // No shared key means no agreement to preserve, so fall back to chance.\n if (key === undefined) {\n return Math.random() < this.rate;\n }\n return hashUnitInterval(key) < this.rate;\n }\n\n sampleRate(): number {\n return toSampleRate(this.rate);\n }\n}\n\n/** Bucket for keys seen after the tracked map fills up. */\nconst OVERFLOW_KEY = '__overflow__';\n\n/**\n * Per-key target-rate sampler for workloads with uneven traffic.\n *\n * A single rate serves a skewed workload badly: 1% floods storage with the\n * busiest endpoint and still loses the rare tenant whose failures you need.\n * This sampler counts traffic per key over a rolling window, then sets each\n * key its own rate so every key contributes roughly `targetPerKey` events.\n * Quiet keys survive intact; loud keys get thinned.\n *\n * The first window keeps everything, because no traffic history exists yet.\n * Rates take effect from the second window onward.\n *\n * @example\n * ```typescript\n * new KeyTargetRateSampler({\n * key: (context) => context.operationName,\n * targetPerKey: 10, // ~10 events per key per window\n * windowMs: 30_000,\n * })\n * ```\n */\nexport class KeyTargetRateSampler implements Sampler {\n private readonly key: (context: SamplingContext) => string | undefined;\n private readonly targetPerKey: number;\n private readonly windowMs: number;\n private readonly maxKeys: number;\n private counts = new Map<string, number>();\n private rates = new Map<string, number>();\n private windowStart = Date.now();\n\n constructor(options: {\n /** Groups traffic. Use the operation, route, tenant, or status. */\n key: (context: SamplingContext) => string | undefined;\n /** Events to keep per key per window. Default 10. */\n targetPerKey?: number;\n /** Length of the counting window in milliseconds. Default 30000. */\n windowMs?: number;\n /** Distinct keys to track before overflowing into one bucket. Default 1000. */\n maxKeys?: number;\n }) {\n this.key = options.key;\n this.targetPerKey = options.targetPerKey ?? 10;\n this.windowMs = options.windowMs ?? 30_000;\n this.maxKeys = options.maxKeys ?? 1000;\n\n if (this.targetPerKey <= 0) {\n throw new Error('Target per key must be greater than 0');\n }\n if (this.windowMs <= 0) {\n throw new Error('Window must be greater than 0');\n }\n }\n\n /** Turn the window's observed counts into the next window's rates. */\n private roll(now: number): void {\n if (now - this.windowStart < this.windowMs) {\n return;\n }\n const rates = new Map<string, number>();\n for (const [key, count] of this.counts) {\n rates.set(key, Math.max(1, count / this.targetPerKey));\n }\n this.rates = rates;\n this.counts = new Map();\n this.windowStart = now;\n }\n\n /**\n * Resolve the key, collapsing into one bucket once the map is full.\n *\n * An unbounded key function would otherwise grow the map without limit,\n * which turns a sampler meant to cut cost into a memory leak.\n */\n private resolveKey(context: SamplingContext): string {\n const key = this.key(context) ?? OVERFLOW_KEY;\n if (this.counts.has(key) || this.counts.size < this.maxKeys) {\n return key;\n }\n return OVERFLOW_KEY;\n }\n\n shouldSample(context: SamplingContext): boolean {\n this.roll(Date.now());\n const key = this.resolveKey(context);\n this.counts.set(key, (this.counts.get(key) ?? 0) + 1);\n const rate = this.rates.get(key) ?? 1;\n return rate <= 1 || Math.random() < 1 / rate;\n }\n\n sampleRate(context: SamplingContext): number {\n return this.rates.get(this.resolveKey(context)) ?? 1;\n }\n}\n\n/**\n * Composite sampler that combines multiple samplers\n *\n * Samples if ANY of the child samplers returns true.\n *\n * @example\n * ```typescript\n * new CompositeSampler([\n * new UserIdSampler({ extractUserId: (args) => args[0]?.userId }),\n * new AdaptiveSampler({ baselineSampleRate: 0.1 })\n * ])\n * ```\n */\nexport class CompositeSampler implements Sampler {\n constructor(private readonly samplers: Sampler[]) {\n if (samplers.length === 0) {\n throw new Error('CompositeSampler requires at least one child sampler');\n }\n }\n\n shouldSample(context: SamplingContext): boolean {\n return this.samplers.some((sampler) => sampler.shouldSample(context));\n }\n}\n\n/**\n * Feature flag sampler\n *\n * Always samples requests with specific feature flags enabled.\n * Perfect for correlating A/B test experiments with metrics.\n *\n * @example\n * ```typescript\n * new FeatureFlagSampler({\n * baselineSampleRate: 0.01,\n * alwaysSampleFlags: ['new_checkout', 'experimental_ui'],\n * extractFlags: (args, metadata) => metadata?.featureFlags\n * })\n * ```\n */\nexport class FeatureFlagSampler implements Sampler {\n private baselineSampleRate: number;\n private alwaysSampleFlags: Set<string>;\n private extractFlags: (\n args: unknown[],\n metadata?: Record<string, unknown>,\n ) => string[] | undefined;\n private logger?: Logger;\n\n constructor(options: {\n baselineSampleRate?: number;\n alwaysSampleFlags?: string[];\n extractFlags: (\n args: unknown[],\n metadata?: Record<string, unknown>,\n ) => string[] | undefined;\n logger?: Logger;\n }) {\n this.baselineSampleRate = options.baselineSampleRate ?? 0.1;\n this.alwaysSampleFlags = new Set(options.alwaysSampleFlags || []);\n this.extractFlags = options.extractFlags;\n this.logger = options.logger;\n }\n\n shouldSample(context: SamplingContext): boolean {\n const flags = this.extractFlags(context.args, context.metadata);\n\n // Always sample if any monitored flag is enabled\n if (flags && flags.some((flag) => this.alwaysSampleFlags.has(flag))) {\n this.logger?.debug(\n {\n operation: context.operationName,\n flags,\n },\n 'Sampling feature flag request',\n );\n return true;\n }\n\n // Fallback to random sampling\n return Math.random() < this.baselineSampleRate;\n }\n\n /**\n * Add feature flags to always-sample list\n */\n addAlwaysSampleFlags(...flags: string[]): void {\n for (const flag of flags) {\n this.alwaysSampleFlags.add(flag);\n }\n }\n\n /**\n * Remove feature flags from always-sample list\n */\n removeAlwaysSampleFlags(...flags: string[]): void {\n for (const flag of flags) {\n this.alwaysSampleFlags.delete(flag);\n }\n }\n}\n\n// ============================================================================\n// Sampling Presets\n// ============================================================================\n\n/**\n * Named sampling presets for common environments.\n * Use with `init({ sampling: 'production' })` or directly via factories.\n */\nexport type SamplingPreset =\n 'development' | 'errors-only' | 'production' | 'off';\n\n/**\n * Sampling preset factories.\n *\n * For most users, the string shorthand on `init()` is simpler:\n * ```typescript\n * init({ service: 'my-app', sampling: 'production' })\n * ```\n *\n * Use factories when you need to customize:\n * ```typescript\n * init({ service: 'my-app', sampler: samplingPresets.production({ baselineSampleRate: 0.05 }) })\n * ```\n */\nexport const samplingPresets = {\n /** Capture everything — best for local development and debugging */\n development: () => new AlwaysSampler(),\n\n /** Only bad outcomes — zero baseline, errors always kept */\n errorsOnly: () =>\n new AdaptiveSampler({\n baselineSampleRate: 0,\n alwaysSampleErrors: true,\n }),\n\n /**\n * Balanced production defaults — 10% baseline + errors + slow traces.\n * Pass overrides to tune (uses the same option names as AdaptiveSampler).\n */\n production: (overrides?: {\n baselineSampleRate?: number;\n slowThresholdMs?: number;\n alwaysSampleErrors?: boolean;\n alwaysSampleSlow?: boolean;\n }) =>\n new AdaptiveSampler({\n baselineSampleRate: 0.1,\n alwaysSampleErrors: true,\n alwaysSampleSlow: true,\n slowThresholdMs: 1000,\n ...overrides,\n }),\n\n /** Disable sampling entirely */\n off: () => new NeverSampler(),\n};\n\n/**\n * Resolve a preset string to a Sampler instance.\n * Used internally by `init()` when `sampling` string is provided.\n *\n * @throws Error if preset is not recognized\n */\nexport function resolveSamplingPreset(preset: SamplingPreset): Sampler {\n switch (preset) {\n case 'development': {\n return samplingPresets.development();\n }\n case 'errors-only': {\n return samplingPresets.errorsOnly();\n }\n case 'production': {\n return samplingPresets.production();\n }\n case 'off': {\n return samplingPresets.off();\n }\n default: {\n throw new Error(\n `Unknown sampling preset: \"${preset}\". Valid presets: development, errors-only, production, off`,\n );\n }\n }\n}\n\n// ============================================================================\n// Link Helper Functions\n// ============================================================================\n\n/**\n * Create a Link from W3C trace context headers (e.g., from a message queue).\n *\n * This is useful for message consumers that need to link to the producer span.\n * The headers should contain at least a `traceparent` header in W3C format.\n *\n * @param headers - Dictionary containing traceparent/tracestate headers\n * @param attributes - Optional attributes for the link\n * @returns Link object if context could be extracted, null otherwise\n *\n * @example\n * ```typescript\n * // In a Kafka consumer\n * const headers = { traceparent: '00-abc123...-def456...-01' };\n * const link = createLinkFromHeaders(headers);\n * if (link) {\n * // Use with tracer.startActiveSpan options or ctx.addLink()\n * tracer.startActiveSpan('process.message', { links: [link] }, span => { ... });\n * }\n * ```\n */\nexport function createLinkFromHeaders(\n headers: Record<string, string>,\n attributes?: Attributes,\n): Link | null {\n // Parse W3C traceparent header directly for reliability\n // Format: version-traceId-spanId-traceFlags (e.g., 00-abc123...-def456...-01)\n const traceparent = headers.traceparent || headers['traceparent'];\n if (!traceparent) {\n return null;\n }\n\n const spanContext = parseTraceparent(traceparent);\n if (!spanContext || !isValidSpanContext(spanContext)) {\n return null;\n }\n\n return {\n context: spanContext,\n attributes: attributes ?? {},\n };\n}\n\n/**\n * Extract Links from a batch of messages for fan-in scenarios.\n *\n * Useful for batch processing where multiple producer spans should be linked.\n * This enables tracing causality in event-driven architectures where a single\n * consumer processes messages from multiple producers.\n *\n * @param messages - List of message objects\n * @param headersKey - Key in each message containing trace headers (default: 'headers')\n * @returns List of Link objects for all valid trace contexts\n *\n * @example\n * ```typescript\n * // Processing a batch of SQS/Kafka messages\n * const messages = [\n * { body: '...', headers: { traceparent: '...' } },\n * { body: '...', headers: { traceparent: '...' } },\n * ];\n * const links = extractLinksFromBatch(messages);\n *\n * tracer.startActiveSpan('process.batch', { links }, span => {\n * for (const msg of messages) {\n * processMessage(msg);\n * }\n * });\n * ```\n */\nexport function extractLinksFromBatch(\n messages: Array<{ [key: string]: unknown }>,\n headersKey: string = 'headers',\n): Link[] {\n const links: Link[] = [];\n\n for (const msg of messages) {\n const msgHeaders = msg[headersKey];\n if (msgHeaders && typeof msgHeaders === 'object' && msgHeaders !== null) {\n const link = createLinkFromHeaders(msgHeaders as Record<string, string>, {\n 'messaging.batch.message_index': links.length,\n });\n if (link) {\n links.push(link);\n }\n }\n }\n\n return links;\n}\n\n/**\n * Parse W3C traceparent header into SpanContext\n * Format: version-traceId-spanId-traceFlags (e.g., 00-abc123...-def456...-01)\n *\n * @see https://www.w3.org/TR/trace-context/#traceparent-header\n */\nfunction parseTraceparent(\n traceparent: string,\n): import('@opentelemetry/api').SpanContext | null {\n // W3C traceparent format: version-traceId-parentId-traceFlags\n // Example: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01\n const TRACEPARENT_REGEX =\n /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;\n\n const match = traceparent.match(TRACEPARENT_REGEX);\n if (!match || match.length < 5) {\n return null;\n }\n\n const version = match[1];\n const traceId = match[2];\n const spanId = match[3];\n const flags = match[4];\n\n // Validate all parts are present (TypeScript narrowing)\n if (!version || !traceId || !spanId || !flags) {\n return null;\n }\n\n // Version 00 is currently the only version, but we should be forward compatible\n if (version === 'ff') {\n // Version ff is invalid according to spec\n return null;\n }\n\n return {\n traceId,\n spanId,\n traceFlags: Number.parseInt(flags, 16),\n isRemote: true,\n };\n}\n\n/**\n * Check if a SpanContext is valid (has non-zero trace and span IDs)\n */\nfunction isValidSpanContext(\n spanContext: import('@opentelemetry/api').SpanContext | null,\n): spanContext is import('@opentelemetry/api').SpanContext {\n if (!spanContext) return false;\n // TraceId should not be all zeros (00000000000000000000000000000000)\n // SpanId should not be all zeros (0000000000000000)\n return (\n spanContext.traceId !== '00000000000000000000000000000000' &&\n spanContext.spanId !== '0000000000000000'\n );\n}\n"],"mappings":"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"}