autotel 2.1.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.
Files changed (272) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1946 -0
  3. package/dist/chunk-2LNRY4QK.js +273 -0
  4. package/dist/chunk-2LNRY4QK.js.map +1 -0
  5. package/dist/chunk-3HENGDW2.js +587 -0
  6. package/dist/chunk-3HENGDW2.js.map +1 -0
  7. package/dist/chunk-4OAT42CA.cjs +73 -0
  8. package/dist/chunk-4OAT42CA.cjs.map +1 -0
  9. package/dist/chunk-5GWX5LFW.js +70 -0
  10. package/dist/chunk-5GWX5LFW.js.map +1 -0
  11. package/dist/chunk-5R2M36QB.js +195 -0
  12. package/dist/chunk-5R2M36QB.js.map +1 -0
  13. package/dist/chunk-5ZN622AO.js +73 -0
  14. package/dist/chunk-5ZN622AO.js.map +1 -0
  15. package/dist/chunk-77MSMAUQ.cjs +498 -0
  16. package/dist/chunk-77MSMAUQ.cjs.map +1 -0
  17. package/dist/chunk-ABPEQ6RK.cjs +596 -0
  18. package/dist/chunk-ABPEQ6RK.cjs.map +1 -0
  19. package/dist/chunk-BWYGJKRB.js +95 -0
  20. package/dist/chunk-BWYGJKRB.js.map +1 -0
  21. package/dist/chunk-BZHG5IZ4.js +73 -0
  22. package/dist/chunk-BZHG5IZ4.js.map +1 -0
  23. package/dist/chunk-G7VZBCD6.cjs +35 -0
  24. package/dist/chunk-G7VZBCD6.cjs.map +1 -0
  25. package/dist/chunk-GVLK7YUU.cjs +30 -0
  26. package/dist/chunk-GVLK7YUU.cjs.map +1 -0
  27. package/dist/chunk-HCCXC7XG.js +205 -0
  28. package/dist/chunk-HCCXC7XG.js.map +1 -0
  29. package/dist/chunk-HE6T6FIX.cjs +203 -0
  30. package/dist/chunk-HE6T6FIX.cjs.map +1 -0
  31. package/dist/chunk-KIXWPOCO.cjs +100 -0
  32. package/dist/chunk-KIXWPOCO.cjs.map +1 -0
  33. package/dist/chunk-KVGNW3FC.js +87 -0
  34. package/dist/chunk-KVGNW3FC.js.map +1 -0
  35. package/dist/chunk-LITNXTTT.js +3 -0
  36. package/dist/chunk-LITNXTTT.js.map +1 -0
  37. package/dist/chunk-M4ANN7RL.js +114 -0
  38. package/dist/chunk-M4ANN7RL.js.map +1 -0
  39. package/dist/chunk-NC52UBR2.cjs +32 -0
  40. package/dist/chunk-NC52UBR2.cjs.map +1 -0
  41. package/dist/chunk-NHCNRQD3.cjs +212 -0
  42. package/dist/chunk-NHCNRQD3.cjs.map +1 -0
  43. package/dist/chunk-NZ72VDNY.cjs +4 -0
  44. package/dist/chunk-NZ72VDNY.cjs.map +1 -0
  45. package/dist/chunk-P6JUDYNO.js +57 -0
  46. package/dist/chunk-P6JUDYNO.js.map +1 -0
  47. package/dist/chunk-RJYY7BWX.js +1349 -0
  48. package/dist/chunk-RJYY7BWX.js.map +1 -0
  49. package/dist/chunk-TRI4V5BF.cjs +126 -0
  50. package/dist/chunk-TRI4V5BF.cjs.map +1 -0
  51. package/dist/chunk-UL33I6IS.js +139 -0
  52. package/dist/chunk-UL33I6IS.js.map +1 -0
  53. package/dist/chunk-URRW6M2C.cjs +61 -0
  54. package/dist/chunk-URRW6M2C.cjs.map +1 -0
  55. package/dist/chunk-UY3UYPBZ.cjs +77 -0
  56. package/dist/chunk-UY3UYPBZ.cjs.map +1 -0
  57. package/dist/chunk-W3253FGB.cjs +277 -0
  58. package/dist/chunk-W3253FGB.cjs.map +1 -0
  59. package/dist/chunk-W7LHZVQF.js +26 -0
  60. package/dist/chunk-W7LHZVQF.js.map +1 -0
  61. package/dist/chunk-WBWNM6LB.cjs +1360 -0
  62. package/dist/chunk-WBWNM6LB.cjs.map +1 -0
  63. package/dist/chunk-WFJ7L2RV.js +494 -0
  64. package/dist/chunk-WFJ7L2RV.js.map +1 -0
  65. package/dist/chunk-X4RMFFMR.js +28 -0
  66. package/dist/chunk-X4RMFFMR.js.map +1 -0
  67. package/dist/chunk-Y4Y2S7BM.cjs +92 -0
  68. package/dist/chunk-Y4Y2S7BM.cjs.map +1 -0
  69. package/dist/chunk-YLPNXZFI.cjs +143 -0
  70. package/dist/chunk-YLPNXZFI.cjs.map +1 -0
  71. package/dist/chunk-YTXEZ4SD.cjs +77 -0
  72. package/dist/chunk-YTXEZ4SD.cjs.map +1 -0
  73. package/dist/chunk-Z6ZWNWWR.js +30 -0
  74. package/dist/chunk-Z6ZWNWWR.js.map +1 -0
  75. package/dist/config.cjs +26 -0
  76. package/dist/config.cjs.map +1 -0
  77. package/dist/config.d.cts +75 -0
  78. package/dist/config.d.ts +75 -0
  79. package/dist/config.js +5 -0
  80. package/dist/config.js.map +1 -0
  81. package/dist/db.cjs +233 -0
  82. package/dist/db.cjs.map +1 -0
  83. package/dist/db.d.cts +123 -0
  84. package/dist/db.d.ts +123 -0
  85. package/dist/db.js +228 -0
  86. package/dist/db.js.map +1 -0
  87. package/dist/decorators.cjs +67 -0
  88. package/dist/decorators.cjs.map +1 -0
  89. package/dist/decorators.d.cts +91 -0
  90. package/dist/decorators.d.ts +91 -0
  91. package/dist/decorators.js +65 -0
  92. package/dist/decorators.js.map +1 -0
  93. package/dist/event-subscriber.cjs +6 -0
  94. package/dist/event-subscriber.cjs.map +1 -0
  95. package/dist/event-subscriber.d.cts +116 -0
  96. package/dist/event-subscriber.d.ts +116 -0
  97. package/dist/event-subscriber.js +3 -0
  98. package/dist/event-subscriber.js.map +1 -0
  99. package/dist/event-testing.cjs +21 -0
  100. package/dist/event-testing.cjs.map +1 -0
  101. package/dist/event-testing.d.cts +110 -0
  102. package/dist/event-testing.d.ts +110 -0
  103. package/dist/event-testing.js +4 -0
  104. package/dist/event-testing.js.map +1 -0
  105. package/dist/event.cjs +30 -0
  106. package/dist/event.cjs.map +1 -0
  107. package/dist/event.d.cts +282 -0
  108. package/dist/event.d.ts +282 -0
  109. package/dist/event.js +13 -0
  110. package/dist/event.js.map +1 -0
  111. package/dist/exporters.cjs +17 -0
  112. package/dist/exporters.cjs.map +1 -0
  113. package/dist/exporters.d.cts +1 -0
  114. package/dist/exporters.d.ts +1 -0
  115. package/dist/exporters.js +4 -0
  116. package/dist/exporters.js.map +1 -0
  117. package/dist/functional.cjs +46 -0
  118. package/dist/functional.cjs.map +1 -0
  119. package/dist/functional.d.cts +478 -0
  120. package/dist/functional.d.ts +478 -0
  121. package/dist/functional.js +13 -0
  122. package/dist/functional.js.map +1 -0
  123. package/dist/http.cjs +189 -0
  124. package/dist/http.cjs.map +1 -0
  125. package/dist/http.d.cts +169 -0
  126. package/dist/http.d.ts +169 -0
  127. package/dist/http.js +184 -0
  128. package/dist/http.js.map +1 -0
  129. package/dist/index.cjs +333 -0
  130. package/dist/index.cjs.map +1 -0
  131. package/dist/index.d.cts +758 -0
  132. package/dist/index.d.ts +758 -0
  133. package/dist/index.js +143 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/instrumentation.cjs +182 -0
  136. package/dist/instrumentation.cjs.map +1 -0
  137. package/dist/instrumentation.d.cts +49 -0
  138. package/dist/instrumentation.d.ts +49 -0
  139. package/dist/instrumentation.js +179 -0
  140. package/dist/instrumentation.js.map +1 -0
  141. package/dist/logger.cjs +19 -0
  142. package/dist/logger.cjs.map +1 -0
  143. package/dist/logger.d.cts +146 -0
  144. package/dist/logger.d.ts +146 -0
  145. package/dist/logger.js +6 -0
  146. package/dist/logger.js.map +1 -0
  147. package/dist/metric-helpers.cjs +31 -0
  148. package/dist/metric-helpers.cjs.map +1 -0
  149. package/dist/metric-helpers.d.cts +13 -0
  150. package/dist/metric-helpers.d.ts +13 -0
  151. package/dist/metric-helpers.js +6 -0
  152. package/dist/metric-helpers.js.map +1 -0
  153. package/dist/metric-testing.cjs +21 -0
  154. package/dist/metric-testing.cjs.map +1 -0
  155. package/dist/metric-testing.d.cts +110 -0
  156. package/dist/metric-testing.d.ts +110 -0
  157. package/dist/metric-testing.js +4 -0
  158. package/dist/metric-testing.js.map +1 -0
  159. package/dist/metric.cjs +26 -0
  160. package/dist/metric.cjs.map +1 -0
  161. package/dist/metric.d.cts +240 -0
  162. package/dist/metric.d.ts +240 -0
  163. package/dist/metric.js +9 -0
  164. package/dist/metric.js.map +1 -0
  165. package/dist/processors.cjs +17 -0
  166. package/dist/processors.cjs.map +1 -0
  167. package/dist/processors.d.cts +1 -0
  168. package/dist/processors.d.ts +1 -0
  169. package/dist/processors.js +4 -0
  170. package/dist/processors.js.map +1 -0
  171. package/dist/sampling.cjs +40 -0
  172. package/dist/sampling.cjs.map +1 -0
  173. package/dist/sampling.d.cts +260 -0
  174. package/dist/sampling.d.ts +260 -0
  175. package/dist/sampling.js +7 -0
  176. package/dist/sampling.js.map +1 -0
  177. package/dist/semantic-helpers.cjs +35 -0
  178. package/dist/semantic-helpers.cjs.map +1 -0
  179. package/dist/semantic-helpers.d.cts +442 -0
  180. package/dist/semantic-helpers.d.ts +442 -0
  181. package/dist/semantic-helpers.js +14 -0
  182. package/dist/semantic-helpers.js.map +1 -0
  183. package/dist/tail-sampling-processor.cjs +13 -0
  184. package/dist/tail-sampling-processor.cjs.map +1 -0
  185. package/dist/tail-sampling-processor.d.cts +27 -0
  186. package/dist/tail-sampling-processor.d.ts +27 -0
  187. package/dist/tail-sampling-processor.js +4 -0
  188. package/dist/tail-sampling-processor.js.map +1 -0
  189. package/dist/testing.cjs +286 -0
  190. package/dist/testing.cjs.map +1 -0
  191. package/dist/testing.d.cts +291 -0
  192. package/dist/testing.d.ts +291 -0
  193. package/dist/testing.js +263 -0
  194. package/dist/testing.js.map +1 -0
  195. package/dist/trace-context-DRZdUvVY.d.cts +181 -0
  196. package/dist/trace-context-DRZdUvVY.d.ts +181 -0
  197. package/dist/trace-helpers.cjs +54 -0
  198. package/dist/trace-helpers.cjs.map +1 -0
  199. package/dist/trace-helpers.d.cts +524 -0
  200. package/dist/trace-helpers.d.ts +524 -0
  201. package/dist/trace-helpers.js +5 -0
  202. package/dist/trace-helpers.js.map +1 -0
  203. package/dist/tracer-provider.cjs +21 -0
  204. package/dist/tracer-provider.cjs.map +1 -0
  205. package/dist/tracer-provider.d.cts +169 -0
  206. package/dist/tracer-provider.d.ts +169 -0
  207. package/dist/tracer-provider.js +4 -0
  208. package/dist/tracer-provider.js.map +1 -0
  209. package/package.json +280 -0
  210. package/src/baggage-span-processor.test.ts +202 -0
  211. package/src/baggage-span-processor.ts +98 -0
  212. package/src/circuit-breaker.test.ts +341 -0
  213. package/src/circuit-breaker.ts +184 -0
  214. package/src/config.test.ts +94 -0
  215. package/src/config.ts +169 -0
  216. package/src/db.test.ts +252 -0
  217. package/src/db.ts +447 -0
  218. package/src/decorators.test.ts +203 -0
  219. package/src/decorators.ts +188 -0
  220. package/src/env-config.test.ts +246 -0
  221. package/src/env-config.ts +158 -0
  222. package/src/event-queue.test.ts +222 -0
  223. package/src/event-queue.ts +203 -0
  224. package/src/event-subscriber.ts +136 -0
  225. package/src/event-testing.ts +197 -0
  226. package/src/event.test.ts +718 -0
  227. package/src/event.ts +556 -0
  228. package/src/exporters.ts +96 -0
  229. package/src/functional.test.ts +1059 -0
  230. package/src/functional.ts +2295 -0
  231. package/src/http.test.ts +487 -0
  232. package/src/http.ts +424 -0
  233. package/src/index.ts +158 -0
  234. package/src/init.customization.test.ts +210 -0
  235. package/src/init.integrations.test.ts +366 -0
  236. package/src/init.openllmetry.test.ts +282 -0
  237. package/src/init.protocol.test.ts +215 -0
  238. package/src/init.ts +1426 -0
  239. package/src/instrumentation.test.ts +108 -0
  240. package/src/instrumentation.ts +308 -0
  241. package/src/logger.test.ts +117 -0
  242. package/src/logger.ts +246 -0
  243. package/src/metric-helpers.ts +47 -0
  244. package/src/metric-testing.ts +197 -0
  245. package/src/metric.ts +434 -0
  246. package/src/metrics.test.ts +205 -0
  247. package/src/operation-context.ts +93 -0
  248. package/src/processors.ts +106 -0
  249. package/src/rate-limiter.test.ts +199 -0
  250. package/src/rate-limiter.ts +98 -0
  251. package/src/sampling.test.ts +513 -0
  252. package/src/sampling.ts +428 -0
  253. package/src/semantic-helpers.test.ts +311 -0
  254. package/src/semantic-helpers.ts +584 -0
  255. package/src/shutdown.test.ts +311 -0
  256. package/src/shutdown.ts +222 -0
  257. package/src/stub.integration.test.ts +361 -0
  258. package/src/tail-sampling-processor.test.ts +226 -0
  259. package/src/tail-sampling-processor.ts +51 -0
  260. package/src/testing.ts +670 -0
  261. package/src/trace-context.ts +470 -0
  262. package/src/trace-helpers.new.test.ts +278 -0
  263. package/src/trace-helpers.test.ts +242 -0
  264. package/src/trace-helpers.ts +690 -0
  265. package/src/tracer-provider.test.ts +183 -0
  266. package/src/tracer-provider.ts +266 -0
  267. package/src/track.test.ts +153 -0
  268. package/src/track.ts +120 -0
  269. package/src/validation.test.ts +306 -0
  270. package/src/validation.ts +239 -0
  271. package/src/variable-name-inference.test.ts +178 -0
  272. package/src/variable-name-inference.ts +242 -0
package/src/init.ts ADDED
@@ -0,0 +1,1426 @@
1
+ /**
2
+ * Simplified initialization for autotel
3
+ *
4
+ * Single init() function with sensible defaults.
5
+ * Replaces initInstrumentation() and separate events config.
6
+ */
7
+
8
+ import { NodeSDK } from '@opentelemetry/sdk-node';
9
+ import type { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
10
+ import {
11
+ BatchSpanProcessor,
12
+ type SpanProcessor,
13
+ SimpleSpanProcessor,
14
+ ConsoleSpanExporter,
15
+ } from '@opentelemetry/sdk-trace-base';
16
+ import type { SpanExporter } from '@opentelemetry/sdk-trace-base';
17
+ import {
18
+ resourceFromAttributes,
19
+ type Resource,
20
+ } from '@opentelemetry/resources';
21
+ import {
22
+ ATTR_SERVICE_NAME,
23
+ ATTR_SERVICE_VERSION,
24
+ } from '@opentelemetry/semantic-conventions';
25
+ import type { Sampler } from './sampling';
26
+ import { AdaptiveSampler } from './sampling';
27
+ import type { EventSubscriber } from './event-subscriber';
28
+ import type { Logger } from './logger';
29
+ import type { Attributes } from '@opentelemetry/api';
30
+ import type { ValidationConfig } from './validation';
31
+ import {
32
+ PeriodicExportingMetricReader,
33
+ type MetricReader,
34
+ } from '@opentelemetry/sdk-metrics';
35
+ import { OTLPMetricExporter as OTLPMetricExporterHTTP } from '@opentelemetry/exporter-metrics-otlp-http';
36
+ import { OTLPTraceExporter as OTLPTraceExporterHTTP } from '@opentelemetry/exporter-trace-otlp-http';
37
+ import type { PushMetricExporter } from '@opentelemetry/sdk-metrics';
38
+ import type { LogRecordProcessor } from '@opentelemetry/sdk-logs';
39
+ import { TailSamplingSpanProcessor } from './tail-sampling-processor';
40
+ import { BaggageSpanProcessor } from './baggage-span-processor';
41
+ import { resolveConfigFromEnv } from './env-config';
42
+ import { PinoInstrumentation } from '@opentelemetry/instrumentation-pino';
43
+ import { WinstonInstrumentation } from '@opentelemetry/instrumentation-winston';
44
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
45
+
46
+ // Type imports for exporters
47
+ type OTLPExporterConfig = {
48
+ url?: string;
49
+ headers?: Record<string, string>;
50
+ timeoutMillis?: number;
51
+ concurrencyLimit?: number;
52
+ };
53
+
54
+ // Lazy-load gRPC exporters (optional peer dependencies)
55
+ let OTLPTraceExporterGRPC:
56
+ | (new (config: OTLPExporterConfig) => SpanExporter)
57
+ | undefined;
58
+ let OTLPMetricExporterGRPC:
59
+ | (new (config: OTLPExporterConfig) => PushMetricExporter)
60
+ | undefined;
61
+
62
+ /**
63
+ * Helper: Lazy-load gRPC trace exporter
64
+ */
65
+ function loadGRPCTraceExporter(): new (
66
+ config: OTLPExporterConfig,
67
+ ) => SpanExporter {
68
+ if (OTLPTraceExporterGRPC) return OTLPTraceExporterGRPC;
69
+
70
+ try {
71
+ // Dynamic import for optional peer dependency
72
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
73
+ const grpcModule = require('@opentelemetry/exporter-trace-otlp-grpc');
74
+ OTLPTraceExporterGRPC = grpcModule.OTLPTraceExporter as new (
75
+ config: OTLPExporterConfig,
76
+ ) => SpanExporter;
77
+ return OTLPTraceExporterGRPC;
78
+ } catch {
79
+ throw new Error(
80
+ 'gRPC trace exporter not found. Install with: pnpm add @opentelemetry/exporter-trace-otlp-grpc',
81
+ );
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Helper: Lazy-load gRPC metric exporter
87
+ */
88
+ function loadGRPCMetricExporter(): new (
89
+ config: OTLPExporterConfig,
90
+ ) => PushMetricExporter {
91
+ if (OTLPMetricExporterGRPC) return OTLPMetricExporterGRPC;
92
+
93
+ try {
94
+ // Dynamic import for optional peer dependency
95
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
96
+ const grpcModule = require('@opentelemetry/exporter-metrics-otlp-grpc');
97
+ OTLPMetricExporterGRPC = grpcModule.OTLPMetricExporter as new (
98
+ config: OTLPExporterConfig,
99
+ ) => PushMetricExporter;
100
+ return OTLPMetricExporterGRPC;
101
+ } catch {
102
+ throw new Error(
103
+ 'gRPC metric exporter not found. Install with: pnpm add @opentelemetry/exporter-metrics-otlp-grpc',
104
+ );
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Helper: Create trace exporter based on protocol
110
+ */
111
+ function createTraceExporter(
112
+ protocol: 'http' | 'grpc',
113
+ config: OTLPExporterConfig,
114
+ ): SpanExporter {
115
+ if (protocol === 'grpc') {
116
+ const Exporter = loadGRPCTraceExporter();
117
+ return new Exporter(config);
118
+ }
119
+
120
+ // Default: HTTP
121
+ return new OTLPTraceExporterHTTP(config);
122
+ }
123
+
124
+ /**
125
+ * Helper: Create metric exporter based on protocol
126
+ */
127
+ function createMetricExporter(
128
+ protocol: 'http' | 'grpc',
129
+ config: OTLPExporterConfig,
130
+ ): PushMetricExporter {
131
+ if (protocol === 'grpc') {
132
+ const Exporter = loadGRPCMetricExporter();
133
+ return new Exporter(config);
134
+ }
135
+
136
+ // Default: HTTP
137
+ return new OTLPMetricExporterHTTP(config);
138
+ }
139
+
140
+ /**
141
+ * Helper: Resolve protocol from config and environment
142
+ */
143
+ function resolveProtocol(configProtocol?: 'http' | 'grpc'): 'http' | 'grpc' {
144
+ // 1. Check config parameter (highest priority)
145
+ if (configProtocol === 'grpc' || configProtocol === 'http') {
146
+ return configProtocol;
147
+ }
148
+
149
+ // 2. Check OTEL_EXPORTER_OTLP_PROTOCOL env var
150
+ const envProtocol = process.env.OTEL_EXPORTER_OTLP_PROTOCOL;
151
+ if (envProtocol === 'grpc') return 'grpc';
152
+ if (envProtocol === 'http/protobuf' || envProtocol === 'http') return 'http';
153
+
154
+ // 3. Default to HTTP
155
+ return 'http';
156
+ }
157
+
158
+ /**
159
+ * Helper: Adjust endpoint URL for protocol
160
+ * gRPC exporters don't need the /v1/traces or /v1/metrics path
161
+ * HTTP exporters need the full path
162
+ */
163
+ function formatEndpointUrl(
164
+ endpoint: string,
165
+ signal: 'traces' | 'metrics',
166
+ protocol: 'http' | 'grpc',
167
+ ): string {
168
+ if (protocol === 'grpc') {
169
+ // gRPC: strip any paths, return base endpoint
170
+ return endpoint.replace(/\/(v1\/)?(traces|metrics|logs)$/, '');
171
+ }
172
+
173
+ // HTTP: append signal path if not present
174
+ if (!endpoint.endsWith(`/v1/${signal}`)) {
175
+ return `${endpoint}/v1/${signal}`;
176
+ }
177
+
178
+ return endpoint;
179
+ }
180
+
181
+ /**
182
+ * Default silent logger (no-op) when user doesn't provide one
183
+ */
184
+ const silentLogger: Logger = {
185
+ info: () => {},
186
+ warn: () => {},
187
+ error: () => {},
188
+ debug: () => {},
189
+ };
190
+
191
+ export interface AutotelConfig {
192
+ /** Service name (required) */
193
+ service: string;
194
+
195
+ /** Event subscribers - bring your own (PostHog, Mixpanel, etc.) */
196
+ subscribers?: EventSubscriber[];
197
+
198
+ /**
199
+ * Additional OpenTelemetry instrumentations to register.
200
+ * Useful when you want HTTP/Prisma/etc auto instrumentation alongside
201
+ * the functional helpers.
202
+ *
203
+ * **Important:** If you need custom instrumentation configs (like `requireParentSpan: false`),
204
+ * use EITHER manual instrumentations OR integrations, not both for the same library.
205
+ * Manual instrumentations always take precedence over auto-instrumentations.
206
+ *
207
+ * @example Manual instrumentations with custom config
208
+ * ```typescript
209
+ * import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'
210
+ *
211
+ * init({
212
+ * service: 'my-app',
213
+ * integrations: false, // Disable auto-instrumentations
214
+ * instrumentations: [
215
+ * new MongoDBInstrumentation({
216
+ * requireParentSpan: false // Custom config
217
+ * })
218
+ * ]
219
+ * })
220
+ * ```
221
+ *
222
+ * @example Mix auto + manual (auto for most, manual for specific configs)
223
+ * ```typescript
224
+ * import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'
225
+ *
226
+ * init({
227
+ * service: 'my-app',
228
+ * integrations: ['http', 'express'], // Auto for these
229
+ * instrumentations: [
230
+ * new MongoDBInstrumentation({
231
+ * requireParentSpan: false // Manual config for MongoDB
232
+ * })
233
+ * ]
234
+ * })
235
+ * ```
236
+ */
237
+ instrumentations?: NodeSDKConfiguration['instrumentations'];
238
+
239
+ /**
240
+ * Simple integration names for auto-instrumentation.
241
+ * Uses @opentelemetry/auto-instrumentations-node (peer dependency).
242
+ *
243
+ * **Important:** If you provide manual instrumentations for the same library,
244
+ * the manual config takes precedence and auto-instrumentation for that library is disabled.
245
+ *
246
+ * @example Enable all integrations (simple approach)
247
+ * ```typescript
248
+ * init({
249
+ * service: 'my-app',
250
+ * integrations: true // Enable all with defaults
251
+ * })
252
+ * ```
253
+ *
254
+ * @example Enable specific integrations
255
+ * ```typescript
256
+ * init({
257
+ * service: 'my-app',
258
+ * integrations: ['express', 'pino', 'http']
259
+ * })
260
+ * ```
261
+ *
262
+ * @example Configure specific integrations
263
+ * ```typescript
264
+ * init({
265
+ * service: 'my-app',
266
+ * integrations: {
267
+ * express: { enabled: true },
268
+ * pino: { enabled: true },
269
+ * http: { enabled: false }
270
+ * }
271
+ * })
272
+ * ```
273
+ *
274
+ * @example Manual config when you need custom settings
275
+ * ```typescript
276
+ * import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'
277
+ *
278
+ * init({
279
+ * service: 'my-app',
280
+ * integrations: false, // Use manual control
281
+ * instrumentations: [
282
+ * new MongoDBInstrumentation({
283
+ * requireParentSpan: false // Custom config not available with auto
284
+ * })
285
+ * ]
286
+ * })
287
+ * ```
288
+ */
289
+ integrations?: string[] | boolean | Record<string, { enabled?: boolean }>;
290
+
291
+ /**
292
+ * OTLP endpoint for traces/metrics/logs
293
+ * Only used if you don't provide custom exporters/processors
294
+ * @default process.env.OTLP_ENDPOINT || 'http://localhost:4318'
295
+ */
296
+ endpoint?: string;
297
+
298
+ /**
299
+ * Custom span processors for traces (supports multiple processors)
300
+ * Allows you to use any backend: Jaeger, Zipkin, Datadog, New Relic, etc.
301
+ * If not provided, defaults to OTLP with tail sampling
302
+ *
303
+ * @example Multiple processors
304
+ * ```typescript
305
+ * import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
306
+ * import { BatchSpanProcessor, SimpleSpanProcessor, ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base'
307
+ *
308
+ * init({
309
+ * service: 'my-app',
310
+ * spanProcessors: [
311
+ * new BatchSpanProcessor(new JaegerExporter()),
312
+ * new SimpleSpanProcessor(new ConsoleSpanExporter()) // Debug alongside production
313
+ * ]
314
+ * })
315
+ * ```
316
+ *
317
+ * @example Single processor
318
+ * ```typescript
319
+ * import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
320
+ *
321
+ * init({
322
+ * service: 'my-app',
323
+ * spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())]
324
+ * })
325
+ * ```
326
+ */
327
+ spanProcessors?: SpanProcessor[];
328
+
329
+ /**
330
+ * Custom span exporters for traces (alternative to spanProcessors, supports multiple exporters)
331
+ * Provide either spanProcessors OR spanExporters, not both
332
+ * Each exporter will be wrapped in TailSamplingSpanProcessor + BatchSpanProcessor
333
+ *
334
+ * @example Multiple exporters
335
+ * ```typescript
336
+ * import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
337
+ * import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
338
+ *
339
+ * init({
340
+ * service: 'my-app',
341
+ * spanExporters: [
342
+ * new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' }),
343
+ * new JaegerExporter() // Send to multiple backends simultaneously
344
+ * ]
345
+ * })
346
+ * ```
347
+ *
348
+ * @example Single exporter
349
+ * ```typescript
350
+ * import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
351
+ *
352
+ * init({
353
+ * service: 'my-app',
354
+ * spanExporters: [new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' })]
355
+ * })
356
+ * ```
357
+ */
358
+ spanExporters?: SpanExporter[];
359
+
360
+ /**
361
+ * Custom metric readers (supports multiple readers)
362
+ * Allows sending metrics to multiple backends: OTLP, Prometheus, custom readers
363
+ * Defaults to OTLP metrics exporter when metrics are enabled.
364
+ *
365
+ * @example Multiple metric readers
366
+ * ```typescript
367
+ * import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
368
+ * import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
369
+ * import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'
370
+ *
371
+ * init({
372
+ * service: 'my-app',
373
+ * metricReaders: [
374
+ * new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter() }),
375
+ * new PrometheusExporter() // Export to multiple backends
376
+ * ]
377
+ * })
378
+ * ```
379
+ */
380
+ metricReaders?: MetricReader[];
381
+
382
+ /**
383
+ * Custom log record processors. When omitted, logs are not configured.
384
+ */
385
+ logRecordProcessors?: LogRecordProcessor[];
386
+
387
+ /** Additional resource attributes to merge with defaults. */
388
+ resourceAttributes?: Attributes;
389
+
390
+ /** Provide a fully custom Resource to merge (advanced use case). */
391
+ resource?: Resource;
392
+
393
+ /**
394
+ * Headers for default OTLP exporters. Accepts either an object map or
395
+ * a "key=value" comma separated string.
396
+ */
397
+ otlpHeaders?: Record<string, string> | string;
398
+
399
+ /**
400
+ * OTLP protocol to use for traces, metrics, and logs
401
+ * - 'http': HTTP/protobuf (default, uses port 4318)
402
+ * - 'grpc': gRPC (uses port 4317)
403
+ *
404
+ * Can be overridden with OTEL_EXPORTER_OTLP_PROTOCOL env var.
405
+ *
406
+ * Note: gRPC exporters are optional peer dependencies. Install them with:
407
+ * ```bash
408
+ * pnpm add @opentelemetry/exporter-trace-otlp-grpc @opentelemetry/exporter-metrics-otlp-grpc
409
+ * ```
410
+ *
411
+ * @example HTTP (default)
412
+ * ```typescript
413
+ * init({
414
+ * service: 'my-app',
415
+ * protocol: 'http', // or omit (defaults to http)
416
+ * endpoint: 'http://localhost:4318'
417
+ * })
418
+ * ```
419
+ *
420
+ * @example gRPC
421
+ * ```typescript
422
+ * init({
423
+ * service: 'my-app',
424
+ * protocol: 'grpc',
425
+ * endpoint: 'grpc://localhost:4317'
426
+ * })
427
+ * ```
428
+ *
429
+ * @default 'http'
430
+ */
431
+ protocol?: 'http' | 'grpc';
432
+
433
+ /**
434
+ * Optional factory to build a customised NodeSDK instance from our defaults.
435
+ */
436
+ sdkFactory?: (defaults: Partial<NodeSDKConfiguration>) => NodeSDK;
437
+
438
+ /**
439
+ * Infrastructure metrics configuration
440
+ * - true: always enabled (default)
441
+ * - false: always disabled
442
+ * - 'auto': always enabled (same as true)
443
+ *
444
+ * Can be overridden with AUTOTELEMETRY_METRICS=on|off env var
445
+ */
446
+ metrics?: boolean | 'auto';
447
+
448
+ /** Sampling strategy (default: AdaptiveSampler with 10% baseline) */
449
+ sampler?: Sampler;
450
+
451
+ /** Service version (default: auto-detect from package.json or '1.0.0') */
452
+ version?: string;
453
+
454
+ /** Environment (default: process.env.NODE_ENV || 'development') */
455
+ environment?: string;
456
+
457
+ /**
458
+ * Logger instance for structured logging with automatic trace correlation
459
+ *
460
+ * **Recommended:** Bring your own Pino or Winston instance
461
+ *
462
+ * Autotel automatically instruments Pino and Winston loggers to:
463
+ * - Inject trace context (traceId, spanId) into every log record
464
+ * - Record errors in the active OpenTelemetry span
465
+ * - Bridge logs to the OpenTelemetry Logs API for OTLP export to Grafana, Datadog, etc.
466
+ *
467
+ * Supports any logger with 4 methods: info/warn/error/debug
468
+ * Default: silent logger (no-op)
469
+ *
470
+ * @example Using Pino (recommended)
471
+ * ```typescript
472
+ * import pino from 'pino' // npm install pino
473
+ * import { init } from 'autotel'
474
+ *
475
+ * const logger = pino({ level: 'info' })
476
+ * init({ service: 'my-app', logger })
477
+ *
478
+ * // Logs automatically include traceId/spanId and export via OTLP!
479
+ * logger.info('User created', { userId: '123' })
480
+ * ```
481
+ *
482
+ * @example Using Winston
483
+ * ```typescript
484
+ * import winston from 'winston' // npm install winston
485
+ * import { init } from 'autotel'
486
+ *
487
+ * const logger = winston.createLogger({
488
+ * level: 'info',
489
+ * format: winston.format.json()
490
+ * })
491
+ * init({ service: 'my-app', logger })
492
+ * ```
493
+ *
494
+ * @example Custom logger (any logger with 4 methods)
495
+ * ```typescript
496
+ * const logger = {
497
+ * info: (msg, extra) => console.log(msg, extra),
498
+ * warn: (msg, extra) => console.warn(msg, extra),
499
+ * error: (msg, err, extra) => console.error(msg, err, extra),
500
+ * debug: (msg, extra) => console.debug(msg, extra),
501
+ * }
502
+ * init({ service: 'my-app', logger })
503
+ * ```
504
+ */
505
+ logger?: Logger;
506
+
507
+ /**
508
+ * Automatically flush events queue when root spans end
509
+ * - true: Auto-flush on root span completion (default)
510
+ * - false: Use batching (events flush every 10 seconds automatically)
511
+ *
512
+ * Only flushes on root spans to avoid excessive network calls.
513
+ * Default is true for serverless/short-lived processes. Set to false
514
+ * for long-running services where batching is more efficient.
515
+ */
516
+ autoFlushEvents?: boolean;
517
+
518
+ /**
519
+ * Include OpenTelemetry span flushing in auto-flush (default: false)
520
+ *
521
+ * When enabled, spans are force-flushed along with events events on root
522
+ * span completion. This is useful for serverless/short-lived processes where
523
+ * spans may not export before the process ends.
524
+ *
525
+ * - true: Force-flush spans on root span completion (~50-200ms latency)
526
+ * - false: Spans export via normal batch processor (default behavior)
527
+ *
528
+ * Only applies when autoFlushEvents is also enabled.
529
+ *
530
+ * Note: For edge runtimes (Cloudflare Workers, Vercel Edge), use the
531
+ * 'autotel-edge' package instead, which handles this automatically.
532
+ *
533
+ * @example Serverless with auto-flush
534
+ * ```typescript
535
+ * init({
536
+ * service: 'my-lambda',
537
+ * autoFlushEvents: true,
538
+ * autoFlush: true, // Force-flush spans
539
+ * });
540
+ * ```
541
+ */
542
+ autoFlush?: boolean;
543
+
544
+ /**
545
+ * Automatically copy baggage entries to span attributes
546
+ *
547
+ * When enabled, all baggage entries are automatically added as span attributes,
548
+ * making them visible in trace UIs (Jaeger, Grafana, DataDog, etc.) without
549
+ * manually calling ctx.setAttribute() for each entry.
550
+ *
551
+ * - `true`: adds baggage with 'baggage.' prefix (e.g. baggage.tenant.id)
552
+ * - `string`: uses custom prefix (e.g. 'ctx' → ctx.tenant.id, '' → tenant.id)
553
+ * - `false` or omit: disabled (default)
554
+ *
555
+ * @default false
556
+ *
557
+ * @example Enable with default prefix
558
+ * ```typescript
559
+ * init({
560
+ * service: 'my-app',
561
+ * baggage: true
562
+ * });
563
+ *
564
+ * // Now baggage automatically appears as span attributes
565
+ * await withBaggage({
566
+ * baggage: { 'tenant.id': 't1', 'user.id': 'u1' },
567
+ * fn: async () => {
568
+ * // Span has baggage.tenant.id and baggage.user.id attributes!
569
+ * }
570
+ * });
571
+ * ```
572
+ *
573
+ * @example Custom prefix
574
+ * ```typescript
575
+ * init({
576
+ * service: 'my-app',
577
+ * baggage: 'ctx' // Uses 'ctx.' prefix
578
+ * });
579
+ * // Creates attributes: ctx.tenant.id, ctx.user.id
580
+ * ```
581
+ *
582
+ * @example No prefix
583
+ * ```typescript
584
+ * init({
585
+ * service: 'my-app',
586
+ * baggage: '' // No prefix
587
+ * });
588
+ * // Creates attributes: tenant.id, user.id
589
+ * ```
590
+ */
591
+ baggage?: boolean | string;
592
+
593
+ /**
594
+ * Validation configuration for events events
595
+ * - Override default sensitive field patterns for redaction
596
+ * - Customize max lengths, nesting depth, etc.
597
+ *
598
+ * @example Disable redaction for development
599
+ * ```typescript
600
+ * init({
601
+ * service: 'my-app',
602
+ * validation: {
603
+ * sensitivePatterns: [] // Disable all redaction
604
+ * }
605
+ * })
606
+ * ```
607
+ *
608
+ * @example Add custom patterns
609
+ * ```typescript
610
+ * init({
611
+ * service: 'my-app',
612
+ * validation: {
613
+ * sensitivePatterns: [
614
+ * /password/i,
615
+ * /apiKey/i,
616
+ * /customSecret/i // Your custom pattern
617
+ * ]
618
+ * }
619
+ * })
620
+ * ```
621
+ */
622
+ validation?: Partial<ValidationConfig>;
623
+
624
+ /**
625
+ * Debug mode for local span inspection.
626
+ * Enables console output to help you see spans as they're created.
627
+ *
628
+ * When true: Outputs spans to console AND sends to backend (if endpoint/exporter configured)
629
+ * When false/undefined: Sends to backend only (default behavior)
630
+ *
631
+ * Perfect for progressive development:
632
+ * - Start with debug: true (no endpoint) → console-only, see traces immediately
633
+ * - Add endpoint later → console + backend, verify before choosing provider
634
+ * - Remove debug in production → backend only, clean production config
635
+ *
636
+ * Can be overridden with AUTOLEMETRY_DEBUG environment variable.
637
+ *
638
+ * @example Getting started - see spans immediately
639
+ * ```typescript
640
+ * init({
641
+ * service: 'my-app',
642
+ * debug: true // No endpoint yet - console only!
643
+ * })
644
+ * ```
645
+ *
646
+ * @example Testing with local collector
647
+ * ```typescript
648
+ * init({
649
+ * service: 'my-app',
650
+ * debug: true,
651
+ * endpoint: 'http://localhost:4318' // Console + OTLP
652
+ * })
653
+ * ```
654
+ *
655
+ * @example Production debugging
656
+ * ```typescript
657
+ * init({
658
+ * service: 'my-app',
659
+ * debug: true, // See what's being sent
660
+ * endpoint: 'https://api.honeycomb.io'
661
+ * })
662
+ * ```
663
+ *
664
+ * @example Environment variable
665
+ * ```bash
666
+ * AUTOLEMETRY_DEBUG=true node server.js
667
+ * ```
668
+ */
669
+ debug?: boolean;
670
+
671
+ /**
672
+ * OpenLLMetry integration for LLM observability.
673
+ * Requires @traceloop/node-server-sdk as an optional peer dependency.
674
+ *
675
+ * @example Enable OpenLLMetry with default settings
676
+ * ```typescript
677
+ * init({
678
+ * service: 'my-app',
679
+ * openllmetry: { enabled: true }
680
+ * })
681
+ * ```
682
+ *
683
+ * @example Enable with custom options
684
+ * ```typescript
685
+ * init({
686
+ * service: 'my-app',
687
+ * openllmetry: {
688
+ * enabled: true,
689
+ * options: {
690
+ * disableBatch: process.env.NODE_ENV !== 'production',
691
+ * apiKey: process.env.TRACELOOP_API_KEY
692
+ * }
693
+ * }
694
+ * })
695
+ * ```
696
+ */
697
+ openllmetry?: {
698
+ enabled: boolean;
699
+ options?: Record<string, unknown>;
700
+ };
701
+ }
702
+
703
+ // Internal state
704
+ let initialized = false;
705
+ let config: AutotelConfig | null = null;
706
+ let sdk: NodeSDK | null = null;
707
+ let warnedOnce = false;
708
+ let logger: Logger = silentLogger;
709
+ let validationConfig: Partial<ValidationConfig> | null = null;
710
+
711
+ /**
712
+ * Resolve metrics flag with env var override support
713
+ */
714
+ export function resolveMetricsFlag(
715
+ configFlag: boolean | 'auto' = 'auto',
716
+ ): boolean {
717
+ // 1. Check env var override (highest priority)
718
+ const envFlag = process.env.AUTOTELEMETRY_METRICS;
719
+ if (envFlag === 'on' || envFlag === 'true') return true;
720
+ if (envFlag === 'off' || envFlag === 'false') return false;
721
+
722
+ // 2. Check config flag
723
+ if (configFlag === true) return true;
724
+ if (configFlag === false) return false;
725
+
726
+ // 3. Default: enabled in all environments (simpler)
727
+ return true;
728
+ }
729
+
730
+ /**
731
+ * Resolve debug flag with env var override support
732
+ */
733
+ export function resolveDebugFlag(configFlag?: boolean): boolean {
734
+ // 1. Check env var override (highest priority)
735
+ const envFlag = process.env.AUTOLEMETRY_DEBUG;
736
+ if (envFlag === 'true' || envFlag === '1') return true;
737
+ if (envFlag === 'false' || envFlag === '0') return false;
738
+
739
+ // 2. Return config flag (defaults to false)
740
+ return configFlag ?? false;
741
+ }
742
+
743
+ function normalizeOtlpHeaders(
744
+ headers?: Record<string, string> | string,
745
+ ): Record<string, string> | undefined {
746
+ if (!headers) return undefined;
747
+ if (typeof headers !== 'string') return headers;
748
+
749
+ const parsed: Record<string, string> = {};
750
+ for (const pair of headers.split(',')) {
751
+ const [key, ...valueParts] = pair.split('=');
752
+ if (!key || valueParts.length === 0) continue;
753
+ parsed[key.trim()] = valueParts.join('=').trim();
754
+ }
755
+ return parsed;
756
+ }
757
+
758
+ /**
759
+ * Initialize autotel - Write Once, Observe Everywhere
760
+ *
761
+ * Follows OpenTelemetry standards: opinionated defaults with full flexibility
762
+ * Idempotent: multiple calls are safe, last one wins
763
+ *
764
+ * @example Minimal setup (OTLP default)
765
+ * ```typescript
766
+ * init({ service: 'my-app' })
767
+ * ```
768
+ *
769
+ * @example With events (observe in PostHog, Mixpanel, etc.)
770
+ * ```typescript
771
+ * import { PostHogSubscriber } from 'autotel-subscribers/posthog';
772
+ *
773
+ * init({
774
+ * service: 'my-app',
775
+ * subscribers: [new PostHogSubscriber({ apiKey: '...' })]
776
+ * })
777
+ * ```
778
+ *
779
+ * @example Observe in Jaeger
780
+ * ```typescript
781
+ * import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
782
+ *
783
+ * init({
784
+ * service: 'my-app',
785
+ * spanExporter: new JaegerExporter({ endpoint: 'http://localhost:14268/api/traces' })
786
+ * })
787
+ * ```
788
+ *
789
+ * @example Observe in Zipkin
790
+ * ```typescript
791
+ * import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
792
+ *
793
+ * init({
794
+ * service: 'my-app',
795
+ * spanExporter: new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' })
796
+ * })
797
+ * ```
798
+ *
799
+ * @example Observe in Datadog
800
+ * ```typescript
801
+ * import { DatadogSpanProcessor } from '@opentelemetry/exporter-datadog'
802
+ *
803
+ * init({
804
+ * service: 'my-app',
805
+ * spanProcessor: new DatadogSpanProcessor({ ... })
806
+ * })
807
+ * ```
808
+ *
809
+ * @example Console output (dev)
810
+ * ```typescript
811
+ * import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
812
+ *
813
+ * init({
814
+ * service: 'my-app',
815
+ * spanProcessor: new SimpleSpanProcessor(new ConsoleSpanExporter())
816
+ * })
817
+ * ```
818
+ */
819
+
820
+ /**
821
+ * Auto-detect logger type and return appropriate instrumentation
822
+ * Detects Pino and Winston loggers based on their unique properties
823
+ */
824
+ function createLoggerInstrumentation(
825
+ logger: Logger,
826
+ ): PinoInstrumentation | WinstonInstrumentation | null {
827
+ // Type guard: check for Pino-specific properties
828
+ // Pino has 'child' and 'bindings' methods
829
+ if (
830
+ 'child' in logger &&
831
+ 'bindings' in logger &&
832
+ typeof logger.child === 'function'
833
+ ) {
834
+ return new PinoInstrumentation();
835
+ }
836
+
837
+ // Type guard: check for Winston-specific properties
838
+ // Winston has 'transports' array or 'defaultMeta' property
839
+ if ('transports' in logger || 'defaultMeta' in logger) {
840
+ return new WinstonInstrumentation();
841
+ }
842
+
843
+ // Unknown logger type - no instrumentation
844
+ return null;
845
+ }
846
+
847
+ export function init(cfg: AutotelConfig): void {
848
+ // Resolve environment variables (standard OTEL env vars)
849
+ const envConfig = resolveConfigFromEnv();
850
+
851
+ // Merge configs: explicit config > env vars > defaults
852
+ // Note: We merge envConfig first, then cfg overrides it
853
+ const mergedConfig: AutotelConfig = {
854
+ ...envConfig,
855
+ ...cfg,
856
+ // Deep merge for resourceAttributes
857
+ resourceAttributes: {
858
+ ...envConfig.resourceAttributes,
859
+ ...cfg.resourceAttributes,
860
+ },
861
+ // Handle otlpHeaders merge (can be string or object)
862
+ otlpHeaders:
863
+ cfg.otlpHeaders === undefined
864
+ ? envConfig.otlpHeaders === undefined
865
+ ? undefined
866
+ : envConfig.otlpHeaders
867
+ : cfg.otlpHeaders,
868
+ } as AutotelConfig;
869
+
870
+ // Set logger (use provided or default to silent)
871
+ logger = mergedConfig.logger || silentLogger;
872
+
873
+ // Warn if re-initializing (same behavior in all environments)
874
+ if (initialized) {
875
+ logger.warn(
876
+ '[autotel] init() called again - last config wins. This may cause unexpected behavior.',
877
+ );
878
+ }
879
+
880
+ config = mergedConfig;
881
+ validationConfig = mergedConfig.validation || null;
882
+
883
+ // Initialize OpenTelemetry
884
+ // Only use endpoint if explicitly configured (no default fallback)
885
+ const endpoint = mergedConfig.endpoint;
886
+ const otlpHeaders = normalizeOtlpHeaders(mergedConfig.otlpHeaders);
887
+ const version = mergedConfig.version || detectVersion();
888
+ const environment =
889
+ mergedConfig.environment || process.env.NODE_ENV || 'development';
890
+ const metricsEnabled = resolveMetricsFlag(mergedConfig.metrics);
891
+
892
+ // Detect hostname for proper Datadog correlation and Service Catalog discovery
893
+ const hostname = detectHostname();
894
+
895
+ let resource = resourceFromAttributes({
896
+ [ATTR_SERVICE_NAME]: cfg.service,
897
+ [ATTR_SERVICE_VERSION]: version,
898
+ // Support both old and new OpenTelemetry semantic conventions for environment
899
+ 'deployment.environment': environment, // Deprecated but widely supported
900
+ 'deployment.environment.name': environment, // OTel v1.27.0+ standard
901
+ });
902
+
903
+ // Add hostname attributes for Datadog Service Catalog and infrastructure correlation
904
+ if (hostname) {
905
+ resource = resource.merge(
906
+ resourceFromAttributes({
907
+ 'host.name': hostname, // OpenTelemetry standard
908
+ 'datadog.host.name': hostname, // Datadog-specific, highest priority for Datadog
909
+ }),
910
+ );
911
+ }
912
+
913
+ if (cfg.resource) {
914
+ resource = resource.merge(cfg.resource);
915
+ }
916
+
917
+ if (cfg.resourceAttributes) {
918
+ resource = resource.merge(resourceFromAttributes(cfg.resourceAttributes));
919
+ }
920
+
921
+ // Resolve OTLP protocol (http or grpc)
922
+ const protocol = resolveProtocol(cfg.protocol);
923
+
924
+ // Build array of span processors (supports multiple)
925
+ const spanProcessors: SpanProcessor[] = [];
926
+
927
+ if (cfg.spanProcessors && cfg.spanProcessors.length > 0) {
928
+ // User provided custom processors (full control)
929
+ spanProcessors.push(...cfg.spanProcessors);
930
+ } else if (cfg.spanExporters && cfg.spanExporters.length > 0) {
931
+ // User provided custom exporters (wrap each with tail sampling)
932
+ for (const exporter of cfg.spanExporters) {
933
+ spanProcessors.push(
934
+ new TailSamplingSpanProcessor(new BatchSpanProcessor(exporter)),
935
+ );
936
+ }
937
+ } else if (endpoint) {
938
+ // Default: OTLP with tail sampling (only if endpoint is configured)
939
+ const traceExporter = createTraceExporter(protocol, {
940
+ url: formatEndpointUrl(endpoint, 'traces', protocol),
941
+ headers: otlpHeaders,
942
+ });
943
+
944
+ spanProcessors.push(
945
+ new TailSamplingSpanProcessor(new BatchSpanProcessor(traceExporter)),
946
+ );
947
+ }
948
+ // If no endpoint and no custom processors/exporters, array remains empty
949
+ // SDK will still work but won't export traces
950
+
951
+ // Add baggage span processor if enabled
952
+ if (cfg.baggage) {
953
+ const prefix =
954
+ typeof cfg.baggage === 'string'
955
+ ? cfg.baggage
956
+ ? `${cfg.baggage}.`
957
+ : ''
958
+ : 'baggage.';
959
+ spanProcessors.push(new BaggageSpanProcessor({ prefix }));
960
+ }
961
+
962
+ // Apply debug mode configuration
963
+ const debugMode = resolveDebugFlag(cfg.debug);
964
+
965
+ if (debugMode) {
966
+ // Debug enabled: add console processor
967
+ spanProcessors.push(new SimpleSpanProcessor(new ConsoleSpanExporter()));
968
+ }
969
+
970
+ // Build array of metric readers (supports multiple)
971
+ const metricReaders: MetricReader[] = [];
972
+
973
+ if (cfg.metricReaders && cfg.metricReaders.length > 0) {
974
+ // User provided custom metric readers
975
+ metricReaders.push(...cfg.metricReaders);
976
+ } else if (metricsEnabled && endpoint) {
977
+ // Default: OTLP metrics exporter (only if endpoint is configured)
978
+ const metricExporter = createMetricExporter(protocol, {
979
+ url: formatEndpointUrl(endpoint, 'metrics', protocol),
980
+ headers: otlpHeaders,
981
+ });
982
+
983
+ metricReaders.push(
984
+ new PeriodicExportingMetricReader({
985
+ exporter: metricExporter,
986
+ }),
987
+ );
988
+ }
989
+
990
+ let logRecordProcessors: LogRecordProcessor[] | undefined;
991
+ if (cfg.logRecordProcessors && cfg.logRecordProcessors.length > 0) {
992
+ logRecordProcessors = [...cfg.logRecordProcessors];
993
+ }
994
+
995
+ // Handle instrumentations: merge manual instrumentations with auto-integrations
996
+ let finalInstrumentations: NodeSDKConfiguration['instrumentations'] =
997
+ cfg.instrumentations ? [...cfg.instrumentations] : [];
998
+
999
+ // Auto-enable logger instrumentation if a logger is provided
1000
+ if (cfg.logger) {
1001
+ const loggerInstrumentation = createLoggerInstrumentation(cfg.logger);
1002
+ if (loggerInstrumentation) {
1003
+ finalInstrumentations = [...finalInstrumentations, loggerInstrumentation];
1004
+ logger.debug(
1005
+ `[autotel] Auto-enabled ${loggerInstrumentation.constructor.name} for logger`,
1006
+ );
1007
+ }
1008
+ }
1009
+
1010
+ if (cfg.integrations !== undefined) {
1011
+ try {
1012
+ // Detect manual instrumentations to avoid conflicts
1013
+ const manualInstrumentationNames = getInstrumentationNames(
1014
+ cfg.instrumentations ?? [],
1015
+ );
1016
+
1017
+ // Warn if both integrations and manual instrumentations are provided
1018
+ if (
1019
+ manualInstrumentationNames.size > 0 &&
1020
+ cfg.integrations !== false &&
1021
+ cfg.integrations !== undefined
1022
+ ) {
1023
+ const manualNames = [...manualInstrumentationNames].join(', ');
1024
+ logger.info(
1025
+ `[autotel] Detected manual instrumentations (${manualNames}). ` +
1026
+ 'These will take precedence over auto-instrumentations. ' +
1027
+ 'Tip: Set integrations:false if you want full manual control, or remove manual configs to use auto-instrumentations.',
1028
+ );
1029
+ }
1030
+
1031
+ const autoInstrumentations = getAutoInstrumentations(
1032
+ cfg.integrations,
1033
+ manualInstrumentationNames,
1034
+ );
1035
+ if (autoInstrumentations && autoInstrumentations.length > 0) {
1036
+ // Cast to proper type - getNodeAutoInstrumentations returns the correct type
1037
+ finalInstrumentations = [
1038
+ ...finalInstrumentations,
1039
+ ...(autoInstrumentations as NodeSDKConfiguration['instrumentations']),
1040
+ ];
1041
+ }
1042
+ } catch (error) {
1043
+ logger.warn(
1044
+ `[autotel] Failed to configure auto-instrumentations: ${error instanceof Error ? error.message : String(error)}`,
1045
+ );
1046
+ }
1047
+ }
1048
+
1049
+ const sdkOptions: Partial<NodeSDKConfiguration> = {
1050
+ resource,
1051
+ instrumentations: finalInstrumentations,
1052
+ };
1053
+
1054
+ if (spanProcessors.length > 0) {
1055
+ sdkOptions.spanProcessors = spanProcessors;
1056
+ }
1057
+
1058
+ if (metricReaders.length > 0) {
1059
+ sdkOptions.metricReaders = metricReaders;
1060
+ }
1061
+
1062
+ if (logRecordProcessors && logRecordProcessors.length > 0) {
1063
+ sdkOptions.logRecordProcessors = logRecordProcessors;
1064
+ }
1065
+
1066
+ sdk = cfg.sdkFactory ? cfg.sdkFactory(sdkOptions) : new NodeSDK(sdkOptions);
1067
+
1068
+ if (!sdk) {
1069
+ throw new Error('[autotel] sdkFactory must return a NodeSDK instance');
1070
+ }
1071
+
1072
+ sdk.start();
1073
+
1074
+ // Initialize OpenLLMetry if enabled (after SDK starts to reuse tracer provider)
1075
+ if (cfg.openllmetry?.enabled) {
1076
+ // Try synchronous initialization first (for require-based modules)
1077
+ let initializedSync = false;
1078
+ try {
1079
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1080
+ const traceloop = require('@traceloop/node-server-sdk');
1081
+ const initOptions: Record<string, unknown> = {
1082
+ ...cfg.openllmetry.options,
1083
+ };
1084
+
1085
+ // Reuse autotel's tracer provider
1086
+ try {
1087
+ // Type assertion needed as getTracerProvider is not in the public NodeSDK interface
1088
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1089
+ const tracerProvider = (sdk as any).getTracerProvider();
1090
+ initOptions.tracerProvider = tracerProvider;
1091
+ } catch {
1092
+ // Ignore if tracer provider not available
1093
+ }
1094
+
1095
+ if (typeof traceloop.initialize === 'function') {
1096
+ traceloop.initialize(initOptions);
1097
+ logger.info('[autotel] OpenLLMetry initialized successfully');
1098
+ initializedSync = true;
1099
+ }
1100
+ } catch (error) {
1101
+ // If require fails, try async import (for ESM modules or when module not found)
1102
+ if (
1103
+ error instanceof Error &&
1104
+ (error.message.includes('Cannot find module') ||
1105
+ error.message.includes('Module not found') ||
1106
+ error.message.includes('Cannot resolve module') ||
1107
+ error.message.includes('Dynamic require'))
1108
+ ) {
1109
+ // Try async import as fallback - this will work with ESM/tsx and mocks in tests
1110
+ initializeOpenLLMetry(
1111
+ cfg.openllmetry.options,
1112
+ sdk,
1113
+ cfg.spanExporters?.[0], // Pass first exporter if available
1114
+ ).catch((error_) => {
1115
+ logger.warn(
1116
+ `[autotel] OpenLLMetry initialization error: ${error_ instanceof Error ? error_.message : String(error_)}`,
1117
+ );
1118
+ });
1119
+ } else if (!initializedSync) {
1120
+ logger.warn(
1121
+ `[autotel] Failed to initialize OpenLLMetry: ${error instanceof Error ? error.message : String(error)}`,
1122
+ );
1123
+ }
1124
+ }
1125
+ }
1126
+
1127
+ initialized = true;
1128
+ }
1129
+
1130
+ /**
1131
+ * Initialize OpenLLMetry integration
1132
+ * Dynamically imports @traceloop/node-server-sdk and initializes it
1133
+ * Returns a promise but can be called without awaiting (fire-and-forget)
1134
+ */
1135
+ async function initializeOpenLLMetry(
1136
+ options?: Record<string, unknown>,
1137
+ sdkInstance?: NodeSDK,
1138
+ spanExporter?: SpanExporter,
1139
+ ): Promise<void> {
1140
+ try {
1141
+ // Try synchronous require first (for testing/mocking), then fall back to dynamic import
1142
+ let traceloop: {
1143
+ initialize?: (options?: Record<string, unknown>) => void;
1144
+ instrumentations?: unknown[];
1145
+ };
1146
+
1147
+ try {
1148
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1149
+ traceloop = require('@traceloop/node-server-sdk');
1150
+ } catch {
1151
+ // Fall back to dynamic import if require fails (ESM modules)
1152
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1153
+ // @ts-ignore - optional peer dependency
1154
+ traceloop = await import('@traceloop/node-server-sdk');
1155
+ }
1156
+
1157
+ // Prepare initialization options
1158
+ const initOptions: Record<string, unknown> = {
1159
+ ...options,
1160
+ };
1161
+
1162
+ // Pass span exporter to OpenLLMetry if provided
1163
+ // This ensures OpenLLMetry uses the same exporter as autotel
1164
+ if (spanExporter) {
1165
+ initOptions.exporter = spanExporter;
1166
+ }
1167
+
1168
+ // Reuse autotel's tracer provider if SDK is available
1169
+ if (sdkInstance) {
1170
+ try {
1171
+ // Type assertion needed as getTracerProvider is not in the public NodeSDK interface
1172
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1173
+ const tracerProvider = (sdkInstance as any).getTracerProvider();
1174
+ initOptions.tracerProvider = tracerProvider;
1175
+ } catch (error) {
1176
+ logger.debug(
1177
+ `[autotel] Could not get tracer provider for OpenLLMetry: ${error instanceof Error ? error.message : String(error)}`,
1178
+ );
1179
+ }
1180
+ }
1181
+
1182
+ // Initialize OpenLLMetry
1183
+ if (typeof traceloop.initialize === 'function') {
1184
+ traceloop.initialize(initOptions);
1185
+ logger.info('[autotel] OpenLLMetry initialized successfully');
1186
+ } else {
1187
+ logger.warn(
1188
+ '[autotel] OpenLLMetry initialize function not found. Check @traceloop/node-server-sdk version.',
1189
+ );
1190
+ }
1191
+ } catch (error) {
1192
+ // Gracefully handle missing dependency
1193
+ if (
1194
+ error instanceof Error &&
1195
+ (error.message.includes('Cannot find module') ||
1196
+ error.message.includes('Module not found') ||
1197
+ error.message.includes('Cannot resolve module'))
1198
+ ) {
1199
+ logger.warn(
1200
+ '[autotel] OpenLLMetry enabled but @traceloop/node-server-sdk is not installed. ' +
1201
+ 'Install it as a peer dependency to use OpenLLMetry integration.',
1202
+ );
1203
+ } else {
1204
+ logger.warn(
1205
+ `[autotel] Failed to initialize OpenLLMetry: ${error instanceof Error ? error.message : String(error)}`,
1206
+ );
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ /**
1212
+ * Extract instrumentation class names from instrumentation instances
1213
+ * Used to detect duplicates between manual and auto instrumentations
1214
+ */
1215
+ function getInstrumentationNames(
1216
+ instrumentations: NodeSDKConfiguration['instrumentations'],
1217
+ ): Set<string> {
1218
+ const names = new Set<string>();
1219
+
1220
+ if (!instrumentations) return names;
1221
+
1222
+ for (const instrumentation of instrumentations) {
1223
+ if (instrumentation && typeof instrumentation === 'object') {
1224
+ names.add(instrumentation.constructor.name);
1225
+ }
1226
+ }
1227
+
1228
+ return names;
1229
+ }
1230
+
1231
+ /**
1232
+ * Map common instrumentation class names to their package names
1233
+ * Used to disable auto-instrumentations when user provides manual configs
1234
+ */
1235
+ const INSTRUMENTATION_CLASS_TO_PACKAGE: Record<string, string> = {
1236
+ HttpInstrumentation: '@opentelemetry/instrumentation-http',
1237
+ HttpsInstrumentation: '@opentelemetry/instrumentation-http',
1238
+ ExpressInstrumentation: '@opentelemetry/instrumentation-express',
1239
+ FastifyInstrumentation: '@opentelemetry/instrumentation-fastify',
1240
+ MongoDBInstrumentation: '@opentelemetry/instrumentation-mongodb',
1241
+ MongooseInstrumentation: '@opentelemetry/instrumentation-mongoose',
1242
+ PrismaInstrumentation: '@opentelemetry/instrumentation-prisma',
1243
+ PinoInstrumentation: '@opentelemetry/instrumentation-pino',
1244
+ WinstonInstrumentation: '@opentelemetry/instrumentation-winston',
1245
+ RedisInstrumentation: '@opentelemetry/instrumentation-redis',
1246
+ GraphQLInstrumentation: '@opentelemetry/instrumentation-graphql',
1247
+ GrpcInstrumentation: '@opentelemetry/instrumentation-grpc',
1248
+ IORedisInstrumentation: '@opentelemetry/instrumentation-ioredis',
1249
+ KnexInstrumentation: '@opentelemetry/instrumentation-knex',
1250
+ NestJsInstrumentation: '@opentelemetry/instrumentation-nestjs-core',
1251
+ PgInstrumentation: '@opentelemetry/instrumentation-pg',
1252
+ MySQLInstrumentation: '@opentelemetry/instrumentation-mysql',
1253
+ MySQL2Instrumentation: '@opentelemetry/instrumentation-mysql2',
1254
+ };
1255
+
1256
+ /**
1257
+ * Get auto-instrumentations based on simple integration names
1258
+ * Excludes instrumentations that are manually provided to avoid conflicts
1259
+ */
1260
+ function getAutoInstrumentations(
1261
+ integrations: string[] | boolean | Record<string, { enabled?: boolean }>,
1262
+ manualInstrumentationNames: Set<string> = new Set(),
1263
+ ): unknown[] {
1264
+ if (integrations === false) {
1265
+ return [];
1266
+ }
1267
+
1268
+ // Build exclusion config for manual instrumentations
1269
+ const exclusionConfig: Record<string, { enabled: boolean }> = {};
1270
+ for (const className of manualInstrumentationNames) {
1271
+ const packageName = INSTRUMENTATION_CLASS_TO_PACKAGE[className];
1272
+ if (packageName) {
1273
+ exclusionConfig[packageName] = { enabled: false };
1274
+ }
1275
+ }
1276
+
1277
+ if (integrations === true) {
1278
+ // If exclusions exist, pass them to getNodeAutoInstrumentations
1279
+ if (Object.keys(exclusionConfig).length > 0) {
1280
+ return getNodeAutoInstrumentations(exclusionConfig);
1281
+ }
1282
+ return getNodeAutoInstrumentations();
1283
+ }
1284
+
1285
+ if (Array.isArray(integrations)) {
1286
+ const config: Record<string, { enabled: boolean }> = { ...exclusionConfig };
1287
+ for (const name of integrations) {
1288
+ const packageName = `@opentelemetry/instrumentation-${name}`;
1289
+ // Don't override exclusions
1290
+ if (!exclusionConfig[packageName]) {
1291
+ config[packageName] = { enabled: true };
1292
+ }
1293
+ }
1294
+ return getNodeAutoInstrumentations(config);
1295
+ }
1296
+
1297
+ const config: Record<string, { enabled?: boolean }> = {
1298
+ ...exclusionConfig,
1299
+ ...integrations,
1300
+ };
1301
+
1302
+ // Override any integrations that conflict with manual instrumentations
1303
+ for (const packageName of Object.keys(exclusionConfig)) {
1304
+ const integrationsKey = Object.keys(integrations).find((key) =>
1305
+ packageName.includes(key),
1306
+ );
1307
+ if (integrationsKey) {
1308
+ // Manual instrumentation takes precedence
1309
+ config[packageName] = { enabled: false };
1310
+ }
1311
+ }
1312
+
1313
+ return getNodeAutoInstrumentations(config);
1314
+ }
1315
+
1316
+ /**
1317
+ * Check if autotel has been initialized
1318
+ */
1319
+ export function isInitialized(): boolean {
1320
+ return initialized;
1321
+ }
1322
+
1323
+ /**
1324
+ * Get current config (internal use)
1325
+ */
1326
+ export function getConfig(): AutotelConfig | null {
1327
+ return config;
1328
+ }
1329
+
1330
+ /**
1331
+ * Get current logger (internal use)
1332
+ */
1333
+ export function getLogger(): Logger {
1334
+ return logger;
1335
+ }
1336
+
1337
+ /**
1338
+ * Get validation config (internal use)
1339
+ */
1340
+ export function getValidationConfig(): Partial<ValidationConfig> | null {
1341
+ return validationConfig;
1342
+ }
1343
+
1344
+ /**
1345
+ * Warn once if not initialized (same behavior in all environments)
1346
+ */
1347
+ export function warnIfNotInitialized(context: string): void {
1348
+ if (!initialized && !warnedOnce) {
1349
+ logger.warn(
1350
+ `[autotel] ${context} used before init() called. ` +
1351
+ 'Call init({ service: "..." }) first. See: https://docs.autotel.dev/quickstart',
1352
+ );
1353
+ warnedOnce = true;
1354
+ }
1355
+ }
1356
+
1357
+ /**
1358
+ * Get default sampler
1359
+ */
1360
+ export function getDefaultSampler(): Sampler {
1361
+ return (
1362
+ config?.sampler ||
1363
+ new AdaptiveSampler({
1364
+ baselineSampleRate: 0.1,
1365
+ alwaysSampleErrors: true,
1366
+ alwaysSampleSlow: true,
1367
+ })
1368
+ );
1369
+ }
1370
+
1371
+ /**
1372
+ * Auto-detect version from package.json
1373
+ */
1374
+ function detectVersion(): string {
1375
+ try {
1376
+ // Try to read package.json from cwd using fs
1377
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1378
+ const fs = require('node:fs');
1379
+ const pkg = JSON.parse(
1380
+ fs.readFileSync(`${process.cwd()}/package.json`, 'utf8'),
1381
+ );
1382
+ return pkg.version || '1.0.0';
1383
+ } catch {
1384
+ return '1.0.0';
1385
+ }
1386
+ }
1387
+
1388
+ /**
1389
+ * Detect hostname for resource attributes.
1390
+ * Supports Datadog conventions (DD_HOSTNAME) and falls back to system hostname.
1391
+ *
1392
+ * Priority order:
1393
+ * 1. DD_HOSTNAME environment variable (Datadog convention)
1394
+ * 2. HOSTNAME environment variable (common Unix convention)
1395
+ * 3. os.hostname() (system hostname)
1396
+ *
1397
+ * @returns hostname string or undefined if detection fails
1398
+ */
1399
+ function detectHostname(): string | undefined {
1400
+ // Priority 1: DD_HOSTNAME (Datadog convention)
1401
+ if (process.env.DD_HOSTNAME) {
1402
+ return process.env.DD_HOSTNAME;
1403
+ }
1404
+
1405
+ // Priority 2: HOSTNAME (common in containers and Unix systems)
1406
+ if (process.env.HOSTNAME) {
1407
+ return process.env.HOSTNAME;
1408
+ }
1409
+
1410
+ // Priority 3: System hostname
1411
+ try {
1412
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1413
+ const os = require('node:os') as typeof import('node:os');
1414
+ return os.hostname();
1415
+ } catch {
1416
+ // os module not available (edge runtime, browser, etc.)
1417
+ return undefined;
1418
+ }
1419
+ }
1420
+
1421
+ /**
1422
+ * Get SDK instance (for shutdown)
1423
+ */
1424
+ export function getSdk(): NodeSDK | null {
1425
+ return sdk;
1426
+ }