autotel 6.2.1 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,13 +18,13 @@ let _opentelemetry_resources = require("@opentelemetry/resources");
18
18
  let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
19
19
  let _opentelemetry_api = require("@opentelemetry/api");
20
20
  let _opentelemetry_sdk_metrics = require("@opentelemetry/sdk-metrics");
21
- let _opentelemetry_exporter_metrics_otlp_http = require("@opentelemetry/exporter-metrics-otlp-http");
22
- let _opentelemetry_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
23
- let _opentelemetry_exporter_logs_otlp_http = require("@opentelemetry/exporter-logs-otlp-http");
24
21
  let _opentelemetry_sdk_logs = require("@opentelemetry/sdk-logs");
25
22
  let node_os = require("node:os");
26
23
  let node_async_hooks = require("node:async_hooks");
27
24
  node_async_hooks = require_rolldown_runtime.__toESM(node_async_hooks, 1);
25
+ let _opentelemetry_exporter_metrics_otlp_http = require("@opentelemetry/exporter-metrics-otlp-http");
26
+ let _opentelemetry_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
27
+ let _opentelemetry_exporter_logs_otlp_http = require("@opentelemetry/exporter-logs-otlp-http");
28
28
 
29
29
  //#region src/posthog-logs.ts
30
30
  var RedactingLogRecordProcessor = class {
@@ -327,6 +327,39 @@ function resolveDevtoolsConfig(config) {
327
327
  //#endregion
328
328
  //#region src/process-handlers.ts
329
329
  let removeOwnedHandlers = [];
330
+ /**
331
+ * Tracked separately from `removeOwnedHandlers` because the exit flush outlives
332
+ * a call to `installProcessHandlers`, which clears that list before installing
333
+ * its own signal listeners.
334
+ */
335
+ let removeExitFlush;
336
+ /**
337
+ * Shared across every path that can end the process: signals, fatal errors and
338
+ * a clean exit. They can overlap — a container stopping a job that has just
339
+ * finished its work sends SIGTERM while the exit flush is still draining — and
340
+ * a second shutdown would tear down queues the first is still using.
341
+ */
342
+ let shutdownInFlight;
343
+ /**
344
+ * The clean-exit flush, while it runs. Doubles as the latch that keeps a
345
+ * re-emitted `beforeExit` from flushing twice, and as the thing a shutdown
346
+ * waits on rather than tearing down queues mid-drain.
347
+ */
348
+ let exitFlushInFlight;
349
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 2e3;
350
+ function runShutdownOnce(shutdown, timeoutMs) {
351
+ if (shutdownInFlight) return shutdownInFlight;
352
+ let timeoutHandle;
353
+ const shutdownAttempt = Promise.resolve(exitFlushInFlight).then(shutdown).catch(() => void 0);
354
+ const timeout = new Promise((resolve) => {
355
+ timeoutHandle = setTimeout(resolve, timeoutMs);
356
+ timeoutHandle.unref();
357
+ });
358
+ shutdownInFlight = Promise.race([shutdownAttempt, timeout]).then(() => {
359
+ if (timeoutHandle) clearTimeout(timeoutHandle);
360
+ });
361
+ return shutdownInFlight;
362
+ }
330
363
  const DEFAULT_SIGNALS = ["SIGTERM", "SIGINT"];
331
364
  function signalExitCode(signal) {
332
365
  return 128 + node_os.constants.signals[signal];
@@ -345,21 +378,7 @@ function reportFatalError(error, event) {
345
378
  }
346
379
  function installProcessHandlers(config, shutdown) {
347
380
  uninstallProcessHandlers();
348
- let shutdownInFlight;
349
- const runShutdownOnce = () => {
350
- if (shutdownInFlight) return shutdownInFlight;
351
- const timeoutMs = config.shutdownTimeoutMs ?? 2e3;
352
- let timeoutHandle;
353
- const shutdownAttempt = Promise.resolve().then(shutdown).catch(() => void 0);
354
- const timeout = new Promise((resolve) => {
355
- timeoutHandle = setTimeout(resolve, timeoutMs);
356
- timeoutHandle.unref();
357
- });
358
- shutdownInFlight = Promise.race([shutdownAttempt, timeout]).then(() => {
359
- if (timeoutHandle) clearTimeout(timeoutHandle);
360
- });
361
- return shutdownInFlight;
362
- };
381
+ const timeoutMs = config.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
363
382
  let exitScheduled = false;
364
383
  let resolvedExitCode = 0;
365
384
  let fatalLatched = false;
@@ -372,7 +391,7 @@ function installProcessHandlers(config, shutdown) {
372
391
  } else if (!exitScheduled && !fatalLatched) resolvedExitCode = exitCode;
373
392
  if (exitScheduled) return;
374
393
  exitScheduled = true;
375
- runShutdownOnce().then(() => {
394
+ runShutdownOnce(shutdown, timeoutMs).then(() => {
376
395
  process.exit(resolvedExitCode);
377
396
  });
378
397
  };
@@ -403,9 +422,43 @@ function installProcessHandlers(config, shutdown) {
403
422
  });
404
423
  }
405
424
  }
425
+ /**
426
+ * Flush telemetry when the process runs to completion.
427
+ *
428
+ * The signal and fatal-error handlers above cover a process that is stopped or
429
+ * that crashes. Neither fires when a script simply finishes: the event loop
430
+ * drains and Node exits, taking whatever the batch span processor was still
431
+ * holding with it. `beforeExit` is the only hook for that case, and it is the
432
+ * one that matters for CLIs, cron jobs, CI steps and serverless handlers.
433
+ *
434
+ * A flush, never a shutdown: `beforeExit` fires on *any* event-loop drain, not
435
+ * only the final one, so tearing the SDK down here would silently kill
436
+ * telemetry in a process that goes on to do more work.
437
+ */
438
+ function installExitFlush(flushTelemetry, timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS) {
439
+ removeExitFlush?.();
440
+ exitFlushInFlight = void 0;
441
+ const listener = (code) => {
442
+ if (exitFlushInFlight || shutdownInFlight) return;
443
+ const deadline = setTimeout(() => {
444
+ process.exit(process.exitCode ?? code);
445
+ }, timeoutMs);
446
+ exitFlushInFlight = flushTelemetry().catch(() => void 0).finally(() => {
447
+ clearTimeout(deadline);
448
+ });
449
+ };
450
+ process.on("beforeExit", listener);
451
+ removeExitFlush = () => {
452
+ process.removeListener("beforeExit", listener);
453
+ removeExitFlush = void 0;
454
+ };
455
+ }
406
456
  function uninstallProcessHandlers() {
407
457
  for (const removeHandler of removeOwnedHandlers) removeHandler();
408
458
  removeOwnedHandlers = [];
459
+ removeExitFlush?.();
460
+ shutdownInFlight = void 0;
461
+ exitFlushInFlight = void 0;
409
462
  }
410
463
 
411
464
  //#endregion
@@ -913,6 +966,11 @@ function getSubscriberName(subscriber) {
913
966
  return (subscriber.constructor?.name || "unknown").replace(/Subscriber$/i, "").toLowerCase();
914
967
  }
915
968
  /**
969
+ * Subscribers whose `shutdown()` has run. Terminal for the client they wrap, so
970
+ * a queue rebuilt from the same config must not reuse them.
971
+ */
972
+ const shutDownSubscribers = /* @__PURE__ */ new WeakSet();
973
+ /**
916
974
  * Events queue with batching and backpressure
917
975
  *
918
976
  * Features:
@@ -934,13 +992,15 @@ var EventQueue = class {
934
992
  observableCleanups = [];
935
993
  subscriberHealthy = /* @__PURE__ */ new Map();
936
994
  constructor(subscribers, config) {
937
- this.subscribers = subscribers;
995
+ const live = subscribers.filter((s) => !shutDownSubscribers.has(s));
996
+ 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()");
997
+ this.subscribers = live;
938
998
  this.config = {
939
999
  ...DEFAULT_CONFIG$2,
940
1000
  ...config
941
1001
  };
942
1002
  this.rateLimiter = this.config.rateLimit ? new TokenBucketRateLimiter(this.config.rateLimit) : null;
943
- for (const subscriber of subscribers) {
1003
+ for (const subscriber of this.subscribers) {
944
1004
  const name = getSubscriberName(subscriber);
945
1005
  this.subscriberHealthy.set(name, true);
946
1006
  }
@@ -1234,6 +1294,20 @@ var EventQueue = class {
1234
1294
  async shutdown() {
1235
1295
  this.isShuttingDown = true;
1236
1296
  await this.flush();
1297
+ const drains = await Promise.allSettled(this.subscribers.map(async (subscriber) => {
1298
+ if (!subscriber.shutdown) return;
1299
+ shutDownSubscribers.add(subscriber);
1300
+ return subscriber.shutdown();
1301
+ }));
1302
+ for (const [index, drain] of drains.entries()) {
1303
+ if (drain.status !== "rejected") continue;
1304
+ const subscriberName = getSubscriberName(this.subscribers[index]);
1305
+ this.subscriberHealthy.set(subscriberName, false);
1306
+ getLogger().error({
1307
+ subscriber: subscriberName,
1308
+ err: drain.reason
1309
+ }, "[autotel] Subscriber shutdown failed, buffered events may be lost");
1310
+ }
1237
1311
  }
1238
1312
  /**
1239
1313
  * Cleanup observable metric callbacks to prevent memory leaks
@@ -2463,47 +2537,7 @@ async function shutdown() {
2463
2537
  }
2464
2538
 
2465
2539
  //#endregion
2466
- //#region src/init.ts
2467
- /**
2468
- * Simplified initialization for autotel
2469
- *
2470
- * Single init() function with sensible defaults.
2471
- * Replaces initInstrumentation() and separate events config.
2472
- */
2473
- /**
2474
- * Silent logger (no-op) - used as default when user doesn't provide one.
2475
- * Internal autotel logs are silent by default to avoid spam.
2476
- * Users can import { autotelLogger } from 'autotel/logger' to create their own.
2477
- */
2478
- const silentLogger = {
2479
- info: () => {},
2480
- warn: () => {},
2481
- error: () => {},
2482
- debug: () => {}
2483
- };
2484
- /**
2485
- * Adapts an Autotel Sampler to the OTel SDK Sampler interface.
2486
- */
2487
- function toOtelSampler(sampler) {
2488
- return {
2489
- shouldSample(_context, _traceId, spanName, _spanKind, _attributes, links) {
2490
- const samplingContext = {
2491
- operationName: spanName,
2492
- args: [],
2493
- links
2494
- };
2495
- const shouldTrace = sampler.shouldSample(samplingContext);
2496
- const rate = sampler.sampleRate?.(samplingContext);
2497
- return {
2498
- decision: shouldTrace ? _opentelemetry_sdk_trace_base.SamplingDecision.RECORD_AND_SAMPLED : _opentelemetry_sdk_trace_base.SamplingDecision.NOT_RECORD,
2499
- ...shouldTrace && rate !== void 0 && rate > 1 ? { attributes: { [require_sampling.AUTOTEL_SAMPLING_RATE]: rate } } : {}
2500
- };
2501
- },
2502
- toString() {
2503
- return `AutotelSamplerAdapter`;
2504
- }
2505
- };
2506
- }
2540
+ //#region src/otlp-exporters.ts
2507
2541
  let OTLPTraceExporterPROTO;
2508
2542
  let OTLPMetricExporterPROTO;
2509
2543
  let OTLPLogExporterPROTO;
@@ -2617,62 +2651,9 @@ function formatEndpointUrl(endpoint, signal, protocol) {
2617
2651
  if (!endpoint.endsWith(`/v1/${signal}`)) return `${endpoint}/v1/${signal}`;
2618
2652
  return endpoint;
2619
2653
  }
2620
- let initialized = false;
2621
- let locked = false;
2622
- let config = null;
2623
- let sdk = null;
2624
- let warnedOnce = false;
2625
- let logger = silentLogger;
2626
- let validationConfig = null;
2627
- let eventsConfig = null;
2628
- let _stringRedactor = null;
2629
- function acceptsStringRedactor(subscriber) {
2630
- return typeof subscriber === "object" && subscriber !== null && "setStringRedactor" in subscriber && typeof subscriber.setStringRedactor === "function";
2631
- }
2632
- let _optionalRequire = require_node_require.safeRequire;
2633
- let _devtoolsClose = null;
2634
- const LOG_LEVELS = {
2635
- debug: 0,
2636
- info: 1,
2637
- warn: 2,
2638
- error: 3
2639
- };
2640
- /**
2641
- * Lock the logger to prevent further `init()` calls.
2642
- * Use this when framework plugins set up instrumentation and you want
2643
- * to prevent accidental re-initialization from user code.
2644
- */
2645
- function lockLogger() {
2646
- locked = true;
2647
- }
2648
- /**
2649
- * Check if the logger has been locked.
2650
- */
2651
- function isLoggerLocked() {
2652
- return locked;
2653
- }
2654
- function createSilentLogger() {
2655
- return {
2656
- info: () => {},
2657
- warn: () => {},
2658
- error: () => {},
2659
- debug: () => {}
2660
- };
2661
- }
2662
- function wrapLogger(base, silent, minLevel) {
2663
- if (silent) return createSilentLogger();
2664
- const threshold = LOG_LEVELS[minLevel];
2665
- const wrap = (fn, level) => {
2666
- if (LOG_LEVELS[level] < threshold) return (() => {});
2667
- return ((...args) => fn(...args));
2668
- };
2669
- return {
2670
- debug: wrap(base.debug, "debug"),
2671
- info: wrap(base.info, "info"),
2672
- warn: wrap(base.warn, "warn"),
2673
- error: wrap(base.error, "error")
2674
- };
2675
- }
2654
+
2655
+ //#endregion
2656
+ //#region src/config-resolution.ts
2676
2657
  /**
2677
2658
  * Resolve the effective attribute redactor. Explicit config wins (`false`
2678
2659
  * disables). Otherwise the `AUTOTEL_REDACT_PII` env var controls it, and as a
@@ -2787,106 +2768,347 @@ function destinationSupportsSignal(destination, signal) {
2787
2768
  return destination.signals ? destination.signals.has(signal) : true;
2788
2769
  }
2789
2770
  /**
2790
- * Initialize autotel - Write Once, Observe Everywhere
2791
- *
2792
- * Follows OpenTelemetry standards: opinionated defaults with full flexibility
2793
- * Idempotent: multiple calls are safe, last one wins
2794
- *
2795
- * @example Minimal setup (OTLP default)
2796
- * ```typescript
2797
- * init({ service: 'my-app' })
2798
- * ```
2799
- *
2800
- * @example With events (observe in PostHog, Mixpanel, etc.)
2801
- * ```typescript
2802
- * import { PostHogSubscriber } from 'autotel-subscribers/posthog';
2803
- *
2804
- * init({
2805
- * service: 'my-app',
2806
- * subscribers: [new PostHogSubscriber({ apiKey: '...' })]
2807
- * })
2808
- * ```
2809
- *
2810
- * @example Observe in Jaeger
2811
- * ```typescript
2812
- * import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
2813
- *
2814
- * init({
2815
- * service: 'my-app',
2816
- * spanExporter: new JaegerExporter({ endpoint: 'http://localhost:14268/api/traces' })
2817
- * })
2818
- * ```
2819
- *
2820
- * @example Observe in Zipkin
2821
- * ```typescript
2822
- * import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
2823
- *
2824
- * init({
2825
- * service: 'my-app',
2826
- * spanExporter: new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' })
2827
- * })
2828
- * ```
2829
- *
2830
- * @example Observe in Datadog
2831
- * ```typescript
2832
- * import { DatadogSpanProcessor } from '@opentelemetry/exporter-datadog'
2833
- *
2834
- * init({
2835
- * service: 'my-app',
2836
- * spanProcessor: new DatadogSpanProcessor({ ... })
2837
- * })
2838
- * ```
2771
+ * Auto-detect version from package.json
2772
+ */
2773
+ function detectVersion() {
2774
+ try {
2775
+ const fs = require_node_require.requireModule("node:fs");
2776
+ return JSON.parse(fs.readFileSync(`${process.cwd()}/package.json`, "utf8")).version || "1.0.0";
2777
+ } catch {
2778
+ return "1.0.0";
2779
+ }
2780
+ }
2781
+ /**
2782
+ * Detect hostname for resource attributes.
2783
+ * Supports Datadog conventions (DD_HOSTNAME) and falls back to system hostname.
2839
2784
  *
2840
- * @example Console output (dev)
2841
- * ```typescript
2842
- * import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
2785
+ * Priority order:
2786
+ * 1. DD_HOSTNAME environment variable (Datadog convention)
2787
+ * 2. HOSTNAME environment variable (common Unix convention)
2788
+ * 3. os.hostname() (system hostname)
2843
2789
  *
2844
- * init({
2845
- * service: 'my-app',
2846
- * spanProcessor: new SimpleSpanProcessor(new ConsoleSpanExporter())
2847
- * })
2848
- * ```
2790
+ * @returns hostname string or undefined if detection fails
2849
2791
  */
2850
- function init(cfg) {
2851
- if (locked) return;
2852
- const envConfig = resolveConfigFromEnv();
2853
- const yamlConfig = require_yaml_config.loadYamlConfig() ?? {};
2854
- const mergedConfig = {
2855
- ...envConfig,
2856
- ...yamlConfig,
2857
- ...cfg,
2858
- resourceAttributes: {
2859
- ...envConfig.resourceAttributes,
2860
- ...yamlConfig.resourceAttributes,
2861
- ...detectEnvironmentAttributes(),
2862
- ...cfg.resourceAttributes
2863
- },
2864
- headers: cfg.headers ?? yamlConfig.headers ?? envConfig.headers
2865
- };
2866
- const resolvedRedactor = resolveAttributeRedactor(mergedConfig.attributeRedactor, mergedConfig.environment || process.env.NODE_ENV || "development");
2867
- if (resolvedRedactor === void 0) mergedConfig.attributeRedactor = void 0;
2868
- else {
2869
- const normalizedRedactor = require_attribute_redacting_processor.normalizeAttributeRedactorConfig(resolvedRedactor);
2870
- if (!normalizedRedactor) throw new Error("Invalid attributeRedactor config");
2871
- mergedConfig.attributeRedactor = normalizedRedactor;
2792
+ function detectHostname() {
2793
+ if (process.env.DD_HOSTNAME) return process.env.DD_HOSTNAME;
2794
+ if (process.env.HOSTNAME) return process.env.HOSTNAME;
2795
+ try {
2796
+ return require_node_require.requireModule("node:os").hostname();
2797
+ } catch {
2798
+ return;
2872
2799
  }
2873
- const devtoolsConfig = resolveDevtoolsConfig(mergedConfig.devtools);
2874
- if (devtoolsConfig.enabled && mergedConfig.logs === void 0) mergedConfig.logs = true;
2875
- const silent = mergedConfig.silent ?? false;
2876
- const minLevel = mergedConfig.minLevel ?? "info";
2877
- logger = wrapLogger(mergedConfig.logger || silentLogger, silent, minLevel);
2878
- if (initialized) logger.warn({}, "[autotel] init() called again - last config wins. This may cause unexpected behavior.");
2879
- config = mergedConfig;
2880
- validationConfig = mergedConfig.validation || null;
2881
- eventsConfig = mergedConfig.events || null;
2882
- let endpoint = mergedConfig.endpoint ?? devtoolsConfig.endpoint;
2883
- const version = mergedConfig.version || detectVersion();
2884
- const environment = mergedConfig.environment || process.env.NODE_ENV || "development";
2885
- const metricsEnabled = resolveMetricsFlag(mergedConfig.metrics);
2886
- const logsEnabled = resolveLogsFlag(mergedConfig.logs);
2887
- if (devtoolsConfig.enabled && devtoolsConfig.embedded) {
2888
- const devtoolsModule = _optionalRequire("autotel-devtools");
2889
- if (devtoolsModule?.createDevtools) {
2800
+ }
2801
+
2802
+ //#endregion
2803
+ //#region src/auto-instrumentations.ts
2804
+ /**
2805
+ * Extract instrumentation class names from instrumentation instances
2806
+ * Used to detect duplicates between manual and auto instrumentations
2807
+ */
2808
+ function getInstrumentationNames(instrumentations) {
2809
+ const names = /* @__PURE__ */ new Set();
2810
+ if (!instrumentations) return names;
2811
+ for (const instrumentation of instrumentations) if (instrumentation && typeof instrumentation === "object") names.add(instrumentation.constructor.name);
2812
+ return names;
2813
+ }
2814
+ /**
2815
+ * Map common instrumentation class names to their package names
2816
+ * Used to disable auto-instrumentations when user provides manual configs
2817
+ */
2818
+ const INSTRUMENTATION_CLASS_TO_PACKAGE = {
2819
+ HttpInstrumentation: "@opentelemetry/instrumentation-http",
2820
+ HttpsInstrumentation: "@opentelemetry/instrumentation-http",
2821
+ ExpressInstrumentation: "@opentelemetry/instrumentation-express",
2822
+ FastifyInstrumentation: "@opentelemetry/instrumentation-fastify",
2823
+ MongoDBInstrumentation: "@opentelemetry/instrumentation-mongodb",
2824
+ MongooseInstrumentation: "@opentelemetry/instrumentation-mongoose",
2825
+ PrismaInstrumentation: "@opentelemetry/instrumentation-prisma",
2826
+ PinoInstrumentation: "@opentelemetry/instrumentation-pino",
2827
+ WinstonInstrumentation: "@opentelemetry/instrumentation-winston",
2828
+ RedisInstrumentation: "@opentelemetry/instrumentation-redis",
2829
+ GraphQLInstrumentation: "@opentelemetry/instrumentation-graphql",
2830
+ GrpcInstrumentation: "@opentelemetry/instrumentation-grpc",
2831
+ IORedisInstrumentation: "@opentelemetry/instrumentation-ioredis",
2832
+ KnexInstrumentation: "@opentelemetry/instrumentation-knex",
2833
+ NestJsInstrumentation: "@opentelemetry/instrumentation-nestjs-core",
2834
+ PgInstrumentation: "@opentelemetry/instrumentation-pg",
2835
+ MySQLInstrumentation: "@opentelemetry/instrumentation-mysql",
2836
+ MySQL2Instrumentation: "@opentelemetry/instrumentation-mysql2"
2837
+ };
2838
+ /**
2839
+ * Detect if we're running in ESM mode
2840
+ */
2841
+ function isESMMode() {
2842
+ try {
2843
+ const fs = require_node_require.requireModule("node:fs");
2844
+ try {
2845
+ return JSON.parse(fs.readFileSync(`${process.cwd()}/package.json`, "utf8")).type === "module";
2846
+ } catch {
2847
+ return false;
2848
+ }
2849
+ } catch {
2850
+ return false;
2851
+ }
2852
+ }
2853
+ /**
2854
+ * Lazy-load auto-instrumentations (optional peer dependency)
2855
+ * Only loads when integrations config is truthy, avoiding ~40+ package imports at startup.
2856
+ */
2857
+ function loadNodeAutoInstrumentations() {
2858
+ try {
2859
+ return require_node_require.requireModule("@opentelemetry/auto-instrumentations-node").getNodeAutoInstrumentations;
2860
+ } catch {
2861
+ const isESM = isESMMode();
2862
+ const baseMessage = "@opentelemetry/auto-instrumentations-node not found.";
2863
+ if (isESM) throw new Error(`${baseMessage}\n\nESM Setup Required:
2864
+ 1. Install as a direct dependency: pnpm add @opentelemetry/auto-instrumentations-node
2865
+ 2. Create instrumentation.mjs with:
2866
+ import 'autotel/register'; // MUST be first!
2867
+ import { init } from 'autotel';
2868
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
2869
+ init({ service: "my-app", instrumentations: getNodeAutoInstrumentations() });
2870
+ 3. Run with: tsx --import ./instrumentation.mjs src/index.ts
2871
+
2872
+ See: https://github.com/jagreehal/autotel#esm-setup`);
2873
+ throw new Error(`${baseMessage} Install it: pnpm add @opentelemetry/auto-instrumentations-node`);
2874
+ }
2875
+ }
2876
+ /**
2877
+ * Injectable loader for testing. Set to override the default loader.
2878
+ * @internal
2879
+ */
2880
+ let _autoInstrumentationsLoader = null;
2881
+ /**
2882
+ * Get auto-instrumentations based on simple integration names
2883
+ * Excludes instrumentations that are manually provided to avoid conflicts
2884
+ */
2885
+ function getAutoInstrumentations(integrations, manualInstrumentationNames = /* @__PURE__ */ new Set()) {
2886
+ if (integrations === false) return [];
2887
+ const getNodeAutoInstrumentations = _autoInstrumentationsLoader ? _autoInstrumentationsLoader() : loadNodeAutoInstrumentations();
2888
+ const exclusionConfig = {};
2889
+ for (const className of manualInstrumentationNames) {
2890
+ const packageName = INSTRUMENTATION_CLASS_TO_PACKAGE[className];
2891
+ if (packageName) exclusionConfig[packageName] = { enabled: false };
2892
+ }
2893
+ if (integrations === true) {
2894
+ if (Object.keys(exclusionConfig).length > 0) return getNodeAutoInstrumentations(exclusionConfig);
2895
+ return getNodeAutoInstrumentations();
2896
+ }
2897
+ if (Array.isArray(integrations)) {
2898
+ const config = { ...exclusionConfig };
2899
+ for (const name of integrations) {
2900
+ const packageName = `@opentelemetry/instrumentation-${name}`;
2901
+ if (!exclusionConfig[packageName]) config[packageName] = { enabled: true };
2902
+ }
2903
+ return getNodeAutoInstrumentations(config);
2904
+ }
2905
+ const config = {
2906
+ ...exclusionConfig,
2907
+ ...integrations
2908
+ };
2909
+ for (const packageName of Object.keys(exclusionConfig)) if (Object.keys(integrations).find((key) => packageName.includes(key))) config[packageName] = { enabled: false };
2910
+ return getNodeAutoInstrumentations(config);
2911
+ }
2912
+
2913
+ //#endregion
2914
+ //#region src/init.ts
2915
+ /**
2916
+ * Simplified initialization for autotel
2917
+ *
2918
+ * Single init() function with sensible defaults.
2919
+ * Replaces initInstrumentation() and separate events config.
2920
+ */
2921
+ /**
2922
+ * Silent logger (no-op) - used as default when user doesn't provide one.
2923
+ * Internal autotel logs are silent by default to avoid spam.
2924
+ * Users can import { autotelLogger } from 'autotel/logger' to create their own.
2925
+ */
2926
+ const silentLogger = {
2927
+ info: () => {},
2928
+ warn: () => {},
2929
+ error: () => {},
2930
+ debug: () => {}
2931
+ };
2932
+ /**
2933
+ * Adapts an Autotel Sampler to the OTel SDK Sampler interface.
2934
+ */
2935
+ function toOtelSampler(sampler) {
2936
+ return {
2937
+ shouldSample(_context, _traceId, spanName, _spanKind, _attributes, links) {
2938
+ const samplingContext = {
2939
+ operationName: spanName,
2940
+ args: [],
2941
+ links
2942
+ };
2943
+ const shouldTrace = sampler.shouldSample(samplingContext);
2944
+ const rate = sampler.sampleRate?.(samplingContext);
2945
+ return {
2946
+ decision: shouldTrace ? _opentelemetry_sdk_trace_base.SamplingDecision.RECORD_AND_SAMPLED : _opentelemetry_sdk_trace_base.SamplingDecision.NOT_RECORD,
2947
+ ...shouldTrace && rate !== void 0 && rate > 1 ? { attributes: { [require_sampling.AUTOTEL_SAMPLING_RATE]: rate } } : {}
2948
+ };
2949
+ },
2950
+ toString() {
2951
+ return `AutotelSamplerAdapter`;
2952
+ }
2953
+ };
2954
+ }
2955
+ let initialized = false;
2956
+ let locked = false;
2957
+ let config = null;
2958
+ let sdk = null;
2959
+ let warnedOnce = false;
2960
+ let logger = silentLogger;
2961
+ let validationConfig = null;
2962
+ let eventsConfig = null;
2963
+ let _stringRedactor = null;
2964
+ function acceptsStringRedactor(subscriber) {
2965
+ return typeof subscriber === "object" && subscriber !== null && "setStringRedactor" in subscriber && typeof subscriber.setStringRedactor === "function";
2966
+ }
2967
+ let _optionalRequire = require_node_require.safeRequire;
2968
+ let _devtoolsClose = null;
2969
+ const LOG_LEVELS = {
2970
+ debug: 0,
2971
+ info: 1,
2972
+ warn: 2,
2973
+ error: 3
2974
+ };
2975
+ /**
2976
+ * Lock the logger to prevent further `init()` calls.
2977
+ * Use this when framework plugins set up instrumentation and you want
2978
+ * to prevent accidental re-initialization from user code.
2979
+ */
2980
+ function lockLogger() {
2981
+ locked = true;
2982
+ }
2983
+ /**
2984
+ * Check if the logger has been locked.
2985
+ */
2986
+ function isLoggerLocked() {
2987
+ return locked;
2988
+ }
2989
+ function createSilentLogger() {
2990
+ return {
2991
+ info: () => {},
2992
+ warn: () => {},
2993
+ error: () => {},
2994
+ debug: () => {}
2995
+ };
2996
+ }
2997
+ function wrapLogger(base, silent, minLevel) {
2998
+ if (silent) return createSilentLogger();
2999
+ const threshold = LOG_LEVELS[minLevel];
3000
+ const wrap = (fn, level) => {
3001
+ if (LOG_LEVELS[level] < threshold) return (() => {});
3002
+ return ((...args) => fn(...args));
3003
+ };
3004
+ return {
3005
+ debug: wrap(base.debug, "debug"),
3006
+ info: wrap(base.info, "info"),
3007
+ warn: wrap(base.warn, "warn"),
3008
+ error: wrap(base.error, "error")
3009
+ };
3010
+ }
3011
+ /**
3012
+ * Initialize autotel - Write Once, Observe Everywhere
3013
+ *
3014
+ * Follows OpenTelemetry standards: opinionated defaults with full flexibility
3015
+ * Idempotent: multiple calls are safe, last one wins
3016
+ *
3017
+ * @example Minimal setup (OTLP default)
3018
+ * ```typescript
3019
+ * init({ service: 'my-app' })
3020
+ * ```
3021
+ *
3022
+ * @example With events (observe in PostHog, Mixpanel, etc.)
3023
+ * ```typescript
3024
+ * import { PostHogSubscriber } from 'autotel-subscribers/posthog';
3025
+ *
3026
+ * init({
3027
+ * service: 'my-app',
3028
+ * subscribers: [new PostHogSubscriber({ apiKey: '...' })]
3029
+ * })
3030
+ * ```
3031
+ *
3032
+ * @example Observe in Jaeger
3033
+ * ```typescript
3034
+ * import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
3035
+ *
3036
+ * init({
3037
+ * service: 'my-app',
3038
+ * spanExporter: new JaegerExporter({ endpoint: 'http://localhost:14268/api/traces' })
3039
+ * })
3040
+ * ```
3041
+ *
3042
+ * @example Observe in Zipkin
3043
+ * ```typescript
3044
+ * import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
3045
+ *
3046
+ * init({
3047
+ * service: 'my-app',
3048
+ * spanExporter: new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' })
3049
+ * })
3050
+ * ```
3051
+ *
3052
+ * @example Observe in Datadog
3053
+ * ```typescript
3054
+ * import { DatadogSpanProcessor } from '@opentelemetry/exporter-datadog'
3055
+ *
3056
+ * init({
3057
+ * service: 'my-app',
3058
+ * spanProcessor: new DatadogSpanProcessor({ ... })
3059
+ * })
3060
+ * ```
3061
+ *
3062
+ * @example Console output (dev)
3063
+ * ```typescript
3064
+ * import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
3065
+ *
3066
+ * init({
3067
+ * service: 'my-app',
3068
+ * spanProcessor: new SimpleSpanProcessor(new ConsoleSpanExporter())
3069
+ * })
3070
+ * ```
3071
+ */
3072
+ function init(cfg) {
3073
+ if (locked) return;
3074
+ const envConfig = resolveConfigFromEnv();
3075
+ const yamlConfig = require_yaml_config.loadYamlConfig() ?? {};
3076
+ const mergedConfig = {
3077
+ ...envConfig,
3078
+ ...yamlConfig,
3079
+ ...cfg,
3080
+ resourceAttributes: {
3081
+ ...envConfig.resourceAttributes,
3082
+ ...yamlConfig.resourceAttributes,
3083
+ ...detectEnvironmentAttributes(),
3084
+ ...cfg.resourceAttributes
3085
+ },
3086
+ headers: cfg.headers ?? yamlConfig.headers ?? envConfig.headers
3087
+ };
3088
+ const resolvedRedactor = resolveAttributeRedactor(mergedConfig.attributeRedactor, mergedConfig.environment || process.env.NODE_ENV || "development");
3089
+ if (resolvedRedactor === void 0) mergedConfig.attributeRedactor = void 0;
3090
+ else {
3091
+ const normalizedRedactor = require_attribute_redacting_processor.normalizeAttributeRedactorConfig(resolvedRedactor);
3092
+ if (!normalizedRedactor) throw new Error("Invalid attributeRedactor config");
3093
+ mergedConfig.attributeRedactor = normalizedRedactor;
3094
+ }
3095
+ const devtoolsConfig = resolveDevtoolsConfig(mergedConfig.devtools);
3096
+ if (devtoolsConfig.enabled && mergedConfig.logs === void 0) mergedConfig.logs = true;
3097
+ const silent = mergedConfig.silent ?? false;
3098
+ const minLevel = mergedConfig.minLevel ?? "info";
3099
+ logger = wrapLogger(mergedConfig.logger || silentLogger, silent, minLevel);
3100
+ if (initialized) logger.warn({}, "[autotel] init() called again - last config wins. This may cause unexpected behavior.");
3101
+ config = mergedConfig;
3102
+ validationConfig = mergedConfig.validation || null;
3103
+ eventsConfig = mergedConfig.events || null;
3104
+ let endpoint = mergedConfig.endpoint ?? devtoolsConfig.endpoint;
3105
+ const version = mergedConfig.version || detectVersion();
3106
+ const environment = mergedConfig.environment || process.env.NODE_ENV || "development";
3107
+ const metricsEnabled = resolveMetricsFlag(mergedConfig.metrics);
3108
+ const logsEnabled = resolveLogsFlag(mergedConfig.logs);
3109
+ if (devtoolsConfig.enabled && devtoolsConfig.embedded) {
3110
+ const devtoolsModule = _optionalRequire("autotel-devtools");
3111
+ if (devtoolsModule?.createDevtools) {
2890
3112
  const devtoolsInstance = devtoolsModule.createDevtools({
2891
3113
  port: devtoolsConfig.port,
2892
3114
  host: devtoolsConfig.host,
@@ -3041,116 +3263,16 @@ function init(cfg) {
3041
3263
  }
3042
3264
  initialized = true;
3043
3265
  const processHandlers = mergedConfig.processHandlers;
3044
- if (processHandlers) installProcessHandlers(processHandlers === true ? {} : processHandlers, shutdown);
3266
+ const handlersConfig = processHandlers && processHandlers !== true ? processHandlers : {};
3267
+ if (processHandlers) installProcessHandlers(handlersConfig, shutdown);
3045
3268
  else uninstallProcessHandlers();
3046
- }
3047
- /**
3048
- * Extract instrumentation class names from instrumentation instances
3049
- * Used to detect duplicates between manual and auto instrumentations
3050
- */
3051
- function getInstrumentationNames(instrumentations) {
3052
- const names = /* @__PURE__ */ new Set();
3053
- if (!instrumentations) return names;
3054
- for (const instrumentation of instrumentations) if (instrumentation && typeof instrumentation === "object") names.add(instrumentation.constructor.name);
3055
- return names;
3056
- }
3057
- /**
3058
- * Map common instrumentation class names to their package names
3059
- * Used to disable auto-instrumentations when user provides manual configs
3060
- */
3061
- const INSTRUMENTATION_CLASS_TO_PACKAGE = {
3062
- HttpInstrumentation: "@opentelemetry/instrumentation-http",
3063
- HttpsInstrumentation: "@opentelemetry/instrumentation-http",
3064
- ExpressInstrumentation: "@opentelemetry/instrumentation-express",
3065
- FastifyInstrumentation: "@opentelemetry/instrumentation-fastify",
3066
- MongoDBInstrumentation: "@opentelemetry/instrumentation-mongodb",
3067
- MongooseInstrumentation: "@opentelemetry/instrumentation-mongoose",
3068
- PrismaInstrumentation: "@opentelemetry/instrumentation-prisma",
3069
- PinoInstrumentation: "@opentelemetry/instrumentation-pino",
3070
- WinstonInstrumentation: "@opentelemetry/instrumentation-winston",
3071
- RedisInstrumentation: "@opentelemetry/instrumentation-redis",
3072
- GraphQLInstrumentation: "@opentelemetry/instrumentation-graphql",
3073
- GrpcInstrumentation: "@opentelemetry/instrumentation-grpc",
3074
- IORedisInstrumentation: "@opentelemetry/instrumentation-ioredis",
3075
- KnexInstrumentation: "@opentelemetry/instrumentation-knex",
3076
- NestJsInstrumentation: "@opentelemetry/instrumentation-nestjs-core",
3077
- PgInstrumentation: "@opentelemetry/instrumentation-pg",
3078
- MySQLInstrumentation: "@opentelemetry/instrumentation-mysql",
3079
- MySQL2Instrumentation: "@opentelemetry/instrumentation-mysql2"
3080
- };
3081
- /**
3082
- * Detect if we're running in ESM mode
3083
- */
3084
- function isESMMode() {
3085
- try {
3086
- const fs = require_node_require.requireModule("node:fs");
3087
- try {
3088
- return JSON.parse(fs.readFileSync(`${process.cwd()}/package.json`, "utf8")).type === "module";
3089
- } catch {
3090
- return false;
3091
- }
3092
- } catch {
3093
- return false;
3094
- }
3095
- }
3096
- /**
3097
- * Lazy-load auto-instrumentations (optional peer dependency)
3098
- * Only loads when integrations config is truthy, avoiding ~40+ package imports at startup.
3099
- */
3100
- function loadNodeAutoInstrumentations() {
3101
- try {
3102
- return require_node_require.requireModule("@opentelemetry/auto-instrumentations-node").getNodeAutoInstrumentations;
3103
- } catch {
3104
- const isESM = isESMMode();
3105
- const baseMessage = "@opentelemetry/auto-instrumentations-node not found.";
3106
- if (isESM) throw new Error(`${baseMessage}\n\nESM Setup Required:
3107
- 1. Install as a direct dependency: pnpm add @opentelemetry/auto-instrumentations-node
3108
- 2. Create instrumentation.mjs with:
3109
- import 'autotel/register'; // MUST be first!
3110
- import { init } from 'autotel';
3111
- import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
3112
- init({ service: "my-app", instrumentations: getNodeAutoInstrumentations() });
3113
- 3. Run with: tsx --import ./instrumentation.mjs src/index.ts
3114
-
3115
- See: https://github.com/jagreehal/autotel#esm-setup`);
3116
- throw new Error(`${baseMessage} Install it: pnpm add @opentelemetry/auto-instrumentations-node`);
3117
- }
3118
- }
3119
- /**
3120
- * Injectable loader for testing. Set to override the default loader.
3121
- * @internal
3122
- */
3123
- let _autoInstrumentationsLoader = null;
3124
- /**
3125
- * Get auto-instrumentations based on simple integration names
3126
- * Excludes instrumentations that are manually provided to avoid conflicts
3127
- */
3128
- function getAutoInstrumentations(integrations, manualInstrumentationNames = /* @__PURE__ */ new Set()) {
3129
- if (integrations === false) return [];
3130
- const getNodeAutoInstrumentations = _autoInstrumentationsLoader ? _autoInstrumentationsLoader() : loadNodeAutoInstrumentations();
3131
- const exclusionConfig = {};
3132
- for (const className of manualInstrumentationNames) {
3133
- const packageName = INSTRUMENTATION_CLASS_TO_PACKAGE[className];
3134
- if (packageName) exclusionConfig[packageName] = { enabled: false };
3135
- }
3136
- if (integrations === true) {
3137
- if (Object.keys(exclusionConfig).length > 0) return getNodeAutoInstrumentations(exclusionConfig);
3138
- return getNodeAutoInstrumentations();
3139
- }
3140
- if (Array.isArray(integrations)) {
3141
- const config = { ...exclusionConfig };
3142
- for (const name of integrations) {
3143
- const packageName = `@opentelemetry/instrumentation-${name}`;
3144
- if (!exclusionConfig[packageName]) config[packageName] = { enabled: true };
3145
- }
3146
- return getNodeAutoInstrumentations(config);
3269
+ if (mergedConfig.flushOnExit !== false) {
3270
+ const timeoutMs = handlersConfig.shutdownTimeoutMs;
3271
+ installExitFlush(() => flush({
3272
+ forShutdown: true,
3273
+ timeout: timeoutMs
3274
+ }), timeoutMs);
3147
3275
  }
3148
- const config = {
3149
- ...exclusionConfig,
3150
- ...integrations
3151
- };
3152
- for (const packageName of Object.keys(exclusionConfig)) if (Object.keys(integrations).find((key) => packageName.includes(key))) config[packageName] = { enabled: false };
3153
- return getNodeAutoInstrumentations(config);
3154
3276
  }
3155
3277
  /**
3156
3278
  * Check if autotel has been initialized
@@ -3192,37 +3314,6 @@ function warnIfNotInitialized(context) {
3192
3314
  }
3193
3315
  }
3194
3316
  /**
3195
- * Auto-detect version from package.json
3196
- */
3197
- function detectVersion() {
3198
- try {
3199
- const fs = require_node_require.requireModule("node:fs");
3200
- return JSON.parse(fs.readFileSync(`${process.cwd()}/package.json`, "utf8")).version || "1.0.0";
3201
- } catch {
3202
- return "1.0.0";
3203
- }
3204
- }
3205
- /**
3206
- * Detect hostname for resource attributes.
3207
- * Supports Datadog conventions (DD_HOSTNAME) and falls back to system hostname.
3208
- *
3209
- * Priority order:
3210
- * 1. DD_HOSTNAME environment variable (Datadog convention)
3211
- * 2. HOSTNAME environment variable (common Unix convention)
3212
- * 3. os.hostname() (system hostname)
3213
- *
3214
- * @returns hostname string or undefined if detection fails
3215
- */
3216
- function detectHostname() {
3217
- if (process.env.DD_HOSTNAME) return process.env.DD_HOSTNAME;
3218
- if (process.env.HOSTNAME) return process.env.HOSTNAME;
3219
- try {
3220
- return require_node_require.requireModule("node:os").hostname();
3221
- } catch {
3222
- return;
3223
- }
3224
- }
3225
- /**
3226
3317
  * @internal Close embedded devtools if running.
3227
3318
  */
3228
3319
  async function _closeEmbeddedDevtools() {