autotel
Version:
Write Once, Observe Anywhere
4,655 lines • 162 kB
JavaScript
const require_rolldown_runtime = require('./rolldown-runtime-C_NdSu1c.cjs');
const require_values = require('./values-tm4bi0FF.cjs');
const require_tracer_provider = require('./tracer-provider.cjs');
const require_config = require('./config.cjs');
const require_structured_error = require('./structured-error-BAXhglRE.cjs');
const require_node_require = require('./node-require-B_IVapV3.cjs');
const require_filtering_span_processor = require('./filtering-span-processor.cjs');
const require_policy = require('./policy.cjs');
const require_span_name_normalizer = require('./span-name-normalizer.cjs');
const require_attribute_redacting_processor = require('./attribute-redacting-processor.cjs');
const require_pretty_console_exporter = require('./pretty-console-exporter-CMzlrRNg.cjs');
const require_canonical_log_line_processor = require('./canonical-log-line-processor-Bvgb1lE4.cjs');
const require_metric = require('./metric.cjs');
let _opentelemetry_sdk_node = require("@opentelemetry/sdk-node");
let _opentelemetry_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
let _opentelemetry_resources = require("@opentelemetry/resources");
let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
let _opentelemetry_api = require("@opentelemetry/api");
let node_async_hooks = require("node:async_hooks");
node_async_hooks = require_rolldown_runtime.__toESM(node_async_hooks, 1);
let _opentelemetry_sdk_metrics = require("@opentelemetry/sdk-metrics");
let _opentelemetry_sdk_logs = require("@opentelemetry/sdk-logs");
let node_fs = require("node:fs");
node_fs = require_rolldown_runtime.__toESM(node_fs, 1);
let node_path = require("node:path");
node_path = require_rolldown_runtime.__toESM(node_path, 1);
let node_os = require("node:os");
let _opentelemetry_exporter_metrics_otlp_http = require("@opentelemetry/exporter-metrics-otlp-http");
let _opentelemetry_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
let _opentelemetry_exporter_logs_otlp_http = require("@opentelemetry/exporter-logs-otlp-http");
//#region src/rate-limiter.ts
/**
* Token bucket rate limiter
*
* Allows bursts up to burstCapacity, then smooths to maxEventsPerSecond.
* Thread-safe for async operations.
*/
var TokenBucketRateLimiter = class {
tokens;
maxTokens;
refillRate;
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 tokensToAdd = (now - this.lastRefill) * 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();
}
};
//#endregion
//#region src/correlation-id.ts
/**
* Correlation ID utilities for event-driven observability
*
* Provides a stable join key across events, logs, and spans even when traces fragment.
* Format: 16 hex chars (64 bits), crypto-random, URL-safe.
*
* Lifecycle:
* 1. Generated at boundary root (HTTP server span, message process span, cron job span)
* 2. Reused within context (nested work shares it via AsyncLocalStorage)
* 3. Propagated via baggage (optional, default OFF to avoid header bloat)
*
* @example Basic usage
* ```typescript
* import { generateCorrelationId, getCorrelationId } from 'autotel/correlation-id';
*
* // Generate a new correlation ID
* const id = generateCorrelationId();
* // Returns: 'a1b2c3d4e5f67890'
*
* // Get current correlation ID from context
* const currentId = getCorrelationId();
* ```
*/
/**
* AsyncLocalStorage for storing correlation ID
* This allows correlation IDs to persist across async boundaries
*/
const correlationStorage = new node_async_hooks.AsyncLocalStorage();
/**
* Baggage key for correlation ID propagation
*/
const CORRELATION_ID_BAGGAGE_KEY = "autotel.correlation_id";
/**
* Generate a new correlation ID
*
* Format: 16 hex chars (64 bits), crypto-random, URL-safe
*
* @returns A new correlation ID
*
* @example
* ```typescript
* const id = generateCorrelationId();
* // Returns: 'a1b2c3d4e5f67890'
* ```
*/
function generateCorrelationId() {
const bytes = /* @__PURE__ */ new Uint8Array(8);
crypto.getRandomValues(bytes);
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
}
/**
* Get the current correlation ID from context
*
* Resolution order:
* 1. AsyncLocalStorage (from explicit setCorrelationId or runWithCorrelationId)
* 2. Baggage (if propagated from upstream)
* 3. Active span's trace ID (first 16 chars as fallback)
* 4. undefined (if not in any context)
*
* @returns Current correlation ID or undefined
*
* @example
* ```typescript
* const id = getCorrelationId();
* if (id) {
* console.log('Correlation ID:', id);
* }
* ```
*/
function getCorrelationId() {
const storedId = correlationStorage.getStore()?.value;
if (storedId) return storedId;
const activeContext = _opentelemetry_api.context.active();
const baggageEntry = _opentelemetry_api.propagation.getBaggage(activeContext)?.getEntry(CORRELATION_ID_BAGGAGE_KEY);
if (baggageEntry?.value) return baggageEntry.value;
const span = _opentelemetry_api.trace.getActiveSpan();
if (span) return span.spanContext().traceId.slice(0, 16);
}
/**
* Get or create a correlation ID
*
* If a correlation ID exists in the current context, returns it.
* Otherwise, generates a new one.
*
* @returns Existing or new correlation ID
*
* @example
* ```typescript
* const id = getOrCreateCorrelationId();
* // Always returns a valid correlation ID
* ```
*/
function getOrCreateCorrelationId() {
return getCorrelationId() ?? generateCorrelationId();
}
/**
* Run a function with a specific correlation ID in context
*
* The correlation ID will be available via getCorrelationId() throughout
* the execution of the function and any async operations it spawns.
*
* @param correlationId - Correlation ID to use
* @param fn - Function to execute
* @returns The return value of the function
*
* @example
* ```typescript
* await runWithCorrelationId('abc123', async () => {
* // getCorrelationId() returns 'abc123' here
* await processRequest();
* });
* ```
*/
function runWithCorrelationId(correlationId, fn) {
return correlationStorage.run({ value: correlationId }, fn);
}
/**
* Set correlation ID in the current context (mutates context)
*
* Note: This updates the AsyncLocalStorage context. For proper scoping
* across async boundaries, prefer runWithCorrelationId() instead.
*
* @param correlationId - Correlation ID to set
*
* @example
* ```typescript
* setCorrelationId('abc123');
* // Now getCorrelationId() returns 'abc123'
* ```
*/
function setCorrelationId(correlationId) {
enterOrRun(correlationStorage, correlationId);
}
/**
* Set correlation ID in baggage for propagation
*
* This adds the correlation ID to the W3C baggage header, allowing it
* to be propagated to downstream services.
*
* Note: Only use this when you explicitly want cross-service propagation.
* Default is OFF to avoid header bloat.
*
* @param correlationId - Correlation ID to propagate
* @returns New context with baggage set
*
* @example
* ```typescript
* const newContext = setCorrelationIdInBaggage('abc123');
* context.with(newContext, () => {
* // Baggage will be propagated in outgoing requests
* });
* ```
*/
function setCorrelationIdInBaggage(correlationId) {
const activeContext = _opentelemetry_api.context.active();
let baggage = _opentelemetry_api.propagation.getBaggage(activeContext) ?? _opentelemetry_api.propagation.createBaggage();
baggage = baggage.setEntry(CORRELATION_ID_BAGGAGE_KEY, { value: correlationId });
return _opentelemetry_api.propagation.setBaggage(activeContext, baggage);
}
/**
* Get the correlation storage instance (for internal use in init/shutdown)
*/
function getCorrelationStorage() {
return correlationStorage;
}
//#endregion
//#region src/event-queue.ts
const DEFAULT_CONFIG$2 = {
maxSize: 5e4,
batchSize: 100,
flushInterval: 1e4,
maxRetries: 3,
rateLimit: {
maxEventsPerSecond: 100,
burstCapacity: 200
}
};
/**
* Get subscriber name for metrics (stable, low-cardinality)
*
* Priority:
* 1. Explicit config: subscriber.name
* 2. Class static property (if available)
* 3. Fallback: lowercase class name without "Subscriber" suffix
*/
function getSubscriberName(subscriber) {
if (subscriber.name) return subscriber.name.toLowerCase();
return (subscriber.constructor?.name || "unknown").replace(/Subscriber$/i, "").toLowerCase();
}
/**
* Subscribers whose `shutdown()` has run. Terminal for the client they wrap, so
* a queue rebuilt from the same config must not reuse them.
*/
const shutDownSubscribers = /* @__PURE__ */ new WeakSet();
/**
* Events queue with batching and backpressure
*
* Features:
* - Batches events for efficient sending
* - Bounded queue with drop-oldest policy (prod) or blocking (dev)
* - Exponential backoff retry
* - Rate limiting to prevent overwhelming subscribers
* - Graceful flush on shutdown
*/
var EventQueue = class {
queue = [];
flushTimer = null;
config;
subscribers;
rateLimiter;
flushPromise = null;
isShuttingDown = false;
metrics = null;
observableCleanups = [];
subscriberHealthy = /* @__PURE__ */ new Map();
constructor(subscribers, config) {
const live = subscribers.filter((s) => !shutDownSubscribers.has(s));
if (live.length < subscribers.length) getLogger().warn({ subscribers: subscribers.filter((s) => shutDownSubscribers.has(s)).map((s) => getSubscriberName(s)) }, "[autotel] Subscriber already shut down, events will not be delivered; pass new subscriber instances to init()");
this.subscribers = live;
this.config = {
...DEFAULT_CONFIG$2,
...config
};
this.rateLimiter = this.config.rateLimit ? new TokenBucketRateLimiter(this.config.rateLimit) : null;
for (const subscriber of this.subscribers) {
const name = getSubscriberName(subscriber);
this.subscriberHealthy.set(name, true);
}
this.initMetrics();
}
/**
* Initialize OTel metrics for queue observability
*/
initMetrics() {
const meter = require_config.getConfig().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;
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 failedEventIndicesOrdered = [...new Set(failedDeliveries.map((f) => f.eventIndex))].toSorted((a, b) => a - b);
const eventsToRetry = failedEventIndicesOrdered.map((i) => events[i]);
const failedSubscribersByRetryIndex = /* @__PURE__ */ new Map();
for (const [j, origIndex] of failedEventIndicesOrdered.entries()) {
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 (const [i, event] of events.entries()) if (event) await sendOne(event, i);
return failedDeliveries;
}
for (const [i, event] of events.entries()) {
await this.rateLimiter.waitForToken();
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,
schema: event.schema
});
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();
const drains = await Promise.allSettled(this.subscribers.map(async (subscriber) => {
if (!subscriber.shutdown) return;
shutDownSubscribers.add(subscriber);
return subscriber.shutdown();
}));
for (const [index, drain] of drains.entries()) {
if (drain.status !== "rejected") continue;
const subscriberName = getSubscriberName(this.subscribers[index]);
this.subscriberHealthy.set(subscriberName, false);
getLogger().error({
subscriber: subscriberName,
err: drain.reason
}, "[autotel] Subscriber shutdown failed, buffered events may be lost");
}
}
/**
* 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);
}
};
//#endregion
//#region src/validation.ts
const DEFAULT_CONFIG$1 = {
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";
}
};
/**
* Validate and sanitize event name
* Throws ValidationError if invalid
*/
function validateEventName(eventName, config = DEFAULT_CONFIG$1) {
if (require_values.asString(eventName) === void 0) throw new ValidationError(`Event name must be a string, got ${require_values.describeValue(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;
}
/**
* Validate and sanitize attributes
* Returns sanitized attributes (sensitive data redacted)
*/
function validateAttributes(attributes, config = DEFAULT_CONFIG$1) {
if (attributes === void 0 || attributes === null) return;
if (!require_values.asRecord(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 value = attributes[key];
if (isSensitiveString(key, value, config)) {
sanitized[key] = "[REDACTED]";
continue;
}
sanitized[key] = sanitizeValue(value, config, 1);
}
return sanitized;
}
/**
* Whether this key/value pair is a credential to redact. Strings only:
* numeric and boolean values are not credentials, and replacing them with the
* literal '[REDACTED]' leaks no signal while breaking every consumer that
* treats them as a number.
*/
function isSensitiveString(key, value, config) {
return require_values.asString(value) !== void 0 && config.sensitivePatterns.some((pattern) => pattern.test(key));
}
/**
* Sanitize attribute value (recursive)
*/
function sanitizeValue(value, config, depth) {
if (depth > config.maxNestingDepth) return "[MAX_DEPTH_EXCEEDED]";
if (value === null || value === void 0) return value;
const text = require_values.asString(value);
if (text !== void 0) return text.length > config.maxAttributeValueLength ? text.slice(0, config.maxAttributeValueLength) + "..." : text;
const scalar = require_values.asNumber(value) ?? require_values.asBoolean(value);
if (scalar !== void 0) return scalar;
if (Array.isArray(value)) return value.map((item) => sanitizeValue(item, config, depth + 1));
const record = require_values.asRecord(value);
if (record) try {
JSON.stringify(record);
const sanitized = {};
for (const [key, nested] of Object.entries(record)) sanitized[key] = isSensitiveString(key, nested, config) ? "[REDACTED]" : sanitizeValue(nested, config, depth + 1);
return sanitized;
} catch {
return "[CIRCULAR]";
}
return `[${require_values.describeValue(value)}]`;
}
/**
* Validate and sanitize an events event
* Returns { eventName, attributes } with sanitized values
*/
function validateEvent(eventName, attributes, config) {
const fullConfig = {
...DEFAULT_CONFIG$1,
...config
};
return {
eventName: validateEventName(eventName, fullConfig),
attributes: validateAttributes(attributes, fullConfig)
};
}
//#endregion
//#region src/track.ts
/**
* Global track() function for business events
*
* Simple, no instantiation needed, auto-attaches trace context
*/
let eventsQueue = null;
let warnedNoSubscribers = false;
/**
* Build autotel event context for trace correlation
*
* Works in multiple contexts:
* 1. Inside a span → use current span's trace_id + span_id
* 2. Outside span → use correlation_id only
* 3. With trace URL config → include clickable trace URL
*/
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;
}
/**
* Initialize events queue lazily
*/
function getOrCreateQueue() {
if (!isInitialized()) {
warnIfNotInitialized("track()");
return null;
}
if (!eventsQueue) {
const config = getConfig();
if (!config?.subscribers || config.subscribers.length === 0) {
if (!warnedNoSubscribers) {
warnedNoSubscribers = true;
getLogger().warn({}, "[autotel] track() dropped an event: init() configured no subscribers. Events (including gen_ai.evaluation.result and audit events) reach a backend through init({ subscribers: [...] }).");
}
return null;
}
eventsQueue = new EventQueue(config.subscribers);
}
return eventsQueue;
}
/**
* Track a business events event
*
* Features:
* - Auto-attaches traceId and spanId if in active span
* - Batched sending with retry
* - Type-safe with optional generic
* - No-op if init() not called or no subscribers configured
*
* @example Basic usage
* ```typescript
* track('user.signup', { userId: '123', plan: 'pro' })
* ```
*
* @example With type safety
* ```typescript
* interface EventDatas {
* 'user.signup': { userId: string; plan: string }
* 'plan.upgraded': { userId: string; revenue: number }
* }
*
* track<EventDatas>('user.signup', { userId: '123', plan: 'pro' })
* ```
*
* @example Trace correlation (automatic)
* ```typescript
* @Instrumented()
* class UserService {
* async createUser(data: CreateUserData) {
* // This track call automatically includes traceId + spanId
* track('user.signup', { userId: data.id })
* }
* }
* ```
*/
function track(event, data, options) {
const queue = getOrCreateQueue();
if (!queue) return;
const validated = validateEvent(event, data, getValidationConfig() || void 0);
const span = _opentelemetry_api.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,
schema: options?.schema
});
}
/**
* Get events queue (for flush/shutdown)
* @internal
*/
function getEventQueue() {
return eventsQueue;
}
/**
* Reset events queue (for shutdown/cleanup)
* @internal
*/
function resetEventQueue() {
eventsQueue = null;
warnedNoSubscribers = false;
}
//#endregion
//#region src/trace-context.ts
const spansWithExplicitStatus = /* @__PURE__ */ new WeakSet();
/** Whether a TraceContext consumer explicitly chose this span's status. */
function hasExplicitSpanStatus(span) {
return spansWithExplicitStatus.has(span);
}
/**
* AsyncLocalStorage for storing the active context with baggage
* This allows setters to update the context and have it persist
*/
const contextStorage = new node_async_hooks.AsyncLocalStorage();
/**
* Get the context storage instance (for initialization in functional.ts)
*/
function getContextStorage() {
return contextStorage;
}
/**
* Get the active OTel context with the latest stored baggage overlaid.
* Span identity always comes from the active OTel scope.
*/
function getActiveContextWithBaggage() {
const activeContext = _opentelemetry_api.context.active();
const stored = contextStorage.getStore()?.value;
if (!stored) return activeContext;
const storedBaggage = _opentelemetry_api.propagation.getBaggage(stored);
return storedBaggage ? _opentelemetry_api.propagation.setBaggage(activeContext, storedBaggage) : activeContext;
}
/**
* Set a value in AsyncLocalStorage, preferring enterWith() when available
* (Node.js) and falling back to run() for environments that only support
* run() (e.g. Cloudflare Workers).
*
* On runtimes without enterWith() we mutate the existing run() scope when one
* exists. This is what allows baggage/correlation updates to remain visible
* for the rest of the traced callback in Workers.
*/
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 manager = _opentelemetry_api.context._getContextManager?.();
if (!manager) return;
const asyncLocal = manager._asyncLocalStorage ?? void 0;
if (asyncLocal?.enterWith) {
asyncLocal.enterWith(newContext);
return;
}
if (require_values.isFunction(manager.with)) manager.with(newContext, () => {});
}
/**
* Create a TraceContext from an OpenTelemetry Span
*
* This utility extracts trace context information from a span
* and provides span manipulation methods and baggage operations in a consistent format.
*
* Note: Baggage methods always operate on the currently active context,
* which may differ from the context when createTraceContext was called.
*/
function createTraceContext(span) {
const spanContext = span.spanContext();
if (!contextStorage.getStore()?.value) {
const activeContext = _opentelemetry_api.context.active();
enterOrRun(contextStorage, activeContext);
}
const traceCtx = {
traceId: spanContext.traceId,
spanId: spanContext.spanId,
correlationId: spanContext.traceId.slice(0, 16),
setAttribute: (key, value) => {
const attr = require_structured_error.toAttributeValue(value);
if (attr === void 0) {
span.setAttributes(require_structured_error.flattenToAttributes({ [key]: value }));
return;
}
span.setAttribute(key, attr);
},
setAttributes: (attrs) => {
span.setAttributes(require_structured_error.flattenToAttributes(attrs));
},
setStatus: (status) => {
spansWithExplicitStatus.add(span);
span.setStatus(status);
},
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: (cause) => {
const err = cause instanceof Error ? cause : new Error(String(cause));
require_structured_error.recordStructuredError(traceCtx, err);
},
track: (event, data) => {
track(event, data);
},
getBaggage(key) {
const activeCtx = _opentelemetry_api.context.active();
let baggage = _opentelemetry_api.propagation.getBaggage(activeCtx);
if (!baggage) {
const storedContext = contextStorage.getStore()?.value;
if (storedContext) baggage = _opentelemetry_api.propagation.getBaggage(storedContext);
}
return baggage?.getEntry(key)?.value;
},
setBaggage(key, value) {
const currentContext = getActiveContextWithBaggage();
const updated = (_opentelemetry_api.propagation.getBaggage(currentContext) ?? _opentelemetry_api.propagation.createBaggage()).setEntry(key, { value });
updateActiveContext(_opentelemetry_api.propagation.setBaggage(currentContext, updated));
return value;
},
deleteBaggage(key) {
const currentContext = getActiveContextWithBaggage();
const baggage = _opentelemetry_api.propagation.getBaggage(currentContext);
if (baggage) {
const updated = baggage.removeEntry(key);
updateActiveContext(_opentelemetry_api.propagation.setBaggage(currentContext, updated));
}
},
getAllBaggage() {
const activeCtx = _opentelemetry_api.context.active();
let baggage = _opentelemetry_api.propagation.getBaggage(activeCtx);
if (!baggage) {
const storedContext = contextStorage.getStore()?.value;
if (storedContext) baggage = _opentelemetry_api.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;
},
getTypedBaggage: ((namespace) => {
const activeCtx = _opentelemetry_api.context.active();
let baggage = _opentelemetry_api.propagation.getBaggage(activeCtx);
if (!baggage) {
const storedContext = contextStorage.getStore()?.value;
if (storedContext) baggage = _opentelemetry_api.propagation.getBaggage(storedContext);
}
if (!baggage) return;
const prefix = namespace ? `${namespace}.` : "";
const entries = [];
for (const [key, entry] of baggage.getAllEntries()) if (namespace && key.startsWith(prefix)) entries.push([key.slice(prefix.length), entry.value]);
else if (!namespace) entries.push([key, entry.value]);
const result = Object.fromEntries(entries);
return entries.length > 0 ? result : void 0;
}),
setTypedBaggage: ((namespace, value) => {
const currentContext = getActiveContextWithBaggage();
let baggage = _opentelemetry_api.propagation.getBaggage(currentContext) ?? _opentelemetry_api.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) });
}
updateActiveContext(_opentelemetry_api.propagation.setBaggage(currentContext, baggage));
})
};
return traceCtx;
}
/**
* Define a typed baggage schema for type-safe baggage operations
*
* This helper provides a type-safe API for working with baggage entries.
* The namespace parameter is optional and prefixes all keys to avoid collisions.
*
* @template T - The baggage schema type (all fields are treated as optional)
* @param namespace - Optional namespace to prefix baggage keys
*
* @example Basic usage
* ```typescript
* type TenantBaggage = { tenantId: string; region?: string };
* const tenantBaggage = defineBaggageSchema<TenantBaggage>('tenant');
*
* export const handler = trace<TenantBaggage>((ctx) => async () => {
* // Get typed baggage
* const tenant = tenantBaggage.get(ctx);
* if (tenant?.tenantId) {
* console.log('Tenant:', tenant.tenantId);
* }
*
* // Set typed baggage
* tenantBaggage.set(ctx, { tenantId: 't1', region: 'us-east-1' });
* });
* ```
*
* @example With withBaggage helper
* ```typescript
* const tenantBaggage = defineBaggageSchema<TenantBaggage>('tenant');
*
* export const handler = trace<TenantBaggage>((ctx) => async () => {
* return await tenantBaggage.with(ctx, { tenantId: 't1' }, async () => {
* // Baggage is available here and in child spans
* const tenant = tenantBaggage.get(ctx);
* });
* });
* ```
*/
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 = _opentelemetry_api.context.active();
let baggage = _opentelemetry_api.propagation.getBaggage(currentContext) ?? _opentelemetry_api.propagation.createBaggage();
for (const [key, val] of Object.entries(flatBaggage)) baggage = baggage.setEntry(key, { value: val });
const newContext = _opentelemetry_api.propagation.setBaggage(currentContext, baggage);
return _opentelemetry_api.context.with(newContext, fn);
}
};
}
//#endregion
//#region src/sampling.ts
/**
* Tail sampling attribute keys (autotel-internal, not OTel semconv)
*/
const AUTOTEL_SAMPLING_TAIL_KEEP = "autotel.sampling.tail.keep";
const AUTOTEL_SAMPLING_TAIL_EVALUATED = "autotel.sampling.tail.evaluated";
/**
* How many events each kept event stands for, expressed as "1 in N".
*
* A query that counts sampled spans undercounts the population. Multiplying
* each kept event by this rate restores the estimate. Autotel records the
* attribute only when N exceeds 1, so fully captured spans stay clean.
*/
const AUTOTEL_SAMPLING_RATE = "autotel.sampling.rate";
/**
* Spans an explicit `forceKeep()` claimed.
*
* The tracing wrapper writes the sampler's tail verdict once the body has
* run, which is after any `forceKeep()` inside it. Without this the verdict
* overwrites the override and the span the caller insisted on is dropped.
*/
/**
* Baggage key that turns on full-fidelity capture for a request.
*
* Baggage arrives on the request and propagates, so a gateway, a proxy, a
* feature flag or a curl can turn this on for one user and it follows them
* across every service. Nobody deploys anything to debug a live problem.
*/
const AUTOTEL_DEBUG_BAGGAGE_KEY = "autotel.debug";
const forceKeptSpans = /* @__PURE__ */ new WeakSet();
function markForceKept(span) {
forceKeptSpans.add(span);
}
function isForceKept(span) {
return forceKeptSpans.has(span);
}
/**
* Convert a keep probability into the "1 in N" rate reported on spans.
*
* Keeping 10% of traces means each survivor stands for 10, so the two numbers
* are reciprocals and easy to publish the wrong way round.
*/
function toSampleRate(probability) {
return probability > 0 ? 1 / probability : 0;
}
/**
* Map a string to a stable, evenly spread position in the unit interval.
*
* Two processes that hash the same key reach the same number, which is what
* lets independent services agree on one trace's sampling decision.
*
* The spread matters as much as the stability. Real sampling keys share long
* prefixes: `user_1000`, `user_1001`, `checkout-trace-0001`. A plain
* multiply-and-add hash lets that shared prefix dominate the high bits, so a
* whole key family lands in one narrow band and a rate of 0.1 keeps all of
* them or none of them. FNV-1a followed by the murmur3 finalizer mixes the
* low bits back through the word, so keys that differ in one character land
* far apart.
*/
function hashUnitInterval(value) {
let hash = 2166136261;
for (let i = 0; i < value.length; i++) {
hash ^= value.codePointAt(i) ?? 0;
hash = Math.imul(hash, 16777619);
}
hash ^= hash >>> 16;
hash = Math.imul(hash, 2246822507);
hash ^= hash >>> 13;
hash = Math.imul(hash, 3266489909);
hash ^= hash >>> 16;
return (hash >>> 0) / 4294967296;
}
/**
* Simple random sampler
*
* @example
* ```typescript
* new RandomSampler(0.1) // Sample 10% of requests
* ```
*/
var RandomSampler = class {
rate;
constructor(rate) {
this.rate = rate;
if (rate < 0 || rate > 1) throw new Error("Sample rate must be between 0 and 1");
}
shouldSample(_context) {
return Math.random() < this.rate;
}
sampleRate() {
return toSampleRate(this.rate);
}
};
/**
* Always sample (100% tracing)
*/
var AlwaysSampler = class {
shouldSample(_context) {
return true;
}
};
/**
* Never sample (0% tracing)
*/
var NeverSampler = class {
shouldSample(_context) {
return false;
}
};
/**
* Adaptive sampler that always traces errors and slow requests
*
* This is the recommended sampler for production use.
* It ensures you never miss critical issues while keeping costs down.
*
* Strategy:
* - Always trace errors (critical for debugging)
* - Always trace slow requests (performance issues)
* - Use baseline sample rate for successful fast requests
*
* **IMPORTANT - Tail Sampling Requirement:**
* This sampler uses tail sampling (makes decisions AFTER execution).
* You MUST use TailSamplingSpanProcessor for it to work correctly:
*
* - If using initInstrumentation(): TailSamplingSpanProcessor is auto-configured
* - If using custom TracerProvider: You MUST manually register TailSamplingSpanProcessor
*
* Without TailSamplingSpanProcessor, ALL spans are exported (defeating the cost savings).
*
* @see TailSamplingSpanProcessor
* @see README.md "Tail Sampling with Custom Providers" section
*
* @example
* ```typescript
* new AdaptiveSampler({
* baselineSampleRate: 0.1, // 10% of normal requests
* slowThresholdMs: 1000, // Requests > 1s are "slow"
* alwaysSampleErrors: true, // Always trace errors
* alwaysSampleSlow: true // Always trace slow requests
* })
* ```
*/
var AdaptiveSampler = class {
baselineSampleRate;
slowThresholdMs;
alwaysSampleErrors;
alwaysSampleSlow;
linksBased;
linksRate;
logger;
samplingDecisions = /* @__PURE__ */ new WeakMap();
operationResults = /* @__PURE__ */ new WeakMap();
constructor(options = {}) {
this.baselineSampleRate = options.baselineSampleRate ?? .1;
this.slowThresholdMs = options.slowThresholdMs ?? 1e3;
this.alwaysSampleErrors = options.alwaysSampleErrors ?? true;
this.alwaysSampleSlow = options.alwaysSampleSlow ?? true;
this.linksBased = options.linksBased ?? false;
this.linksRate = options.linksRate ?? 1;
this.logger = options.logger;
if (this.baselineSampleRate < 0 || this.baselineSampleRate > 1) throw new Error("Baseline sample rate must be between 0 and 1");
if (this.linksRate < 0 || this.linksRate > 1) throw new Error("Links rate must be between 0 and 1");
}
needsTailSampling() {
return true;
}
shouldSample(context) {
const baselineDecision = Math.random() < this.baselineSampleRate;
this.samplingDecisions.set(context.args, baselineDecision);
return true;
}
/**
* Check if any links point to sampled spans.
*
* A span is considered linked to a sampled span if any of its links
* have trace_flags with the sampled bit set (0x01).
*
* @param links - Array of span links to check
* @returns true if any linked span is sampled, false otherwise
*/
hasSampledLink(links) {
if (!links || links.length === 0) return false;
return links.some((link) => link.context && (link.context.traceFlags & _opentelemetry_api.TraceFlags.SAMPLED) !== 0);
}
/**
* Re-evaluate sampling decision after operation completes
*
* This allows us to always capture errors and slow requests,
* even if they weren't initially sampled.
*
* @param context - Sampling context
* @param result - Operation result
* @returns true if this operation should be kept (not discarded)
*/
shouldKeepTrace(context, result) {
const baselineDecision = this.samplingDecisions.get(context.args) ?? false;
if (this.alwaysSampleErrors && !result.success) {
if (!baselineDecision) this.logger?.debug({
operation: context.operationName,
error: result.error?.message
}, "Adaptive sampling: Keeping error trace");
return true;
}
if (this.alwaysSampleSlow && result.duration >= this.slowThresholdMs) {
if (!baselineDecision) this.logger?.debug({
operation: context.operationName,
duration: result.duration
}, "Adaptive sampling: Keeping slow trace");
return true;
}
if (this.linksBased && context.links && this.hasSampledLink(context.links)) {
const keepLinked = Math.random() < this.linksRate;
if (keepLinked && !baselineDecision) this.logger?.debug({
operation: context.operationName,
linkCount: context.links.length
}, "Adaptive sampling: Keeping trace due to sampled link");
return keepLinked;
}
return baselineDecision;
}
};
/**
* User-based sampler for consistent tracing
*
* Always samples requests from specific user IDs.
* Useful for debugging specific user issues or monitoring VIP users.
*
* @example
* ```typescript
* new UserIdSampler({
* baselineSampleRate: 0.01, // 1% of normal users
* alwaysSampleUsers: ['vip_123'], // Always trace VIP users
* extractUserId: (args) => args[0]?.userId // Extract user ID from first arg
* })
* ```
*/
var UserIdSampler = class {
baselineSampleRate;
alwaysSampleUsers;
extractUserId;
logger;
constructor(options) {
this.baselineSampleRate = options.baselineSampleRate ?? .1;
this.alwaysSampleUsers = new Set(options.alwaysSampleUsers || []);
this.extractUserId = options.extractUserId;
this.logger = options.logger;
}
shouldSample(context) {
const userId = this.extractUserId(context.args);
if (userId && this.alwaysSampleUsers.has(userId)) {
this.logger?.debug({
operation: context.operationName,
userId
}, "Sampling user request");
return true;
}
if (userId) return this.hashString(userId) < this.baselineSampleRate;
return Math.random() < this.baselineSampleRate;
}
/**
* Add user IDs to always-sample list
*/
addAlwaysSampleUsers(...userIds) {
for (const userId of userIds) this.alwaysSampleUsers.add(userId);
}
/**
* Remove user IDs from always-sample list
*/
removeAlwaysSampleUsers(...userIds) {
for (const userId of userIds) this.alwaysSampleUsers.delete(userId);
}
/**
* Simple hash function for consistent user sampling
*/
hashString(str) {
return hashUnitInterval(str);
}
};
/**
* Consistent sampler: every service reaches the same verdict for one trace.
*
* `RandomSampler` rolls the dice per process, so an upstream service can keep
* a trace that its downstream drops, leaving a waterfall with holes in it.
* Hashing a key that travels with the request removes the disagreement. Pass
* the trace id, or any identifier every hop already shares.
*
* @example
* ```typescript
* new DeterministicSampler({
* sampleRate: 0.1,
* key: (context) => trace.getActiveSpan()?.spanContext().traceId,
* })
* ```
*/
var DeterministicSampler = class {
rate;
key;
constructor(options) {
if (options.sampleRate < 0 || options.sampleRate > 1) throw new Error("Sample rate must be between 0 and 1");
this.rate = options.sampleRate;
this.key = options.key;
}
shouldSample(context) {
const key = this.key(context);
if (key === void 0) return Math.random() < this.rate;
return hashUnitInterval(key) < this.rate;
}
sampleRate() {
return toSampleRate(this.rate);
}
};
/** Bucket for keys seen after the tracked map fills up. */
const OVERFLOW_KEY = "__overflow__";
/**
* Per-key target-rate sampler for workloads with uneven traffic.
*
* A single rate serves a skewed workload badly: 1% floods storage with the
* busiest endpoint and still loses the rare tenant whose failures you need.
* This sampler counts traffic per key over a rolling window, then sets each
* key its own rate so every key contributes roughly `targetPerKey` events.
* Quiet keys survive intact; loud keys get thinned.
*
* The first window keeps everything, because no traffic history exists yet.
* Rates take effect from the second window onward.
*
* @example
* ```typescript
* new KeyTargetRateSampler({
* key: (context) => context.operationName,
* targetPerKey: 10, // ~10 events per key per window
* windowMs: 30_000,
* })
* ```
*/
var KeyTargetRateSampler = class {
key;
targetPerKey;
windowMs;
maxKeys;
counts = /* @__PURE__ */ new Map();
rates = /* @__PURE__ */ new Map();
windowStart = Date.now();
constructor(options) {
this.key = options.key;
this.targetPerKey = options.targetPerKey ?? 10;
this.windowMs = options.windowMs ?? 3e4;
this.maxKeys = options.maxKeys ?? 1e3;
if (this.targetPerKey <= 0) throw new Error("Target per key must be greater than 0");
if (this.windowMs <= 0) throw new Error("Window must be greater than 0");
}
/** Turn the window's observed counts into the next window's rates. */
roll(now) {
if (now - this.windowStart < this.windowMs) return;
const rates = /* @__PURE__ */ new Map();
for (const [key, count] of this.counts) rates.set(key, Math.max(1, count / this.targetPerKey));
this.rates = rates;
this.counts = /* @__PURE__ */ new Map();
this.windowStart = now;
}
/**
* Resolve the key, collapsing into one bucket once the map is full.
*
* An unbounded key function would otherwise grow the map without limit,
* which turns a sampler meant to cut cost into a memory leak.
*/
resolveKey(context) {
const key = this.key(context) ?? OVERFLOW_KEY;
if (this.counts.has(key) || this.counts.size < this.maxKeys) return key;
return OVERFLOW_KEY;
}
shouldSample(context) {
this.roll(Date.now());
const key = this.resolveKey(context);
this.counts.set(key, (this.counts.get(key) ?? 0) + 1);
const rate = this.rates.get(key) ?? 1;
return rate <= 1 || Math.random() < 1 / rate;
}
sampleRate(context) {
return this.rates.get(this.resolveKey(context)) ?? 1;
}
};
/**
* Composite sampler that combines multiple samplers
*
* Samples if ANY of the child samplers returns true.
*
* @example
* ```typescript
* new CompositeSampler([
* new UserIdSampler({ extractUserId: (args) => args[0]?.userId }),
* new AdaptiveSampler({ baselineSampleRate: 0.1 })
* ])
* ```
*/
var CompositeSampler = class {
samplers;
constructor(samplers) {
this.samplers = samplers;
if (samplers.length === 0) throw new Error("CompositeSampler requires at least one child sampler");
}
shouldSample(context) {
return this.samplers.some((sampler) => sampler.shouldSample(context));
}
};
/**
* Feature flag sampler
*
* Always samples requests with specific feature flags enabled.
* Perfect for correlating A/B test experiments with metrics.
*
* @example
* ```typescript
* new FeatureFlagSampler({
* baselineSampleRate: 0.01,
* alwaysSampleFlags: ['new_checkout', 'experimental_ui'],
* extractFlags: (args, metadata) => metadata?.featureFlags
* })
* ```
*/
var FeatureFlagSampler = class {
baselineSampleRate;
alwaysSampleFlags;
extractFlags;
logger;
constructor(options) {
this.baselineSampleRate = options.baselineSampleRate ?? .1;
this.alwaysSampleFlags = new Set(options.alwaysSampleFlags || []);
this.extractFlags = options.extractFlags;
this.logger = options.logger;
}
shouldSample(context) {
const flags = this.extractFlags(context.args, context.metadata);
if (flags && flags.some((flag) => this.alwaysSampleFlags.has(flag))) {
this.logger?.debug({
operation: context.operationName,
flags
}, "Sampling feature flag request");
return true;
}
return Math.random() < this.baselineSampleRate;
}
/**
* Add feature flags to always-sample list
*/
addAlwaysSampleFlags(...flags) {
for (const flag of flags) this.alwaysSampleFlags.add(flag);
}
/**
* Remove feature flags from always-sample list
*/
removeAlwaysSampleFlags(...flags) {
for (const flag of flags) this.alwaysSampleFlags.delete(flag);
}
};
/**
* Sampling preset factories.
*
* For most users, the string shorthand on `init()` is simpler:
* ```typescript
* init({ service: 'my-app', sampling: 'production' })
* ```
*
* Use factories when you need to customize:
* ```typescript
* init({ service: 'my-app', sampler: samplingPresets.production({ baselineSampleRate: 0.05 }) })
* ```
*/
const samplingPresets = {
/** Capture everything — best for local development and debugging */
development: () => new AlwaysSampler(),
/** Only bad outcomes — zero baseline, errors always kept */
errorsOnly: () => new AdaptiveSampler({
baselineSampleRate: 0,
alwaysSampleErrors: true
}),
/**
* Balanced production defaults — 10% baseline + errors + slow traces.
* Pass overrides to tune (uses the same option names as AdaptiveSampler).
*/
production: (overrides) => new AdaptiveSampler({
baselineSampleRate: .1,
alwaysSampleErrors: true,
alwaysSampleSlow: true,
slowThresholdMs: 1e3,
...overrides
}),
/** Disable sampling entirely */
off: () => new NeverSampler()
};
/**
* Resolve a preset string to a Sampler instance.
* Used internally by `init()` when `sampling` string is provided.
*
* @throws Error if preset is not recognized
*/
function resolveSamplingPreset(preset) {
switch (preset) {
case "development": return samplingPresets.development();
case "errors-only": return samplingPresets.errorsOnly();
case "production": return samplingPresets.production();
case "off": return samplingPresets.off();
default: throw new Error(`Unknown sampling preset: "${preset}". Valid presets: development, errors-only, production, off`);
}
}
/**
* Create a Link from W3C trace context headers (e.g., from a message queue).
*
* This is useful for message consumers that need to link to the producer span.
* The headers should contain at least a `traceparent` header in W3C format.
*
* @param headers - Dictionary containing traceparent/tracestate headers
* @param attributes - Optional attributes for the link
* @returns Link object if context could be extracted, null otherwise
*
* @example
* ```typescript
* // In a Kafka consumer
* const headers = { traceparent: '00-abc123...-def456...-01' };
* const link = createLinkFromHeaders(headers);
* if (link) {
* // Use with tracer.startActiveSpan options or ctx.addLink()
* tracer.startActiveSpan('process.message', { links: [link] }, span => { ... });
* }
* ```
*/
function createLinkFromHeaders(headers, attributes) {
const traceparent = headers.traceparent || headers["traceparent"];
if (!traceparent) return null;
const spanContext = parseTraceparent(traceparent);
if (!spanContext || !isValidSpanContext(spanContext)) return null;
return {
context: spanContext,
attributes: attributes ?? {}
};
}
/**
* Extract Links from a batch of messages for fan-in scenarios.
*
* Useful for batch processing where multiple producer spans should be linked.
* This enables tracing causality in event-driven architectures where a single
* consumer processes messages from multiple producers.
*
* @param messages - List of message objects
* @param headersKey - Key in each message containing trace headers (default: 'headers')
* @returns List of Link objects for all valid trace contexts
*
* @example
* ```typescript
* // Processing a batch of SQS/Kafka messages
* const messages = [
* { body: '...', headers: { traceparent: '...' } },
* { body: '...', headers: { traceparent: '...' } },
* ];
* const links = extractLinksFromBatch(messages);
*
* tracer.startActiveSpan('process.batch', { links }, span => {
* for (const msg of messages) {
* processMessage(msg);
* }
* });
* ```
*/
function extractLinksFromBatch(messages, headersKey = "headers") {
const links = [];
for (const msg of messages) {
const msgHeaders = require_values.asStringRecord(msg[headersKey]);
if (msgHeaders) {
const link = createLinkFromHeaders(msgHeaders, { "messaging.batch.message_index": links.length });
if (link) links.push(link);
}
}
return links;
}
/**
* Parse W3C traceparent header into SpanContext
* Format: version-traceId-spanId-traceFlags (e.g., 00-abc123...-def456...-01)
*
* @see https://www.w3.org/TR/trace-context/#traceparent-header
*/
function parseTraceparent(traceparent) {
const match = traceparent.match(/^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i);
if (!match || match.length < 5) return null;
const version = match[1];
const traceId = match[2];
const spanId = match[3];
const flags = match[4];
if (!version || !traceId || !spanId || !flags) return null;
if (version === "ff") return null;
return {
traceId,
spanId,
traceFlags: Number.parseInt(flags, 16),
isRemote: true
};
}
/**
* Check if a SpanContext is valid (has non-zero trace and span IDs)
*/
function isValidSpanContext(spanContext) {
if (!spanContext) return false;
return spanContext.traceId !== "00000000000000000000000000000000" && spanContext.spanId !== "0000000000000000";
}
/** True when the request in flight asked for full-fidelity capture. */
function debugCaptureRequested() {
const entry = _opentelemetry_api.propagation.getBaggage(getActiveContextWithBaggage())?.getEntry(AUTOTEL_DEBUG_BAGGAGE_KEY);
return entry !== void 0 && entry.value !== "" && entry.value !== "0";
}
//#endregion
//#region src/init-logger.ts
const LOG_LEVELS = {
debug: 0,
info: 1,
warn: 2,
error: 3
};
/** Silent logger used until and unless the application opts into diagnostics. */
const silentLogger = {
info: () => {},
warn: () => {},
error: () => {},
debug: () => {}
};
/** Apply the resolved silence and minimum-level settings to a logger. */
function wrapLogger(base, silent, minLevel) {
if (silent) return silentLogger;
const threshold = LOG_LEVELS[minLevel];
const wrap = (fn, level) => {
if (LOG_LEVELS[level] < threshold) return (() => {});
return ((...args) => fn(...args));
};
return {
debug: wrap(base.debug, "debug"),
info: wrap(base.info, "info"),
warn: wrap(base.warn, "warn"),
error: wrap(base.error, "error")
};
}
//#endregion
//#region src/posthog-logs.ts
var RedactingLogRecordProcessor = class {
wrapped;
redact;
constructor(wrapped, redact) {
this.wrapped = wrapped;
this.redact = redact;
}
onEmit(logRecord, context) {
const body = require_values.asString(logRecord.body);
if (body) logRecord.body = this.redact(body);
if (logRecord.attributes) for (const [key, value] of Object.entries(logRecord.attributes)) {
const text = require_values.asString(value);
if (text !== void 0) logRecord.attributes[key] = this.redact(text);
else if (Array.isArray(value)) logRecord.attributes[key] = value.map((item) => {
const entry = require_values.asString(item);
return entry === void 0 ? item : this.redact(entry);
});
}
this.wrapped.onEmit(logRecord, context);
}
shutdown() {
return this.wrapped.shutdown();
}
forceFlush() {
return this.wrapped.forceFlush();
}
};
/**
* Build log record processors for PostHog OTLP logs integration.
*
* Resolution order:
* 1. config.url if provided
* 2. POSTHOG_LOGS_URL env var
* 3. Empty array (disabled)
*/
function buildPostHogLogProcessors(config, stringRedactor) {
const url = config?.url || process.env.POSTHOG_LOGS_URL;
if (!url) return [];
const sdkLogs = require_node_require.safeRequire("@opentelemetry/sdk-logs");
const exporterModule = require_node_require.safeRequire("@opentelemetry/exporter-logs-otlp-http");
if (!sdkLogs || !exporterModule) return [];
const exporter = new exporterModule.OTLPLogExporter({ url });
let processor = new sdkLogs.BatchLogRecordProcessor({ exporter });
if (stringRedactor) processor = new RedactingLogRecordProcessor(processor, stringRedactor);
return [processor];
}
//#endregion
//#region src/tail-sampling-processor.ts
var TailSamplingSpanProcessor = class {
wrappedProcessor;
constructor(wrappedProcessor) {
this.wrappedProcessor = wrappedProcessor;
}
onStart(span, parentContext) {
this.wrappedProcessor.onStart(span, parentContext);
}
onEnd(span) {
const tailEvaluated = span.attributes[AUTOTEL_SAMPLING_TAIL_EVALUATED];
const shouldKeep = span.attributes[AUTOTEL_SAMPLING_TAIL_KEEP];
if (tailEvaluated === true && shouldKeep === false) return;
this.wrappedProcessor.onEnd(span);
}
forceFlush() {
return this.wrappedProcessor.forceFlush();
}
shutdown() {
return this.wrappedProcessor.shutdown();
}
};
//#endregion
//#region src/baggage-span-processor.ts
/**
* Span processor that automatically copies baggage entries to span attributes
*
* This makes baggage visible in trace UIs (Jaeger, Grafana, DataDog, etc.)
* without manually calling ctx.setAttribute() for each baggage entry.
*
* @example Enable in init()
* ```typescript
* init({
* service: 'my-app',
* baggage: true // Uses default 'baggage.' prefix
* });
*
* // Now baggage automatically appears as span attributes
* await withBaggage({
* baggage: { 'tenant.id': 't1', 'user.id': 'u1' },
* fn: async () => {
* // Span has baggage.tenant.id and baggage.user.id attributes!
* }
* });
* ```
*
* @example Custom prefix
* ```typescript
* init({
* service: 'my-app',
* baggage: 'ctx' // Uses 'ctx.' prefix
* });
* // Creates attributes: ctx.tenant.id, ctx.user.id
* ```
*/
var BaggageSpanProcessor = class {
prefix;
constructor(options = {}) {
this.prefix = options.prefix ?? "baggage.";
}
onStart(span, parentContext) {
let baggage = _opentelemetry_api.propagation.getBaggage(parentContext);
if (!baggage) baggage = _opentelemetry_api.propagation.getBaggage(_opentelemetry_api.context.active());
if (!baggage) try {
const { getActiveContextWithBaggage } = require_node_require.requireModule("./trace-context");
const storedContext = getActiveContextWithBaggage();
baggage = _opentelemetry_api.propagation.getBaggage(storedContext);
} catch {}
if (!baggage) return;
for (const [key, entry] of baggage.getAllEntries()) span.setAttribute(`${this.prefix}${key}`, entry.value);
}
onEnd(_span) {}
async shutdown() {}
async forceFlush() {}
};
//#endregion
//#region src/redact-values.ts
/** Standalone string redaction for use outside the span processor pipeline. */
function createStringRedactor(config) {
const resolved = typeof config === "string" ? require_attribute_redacting_processor.REDACTOR_PRESETS[config] : config;
const valuePatterns = resolved.valuePatterns ?? [];
const defaultReplacement = resolved.replacement ?? "[REDACTED]";
return (value) => {
let result = value;
for (const { pattern, replacement, mask } of valuePatterns) {
pattern.lastIndex = 0;
result = mask ? result.replaceAll(pattern, (match) => mask(match)) : result.replaceAll(pattern, replacement ?? defaultReplacement);
}
return result;
};
}
//#endregion
//#region src/env-config.ts
/**
* Validate URL format
*/
function isValidUrl(urlString) {
try {
const url = new URL(urlString);
return url.protocol === "http:" || url.protocol === "https:";
} catch {
return false;
}
}
/**
* Resolve OpenTelemetry environment variables from process.env
*/
function resolveOtelEnv() {
const env = {};
if (process.env.OTEL_SERVICE_NAME) {
const value = process.env.OTEL_SERVICE_NAME.trim();
if (value) env.OTEL_SERVICE_NAME = value;
}
if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
const value = process.env.OTEL_EXPORTER_OTLP_ENDPOINT.trim();
if (value && isValidUrl(value)) env.OTEL_EXPORTER_OTLP_ENDPOINT = value;
}
if (process.env.OTEL_EXPORTER_OTLP_HEADERS) {
const value = process.env.OTEL_EXPORTER_OTLP_HEADERS.trim();
if (value) env.OTEL_EXPORTER_OTLP_HEADERS = value;
}
if (process.env.OTEL_RESOURCE_ATTRIBUTES) {
const value = process.env.OTEL_RESOURCE_ATTRIBUTES.trim();
if (value) env.OTEL_RESOURCE_ATTRIBUTES = value;
}
if (process.env.OTEL_EXPORTER_OTLP_PROTOCOL) {
const value = process.env.OTEL_EXPORTER_OTLP_PROTOCOL.trim().toLowerCase();
if (value === "http" || value === "http/json") env.OTEL_EXPORTER_OTLP_PROTOCOL = "http";
else if (value === "http/protobuf" || value === "grpc") env.OTEL_EXPORTER_OTLP_PROTOCOL = value;
}
if (process.env.OTEL_TRACES_SAMPLER) {
const value = process.env.OTEL_TRACES_SAMPLER.trim();
if (value) env.OTEL_TRACES_SAMPLER = value;
}
if (process.env.OTEL_TRACES_SAMPLER_ARG) {
const value = process.env.OTEL_TRACES_SAMPLER_ARG.trim();
if (value) env.OTEL_TRACES_SAMPLER_ARG = value;
}
return env;
}
function parseRatioSamplerArg(samplerName, samplerArg) {
if (samplerArg === void 0) return 1;
const ratio = Number(samplerArg);
if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {
console.error(`[autotel] Invalid OTEL_TRACES_SAMPLER_ARG="${samplerArg}" for ${samplerName}. Expected a number in [0..1]. Falling back to 1.0.`);
return 1;
}
return ratio;
}
function warnOnUnusedSamplerArg(samplerName, samplerArg) {
if (samplerArg !== void 0) console.error(`[autotel] OTEL_TRACES_SAMPLER_ARG is not used by OTEL_TRACES_SAMPLER="${samplerName}". Ignoring value "${samplerArg}".`);
}
function createSamplerFromEnv(env) {
const samplerName = env.OTEL_TRACES_SAMPLER;
if (!samplerName) return;
switch (samplerName) {
case "always_on":
warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
return new _opentelemetry_sdk_trace_base.AlwaysOnSampler();
case "always_off":
warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
return new _opentelemetry_sdk_trace_base.AlwaysOffSampler();
case "traceidratio": return new _opentelemetry_sdk_trace_base.TraceIdRatioBasedSampler(parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG));
case "parentbased_always_on":
warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
return new _opentelemetry_sdk_trace_base.ParentBasedSampler({ root: new _opentelemetry_sdk_trace_base.AlwaysOnSampler() });
case "parentbased_always_off":
warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
return new _opentelemetry_sdk_trace_base.ParentBasedSampler({ root: new _opentelemetry_sdk_trace_base.AlwaysOffSampler() });
case "parentbased_traceidratio": return new _opentelemetry_sdk_trace_base.ParentBasedSampler({ root: new _opentelemetry_sdk_trace_base.TraceIdRatioBasedSampler(parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG)) });
case "jaeger_remote":
case "parentbased_jaeger_remote":
case "xray":
console.error(`[autotel] OTEL_TRACES_SAMPLER="${samplerName}" is not supported yet by autotel. Falling back to the next sampler source.`);
return;
default:
console.error(`[autotel] Unknown OTEL_TRACES_SAMPLER="${samplerName}". Falling back to the next sampler source.`);
return;
}
}
/**
* Parse OTEL_RESOURCE_ATTRIBUTES from comma-separated key=value pairs
* Example: "service.version=1.0.0,deployment.environment=production"
*/
function parseResourceAttributes(input) {
if (!input || input.trim() === "") return {};
const attributes = {};
const pairs = input.split(",");
for (const pair of pairs) {
const trimmedPair = pair.trim();
if (!trimmedPair) continue;
const equalIndex = trimmedPair.indexOf("=");
if (equalIndex === -1) continue;
const key = trimmedPair.slice(0, equalIndex).trim();
const value = trimmedPair.slice(equalIndex + 1).trim();
if (key && value) attributes[key] = value;
}
return attributes;
}
/**
* Parse OTEL_EXPORTER_OTLP_HEADERS from comma-separated key=value pairs
* Example: "api-key=secret123,x-custom-header=value"
*/
function parseOtlpHeaders(input) {
if (!input || input.trim() === "") return {};
const headers = {};
const pairs = input.split(",");
for (const pair of pairs) {
const trimmedPair = pair.trim();
if (!trimmedPair) continue;
const equalIndex = trimmedPair.indexOf("=");
if (equalIndex === -1) continue;
const key = trimmedPair.slice(0, equalIndex).trim();
const value = trimmedPair.slice(equalIndex + 1).trim();
if (key && value) headers[key] = value;
}
return headers;
}
/**
* Convert resolved environment variables to config
*/
function envToConfig(env) {
const config = {};
if (env.OTEL_SERVICE_NAME) config.service = env.OTEL_SERVICE_NAME;
if (env.OTEL_EXPORTER_OTLP_ENDPOINT) config.endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT;
if (env.OTEL_EXPORTER_OTLP_PROTOCOL) config.protocol = env.OTEL_EXPORTER_OTLP_PROTOCOL;
if (env.OTEL_EXPORTER_OTLP_HEADERS) config.headers = parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS);
const resourceAttrs = parseResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES);
if (Object.keys(resourceAttrs).length > 0) config.resourceAttributes = resourceAttrs;
const sampler = createSamplerFromEnv(env);
if (sampler) config.otelSampler = sampler;
return config;
}
/**
* Main function to resolve config from environment variables
*/
function resolveConfigFromEnv() {
return envToConfig(resolveOtelEnv());
}
//#endregion
//#region src/yaml-config.ts
/**
* YAML configuration loader for autotel
*
* Supports:
* - Auto-discovery of autotel.yaml in cwd
* - AUTOTEL_CONFIG_FILE env var override
* - Environment variable substitution: ${env:VAR} and ${env:VAR:-default}
*
* @example Auto-discovery
* ```yaml
* # autotel.yaml in project root
* service:
* name: my-service
* exporter:
* endpoint: ${env:OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}
* ```
*
* @example Explicit path
* ```bash
* AUTOTEL_CONFIG_FILE=./config/otel.yaml tsx --import autotel/auto src/index.ts
* ```
*/
/**
* Lazy-load yaml parser (optional peer dependency)
* Only loads when a YAML config file is actually found
*/
function loadYamlParser() {
try {
return require_node_require.requireModule("yaml").parse;
} catch {
throw new Error("YAML parser not found. Install with: pnpm add yaml");
}
}
/**
* Environment variable substitution regex
* Matches ${env:VAR_NAME} and ${env:VAR_NAME:-default}
*/
const ENV_VAR_PATTERN = /\$\{env:([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
/**
* Substitute ${env:VAR} and ${env:VAR:-default} in a string
*
* @param value - String potentially containing env var references
* @returns String with env vars substituted
*
* @example
* substituteEnvVars('${env:NODE_ENV:-development}')
* // Returns 'production' if NODE_ENV=production, else 'development'
*/
function substituteEnvVars(value) {
return value.replaceAll(ENV_VAR_PATTERN, (_match, varName, defaultValue) => {
const envValue = process.env[varName];
if (envValue !== void 0) return envValue;
if (defaultValue !== void 0) return defaultValue;
console.warn(`[autotel] Environment variable ${varName} not set and no default provided`);
return "";
});
}
/**
* Recursively substitute env vars in an object
*
* @param obj - Object to process
* @returns Object with all string values having env vars substituted
*/
function substituteEnvVarsDeep(value) {
if (Array.isArray(value)) return value.map(substituteEnvVarsDeep);
const mapping = asYamlMapping(value);
if (mapping) {
const result = {};
for (const [key, entry] of Object.entries(mapping)) result[key] = substituteEnvVarsDeep(entry);
return result;
}
const text = require_values.asString(value);
return text === void 0 ? value : substituteEnvVars(text);
}
/** The mapping a YAML value is, when it is one. */
function asYamlMapping(value) {
return require_values.asRecord(value) === void 0 ? void 0 : value;
}
/**
* A YAML document read into the config shape, with `${env:...}` placeholders
* resolved.
*
* This is the boundary: the file is text until here. Every field of
* YamlConfig is optional, so any document satisfies it, and
* `yamlToAutotelConfig` picks out only the fields it understands.
*/
function parseYamlConfig(content) {
return asYamlMapping(substituteEnvVarsDeep(loadYamlParser()(content))) ?? {};
}
/**
* Find YAML config file path
*
* Priority:
* 1. AUTOTEL_CONFIG_FILE env var (explicit path)
* 2. autotel.yaml in cwd (convention)
* 3. autotel.yml in cwd (alternative extension)
*
* @returns File path if found, null otherwise
*/
function findConfigFile() {
const envPath = process.env.AUTOTEL_CONFIG_FILE;
if (envPath) {
const resolved = node_path.default.resolve(envPath);
if (node_fs.existsSync(resolved)) return resolved;
console.warn(`[autotel] Config file not found: ${envPath}`);
return null;
}
const conventionPath = node_path.default.resolve(process.cwd(), "autotel.yaml");
if (node_fs.existsSync(conventionPath)) return conventionPath;
const altPath = node_path.default.resolve(process.cwd(), "autotel.yml");
if (node_fs.existsSync(altPath)) return altPath;
return null;
}
/**
* Convert YAML config structure to AutotelConfig
*
* @param yaml - Parsed and env-substituted YAML config
* @returns Partial AutotelConfig ready for merging
*/
function yamlToAutotelConfig(yaml) {
const config = {};
if (yaml.service?.name) config.service = yaml.service.name;
if (yaml.service?.version) config.version = yaml.service.version;
if (yaml.service?.environment) config.environment = yaml.service.environment;
if (yaml.exporter?.endpoint) config.endpoint = yaml.exporter.endpoint;
if (yaml.exporter?.protocol) config.protocol = yaml.exporter.protocol;
if (yaml.exporter?.headers) config.headers = yaml.exporter.headers;
if (yaml.exporter?.destinations) config.destinations = yaml.exporter.destinations;
if (yaml.resource) config.resourceAttributes = yaml.resource;
if (yaml.autoInstrumentations) config.autoInstrumentations = yaml.autoInstrumentations;
if (yaml.policies) config.policies = yaml.policies;
if (yaml.debug !== void 0) config.debug = yaml.debug;
if (yaml.sampling?.preset) {
warnOnIgnoredPresetOverrides(yaml.sampling);
config.sampling = yaml.sampling.preset;
} else {
const sampler = createSamplerFromYaml(yaml.sampling);
if (sampler) config.sampler = sampler;
}
return config;
}
function createSamplerFromYaml(sampling) {
if (!sampling) return void 0;
if (sampling.preset) return void 0;
const type = sampling.type ?? "adaptive";
try {
switch (type) {
case "adaptive": return new AdaptiveSampler({
baselineSampleRate: sampling.baseline_rate,
alwaysSampleErrors: sampling.always_sample_errors,
alwaysSampleSlow: sampling.always_sample_slow,
slowThresholdMs: sampling.slow_threshold_ms
});
case "always_on": return new AlwaysSampler();
case "always_off": return new NeverSampler();
case "ratio":
if (sampling.ratio === void 0) {
console.warn("[autotel] sampling.ratio missing in YAML sampling config. Falling back to adaptive sampler.");
return new AdaptiveSampler();
}
return new RandomSampler(sampling.ratio);
default:
console.warn(`[autotel] Unknown sampling type "${type}" in YAML config. Falling back to defaults.`);
return;
}
} catch (error) {
console.warn(`[autotel] Failed to configure sampling from YAML: ${error instanceof Error ? error.message : String(error)}`);
return;
}
}
function warnOnIgnoredPresetOverrides(sampling) {
const ignoredFields = [
"type",
"ratio",
"baseline_rate",
"always_sample_errors",
"always_sample_slow",
"slow_threshold_ms"
].filter((field) => sampling[field] !== void 0);
if (ignoredFields.length === 0) return;
console.warn(`[autotel] sampling.preset="${sampling.preset}" ignores these YAML fields: ${ignoredFields.join(", ")}. Use the programmatic API with sampler or samplingPresets.*(...) for tuned presets.`);
}
/**
* Load and parse YAML config file (auto-discovery)
*
* Automatically finds and loads autotel.yaml or uses AUTOTEL_CONFIG_FILE.
* Returns null if no config file found (not an error - YAML config is optional).
*
* @returns Partial AutotelConfig or null if no config file found
*
* @example
* const yamlConfig = loadYamlConfig();
* if (yamlConfig) {
* init({ ...yamlConfig, debug: true });
* }
*/
function loadYamlConfig() {
const filePath = findConfigFile();
if (!filePath) return null;
try {
return yamlToAutotelConfig(parseYamlConfig(node_fs.readFileSync(filePath, "utf8")));
} catch (error) {
console.error(`[autotel] Failed to load YAML config from ${filePath}:`, error);
return null;
}
}
/**
* Load YAML config from a specific file path
*
* Unlike loadYamlConfig(), this throws if the file cannot be read.
*
* @param filePath - Path to YAML config file
* @returns Partial AutotelConfig
* @throws Error if file cannot be read or parsed
*
* @example
* import { loadYamlConfigFromFile } from 'autotel/yaml';
* import { init } from 'autotel';
*
* const config = loadYamlConfigFromFile('./config/otel.yaml');
* init({ ...config, debug: true });
*/
function loadYamlConfigFromFile(filePath) {
const resolved = node_path.default.resolve(filePath);
return yamlToAutotelConfig(parseYamlConfig(node_fs.readFileSync(resolved, "utf8")));
}
/**
* Check if a YAML config file exists (without loading it)
*
* @returns true if a config file would be found by loadYamlConfig()
*/
function hasYamlConfig() {
return findConfigFile() !== null;
}
//#endregion
//#region src/devtools.ts
const defaultHost = "127.0.0.1";
const defaultPort = 4318;
function resolveDevtoolsConfig(config) {
if (!config) return {
enabled: false,
endpoint: void 0,
embedded: false,
host: defaultHost,
port: defaultPort,
verbose: false
};
if (config === true) return {
enabled: true,
endpoint: `http://${defaultHost}:${defaultPort}`,
embedded: false,
host: defaultHost,
port: defaultPort,
verbose: false
};
const enabled = config.enabled ?? true;
const host = config.host ?? defaultHost;
const port = config.port ?? defaultPort;
const endpoint = config.endpoint ?? `http://${host}:${port}`;
return {
enabled,
endpoint: enabled ? endpoint : void 0,
embedded: enabled && (config.embedded ?? false),
host,
port,
verbose: config.verbose ?? false
};
}
//#endregion
//#region src/process-handlers.ts
let removeOwnedHandlers = [];
/**
* Tracked separately from `removeOwnedHandlers` because the exit flush outlives
* a call to `installProcessHandlers`, which clears that list before installing
* its own signal listeners.
*/
let removeExitFlush;
/**
* Shared across every path that can end the process: signals, fatal errors and
* a clean exit. They can overlap — a container stopping a job that has just
* finished its work sends SIGTERM while the exit flush is still draining — and
* a second shutdown would tear down queues the first is still using.
*/
let shutdownInFlight;
/**
* The clean-exit flush, while it runs. Doubles as the latch that keeps a
* re-emitted `beforeExit` from flushing twice, and as the thing a shutdown
* waits on rather than tearing down queues mid-drain.
*/
let exitFlushInFlight;
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 2e3;
function runShutdownOnce(shutdown, timeoutMs) {
if (shutdownInFlight) return shutdownInFlight;
let timeoutHandle;
const shutdownAttempt = Promise.resolve(exitFlushInFlight).then(shutdown).catch(() => void 0);
const timeout = new Promise((resolve) => {
timeoutHandle = setTimeout(resolve, timeoutMs);
timeoutHandle.unref();
});
shutdownInFlight = Promise.race([shutdownAttempt, timeout]).then(() => {
if (timeoutHandle) clearTimeout(timeoutHandle);
});
return shutdownInFlight;
}
const DEFAULT_SIGNALS = ["SIGTERM", "SIGINT"];
function signalExitCode(signal) {
return 128 + node_os.constants.signals[signal];
}
/**
* Surface a fatal error before shutting down.
*
* Registering an `uncaughtException` / `unhandledRejection` listener overrides
* Node's default of printing the stack to stderr, so without this a crash under
* `fatalErrors` would exit silently. Autotel's own logger is silent by default,
* so we print to stderr directly to guarantee the crash stays visible.
*/
function reportFatalError(error, event) {
const err = error instanceof Error ? error : new Error(String(error));
console.error(`[autotel] ${event}, flushing telemetry then exiting`, err);
}
function installProcessHandlers(config, shutdown) {
uninstallProcessHandlers();
const timeoutMs = config.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
let exitScheduled = false;
let resolvedExitCode = 0;
let fatalLatched = false;
const shutdownAndExit = (exitCode, fatal) => {
if (fatal) {
if (!fatalLatched) {
resolvedExitCode = exitCode;
fatalLatched = true;
}
} else if (!exitScheduled && !fatalLatched) resolvedExitCode = exitCode;
if (exitScheduled) return;
exitScheduled = true;
runShutdownOnce(shutdown, timeoutMs).then(() => {
process.exit(resolvedExitCode);
});
};
for (const signal of config.signals ?? DEFAULT_SIGNALS) {
const listener = () => {
shutdownAndExit(signalExitCode(signal), false);
};
process.on(signal, listener);
removeOwnedHandlers.push(() => {
process.removeListener(signal, listener);
});
}
if (config.fatalErrors ?? true) {
const uncaughtExceptionListener = (error) => {
reportFatalError(error, "uncaughtException");
shutdownAndExit(1, true);
};
const unhandledRejectionListener = (reason) => {
reportFatalError(reason, "unhandledRejection");
shutdownAndExit(1, true);
};
process.on("uncaughtException", uncaughtExceptionListener);
process.on("unhandledRejection", unhandledRejectionListener);
removeOwnedHandlers.push(() => {
process.removeListener("uncaughtException", uncaughtExceptionListener);
}, () => {
process.removeListener("unhandledRejection", unhandledRejectionListener);
});
}
}
/**
* Flush telemetry when the process runs to completion.
*
* The signal and fatal-error handlers above cover a process that is stopped or
* that crashes. Neither fires when a script simply finishes: the event loop
* drains and Node exits, taking whatever the batch span processor was still
* holding with it. `beforeExit` is the only hook for that case, and it is the
* one that matters for CLIs, cron jobs, CI steps and serverless handlers.
*
* A flush, never a shutdown: `beforeExit` fires on *any* event-loop drain, not
* only the final one, so tearing the SDK down here would silently kill
* telemetry in a process that goes on to do more work.
*/
function installExitFlush(flushTelemetry, timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS) {
removeExitFlush?.();
exitFlushInFlight = void 0;
const listener = (code) => {
if (exitFlushInFlight || shutdownInFlight) return;
const deadline = setTimeout(() => {
process.exit(process.exitCode ?? code);
}, timeoutMs);
exitFlushInFlight = flushTelemetry().catch(() => void 0).finally(() => {
clearTimeout(deadline);
});
};
process.on("beforeExit", listener);
removeExitFlush = () => {
process.removeListener("beforeExit", listener);
removeExitFlush = void 0;
};
}
function uninstallProcessHandlers() {
for (const removeHandler of removeOwnedHandlers) removeHandler();
removeOwnedHandlers = [];
removeExitFlush?.();
shutdownInFlight = void 0;
exitFlushInFlight = void 0;
}
//#endregion
//#region src/circuit-breaker.ts
const DEFAULT_CONFIG = {
failureThreshold: 5,
resetTimeout: 3e4,
windowSize: 6e4
};
const CircuitState = {
CLOSED: "CLOSED",
OPEN: "OPEN",
HALF_OPEN: "HALF_OPEN"
};
/**
* Circuit breaker implementation
*
* Tracks failures and automatically opens the circuit to prevent
* overwhelming failing subscribers.
*/
var CircuitBreaker = class {
state = CircuitState.CLOSED;
failures = [];
lastFailureTime = 0;
config;
name;
constructor(name, config) {
this.name = name;
this.config = {
...DEFAULT_CONFIG,
...config
};
}
/**
* Execute a function with circuit breaker protection
* Throws CircuitOpenError if circuit is open
*/
async execute(fn) {
if (this.state === CircuitState.OPEN) {
const now = Date.now();
if (now - this.lastFailureTime >= this.config.resetTimeout) this.state = CircuitState.HALF_OPEN;
else throw new CircuitOpenError(`Circuit breaker is OPEN for ${this.name}. Will retry in ${Math.ceil((this.config.resetTimeout - (now - this.lastFailureTime)) / 1e3)}s`);
}
try {
const result = await fn();
if (this.state === CircuitState.HALF_OPEN) this.reset();
return result;
} catch (error) {
this.recordFailure(error);
throw error;
}
}
/**
* Record a failure and potentially open the circuit
*/
recordFailure(error) {
const now = Date.now();
this.failures = this.failures.filter((f) => now - f.timestamp < this.config.windowSize);
this.failures.push({
timestamp: now,
error: error instanceof Error ? error.message : String(error)
});
this.lastFailureTime = now;
if (this.failures.length >= this.config.failureThreshold) {
if (this.state === CircuitState.HALF_OPEN) this.state = CircuitState.OPEN;
else if (this.state === CircuitState.CLOSED) this.state = CircuitState.OPEN;
}
}
/**
* Reset the circuit breaker (on success)
*/
reset() {
this.state = CircuitState.CLOSED;
this.failures = [];
this.lastFailureTime = 0;
}
/**
* Get current state (for monitoring)
*/
getState() {
return this.state;
}
/**
* Get failure count in current window
*/
getFailureCount() {
const now = Date.now();
this.failures = this.failures.filter((f) => now - f.timestamp < this.config.windowSize);
return this.failures.length;
}
/**
* Get recent failures (for debugging)
*/
getRecentFailures() {
const now = Date.now();
return this.failures.filter((f) => now - f.timestamp < this.config.windowSize);
}
/**
* Manually reset the circuit breaker (for testing or manual intervention)
*/
forceReset() {
this.reset();
}
/**
* Manually open the circuit (for testing or manual intervention)
*/
forceOpen() {
this.state = CircuitState.OPEN;
this.lastFailureTime = Date.now();
}
};
/**
* Error thrown when circuit is open
*/
var CircuitOpenError = class extends Error {
constructor(message) {
super(message);
this.name = "CircuitOpenError";
}
};
//#endregion
//#region src/operation-context.ts
/**
* Operation context tracking using AsyncLocalStorage
*
* This module provides a way to track operation names across async boundaries
* so they can be automatically captured in events events.
*
* We cannot read span attributes from OpenTelemetry's API (it's write-only),
* so we maintain our own async context storage.
*/
/**
* AsyncLocalStorage instance for tracking operation context
*/
const operationStorage = new node_async_hooks.AsyncLocalStorage();
/**
* Get the current operation context (if any)
*
* @returns The current operation context, or undefined if not in an operation
*
* @example
* ```typescript
* const ctx = getOperationContext();
* if (ctx) {
* console.log('Current operation:', ctx.name);
* }
* ```
*/
function getOperationContext() {
return operationStorage.getStore();
}
/**
* Run a function within an operation context
*
* This sets the operation name for the duration of the function execution,
* including all async operations spawned from it.
*
* @param name - The operation name to set
* @param fn - The function to execute within the context
* @returns The result of the function
*
* @example
* ```typescript
* const result = await runInOperationContext('user.create', async () => {
* // Any events.trackEvent() calls here will automatically capture
* // 'operation.name': 'user.create'
* await createUser();
* return 'success';
* });
* ```
*/
function runInOperationContext(name, fn) {
return operationStorage.run({ name }, fn);
}
//#endregion
//#region src/events-config.ts
/**
* Hash a string value for PII protection
*
* Uses a simple, fast hash function suitable for correlation.
* NOT cryptographically secure - use for PII masking, not security.
*/
function hashValue(value) {
let hash = 0;
for (let i = 0; i < value.length; i++) {
const char = value.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return (hash >>> 0).toString(16).padStart(8, "0");
}
//#endregion
//#region src/event.ts
/**
* Events API for product events platforms
*
* Track user behavior, business events, and critical actions.
* Sends to product events platforms (PostHog, Mixpanel, Amplitude) via subscribers.
* For business people who think in events/funnels.
*
* For OpenTelemetry metrics (Prometheus/Grafana), use the Metrics class instead.
*
* @example Recommended: Configure subscribers in init(), use track() function
* ```typescript
* import { init, track } from 'autotel';
* import { PostHogSubscriber } from 'autotel-posthog/subscriber';
*
* init({
* service: 'my-app',
* subscribers: [new PostHogSubscriber({ apiKey: 'phc_...' })]
* });
*
* // Track events - uses subscribers from init()
* track('application.submitted', { jobId: '123', userId: '456' });
* ```
*
* @example Create Event instance (inherits subscribers from init)
* ```typescript
* import { Event } from 'autotel/event';
*
* // Uses subscribers configured in init()
* const event = new Event('job-application');
* event.trackEvent('application.submitted', { jobId: '123' });
* ```
*
* @example Override subscribers for specific Event instance
* ```typescript
* import { Event } from 'autotel/event';
* import { PostHogSubscriber } from 'autotel-posthog/subscriber';
*
* // Override: use different subscribers for this instance
* const event = new Event('job-application', {
* subscribers: [new PostHogSubscriber({ apiKey: 'phc_different_project' })]
* });
*
* event.trackEvent('application.submitted', { jobId: '123' });
* ```
*/
var Event = class {
serviceName;
logger;
collector;
subscribers;
hasSubscribers;
circuitBreakers;
/**
* Create a new Event instance
*
* **Note**: Most users should use `init()` + `track()` instead of creating Event instances directly.
*
* **Subscriber Resolution**:
* - If `subscribers` provided in options → uses those (instance override)
* - If `subscribers` not provided → falls back to subscribers from `init()` (global config)
* - If neither → no subscribers (events logged only)
*
* @param serviceName - Service name for identifying events
* @param options - Optional configuration (logger, collector, subscribers)
*
* @example Recommended: Use track() with init()
* ```typescript
* import { init, track } from 'autotel';
* import { PostHogSubscriber } from 'autotel-posthog/subscriber';
*
* init({
* service: 'checkout',
* subscribers: [new PostHogSubscriber({ apiKey: 'phc_...' })]
* });
*
* track('purchase.completed', { amount: 99.99 });
* ```
*
* @example Inherit subscribers from init()
* ```typescript
* // Uses subscribers configured in init()
* const event = new Event('checkout');
* event.trackEvent('purchase.completed', { amount: 99.99 });
* ```
*
* @example Override subscribers for this instance
* ```typescript
* import { Event } from 'autotel/event';
* import { PostHogSubscriber } from 'autotel-posthog/subscriber';
*
* // Override: use different subscribers for this instance only
* const event = new Event('checkout', {
* subscribers: [new PostHogSubscriber({ apiKey: 'phc_different_project' })]
* });
* ```
*/
constructor(serviceName, options = {}) {
this.serviceName = serviceName;
this.logger = options.logger;
this.collector = options.collector;
this.subscribers = options.subscribers === void 0 ? getConfig()?.subscribers || [] : options.subscribers;
this.hasSubscribers = this.subscribers.length > 0;
this.circuitBreakers = /* @__PURE__ */ new Map();
for (const subscriber of this.subscribers) {
const subscriberName = subscriber.name || "Unknown";
this.circuitBreakers.set(subscriber, new CircuitBreaker(subscriberName, {
failureThreshold: 5,
resetTimeout: 3e4,
windowSize: 6e4
}));
}
}
/**
* Automatically enrich attributes with all available telemetry context
*
* Auto-captures:
* - Resource attributes: service.version, deployment.environment
* - Trace context: traceId, spanId, correlationId
* - Operation context: operation.name
*/
enrichWithTelemetryContext(attributes = {}) {
const enriched = {
service: this.serviceName,
...attributes
};
const config = getConfig();
if (config) {
if (config.version) enriched["service.version"] = config.version;
if (config.environment) {
enriched["deployment.environment"] = config.environment;
enriched["deployment.environment.name"] = config.environment;
}
}
const spanContext = _opentelemetry_api.trace.getActiveSpan()?.spanContext();
if (spanContext) {
enriched.traceId = spanContext.traceId;
enriched.spanId = spanContext.spanId;
enriched.correlationId = spanContext.traceId.slice(0, 16);
}
const operationContext = getOperationContext();
if (operationContext) enriched["operation.name"] = operationContext.name;
return enriched;
}
/**
* Build autotel event context for trace correlation
*
* Works in 4 contexts:
* 1. Inside a span → use current span's trace_id + span_id
* 2. Outside span but in AsyncLocalStorage context → use trace_id + correlation_id
* 3. Totally standalone → use correlation_id + service/env/version
* 4. Batch/fan-in (multiple linked parents) → use count + hash or full array
*
* @returns AutotelEventContext or undefined if trace context is disabled
*/
buildAutotelContext() {
const eventsConfig = getEventsConfig();
if (!eventsConfig?.includeTraceContext) return { correlation_id: getOrCreateCorrelationId() };
const config = getConfig();
const spanContext = _opentelemetry_api.trace.getActiveSpan()?.spanContext();
const correlationId = getOrCreateCorrelationId();
const autotelContext = { correlation_id: correlationId };
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) {
let traceStateStr = "";
try {
if (require_values.isFunction(traceState.serialize)) traceStateStr = traceState.serialize();
} catch {}
if (traceStateStr) autotelContext.trace_state = traceStateStr;
}
if (eventsConfig.traceUrl) {
const traceUrl = eventsConfig.traceUrl({
traceId: spanContext.traceId,
spanId: spanContext.spanId,
correlationId,
serviceName: config?.service || this.serviceName,
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;
}
/**
* Enrich event attributes from baggage with guardrails
*
* @param attributes - Current event attributes
* @returns Enriched attributes with baggage values
*/
enrichFromBaggage(attributes) {
const enrichConfig = getEventsConfig()?.enrichFromBaggage;
if (!enrichConfig) return attributes;
const enriched = { ...attributes };
const activeContext = _opentelemetry_api.context.active();
const baggage = _opentelemetry_api.propagation.getBaggage(activeContext);
if (!baggage) return enriched;
let keyCount = 0;
let byteCount = 0;
const maxKeys = enrichConfig.maxKeys ?? 10;
const maxBytes = enrichConfig.maxBytes ?? 1024;
const prefix = enrichConfig.prefix ?? "";
for (const [key, entry] of baggage.getAllEntries()) {
if (!this.isBaggageKeyAllowed(key, enrichConfig)) continue;
if (keyCount >= maxKeys) break;
const value = entry.value;
const transform = enrichConfig.transform?.[key];
let transformedValue;
if (transform === "hash") transformedValue = hashValue(value);
else if (transform === void 0 || transform === "plain") transformedValue = value;
else transformedValue = transform(value);
const valueBytes = new TextEncoder().encode(transformedValue).length;
if (byteCount + valueBytes > maxBytes) continue;
const enrichedKey = `${prefix}${key}`;
enriched[enrichedKey] = transformedValue;
keyCount++;
byteCount += valueBytes;
}
return enriched;
}
/**
* Check if a baggage key is allowed based on config
*/
isBaggageKeyAllowed(key, config) {
if (config.deny) {
for (const pattern of config.deny) if (this.matchesBaggagePattern(key, pattern)) return false;
}
for (const pattern of config.allow) if (this.matchesBaggagePattern(key, pattern)) return true;
return false;
}
/**
* Check if a key matches a baggage pattern
* Supports exact matches and wildcard patterns (e.g., 'tenant.*')
*/
matchesBaggagePattern(key, pattern) {
if (pattern.endsWith(".*")) {
const prefix = pattern.slice(0, -2);
return key.startsWith(prefix + ".");
}
return key === pattern;
}
/**
* Track a business event
*
* Use this for tracking user actions, business events, product usage:
* - "user.signup"
* - "order.completed"
* - "feature.used"
*
* Events are sent to configured subscribers (PostHog, Mixpanel, etc.).
*
* @example
* ```typescript
* // Track user signup
* events.trackEvent('user.signup', {
* userId: '123',
* plan: 'pro'
* })
*
* // Track order
* events.trackEvent('order.completed', {
* orderId: 'ord_123',
* amount: 99.99
* })
* ```
*/
trackEvent(eventName, attributes) {
const validated = validateEvent(eventName, attributes, getValidationConfig() || void 0);
const enrichedAttributes = this.enrichWithTelemetryContext(validated.attributes);
this.logger?.info({
event: validated.eventName,
attributes: enrichedAttributes
}, "Event tracked");
this.collector?.recordEvent({
event: validated.eventName,
attributes: enrichedAttributes,
service: this.serviceName,
timestamp: Date.now()
});
if (this.hasSubscribers) {
const autotelContext = this.buildAutotelContext();
const finalAttributes = this.enrichFromBaggage(enrichedAttributes);
this.notifySubscribers((subscriber) => subscriber.trackEvent(validated.eventName, finalAttributes, { autotel: autotelContext }));
}
}
/**
* Notify all subscribers concurrently without blocking
* Uses circuit breakers to protect against failing subscribers
* Uses Promise.allSettled to prevent subscriber errors from affecting other subscribers
*/
async notifySubscribers(fn) {
const promises = this.subscribers.map(async (subscriber) => {
const circuitBreaker = this.circuitBreakers.get(subscriber);
if (!circuitBreaker) return;
try {
await circuitBreaker.execute(() => fn(subscriber));
} catch (error) {
if (error instanceof CircuitOpenError) {
getLogger().warn({ subscriberName: subscriber.name || "Unknown" }, `[Events] ${error.message}`);
return;
}
getLogger().error({
err: error instanceof Error ? error : void 0,
subscriberName: subscriber.name || "Unknown"
}, `[Events] Subscriber ${subscriber.name || "Unknown"} failed`);
}
});
await Promise.allSettled(promises);
}
/**
* Track conversion funnel steps
*
* Monitor where users drop off in multi-step processes.
*
* @example
* ```typescript
* // Track signup funnel
* events.trackFunnelStep('signup', 'started', { userId: '123' })
* events.trackFunnelStep('signup', 'email_verified', { userId: '123' })
* events.trackFunnelStep('signup', 'completed', { userId: '123' })
*
* // Track checkout flow
* events.trackFunnelStep('checkout', 'started', { cartValue: 99.99 })
* events.trackFunnelStep('checkout', 'payment_info', { cartValue: 99.99 })
* events.trackFunnelStep('checkout', 'completed', { cartValue: 99.99 })
* ```
*/
trackFunnelStep(funnelName, status, attributes) {
const enrichedAttributes = this.enrichWithTelemetryContext(attributes);
this.logger?.info({
funnel: funnelName,
status,
attributes: enrichedAttributes
}, "Funnel step tracked");
this.collector?.recordFunnelStep({
funnel: funnelName,
status,
attributes: enrichedAttributes,
service: this.serviceName,
timestamp: Date.now()
});
if (this.hasSubscribers) {
const autotelContext = this.buildAutotelContext();
const finalAttributes = this.enrichFromBaggage(enrichedAttributes);
this.notifySubscribers((subscriber) => subscriber.trackFunnelStep(funnelName, status, finalAttributes, { autotel: autotelContext }));
}
}
/**
* Track outcomes (success/failure/partial)
*
* Monitor success rates of critical operations.
*
* @example
* ```typescript
* // Track email delivery
* events.trackOutcome('email.delivery', 'success', {
* recipientType: 'user',
* emailType: 'welcome'
* })
*
* events.trackOutcome('email.delivery', 'failure', {
* recipientType: 'user',
* errorCode: 'invalid_email'
* })
*
* // Track payment processing
* events.trackOutcome('payment.process', 'success', { amount: 99.99 })
* events.trackOutcome('payment.process', 'failure', { error: 'insufficient_funds' })
* ```
*/
trackOutcome(operationName, status, attributes) {
const enrichedAttributes = this.enrichWithTelemetryContext(attributes);
this.logger?.info({
operation: operationName,
status,
attributes: enrichedAttributes
}, "Outcome tracked");
this.collector?.recordOutcome({
operation: operationName,
status,
attributes: enrichedAttributes,
service: this.serviceName,
timestamp: Date.now()
});
if (this.hasSubscribers) {
const autotelContext = this.buildAutotelContext();
const finalAttributes = this.enrichFromBaggage(enrichedAttributes);
this.notifySubscribers((subscriber) => subscriber.trackOutcome(operationName, status, finalAttributes, { autotel: autotelContext }));
}
}
/**
* Track value metrics
*
* Record numerical values like revenue, transaction amounts,
* item counts, processing times, engagement scores, etc.
*
* @example
* ```typescript
* // Track revenue
* events.trackValue('order.revenue', 149.99, {
* currency: 'USD',
* productCategory: 'electronics'
* })
*
* // Track items per cart
* events.trackValue('cart.item_count', 5, {
* userId: '123'
* })
*
* // Track processing time
* events.trackValue('api.response_time', 250, {
* unit: 'ms',
* endpoint: '/api/checkout'
* })
* ```
*/
trackValue(metricName, value, attributes) {
const enrichedAttributes = this.enrichWithTelemetryContext({
metric: metricName,
...attributes
});
this.logger?.debug({
metric: metricName,
value,
attributes: enrichedAttributes
}, "Value tracked");
this.collector?.recordValue({
metric: metricName,
value,
attributes: enrichedAttributes,
service: this.serviceName,
timestamp: Date.now()
});
if (this.hasSubscribers) {
const autotelContext = this.buildAutotelContext();
const finalAttributes = this.enrichFromBaggage(enrichedAttributes);
this.notifySubscribers((subscriber) => subscriber.trackValue(metricName, value, finalAttributes, { autotel: autotelContext }));
}
}
/**
* Flush all subscribers and wait for pending events
*
* Call this before shutdown to ensure all events are delivered.
*
* @example
* ```typescript
* const event =new Event('app', { subscribers: [...] });
*
* // Before shutdown
* await events.flush();
* ```
*/
async flush() {
if (!this.hasSubscribers) return;
const shutdownPromises = this.subscribers.map(async (subscriber) => {
if (subscriber.shutdown) try {
await subscriber.shutdown();
} catch (error) {
getLogger().error({
err: error instanceof Error ? error : void 0,
subscriberName: subscriber.name || "Unknown"
}, `[Events] Failed to shutdown subscriber ${subscriber.name || "Unknown"}`);
}
});
await Promise.allSettled(shutdownPromises);
}
/**
* Shutdown the Event instance and all subscribers
*
* Unlike `flush()`, this method:
* - Shuts down all subscribers
* - Prevents further event tracking (hasSubscribers becomes false)
* - Should only be called once at application shutdown
*
* @example
* ```typescript
* // In Next.js API route with after()
* import { after } from 'next/server';
*
* export async function POST(req: Request) {
* const event = new Event('checkout', { subscribers: [...] });
* event.trackEvent('order.completed', { orderId: '123' });
*
* after(async () => {
* await event.shutdown();
* });
*
* return Response.json({ success: true });
* }
* ```
*/
async shutdown() {
if (!this.hasSubscribers) return;
await Promise.allSettled(this.subscribers.map(async (subscriber) => {
if (subscriber.shutdown) try {
await subscriber.shutdown();
} catch (error) {
getLogger().error({
err: error instanceof Error ? error : void 0,
subscriberName: subscriber.name || "Unknown"
}, `[Events] Failed to shutdown subscriber ${subscriber.name || "Unknown"}`);
}
}));
this.hasSubscribers = false;
}
/**
* Track funnel progression with custom step names
*
* Unlike trackFunnelStep which uses FunnelStatus enum values,
* this method allows any string as the step name for flexible funnel tracking.
*
* @param funnelName - Name of the funnel (e.g., "checkout", "onboarding")
* @param stepName - Custom step name (e.g., "cart_viewed", "payment_entered")
* @param stepNumber - Optional numeric position in the funnel
* @param attributes - Optional event attributes
*
* @example
* ```typescript
* // Track custom checkout steps
* event.trackFunnelProgression('checkout', 'cart_viewed', 1);
* event.trackFunnelProgression('checkout', 'shipping_selected', 2);
* event.trackFunnelProgression('checkout', 'payment_entered', 3);
* event.trackFunnelProgression('checkout', 'order_confirmed', 4);
* ```
*/
trackFunnelProgression(funnelName, stepName, stepNumber, attributes) {
const enrichedAttributes = this.enrichWithTelemetryContext(attributes);
this.logger?.info({
funnel: funnelName,
stepName,
stepNumber,
attributes: enrichedAttributes
}, "Funnel progression tracked");
const stepAttributes = {
...enrichedAttributes,
step_name: stepName
};
if (stepNumber !== void 0) stepAttributes.step_number = stepNumber;
const stepAsStatus = stepName;
this.collector?.recordFunnelStep({
funnel: funnelName,
status: stepAsStatus,
attributes: stepAttributes,
service: this.serviceName,
timestamp: Date.now()
});
if (this.hasSubscribers) {
const autotelContext = this.buildAutotelContext();
const finalAttributes = this.enrichFromBaggage(enrichedAttributes);
this.notifySubscribers(async (subscriber) => {
await (subscriber.trackFunnelProgression ? subscriber.trackFunnelProgression(funnelName, stepName, stepNumber, finalAttributes, { autotel: autotelContext }) : subscriber.trackFunnelStep(funnelName, stepAsStatus, {
...finalAttributes,
...stepAttributes
}, { autotel: autotelContext }));
});
}
}
/**
* Track multiple events in a batch
*
* Useful for bulk event tracking with consistent timestamps.
* Events are sent to subscribers individually but processed together.
*
* @param events - Array of events to track
*
* @example
* ```typescript
* event.trackBatch([
* { name: 'item.viewed', attributes: { itemId: '1' } },
* { name: 'item.viewed', attributes: { itemId: '2' } },
* { name: 'cart.updated', attributes: { itemCount: 2 } },
* ]);
* ```
*/
trackBatch(events) {
for (const event of events) {
const filteredAttributes = event.attributes ? definedAttributes(event.attributes) : void 0;
this.trackEvent(event.name, filteredAttributes);
}
}
};
/** The attributes with the entries that carry nothing left out. */
function definedAttributes(input) {
const defined = {};
for (const [key, value] of Object.entries(input)) if (value !== void 0 && value !== null) defined[key] = value;
return defined;
}
/**
* Global events instances (singleton pattern)
*/
const eventsInstances = /* @__PURE__ */ new Map();
/**
* Get or create an Events instance for a service
*
* @param serviceName - Service name for identifying events
* @param logger - Optional logger
* @returns Events instance
*
* @example
* ```typescript
* const event =getEvents('job-application')
* events.trackEvent('application.submitted', { jobId: '123' })
* ```
*/
function getEvents(serviceName, logger) {
if (!eventsInstances.has(serviceName)) eventsInstances.set(serviceName, new Event(serviceName, { logger }));
return eventsInstances.get(serviceName);
}
/**
* Reset all events instances (mainly for testing)
*/
function resetEvents() {
eventsInstances.clear();
}
//#endregion
//#region src/shutdown.ts
/**
* Graceful shutdown with flush and cleanup
*/
/**
* Error codes that mean "the OTLP endpoint wasn't reachable" — expected and
* harmless when no collector is configured. Deliberately limited to
* connection-establishment failures (refused / DNS), not post-connection
* errors like ECONNRESET or ETIMEDOUT, which can indicate a real problem
* talking to a configured backend and should surface.
*/
const UNREACHABLE_ENDPOINT_CODES = /* @__PURE__ */ new Set([
"ECONNREFUSED",
"ENOTFOUND",
"EAI_AGAIN"
]);
function errorCode(cause) {
return require_values.asString(require_values.readProperty(cause, "code"));
}
/**
* A bare "Flush timeout" left you with a stack trace into bundled internals and
* nothing to act on. Name how long we waited and where we were sending, since
* the usual cause is that nothing is listening at that endpoint.
*/
function flushTimeoutMessage(timeout) {
const endpoint = getConfig()?.endpoint;
return `Flush timeout after ${timeout}ms${endpoint === void 0 ? "" : ` sending to ${endpoint}`}. No collector acknowledged the export; check that one is running and reachable.`;
}
/**
* True when the error (or every error it wraps) is an unreachable-endpoint
* failure. Traverses `AggregateError.errors` and the `cause` chain, since the
* SDK often wraps the underlying network error.
*/
function isUnreachableEndpointError(cause, depth = 0) {
if (depth > 5 || require_values.asRecord(cause) === void 0) return false;
if (cause instanceof AggregateError) return cause.errors.length > 0 && cause.errors.every((e) => isUnreachableEndpointError(e, depth + 1));
const code = errorCode(cause);
if (code && UNREACHABLE_ENDPOINT_CODES.has(code)) return true;
const wrapped = require_values.readProperty(cause, "cause");
return wrapped !== void 0 && wrapped !== cause ? isUnreachableEndpointError(wrapped, depth + 1) : false;
}
/**
* Flush all pending telemetry
*
* Flushes both events events and OpenTelemetry spans to their destinations.
* Includes timeout protection to prevent hanging in serverless environments.
*
* Safe to call multiple times.
*
* @param options - Optional configuration
* @param options.timeout - Timeout in milliseconds (default: 2000ms)
* @param options.forShutdown - If true, permanently disables the events queue after flush (used internally by shutdown())
*
* @example Manual flush in serverless
* ```typescript
* import { flush } from 'autotel';
*
* export const handler = async (event) => {
* // ... process event
* await flush(); // Flush before function returns
* return result;
* };
* ```
*
* @example With custom timeout
* ```typescript
* await flush({ timeout: 5000 }); // 5 second timeout
* ```
*/
async function flush(options) {
const timeout = options?.timeout ?? 2e3;
const forShutdown = options?.forShutdown ?? false;
const doFlush = async () => {
const eventsQueue = getEventQueue();
if (eventsQueue) await (forShutdown ? eventsQueue.shutdown() : eventsQueue.flush());
try {
const tracerProvider = require_tracer_provider.getForceFlushableProvider(getSdk());
if (tracerProvider) await tracerProvider.forceFlush();
} catch {}
};
let timeoutHandle;
try {
await Promise.race([doFlush().finally(() => {
if (timeoutHandle) clearTimeout(timeoutHandle);
}), new Promise((_, reject) => {
timeoutHandle = setTimeout(() => reject(new Error(flushTimeoutMessage(timeout))), timeout);
timeoutHandle.unref();
})]);
} catch (error) {
if (timeoutHandle) clearTimeout(timeoutHandle);
getLogger().error({ err: error instanceof Error ? error : new Error(String(error)) }, "[autotel] Flush error");
throw error;
}
}
/**
* Shutdown telemetry and cleanup resources
*
* - Flushes all pending data
* - Shuts down OpenTelemetry SDK
* - Cleans up resources
*
* Call this before process exit.
*
* Always performs cleanup even if flush fails, preventing resource leaks
* in serverless handlers or tests.
*
* @example Express server
* ```typescript
* const server = app.listen(3000)
*
* process.on('SIGTERM', async () => {
* await server.close()
* await shutdown()
* process.exit(0)
* })
* ```
*/
async function shutdown() {
const logger = getLogger();
let shutdownError = null;
try {
await flush({ forShutdown: true });
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
shutdownError = err;
logger.error({ err }, "[autotel] Flush failed during shutdown, continuing cleanup");
}
try {
const sdk = getSdk();
if (sdk) await sdk.shutdown();
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
if (!isUnreachableEndpointError(error)) {
if (!shutdownError) shutdownError = err;
logger.error({ err }, "[autotel] SDK shutdown failed");
}
} finally {
uninstallProcessHandlers();
await _closeEmbeddedDevtools();
const eventsQueue = getEventQueue();
if (require_values.isFunction(eventsQueue?.cleanup)) eventsQueue.cleanup();
resetEvents();
require_metric.resetMetrics();
resetEventQueue();
}
if (shutdownError) throw shutdownError;
}
//#endregion
//#region src/otlp-exporters.ts
let OTLPTraceExporterPROTO;
let OTLPMetricExporterPROTO;
let OTLPLogExporterPROTO;
let OTLPTraceExporterGRPC;
let OTLPMetricExporterGRPC;
let OTLPLogExporterGRPC;
/**
* Helper: Lazy-load gRPC trace exporter
*/
function loadGRPCTraceExporter() {
if (OTLPTraceExporterGRPC) return OTLPTraceExporterGRPC;
try {
OTLPTraceExporterGRPC = require_node_require.requireModule("@opentelemetry/exporter-trace-otlp-grpc").OTLPTraceExporter;
return OTLPTraceExporterGRPC;
} catch {
throw new Error("gRPC trace exporter not found. Install @opentelemetry/exporter-trace-otlp-grpc. It is an optional peer dependency, and bundlers (Vercel, Nitro, esbuild) do not follow the lazy require that loads it, so add it as a direct dependency of your application, not just of autotel.");
}
}
/**
* Helper: Lazy-load gRPC metric exporter
*/
function loadGRPCMetricExporter() {
if (OTLPMetricExporterGRPC) return OTLPMetricExporterGRPC;
try {
OTLPMetricExporterGRPC = require_node_require.requireModule("@opentelemetry/exporter-metrics-otlp-grpc").OTLPMetricExporter;
return OTLPMetricExporterGRPC;
} catch {
throw new Error("gRPC metric exporter not found. Install @opentelemetry/exporter-metrics-otlp-grpc. It is an optional peer dependency, and bundlers (Vercel, Nitro, esbuild) do not follow the lazy require that loads it, so add it as a direct dependency of your application, not just of autotel.");
}
}
/**
* Helper: Lazy-load protobuf trace exporter
*/
function loadProtoTraceExporter() {
if (OTLPTraceExporterPROTO) return OTLPTraceExporterPROTO;
try {
OTLPTraceExporterPROTO = require_node_require.requireModule("@opentelemetry/exporter-trace-otlp-proto").OTLPTraceExporter;
return OTLPTraceExporterPROTO;
} catch {
throw new Error("Protobuf trace exporter not found. Install @opentelemetry/exporter-trace-otlp-proto. It is an optional peer dependency, and bundlers (Vercel, Nitro, esbuild) do not follow the lazy require that loads it, so add it as a direct dependency of your application, not just of autotel. Or drop `protocol` to use the default JSON exporter, which ships with autotel and is accepted by Grafana Cloud, Honeycomb and the other hosted OTLP gateways.");
}
}
/**
* Helper: Create trace exporter based on protocol
*/
function createTraceExporter(protocol, config) {
if (protocol === "grpc") return new (loadGRPCTraceExporter())(config);
if (protocol === "http/protobuf") return new (loadProtoTraceExporter())(config);
return new _opentelemetry_exporter_trace_otlp_http.OTLPTraceExporter(config);
}
/**
* Helper: Create metric exporter based on protocol
*/
function createMetricExporter(protocol, config) {
if (protocol === "grpc") return new (loadGRPCMetricExporter())(config);
if (protocol === "http/protobuf") {
if (!OTLPMetricExporterPROTO) try {
OTLPMetricExporterPROTO = require_node_require.requireModule("@opentelemetry/exporter-metrics-otlp-proto").OTLPMetricExporter;
} catch {
throw new Error("Protobuf metric exporter not found. Install @opentelemetry/exporter-metrics-otlp-proto. It is an optional peer dependency, and bundlers (Vercel, Nitro, esbuild) do not follow the lazy require that loads it, so add it as a direct dependency of your application, not just of autotel. Or drop `protocol` to use the default JSON exporter, which ships with autotel and is accepted by Grafana Cloud, Honeycomb and the other hosted OTLP gateways.");
}
return new OTLPMetricExporterPROTO(config);
}
return new _opentelemetry_exporter_metrics_otlp_http.OTLPMetricExporter(config);
}
/**
* Helper: Lazy-load gRPC log exporter
*/
function loadGRPCLogExporter() {
if (OTLPLogExporterGRPC) return OTLPLogExporterGRPC;
try {
OTLPLogExporterGRPC = require_node_require.requireModule("@opentelemetry/exporter-logs-otlp-grpc").OTLPLogExporter;
return OTLPLogExporterGRPC;
} catch {
throw new Error("gRPC log exporter not found. Install @opentelemetry/exporter-logs-otlp-grpc. It is an optional peer dependency, and bundlers (Vercel, Nitro, esbuild) do not follow the lazy require that loads it, so add it as a direct dependency of your application, not just of autotel.");
}
}
/**
* Helper: Create log exporter based on protocol
*/
function createLogExporter(protocol, config) {
if (protocol === "grpc") return new (loadGRPCLogExporter())(config);
if (protocol === "http/protobuf") {
if (!OTLPLogExporterPROTO) try {
OTLPLogExporterPROTO = require_node_require.requireModule("@opentelemetry/exporter-logs-otlp-proto").OTLPLogExporter;
} catch {
throw new Error("Protobuf log exporter not found. Install @opentelemetry/exporter-logs-otlp-proto. It is an optional peer dependency, and bundlers (Vercel, Nitro, esbuild) do not follow the lazy require that loads it, so add it as a direct dependency of your application, not just of autotel. Or drop `protocol` to use the default JSON exporter, which ships with autotel and is accepted by Grafana Cloud, Honeycomb and the other hosted OTLP gateways.");
}
return new OTLPLogExporterPROTO(config);
}
return new _opentelemetry_exporter_logs_otlp_http.OTLPLogExporter(config);
}
/**
* Helper: Resolve protocol from config and environment
*/
function resolveProtocol(configProtocol) {
if (configProtocol === "grpc" || configProtocol === "http" || configProtocol === "http/protobuf") return configProtocol;
const envProtocol = process.env.OTEL_EXPORTER_OTLP_PROTOCOL;
if (envProtocol === "grpc") return "grpc";
if (envProtocol === "http/protobuf") return "http/protobuf";
if (envProtocol === "http/json" || envProtocol === "http") return "http";
return "http";
}
/**
* Helper: Adjust endpoint URL for protocol
* gRPC exporters don't need the /v1/traces or /v1/metrics path
* HTTP exporters need the full path
*/
function formatEndpointUrl(endpoint, signal, protocol) {
if (protocol === "grpc") return endpoint.replace(/\/(v1\/)?(traces|metrics|logs)$/, "");
if (!endpoint.endsWith(`/v1/${signal}`)) return `${endpoint}/v1/${signal}`;
return endpoint;
}
//#endregion
//#region src/config-resolution.ts
/**
* Resolve the effective attribute redactor. Explicit config wins (`false`
* disables). Otherwise the `AUTOTEL_REDACT_PII` env var controls it, and as a
* final default PII redaction is auto-enabled in production.
*/
function resolveAttributeRedactor(explicit, environment) {
if (explicit === false) return void 0;
if (explicit !== void 0) return explicit;
const flag = process.env.AUTOTEL_REDACT_PII?.trim().toLowerCase();
if (flag) {
if ([
"off",
"false",
"0",
"none",
"disabled"
].includes(flag)) return;
if (flag === "default" || flag === "strict" || flag === "pci-dss") return flag;
if ([
"on",
"true",
"1",
"enabled"
].includes(flag)) return "default";
}
return environment === "production" ? "default" : void 0;
}
/**
* Read a duration-in-milliseconds environment variable, ignoring anything that
* is not a positive number so a typo falls back to the SDK default rather than
* throwing at startup.
*/
function readMillisEnv(name) {
const raw = process.env[name]?.trim();
if (!raw) return void 0;
const value = Number(raw);
return Number.isFinite(value) && value > 0 ? value : void 0;
}
function detectEnvironmentAttributes() {
const attrs = {};
const commitSha = process.env.COMMIT_SHA || process.env.GITHUB_SHA || process.env.VERCEL_GIT_COMMIT_SHA || process.env.CF_PAGES_COMMIT_SHA || process.env.AWS_CODEPIPELINE_EXECUTION_ID;
if (commitSha) attrs["service.commit.sha"] = commitSha;
const region = process.env.VERCEL_REGION || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || process.env.FLY_REGION || process.env.CF_REGION || process.env.GOOGLE_CLOUD_REGION;
if (region) attrs["service.region"] = region;
const version = process.env.APP_VERSION || process.env.HEROKU_RELEASE_VERSION || process.env.VERCEL_GIT_COMMIT_REF;
if (version) attrs["service.deploy.version"] = version;
return attrs;
}
/**
* Resolve metrics flag with env var override support
*/
function resolveMetricsFlag(configFlag = "auto") {
const envFlag = process.env.AUTOTEL_METRICS;
if (envFlag === "on" || envFlag === "true") return true;
if (envFlag === "off" || envFlag === "false") return false;
if (configFlag === true) return true;
if (configFlag === false) return false;
return true;
}
/**
* Resolve logs flag with env var override support.
* Defaults to disabled (opt-in only) to avoid unexpected log export
* and to preserve the upstream SDK's OTEL_LOGS_EXPORTER handling.
*/
function resolveLogsFlag(configFlag = "auto") {
const envFlag = process.env.AUTOTEL_LOGS;
if (envFlag === "on" || envFlag === "true") return true;
if (envFlag === "off" || envFlag === "false") return false;
if (configFlag === true) return true;
if (configFlag === false) return false;
return false;
}
/**
* Resolve debug flag with env var override support
*
* Supports:
* - `'pretty'`: Colorized, hierarchical output (PrettyConsoleExporter)
* - `true` / `'true'` / `'1'`: Raw JSON output (ConsoleSpanExporter)
* - `false` / `'false'` / `'0'`: Disabled
*/
function resolveDebugFlag(configFlag) {
const envFlag = process.env.AUTOTEL_DEBUG;
if (envFlag === "pretty") return "pretty";
if (envFlag === "true" || envFlag === "1") return true;
if (envFlag === "false" || envFlag === "0") return false;
return configFlag ?? false;
}
function normalizeOtlpHeaders(headers) {
if (!headers) return void 0;
if (typeof headers !== "string") return headers;
const parsed = {};
for (const pair of headers.split(",")) {
const [key, ...valueParts] = pair.split("=");
if (!key || valueParts.length === 0) continue;
parsed[key.trim()] = valueParts.join("=").trim();
}
return parsed;
}
function resolveOtlpDestinations(config, fallbackEndpoint) {
return (config.destinations === void 0 ? fallbackEndpoint ? [{
endpoint: fallbackEndpoint,
headers: config.headers,
protocol: config.protocol
}] : [] : config.destinations).map((destination) => ({
endpoint: destination.endpoint,
protocol: resolveProtocol(destination.protocol ?? config.protocol),
headers: normalizeOtlpHeaders(destination.headers ?? config.headers),
signals: destination.signals ? new Set(destination.signals) : void 0
}));
}
function destinationSupportsSignal(destination, signal) {
return destination.signals ? destination.signals.has(signal) : true;
}
/**
* Auto-detect version from package.json
*/
function detectVersion() {
try {
const fs = require_node_require.requireModule("node:fs");
return JSON.parse(fs.readFileSync(`${process.cwd()}/package.json`, "utf8")).version || "1.0.0";
} catch {
return "1.0.0";
}
}
/**
* Detect hostname for resource attributes.
* Supports Datadog conventions (DD_HOSTNAME) and falls back to system hostname.
*
* Priority order:
* 1. DD_HOSTNAME environment variable (Datadog convention)
* 2. HOSTNAME environment variable (common Unix convention)
* 3. os.hostname() (system hostname)
*
* @returns hostname string or undefined if detection fails
*/
function detectHostname() {
if (process.env.DD_HOSTNAME) return process.env.DD_HOSTNAME;
if (process.env.HOSTNAME) return process.env.HOSTNAME;
try {
return require_node_require.requireModule("node:os").hostname();
} catch {
return;
}
}
//#endregion
//#region src/auto-instrumentations.ts
/**
* Extract instrumentation class names from instrumentation instances
* Used to detect duplicates between manual and auto instrumentations
*/
function getInstrumentationNames(instrumentations) {
const names = /* @__PURE__ */ new Set();
if (!instrumentations) return names;
for (const instrumentation of instrumentations) {
const className = require_values.asFunction(require_values.readProperty(instrumentation, "constructor"))?.name;
if (className) names.add(className);
}
return names;
}
/**
* Map common instrumentation class names to their package names
* Used to disable auto-instrumentations when user provides manual configs
*/
const INSTRUMENTATION_CLASS_TO_PACKAGE = new Map(Object.entries({
HttpInstrumentation: "@opentelemetry/instrumentation-http",
HttpsInstrumentation: "@opentelemetry/instrumentation-http",
ExpressInstrumentation: "@opentelemetry/instrumentation-express",
FastifyInstrumentation: "@opentelemetry/instrumentation-fastify",
MongoDBInstrumentation: "@opentelemetry/instrumentation-mongodb",
MongooseInstrumentation: "@opentelemetry/instrumentation-mongoose",
PrismaInstrumentation: "@opentelemetry/instrumentation-prisma",
PinoInstrumentation: "@opentelemetry/instrumentation-pino",
WinstonInstrumentation: "@opentelemetry/instrumentation-winston",
RedisInstrumentation: "@opentelemetry/instrumentation-redis",
GraphQLInstrumentation: "@opentelemetry/instrumentation-graphql",
GrpcInstrumentation: "@opentelemetry/instrumentation-grpc",
IORedisInstrumentation: "@opentelemetry/instrumentation-ioredis",
KnexInstrumentation: "@opentelemetry/instrumentation-knex",
NestJsInstrumentation: "@opentelemetry/instrumentation-nestjs-core",
PgInstrumentation: "@opentelemetry/instrumentation-pg",
MySQLInstrumentation: "@opentelemetry/instrumentation-mysql",
MySQL2Instrumentation: "@opentelemetry/instrumentation-mysql2"
}));
/**
* What autotel configures when the caller has not said otherwise.
*
* `instrumentation-express` opens a span per layer and runs the layer under it,
* so whatever is active inside a middleware is that layer's span - one that
* ends the moment `next()` fires. Request-wide attributes set there would land
* on `middleware - anonymous` rather than on the request span every backend
* shows as the resource. Ignoring the two leaf layer types puts them back on
* the request span and drops a pile of noise spans with them; the route rename
* (`GET /users/:id`) survives, because `rpcMetadata.route` is assigned before
* the ignore check.
*
* Pass your own `ignoreLayersType` - `[]` for the upstream behaviour - to
* override it, or `ignoreLayers` to silence only the noisy paths.
*/
const AUTOTEL_DEFAULTS = { "@opentelemetry/instrumentation-express": { ignoreLayersType: ["middleware", "request_handler"] } };
/** Apply {@link AUTOTEL_DEFAULTS} under whatever the caller already chose. */
function withAutotelDefaults(config) {
const merged = { ...config };
for (const [packageName, defaults] of Object.entries(AUTOTEL_DEFAULTS)) {
const options = merged[packageName];
if (options?.enabled === false) continue;
merged[packageName] = {
...defaults,
...options
};
}
return merged;
}
/**
* `express` and `@opentelemetry/instrumentation-express` name the same thing.
* `getNodeAutoInstrumentations` only answers to the full name - it
* `diag.error`s anything else and ignores the config - so the short form every
* autotel example uses has to be expanded before it gets there.
*/
function toPackageName(name) {
return name.startsWith("@opentelemetry/instrumentation-") ? name : `@opentelemetry/instrumentation-${name}`;
}
/**
* Detect if we're running in ESM mode
*/
function isESMMode() {
try {
const fs = require_node_require.requireModule("node:fs");
try {
return JSON.parse(fs.readFileSync(`${process.cwd()}/package.json`, "utf8")).type === "module";
} catch {
return false;
}
} catch {
return false;
}
}
/**
* Lazy-load auto-instrumentations (optional peer dependency)
* Only loads when integrations config is truthy, avoiding ~40+ package imports at startup.
*/
function loadNodeAutoInstrumentations() {
try {
return require_node_require.requireModule("@opentelemetry/auto-instrumentations-node").getNodeAutoInstrumentations;
} catch {
const isESM = isESMMode();
const baseMessage = "@opentelemetry/auto-instrumentations-node not found.";
if (isESM) throw new Error(`${baseMessage}\n\nESM Setup Required:
1. Install as a direct dependency: pnpm add @opentelemetry/auto-instrumentations-node
2. Create instrumentation.mjs with:
import 'autotel/register'; // MUST be first!
import { init } from 'autotel';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
init({ service: "my-app", instrumentations: getNodeAutoInstrumentations() });
3. Run with: tsx --import ./instrumentation.mjs src/index.ts
See: https://github.com/jagreehal/autotel#esm-setup`);
throw new Error(`${baseMessage} Install it: pnpm add @opentelemetry/auto-instrumentations-node`);
}
}
/**
* Injectable loader for testing. Set to override the default loader.
* @internal
*/
let _autoInstrumentationsLoader = null;
/**
* Get auto-instrumentations based on simple integration names
* Excludes instrumentations that are manually provided to avoid conflicts
*/
function getAutoInstrumentations(integrations, manualInstrumentationNames = /* @__PURE__ */ new Set()) {
if (integrations === false) return [];
const getNodeAutoInstrumentations = _autoInstrumentationsLoader ? _autoInstrumentationsLoader() : loadNodeAutoInstrumentations();
const exclusionConfig = {};
for (const className of manualInstrumentationNames) {
const packageName = INSTRUMENTATION_CLASS_TO_PACKAGE.get(className);
if (packageName) exclusionConfig[packageName] = { enabled: false };
}
const config = { ...exclusionConfig };
const requested = integrations === true ? [] : Array.isArray(integrations) ? integrations.map((name) => [name, { enabled: true }]) : Object.entries(integrations);
for (const [name, options] of requested) {
const packageName = toPackageName(name);
if (packageName in exclusionConfig) continue;
config[packageName] = options;
}
return getNodeAutoInstrumentations(withAutotelDefaults(config));
}
//#endregion
//#region src/init.ts
/**
* Simplified initialization for autotel
*
* Single init() function with sensible defaults.
* Replaces initInstrumentation() and separate events config.
*/
/**
* Records nothing and exports nothing, so a `TracerProvider` is still
* registered when there is no destination. See where it is used below.
*/
var NoopSpanProcessor = class {
onStart() {}
onEnd() {}
forceFlush() {
return Promise.resolve();
}
shutdown() {
return Promise.resolve();
}
};
/**
* Adapts an Autotel Sampler to the OTel SDK Sampler interface.
*/
function toOtelSampler(sampler) {
return {
shouldSample(_context, _traceId, spanName, _spanKind, _attributes, links) {
const samplingContext = {
operationName: spanName,
args: [],
links
};
const shouldTrace = sampler.shouldSample(samplingContext);
const rate = sampler.sampleRate?.(samplingContext);
const result = { decision: shouldTrace ? _opentelemetry_sdk_trace_base.SamplingDecision.RECORD_AND_SAMPLED : _opentelemetry_sdk_trace_base.SamplingDecision.NOT_RECORD };
if (shouldTrace && rate !== void 0 && rate > 1) result.attributes = { [AUTOTEL_SAMPLING_RATE]: rate };
return result;
},
toString() {
return `AutotelSamplerAdapter`;
}
};
}
let initialized = false;
let locked = false;
let config = null;
let sdk = null;
let warnedOnce = false;
let logger = silentLogger;
let validationConfig = null;
let eventsConfig = null;
let _stringRedactor = null;
function acceptsStringRedactor(subscriber) {
return require_values.isFunction(require_values.readProperty(subscriber, "setStringRedactor")) ? subscriber : void 0;
}
let _optionalRequire = require_node_require.safeRequire;
let _devtoolsClose = null;
/**
* Lock the logger to prevent further `init()` calls.
* Use this when framework plugins set up instrumentation and you want
* to prevent accidental re-initialization from user code.
*/
function lockLogger() {
locked = true;
}
/**
* Check if the logger has been locked.
*/
function isLoggerLocked() {
return locked;
}
/**
* Initialize autotel - Write Once, Observe Everywhere
*
* Follows OpenTelemetry standards: opinionated defaults with full flexibility
* Idempotent: multiple calls are safe, last one wins
*
* @example Minimal setup (OTLP default)
* ```typescript
* init({ service: 'my-app' })
* ```
*
* @example With events (observe in PostHog, Mixpanel, etc.)
* ```typescript
* import { PostHogSubscriber } from 'autotel-posthog/subscriber';
*
* init({
* service: 'my-app',
* subscribers: [new PostHogSubscriber({ apiKey: '...' })]
* })
* ```
*
* @example Observe in Jaeger
* ```typescript
* import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
*
* init({
* service: 'my-app',
* spanExporter: new JaegerExporter({ endpoint: 'http://localhost:14268/api/traces' })
* })
* ```
*
* @example Observe in Zipkin
* ```typescript
* import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
*
* init({
* service: 'my-app',
* spanExporter: new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' })
* })
* ```
*
* @example Observe in Datadog
* ```typescript
* import { DatadogSpanProcessor } from '@opentelemetry/exporter-datadog'
*
* init({
* service: 'my-app',
* spanProcessor: new DatadogSpanProcessor({ ... })
* })
* ```
*
* @example Console output (dev)
* ```typescript
* import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
*
* init({
* service: 'my-app',
* spanProcessor: new SimpleSpanProcessor(new ConsoleSpanExporter())
* })
* ```
*/
function init(cfg) {
if (locked) return;
const envConfig = resolveConfigFromEnv();
const yamlConfig = loadYamlConfig() ?? {};
const mergedConfig = {
...envConfig,
...yamlConfig,
...cfg,
resourceAttributes: {
...envConfig.resourceAttributes,
...yamlConfig.resourceAttributes,
...detectEnvironmentAttributes(),
...cfg.resourceAttributes
},
headers: cfg.headers ?? yamlConfig.headers ?? envConfig.headers
};
const resolvedRedactor = resolveAttributeRedactor(mergedConfig.attributeRedactor, mergedConfig.environment || process.env.NODE_ENV || "development");
if (resolvedRedactor === void 0) mergedConfig.attributeRedactor = void 0;
else {
const normalizedRedactor = require_attribute_redacting_processor.normalizeAttributeRedactorConfig(resolvedRedactor);
if (!normalizedRedactor) throw new Error("Invalid attributeRedactor config");
mergedConfig.attributeRedactor = normalizedRedactor;
}
const devtoolsConfig = resolveDevtoolsConfig(mergedConfig.devtools);
if (devtoolsConfig.enabled && mergedConfig.logs === void 0) mergedConfig.logs = true;
const silent = mergedConfig.silent ?? false;
const minLevel = mergedConfig.minLevel ?? "info";
logger = wrapLogger(mergedConfig.logger || silentLogger, silent, minLevel);
if (initialized) logger.warn({}, "[autotel] init() called again - last config wins. This may cause unexpected behavior.");
config = mergedConfig;
validationConfig = mergedConfig.validation || null;
eventsConfig = mergedConfig.events || null;
let endpoint = mergedConfig.endpoint ?? devtoolsConfig.endpoint;
const version = mergedConfig.version || detectVersion();
const environment = mergedConfig.environment || process.env.NODE_ENV || "development";
const metricsEnabled = resolveMetricsFlag(mergedConfig.metrics);
const canonicalLogger = mergedConfig.canonicalLogLines?.logger ?? mergedConfig.logger;
const hasCanonicalLogger = Array.isArray(canonicalLogger) ? canonicalLogger.length > 0 : canonicalLogger !== void 0;
const canonicalUsesOtel = mergedConfig.canonicalLogLines?.enabled === true && (mergedConfig.canonicalLogLines.otel ?? !hasCanonicalLogger);
const logsEnabled = resolveLogsFlag(mergedConfig.logs ?? (canonicalUsesOtel ? true : "auto"));
if (devtoolsConfig.enabled && devtoolsConfig.embedded) {
const devtoolsModule = _optionalRequire("autotel-devtools");
if (devtoolsModule?.createDevtools) {
const devtoolsInstance = devtoolsModule.createDevtools({
port: devtoolsConfig.port,
host: devtoolsConfig.host,
verbose: devtoolsConfig.verbose
});
_devtoolsClose = devtoolsInstance.close;
endpoint = `http://${devtoolsConfig.host}:${devtoolsInstance.port}`;
logger.info({}, `[autotel] autotel-devtools embedded server started at ${endpoint}`);
} else logger.warn({}, "[autotel] devtools.embedded requested but autotel-devtools is not installed. Falling back to endpoint-only mode.");
}
const hostname = detectHostname();
let resource = (0, _opentelemetry_resources.resourceFromAttributes)({
[_opentelemetry_semantic_conventions.ATTR_SERVICE_NAME]: mergedConfig.service,
[_opentelemetry_semantic_conventions.ATTR_SERVICE_VERSION]: version,
"deployment.environment": environment,
"deployment.environment.name": environment
});
if (hostname) resource = resource.merge((0, _opentelemetry_resources.resourceFromAttributes)({
"host.name": hostname,
"datadog.host.name": hostname
}));
if (mergedConfig.resource) resource = resource.merge(mergedConfig.resource);
if (mergedConfig.resourceAttributes) resource = resource.merge((0, _opentelemetry_resources.resourceFromAttributes)(mergedConfig.resourceAttributes));
const otlpDestinations = resolveOtlpDestinations(mergedConfig, endpoint);
const configuredSpanProcessors = mergedConfig.spanProcessors !== void 0 ? mergedConfig.spanProcessors : mergedConfig.spanProcessor ? [mergedConfig.spanProcessor] : void 0;
const configuredSpanExporters = mergedConfig.spanExporters && mergedConfig.spanExporters.length > 0 ? mergedConfig.spanExporters : mergedConfig.spanExporter ? [mergedConfig.spanExporter] : void 0;
const configuredMetricReaders = mergedConfig.metricReaders && mergedConfig.metricReaders.length > 0 ? mergedConfig.metricReaders : mergedConfig.metricReader ? [mergedConfig.metricReader] : void 0;
const configuredLogRecordProcessors = mergedConfig.logRecordProcessors && mergedConfig.logRecordProcessors.length > 0 ? mergedConfig.logRecordProcessors : mergedConfig.logRecordProcessor ? [mergedConfig.logRecordProcessor] : void 0;
let spanProcessors = [];
if (configuredSpanProcessors !== void 0) spanProcessors.push(...configuredSpanProcessors);
else if (configuredSpanExporters && configuredSpanExporters.length > 0) for (const exporter of configuredSpanExporters) spanProcessors.push(new TailSamplingSpanProcessor(new _opentelemetry_sdk_trace_base.BatchSpanProcessor(exporter)));
else for (const destination of otlpDestinations) {
if (!destinationSupportsSignal(destination, "traces")) continue;
const traceExporter = createTraceExporter(destination.protocol, {
url: formatEndpointUrl(destination.endpoint, "traces", destination.protocol),
headers: destination.headers
});
spanProcessors.push(new TailSamplingSpanProcessor(new _opentelemetry_sdk_trace_base.BatchSpanProcessor(traceExporter)));
}
if (mergedConfig.baggage || mergedConfig.baggage === "") {
const prefix = typeof mergedConfig.baggage === "string" ? mergedConfig.baggage ? `${mergedConfig.baggage}.` : "" : "baggage.";
spanProcessors.push(new BaggageSpanProcessor({ prefix }));
}
const debugMode = resolveDebugFlag(mergedConfig.debug);
if (debugMode === "pretty") spanProcessors.push(new _opentelemetry_sdk_trace_base.SimpleSpanProcessor(new require_pretty_console_exporter.PrettyConsoleExporter()));
else if (debugMode === true) spanProcessors.push(new _opentelemetry_sdk_trace_base.SimpleSpanProcessor(new _opentelemetry_sdk_trace_base.ConsoleSpanExporter()));
if (mergedConfig.canonicalLogLines?.enabled) {
const canonicalOptions = {
logger: canonicalLogger,
otel: mergedConfig.canonicalLogLines.otel,
rootSpansOnly: mergedConfig.canonicalLogLines.rootSpansOnly,
minLevel: mergedConfig.canonicalLogLines.minLevel,
messageFormat: mergedConfig.canonicalLogLines.messageFormat,
includeResourceAttributes: mergedConfig.canonicalLogLines.includeResourceAttributes,
shouldEmit: mergedConfig.canonicalLogLines.shouldEmit,
keep: mergedConfig.canonicalLogLines.keep,
drain: mergedConfig.canonicalLogLines.drain,
onDrainError: mergedConfig.canonicalLogLines.onDrainError,
pretty: mergedConfig.canonicalLogLines.pretty
};
spanProcessors.push(new require_canonical_log_line_processor.CanonicalLogLineProcessor(canonicalOptions));
}
if (mergedConfig.attributeRedactor && spanProcessors.length > 0) {
const redactor = mergedConfig.attributeRedactor;
spanProcessors = spanProcessors.map((processor) => new require_attribute_redacting_processor.AttributeRedactingProcessor(processor, { redactor }));
}
if (mergedConfig.attributeRedactor) _stringRedactor = createStringRedactor(mergedConfig.attributeRedactor);
if (_stringRedactor && mergedConfig.subscribers) for (const subscriber of mergedConfig.subscribers) acceptsStringRedactor(subscriber)?.setStringRedactor(_stringRedactor);
if (mergedConfig.spanNameNormalizer && spanProcessors.length > 0) spanProcessors = spanProcessors.map((processor) => new require_span_name_normalizer.SpanNameNormalizingProcessor(processor, { normalizer: mergedConfig.spanNameNormalizer }));
if (mergedConfig.policies) {
if (typeof mergedConfig.policies === "string") require_policy.watchPolicyFile(mergedConfig.policies);
else require_policy.setPolicies(mergedConfig.policies);
const userSpanFilter = mergedConfig.spanFilter;
mergedConfig.spanFilter = userSpanFilter ? (span) => userSpanFilter(span) && require_policy.policySpanFilter(span) : require_policy.policySpanFilter;
}
if (mergedConfig.spanFilter && spanProcessors.length > 0) spanProcessors = spanProcessors.map((processor) => new require_filtering_span_processor.FilteringSpanProcessor(processor, { filter: mergedConfig.spanFilter }));
if (mergedConfig.spanEnrichers && mergedConfig.spanEnrichers.length > 0) spanProcessors.unshift(...mergedConfig.spanEnrichers);
const metricReaders = [];
if (configuredMetricReaders && configuredMetricReaders.length > 0) metricReaders.push(...configuredMetricReaders);
else if (metricsEnabled) for (const destination of otlpDestinations) {
if (!destinationSupportsSignal(destination, "metrics")) continue;
const metricExporter = createMetricExporter(destination.protocol, {
url: formatEndpointUrl(destination.endpoint, "metrics", destination.protocol),
headers: destination.headers
});
const exportIntervalMillis = readMillisEnv("OTEL_METRIC_EXPORT_INTERVAL");
const exportTimeoutMillis = readMillisEnv("OTEL_METRIC_EXPORT_TIMEOUT");
const readerOptions = { exporter: metricExporter };
if (exportIntervalMillis !== void 0) readerOptions.exportIntervalMillis = exportIntervalMillis;
if (exportTimeoutMillis !== void 0) readerOptions.exportTimeoutMillis = exportTimeoutMillis;
metricReaders.push(new _opentelemetry_sdk_metrics.PeriodicExportingMetricReader(readerOptions));
}
let logRecordProcessors;
if (configuredLogRecordProcessors && configuredLogRecordProcessors.length > 0) logRecordProcessors = [...configuredLogRecordProcessors];
if (logsEnabled) {
for (const destination of otlpDestinations) {
if (!destinationSupportsSignal(destination, "logs")) continue;
let processor = new _opentelemetry_sdk_logs.BatchLogRecordProcessor({ exporter: createLogExporter(destination.protocol, {
url: formatEndpointUrl(destination.endpoint, "logs", destination.protocol),
headers: destination.headers
}) });
if (_stringRedactor) processor = new RedactingLogRecordProcessor(processor, _stringRedactor);
if (!logRecordProcessors) logRecordProcessors = [];
logRecordProcessors.push(processor);
}
if (otlpDestinations.some((destination) => destinationSupportsSignal(destination, "logs"))) logger.info({}, "[autotel] OTLP log exporter configured");
}
const posthogProcessors = buildPostHogLogProcessors(mergedConfig.posthog, _stringRedactor);
if (posthogProcessors.length > 0) {
if (!logRecordProcessors) logRecordProcessors = [];
logRecordProcessors.push(...posthogProcessors);
logger.info({}, "[autotel] PostHog OTLP logs configured");
}
if (mergedConfig.policies && logRecordProcessors) logRecordProcessors = logRecordProcessors.map((processor) => new require_policy.PolicyLogRecordProcessor(processor));
let finalInstrumentations = mergedConfig.instrumentations ? [...mergedConfig.instrumentations] : [];
if (mergedConfig.autoInstrumentations !== void 0 && mergedConfig.autoInstrumentations !== false) {
if (isESMMode()) logger.info({}, "[autotel] ESM mode detected. For auto-instrumentation to work:\n 1. Install @opentelemetry/auto-instrumentations-node as a direct dependency\n 2. Import autotel/register FIRST in your instrumentation file\n 3. Use getNodeAutoInstrumentations() directly instead of autoInstrumentations\n See: https://github.com/jagreehal/autotel#esm-setup");
try {
const manualInstrumentationNames = getInstrumentationNames(mergedConfig.instrumentations ?? []);
if (manualInstrumentationNames.size > 0) {
const manualNames = [...manualInstrumentationNames].join(", ");
logger.info({}, `[autotel] Detected manual instrumentations (${manualNames}). These will take precedence over auto-instrumentations. Tip: Set autoInstrumentations:false if you want full manual control, or remove manual configs to use auto-instrumentations.`);
}
const autoInstrumentations = getAutoInstrumentations(mergedConfig.autoInstrumentations, manualInstrumentationNames);
if (autoInstrumentations && autoInstrumentations.length > 0) finalInstrumentations = [...finalInstrumentations, ...autoInstrumentations];
} catch (error) {
logger.warn({}, `[autotel] Failed to configure auto-instrumentations: ${error instanceof Error ? error.message : String(error)}`);
}
}
const autotelSampler = mergedConfig.sampler ?? (mergedConfig.sampling ? resolveSamplingPreset(mergedConfig.sampling) : void 0);
if (autotelSampler) mergedConfig.sampler = autotelSampler;
const sampler = autotelSampler ? toOtelSampler(autotelSampler) : envConfig.otelSampler ?? toOtelSampler(samplingPresets.production());
const sdkOptions = {
resource,
serviceName: mergedConfig.service,
sampler,
instrumentations: finalInstrumentations
};
sdkOptions.spanProcessors = spanProcessors.length > 0 ? spanProcessors : [new NoopSpanProcessor()];
if (metricReaders.length > 0) sdkOptions.metricReaders = metricReaders;
if (logRecordProcessors && logRecordProcessors.length > 0) sdkOptions.logRecordProcessors = logRecordProcessors;
else if (!process.env.OTEL_LOGS_EXPORTER) sdkOptions.logRecordProcessors = [];
sdk = mergedConfig.sdkFactory ? mergedConfig.sdkFactory(sdkOptions) : new _opentelemetry_sdk_node.NodeSDK(sdkOptions);
if (!sdk) throw new Error("[autotel] sdkFactory must return a NodeSDK instance");
sdk.start();
if (mergedConfig.openllmetry?.enabled) {
const traceloop = _optionalRequire("@traceloop/node-server-sdk");
if (traceloop) {
const initOptions = { ...mergedConfig.openllmetry.options };
const getTracerProvider = require_values.asFunction(require_values.readProperty(sdk, "getTracerProvider"));
if (getTracerProvider) initOptions.tracerProvider = getTracerProvider.call(sdk);
if (configuredSpanExporters?.[0]) initOptions.exporter = configuredSpanExporters[0];
if (require_values.isFunction(traceloop.initialize)) {
traceloop.initialize(initOptions);
logger.info({}, "[autotel] OpenLLMetry initialized successfully");
} else logger.warn({}, "[autotel] OpenLLMetry initialize function not found. Check @traceloop/node-server-sdk version.");
} else logger.warn({}, "[autotel] OpenLLMetry enabled but @traceloop/node-server-sdk is not installed. Install it as a peer dependency to use OpenLLMetry integration.");
}
initialized = true;
const processHandlers = mergedConfig.processHandlers;
const handlersConfig = processHandlers && processHandlers !== true ? processHandlers : {};
if (processHandlers) installProcessHandlers(handlersConfig, shutdown);
else uninstallProcessHandlers();
if (mergedConfig.flushOnExit !== false) {
const timeoutMs = handlersConfig.shutdownTimeoutMs;
installExitFlush(() => flush({
forShutdown: true,
timeout: timeoutMs
}), timeoutMs);
}
}
/**
* Check if autotel has been initialized
*/
function isInitialized() {
return initialized;
}
/**
* Get current config (internal use)
*/
function getConfig() {
return config;
}
/**
* Get current logger (internal use)
*/
function getLogger() {
return logger;
}
/**
* Get validation config (internal use)
*/
function getValidationConfig() {
return validationConfig;
}
/**
* Get events config (internal use)
*/
function getEventsConfig() {
return eventsConfig;
}
/**
* Warn once if not initialized (same behavior in all environments)
*/
function warnIfNotInitialized(context) {
if (!initialized && !warnedOnce) {
logger.warn({}, `[autotel] ${context} used before init() called. Call init({ service: "..." }) first. See: https://docs.autotel.dev/quickstart`);
warnedOnce = true;
}
}
/**
* The sampler `init()` was configured with, or undefined when it was not.
*
* Separate from {@link getDefaultSampler} on purpose: the tracing wrapper needs
* to know whether a sampler was *chosen*. Falling back to the production preset
* there would start dropping spans for every app that never asked to sample.
*
* @internal
*/
function getConfiguredSampler() {
return config?.sampler;
}
/**
* @internal Close embedded devtools if running.
*/
async function _closeEmbeddedDevtools() {
if (_devtoolsClose) {
await _devtoolsClose();
_devtoolsClose = null;
}
}
/**
* Get SDK instance (for shutdown)
*/
function getSdk() {
return sdk;
}
//#endregion
Object.defineProperty(exports, 'AUTOTEL_DEBUG_BAGGAGE_KEY', {
enumerable: true,
get: function () {
return AUTOTEL_DEBUG_BAGGAGE_KEY;
}
});
Object.defineProperty(exports, 'AUTOTEL_SAMPLING_RATE', {
enumerable: true,
get: function () {
return AUTOTEL_SAMPLING_RATE;
}
});
Object.defineProperty(exports, 'AUTOTEL_SAMPLING_TAIL_EVALUATED', {
enumerable: true,
get: function () {
return AUTOTEL_SAMPLING_TAIL_EVALUATED;
}
});
Object.defineProperty(exports, 'AUTOTEL_SAMPLING_TAIL_KEEP', {
enumerable: true,
get: function () {
return AUTOTEL_SAMPLING_TAIL_KEEP;
}
});
Object.defineProperty(exports, 'AdaptiveSampler', {
enumerable: true,
get: function () {
return AdaptiveSampler;
}
});
Object.defineProperty(exports, 'AlwaysSampler', {
enumerable: true,
get: function () {
return AlwaysSampler;
}
});
Object.defineProperty(exports, 'BaggageSpanProcessor', {
enumerable: true,
get: function () {
return BaggageSpanProcessor;
}
});
Object.defineProperty(exports, 'CORRELATION_ID_BAGGAGE_KEY', {
enumerable: true,
get: function () {
return CORRELATION_ID_BAGGAGE_KEY;
}
});
Object.defineProperty(exports, 'CompositeSampler', {
enumerable: true,
get: function () {
return CompositeSampler;
}
});
Object.defineProperty(exports, 'DeterministicSampler', {
enumerable: true,
get: function () {
return DeterministicSampler;
}
});
Object.defineProperty(exports, 'Event', {
enumerable: true,
get: function () {
return Event;
}
});
Object.defineProperty(exports, 'FeatureFlagSampler', {
enumerable: true,
get: function () {
return FeatureFlagSampler;
}
});
Object.defineProperty(exports, 'KeyTargetRateSampler', {
enumerable: true,
get: function () {
return KeyTargetRateSampler;
}
});
Object.defineProperty(exports, 'NeverSampler', {
enumerable: true,
get: function () {
return NeverSampler;
}
});
Object.defineProperty(exports, 'RandomSampler', {
enumerable: true,
get: function () {
return RandomSampler;
}
});
Object.defineProperty(exports, 'TailSamplingSpanProcessor', {
enumerable: true,
get: function () {
return TailSamplingSpanProcessor;
}
});
Object.defineProperty(exports, 'UserIdSampler', {
enumerable: true,
get: function () {
return UserIdSampler;
}
});
Object.defineProperty(exports, 'createLinkFromHeaders', {
enumerable: true,
get: function () {
return createLinkFromHeaders;
}
});
Object.defineProperty(exports, 'createStringRedactor', {
enumerable: true,
get: function () {
return createStringRedactor;
}
});
Object.defineProperty(exports, 'createTraceContext', {
enumerable: true,
get: function () {
return createTraceContext;
}
});
Object.defineProperty(exports, 'debugCaptureRequested', {
enumerable: true,
get: function () {
return debugCaptureRequested;
}
});
Object.defineProperty(exports, 'defineBaggageSchema', {
enumerable: true,
get: function () {
return defineBaggageSchema;
}
});
Object.defineProperty(exports, 'extractLinksFromBatch', {
enumerable: true,
get: function () {
return extractLinksFromBatch;
}
});
Object.defineProperty(exports, 'flush', {
enumerable: true,
get: function () {
return flush;
}
});
Object.defineProperty(exports, 'generateCorrelationId', {
enumerable: true,
get: function () {
return generateCorrelationId;
}
});
Object.defineProperty(exports, 'getActiveContextWithBaggage', {
enumerable: true,
get: function () {
return getActiveContextWithBaggage;
}
});
Object.defineProperty(exports, 'getConfig', {
enumerable: true,
get: function () {
return getConfig;
}
});
Object.defineProperty(exports, 'getConfiguredSampler', {
enumerable: true,
get: function () {
return getConfiguredSampler;
}
});
Object.defineProperty(exports, 'getContextStorage', {
enumerable: true,
get: function () {
return getContextStorage;
}
});
Object.defineProperty(exports, 'getCorrelationId', {
enumerable: true,
get: function () {
return getCorrelationId;
}
});
Object.defineProperty(exports, 'getCorrelationStorage', {
enumerable: true,
get: function () {
return getCorrelationStorage;
}
});
Object.defineProperty(exports, 'getEventQueue', {
enumerable: true,
get: function () {
return getEventQueue;
}
});
Object.defineProperty(exports, 'getEvents', {
enumerable: true,
get: function () {
return getEvents;
}
});
Object.defineProperty(exports, 'getLogger', {
enumerable: true,
get: function () {
return getLogger;
}
});
Object.defineProperty(exports, 'getOperationContext', {
enumerable: true,
get: function () {
return getOperationContext;
}
});
Object.defineProperty(exports, 'getOrCreateCorrelationId', {
enumerable: true,
get: function () {
return getOrCreateCorrelationId;
}
});
Object.defineProperty(exports, 'getSdk', {
enumerable: true,
get: function () {
return getSdk;
}
});
Object.defineProperty(exports, 'hasExplicitSpanStatus', {
enumerable: true,
get: function () {
return hasExplicitSpanStatus;
}
});
Object.defineProperty(exports, 'hasYamlConfig', {
enumerable: true,
get: function () {
return hasYamlConfig;
}
});
Object.defineProperty(exports, 'hashUnitInterval', {
enumerable: true,
get: function () {
return hashUnitInterval;
}
});
Object.defineProperty(exports, 'init', {
enumerable: true,
get: function () {
return init;
}
});
Object.defineProperty(exports, 'isForceKept', {
enumerable: true,
get: function () {
return isForceKept;
}
});
Object.defineProperty(exports, 'isInitialized', {
enumerable: true,
get: function () {
return isInitialized;
}
});
Object.defineProperty(exports, 'isLoggerLocked', {
enumerable: true,
get: function () {
return isLoggerLocked;
}
});
Object.defineProperty(exports, 'loadYamlConfig', {
enumerable: true,
get: function () {
return loadYamlConfig;
}
});
Object.defineProperty(exports, 'loadYamlConfigFromFile', {
enumerable: true,
get: function () {
return loadYamlConfigFromFile;
}
});
Object.defineProperty(exports, 'lockLogger', {
enumerable: true,
get: function () {
return lockLogger;
}
});
Object.defineProperty(exports, 'markForceKept', {
enumerable: true,
get: function () {
return markForceKept;
}
});
Object.defineProperty(exports, 'resetEvents', {
enumerable: true,
get: function () {
return resetEvents;
}
});
Object.defineProperty(exports, 'resolveSamplingPreset', {
enumerable: true,
get: function () {
return resolveSamplingPreset;
}
});
Object.defineProperty(exports, 'runInOperationContext', {
enumerable: true,
get: function () {
return runInOperationContext;
}
});
Object.defineProperty(exports, 'runWithCorrelationId', {
enumerable: true,
get: function () {
return runWithCorrelationId;
}
});
Object.defineProperty(exports, 'samplingPresets', {
enumerable: true,
get: function () {
return samplingPresets;
}
});
Object.defineProperty(exports, 'setCorrelationId', {
enumerable: true,
get: function () {
return setCorrelationId;
}
});
Object.defineProperty(exports, 'setCorrelationIdInBaggage', {
enumerable: true,
get: function () {
return setCorrelationIdInBaggage;
}
});
Object.defineProperty(exports, 'shutdown', {
enumerable: true,
get: function () {
return shutdown;
}
});
Object.defineProperty(exports, 'track', {
enumerable: true,
get: function () {
return track;
}
});