autotel 4.1.0 → 4.2.1

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 (253) hide show
  1. package/dist/auto.cjs +5 -3
  2. package/dist/auto.cjs.map +1 -1
  3. package/dist/auto.js +3 -3
  4. package/dist/auto.js.map +1 -1
  5. package/dist/chunk-C_NdSu1c.cjs +34 -0
  6. package/dist/correlation-id.cjs +1 -1
  7. package/dist/correlation-id.d.cts.map +1 -1
  8. package/dist/correlation-id.d.ts.map +1 -1
  9. package/dist/correlation-id.js +1 -1
  10. package/dist/decorators.cjs +1 -1
  11. package/dist/decorators.js +1 -1
  12. package/dist/{event-ByBTV9M2.js → event-531asIM6.js} +4 -4
  13. package/dist/{event-ByBTV9M2.js.map → event-531asIM6.js.map} +1 -1
  14. package/dist/{event-BhHREDJk.cjs → event-CcZYwp50.cjs} +4 -4
  15. package/dist/{event-BhHREDJk.cjs.map → event-CcZYwp50.cjs.map} +1 -1
  16. package/dist/event.cjs +1 -1
  17. package/dist/event.js +1 -1
  18. package/dist/{functional-zpzNLhky.cjs → functional-C8B0Qa7o.cjs} +10 -7
  19. package/dist/functional-C8B0Qa7o.cjs.map +1 -0
  20. package/dist/{functional-DtI0u4vx.js → functional-r-AUIRy_.js} +9 -9
  21. package/dist/functional-r-AUIRy_.js.map +1 -0
  22. package/dist/functional.cjs +1 -1
  23. package/dist/functional.js +1 -1
  24. package/dist/http.cjs +1 -1
  25. package/dist/http.js +1 -1
  26. package/dist/index.cjs +15 -13
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +14 -14
  31. package/dist/index.js.map +1 -1
  32. package/dist/{init-D-jnNMix.js → init-BS2JVkrL.js} +2 -2
  33. package/dist/{init-D-jnNMix.js.map → init-BS2JVkrL.js.map} +1 -1
  34. package/dist/{init-BX7AmFRl.cjs → init-BXiuPK6j.cjs} +3 -3
  35. package/dist/{init-BX7AmFRl.cjs.map → init-BXiuPK6j.cjs.map} +1 -1
  36. package/dist/instrumentation.cjs +2 -2
  37. package/dist/instrumentation.js +2 -2
  38. package/dist/logger.cjs +236 -8
  39. package/dist/logger.cjs.map +1 -0
  40. package/dist/messaging.cjs +1 -1
  41. package/dist/messaging.js +1 -1
  42. package/dist/{node-require-DF5QBX6z.cjs → node-require-CZ_PU448.cjs} +6 -4
  43. package/dist/node-require-CZ_PU448.cjs.map +1 -0
  44. package/dist/{node-require-Db1oDpLj.js → node-require-vROmTeJ8.js} +5 -5
  45. package/dist/node-require-vROmTeJ8.js.map +1 -0
  46. package/dist/{operation-context-C-2hmmtP.js → operation-context-CKBoA4Qy.js} +3 -3
  47. package/dist/operation-context-CKBoA4Qy.js.map +1 -0
  48. package/dist/{operation-context-n4_obUwq.cjs → operation-context-D6LDf4W_.cjs} +3 -1
  49. package/dist/operation-context-D6LDf4W_.cjs.map +1 -0
  50. package/dist/register.cjs +3 -1
  51. package/dist/register.cjs.map +1 -1
  52. package/dist/register.js +2 -2
  53. package/dist/register.js.map +1 -1
  54. package/dist/semantic-helpers.cjs +1 -1
  55. package/dist/semantic-helpers.js +1 -1
  56. package/dist/{stable-hash-Cg5cT34Q.js → stable-hash-ChFBIhNt.js} +3 -3
  57. package/dist/stable-hash-ChFBIhNt.js.map +1 -0
  58. package/dist/{stable-hash-BNTMrmdB.cjs → stable-hash-brKISGf1.cjs} +4 -2
  59. package/dist/stable-hash-brKISGf1.cjs.map +1 -0
  60. package/dist/trace-context-Cijqoi6e.d.cts.map +1 -1
  61. package/dist/trace-context-Cijqoi6e.d.ts.map +1 -1
  62. package/dist/trace-helpers.cjs +1 -1
  63. package/dist/trace-helpers.js +1 -1
  64. package/dist/{track-wc0HafS_.js → track-COUuU48p.js} +5 -5
  65. package/dist/track-COUuU48p.js.map +1 -0
  66. package/dist/{track-D59FfpL0.cjs → track-Cb3Q4QmS.cjs} +4 -2
  67. package/dist/track-Cb3Q4QmS.cjs.map +1 -0
  68. package/dist/validate.cjs +1 -1
  69. package/dist/validate.js +1 -1
  70. package/dist/webhook.cjs +1 -1
  71. package/dist/webhook.js +1 -1
  72. package/dist/workflow-distributed.cjs +1 -1
  73. package/dist/workflow-distributed.js +1 -1
  74. package/dist/workflow.cjs +3 -1
  75. package/dist/workflow.cjs.map +1 -1
  76. package/dist/workflow.d.cts.map +1 -1
  77. package/dist/workflow.d.ts.map +1 -1
  78. package/dist/workflow.js +3 -3
  79. package/dist/workflow.js.map +1 -1
  80. package/dist/yaml-config.cjs +233 -4
  81. package/dist/yaml-config.cjs.map +1 -0
  82. package/dist/yaml-config.d.cts.map +1 -1
  83. package/dist/yaml-config.d.ts.map +1 -1
  84. package/dist/yaml-config.js +8 -7
  85. package/dist/yaml-config.js.map +1 -1
  86. package/package.json +1 -2
  87. package/dist/functional-DtI0u4vx.js.map +0 -1
  88. package/dist/functional-zpzNLhky.cjs.map +0 -1
  89. package/dist/logger-thMPLpOG.cjs +0 -487
  90. package/dist/logger-thMPLpOG.cjs.map +0 -1
  91. package/dist/node-require-DF5QBX6z.cjs.map +0 -1
  92. package/dist/node-require-Db1oDpLj.js.map +0 -1
  93. package/dist/operation-context-C-2hmmtP.js.map +0 -1
  94. package/dist/operation-context-n4_obUwq.cjs.map +0 -1
  95. package/dist/stable-hash-BNTMrmdB.cjs.map +0 -1
  96. package/dist/stable-hash-Cg5cT34Q.js.map +0 -1
  97. package/dist/track-D59FfpL0.cjs.map +0 -1
  98. package/dist/track-wc0HafS_.js.map +0 -1
  99. package/dist/yaml-config-Ck2uB0Dp.cjs +0 -273
  100. package/dist/yaml-config-Ck2uB0Dp.cjs.map +0 -1
  101. package/src/attribute-redacting-processor.test.ts +0 -763
  102. package/src/attribute-redacting-processor.ts +0 -621
  103. package/src/attributes/attachers.ts +0 -161
  104. package/src/attributes/builders.ts +0 -529
  105. package/src/attributes/domains.ts +0 -42
  106. package/src/attributes/index.ts +0 -81
  107. package/src/attributes/registry.ts +0 -323
  108. package/src/attributes/types.ts +0 -211
  109. package/src/attributes/utils.ts +0 -64
  110. package/src/attributes/validators.ts +0 -266
  111. package/src/attributes.test.ts +0 -292
  112. package/src/auto.ts +0 -67
  113. package/src/autotel-logger.test.ts +0 -548
  114. package/src/autotel-logger.ts +0 -364
  115. package/src/baggage-span-processor.test.ts +0 -202
  116. package/src/baggage-span-processor.ts +0 -100
  117. package/src/business-baggage.test.ts +0 -500
  118. package/src/business-baggage.ts +0 -669
  119. package/src/circuit-breaker.test.ts +0 -341
  120. package/src/circuit-breaker.ts +0 -184
  121. package/src/config.test.ts +0 -94
  122. package/src/config.ts +0 -172
  123. package/src/correlated-events.test.ts +0 -151
  124. package/src/correlated-events.ts +0 -47
  125. package/src/correlation-id.test.ts +0 -163
  126. package/src/correlation-id.ts +0 -206
  127. package/src/db.test.ts +0 -252
  128. package/src/db.ts +0 -447
  129. package/src/decorators.test.ts +0 -153
  130. package/src/decorators.ts +0 -188
  131. package/src/define-event.test.ts +0 -41
  132. package/src/define-event.ts +0 -58
  133. package/src/devtools.ts +0 -60
  134. package/src/drain-pipeline.test.ts +0 -68
  135. package/src/drain-pipeline.ts +0 -199
  136. package/src/drain-toolkit.test.ts +0 -113
  137. package/src/drain-toolkit.ts +0 -129
  138. package/src/enricher-toolkit.test.ts +0 -67
  139. package/src/enricher-toolkit.ts +0 -79
  140. package/src/enrichers.test.ts +0 -150
  141. package/src/enrichers.ts +0 -145
  142. package/src/env-config.test.ts +0 -323
  143. package/src/env-config.ts +0 -309
  144. package/src/error-catalog.test.ts +0 -133
  145. package/src/error-catalog.ts +0 -262
  146. package/src/event-queue.test.ts +0 -864
  147. package/src/event-queue.ts +0 -699
  148. package/src/event-subscriber.ts +0 -262
  149. package/src/event-testing.ts +0 -197
  150. package/src/event.test.ts +0 -1104
  151. package/src/event.ts +0 -988
  152. package/src/events-config.ts +0 -235
  153. package/src/exporters.ts +0 -165
  154. package/src/filtering-span-processor.test.ts +0 -281
  155. package/src/filtering-span-processor.ts +0 -111
  156. package/src/flatten-attributes.test.ts +0 -76
  157. package/src/flatten-attributes.ts +0 -80
  158. package/src/functional.strict-types.typecheck.ts +0 -53
  159. package/src/functional.test.ts +0 -1464
  160. package/src/functional.ts +0 -2539
  161. package/src/functional.types.test.ts +0 -135
  162. package/src/hook.mjs +0 -15
  163. package/src/http.test.ts +0 -485
  164. package/src/http.ts +0 -424
  165. package/src/index.ts +0 -433
  166. package/src/init-auto-redactor.test.ts +0 -53
  167. package/src/init-redactor.test.ts +0 -8
  168. package/src/init.customization.test.ts +0 -665
  169. package/src/init.integrations.test.ts +0 -399
  170. package/src/init.openllmetry.test.ts +0 -194
  171. package/src/init.protocol.test.ts +0 -215
  172. package/src/init.ts +0 -2439
  173. package/src/instrumentation.test.ts +0 -108
  174. package/src/instrumentation.ts +0 -319
  175. package/src/logger.test.ts +0 -125
  176. package/src/logger.ts +0 -341
  177. package/src/messaging-adapters.test.ts +0 -595
  178. package/src/messaging-adapters.ts +0 -583
  179. package/src/messaging-testing.test.ts +0 -573
  180. package/src/messaging-testing.ts +0 -935
  181. package/src/messaging.test.ts +0 -1646
  182. package/src/messaging.ts +0 -2245
  183. package/src/metric-helpers.ts +0 -47
  184. package/src/metric-testing.ts +0 -197
  185. package/src/metric.ts +0 -446
  186. package/src/metrics.test.ts +0 -241
  187. package/src/node-require.ts +0 -123
  188. package/src/operation-context.ts +0 -93
  189. package/src/parse-error.test.ts +0 -73
  190. package/src/parse-error.ts +0 -112
  191. package/src/posthog-logs.test.ts +0 -115
  192. package/src/posthog-logs.ts +0 -77
  193. package/src/pretty-console-exporter.test.ts +0 -545
  194. package/src/pretty-console-exporter.ts +0 -413
  195. package/src/pretty-log-formatter.test.ts +0 -123
  196. package/src/pretty-log-formatter.ts +0 -210
  197. package/src/processors/canonical-log-line-processor.test.ts +0 -523
  198. package/src/processors/canonical-log-line-processor.ts +0 -396
  199. package/src/processors.ts +0 -152
  200. package/src/rate-limiter.test.ts +0 -199
  201. package/src/rate-limiter.ts +0 -98
  202. package/src/redact-values.test.ts +0 -90
  203. package/src/redact-values.ts +0 -34
  204. package/src/register.ts +0 -37
  205. package/src/request-logger.test.ts +0 -545
  206. package/src/request-logger.ts +0 -342
  207. package/src/sampling.test.ts +0 -1060
  208. package/src/sampling.ts +0 -737
  209. package/src/security-schema.test.ts +0 -45
  210. package/src/security-schema.ts +0 -107
  211. package/src/semantic-conventions.ts +0 -15
  212. package/src/semantic-helpers.test.ts +0 -226
  213. package/src/semantic-helpers.ts +0 -438
  214. package/src/shutdown.test.ts +0 -364
  215. package/src/shutdown.ts +0 -246
  216. package/src/span-name-normalizer.test.ts +0 -377
  217. package/src/span-name-normalizer.ts +0 -213
  218. package/src/stable-hash.ts +0 -27
  219. package/src/structured-error.test.ts +0 -191
  220. package/src/structured-error.ts +0 -157
  221. package/src/stub.integration.test.ts +0 -361
  222. package/src/tail-sampling-processor.test.ts +0 -230
  223. package/src/tail-sampling-processor.ts +0 -55
  224. package/src/test-span-collector.test.ts +0 -234
  225. package/src/test-span-collector.ts +0 -150
  226. package/src/testing.ts +0 -705
  227. package/src/trace-context.test.ts +0 -73
  228. package/src/trace-context.ts +0 -567
  229. package/src/trace-helpers.new.test.ts +0 -278
  230. package/src/trace-helpers.test.ts +0 -290
  231. package/src/trace-helpers.ts +0 -710
  232. package/src/trace-hybrid.test.ts +0 -42
  233. package/src/trace-hybrid.ts +0 -37
  234. package/src/tracer-provider.test.ts +0 -183
  235. package/src/tracer-provider.ts +0 -266
  236. package/src/track.test.ts +0 -154
  237. package/src/track.ts +0 -216
  238. package/src/validate.test.ts +0 -287
  239. package/src/validate.ts +0 -307
  240. package/src/validation-attributes.ts +0 -43
  241. package/src/validation.test.ts +0 -330
  242. package/src/validation.ts +0 -246
  243. package/src/variable-name-inference.test.ts +0 -178
  244. package/src/variable-name-inference.ts +0 -242
  245. package/src/webhook.test.ts +0 -649
  246. package/src/webhook.ts +0 -637
  247. package/src/workflow-distributed.test.ts +0 -786
  248. package/src/workflow-distributed.ts +0 -916
  249. package/src/workflow.async-safety.integration.test.ts +0 -345
  250. package/src/workflow.test.ts +0 -647
  251. package/src/workflow.ts +0 -810
  252. package/src/yaml-config.test.ts +0 -373
  253. package/src/yaml-config.ts +0 -351
package/src/init.ts DELETED
@@ -1,2439 +0,0 @@
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
- SamplingDecision,
16
- type SpanExporter,
17
- type Sampler as OtelSampler,
18
- type SamplingResult,
19
- } from '@opentelemetry/sdk-trace-base';
20
- import {
21
- resourceFromAttributes,
22
- type Resource,
23
- } from '@opentelemetry/resources';
24
- import {
25
- ATTR_SERVICE_NAME,
26
- ATTR_SERVICE_VERSION,
27
- } from '@opentelemetry/semantic-conventions';
28
- import type { Sampler, SamplingPreset } from './sampling';
29
- import { samplingPresets, resolveSamplingPreset } from './sampling';
30
- import type { EventSubscriber } from './event-subscriber';
31
- import type { Logger } from './logger';
32
- import type { Attributes, Context, SpanKind, Link } from '@opentelemetry/api';
33
- import type { ValidationConfig } from './validation';
34
- import {
35
- PeriodicExportingMetricReader,
36
- type MetricReader,
37
- } from '@opentelemetry/sdk-metrics';
38
- import { OTLPMetricExporter as OTLPMetricExporterHTTP } from '@opentelemetry/exporter-metrics-otlp-http';
39
- import { OTLPTraceExporter as OTLPTraceExporterHTTP } from '@opentelemetry/exporter-trace-otlp-http';
40
- import { OTLPLogExporter as OTLPLogExporterHTTP } from '@opentelemetry/exporter-logs-otlp-http';
41
- import type { PushMetricExporter } from '@opentelemetry/sdk-metrics';
42
- import {
43
- BatchLogRecordProcessor,
44
- type LogRecordExporter,
45
- type LogRecordProcessor,
46
- } from '@opentelemetry/sdk-logs';
47
- import {
48
- buildPostHogLogProcessors,
49
- RedactingLogRecordProcessor,
50
- } from './posthog-logs';
51
- import { TailSamplingSpanProcessor } from './tail-sampling-processor';
52
- import { BaggageSpanProcessor } from './baggage-span-processor';
53
- import {
54
- FilteringSpanProcessor,
55
- type SpanFilterPredicate,
56
- } from './filtering-span-processor';
57
- import {
58
- SpanNameNormalizingProcessor,
59
- type SpanNameNormalizerConfig,
60
- } from './span-name-normalizer';
61
- import {
62
- AttributeRedactingProcessor,
63
- normalizeAttributeRedactorConfig,
64
- type AttributeRedactorConfig,
65
- type AttributeRedactorPreset,
66
- } from './attribute-redacting-processor';
67
- import { createStringRedactor, type StringRedactor } from './redact-values';
68
- import { PrettyConsoleExporter } from './pretty-console-exporter';
69
- import { resolveConfigFromEnv } from './env-config';
70
- import { loadYamlConfig } from './yaml-config';
71
- import { requireModule, safeRequire } from './node-require';
72
- import {
73
- CanonicalLogLineProcessor,
74
- type CanonicalLogLineOptions,
75
- } from './processors/canonical-log-line-processor';
76
- import type { EventsConfig } from './events-config';
77
- import { resolveDevtoolsConfig, type AutotelDevtoolsConfig } from './devtools';
78
-
79
- /**
80
- * Silent logger (no-op) - used as default when user doesn't provide one.
81
- * Internal autotel logs are silent by default to avoid spam.
82
- * Users can import { autotelLogger } from 'autotel/logger' to create their own.
83
- */
84
- const silentLogger: Logger = {
85
- info: () => {},
86
- warn: () => {},
87
- error: () => {},
88
- debug: () => {},
89
- };
90
-
91
- /**
92
- * Adapts an Autotel Sampler to the OTel SDK Sampler interface.
93
- */
94
- function toOtelSampler(sampler: Sampler): OtelSampler {
95
- return {
96
- shouldSample(
97
- _context: Context,
98
- _traceId: string,
99
- spanName: string,
100
- _spanKind: SpanKind,
101
- _attributes: Attributes,
102
- links: Link[],
103
- ): SamplingResult {
104
- const shouldTrace = sampler.shouldSample({
105
- operationName: spanName,
106
- args: [],
107
- links,
108
- });
109
- return {
110
- decision: shouldTrace
111
- ? SamplingDecision.RECORD_AND_SAMPLED
112
- : SamplingDecision.NOT_RECORD,
113
- };
114
- },
115
- toString(): string {
116
- return `AutotelSamplerAdapter`;
117
- },
118
- };
119
- }
120
-
121
- // Type imports for exporters
122
- type OTLPExporterConfig = {
123
- url?: string;
124
- headers?: Record<string, string>;
125
- timeoutMillis?: number;
126
- concurrencyLimit?: number;
127
- };
128
-
129
- export type OtlpSignal = 'traces' | 'metrics' | 'logs';
130
-
131
- export interface OtlpDestinationConfig {
132
- /**
133
- * Base OTLP endpoint for this destination.
134
- * HTTP destinations may omit `/v1/{signal}`; autotel appends it automatically.
135
- * gRPC destinations should point at the collector host:port.
136
- */
137
- endpoint: string;
138
-
139
- /**
140
- * Headers for this destination. Falls back to top-level `headers`.
141
- */
142
- headers?: Record<string, string> | string;
143
-
144
- /**
145
- * Protocol for this destination. Falls back to top-level `protocol`.
146
- */
147
- protocol?: 'http' | 'grpc';
148
-
149
- /**
150
- * Signals to send to this destination.
151
- * Defaults to all signals supported by the current init() config.
152
- */
153
- signals?: OtlpSignal[];
154
- }
155
-
156
- // Lazy-load gRPC exporters (optional peer dependencies)
157
- let OTLPTraceExporterGRPC:
158
- | (new (config: OTLPExporterConfig) => SpanExporter)
159
- | undefined;
160
- let OTLPMetricExporterGRPC:
161
- | (new (config: OTLPExporterConfig) => PushMetricExporter)
162
- | undefined;
163
- let OTLPLogExporterGRPC:
164
- | (new (config: OTLPExporterConfig) => LogRecordExporter)
165
- | undefined;
166
-
167
- /**
168
- * Helper: Lazy-load gRPC trace exporter
169
- */
170
- function loadGRPCTraceExporter(): new (
171
- config: OTLPExporterConfig,
172
- ) => SpanExporter {
173
- if (OTLPTraceExporterGRPC) return OTLPTraceExporterGRPC;
174
-
175
- try {
176
- // Dynamic import for optional peer dependency
177
- const grpcModule = requireModule<{
178
- OTLPTraceExporter: new (config: OTLPExporterConfig) => SpanExporter;
179
- }>('@opentelemetry/exporter-trace-otlp-grpc');
180
- OTLPTraceExporterGRPC = grpcModule.OTLPTraceExporter;
181
- return OTLPTraceExporterGRPC;
182
- } catch {
183
- throw new Error(
184
- 'gRPC trace exporter not found. Install @opentelemetry/exporter-trace-otlp-grpc',
185
- );
186
- }
187
- }
188
-
189
- /**
190
- * Helper: Lazy-load gRPC metric exporter
191
- */
192
- function loadGRPCMetricExporter(): new (
193
- config: OTLPExporterConfig,
194
- ) => PushMetricExporter {
195
- if (OTLPMetricExporterGRPC) return OTLPMetricExporterGRPC;
196
-
197
- try {
198
- // Dynamic import for optional peer dependency
199
- const grpcModule = requireModule<{
200
- OTLPMetricExporter: new (
201
- config: OTLPExporterConfig,
202
- ) => PushMetricExporter;
203
- }>('@opentelemetry/exporter-metrics-otlp-grpc');
204
- OTLPMetricExporterGRPC = grpcModule.OTLPMetricExporter;
205
- return OTLPMetricExporterGRPC;
206
- } catch {
207
- throw new Error(
208
- 'gRPC metric exporter not found. Install @opentelemetry/exporter-metrics-otlp-grpc',
209
- );
210
- }
211
- }
212
-
213
- /**
214
- * Helper: Create trace exporter based on protocol
215
- */
216
- function createTraceExporter(
217
- protocol: 'http' | 'grpc',
218
- config: OTLPExporterConfig,
219
- ): SpanExporter {
220
- if (protocol === 'grpc') {
221
- const Exporter = loadGRPCTraceExporter();
222
- return new Exporter(config);
223
- }
224
-
225
- // Default: HTTP
226
- return new OTLPTraceExporterHTTP(config);
227
- }
228
-
229
- /**
230
- * Helper: Create metric exporter based on protocol
231
- */
232
- function createMetricExporter(
233
- protocol: 'http' | 'grpc',
234
- config: OTLPExporterConfig,
235
- ): PushMetricExporter {
236
- if (protocol === 'grpc') {
237
- const Exporter = loadGRPCMetricExporter();
238
- return new Exporter(config);
239
- }
240
-
241
- // Default: HTTP
242
- return new OTLPMetricExporterHTTP(config);
243
- }
244
-
245
- /**
246
- * Helper: Lazy-load gRPC log exporter
247
- */
248
- function loadGRPCLogExporter(): new (
249
- config: OTLPExporterConfig,
250
- ) => LogRecordExporter {
251
- if (OTLPLogExporterGRPC) return OTLPLogExporterGRPC;
252
-
253
- try {
254
- const grpcModule = requireModule<{
255
- OTLPLogExporter: new (config: OTLPExporterConfig) => LogRecordExporter;
256
- }>('@opentelemetry/exporter-logs-otlp-grpc');
257
- OTLPLogExporterGRPC = grpcModule.OTLPLogExporter;
258
- return OTLPLogExporterGRPC;
259
- } catch {
260
- throw new Error(
261
- 'gRPC log exporter not found. Install @opentelemetry/exporter-logs-otlp-grpc',
262
- );
263
- }
264
- }
265
-
266
- /**
267
- * Helper: Create log exporter based on protocol
268
- */
269
- function createLogExporter(
270
- protocol: 'http' | 'grpc',
271
- config: OTLPExporterConfig,
272
- ): LogRecordExporter {
273
- if (protocol === 'grpc') {
274
- const Exporter = loadGRPCLogExporter();
275
- return new Exporter(config);
276
- }
277
-
278
- // Default: HTTP
279
- return new OTLPLogExporterHTTP(config);
280
- }
281
-
282
- /**
283
- * Helper: Resolve protocol from config and environment
284
- */
285
- function resolveProtocol(configProtocol?: 'http' | 'grpc'): 'http' | 'grpc' {
286
- // 1. Check config parameter (highest priority)
287
- if (configProtocol === 'grpc' || configProtocol === 'http') {
288
- return configProtocol;
289
- }
290
-
291
- // 2. Check OTEL_EXPORTER_OTLP_PROTOCOL env var
292
- const envProtocol = process.env.OTEL_EXPORTER_OTLP_PROTOCOL;
293
- if (envProtocol === 'grpc') return 'grpc';
294
- if (envProtocol === 'http/protobuf' || envProtocol === 'http') return 'http';
295
-
296
- // 3. Default to HTTP
297
- return 'http';
298
- }
299
-
300
- /**
301
- * Helper: Adjust endpoint URL for protocol
302
- * gRPC exporters don't need the /v1/traces or /v1/metrics path
303
- * HTTP exporters need the full path
304
- */
305
- function formatEndpointUrl(
306
- endpoint: string,
307
- signal: 'traces' | 'metrics' | 'logs',
308
- protocol: 'http' | 'grpc',
309
- ): string {
310
- if (protocol === 'grpc') {
311
- // gRPC: strip any paths, return base endpoint
312
- return endpoint.replace(/\/(v1\/)?(traces|metrics|logs)$/, '');
313
- }
314
-
315
- // HTTP: append signal path if not present
316
- if (!endpoint.endsWith(`/v1/${signal}`)) {
317
- return `${endpoint}/v1/${signal}`;
318
- }
319
-
320
- return endpoint;
321
- }
322
-
323
- // Built-in logger is created dynamically in init() with service name
324
-
325
- export interface AutotelConfig {
326
- /** Service name (required) */
327
- service: string;
328
-
329
- /**
330
- * Local developer UX for autotel-devtools.
331
- *
332
- * - `true`: send traces, metrics, and logs to `http://127.0.0.1:4318`
333
- * - `{ embedded: true }`: attempt to start `autotel-devtools` automatically
334
- *
335
- * When enabled:
336
- * - `endpoint` defaults to the local devtools URL
337
- * - `logs` default to `true` unless explicitly set
338
- *
339
- * This keeps production config unchanged while making local debugging
340
- * effectively zero-config.
341
- */
342
- devtools?: boolean | AutotelDevtoolsConfig;
343
-
344
- /** Event subscribers - bring your own (PostHog, Mixpanel, etc.) */
345
- subscribers?: EventSubscriber[];
346
-
347
- /**
348
- * Additional OpenTelemetry instrumentations to register (raw OTel classes).
349
- * Useful when you need custom instrumentation configs or instrumentations
350
- * not covered by autoInstrumentations.
351
- *
352
- * **Important:** If you need custom instrumentation configs (like `requireParentSpan: false`),
353
- * use EITHER manual instrumentations OR autoInstrumentations, not both for the same library.
354
- * Manual instrumentations always take precedence over auto-instrumentations.
355
- *
356
- * @example Manual instrumentations with custom config
357
- * ```typescript
358
- * import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'
359
- *
360
- * init({
361
- * service: 'my-app',
362
- * autoInstrumentations: false, // Disable auto-instrumentations
363
- * instrumentations: [
364
- * new MongoDBInstrumentation({
365
- * requireParentSpan: false // Custom config
366
- * })
367
- * ]
368
- * })
369
- * ```
370
- *
371
- * @example Mix auto + manual (auto for most, manual for specific configs)
372
- * ```typescript
373
- * import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'
374
- *
375
- * init({
376
- * service: 'my-app',
377
- * autoInstrumentations: ['http', 'express'], // Auto for these
378
- * instrumentations: [
379
- * new MongoDBInstrumentation({
380
- * requireParentSpan: false // Manual config for MongoDB
381
- * })
382
- * ]
383
- * })
384
- * ```
385
- */
386
- instrumentations?: NodeSDKConfiguration['instrumentations'];
387
-
388
- /**
389
- * Simple names for auto-instrumentation.
390
- * Uses @opentelemetry/auto-instrumentations-node (peer dependency).
391
- *
392
- * **Important:** If you provide manual instrumentations for the same library,
393
- * the manual config takes precedence and auto-instrumentation for that library is disabled.
394
- *
395
- * @example Enable all auto-instrumentations (simple approach)
396
- * ```typescript
397
- * init({
398
- * service: 'my-app',
399
- * autoInstrumentations: true // Enable all with defaults
400
- * })
401
- * ```
402
- *
403
- * @example Enable specific auto-instrumentations
404
- * ```typescript
405
- * init({
406
- * service: 'my-app',
407
- * autoInstrumentations: ['express', 'pino', 'http']
408
- * })
409
- * ```
410
- *
411
- * @example Configure specific auto-instrumentations
412
- * ```typescript
413
- * init({
414
- * service: 'my-app',
415
- * autoInstrumentations: {
416
- * express: { enabled: true },
417
- * pino: { enabled: true },
418
- * http: { enabled: false }
419
- * }
420
- * })
421
- * ```
422
- *
423
- * @example Manual config when you need custom settings
424
- * ```typescript
425
- * import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'
426
- *
427
- * init({
428
- * service: 'my-app',
429
- * autoInstrumentations: false, // Use manual control
430
- * instrumentations: [
431
- * new MongoDBInstrumentation({
432
- * requireParentSpan: false // Custom config not available with auto
433
- * })
434
- * ]
435
- * })
436
- * ```
437
- */
438
- autoInstrumentations?:
439
- | string[]
440
- | boolean
441
- | Record<string, { enabled?: boolean }>;
442
-
443
- /**
444
- * OTLP endpoint for traces/metrics/logs
445
- * Single-destination shorthand. For multi-backend OTLP fan-out, use
446
- * `destinations` instead.
447
- * Only used if you don't provide custom exporters/processors
448
- * @default process.env.OTLP_ENDPOINT || 'http://localhost:4318'
449
- */
450
- endpoint?: string;
451
-
452
- /**
453
- * Declarative OTLP multi-destination config.
454
- * Each destination can override endpoint, headers, protocol, and signals.
455
- *
456
- * This is the high-level alternative to wiring `spanExporters`,
457
- * `spanProcessors`, `metricReaders`, and `logRecordProcessors` manually when
458
- * you want to fan telemetry out to multiple OTLP backends.
459
- *
460
- * When provided, `destinations` takes precedence over the single `endpoint`
461
- * shorthand for built-in OTLP exporters/readers/processors.
462
- *
463
- * @example Grafana + Honeycomb for traces, Grafana only for metrics/logs
464
- * ```typescript
465
- * init({
466
- * service: 'my-app',
467
- * logs: true,
468
- * destinations: [
469
- * {
470
- * endpoint: 'https://otlp-gateway-prod-eu-west-2.grafana.net/otlp',
471
- * headers: { Authorization: 'Basic ...' },
472
- * },
473
- * {
474
- * endpoint: 'https://api.honeycomb.io',
475
- * headers: { 'x-honeycomb-team': '...' },
476
- * signals: ['traces'],
477
- * },
478
- * ],
479
- * })
480
- * ```
481
- */
482
- destinations?: OtlpDestinationConfig[];
483
-
484
- /**
485
- * Custom span processors for traces (supports multiple processors)
486
- * Allows you to use any backend: Jaeger, Zipkin, Datadog, New Relic, etc.
487
- * If not provided, defaults to OTLP with tail sampling
488
- *
489
- * @example Multiple processors
490
- * ```typescript
491
- * import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
492
- * import { BatchSpanProcessor, SimpleSpanProcessor, ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base'
493
- *
494
- * init({
495
- * service: 'my-app',
496
- * spanProcessors: [
497
- * new BatchSpanProcessor(new JaegerExporter()),
498
- * new SimpleSpanProcessor(new ConsoleSpanExporter()) // Debug alongside production
499
- * ]
500
- * })
501
- * ```
502
- *
503
- * @example Single processor
504
- * ```typescript
505
- * import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
506
- *
507
- * init({
508
- * service: 'my-app',
509
- * spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())]
510
- * })
511
- * ```
512
- */
513
- spanProcessors?: SpanProcessor[];
514
-
515
- /**
516
- * Custom span processor for traces (single-item alias of spanProcessors)
517
- * @deprecated Use spanProcessors for consistency with other multi-value config fields
518
- */
519
- spanProcessor?: SpanProcessor;
520
-
521
- /**
522
- * Custom span exporters for traces (alternative to spanProcessors, supports multiple exporters)
523
- * Provide either spanProcessors OR spanExporters, not both
524
- * Each exporter will be wrapped in TailSamplingSpanProcessor + BatchSpanProcessor
525
- *
526
- * @example Multiple exporters
527
- * ```typescript
528
- * import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
529
- * import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
530
- *
531
- * init({
532
- * service: 'my-app',
533
- * spanExporters: [
534
- * new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' }),
535
- * new JaegerExporter() // Send to multiple backends simultaneously
536
- * ]
537
- * })
538
- * ```
539
- *
540
- * @example Single exporter
541
- * ```typescript
542
- * import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
543
- *
544
- * init({
545
- * service: 'my-app',
546
- * spanExporters: [new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' })]
547
- * })
548
- * ```
549
- */
550
- spanExporters?: SpanExporter[];
551
-
552
- /**
553
- * Custom span exporter for traces (single-item alias of spanExporters)
554
- * @deprecated Use spanExporters for consistency with other multi-value config fields
555
- */
556
- spanExporter?: SpanExporter;
557
-
558
- /**
559
- * Custom metric readers (supports multiple readers)
560
- * Allows sending metrics to multiple backends: OTLP, Prometheus, custom readers
561
- * Defaults to OTLP metrics exporter when metrics are enabled.
562
- *
563
- * @example Multiple metric readers
564
- * ```typescript
565
- * import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
566
- * import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
567
- * import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'
568
- *
569
- * init({
570
- * service: 'my-app',
571
- * metricReaders: [
572
- * new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter() }),
573
- * new PrometheusExporter() // Export to multiple backends
574
- * ]
575
- * })
576
- * ```
577
- */
578
- metricReaders?: MetricReader[];
579
-
580
- /**
581
- * Custom metric reader (single-item alias of metricReaders)
582
- * @deprecated Use metricReaders for consistency with other multi-value config fields
583
- */
584
- metricReader?: MetricReader;
585
-
586
- /**
587
- * Custom log record processors. When omitted, logs are not configured.
588
- */
589
- logRecordProcessors?: LogRecordProcessor[];
590
-
591
- /**
592
- * Custom log record processor (single-item alias of logRecordProcessors)
593
- * @deprecated Use logRecordProcessors for consistency with other multi-value config fields
594
- */
595
- logRecordProcessor?: LogRecordProcessor;
596
-
597
- /**
598
- * PostHog integration - auto-configures OTLP log exporter.
599
- *
600
- * @example
601
- * ```typescript
602
- * init({
603
- * service: 'my-app',
604
- * posthog: { url: 'https://us.i.posthog.com/i/v1/logs?token=phc_xxx' }
605
- * });
606
- * ```
607
- *
608
- * Also reads from POSTHOG_LOGS_URL environment variable as fallback.
609
- */
610
- posthog?: { url: string };
611
-
612
- /** Additional resource attributes to merge with defaults. */
613
- resourceAttributes?: Attributes;
614
-
615
- /** Provide a fully custom Resource to merge (advanced use case). */
616
- resource?: Resource;
617
-
618
- /**
619
- * Headers for OTLP exporters. Accepts either an object map or
620
- * a "key=value" comma separated string.
621
- *
622
- * @example
623
- * ```typescript
624
- * init({
625
- * service: 'my-app',
626
- * endpoint: 'https://api.honeycomb.io',
627
- * headers: { 'x-honeycomb-team': 'YOUR_API_KEY' }
628
- * })
629
- * ```
630
- */
631
- headers?: Record<string, string> | string;
632
-
633
- /**
634
- * OTLP protocol to use for traces, metrics, and logs
635
- * - 'http': HTTP/protobuf (default, uses port 4318)
636
- * - 'grpc': gRPC (uses port 4317)
637
- *
638
- * Can be overridden with OTEL_EXPORTER_OTLP_PROTOCOL env var.
639
- *
640
- * Note: gRPC exporters are optional peer dependencies. Install them with:
641
- * ```bash
642
- * pnpm add @opentelemetry/exporter-trace-otlp-grpc @opentelemetry/exporter-metrics-otlp-grpc
643
- * ```
644
- *
645
- * @example HTTP (default)
646
- * ```typescript
647
- * init({
648
- * service: 'my-app',
649
- * protocol: 'http', // or omit (defaults to http)
650
- * endpoint: 'http://localhost:4318'
651
- * })
652
- * ```
653
- *
654
- * @example gRPC
655
- * ```typescript
656
- * init({
657
- * service: 'my-app',
658
- * protocol: 'grpc',
659
- * endpoint: 'grpc://localhost:4317'
660
- * })
661
- * ```
662
- *
663
- * @default 'http'
664
- */
665
- protocol?: 'http' | 'grpc';
666
-
667
- /**
668
- * Optional factory to build a customised NodeSDK instance from our defaults.
669
- */
670
- sdkFactory?: (defaults: Partial<NodeSDKConfiguration>) => NodeSDK;
671
-
672
- /**
673
- * Infrastructure metrics configuration
674
- * - true: always enabled (default)
675
- * - false: always disabled
676
- * - 'auto': always enabled (same as true)
677
- *
678
- * Can be overridden with AUTOTEL_METRICS=on|off env var
679
- */
680
- metrics?: boolean | 'auto';
681
-
682
- /**
683
- * OTLP logs configuration
684
- * - true: auto-configure OTLP log exporter from endpoint
685
- * - false: disabled (default)
686
- * - 'auto': same as false (opt-in only)
687
- *
688
- * When enabled and an endpoint is configured, autotel will automatically
689
- * create a BatchLogRecordProcessor with an OTLPLogExporter - no manual
690
- * imports needed. Works alongside logRecordProcessors (additive).
691
- *
692
- * Requires @opentelemetry/sdk-logs and @opentelemetry/exporter-logs-otlp-http
693
- * (or -grpc) as peer dependencies.
694
- *
695
- * Can be overridden with AUTOTEL_LOGS=on|off env var.
696
- *
697
- * @example
698
- * ```typescript
699
- * init({
700
- * service: 'my-app',
701
- * endpoint: 'http://localhost:4318',
702
- * logs: true,
703
- * });
704
- * ```
705
- */
706
- logs?: boolean | 'auto';
707
-
708
- /** Sampling strategy - takes precedence over `sampling` preset */
709
- sampler?: Sampler;
710
-
711
- /**
712
- * Sampling preset shorthand — resolves to a pre-configured sampler.
713
- * If both `sampler` and `sampling` are provided, `sampler` takes precedence.
714
- *
715
- * @default 'production' (10% baseline + always-on for errors/slow)
716
- *
717
- * **Footgun for one-shot scripts:** the default samples ~90% of spans away,
718
- * which means a fixture-capture script that emits a single normal span
719
- * almost always sees zero output. For local debugging and capture use:
720
- *
721
- * ```typescript
722
- * init({
723
- * service: 'fixture-capture',
724
- * spanProcessors: [new SimpleSpanProcessor(new InMemorySpanExporter())],
725
- * sampling: 'development', // capture EVERY span
726
- * });
727
- * ```
728
- *
729
- * Read `exporter.getFinishedSpans()` **before** calling `shutdown()` —
730
- * `InMemorySpanExporter.shutdown()` resets state.
731
- */
732
- sampling?: SamplingPreset;
733
-
734
- /** Service version (default: auto-detect from package.json or '1.0.0') */
735
- version?: string;
736
-
737
- /** Environment (default: process.env.NODE_ENV || 'development') */
738
- environment?: string;
739
-
740
- /**
741
- * Logger instance for internal autotel diagnostic messages
742
- *
743
- * This logger is used by autotel internally to log initialization, warnings,
744
- * and debug information. Any logger with info/warn/error/debug methods works.
745
- *
746
- * **For OTel instrumentation of your application logs**, use the `autoInstrumentations` option:
747
- * - `autoInstrumentations: ['pino']` - Injects traceId/spanId into Pino logs
748
- * - `autoInstrumentations: ['winston']` - Injects traceId/spanId into Winston logs
749
- *
750
- * Default: silent logger (no-op)
751
- *
752
- * @example Pino with OTel instrumentation
753
- * ```typescript
754
- * import pino from 'pino'
755
- * import { init } from 'autotel'
756
- *
757
- * const logger = pino({ level: 'info' })
758
- * init({
759
- * service: 'my-app',
760
- * logger, // For autotel's internal logs
761
- * autoInstrumentations: ['pino'] // For OTel trace context in YOUR logs
762
- * })
763
- * ```
764
- *
765
- * @example Custom logger for autotel diagnostics
766
- * ```typescript
767
- * const logger = {
768
- * info: (msg, extra) => console.log(msg, extra),
769
- * warn: (msg, extra) => console.warn(msg, extra),
770
- * error: (msg, err, extra) => console.error(msg, err, extra),
771
- * debug: (msg, extra) => console.debug(msg, extra),
772
- * }
773
- * init({ service: 'my-app', logger })
774
- * ```
775
- */
776
- logger?: Logger;
777
-
778
- /**
779
- * Flush events queue when root spans end
780
- * - true: Flush on root span completion (default)
781
- * - false: Use batching (events flush every 10 seconds automatically)
782
- *
783
- * Only flushes on root spans to avoid excessive network calls.
784
- * Default is true for serverless/short-lived processes. Set to false
785
- * for long-running services where batching is more efficient.
786
- */
787
- flushOnRootSpanEnd?: boolean;
788
-
789
- /**
790
- * Force-flush OpenTelemetry spans on shutdown (default: false)
791
- *
792
- * When enabled, spans are force-flushed along with events on root
793
- * span completion. This is useful for serverless/short-lived processes where
794
- * spans may not export before the process ends.
795
- *
796
- * - true: Force-flush spans on root span completion (~50-200ms latency)
797
- * - false: Spans export via normal batch processor (default behavior)
798
- *
799
- * Only applies when flushOnRootSpanEnd is also enabled.
800
- *
801
- * Note: For edge runtimes (Cloudflare Workers, Vercel Edge), use the
802
- * 'autotel-edge' package instead, which handles this automatically.
803
- *
804
- * @example Serverless with force-flush
805
- * ```typescript
806
- * init({
807
- * service: 'my-lambda',
808
- * flushOnRootSpanEnd: true,
809
- * forceFlushOnShutdown: true, // Force-flush spans
810
- * });
811
- * ```
812
- */
813
- forceFlushOnShutdown?: boolean;
814
-
815
- /**
816
- * Automatically copy baggage entries to span attributes
817
- *
818
- * When enabled, all baggage entries are automatically added as span attributes,
819
- * making them visible in trace UIs (Jaeger, Grafana, DataDog, etc.) without
820
- * manually calling ctx.setAttribute() for each entry.
821
- *
822
- * - `true`: adds baggage with 'baggage.' prefix (e.g. baggage.tenant.id)
823
- * - `string`: uses custom prefix (e.g. 'ctx' → ctx.tenant.id, '' → tenant.id)
824
- * - `false` or omit: disabled (default)
825
- *
826
- * @default false
827
- *
828
- * @example Enable with default prefix
829
- * ```typescript
830
- * init({
831
- * service: 'my-app',
832
- * baggage: true
833
- * });
834
- *
835
- * // Now baggage automatically appears as span attributes
836
- * await withBaggage({
837
- * baggage: { 'tenant.id': 't1', 'user.id': 'u1' },
838
- * fn: async () => {
839
- * // Span has baggage.tenant.id and baggage.user.id attributes!
840
- * }
841
- * });
842
- * ```
843
- *
844
- * @example Custom prefix
845
- * ```typescript
846
- * init({
847
- * service: 'my-app',
848
- * baggage: 'ctx' // Uses 'ctx.' prefix
849
- * });
850
- * // Creates attributes: ctx.tenant.id, ctx.user.id
851
- * ```
852
- *
853
- * @example No prefix
854
- * ```typescript
855
- * init({
856
- * service: 'my-app',
857
- * baggage: '' // No prefix
858
- * });
859
- * // Creates attributes: tenant.id, user.id
860
- * ```
861
- */
862
- baggage?: boolean | string;
863
-
864
- /**
865
- * Validation configuration for events events
866
- * - Override default sensitive field patterns for redaction
867
- * - Customize max lengths, nesting depth, etc.
868
- *
869
- * @example Disable redaction for development
870
- * ```typescript
871
- * init({
872
- * service: 'my-app',
873
- * validation: {
874
- * sensitivePatterns: [] // Disable all redaction
875
- * }
876
- * })
877
- * ```
878
- *
879
- * @example Add custom patterns
880
- * ```typescript
881
- * init({
882
- * service: 'my-app',
883
- * validation: {
884
- * sensitivePatterns: [
885
- * /password/i,
886
- * /apiKey/i,
887
- * /customSecret/i // Your custom pattern
888
- * ]
889
- * }
890
- * })
891
- * ```
892
- */
893
- validation?: Partial<ValidationConfig>;
894
-
895
- /**
896
- * Events configuration for trace context, correlation IDs, and enrichment
897
- *
898
- * Controls how product events integrate with distributed tracing:
899
- * - `includeTraceContext`: Automatically include trace context in events
900
- * - `includeLinkedTraceIds`: Include full array of linked trace IDs (for batch/fan-in)
901
- * - `traceUrl`: Generate clickable trace URLs in events
902
- * - `enrichFromBaggage`: Auto-enrich events from baggage with guardrails
903
- *
904
- * @example Basic trace context
905
- * ```typescript
906
- * init({
907
- * service: 'my-app',
908
- * events: {
909
- * includeTraceContext: true
910
- * }
911
- * });
912
- * // Events now include autotel.trace_id, autotel.span_id, autotel.correlation_id
913
- * ```
914
- *
915
- * @example With clickable trace URLs
916
- * ```typescript
917
- * init({
918
- * service: 'my-app',
919
- * events: {
920
- * includeTraceContext: true,
921
- * traceUrl: (ctx) => `https://grafana.internal/explore?traceId=${ctx.traceId}`
922
- * }
923
- * });
924
- * ```
925
- *
926
- * @example With baggage enrichment
927
- * ```typescript
928
- * init({
929
- * service: 'my-app',
930
- * events: {
931
- * includeTraceContext: true,
932
- * enrichFromBaggage: {
933
- * allow: ['tenant.id', 'user.id'],
934
- * prefix: 'ctx.',
935
- * maxKeys: 10,
936
- * maxBytes: 1024
937
- * }
938
- * }
939
- * });
940
- * ```
941
- */
942
- events?: EventsConfig;
943
-
944
- /**
945
- * Debug mode for local span inspection.
946
- * Enables console output to help you see spans as they're created.
947
- *
948
- * - `true`: Raw JSON output (ConsoleSpanExporter)
949
- * - `'pretty'`: Colorized, hierarchical output (PrettyConsoleExporter)
950
- * - `false`/undefined: No console output (default)
951
- *
952
- * When enabled: Outputs spans to console AND sends to backend (if endpoint/exporter configured)
953
- *
954
- * Perfect for progressive development:
955
- * - Start with debug: 'pretty' (no endpoint) → see traces immediately with nice formatting
956
- * - Add endpoint later → console + backend, verify before choosing provider
957
- * - Remove debug in production → backend only, clean production config
958
- *
959
- * Can be overridden with AUTOTEL_DEBUG environment variable.
960
- *
961
- * @example Pretty debug output (recommended for development)
962
- * ```typescript
963
- * init({
964
- * service: 'my-app',
965
- * debug: 'pretty' // Colorized, hierarchical output
966
- * })
967
- * ```
968
- *
969
- * @example Raw JSON output (verbose)
970
- * ```typescript
971
- * init({
972
- * service: 'my-app',
973
- * debug: true // Raw ConsoleSpanExporter output
974
- * })
975
- * ```
976
- *
977
- * @example Environment variable
978
- * ```bash
979
- * AUTOTEL_DEBUG=pretty node server.js
980
- * AUTOTEL_DEBUG=true node server.js
981
- * ```
982
- */
983
- debug?: boolean | 'pretty';
984
-
985
- /**
986
- * Filter predicate to drop unwanted spans before processing.
987
- *
988
- * Useful for filtering out noisy spans from specific instrumentations
989
- * (e.g., Next.js internal spans, health check endpoints).
990
- *
991
- * The filter runs on completed spans (onEnd), so you have access to:
992
- * - `span.name` - Span name
993
- * - `span.attributes` - All span attributes
994
- * - `span.instrumentationScope` - `{ name, version }` of the instrumentation
995
- * - `span.status` - Span status code and message
996
- * - `span.duration` - Span duration as `[seconds, nanoseconds]`
997
- *
998
- * Return `true` to keep the span, `false` to drop it.
999
- *
1000
- * @example Filter out Next.js instrumentation spans
1001
- * ```typescript
1002
- * init({
1003
- * service: 'my-app',
1004
- * spanFilter: (span) => span.instrumentationScope.name !== 'next.js'
1005
- * })
1006
- * ```
1007
- *
1008
- * @example Filter out health check spans
1009
- * ```typescript
1010
- * init({
1011
- * service: 'my-app',
1012
- * spanFilter: (span) => !span.name.includes('/health')
1013
- * })
1014
- * ```
1015
- *
1016
- * @example Complex filtering (multiple conditions)
1017
- * ```typescript
1018
- * init({
1019
- * service: 'my-app',
1020
- * spanFilter: (span) => {
1021
- * // Drop Next.js internal spans
1022
- * if (span.instrumentationScope.name === 'next.js') return false;
1023
- * // Drop health checks
1024
- * if (span.name.includes('/health')) return false;
1025
- * // Drop very short spans (less than 1ms)
1026
- * const [secs, nanos] = span.duration;
1027
- * if (secs === 0 && nanos < 1_000_000) return false;
1028
- * return true;
1029
- * }
1030
- * })
1031
- * ```
1032
- */
1033
- spanFilter?: SpanFilterPredicate;
1034
-
1035
- /**
1036
- * Normalize span names to reduce cardinality from dynamic path segments.
1037
- *
1038
- * High-cardinality span names (e.g., `/users/123/posts/456`) cause issues:
1039
- * - Cost explosions in observability backends
1040
- * - Cardinality limits exceeded
1041
- * - Poor UX when searching/filtering traces
1042
- *
1043
- * The normalizer transforms dynamic segments into placeholders:
1044
- * - `/users/123` → `/users/:id`
1045
- * - `/items/550e8400-e29b-...` → `/items/:uuid`
1046
- *
1047
- * Provide either a custom function or use a built-in preset:
1048
- * - `'rest-api'` - Numeric IDs, UUIDs, ObjectIds, dates, timestamps, emails
1049
- * - `'graphql'` - GraphQL operation name normalization
1050
- * - `'minimal'` - Only numeric IDs and UUIDs
1051
- *
1052
- * @example Custom normalizer function
1053
- * ```typescript
1054
- * init({
1055
- * service: 'my-app',
1056
- * spanNameNormalizer: (name) => {
1057
- * return name
1058
- * .replace(/\/[0-9]+/g, '/:id')
1059
- * .replace(/\/[a-f0-9-]{36}/gi, '/:uuid');
1060
- * }
1061
- * })
1062
- * ```
1063
- *
1064
- * @example Using built-in preset
1065
- * ```typescript
1066
- * init({
1067
- * service: 'my-app',
1068
- * spanNameNormalizer: 'rest-api'
1069
- * })
1070
- * ```
1071
- *
1072
- * @example Combining with spanFilter
1073
- * ```typescript
1074
- * init({
1075
- * service: 'my-app',
1076
- * spanNameNormalizer: 'rest-api',
1077
- * spanFilter: (span) => span.instrumentationScope.name !== 'next.js'
1078
- * })
1079
- * ```
1080
- */
1081
- spanNameNormalizer?: SpanNameNormalizerConfig;
1082
-
1083
- /**
1084
- * Automatically redact PII and sensitive data from span attributes before export.
1085
- * Critical for compliance (GDPR, PCI-DSS, HIPAA) and data security.
1086
- *
1087
- * Auto-enabled in production: when this is left unset and the resolved
1088
- * environment is `production`, the `'default'` preset is applied. Override
1089
- * with the `AUTOTEL_REDACT_PII` env var (`off` / `strict` / `pci-dss` / ...)
1090
- * or pass `false` to disable redaction entirely.
1091
- *
1092
- * Can be a preset name, custom configuration, or `false` to disable:
1093
- * - `'default'`: Emails, phones, SSNs, credit cards, sensitive keys (password, secret, token)
1094
- * - `'strict'`: Default + Bearer tokens, JWTs, API keys in values
1095
- * - `'pci-dss'`: Payment card industry focus (credit cards, CVV, card-related keys)
1096
- *
1097
- * @example Use default preset
1098
- * ```typescript
1099
- * init({
1100
- * service: 'my-app',
1101
- * attributeRedactor: 'default'
1102
- * })
1103
- * ```
1104
- *
1105
- * @example Custom patterns
1106
- * ```typescript
1107
- * init({
1108
- * service: 'my-app',
1109
- * attributeRedactor: {
1110
- * keyPatterns: [/password/i, /secret/i],
1111
- * valuePatterns: [
1112
- * { name: 'customerId', pattern: /CUST-\d{8}/g, replacement: 'CUST-***' }
1113
- * ]
1114
- * }
1115
- * })
1116
- * ```
1117
- *
1118
- * @example Custom redactor function
1119
- * ```typescript
1120
- * init({
1121
- * service: 'my-app',
1122
- * attributeRedactor: {
1123
- * redactor: (key, value) => {
1124
- * if (key === 'user.email' && typeof value === 'string') {
1125
- * return value.replace(/@.+/, '@[REDACTED]');
1126
- * }
1127
- * return value;
1128
- * }
1129
- * }
1130
- * })
1131
- * ```
1132
- */
1133
- attributeRedactor?: AttributeRedactorConfig | AttributeRedactorPreset | false;
1134
-
1135
- /**
1136
- * OpenLLMetry integration for LLM observability.
1137
- * Requires @traceloop/node-server-sdk as an optional peer dependency.
1138
- *
1139
- * @example Enable OpenLLMetry with default settings
1140
- * ```typescript
1141
- * init({
1142
- * service: 'my-app',
1143
- * openllmetry: { enabled: true }
1144
- * })
1145
- * ```
1146
- *
1147
- * @example Enable with custom options
1148
- * ```typescript
1149
- * init({
1150
- * service: 'my-app',
1151
- * openllmetry: {
1152
- * enabled: true,
1153
- * options: {
1154
- * disableBatch: process.env.NODE_ENV !== 'production',
1155
- * apiKey: process.env.TRACELOOP_API_KEY
1156
- * }
1157
- * }
1158
- * })
1159
- * ```
1160
- */
1161
- openllmetry?: {
1162
- enabled: boolean;
1163
- options?: Record<string, unknown>;
1164
- };
1165
-
1166
- /**
1167
- * Canonical log lines - automatically emit spans as wide events (canonical log lines)
1168
- *
1169
- * When enabled, each span (or root span only) is automatically emitted as a
1170
- * comprehensive log record with ALL span attributes. This implements the
1171
- * "canonical log line" pattern: one comprehensive event per request with all context.
1172
- *
1173
- * **Benefits:**
1174
- * - One log line per request with all context (wide event)
1175
- * - High-cardinality, high-dimensionality data for powerful queries
1176
- * - Automatic - no manual logging needed
1177
- * - Queryable as structured data instead of string search
1178
- *
1179
- * @example Basic usage (one canonical log line per request)
1180
- * ```typescript
1181
- * init({
1182
- * service: 'checkout-api',
1183
- * canonicalLogLines: {
1184
- * enabled: true,
1185
- * rootSpansOnly: true, // One canonical log line per request
1186
- * },
1187
- * });
1188
- * ```
1189
- *
1190
- * @example With custom logger
1191
- * ```typescript
1192
- * import pino from 'pino';
1193
- * const logger = pino();
1194
- * init({
1195
- * service: 'my-app',
1196
- * logger,
1197
- * canonicalLogLines: {
1198
- * enabled: true,
1199
- * logger, // Use Pino for canonical log lines
1200
- * rootSpansOnly: true,
1201
- * },
1202
- * });
1203
- * ```
1204
- *
1205
- * @example Custom message format
1206
- * ```typescript
1207
- * init({
1208
- * service: 'my-app',
1209
- * canonicalLogLines: {
1210
- * enabled: true,
1211
- * messageFormat: (span) => {
1212
- * const status = span.status.code === 2 ? 'ERROR' : 'SUCCESS';
1213
- * return `${span.name} [${status}]`;
1214
- * },
1215
- * },
1216
- * });
1217
- * ```
1218
- */
1219
- canonicalLogLines?: {
1220
- enabled: boolean;
1221
- /** Logger to use for emitting canonical log lines (defaults to OTel Logs API) */
1222
- logger?: Logger;
1223
- /** Only emit canonical log lines for root spans (default: false) */
1224
- rootSpansOnly?: boolean;
1225
- /** Minimum log level for canonical log lines (default: 'info') */
1226
- minLevel?: 'debug' | 'info' | 'warn' | 'error';
1227
- /** Custom message format (default: uses span name) */
1228
- messageFormat?: (
1229
- span: import('@opentelemetry/sdk-trace-base').ReadableSpan,
1230
- ) => string;
1231
- /** Whether to include resource attributes (default: true) */
1232
- includeResourceAttributes?: boolean;
1233
- /** Predicate to decide whether to emit (runs after event is built). */
1234
- shouldEmit?: CanonicalLogLineOptions['shouldEmit'];
1235
- /**
1236
- * Declarative tail sampling conditions (OR logic).
1237
- * Ignored when `shouldEmit` is provided.
1238
- * @example keep: [{ status: 500 }, { durationMs: 1000 }]
1239
- */
1240
- keep?: CanonicalLogLineOptions['keep'];
1241
- /** Callback invoked after emit for custom fan-out. */
1242
- drain?: CanonicalLogLineOptions['drain'];
1243
- /** Handler for drain failures. */
1244
- onDrainError?: CanonicalLogLineOptions['onDrainError'];
1245
- /**
1246
- * Pretty-print canonical log lines to console.
1247
- * Defaults to true when NODE_ENV is 'development'.
1248
- */
1249
- pretty?: boolean;
1250
- };
1251
-
1252
- /**
1253
- * Suppress console output while keeping OTel exporters running.
1254
- * Useful for platforms like GCP Cloud Run / AWS Lambda where stdout
1255
- * is managed externally by the platform's log collector.
1256
- *
1257
- * @default false
1258
- */
1259
- silent?: boolean;
1260
-
1261
- /**
1262
- * Minimum log level for internal autotel diagnostic messages.
1263
- * Messages below this level are dropped before processing.
1264
- *
1265
- * @default 'info'
1266
- */
1267
- minLevel?: 'debug' | 'info' | 'warn' | 'error';
1268
- }
1269
-
1270
- // Internal state
1271
- let initialized = false;
1272
- let locked = false;
1273
- let config: AutotelConfig | null = null;
1274
- let sdk: NodeSDK | null = null;
1275
- let warnedOnce = false;
1276
- let logger: Logger = silentLogger; // Silent by default - no spam
1277
- let validationConfig: Partial<ValidationConfig> | null = null;
1278
- let eventsConfig: EventsConfig | null = null;
1279
- let _stringRedactor: StringRedactor | null = null;
1280
- let _optionalRequire: typeof safeRequire = safeRequire;
1281
- let _devtoolsClose: (() => Promise<void> | void) | null = null;
1282
-
1283
- const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 } as const;
1284
- type LogLevelKey = keyof typeof LOG_LEVELS;
1285
-
1286
- /**
1287
- * Lock the logger to prevent further `init()` calls.
1288
- * Use this when framework plugins set up instrumentation and you want
1289
- * to prevent accidental re-initialization from user code.
1290
- */
1291
- export function lockLogger(): void {
1292
- locked = true;
1293
- }
1294
-
1295
- /**
1296
- * Check if the logger has been locked.
1297
- */
1298
- export function isLoggerLocked(): boolean {
1299
- return locked;
1300
- }
1301
-
1302
- function createSilentLogger(): Logger {
1303
- return {
1304
- info: () => {},
1305
- warn: () => {},
1306
- error: () => {},
1307
- debug: () => {},
1308
- };
1309
- }
1310
-
1311
- function wrapLogger(
1312
- base: Logger,
1313
- silent: boolean,
1314
- minLevel: LogLevelKey,
1315
- ): Logger {
1316
- if (silent) return createSilentLogger();
1317
- const threshold = LOG_LEVELS[minLevel];
1318
- const wrap = (fn: Logger['info'], level: LogLevelKey): Logger['info'] => {
1319
- if (LOG_LEVELS[level] < threshold) {
1320
- return (() => {}) as Logger['info'];
1321
- }
1322
- return ((...args: Parameters<Logger['info']>) =>
1323
- fn(...args)) as Logger['info'];
1324
- };
1325
- return {
1326
- debug: wrap(base.debug, 'debug'),
1327
- info: wrap(base.info, 'info'),
1328
- warn: wrap(base.warn, 'warn'),
1329
- error: wrap(base.error, 'error'),
1330
- };
1331
- }
1332
-
1333
- /**
1334
- * Resolve the effective attribute redactor. Explicit config wins (`false`
1335
- * disables). Otherwise the `AUTOTEL_REDACT_PII` env var controls it, and as a
1336
- * final default PII redaction is auto-enabled in production.
1337
- */
1338
- export function resolveAttributeRedactor(
1339
- explicit:
1340
- | AttributeRedactorConfig
1341
- | AttributeRedactorPreset
1342
- | false
1343
- | undefined,
1344
- environment: string,
1345
- ): AttributeRedactorConfig | AttributeRedactorPreset | undefined {
1346
- if (explicit === false) return undefined;
1347
- if (explicit !== undefined) return explicit;
1348
-
1349
- const flag = process.env.AUTOTEL_REDACT_PII?.trim().toLowerCase();
1350
- if (flag) {
1351
- if (['off', 'false', '0', 'none', 'disabled'].includes(flag)) {
1352
- return undefined;
1353
- }
1354
- if (flag === 'default' || flag === 'strict' || flag === 'pci-dss') {
1355
- return flag;
1356
- }
1357
- if (['on', 'true', '1', 'enabled'].includes(flag)) {
1358
- return 'default';
1359
- }
1360
- }
1361
-
1362
- return environment === 'production' ? 'default' : undefined;
1363
- }
1364
-
1365
- function detectEnvironmentAttributes(): Record<string, string> {
1366
- const attrs: Record<string, string> = {};
1367
-
1368
- const commitSha =
1369
- process.env.COMMIT_SHA ||
1370
- process.env.GITHUB_SHA ||
1371
- process.env.VERCEL_GIT_COMMIT_SHA ||
1372
- process.env.CF_PAGES_COMMIT_SHA ||
1373
- process.env.AWS_CODEPIPELINE_EXECUTION_ID;
1374
- if (commitSha) attrs['service.commit.sha'] = commitSha;
1375
-
1376
- const region =
1377
- process.env.VERCEL_REGION ||
1378
- process.env.AWS_REGION ||
1379
- process.env.AWS_DEFAULT_REGION ||
1380
- process.env.FLY_REGION ||
1381
- process.env.CF_REGION ||
1382
- process.env.GOOGLE_CLOUD_REGION;
1383
- if (region) attrs['service.region'] = region;
1384
-
1385
- const version =
1386
- process.env.APP_VERSION ||
1387
- process.env.HEROKU_RELEASE_VERSION ||
1388
- process.env.VERCEL_GIT_COMMIT_REF;
1389
- if (version) attrs['service.deploy.version'] = version;
1390
-
1391
- return attrs;
1392
- }
1393
-
1394
- /**
1395
- * Resolve metrics flag with env var override support
1396
- */
1397
- export function resolveMetricsFlag(
1398
- configFlag: boolean | 'auto' = 'auto',
1399
- ): boolean {
1400
- // 1. Check env var override (highest priority)
1401
- const envFlag = process.env.AUTOTEL_METRICS;
1402
- if (envFlag === 'on' || envFlag === 'true') return true;
1403
- if (envFlag === 'off' || envFlag === 'false') return false;
1404
-
1405
- // 2. Check config flag
1406
- if (configFlag === true) return true;
1407
- if (configFlag === false) return false;
1408
-
1409
- // 3. Default: enabled in all environments (simpler)
1410
- return true;
1411
- }
1412
-
1413
- /**
1414
- * Resolve logs flag with env var override support.
1415
- * Defaults to disabled (opt-in only) to avoid unexpected log export
1416
- * and to preserve the upstream SDK's OTEL_LOGS_EXPORTER handling.
1417
- */
1418
- export function resolveLogsFlag(
1419
- configFlag: boolean | 'auto' = 'auto',
1420
- ): boolean {
1421
- // 1. Check env var override (highest priority)
1422
- const envFlag = process.env.AUTOTEL_LOGS;
1423
- if (envFlag === 'on' || envFlag === 'true') return true;
1424
- if (envFlag === 'off' || envFlag === 'false') return false;
1425
-
1426
- // 2. Check config flag
1427
- if (configFlag === true) return true;
1428
- if (configFlag === false) return false;
1429
-
1430
- // 3. Default: disabled (opt-in only)
1431
- return false;
1432
- }
1433
-
1434
- /**
1435
- * Resolve debug flag with env var override support
1436
- *
1437
- * Supports:
1438
- * - `'pretty'`: Colorized, hierarchical output (PrettyConsoleExporter)
1439
- * - `true` / `'true'` / `'1'`: Raw JSON output (ConsoleSpanExporter)
1440
- * - `false` / `'false'` / `'0'`: Disabled
1441
- */
1442
- export function resolveDebugFlag(
1443
- configFlag?: boolean | 'pretty',
1444
- ): boolean | 'pretty' {
1445
- // 1. Check env var override (highest priority)
1446
- const envFlag = process.env.AUTOTEL_DEBUG;
1447
- if (envFlag === 'pretty') return 'pretty';
1448
- if (envFlag === 'true' || envFlag === '1') return true;
1449
- if (envFlag === 'false' || envFlag === '0') return false;
1450
-
1451
- // 2. Return config flag (defaults to false)
1452
- return configFlag ?? false;
1453
- }
1454
-
1455
- function normalizeOtlpHeaders(
1456
- headers?: Record<string, string> | string,
1457
- ): Record<string, string> | undefined {
1458
- if (!headers) return undefined;
1459
- if (typeof headers !== 'string') return headers;
1460
-
1461
- const parsed: Record<string, string> = {};
1462
- for (const pair of headers.split(',')) {
1463
- const [key, ...valueParts] = pair.split('=');
1464
- if (!key || valueParts.length === 0) continue;
1465
- parsed[key.trim()] = valueParts.join('=').trim();
1466
- }
1467
- return parsed;
1468
- }
1469
-
1470
- type ResolvedOtlpDestination = {
1471
- endpoint: string;
1472
- protocol: 'http' | 'grpc';
1473
- headers?: Record<string, string>;
1474
- signals?: Set<OtlpSignal>;
1475
- };
1476
-
1477
- function resolveOtlpDestinations(
1478
- config: AutotelConfig,
1479
- fallbackEndpoint?: string,
1480
- ): ResolvedOtlpDestination[] {
1481
- const rawDestinations =
1482
- config.destinations !== undefined
1483
- ? config.destinations
1484
- : fallbackEndpoint
1485
- ? [
1486
- {
1487
- endpoint: fallbackEndpoint,
1488
- headers: config.headers,
1489
- protocol: config.protocol,
1490
- },
1491
- ]
1492
- : [];
1493
-
1494
- return rawDestinations.map((destination) => ({
1495
- endpoint: destination.endpoint,
1496
- protocol: resolveProtocol(destination.protocol ?? config.protocol),
1497
- headers: normalizeOtlpHeaders(destination.headers ?? config.headers),
1498
- signals: destination.signals ? new Set(destination.signals) : undefined,
1499
- }));
1500
- }
1501
-
1502
- function destinationSupportsSignal(
1503
- destination: ResolvedOtlpDestination,
1504
- signal: OtlpSignal,
1505
- ): boolean {
1506
- return destination.signals ? destination.signals.has(signal) : true;
1507
- }
1508
-
1509
- /**
1510
- * Initialize autotel - Write Once, Observe Everywhere
1511
- *
1512
- * Follows OpenTelemetry standards: opinionated defaults with full flexibility
1513
- * Idempotent: multiple calls are safe, last one wins
1514
- *
1515
- * @example Minimal setup (OTLP default)
1516
- * ```typescript
1517
- * init({ service: 'my-app' })
1518
- * ```
1519
- *
1520
- * @example With events (observe in PostHog, Mixpanel, etc.)
1521
- * ```typescript
1522
- * import { PostHogSubscriber } from 'autotel-subscribers/posthog';
1523
- *
1524
- * init({
1525
- * service: 'my-app',
1526
- * subscribers: [new PostHogSubscriber({ apiKey: '...' })]
1527
- * })
1528
- * ```
1529
- *
1530
- * @example Observe in Jaeger
1531
- * ```typescript
1532
- * import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
1533
- *
1534
- * init({
1535
- * service: 'my-app',
1536
- * spanExporter: new JaegerExporter({ endpoint: 'http://localhost:14268/api/traces' })
1537
- * })
1538
- * ```
1539
- *
1540
- * @example Observe in Zipkin
1541
- * ```typescript
1542
- * import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
1543
- *
1544
- * init({
1545
- * service: 'my-app',
1546
- * spanExporter: new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' })
1547
- * })
1548
- * ```
1549
- *
1550
- * @example Observe in Datadog
1551
- * ```typescript
1552
- * import { DatadogSpanProcessor } from '@opentelemetry/exporter-datadog'
1553
- *
1554
- * init({
1555
- * service: 'my-app',
1556
- * spanProcessor: new DatadogSpanProcessor({ ... })
1557
- * })
1558
- * ```
1559
- *
1560
- * @example Console output (dev)
1561
- * ```typescript
1562
- * import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
1563
- *
1564
- * init({
1565
- * service: 'my-app',
1566
- * spanProcessor: new SimpleSpanProcessor(new ConsoleSpanExporter())
1567
- * })
1568
- * ```
1569
- */
1570
-
1571
- export function init(cfg: AutotelConfig): void {
1572
- if (locked) {
1573
- return;
1574
- }
1575
-
1576
- // Resolve configs in priority order: explicit > yaml > env > defaults
1577
- const envConfig = resolveConfigFromEnv();
1578
- const yamlConfig = loadYamlConfig() ?? {};
1579
-
1580
- // Merge configs: explicit config > yaml file > env vars > defaults
1581
- const mergedConfig: AutotelConfig = {
1582
- ...envConfig, // Environment variables (lowest priority)
1583
- ...yamlConfig, // YAML file (middle priority)
1584
- ...cfg, // Explicit config (highest priority)
1585
- // Deep merge for resourceAttributes
1586
- resourceAttributes: {
1587
- ...envConfig.resourceAttributes,
1588
- ...yamlConfig.resourceAttributes,
1589
- ...detectEnvironmentAttributes(),
1590
- ...cfg.resourceAttributes,
1591
- },
1592
- // Handle headers merge (can be string or object)
1593
- headers: cfg.headers ?? yamlConfig.headers ?? envConfig.headers,
1594
- } as AutotelConfig;
1595
-
1596
- const resolvedRedactor = resolveAttributeRedactor(
1597
- mergedConfig.attributeRedactor,
1598
- mergedConfig.environment || process.env.NODE_ENV || 'development',
1599
- );
1600
- if (resolvedRedactor === undefined) {
1601
- mergedConfig.attributeRedactor = undefined;
1602
- } else {
1603
- const normalizedRedactor =
1604
- normalizeAttributeRedactorConfig(resolvedRedactor);
1605
- if (!normalizedRedactor) {
1606
- throw new Error('Invalid attributeRedactor config');
1607
- }
1608
- mergedConfig.attributeRedactor = normalizedRedactor;
1609
- }
1610
-
1611
- const devtoolsConfig = resolveDevtoolsConfig(mergedConfig.devtools);
1612
- if (devtoolsConfig.enabled && mergedConfig.logs === undefined) {
1613
- mergedConfig.logs = true;
1614
- }
1615
-
1616
- const silent = mergedConfig.silent ?? false;
1617
- const minLevel = mergedConfig.minLevel ?? 'info';
1618
- const baseLogger = mergedConfig.logger || silentLogger;
1619
- logger = wrapLogger(baseLogger, silent, minLevel);
1620
-
1621
- // Warn if re-initializing (same behavior in all environments)
1622
- if (initialized) {
1623
- logger.warn(
1624
- {},
1625
- '[autotel] init() called again - last config wins. This may cause unexpected behavior.',
1626
- );
1627
- }
1628
-
1629
- config = mergedConfig;
1630
- validationConfig = mergedConfig.validation || null;
1631
- eventsConfig = mergedConfig.events || null;
1632
-
1633
- // Initialize OpenTelemetry
1634
- // Only use endpoint if explicitly configured (no default fallback)
1635
- let endpoint = mergedConfig.endpoint ?? devtoolsConfig.endpoint;
1636
- const version = mergedConfig.version || detectVersion();
1637
- const environment =
1638
- mergedConfig.environment || process.env.NODE_ENV || 'development';
1639
- const metricsEnabled = resolveMetricsFlag(mergedConfig.metrics);
1640
- const logsEnabled = resolveLogsFlag(mergedConfig.logs);
1641
-
1642
- if (devtoolsConfig.enabled && devtoolsConfig.embedded) {
1643
- const devtoolsModule = _optionalRequire<{
1644
- createDevtools?: (options?: {
1645
- port?: number;
1646
- host?: string;
1647
- verbose?: boolean;
1648
- }) => { port: number; close: () => Promise<void> | void };
1649
- }>('autotel-devtools');
1650
-
1651
- if (devtoolsModule?.createDevtools) {
1652
- const devtoolsInstance = devtoolsModule.createDevtools({
1653
- port: devtoolsConfig.port,
1654
- host: devtoolsConfig.host,
1655
- verbose: devtoolsConfig.verbose,
1656
- });
1657
- _devtoolsClose = devtoolsInstance.close;
1658
- endpoint = `http://${devtoolsConfig.host}:${devtoolsInstance.port}`;
1659
- logger.info(
1660
- {},
1661
- `[autotel] autotel-devtools embedded server started at ${endpoint}`,
1662
- );
1663
- } else {
1664
- logger.warn(
1665
- {},
1666
- '[autotel] devtools.embedded requested but autotel-devtools is not installed. Falling back to endpoint-only mode.',
1667
- );
1668
- }
1669
- }
1670
-
1671
- // Detect hostname for proper Datadog correlation and Service Catalog discovery
1672
- const hostname = detectHostname();
1673
-
1674
- let resource = resourceFromAttributes({
1675
- [ATTR_SERVICE_NAME]: mergedConfig.service,
1676
- [ATTR_SERVICE_VERSION]: version,
1677
- // Support both old and new OpenTelemetry semantic conventions for environment
1678
- 'deployment.environment': environment, // Deprecated but widely supported
1679
- 'deployment.environment.name': environment, // OTel v1.27.0+ standard
1680
- });
1681
-
1682
- // Add hostname attributes for Datadog Service Catalog and infrastructure correlation
1683
- if (hostname) {
1684
- resource = resource.merge(
1685
- resourceFromAttributes({
1686
- 'host.name': hostname, // OpenTelemetry standard
1687
- 'datadog.host.name': hostname, // Datadog-specific, highest priority for Datadog
1688
- }),
1689
- );
1690
- }
1691
-
1692
- if (mergedConfig.resource) {
1693
- resource = resource.merge(mergedConfig.resource);
1694
- }
1695
-
1696
- if (mergedConfig.resourceAttributes) {
1697
- resource = resource.merge(
1698
- resourceFromAttributes(mergedConfig.resourceAttributes),
1699
- );
1700
- }
1701
-
1702
- const otlpDestinations = resolveOtlpDestinations(mergedConfig, endpoint);
1703
-
1704
- // Backward-compatible singular aliases. Plural forms take precedence when both are provided.
1705
- const configuredSpanProcessors =
1706
- mergedConfig.spanProcessors && mergedConfig.spanProcessors.length > 0
1707
- ? mergedConfig.spanProcessors
1708
- : mergedConfig.spanProcessor
1709
- ? [mergedConfig.spanProcessor]
1710
- : undefined;
1711
- const configuredSpanExporters =
1712
- mergedConfig.spanExporters && mergedConfig.spanExporters.length > 0
1713
- ? mergedConfig.spanExporters
1714
- : mergedConfig.spanExporter
1715
- ? [mergedConfig.spanExporter]
1716
- : undefined;
1717
- const configuredMetricReaders =
1718
- mergedConfig.metricReaders && mergedConfig.metricReaders.length > 0
1719
- ? mergedConfig.metricReaders
1720
- : mergedConfig.metricReader
1721
- ? [mergedConfig.metricReader]
1722
- : undefined;
1723
- const configuredLogRecordProcessors =
1724
- mergedConfig.logRecordProcessors &&
1725
- mergedConfig.logRecordProcessors.length > 0
1726
- ? mergedConfig.logRecordProcessors
1727
- : mergedConfig.logRecordProcessor
1728
- ? [mergedConfig.logRecordProcessor]
1729
- : undefined;
1730
-
1731
- // Build array of span processors (supports multiple)
1732
- let spanProcessors: SpanProcessor[] = [];
1733
-
1734
- if (configuredSpanProcessors && configuredSpanProcessors.length > 0) {
1735
- // User provided custom processors (full control)
1736
- spanProcessors.push(...configuredSpanProcessors);
1737
- } else if (configuredSpanExporters && configuredSpanExporters.length > 0) {
1738
- // User provided custom exporters (wrap each with tail sampling)
1739
- for (const exporter of configuredSpanExporters) {
1740
- spanProcessors.push(
1741
- new TailSamplingSpanProcessor(new BatchSpanProcessor(exporter)),
1742
- );
1743
- }
1744
- } else {
1745
- for (const destination of otlpDestinations) {
1746
- if (!destinationSupportsSignal(destination, 'traces')) continue;
1747
-
1748
- const traceExporter = createTraceExporter(destination.protocol, {
1749
- url: formatEndpointUrl(
1750
- destination.endpoint,
1751
- 'traces',
1752
- destination.protocol,
1753
- ),
1754
- headers: destination.headers,
1755
- });
1756
-
1757
- spanProcessors.push(
1758
- new TailSamplingSpanProcessor(new BatchSpanProcessor(traceExporter)),
1759
- );
1760
- }
1761
- }
1762
- // If no endpoint and no custom processors/exporters, array remains empty
1763
- // SDK will still work but won't export traces
1764
-
1765
- // Add baggage span processor if enabled
1766
- if (mergedConfig.baggage) {
1767
- const prefix =
1768
- typeof mergedConfig.baggage === 'string'
1769
- ? mergedConfig.baggage
1770
- ? `${mergedConfig.baggage}.`
1771
- : ''
1772
- : 'baggage.';
1773
- spanProcessors.push(new BaggageSpanProcessor({ prefix }));
1774
- }
1775
-
1776
- // Apply debug mode configuration
1777
- const debugMode = resolveDebugFlag(mergedConfig.debug);
1778
-
1779
- if (debugMode === 'pretty') {
1780
- // Pretty debug: colorized, hierarchical output
1781
- spanProcessors.push(new SimpleSpanProcessor(new PrettyConsoleExporter()));
1782
- } else if (debugMode === true) {
1783
- // Raw debug: JSON output
1784
- spanProcessors.push(new SimpleSpanProcessor(new ConsoleSpanExporter()));
1785
- }
1786
-
1787
- // Add canonical log line processor BEFORE wrapping processors
1788
- // This ensures it gets wrapped with the same filter/normalizer/redactor as other processors,
1789
- // so canonical logs respect spanFilter (filtered spans aren't logged), spanNameNormalizer
1790
- // (normalized names are used), and attributeRedactor (sensitive data is redacted).
1791
- if (mergedConfig.canonicalLogLines?.enabled) {
1792
- const canonicalOptions: CanonicalLogLineOptions = {
1793
- logger: mergedConfig.canonicalLogLines.logger || mergedConfig.logger,
1794
- rootSpansOnly: mergedConfig.canonicalLogLines.rootSpansOnly,
1795
- minLevel: mergedConfig.canonicalLogLines.minLevel,
1796
- messageFormat: mergedConfig.canonicalLogLines.messageFormat,
1797
- includeResourceAttributes:
1798
- mergedConfig.canonicalLogLines.includeResourceAttributes,
1799
- shouldEmit: mergedConfig.canonicalLogLines.shouldEmit,
1800
- keep: mergedConfig.canonicalLogLines.keep,
1801
- drain: mergedConfig.canonicalLogLines.drain,
1802
- onDrainError: mergedConfig.canonicalLogLines.onDrainError,
1803
- pretty: mergedConfig.canonicalLogLines.pretty,
1804
- };
1805
- spanProcessors.push(new CanonicalLogLineProcessor(canonicalOptions));
1806
- }
1807
-
1808
- // Wrap processors in order: redactor (innermost) → normalizer → filter (outermost)
1809
- // This ensures onEnd() execution order is: filter → normalizer → redactor
1810
- // So filtering sees original attributes, and redaction happens last before export.
1811
-
1812
- // Step 1: Wrap with AttributeRedactingProcessor (innermost - executes last in onEnd)
1813
- if (mergedConfig.attributeRedactor && spanProcessors.length > 0) {
1814
- const redactor = mergedConfig.attributeRedactor;
1815
- spanProcessors = spanProcessors.map(
1816
- (processor) =>
1817
- new AttributeRedactingProcessor(processor, {
1818
- redactor,
1819
- }),
1820
- );
1821
- }
1822
-
1823
- // Store string redactor for use by PostHog log/subscriber paths
1824
- if (mergedConfig.attributeRedactor) {
1825
- _stringRedactor = createStringRedactor(mergedConfig.attributeRedactor);
1826
- }
1827
-
1828
- // Wire string redactor to subscribers that support it (e.g., PostHogSubscriber)
1829
- if (_stringRedactor && mergedConfig.subscribers) {
1830
- for (const subscriber of mergedConfig.subscribers) {
1831
- if (
1832
- 'setStringRedactor' in subscriber &&
1833
- typeof (subscriber as any).setStringRedactor === 'function'
1834
- ) {
1835
- (subscriber as any).setStringRedactor(_stringRedactor);
1836
- }
1837
- }
1838
- }
1839
-
1840
- // Step 2: Wrap with SpanNameNormalizingProcessor (middle)
1841
- // Normalizer runs in onStart(), so span names are normalized before any onEnd processing
1842
- if (mergedConfig.spanNameNormalizer && spanProcessors.length > 0) {
1843
- spanProcessors = spanProcessors.map(
1844
- (processor) =>
1845
- new SpanNameNormalizingProcessor(processor, {
1846
- normalizer: mergedConfig.spanNameNormalizer!,
1847
- }),
1848
- );
1849
- }
1850
-
1851
- // Step 3: Wrap with FilteringSpanProcessor (outermost - executes first in onEnd)
1852
- // Filter sees original (unredacted) attributes, so it can match on sensitive values
1853
- if (mergedConfig.spanFilter && spanProcessors.length > 0) {
1854
- spanProcessors = spanProcessors.map(
1855
- (processor) =>
1856
- new FilteringSpanProcessor(processor, {
1857
- filter: mergedConfig.spanFilter!,
1858
- }),
1859
- );
1860
- }
1861
-
1862
- // Build array of metric readers (supports multiple)
1863
- const metricReaders: MetricReader[] = [];
1864
-
1865
- if (configuredMetricReaders && configuredMetricReaders.length > 0) {
1866
- // User provided custom metric readers
1867
- metricReaders.push(...configuredMetricReaders);
1868
- } else if (metricsEnabled) {
1869
- for (const destination of otlpDestinations) {
1870
- if (!destinationSupportsSignal(destination, 'metrics')) continue;
1871
-
1872
- const metricExporter = createMetricExporter(destination.protocol, {
1873
- url: formatEndpointUrl(
1874
- destination.endpoint,
1875
- 'metrics',
1876
- destination.protocol,
1877
- ),
1878
- headers: destination.headers,
1879
- });
1880
-
1881
- metricReaders.push(
1882
- new PeriodicExportingMetricReader({
1883
- exporter: metricExporter,
1884
- }),
1885
- );
1886
- }
1887
- }
1888
-
1889
- let logRecordProcessors: LogRecordProcessor[] | undefined;
1890
- if (
1891
- configuredLogRecordProcessors &&
1892
- configuredLogRecordProcessors.length > 0
1893
- ) {
1894
- logRecordProcessors = [...configuredLogRecordProcessors];
1895
- }
1896
-
1897
- // Auto-configure OTLP log exporters when logs are enabled.
1898
- if (logsEnabled) {
1899
- for (const destination of otlpDestinations) {
1900
- if (!destinationSupportsSignal(destination, 'logs')) continue;
1901
-
1902
- const logExporter = createLogExporter(destination.protocol, {
1903
- url: formatEndpointUrl(
1904
- destination.endpoint,
1905
- 'logs',
1906
- destination.protocol,
1907
- ),
1908
- headers: destination.headers,
1909
- });
1910
-
1911
- let processor: LogRecordProcessor = new BatchLogRecordProcessor(
1912
- logExporter,
1913
- );
1914
- if (_stringRedactor) {
1915
- processor = new RedactingLogRecordProcessor(processor, _stringRedactor);
1916
- }
1917
- if (!logRecordProcessors) {
1918
- logRecordProcessors = [];
1919
- }
1920
- logRecordProcessors.push(processor);
1921
- }
1922
-
1923
- if (
1924
- otlpDestinations.some((destination) =>
1925
- destinationSupportsSignal(destination, 'logs'),
1926
- )
1927
- ) {
1928
- logger.info({}, '[autotel] OTLP log exporter configured');
1929
- }
1930
- }
1931
-
1932
- // PostHog OTLP logs integration
1933
- const posthogProcessors = buildPostHogLogProcessors(
1934
- mergedConfig.posthog,
1935
- _stringRedactor,
1936
- );
1937
- if (posthogProcessors.length > 0) {
1938
- if (!logRecordProcessors) {
1939
- logRecordProcessors = [];
1940
- }
1941
- logRecordProcessors.push(...posthogProcessors);
1942
- logger.info({}, '[autotel] PostHog OTLP logs configured');
1943
- }
1944
-
1945
- // Handle instrumentations: merge manual instrumentations with auto-instrumentations
1946
- let finalInstrumentations: NodeSDKConfiguration['instrumentations'] =
1947
- mergedConfig.instrumentations ? [...mergedConfig.instrumentations] : [];
1948
-
1949
- if (
1950
- mergedConfig.autoInstrumentations !== undefined &&
1951
- mergedConfig.autoInstrumentations !== false
1952
- ) {
1953
- // Check for ESM mode and provide guidance
1954
- const isESM = isESMMode();
1955
- if (isESM) {
1956
- logger.info(
1957
- {},
1958
- '[autotel] ESM mode detected. For auto-instrumentation to work:\n' +
1959
- ' 1. Install @opentelemetry/auto-instrumentations-node as a direct dependency\n' +
1960
- ' 2. Import autotel/register FIRST in your instrumentation file\n' +
1961
- ' 3. Use getNodeAutoInstrumentations() directly instead of autoInstrumentations\n' +
1962
- ' See: https://github.com/jagreehal/autotel#esm-setup',
1963
- );
1964
- }
1965
-
1966
- try {
1967
- // Detect manual instrumentations to avoid conflicts
1968
- const manualInstrumentationNames = getInstrumentationNames(
1969
- mergedConfig.instrumentations ?? [],
1970
- );
1971
-
1972
- // Warn if both autoInstrumentations and manual instrumentations are provided
1973
- if (manualInstrumentationNames.size > 0) {
1974
- const manualNames = [...manualInstrumentationNames].join(', ');
1975
- logger.info(
1976
- {},
1977
- `[autotel] Detected manual instrumentations (${manualNames}). ` +
1978
- 'These will take precedence over auto-instrumentations. ' +
1979
- 'Tip: Set autoInstrumentations:false if you want full manual control, or remove manual configs to use auto-instrumentations.',
1980
- );
1981
- }
1982
-
1983
- const autoInstrumentations = getAutoInstrumentations(
1984
- mergedConfig.autoInstrumentations,
1985
- manualInstrumentationNames,
1986
- );
1987
- if (autoInstrumentations && autoInstrumentations.length > 0) {
1988
- // Cast to proper type - getNodeAutoInstrumentations returns the correct type
1989
- finalInstrumentations = [
1990
- ...finalInstrumentations,
1991
- ...(autoInstrumentations as NodeSDKConfiguration['instrumentations']),
1992
- ];
1993
- }
1994
- } catch (error) {
1995
- logger.warn(
1996
- {},
1997
- `[autotel] Failed to configure auto-instrumentations: ${error instanceof Error ? error.message : String(error)}`,
1998
- );
1999
- }
2000
- }
2001
-
2002
- const autotelSampler =
2003
- mergedConfig.sampler ??
2004
- (mergedConfig.sampling
2005
- ? resolveSamplingPreset(mergedConfig.sampling)
2006
- : undefined);
2007
- if (autotelSampler) {
2008
- mergedConfig.sampler = autotelSampler;
2009
- }
2010
- const sampler: OtelSampler = autotelSampler
2011
- ? toOtelSampler(autotelSampler)
2012
- : (envConfig.otelSampler ?? toOtelSampler(samplingPresets.production()));
2013
-
2014
- const sdkOptions: Partial<NodeSDKConfiguration> = {
2015
- resource,
2016
- sampler,
2017
- instrumentations: finalInstrumentations,
2018
- };
2019
-
2020
- if (spanProcessors.length > 0) {
2021
- sdkOptions.spanProcessors = spanProcessors;
2022
- }
2023
-
2024
- if (metricReaders.length > 0) {
2025
- sdkOptions.metricReaders = metricReaders;
2026
- }
2027
-
2028
- if (logRecordProcessors && logRecordProcessors.length > 0) {
2029
- sdkOptions.logRecordProcessors = logRecordProcessors;
2030
- }
2031
-
2032
- sdk = mergedConfig.sdkFactory
2033
- ? mergedConfig.sdkFactory(sdkOptions)
2034
- : new NodeSDK(sdkOptions);
2035
-
2036
- if (!sdk) {
2037
- throw new Error('[autotel] sdkFactory must return a NodeSDK instance');
2038
- }
2039
-
2040
- sdk.start();
2041
-
2042
- // Initialize OpenLLMetry if enabled (after SDK starts to reuse tracer provider)
2043
- if (mergedConfig.openllmetry?.enabled) {
2044
- const traceloop = _optionalRequire<{
2045
- initialize?: (options?: Record<string, unknown>) => void;
2046
- }>('@traceloop/node-server-sdk');
2047
-
2048
- if (traceloop) {
2049
- const initOptions: Record<string, unknown> = {
2050
- ...mergedConfig.openllmetry.options,
2051
- };
2052
-
2053
- // Reuse autotel's tracer provider
2054
- try {
2055
- // Type assertion needed as getTracerProvider is not in the public NodeSDK interface
2056
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2057
- const tracerProvider = (sdk as any).getTracerProvider();
2058
- initOptions.tracerProvider = tracerProvider;
2059
- } catch {
2060
- // Ignore if tracer provider not available
2061
- }
2062
-
2063
- // Pass span exporter to OpenLLMetry if provided
2064
- if (configuredSpanExporters?.[0]) {
2065
- initOptions.exporter = configuredSpanExporters[0];
2066
- }
2067
-
2068
- if (typeof traceloop.initialize === 'function') {
2069
- traceloop.initialize(initOptions);
2070
- logger.info({}, '[autotel] OpenLLMetry initialized successfully');
2071
- } else {
2072
- logger.warn(
2073
- {},
2074
- '[autotel] OpenLLMetry initialize function not found. Check @traceloop/node-server-sdk version.',
2075
- );
2076
- }
2077
- } else {
2078
- logger.warn(
2079
- {},
2080
- '[autotel] OpenLLMetry enabled but @traceloop/node-server-sdk is not installed. ' +
2081
- 'Install it as a peer dependency to use OpenLLMetry integration.',
2082
- );
2083
- }
2084
- }
2085
-
2086
- initialized = true;
2087
- }
2088
-
2089
- /**
2090
- * Extract instrumentation class names from instrumentation instances
2091
- * Used to detect duplicates between manual and auto instrumentations
2092
- */
2093
- function getInstrumentationNames(
2094
- instrumentations: NodeSDKConfiguration['instrumentations'],
2095
- ): Set<string> {
2096
- const names = new Set<string>();
2097
-
2098
- if (!instrumentations) return names;
2099
-
2100
- for (const instrumentation of instrumentations) {
2101
- if (instrumentation && typeof instrumentation === 'object') {
2102
- names.add(instrumentation.constructor.name);
2103
- }
2104
- }
2105
-
2106
- return names;
2107
- }
2108
-
2109
- /**
2110
- * Map common instrumentation class names to their package names
2111
- * Used to disable auto-instrumentations when user provides manual configs
2112
- */
2113
- const INSTRUMENTATION_CLASS_TO_PACKAGE: Record<string, string> = {
2114
- HttpInstrumentation: '@opentelemetry/instrumentation-http',
2115
- HttpsInstrumentation: '@opentelemetry/instrumentation-http',
2116
- ExpressInstrumentation: '@opentelemetry/instrumentation-express',
2117
- FastifyInstrumentation: '@opentelemetry/instrumentation-fastify',
2118
- MongoDBInstrumentation: '@opentelemetry/instrumentation-mongodb',
2119
- MongooseInstrumentation: '@opentelemetry/instrumentation-mongoose',
2120
- PrismaInstrumentation: '@opentelemetry/instrumentation-prisma',
2121
- PinoInstrumentation: '@opentelemetry/instrumentation-pino',
2122
- WinstonInstrumentation: '@opentelemetry/instrumentation-winston',
2123
- RedisInstrumentation: '@opentelemetry/instrumentation-redis',
2124
- GraphQLInstrumentation: '@opentelemetry/instrumentation-graphql',
2125
- GrpcInstrumentation: '@opentelemetry/instrumentation-grpc',
2126
- IORedisInstrumentation: '@opentelemetry/instrumentation-ioredis',
2127
- KnexInstrumentation: '@opentelemetry/instrumentation-knex',
2128
- NestJsInstrumentation: '@opentelemetry/instrumentation-nestjs-core',
2129
- PgInstrumentation: '@opentelemetry/instrumentation-pg',
2130
- MySQLInstrumentation: '@opentelemetry/instrumentation-mysql',
2131
- MySQL2Instrumentation: '@opentelemetry/instrumentation-mysql2',
2132
- };
2133
-
2134
- /**
2135
- * Type for the auto-instrumentations loader function
2136
- * @internal Used for testing injection
2137
- */
2138
- export type AutoInstrumentationsLoader = (
2139
- config?: Record<string, { enabled?: boolean }>,
2140
- ) => unknown[];
2141
-
2142
- /**
2143
- * Detect if we're running in ESM mode
2144
- */
2145
- function isESMMode(): boolean {
2146
- // Check if we're in an ESM context by looking for common ESM indicators
2147
- try {
2148
- // In ESM, module.exports doesn't exist in the global scope the same way
2149
- // Also check if the package.json type is "module"
2150
- const fs = requireModule<typeof import('node:fs')>('node:fs');
2151
- try {
2152
- const pkg = JSON.parse(
2153
- fs.readFileSync(`${process.cwd()}/package.json`, 'utf8'),
2154
- );
2155
- return pkg.type === 'module';
2156
- } catch {
2157
- return false;
2158
- }
2159
- } catch {
2160
- return false;
2161
- }
2162
- }
2163
-
2164
- /**
2165
- * Lazy-load auto-instrumentations (optional peer dependency)
2166
- * Only loads when integrations config is truthy, avoiding ~40+ package imports at startup.
2167
- */
2168
- function loadNodeAutoInstrumentations(): AutoInstrumentationsLoader {
2169
- try {
2170
- const mod = requireModule<{
2171
- getNodeAutoInstrumentations: AutoInstrumentationsLoader;
2172
- }>('@opentelemetry/auto-instrumentations-node');
2173
- return mod.getNodeAutoInstrumentations;
2174
- } catch {
2175
- const isESM = isESMMode();
2176
- const baseMessage = '@opentelemetry/auto-instrumentations-node not found.';
2177
-
2178
- if (isESM) {
2179
- throw new Error(
2180
- `${baseMessage}\n\n` +
2181
- 'ESM Setup Required:\n' +
2182
- '1. Install as a direct dependency: pnpm add @opentelemetry/auto-instrumentations-node\n' +
2183
- '2. Create instrumentation.mjs with:\n' +
2184
- " import 'autotel/register'; // MUST be first!\n" +
2185
- " import { init } from 'autotel';\n" +
2186
- " import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';\n" +
2187
- ' init({ service: "my-app", instrumentations: getNodeAutoInstrumentations() });\n' +
2188
- '3. Run with: tsx --import ./instrumentation.mjs src/index.ts\n\n' +
2189
- 'See: https://github.com/jagreehal/autotel#esm-setup',
2190
- );
2191
- }
2192
-
2193
- throw new Error(
2194
- `${baseMessage} Install it: pnpm add @opentelemetry/auto-instrumentations-node`,
2195
- );
2196
- }
2197
- }
2198
-
2199
- /**
2200
- * Injectable loader for testing. Set to override the default loader.
2201
- * @internal
2202
- */
2203
- let _autoInstrumentationsLoader: (() => AutoInstrumentationsLoader) | null =
2204
- null;
2205
-
2206
- /**
2207
- * @internal Set custom loader (for testing)
2208
- */
2209
- export function _setAutoInstrumentationsLoader(
2210
- loader: (() => AutoInstrumentationsLoader) | null,
2211
- ): void {
2212
- _autoInstrumentationsLoader = loader;
2213
- }
2214
-
2215
- /**
2216
- * @internal Reset loader to default (for testing cleanup)
2217
- */
2218
- export function _resetAutoInstrumentationsLoader(): void {
2219
- _autoInstrumentationsLoader = null;
2220
- }
2221
-
2222
- /**
2223
- * Get auto-instrumentations based on simple integration names
2224
- * Excludes instrumentations that are manually provided to avoid conflicts
2225
- */
2226
- function getAutoInstrumentations(
2227
- integrations: string[] | boolean | Record<string, { enabled?: boolean }>,
2228
- manualInstrumentationNames: Set<string> = new Set(),
2229
- ): unknown[] {
2230
- if (integrations === false) {
2231
- return [];
2232
- }
2233
-
2234
- // Use injected loader if set (for testing), otherwise lazy-load
2235
- const getNodeAutoInstrumentations = _autoInstrumentationsLoader
2236
- ? _autoInstrumentationsLoader()
2237
- : loadNodeAutoInstrumentations();
2238
-
2239
- // Build exclusion config for manual instrumentations
2240
- const exclusionConfig: Record<string, { enabled: boolean }> = {};
2241
- for (const className of manualInstrumentationNames) {
2242
- const packageName = INSTRUMENTATION_CLASS_TO_PACKAGE[className];
2243
- if (packageName) {
2244
- exclusionConfig[packageName] = { enabled: false };
2245
- }
2246
- }
2247
-
2248
- if (integrations === true) {
2249
- // If exclusions exist, pass them to getNodeAutoInstrumentations
2250
- if (Object.keys(exclusionConfig).length > 0) {
2251
- return getNodeAutoInstrumentations(exclusionConfig);
2252
- }
2253
- return getNodeAutoInstrumentations();
2254
- }
2255
-
2256
- if (Array.isArray(integrations)) {
2257
- const config: Record<string, { enabled: boolean }> = { ...exclusionConfig };
2258
- for (const name of integrations) {
2259
- const packageName = `@opentelemetry/instrumentation-${name}`;
2260
- // Don't override exclusions
2261
- if (!exclusionConfig[packageName]) {
2262
- config[packageName] = { enabled: true };
2263
- }
2264
- }
2265
- return getNodeAutoInstrumentations(config);
2266
- }
2267
-
2268
- const config: Record<string, { enabled?: boolean }> = {
2269
- ...exclusionConfig,
2270
- ...integrations,
2271
- };
2272
-
2273
- // Override any integrations that conflict with manual instrumentations
2274
- for (const packageName of Object.keys(exclusionConfig)) {
2275
- const integrationsKey = Object.keys(integrations).find((key) =>
2276
- packageName.includes(key),
2277
- );
2278
- if (integrationsKey) {
2279
- // Manual instrumentation takes precedence
2280
- config[packageName] = { enabled: false };
2281
- }
2282
- }
2283
-
2284
- return getNodeAutoInstrumentations(config);
2285
- }
2286
-
2287
- /**
2288
- * Check if autotel has been initialized
2289
- */
2290
- export function isInitialized(): boolean {
2291
- return initialized;
2292
- }
2293
-
2294
- /**
2295
- * Get current config (internal use)
2296
- */
2297
- export function getConfig(): AutotelConfig | null {
2298
- return config;
2299
- }
2300
-
2301
- /**
2302
- * Get current logger (internal use)
2303
- */
2304
- export function getLogger(): Logger {
2305
- return logger;
2306
- }
2307
-
2308
- /**
2309
- * Get validation config (internal use)
2310
- */
2311
- export function getValidationConfig(): Partial<ValidationConfig> | null {
2312
- return validationConfig;
2313
- }
2314
-
2315
- /**
2316
- * Get events config (internal use)
2317
- */
2318
- export function getEventsConfig(): EventsConfig | null {
2319
- return eventsConfig;
2320
- }
2321
-
2322
- /**
2323
- * Warn once if not initialized (same behavior in all environments)
2324
- */
2325
- export function warnIfNotInitialized(context: string): void {
2326
- if (!initialized && !warnedOnce) {
2327
- logger.warn(
2328
- {},
2329
- `[autotel] ${context} used before init() called. ` +
2330
- 'Call init({ service: "..." }) first. See: https://docs.autotel.dev/quickstart',
2331
- );
2332
- warnedOnce = true;
2333
- }
2334
- }
2335
-
2336
- /**
2337
- * Get default sampler
2338
- */
2339
- export function getDefaultSampler(): Sampler {
2340
- return config?.sampler || samplingPresets.production();
2341
- }
2342
-
2343
- /**
2344
- * Auto-detect version from package.json
2345
- */
2346
- function detectVersion(): string {
2347
- try {
2348
- // Try to read package.json from cwd using fs
2349
- const fs = requireModule<typeof import('node:fs')>('node:fs');
2350
- const pkg = JSON.parse(
2351
- fs.readFileSync(`${process.cwd()}/package.json`, 'utf8'),
2352
- );
2353
- return pkg.version || '1.0.0';
2354
- } catch {
2355
- return '1.0.0';
2356
- }
2357
- }
2358
-
2359
- /**
2360
- * Detect hostname for resource attributes.
2361
- * Supports Datadog conventions (DD_HOSTNAME) and falls back to system hostname.
2362
- *
2363
- * Priority order:
2364
- * 1. DD_HOSTNAME environment variable (Datadog convention)
2365
- * 2. HOSTNAME environment variable (common Unix convention)
2366
- * 3. os.hostname() (system hostname)
2367
- *
2368
- * @returns hostname string or undefined if detection fails
2369
- */
2370
- function detectHostname(): string | undefined {
2371
- // Priority 1: DD_HOSTNAME (Datadog convention)
2372
- if (process.env.DD_HOSTNAME) {
2373
- return process.env.DD_HOSTNAME;
2374
- }
2375
-
2376
- // Priority 2: HOSTNAME (common in containers and Unix systems)
2377
- if (process.env.HOSTNAME) {
2378
- return process.env.HOSTNAME;
2379
- }
2380
-
2381
- // Priority 3: System hostname
2382
- try {
2383
- const os = requireModule<typeof import('node:os')>('node:os');
2384
- return os.hostname();
2385
- } catch {
2386
- // os module not available (edge runtime, browser, etc.)
2387
- return undefined;
2388
- }
2389
- }
2390
-
2391
- /**
2392
- * Get the string redactor configured via init({ attributeRedactor }).
2393
- * Returns null if no redactor was configured.
2394
- */
2395
- export function getStringRedactor(): StringRedactor | null {
2396
- return _stringRedactor;
2397
- }
2398
-
2399
- /**
2400
- * @internal Override optional require for deterministic tests.
2401
- */
2402
- export function _setOptionalRequireForTesting(
2403
- loader: typeof safeRequire,
2404
- ): void {
2405
- _optionalRequire = loader;
2406
- }
2407
-
2408
- /**
2409
- * @internal Reset optional require override.
2410
- */
2411
- export function _resetOptionalRequireForTesting(): void {
2412
- _optionalRequire = safeRequire;
2413
- }
2414
-
2415
- /**
2416
- * @internal Close embedded devtools if running.
2417
- */
2418
- export async function _closeEmbeddedDevtools(): Promise<void> {
2419
- if (_devtoolsClose) {
2420
- await _devtoolsClose();
2421
- _devtoolsClose = null;
2422
- }
2423
- }
2424
-
2425
- /**
2426
- * @internal Get embedded devtools close handle.
2427
- */
2428
- export function _getEmbeddedDevtoolsCloseForTesting():
2429
- | (() => Promise<void> | void)
2430
- | null {
2431
- return _devtoolsClose;
2432
- }
2433
-
2434
- /**
2435
- * Get SDK instance (for shutdown)
2436
- */
2437
- export function getSdk(): NodeSDK | null {
2438
- return sdk;
2439
- }