autotel
Version:
Write Once, Observe Anywhere
1,034 lines (1,026 loc) • 40.9 kB
JavaScript
import { resolveSamplingPreset, samplingPresets } from "./sampling.js";
import { n as safeRequire, t as requireModule } from "./node-require-vROmTeJ8.js";
import { TailSamplingSpanProcessor } from "./tail-sampling-processor.js";
import { FilteringSpanProcessor } from "./filtering-span-processor.js";
import { SpanNameNormalizingProcessor } from "./span-name-normalizer.js";
import { AttributeRedactingProcessor, REDACTOR_PRESETS, normalizeAttributeRedactorConfig } from "./attribute-redacting-processor.js";
import { t as PrettyConsoleExporter } from "./pretty-console-exporter-DqKl_q9z.js";
import { loadYamlConfig } from "./yaml-config.js";
import { t as CanonicalLogLineProcessor } from "./canonical-log-line-processor-DbBQT5vY.js";
import { NodeSDK } from "@opentelemetry/sdk-node";
import { AlwaysOffSampler, AlwaysOnSampler, BatchSpanProcessor, ConsoleSpanExporter, ParentBasedSampler, SamplingDecision, SimpleSpanProcessor, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
import { context, propagation } from "@opentelemetry/api";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
//#region src/posthog-logs.ts
var RedactingLogRecordProcessor = class {
wrapped;
redact;
constructor(wrapped, redact) {
this.wrapped = wrapped;
this.redact = redact;
}
onEmit(logRecord, context) {
if (logRecord.body && typeof logRecord.body === "string") logRecord.body = this.redact(logRecord.body);
if (logRecord.attributes) {
for (const [key, value] of Object.entries(logRecord.attributes)) if (typeof value === "string") logRecord.attributes[key] = this.redact(value);
else if (Array.isArray(value)) logRecord.attributes[key] = value.map((item) => typeof item === "string" ? this.redact(item) : item);
}
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 = safeRequire("@opentelemetry/sdk-logs");
const exporterModule = 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/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 = propagation.getBaggage(parentContext);
if (!baggage) baggage = propagation.getBaggage(context.active());
if (!baggage) try {
const { getActiveContextWithBaggage } = requireModule("./trace-context");
const storedContext = getActiveContextWithBaggage();
baggage = 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" ? 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;
if (mask) result = result.replaceAll(pattern, (match) => mask(match));
else result = 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 === "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 AlwaysOnSampler();
case "always_off":
warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
return new AlwaysOffSampler();
case "traceidratio": return new TraceIdRatioBasedSampler(parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG));
case "parentbased_always_on":
warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
return new ParentBasedSampler({ root: new AlwaysOnSampler() });
case "parentbased_always_off":
warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
return new ParentBasedSampler({ root: new AlwaysOffSampler() });
case "parentbased_traceidratio": return new ParentBasedSampler({ root: new 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/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/init.ts
/**
* Simplified initialization for autotel
*
* Single init() function with sensible defaults.
* Replaces initInstrumentation() and separate events config.
*/
/**
* Silent logger (no-op) - used as default when user doesn't provide one.
* Internal autotel logs are silent by default to avoid spam.
* Users can import { autotelLogger } from 'autotel/logger' to create their own.
*/
const silentLogger = {
info: () => {},
warn: () => {},
error: () => {},
debug: () => {}
};
/**
* Adapts an Autotel Sampler to the OTel SDK Sampler interface.
*/
function toOtelSampler(sampler) {
return {
shouldSample(_context, _traceId, spanName, _spanKind, _attributes, links) {
return { decision: sampler.shouldSample({
operationName: spanName,
args: [],
links
}) ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD };
},
toString() {
return `AutotelSamplerAdapter`;
}
};
}
let OTLPTraceExporterGRPC;
let OTLPMetricExporterGRPC;
let OTLPLogExporterGRPC;
/**
* Helper: Lazy-load gRPC trace exporter
*/
function loadGRPCTraceExporter() {
if (OTLPTraceExporterGRPC) return OTLPTraceExporterGRPC;
try {
OTLPTraceExporterGRPC = requireModule("@opentelemetry/exporter-trace-otlp-grpc").OTLPTraceExporter;
return OTLPTraceExporterGRPC;
} catch {
throw new Error("gRPC trace exporter not found. Install @opentelemetry/exporter-trace-otlp-grpc");
}
}
/**
* Helper: Lazy-load gRPC metric exporter
*/
function loadGRPCMetricExporter() {
if (OTLPMetricExporterGRPC) return OTLPMetricExporterGRPC;
try {
OTLPMetricExporterGRPC = requireModule("@opentelemetry/exporter-metrics-otlp-grpc").OTLPMetricExporter;
return OTLPMetricExporterGRPC;
} catch {
throw new Error("gRPC metric exporter not found. Install @opentelemetry/exporter-metrics-otlp-grpc");
}
}
/**
* Helper: Create trace exporter based on protocol
*/
function createTraceExporter(protocol, config) {
if (protocol === "grpc") return new (loadGRPCTraceExporter())(config);
return new OTLPTraceExporter(config);
}
/**
* Helper: Create metric exporter based on protocol
*/
function createMetricExporter(protocol, config) {
if (protocol === "grpc") return new (loadGRPCMetricExporter())(config);
return new OTLPMetricExporter(config);
}
/**
* Helper: Lazy-load gRPC log exporter
*/
function loadGRPCLogExporter() {
if (OTLPLogExporterGRPC) return OTLPLogExporterGRPC;
try {
OTLPLogExporterGRPC = requireModule("@opentelemetry/exporter-logs-otlp-grpc").OTLPLogExporter;
return OTLPLogExporterGRPC;
} catch {
throw new Error("gRPC log exporter not found. Install @opentelemetry/exporter-logs-otlp-grpc");
}
}
/**
* Helper: Create log exporter based on protocol
*/
function createLogExporter(protocol, config) {
if (protocol === "grpc") return new (loadGRPCLogExporter())(config);
return new OTLPLogExporter(config);
}
/**
* Helper: Resolve protocol from config and environment
*/
function resolveProtocol(configProtocol) {
if (configProtocol === "grpc" || configProtocol === "http") return configProtocol;
const envProtocol = process.env.OTEL_EXPORTER_OTLP_PROTOCOL;
if (envProtocol === "grpc") return "grpc";
if (envProtocol === "http/protobuf" || 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;
}
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;
let _optionalRequire = safeRequire;
let _devtoolsClose = null;
const LOG_LEVELS = {
debug: 0,
info: 1,
warn: 2,
error: 3
};
/**
* 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;
}
function createSilentLogger() {
return {
info: () => {},
warn: () => {},
error: () => {},
debug: () => {}
};
}
function wrapLogger(base, silent, minLevel) {
if (silent) return createSilentLogger();
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")
};
}
/**
* 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;
}
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 ? config.destinations : fallbackEndpoint ? [{
endpoint: fallbackEndpoint,
headers: config.headers,
protocol: config.protocol
}] : []).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;
}
/**
* 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-subscribers/posthog';
*
* 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 = 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 logsEnabled = resolveLogsFlag(mergedConfig.logs);
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 = resourceFromAttributes({
[ATTR_SERVICE_NAME]: mergedConfig.service,
[ATTR_SERVICE_VERSION]: version,
"deployment.environment": environment,
"deployment.environment.name": environment
});
if (hostname) resource = resource.merge(resourceFromAttributes({
"host.name": hostname,
"datadog.host.name": hostname
}));
if (mergedConfig.resource) resource = resource.merge(mergedConfig.resource);
if (mergedConfig.resourceAttributes) resource = resource.merge(resourceFromAttributes(mergedConfig.resourceAttributes));
const otlpDestinations = resolveOtlpDestinations(mergedConfig, endpoint);
const configuredSpanProcessors = mergedConfig.spanProcessors && mergedConfig.spanProcessors.length > 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 && configuredSpanProcessors.length > 0) spanProcessors.push(...configuredSpanProcessors);
else if (configuredSpanExporters && configuredSpanExporters.length > 0) for (const exporter of configuredSpanExporters) spanProcessors.push(new TailSamplingSpanProcessor(new 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 BatchSpanProcessor(traceExporter)));
}
if (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 SimpleSpanProcessor(new PrettyConsoleExporter()));
else if (debugMode === true) spanProcessors.push(new SimpleSpanProcessor(new ConsoleSpanExporter()));
if (mergedConfig.canonicalLogLines?.enabled) {
const canonicalOptions = {
logger: mergedConfig.canonicalLogLines.logger || mergedConfig.logger,
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 CanonicalLogLineProcessor(canonicalOptions));
}
if (mergedConfig.attributeRedactor && spanProcessors.length > 0) {
const redactor = mergedConfig.attributeRedactor;
spanProcessors = spanProcessors.map((processor) => new AttributeRedactingProcessor(processor, { redactor }));
}
if (mergedConfig.attributeRedactor) _stringRedactor = createStringRedactor(mergedConfig.attributeRedactor);
if (_stringRedactor && mergedConfig.subscribers) {
for (const subscriber of mergedConfig.subscribers) if ("setStringRedactor" in subscriber && typeof subscriber.setStringRedactor === "function") subscriber.setStringRedactor(_stringRedactor);
}
if (mergedConfig.spanNameNormalizer && spanProcessors.length > 0) spanProcessors = spanProcessors.map((processor) => new SpanNameNormalizingProcessor(processor, { normalizer: mergedConfig.spanNameNormalizer }));
if (mergedConfig.spanFilter && spanProcessors.length > 0) spanProcessors = spanProcessors.map((processor) => new FilteringSpanProcessor(processor, { filter: mergedConfig.spanFilter }));
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
});
metricReaders.push(new PeriodicExportingMetricReader({ exporter: metricExporter }));
}
let logRecordProcessors;
if (configuredLogRecordProcessors && configuredLogRecordProcessors.length > 0) logRecordProcessors = [...configuredLogRecordProcessors];
if (logsEnabled) {
for (const destination of otlpDestinations) {
if (!destinationSupportsSignal(destination, "logs")) continue;
let processor = new BatchLogRecordProcessor(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");
}
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,
sampler,
instrumentations: finalInstrumentations
};
if (spanProcessors.length > 0) sdkOptions.spanProcessors = spanProcessors;
if (metricReaders.length > 0) sdkOptions.metricReaders = metricReaders;
if (logRecordProcessors && logRecordProcessors.length > 0) sdkOptions.logRecordProcessors = logRecordProcessors;
sdk = mergedConfig.sdkFactory ? mergedConfig.sdkFactory(sdkOptions) : new 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 };
try {
initOptions.tracerProvider = sdk.getTracerProvider();
} catch {}
if (configuredSpanExporters?.[0]) initOptions.exporter = configuredSpanExporters[0];
if (typeof traceloop.initialize === "function") {
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;
}
/**
* 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) if (instrumentation && typeof instrumentation === "object") names.add(instrumentation.constructor.name);
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 = {
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"
};
/**
* Detect if we're running in ESM mode
*/
function isESMMode() {
try {
const fs = 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 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[className];
if (packageName) exclusionConfig[packageName] = { enabled: false };
}
if (integrations === true) {
if (Object.keys(exclusionConfig).length > 0) return getNodeAutoInstrumentations(exclusionConfig);
return getNodeAutoInstrumentations();
}
if (Array.isArray(integrations)) {
const config = { ...exclusionConfig };
for (const name of integrations) {
const packageName = `@opentelemetry/instrumentation-${name}`;
if (!exclusionConfig[packageName]) config[packageName] = { enabled: true };
}
return getNodeAutoInstrumentations(config);
}
const config = {
...exclusionConfig,
...integrations
};
for (const packageName of Object.keys(exclusionConfig)) if (Object.keys(integrations).find((key) => packageName.includes(key))) config[packageName] = { enabled: false };
return getNodeAutoInstrumentations(config);
}
/**
* 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;
}
}
/**
* Auto-detect version from package.json
*/
function detectVersion() {
try {
const fs = 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 requireModule("node:os").hostname();
} catch {
return;
}
}
/**
* @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
export { getSdk as a, isInitialized as c, warnIfNotInitialized as d, createStringRedactor as f, getLogger as i, isLoggerLocked as l, getConfig as n, getValidationConfig as o, BaggageSpanProcessor as p, getEventsConfig as r, init as s, _closeEmbeddedDevtools as t, lockLogger as u };
//# sourceMappingURL=init-BS2JVkrL.js.map