UNPKG

autotel

Version:
1,183 lines (1,174 loc) 37.6 kB
import { getValidationConfig, isInitialized, warnIfNotInitialized, getConfig, getEventsConfig, getLogger } from './chunk-ZDPIWKWD.js'; import { getConfig as getConfig$1 } from './chunk-J5QENANM.js'; import { SpanStatusCode, trace, context, propagation } from '@opentelemetry/api'; import { AsyncLocalStorage } from 'async_hooks'; // src/flatten-attributes.ts function toAttributeValue(value) { if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { return value; } if (Array.isArray(value)) { if (value.every((v) => typeof v === "string") || value.every((v) => typeof v === "number") || value.every((v) => typeof v === "boolean")) { return value; } try { return JSON.stringify(value); } catch { return "<serialization-failed>"; } } if (value instanceof Date) { return value.toISOString(); } if (value instanceof Error) { return value.message; } return void 0; } function flattenToAttributes(fields, prefix = "") { const out = {}; const seen = /* @__PURE__ */ new WeakSet(); function flatten(obj, currentPrefix) { for (const [key, value] of Object.entries(obj)) { if (value == null) continue; const nextKey = currentPrefix ? `${currentPrefix}.${key}` : key; const attr = toAttributeValue(value); if (attr !== void 0) { out[nextKey] = attr; continue; } if (typeof value === "object" && value.constructor === Object) { if (seen.has(value)) { out[nextKey] = "<circular-reference>"; continue; } seen.add(value); flatten(value, nextKey); continue; } try { out[nextKey] = JSON.stringify(value); } catch { out[nextKey] = "<serialization-failed>"; } } } flatten(fields, prefix); return out; } // src/structured-error.ts var internalKey = /* @__PURE__ */ Symbol.for("autotel.error.internal"); function createStructuredError(input) { const error = new Error(input.message, { cause: input.cause }); error.name = input.name ?? "StructuredError"; if (input.why !== void 0) error.why = input.why; if (input.fix !== void 0) error.fix = input.fix; if (input.link !== void 0) error.link = input.link; if (input.code !== void 0) error.code = input.code; if (input.status !== void 0) error.status = input.status; if (input.details !== void 0) error.details = input.details; if (input.internal !== void 0) { Object.defineProperty(error, internalKey, { value: input.internal, enumerable: false, writable: false, configurable: true }); } Object.defineProperty(error, "internal", { get() { return this[internalKey]; }, enumerable: false, configurable: true }); error.toString = () => { const lines = [`${error.name}: ${error.message}`]; if (error.why) lines.push(` Why: ${error.why}`); if (error.fix) lines.push(` Fix: ${error.fix}`); if (error.link) lines.push(` Link: ${error.link}`); if (error.code !== void 0) lines.push(` Code: ${error.code}`); if (error.status !== void 0) lines.push(` Status: ${error.status}`); if (error.cause) { const cause = error.cause; lines.push(` Caused by: ${cause.name}: ${cause.message}`); } return lines.join("\n"); }; return error; } function structuredErrorToJSON(error) { const result = { name: error.name, message: error.message }; if (error.status !== void 0) result.status = error.status; if (error.why || error.fix || error.link) { result.data = { ...error.why && { why: error.why }, ...error.fix && { fix: error.fix }, ...error.link && { link: error.link } }; } if (error.code !== void 0) result.code = error.code; if (error.details) result.details = error.details; if (error.cause instanceof Error) { result.cause = { name: error.cause.name, message: error.cause.message }; } return result; } function getStructuredErrorAttributes(error) { const structured = error; const attributes = { "error.type": error.name || "Error", "error.message": error.message }; if (error.stack) attributes["error.stack"] = error.stack; if (structured.why) attributes["error.why"] = structured.why; if (structured.fix) attributes["error.fix"] = structured.fix; if (structured.link) attributes["error.link"] = structured.link; if (structured.code !== void 0) { attributes["error.code"] = typeof structured.code === "string" ? structured.code : String(structured.code); } if (structured.status !== void 0) { attributes["error.status"] = structured.status; } if (structured.details) { Object.assign( attributes, flattenToAttributes(structured.details, "error.details") ); } return attributes; } function recordStructuredError(ctx, error) { const maybeRecordException = ctx.recordException; if (typeof maybeRecordException === "function") { maybeRecordException(error); } ctx.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); ctx.setAttributes(getStructuredErrorAttributes(error)); } // src/rate-limiter.ts var TokenBucketRateLimiter = class { tokens; maxTokens; refillRate; // tokens per millisecond lastRefill; constructor(config) { this.maxTokens = config.burstCapacity || config.maxEventsPerSecond * 2; this.tokens = this.maxTokens; this.refillRate = config.maxEventsPerSecond / 1e3; this.lastRefill = Date.now(); } /** * Try to consume a token (allow an event) * Returns true if allowed, false if rate limit exceeded */ tryConsume(count = 1) { this.refill(); if (this.tokens >= count) { this.tokens -= count; return true; } return false; } /** * Wait until a token is available (async rate limiting) * Returns a promise that resolves when the event can be processed */ async waitForToken(count = 1) { this.refill(); if (this.tokens >= count) { this.tokens -= count; return; } const tokensNeeded = count - this.tokens; const waitMs = Math.ceil(tokensNeeded / this.refillRate); await new Promise((resolve) => setTimeout(resolve, waitMs)); return this.waitForToken(count); } /** * Refill tokens based on elapsed time */ refill() { const now = Date.now(); const elapsed = now - this.lastRefill; const tokensToAdd = elapsed * this.refillRate; this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd); this.lastRefill = now; } /** * Get current available tokens (for testing/debugging) */ getAvailableTokens() { this.refill(); return Math.floor(this.tokens); } /** * Reset the rate limiter (for testing) */ reset() { this.tokens = this.maxTokens; this.lastRefill = Date.now(); } }; // src/event-queue.ts var DEFAULT_CONFIG = { maxSize: 5e4, batchSize: 100, flushInterval: 1e4, maxRetries: 3, rateLimit: { maxEventsPerSecond: 100, burstCapacity: 200 } }; function getSubscriberName(subscriber) { if (subscriber.name) { return subscriber.name.toLowerCase(); } const className = subscriber.constructor?.name || "unknown"; return className.replace(/Subscriber$/i, "").toLowerCase(); } var EventQueue = class { queue = []; flushTimer = null; config; subscribers; rateLimiter; flushPromise = null; isShuttingDown = false; // Metrics metrics = null; // Observable callback cleanup functions observableCleanups = []; // Subscriber health tracking (for observable gauges) subscriberHealthy = /* @__PURE__ */ new Map(); constructor(subscribers, config) { this.subscribers = subscribers; this.config = { ...DEFAULT_CONFIG, ...config }; this.rateLimiter = this.config.rateLimit ? new TokenBucketRateLimiter(this.config.rateLimit) : null; for (const subscriber of subscribers) { const name = getSubscriberName(subscriber); this.subscriberHealthy.set(name, true); } this.initMetrics(); } /** * Initialize OTel metrics for queue observability */ initMetrics() { const runtimeConfig = getConfig$1(); const meter = runtimeConfig.meter; const queueSize = meter.createObservableGauge( "autotel.event_delivery.queue.size", { description: "Current number of events in the delivery queue", unit: "count" } ); const queueSizeCallback = (observableResult) => { observableResult.observe(this.queue.length); }; queueSize.addCallback(queueSizeCallback); this.observableCleanups.push( () => queueSize.removeCallback(queueSizeCallback) ); const oldestAge = meter.createObservableGauge( "autotel.event_delivery.queue.oldest_age_ms", { description: "Age of the oldest event in the queue in milliseconds", unit: "ms" } ); const oldestAgeCallback = (observableResult) => { if (this.queue.length > 0) { const oldest = this.queue[0]; const ageMs = Date.now() - oldest.timestamp; observableResult.observe(ageMs); } else { observableResult.observe(0); } }; oldestAge.addCallback(oldestAgeCallback); this.observableCleanups.push( () => oldestAge.removeCallback(oldestAgeCallback) ); const delivered = meter.createCounter( "autotel.event_delivery.queue.delivered", { description: "Number of events successfully delivered to subscribers", unit: "count" } ); const failed = meter.createCounter("autotel.event_delivery.queue.failed", { description: "Number of events that failed delivery after all retry attempts", unit: "count" }); const dropped = meter.createCounter( "autotel.event_delivery.queue.dropped", { description: "Number of events dropped from the queue", unit: "count" } ); const latency = meter.createHistogram( "autotel.event_delivery.queue.latency_ms", { description: "Event delivery latency from enqueue to successful send", unit: "ms" } ); const subscriberHealth = meter.createObservableGauge( "autotel.event_delivery.subscriber.health", { description: "Subscriber health status (1=healthy, 0=unhealthy)", unit: "1" } ); const subscriberHealthCallback = (observableResult) => { for (const [subscriberName, isHealthy] of this.subscriberHealthy) { observableResult.observe(isHealthy ? 1 : 0, { subscriber: subscriberName }); } }; subscriberHealth.addCallback(subscriberHealthCallback); this.observableCleanups.push( () => subscriberHealth.removeCallback(subscriberHealthCallback) ); this.metrics = { queueSize, oldestAge, delivered, failed, dropped, latency, subscriberHealth }; } /** * Record a dropped event with reason and emit debug breadcrumb */ recordDropped(reason, event, subscriberName) { const attrs = { reason }; if (subscriberName) { attrs.subscriber = subscriberName; } this.metrics?.dropped.add(1, attrs); const logLevel = reason === "payload_invalid" ? "error" : "warn"; const logger = getLogger(); if (logLevel === "error") { logger.error( { eventName: event?.name, subscriber: subscriberName, reason, correlationId: event?._correlationId, traceId: event?._traceId }, `[autotel] Event dropped: ${reason}` ); } else { logger.warn( { eventName: event?.name, subscriber: subscriberName, reason, correlationId: event?._correlationId, traceId: event?._traceId }, `[autotel] Event dropped: ${reason}` ); } } /** * Record permanent delivery failure (after all retries exhausted) * Increments failed counter and logs error */ recordFailed(event, subscriberName, error) { this.metrics?.failed.add(1, { subscriber: subscriberName }); this.subscriberHealthy.set(subscriberName, false); getLogger().error( { eventName: event.name, subscriber: subscriberName, correlationId: event._correlationId, traceId: event._traceId, err: error }, `[autotel] Event delivery failed after all retries` ); } /** * Mark subscriber as unhealthy on transient failure (without incrementing failed counter) * Used during retry attempts - only recordFailed should increment the counter */ markSubscriberUnhealthy(subscriberName) { this.subscriberHealthy.set(subscriberName, false); } /** * Record successful delivery */ recordDelivered(event, subscriberName, startTime) { const latencyMs = Date.now() - startTime; this.metrics?.delivered.add(1, { subscriber: subscriberName }); this.metrics?.latency.record(latencyMs, { subscriber: subscriberName }); this.subscriberHealthy.set(subscriberName, true); } /** * Enqueue an event for sending * * Backpressure policy: * - Drops oldest event and logs warning if queue is full (same behavior in all environments) */ enqueue(event) { if (this.isShuttingDown) { this.recordDropped("shutdown", event); return; } if (this.queue.length >= this.config.maxSize) { const droppedEvent = this.queue.shift(); this.recordDropped("rate_limit", droppedEvent); getLogger().warn( { droppedEvent: droppedEvent?.name }, `[autotel] Events queue full (${this.config.maxSize} events). Dropping oldest event. Events are being produced faster than they can be sent. Check your subscribers or reduce tracking frequency.` ); } const enrichedEvent = { ...event, _correlationId: event._correlationId || getOrCreateCorrelationId() }; this.queue.push(enrichedEvent); this.scheduleBatchFlush(); } /** * Schedule a batch flush if not already scheduled */ scheduleBatchFlush() { if (this.flushTimer || this.flushPromise) return; this.flushTimer = setTimeout(() => { this.flushTimer = null; void this.flushBatch(); }, this.config.flushInterval); } /** * Flush a batch of events * Uses promise-based concurrency control to prevent race conditions */ async flushBatch() { if (this.queue.length === 0) return; if (this.flushPromise) { await this.flushPromise; return; } this.flushPromise = this.doFlushBatch(); try { await this.flushPromise; } finally { this.flushPromise = null; if (this.queue.length > 0) { this.scheduleBatchFlush(); } } } /** * Internal flush implementation */ async doFlushBatch() { const batch = this.queue.splice(0, this.config.batchSize); await this.sendWithRetry(batch, this.config.maxRetries); } /** * Send events with exponential backoff retry * Tracks per-event, per-subscriber failures so failed counter reflects actual failed deliveries. * On retry, only failed (event, subscriber) pairs are re-sent to avoid double-counting delivered. */ async sendWithRetry(events, retriesLeft, subscribersByEventIndex) { const failedDeliveries = await this.sendToSubscribers( events, subscribersByEventIndex ); if (failedDeliveries.length > 0) { if (retriesLeft > 0) { const failedEventIndices = new Set( failedDeliveries.map((f) => f.eventIndex) ); const failedEventIndicesOrdered = [...failedEventIndices].sort( (a, b) => a - b ); const eventsToRetry = failedEventIndicesOrdered.map( (i) => events[i] ); const failedSubscribersByRetryIndex = /* @__PURE__ */ new Map(); for (let j = 0; j < failedEventIndicesOrdered.length; j++) { const origIndex = failedEventIndicesOrdered[j]; const set = /* @__PURE__ */ new Set(); for (const { eventIndex, subscriberName } of failedDeliveries) { if (eventIndex === origIndex) set.add(subscriberName); } failedSubscribersByRetryIndex.set(j, set); } const delay = Math.pow(2, this.config.maxRetries - retriesLeft) * 1e3; await new Promise((resolve) => setTimeout(resolve, delay)); return this.sendWithRetry( eventsToRetry, retriesLeft - 1, failedSubscribersByRetryIndex ); } else { for (const { eventIndex, subscriberName, error } of failedDeliveries) { const event = events[eventIndex]; if (event) this.recordFailed(event, subscriberName, error); } const failedSubscriberNames = [ ...new Set(failedDeliveries.map((f) => f.subscriberName)) ]; getLogger().error( { failedSubscribers: failedSubscriberNames, retriesAttempted: this.config.maxRetries }, "[autotel] Failed to send events after retries" ); } } } /** * Send events to configured subscribers with rate limiting and metrics. * When subscribersByEventIndex is provided (retry path), only those subscribers are tried per event. * Returns per-event, per-subscriber failures (empty if all succeeded). */ async sendToSubscribers(events, subscribersByEventIndex) { const failedDeliveries = []; const sendOne = async (event, eventIndex) => { const subscriberNames = subscribersByEventIndex?.get(eventIndex); const failures = await this.sendEventToSubscribers( event, subscriberNames ?? void 0 ); for (const failure of failures) { failedDeliveries.push({ eventIndex, subscriberName: failure.subscriberName, error: failure.error }); } }; if (!this.rateLimiter) { for (let i = 0; i < events.length; i++) { const event = events[i]; if (event) await sendOne(event, i); } return failedDeliveries; } for (let i = 0; i < events.length; i++) { await this.rateLimiter.waitForToken(); const event = events[i]; if (event) await sendOne(event, i); } return failedDeliveries; } /** * Send a single event to subscribers. * - When subscriberNames is undefined (initial attempt): send to all subscribers. * - When subscriberNames is provided (retry): send only to those subscribers (never re-send to healthy ones). * Returns list of subscribers that failed (empty if all succeeded). */ async sendEventToSubscribers(event, subscriberNames) { const startTime = event.timestamp; const failures = []; const subscribersToTry = subscriberNames === void 0 ? this.subscribers : this.subscribers.filter( (s) => subscriberNames.has(getSubscriberName(s)) ); const results = await Promise.allSettled( subscribersToTry.map(async (subscriber) => { const subscriberName = getSubscriberName(subscriber); try { await subscriber.trackEvent(event.name, event.attributes, { autotel: event.autotel }); this.recordDelivered(event, subscriberName, startTime); return { subscriberName, success: true }; } catch (error) { this.markSubscriberUnhealthy(subscriberName); return { subscriberName, success: false, error: error instanceof Error ? error : void 0 }; } }) ); for (const result of results) { if (result.status === "fulfilled" && !result.value.success) { failures.push({ subscriberName: result.value.subscriberName, error: result.value.error }); } } return failures; } /** * Flush all remaining events. Queue remains usable after flush (e.g. for * auto-flush at root span end). Use shutdown() when tearing down the queue. */ async flush() { if (this.flushTimer) { clearTimeout(this.flushTimer); this.flushTimer = null; } if (this.flushPromise) { await this.flushPromise; } while (this.queue.length > 0) { await this.doFlushBatch(); } } /** * Flush remaining events and permanently disable the queue (reject new events). * Use for process/SDK shutdown; use flush() for periodic or span-end drain. */ async shutdown() { this.isShuttingDown = true; await this.flush(); } /** * Cleanup observable metric callbacks to prevent memory leaks * Call this when destroying the EventQueue instance */ cleanup() { for (const cleanupFn of this.observableCleanups) { try { cleanupFn(); } catch { } } this.observableCleanups = []; } /** * Get queue size (for testing/debugging) */ size() { return this.queue.length; } /** * Get subscriber health status (for testing/debugging) */ getSubscriberHealth() { return new Map(this.subscriberHealthy); } /** * Check if a specific subscriber is healthy */ isSubscriberHealthy(subscriberName) { return this.subscriberHealthy.get(subscriberName.toLowerCase()) ?? true; } /** * Manually mark a subscriber as healthy or unhealthy * (used for circuit breaker integration) */ setSubscriberHealth(subscriberName, healthy) { this.subscriberHealthy.set(subscriberName.toLowerCase(), healthy); } }; // src/validation.ts var DEFAULT_CONFIG2 = { maxEventNameLength: 100, maxAttributeKeyLength: 100, maxAttributeValueLength: 1e3, maxAttributeCount: 50, maxNestingDepth: 3, sensitivePatterns: [ /password/i, /secret/i, /token/i, /api[_-]?key/i, /access[_-]?key/i, /private[_-]?key/i, /auth/i, /credential/i, /ssn/i, /credit[_-]?card/i ] }; var ValidationError = class extends Error { constructor(message) { super(message); this.name = "ValidationError"; } }; function validateEventName(eventName, config = DEFAULT_CONFIG2) { if (typeof eventName !== "string") { throw new ValidationError( `Event name must be a string, got ${typeof eventName}` ); } const trimmed = eventName.trim(); if (trimmed.length === 0) { throw new ValidationError("Event name cannot be empty"); } if (trimmed.length > config.maxEventNameLength) { throw new ValidationError( `Event name too long (${trimmed.length} chars). Max: ${config.maxEventNameLength}` ); } if (!/^[a-zA-Z0-9._-]+$/.test(trimmed)) { throw new ValidationError( `Event name contains invalid characters: "${trimmed}". Use only letters, numbers, dots, underscores, and hyphens.` ); } return trimmed; } function validateAttributes(attributes, config = DEFAULT_CONFIG2) { if (attributes === void 0 || attributes === null) { return void 0; } if (typeof attributes !== "object" || Array.isArray(attributes)) { throw new ValidationError("Attributes must be an object"); } const keys = Object.keys(attributes); if (keys.length > config.maxAttributeCount) { throw new ValidationError( `Too many attributes (${keys.length}). Max: ${config.maxAttributeCount}` ); } const sanitized = {}; for (const key of keys) { if (key.length > config.maxAttributeKeyLength) { throw new ValidationError( `Attribute key too long: "${key.slice(0, 20)}..." (${key.length} chars). Max: ${config.maxAttributeKeyLength}` ); } const isSensitive = config.sensitivePatterns.some( (pattern) => pattern.test(key) ); if (isSensitive) { sanitized[key] = "[REDACTED]"; continue; } const value = attributes[key]; sanitized[key] = sanitizeValue(value, config, 1); } return sanitized; } function sanitizeValue(value, config, depth) { if (depth > config.maxNestingDepth) { return "[MAX_DEPTH_EXCEEDED]"; } if (value === null || value === void 0) { return value; } if (typeof value === "string") { if (value.length > config.maxAttributeValueLength) { return value.slice(0, config.maxAttributeValueLength) + "..."; } return value; } if (typeof value === "number" || typeof value === "boolean") { return value; } if (Array.isArray(value)) { return value.map((item) => sanitizeValue(item, config, depth + 1)); } if (typeof value === "object") { try { JSON.stringify(value); const sanitized = {}; for (const key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { const isSensitive = config.sensitivePatterns.some( (pattern) => pattern.test(key) ); if (isSensitive) { sanitized[key] = "[REDACTED]"; } else { sanitized[key] = sanitizeValue( value[key], config, depth + 1 ); } } } return sanitized; } catch { return "[CIRCULAR]"; } } return `[${typeof value}]`; } function validateEvent(eventName, attributes, config) { const fullConfig = { ...DEFAULT_CONFIG2, ...config }; return { eventName: validateEventName(eventName, fullConfig), attributes: validateAttributes(attributes, fullConfig) }; } // src/track.ts var eventsQueue = null; function buildAutotelContext(span) { const eventsConfig = getEventsConfig(); const config = getConfig(); const correlationId = getOrCreateCorrelationId(); if (!eventsConfig?.includeTraceContext) { return { correlation_id: correlationId }; } const autotelContext = { correlation_id: correlationId }; const spanContext = span?.spanContext(); if (spanContext) { autotelContext.trace_id = spanContext.traceId; autotelContext.span_id = spanContext.spanId; autotelContext.trace_flags = spanContext.traceFlags.toString(16).padStart(2, "0"); const traceState = spanContext.traceState; if (traceState) { try { if (typeof traceState.serialize === "function") { const traceStateStr = traceState.serialize(); if (traceStateStr) { autotelContext.trace_state = traceStateStr; } } } catch { } } if (eventsConfig.traceUrl && config) { const traceUrl = eventsConfig.traceUrl({ traceId: spanContext.traceId, spanId: spanContext.spanId, correlationId, serviceName: config.service, environment: config.environment }); if (traceUrl) { autotelContext.trace_url = traceUrl; } } } else { if (eventsConfig.traceUrl && config) { const traceUrl = eventsConfig.traceUrl({ correlationId, serviceName: config.service, environment: config.environment }); if (traceUrl) { autotelContext.trace_url = traceUrl; } } } return autotelContext; } function getOrCreateQueue() { if (!isInitialized()) { warnIfNotInitialized("track()"); return null; } if (!eventsQueue) { const config = getConfig(); if (!config?.subscribers || config.subscribers.length === 0) { return null; } eventsQueue = new EventQueue(config.subscribers); } return eventsQueue; } function track(event, data) { const queue = getOrCreateQueue(); if (!queue) return; const validationConfig = getValidationConfig(); const validated = validateEvent(event, data, validationConfig || void 0); const span = trace.getActiveSpan(); const enrichedData = span ? { ...validated.attributes, traceId: span.spanContext().traceId, spanId: span.spanContext().spanId } : validated.attributes; const autotelContext = buildAutotelContext(span); queue.enqueue({ name: validated.eventName, attributes: enrichedData, timestamp: Date.now(), autotel: autotelContext }); } function getEventQueue() { return eventsQueue; } function resetEventQueue() { eventsQueue = null; } // src/trace-context.ts var contextStorage = new AsyncLocalStorage(); function getContextStorage() { return contextStorage; } function getActiveContextWithBaggage() { const stored = contextStorage.getStore()?.value; return stored ?? context.active(); } function enterOrRun(storage, value) { const existingStore = storage.getStore(); if (existingStore) { existingStore.value = value; return; } const boxedValue = { value }; try { storage.enterWith(boxedValue); } catch { storage.run(boxedValue, () => { }); } } function updateActiveContext(newContext) { enterOrRun(contextStorage, newContext); const contextWithManager = context; const manager = contextWithManager._getContextManager?.(); if (!manager) return; const asyncLocal = manager._asyncLocalStorage ?? void 0; if (asyncLocal?.enterWith) { asyncLocal.enterWith(newContext); return; } if (typeof manager.with === "function") { manager.with(newContext, () => { }); } } function createTraceContext(span) { const spanContext = span.spanContext(); const existingStored = contextStorage.getStore()?.value; if (!existingStored) { const activeContext = context.active(); enterOrRun(contextStorage, activeContext); } const baggageHelpers = { getBaggage(key) { const activeCtx = context.active(); let baggage = propagation.getBaggage(activeCtx); if (!baggage) { const storedContext = contextStorage.getStore()?.value; if (storedContext) { baggage = propagation.getBaggage(storedContext); } } return baggage?.getEntry(key)?.value; }, setBaggage(key, value) { const activeCtx = context.active(); const storedContext = contextStorage.getStore()?.value; const currentContext = storedContext ?? activeCtx; const baggage = propagation.getBaggage(currentContext) ?? propagation.createBaggage(); const updated = baggage.setEntry(key, { value }); const newContext = propagation.setBaggage(currentContext, updated); updateActiveContext(newContext); return value; }, deleteBaggage(key) { const activeCtx = context.active(); const storedContext = contextStorage.getStore()?.value; const currentContext = storedContext ?? activeCtx; const baggage = propagation.getBaggage(currentContext); if (baggage) { const updated = baggage.removeEntry(key); const newContext = propagation.setBaggage(currentContext, updated); updateActiveContext(newContext); } }, getAllBaggage() { const activeCtx = context.active(); let baggage = propagation.getBaggage(activeCtx); if (!baggage) { const storedContext = contextStorage.getStore()?.value; if (storedContext) { baggage = propagation.getBaggage(storedContext); } } if (!baggage) { return /* @__PURE__ */ new Map(); } const entries = /* @__PURE__ */ new Map(); for (const [key, entry] of baggage.getAllEntries()) { entries.set(key, entry); } return entries; }, // Typed baggage helpers (used by defineBaggageSchema) getTypedBaggage: ((namespace) => { const activeCtx = context.active(); let baggage = propagation.getBaggage(activeCtx); if (!baggage) { const storedContext = contextStorage.getStore()?.value; if (storedContext) { baggage = propagation.getBaggage(storedContext); } } if (!baggage) return; const prefix = namespace ? `${namespace}.` : ""; const result = {}; for (const [key, entry] of baggage.getAllEntries()) { if (namespace && key.startsWith(prefix)) { const fieldName = key.slice(prefix.length); result[fieldName] = entry.value; } else if (!namespace) { result[key] = entry.value; } } return Object.keys(result).length > 0 ? result : void 0; }), setTypedBaggage: ((namespace, value) => { const activeCtx = context.active(); const storedContext = contextStorage.getStore()?.value; const currentContext = storedContext ?? activeCtx; let baggage = propagation.getBaggage(currentContext) ?? propagation.createBaggage(); const prefix = namespace ? `${namespace}.` : ""; for (const [key, val] of Object.entries(value)) { if (val !== void 0) { const baggageKey = `${prefix}${key}`; baggage = baggage.setEntry(baggageKey, { value: String(val) }); } } const newContext = propagation.setBaggage(currentContext, baggage); updateActiveContext(newContext); }) }; const traceCtx = { traceId: spanContext.traceId, spanId: spanContext.spanId, correlationId: spanContext.traceId.slice(0, 16), setAttribute: span.setAttribute.bind(span), setAttributes: span.setAttributes.bind(span), setStatus: span.setStatus.bind(span), recordException: span.recordException.bind(span), addEvent: span.addEvent.bind(span), addLink: span.addLink.bind(span), addLinks: span.addLinks.bind(span), updateName: span.updateName.bind(span), isRecording: span.isRecording.bind(span), recordError: (error) => { const err = error instanceof Error ? error : new Error(String(error)); recordStructuredError(traceCtx, err); }, track: (event, data) => { track(event, data); }, ...baggageHelpers }; return traceCtx; } function defineBaggageSchema(namespace) { return { /** * Get typed baggage from context * @param ctx - Trace context * @returns Partial baggage object or undefined if no baggage is set */ get: (ctx) => { if (!ctx.getTypedBaggage) return void 0; return ctx.getTypedBaggage(namespace); }, /** * Set typed baggage in context * * Note: For proper scoping across async boundaries, use the `with` method instead * * @param ctx - Trace context * @param value - Partial baggage object to set */ set: (ctx, value) => { if (!ctx.setTypedBaggage) return; ctx.setTypedBaggage(namespace, value); }, /** * Run a function with typed baggage properly scoped * * This is the recommended way to set baggage as it ensures proper * scoping across async boundaries. * * @param ctx - Trace context (can be omitted, will use active context) * @param value - Partial baggage object to set * @param fn - Function to execute with the baggage */ with: (ctxOrValue, valueOrFn, maybeFn) => { const value = maybeFn ? valueOrFn : ctxOrValue; const fn = maybeFn || valueOrFn; const prefix = namespace ? `${namespace}.` : ""; const flatBaggage = {}; for (const [key, val] of Object.entries(value)) { if (val !== void 0) { flatBaggage[`${prefix}${key}`] = String(val); } } const currentContext = context.active(); let baggage = propagation.getBaggage(currentContext) ?? propagation.createBaggage(); for (const [key, val] of Object.entries(flatBaggage)) { baggage = baggage.setEntry(key, { value: val }); } const newContext = propagation.setBaggage(currentContext, baggage); return context.with(newContext, fn); } }; } // src/correlation-id.ts var correlationStorage = new AsyncLocalStorage(); var CORRELATION_ID_BAGGAGE_KEY = "autotel.correlation_id"; function generateCorrelationId() { const bytes = new Uint8Array(8); crypto.getRandomValues(bytes); return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join(""); } function getCorrelationId() { const storedId = correlationStorage.getStore()?.value; if (storedId) { return storedId; } const activeContext = context.active(); const baggage = propagation.getBaggage(activeContext); const baggageEntry = baggage?.getEntry(CORRELATION_ID_BAGGAGE_KEY); if (baggageEntry?.value) { return baggageEntry.value; } const span = trace.getActiveSpan(); if (span) { const spanContext = span.spanContext(); return spanContext.traceId.slice(0, 16); } return void 0; } function getOrCreateCorrelationId() { return getCorrelationId() ?? generateCorrelationId(); } function runWithCorrelationId(correlationId, fn) { return correlationStorage.run({ value: correlationId }, fn); } function setCorrelationId(correlationId) { enterOrRun(correlationStorage, correlationId); } function setCorrelationIdInBaggage(correlationId) { 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); } function getCorrelationStorage() { return correlationStorage; } export { CORRELATION_ID_BAGGAGE_KEY, createStructuredError, createTraceContext, defineBaggageSchema, enterOrRun, flattenToAttributes, generateCorrelationId, getActiveContextWithBaggage, getContextStorage, getCorrelationId, getCorrelationStorage, getEventQueue, getOrCreateCorrelationId, getStructuredErrorAttributes, recordStructuredError, resetEventQueue, runWithCorrelationId, setCorrelationId, setCorrelationIdInBaggage, structuredErrorToJSON, toAttributeValue, track, validateEvent }; //# sourceMappingURL=chunk-Z7VAOK5X.js.map //# sourceMappingURL=chunk-Z7VAOK5X.js.map