autotel 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1946 -0
  3. package/dist/chunk-2LNRY4QK.js +273 -0
  4. package/dist/chunk-2LNRY4QK.js.map +1 -0
  5. package/dist/chunk-3HENGDW2.js +587 -0
  6. package/dist/chunk-3HENGDW2.js.map +1 -0
  7. package/dist/chunk-4OAT42CA.cjs +73 -0
  8. package/dist/chunk-4OAT42CA.cjs.map +1 -0
  9. package/dist/chunk-5GWX5LFW.js +70 -0
  10. package/dist/chunk-5GWX5LFW.js.map +1 -0
  11. package/dist/chunk-5R2M36QB.js +195 -0
  12. package/dist/chunk-5R2M36QB.js.map +1 -0
  13. package/dist/chunk-5ZN622AO.js +73 -0
  14. package/dist/chunk-5ZN622AO.js.map +1 -0
  15. package/dist/chunk-77MSMAUQ.cjs +498 -0
  16. package/dist/chunk-77MSMAUQ.cjs.map +1 -0
  17. package/dist/chunk-ABPEQ6RK.cjs +596 -0
  18. package/dist/chunk-ABPEQ6RK.cjs.map +1 -0
  19. package/dist/chunk-BWYGJKRB.js +95 -0
  20. package/dist/chunk-BWYGJKRB.js.map +1 -0
  21. package/dist/chunk-BZHG5IZ4.js +73 -0
  22. package/dist/chunk-BZHG5IZ4.js.map +1 -0
  23. package/dist/chunk-G7VZBCD6.cjs +35 -0
  24. package/dist/chunk-G7VZBCD6.cjs.map +1 -0
  25. package/dist/chunk-GVLK7YUU.cjs +30 -0
  26. package/dist/chunk-GVLK7YUU.cjs.map +1 -0
  27. package/dist/chunk-HCCXC7XG.js +205 -0
  28. package/dist/chunk-HCCXC7XG.js.map +1 -0
  29. package/dist/chunk-HE6T6FIX.cjs +203 -0
  30. package/dist/chunk-HE6T6FIX.cjs.map +1 -0
  31. package/dist/chunk-KIXWPOCO.cjs +100 -0
  32. package/dist/chunk-KIXWPOCO.cjs.map +1 -0
  33. package/dist/chunk-KVGNW3FC.js +87 -0
  34. package/dist/chunk-KVGNW3FC.js.map +1 -0
  35. package/dist/chunk-LITNXTTT.js +3 -0
  36. package/dist/chunk-LITNXTTT.js.map +1 -0
  37. package/dist/chunk-M4ANN7RL.js +114 -0
  38. package/dist/chunk-M4ANN7RL.js.map +1 -0
  39. package/dist/chunk-NC52UBR2.cjs +32 -0
  40. package/dist/chunk-NC52UBR2.cjs.map +1 -0
  41. package/dist/chunk-NHCNRQD3.cjs +212 -0
  42. package/dist/chunk-NHCNRQD3.cjs.map +1 -0
  43. package/dist/chunk-NZ72VDNY.cjs +4 -0
  44. package/dist/chunk-NZ72VDNY.cjs.map +1 -0
  45. package/dist/chunk-P6JUDYNO.js +57 -0
  46. package/dist/chunk-P6JUDYNO.js.map +1 -0
  47. package/dist/chunk-RJYY7BWX.js +1349 -0
  48. package/dist/chunk-RJYY7BWX.js.map +1 -0
  49. package/dist/chunk-TRI4V5BF.cjs +126 -0
  50. package/dist/chunk-TRI4V5BF.cjs.map +1 -0
  51. package/dist/chunk-UL33I6IS.js +139 -0
  52. package/dist/chunk-UL33I6IS.js.map +1 -0
  53. package/dist/chunk-URRW6M2C.cjs +61 -0
  54. package/dist/chunk-URRW6M2C.cjs.map +1 -0
  55. package/dist/chunk-UY3UYPBZ.cjs +77 -0
  56. package/dist/chunk-UY3UYPBZ.cjs.map +1 -0
  57. package/dist/chunk-W3253FGB.cjs +277 -0
  58. package/dist/chunk-W3253FGB.cjs.map +1 -0
  59. package/dist/chunk-W7LHZVQF.js +26 -0
  60. package/dist/chunk-W7LHZVQF.js.map +1 -0
  61. package/dist/chunk-WBWNM6LB.cjs +1360 -0
  62. package/dist/chunk-WBWNM6LB.cjs.map +1 -0
  63. package/dist/chunk-WFJ7L2RV.js +494 -0
  64. package/dist/chunk-WFJ7L2RV.js.map +1 -0
  65. package/dist/chunk-X4RMFFMR.js +28 -0
  66. package/dist/chunk-X4RMFFMR.js.map +1 -0
  67. package/dist/chunk-Y4Y2S7BM.cjs +92 -0
  68. package/dist/chunk-Y4Y2S7BM.cjs.map +1 -0
  69. package/dist/chunk-YLPNXZFI.cjs +143 -0
  70. package/dist/chunk-YLPNXZFI.cjs.map +1 -0
  71. package/dist/chunk-YTXEZ4SD.cjs +77 -0
  72. package/dist/chunk-YTXEZ4SD.cjs.map +1 -0
  73. package/dist/chunk-Z6ZWNWWR.js +30 -0
  74. package/dist/chunk-Z6ZWNWWR.js.map +1 -0
  75. package/dist/config.cjs +26 -0
  76. package/dist/config.cjs.map +1 -0
  77. package/dist/config.d.cts +75 -0
  78. package/dist/config.d.ts +75 -0
  79. package/dist/config.js +5 -0
  80. package/dist/config.js.map +1 -0
  81. package/dist/db.cjs +233 -0
  82. package/dist/db.cjs.map +1 -0
  83. package/dist/db.d.cts +123 -0
  84. package/dist/db.d.ts +123 -0
  85. package/dist/db.js +228 -0
  86. package/dist/db.js.map +1 -0
  87. package/dist/decorators.cjs +67 -0
  88. package/dist/decorators.cjs.map +1 -0
  89. package/dist/decorators.d.cts +91 -0
  90. package/dist/decorators.d.ts +91 -0
  91. package/dist/decorators.js +65 -0
  92. package/dist/decorators.js.map +1 -0
  93. package/dist/event-subscriber.cjs +6 -0
  94. package/dist/event-subscriber.cjs.map +1 -0
  95. package/dist/event-subscriber.d.cts +116 -0
  96. package/dist/event-subscriber.d.ts +116 -0
  97. package/dist/event-subscriber.js +3 -0
  98. package/dist/event-subscriber.js.map +1 -0
  99. package/dist/event-testing.cjs +21 -0
  100. package/dist/event-testing.cjs.map +1 -0
  101. package/dist/event-testing.d.cts +110 -0
  102. package/dist/event-testing.d.ts +110 -0
  103. package/dist/event-testing.js +4 -0
  104. package/dist/event-testing.js.map +1 -0
  105. package/dist/event.cjs +30 -0
  106. package/dist/event.cjs.map +1 -0
  107. package/dist/event.d.cts +282 -0
  108. package/dist/event.d.ts +282 -0
  109. package/dist/event.js +13 -0
  110. package/dist/event.js.map +1 -0
  111. package/dist/exporters.cjs +17 -0
  112. package/dist/exporters.cjs.map +1 -0
  113. package/dist/exporters.d.cts +1 -0
  114. package/dist/exporters.d.ts +1 -0
  115. package/dist/exporters.js +4 -0
  116. package/dist/exporters.js.map +1 -0
  117. package/dist/functional.cjs +46 -0
  118. package/dist/functional.cjs.map +1 -0
  119. package/dist/functional.d.cts +478 -0
  120. package/dist/functional.d.ts +478 -0
  121. package/dist/functional.js +13 -0
  122. package/dist/functional.js.map +1 -0
  123. package/dist/http.cjs +189 -0
  124. package/dist/http.cjs.map +1 -0
  125. package/dist/http.d.cts +169 -0
  126. package/dist/http.d.ts +169 -0
  127. package/dist/http.js +184 -0
  128. package/dist/http.js.map +1 -0
  129. package/dist/index.cjs +333 -0
  130. package/dist/index.cjs.map +1 -0
  131. package/dist/index.d.cts +758 -0
  132. package/dist/index.d.ts +758 -0
  133. package/dist/index.js +143 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/instrumentation.cjs +182 -0
  136. package/dist/instrumentation.cjs.map +1 -0
  137. package/dist/instrumentation.d.cts +49 -0
  138. package/dist/instrumentation.d.ts +49 -0
  139. package/dist/instrumentation.js +179 -0
  140. package/dist/instrumentation.js.map +1 -0
  141. package/dist/logger.cjs +19 -0
  142. package/dist/logger.cjs.map +1 -0
  143. package/dist/logger.d.cts +146 -0
  144. package/dist/logger.d.ts +146 -0
  145. package/dist/logger.js +6 -0
  146. package/dist/logger.js.map +1 -0
  147. package/dist/metric-helpers.cjs +31 -0
  148. package/dist/metric-helpers.cjs.map +1 -0
  149. package/dist/metric-helpers.d.cts +13 -0
  150. package/dist/metric-helpers.d.ts +13 -0
  151. package/dist/metric-helpers.js +6 -0
  152. package/dist/metric-helpers.js.map +1 -0
  153. package/dist/metric-testing.cjs +21 -0
  154. package/dist/metric-testing.cjs.map +1 -0
  155. package/dist/metric-testing.d.cts +110 -0
  156. package/dist/metric-testing.d.ts +110 -0
  157. package/dist/metric-testing.js +4 -0
  158. package/dist/metric-testing.js.map +1 -0
  159. package/dist/metric.cjs +26 -0
  160. package/dist/metric.cjs.map +1 -0
  161. package/dist/metric.d.cts +240 -0
  162. package/dist/metric.d.ts +240 -0
  163. package/dist/metric.js +9 -0
  164. package/dist/metric.js.map +1 -0
  165. package/dist/processors.cjs +17 -0
  166. package/dist/processors.cjs.map +1 -0
  167. package/dist/processors.d.cts +1 -0
  168. package/dist/processors.d.ts +1 -0
  169. package/dist/processors.js +4 -0
  170. package/dist/processors.js.map +1 -0
  171. package/dist/sampling.cjs +40 -0
  172. package/dist/sampling.cjs.map +1 -0
  173. package/dist/sampling.d.cts +260 -0
  174. package/dist/sampling.d.ts +260 -0
  175. package/dist/sampling.js +7 -0
  176. package/dist/sampling.js.map +1 -0
  177. package/dist/semantic-helpers.cjs +35 -0
  178. package/dist/semantic-helpers.cjs.map +1 -0
  179. package/dist/semantic-helpers.d.cts +442 -0
  180. package/dist/semantic-helpers.d.ts +442 -0
  181. package/dist/semantic-helpers.js +14 -0
  182. package/dist/semantic-helpers.js.map +1 -0
  183. package/dist/tail-sampling-processor.cjs +13 -0
  184. package/dist/tail-sampling-processor.cjs.map +1 -0
  185. package/dist/tail-sampling-processor.d.cts +27 -0
  186. package/dist/tail-sampling-processor.d.ts +27 -0
  187. package/dist/tail-sampling-processor.js +4 -0
  188. package/dist/tail-sampling-processor.js.map +1 -0
  189. package/dist/testing.cjs +286 -0
  190. package/dist/testing.cjs.map +1 -0
  191. package/dist/testing.d.cts +291 -0
  192. package/dist/testing.d.ts +291 -0
  193. package/dist/testing.js +263 -0
  194. package/dist/testing.js.map +1 -0
  195. package/dist/trace-context-DRZdUvVY.d.cts +181 -0
  196. package/dist/trace-context-DRZdUvVY.d.ts +181 -0
  197. package/dist/trace-helpers.cjs +54 -0
  198. package/dist/trace-helpers.cjs.map +1 -0
  199. package/dist/trace-helpers.d.cts +524 -0
  200. package/dist/trace-helpers.d.ts +524 -0
  201. package/dist/trace-helpers.js +5 -0
  202. package/dist/trace-helpers.js.map +1 -0
  203. package/dist/tracer-provider.cjs +21 -0
  204. package/dist/tracer-provider.cjs.map +1 -0
  205. package/dist/tracer-provider.d.cts +169 -0
  206. package/dist/tracer-provider.d.ts +169 -0
  207. package/dist/tracer-provider.js +4 -0
  208. package/dist/tracer-provider.js.map +1 -0
  209. package/package.json +280 -0
  210. package/src/baggage-span-processor.test.ts +202 -0
  211. package/src/baggage-span-processor.ts +98 -0
  212. package/src/circuit-breaker.test.ts +341 -0
  213. package/src/circuit-breaker.ts +184 -0
  214. package/src/config.test.ts +94 -0
  215. package/src/config.ts +169 -0
  216. package/src/db.test.ts +252 -0
  217. package/src/db.ts +447 -0
  218. package/src/decorators.test.ts +203 -0
  219. package/src/decorators.ts +188 -0
  220. package/src/env-config.test.ts +246 -0
  221. package/src/env-config.ts +158 -0
  222. package/src/event-queue.test.ts +222 -0
  223. package/src/event-queue.ts +203 -0
  224. package/src/event-subscriber.ts +136 -0
  225. package/src/event-testing.ts +197 -0
  226. package/src/event.test.ts +718 -0
  227. package/src/event.ts +556 -0
  228. package/src/exporters.ts +96 -0
  229. package/src/functional.test.ts +1059 -0
  230. package/src/functional.ts +2295 -0
  231. package/src/http.test.ts +487 -0
  232. package/src/http.ts +424 -0
  233. package/src/index.ts +158 -0
  234. package/src/init.customization.test.ts +210 -0
  235. package/src/init.integrations.test.ts +366 -0
  236. package/src/init.openllmetry.test.ts +282 -0
  237. package/src/init.protocol.test.ts +215 -0
  238. package/src/init.ts +1426 -0
  239. package/src/instrumentation.test.ts +108 -0
  240. package/src/instrumentation.ts +308 -0
  241. package/src/logger.test.ts +117 -0
  242. package/src/logger.ts +246 -0
  243. package/src/metric-helpers.ts +47 -0
  244. package/src/metric-testing.ts +197 -0
  245. package/src/metric.ts +434 -0
  246. package/src/metrics.test.ts +205 -0
  247. package/src/operation-context.ts +93 -0
  248. package/src/processors.ts +106 -0
  249. package/src/rate-limiter.test.ts +199 -0
  250. package/src/rate-limiter.ts +98 -0
  251. package/src/sampling.test.ts +513 -0
  252. package/src/sampling.ts +428 -0
  253. package/src/semantic-helpers.test.ts +311 -0
  254. package/src/semantic-helpers.ts +584 -0
  255. package/src/shutdown.test.ts +311 -0
  256. package/src/shutdown.ts +222 -0
  257. package/src/stub.integration.test.ts +361 -0
  258. package/src/tail-sampling-processor.test.ts +226 -0
  259. package/src/tail-sampling-processor.ts +51 -0
  260. package/src/testing.ts +670 -0
  261. package/src/trace-context.ts +470 -0
  262. package/src/trace-helpers.new.test.ts +278 -0
  263. package/src/trace-helpers.test.ts +242 -0
  264. package/src/trace-helpers.ts +690 -0
  265. package/src/tracer-provider.test.ts +183 -0
  266. package/src/tracer-provider.ts +266 -0
  267. package/src/track.test.ts +153 -0
  268. package/src/track.ts +120 -0
  269. package/src/validation.test.ts +306 -0
  270. package/src/validation.ts +239 -0
  271. package/src/variable-name-inference.test.ts +178 -0
  272. package/src/variable-name-inference.ts +242 -0
@@ -0,0 +1,188 @@
1
+ /**
2
+ * TypeScript 5+ Decorators for autotel
3
+ *
4
+ * Provides @Trace decorator for class-based code.
5
+ *
6
+ * **Requires TypeScript 5.0+**
7
+ *
8
+ * @example Method decorator
9
+ * ```typescript
10
+ * import { Trace } from 'autotel/decorators'
11
+ *
12
+ * class OrderService {
13
+ * @Trace('order.create', { withMetrics: true })
14
+ * async createOrder(data: OrderData) {
15
+ * return await db.orders.create(data)
16
+ * }
17
+ *
18
+ * @Trace() // Uses method name as span name
19
+ * async processPayment(orderId: string) {
20
+ * return await stripe.charge(orderId)
21
+ * }
22
+ * }
23
+ * ```
24
+ */
25
+
26
+ import type { TracingOptions, TraceContext } from './functional';
27
+ import { getConfig } from './config';
28
+ import { SpanStatusCode } from '@opentelemetry/api';
29
+ import { createTraceContext } from './trace-context';
30
+
31
+ /**
32
+ * Options for @Trace method decorator
33
+ */
34
+ export interface TraceDecoratorOptions extends Omit<TracingOptions, 'name'> {
35
+ /**
36
+ * Custom span name. If not provided, uses the method name.
37
+ */
38
+ name?: string;
39
+ }
40
+
41
+ /**
42
+ * @Trace - Method decorator for fine-grained tracing
43
+ *
44
+ * Wraps a class method with automatic tracing. Supports both patterns:
45
+ * - Simple: method doesn't use ctx
46
+ * - Advanced: method accesses ctx via this.ctx
47
+ *
48
+ * @example Simple usage (no ctx)
49
+ * ```typescript
50
+ * class OrderService {
51
+ * @Trace()
52
+ * async createOrder(data: OrderData) {
53
+ * return await db.orders.create(data)
54
+ * }
55
+ * }
56
+ * ```
57
+ *
58
+ * @example With custom name and options
59
+ * ```typescript
60
+ * class PaymentService {
61
+ * @Trace('payment.charge', { withMetrics: true })
62
+ * async chargeCard(amount: number) {
63
+ * return await stripe.charges.create({ amount })
64
+ * }
65
+ * }
66
+ * ```
67
+ *
68
+ * @example Accessing ctx
69
+ * ```typescript
70
+ * interface WithTraceContext {
71
+ * ctx?: TraceContext
72
+ * }
73
+ *
74
+ * class UserService {
75
+ * @Trace()
76
+ * async createUser(data: UserData) {
77
+ * // Access ctx via this.ctx (available during execution)
78
+ * const ctx = (this as unknown as WithTraceContext).ctx
79
+ * if (ctx) {
80
+ * ctx.setAttribute('user.id', data.id)
81
+ * }
82
+ * return await db.users.create(data)
83
+ * }
84
+ * }
85
+ * ```
86
+ */
87
+ export function Trace(
88
+ options?: TraceDecoratorOptions,
89
+ ): <T extends (...args: unknown[]) => Promise<unknown>>(
90
+ originalMethod: T,
91
+ context: ClassMethodDecoratorContext,
92
+ ) => T;
93
+ export function Trace(
94
+ name?: string,
95
+ options?: TraceDecoratorOptions,
96
+ ): <T extends (...args: unknown[]) => Promise<unknown>>(
97
+ originalMethod: T,
98
+ context: ClassMethodDecoratorContext,
99
+ ) => T;
100
+ export function Trace(
101
+ nameOrOptions?: string | TraceDecoratorOptions,
102
+ maybeOptions?: TraceDecoratorOptions,
103
+ ): <T extends (...args: unknown[]) => Promise<unknown>>(
104
+ originalMethod: T,
105
+ context: ClassMethodDecoratorContext,
106
+ ) => T {
107
+ // Parse arguments
108
+ const name =
109
+ typeof nameOrOptions === 'string' ? nameOrOptions : nameOrOptions?.name;
110
+ // Options are used in the returned decorator function, not here
111
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
112
+ const _options: TraceDecoratorOptions =
113
+ typeof nameOrOptions === 'string'
114
+ ? maybeOptions || {}
115
+ : nameOrOptions || {};
116
+
117
+ // TypeScript 5+ decorator signature
118
+ return function <T extends (...args: unknown[]) => Promise<unknown>>(
119
+ originalMethod: T,
120
+ context: ClassMethodDecoratorContext,
121
+ ): T {
122
+ const methodName = String(context.name);
123
+
124
+ // Skip if not an async function
125
+ // Check multiple ways to detect async functions (for different transpilation environments)
126
+ // TypeScript decorators run at class definition time, so we need robust detection
127
+ const methodStr = originalMethod?.toString() || '';
128
+ const isAsync =
129
+ originalMethod &&
130
+ (originalMethod.constructor?.name === 'AsyncFunction' ||
131
+ methodStr.trim().startsWith('async ') ||
132
+ (methodStr.includes('[native code]') && methodStr.includes('async')) ||
133
+ // Fallback: if function has async in its string representation
134
+ /async\s+/.test(methodStr));
135
+
136
+ if (!isAsync) {
137
+ // Not an async function, return as-is
138
+ return originalMethod;
139
+ }
140
+
141
+ const spanName = name || methodName;
142
+
143
+ return async function <This>(
144
+ this: This,
145
+ ...args: unknown[]
146
+ ): Promise<unknown> {
147
+ const config = getConfig();
148
+ const tracer = config.tracer;
149
+
150
+ return tracer.startActiveSpan(spanName, async (span) => {
151
+ try {
152
+ // Make ctx available via this.ctx for methods that need it
153
+ const ctx: TraceContext = createTraceContext(span);
154
+
155
+ const originalCtx = (this as { ctx?: TraceContext }).ctx;
156
+ try {
157
+ (this as { ctx?: TraceContext }).ctx = ctx;
158
+ const result = await originalMethod.apply(this, args as []);
159
+ span.setStatus({ code: SpanStatusCode.OK });
160
+ return result;
161
+ } finally {
162
+ // Restore original ctx
163
+ if (originalCtx === undefined) {
164
+ delete (this as { ctx?: TraceContext }).ctx;
165
+ } else {
166
+ (this as { ctx?: TraceContext }).ctx = originalCtx;
167
+ }
168
+ }
169
+ } catch (error) {
170
+ span.setStatus({
171
+ code: SpanStatusCode.ERROR,
172
+ message: error instanceof Error ? error.message : 'Unknown error',
173
+ });
174
+ span.recordException(
175
+ error instanceof Error ? error : new Error(String(error)),
176
+ );
177
+ throw error;
178
+ } finally {
179
+ span.end();
180
+ }
181
+ });
182
+ } as T;
183
+ };
184
+ }
185
+
186
+ // Re-export types for convenience
187
+
188
+ export { type TraceContext, type TracingOptions } from './functional';
@@ -0,0 +1,246 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import {
3
+ resolveOtelEnv,
4
+ parseResourceAttributes,
5
+ parseOtlpHeaders,
6
+ envToConfig,
7
+ resolveConfigFromEnv,
8
+ } from './env-config';
9
+
10
+ describe('env-config', () => {
11
+ const originalEnv = process.env;
12
+
13
+ beforeEach(() => {
14
+ // Reset environment before each test
15
+ process.env = { ...originalEnv };
16
+ });
17
+
18
+ afterEach(() => {
19
+ // Restore original environment
20
+ process.env = originalEnv;
21
+ });
22
+
23
+ describe('resolveOtelEnv', () => {
24
+ it('should resolve standard OTEL env vars', () => {
25
+ process.env.OTEL_SERVICE_NAME = 'test-service';
26
+ process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://localhost:4318';
27
+ process.env.OTEL_EXPORTER_OTLP_PROTOCOL = 'http';
28
+
29
+ const env = resolveOtelEnv();
30
+
31
+ expect(env.OTEL_SERVICE_NAME).toBe('test-service');
32
+ expect(env.OTEL_EXPORTER_OTLP_ENDPOINT).toBe('http://localhost:4318');
33
+ expect(env.OTEL_EXPORTER_OTLP_PROTOCOL).toBe('http');
34
+ });
35
+
36
+ it('should return undefined for unset env vars', () => {
37
+ const env = resolveOtelEnv();
38
+
39
+ expect(env.OTEL_SERVICE_NAME).toBeUndefined();
40
+ expect(env.OTEL_EXPORTER_OTLP_ENDPOINT).toBeUndefined();
41
+ });
42
+
43
+ it('should validate protocol enum', () => {
44
+ process.env.OTEL_EXPORTER_OTLP_PROTOCOL = 'grpc';
45
+ const env = resolveOtelEnv();
46
+ expect(env.OTEL_EXPORTER_OTLP_PROTOCOL).toBe('grpc');
47
+ });
48
+ });
49
+
50
+ describe('parseResourceAttributes', () => {
51
+ it('should parse comma-separated key=value pairs', () => {
52
+ const input = 'service.version=1.0.0,deployment.environment=production';
53
+ const result = parseResourceAttributes(input);
54
+
55
+ expect(result).toEqual({
56
+ 'service.version': '1.0.0',
57
+ 'deployment.environment': 'production',
58
+ });
59
+ });
60
+
61
+ it('should handle single attribute', () => {
62
+ const result = parseResourceAttributes('team=backend');
63
+ expect(result).toEqual({ team: 'backend' });
64
+ });
65
+
66
+ it('should handle empty string', () => {
67
+ expect(parseResourceAttributes('')).toEqual({});
68
+ expect(parseResourceAttributes(' ')).toEqual({});
69
+ });
70
+
71
+ it('should handle undefined', () => {
72
+ expect(parseResourceAttributes()).toEqual({});
73
+ });
74
+
75
+ it('should skip invalid pairs without =', () => {
76
+ const result = parseResourceAttributes('valid=value,invalid,another=ok');
77
+ expect(result).toEqual({
78
+ valid: 'value',
79
+ another: 'ok',
80
+ });
81
+ });
82
+
83
+ it('should handle whitespace around keys and values', () => {
84
+ const result = parseResourceAttributes(' key1 = value1 , key2 = value2 ');
85
+ expect(result).toEqual({
86
+ key1: 'value1',
87
+ key2: 'value2',
88
+ });
89
+ });
90
+
91
+ it('should skip empty pairs', () => {
92
+ const result = parseResourceAttributes('key1=value1,,key2=value2');
93
+ expect(result).toEqual({
94
+ key1: 'value1',
95
+ key2: 'value2',
96
+ });
97
+ });
98
+
99
+ it('should handle values with = in them (takes first = as delimiter)', () => {
100
+ const result = parseResourceAttributes('key=value=with=equals');
101
+ expect(result).toEqual({
102
+ key: 'value=with=equals',
103
+ });
104
+ });
105
+ });
106
+
107
+ describe('parseOtlpHeaders', () => {
108
+ it('should parse comma-separated header pairs', () => {
109
+ const input = 'api-key=secret123,x-custom-header=value';
110
+ const result = parseOtlpHeaders(input);
111
+
112
+ expect(result).toEqual({
113
+ 'api-key': 'secret123',
114
+ 'x-custom-header': 'value',
115
+ });
116
+ });
117
+
118
+ it('should handle single header', () => {
119
+ const result = parseOtlpHeaders('authorization=Bearer token');
120
+ expect(result).toEqual({ authorization: 'Bearer token' });
121
+ });
122
+
123
+ it('should handle empty string', () => {
124
+ expect(parseOtlpHeaders('')).toEqual({});
125
+ expect(parseOtlpHeaders(' ')).toEqual({});
126
+ });
127
+
128
+ it('should handle undefined', () => {
129
+ expect(parseOtlpHeaders()).toEqual({});
130
+ });
131
+
132
+ it('should skip invalid pairs', () => {
133
+ const result = parseOtlpHeaders('valid=value,invalid,another=ok');
134
+ expect(result).toEqual({
135
+ valid: 'value',
136
+ another: 'ok',
137
+ });
138
+ });
139
+
140
+ it('should handle whitespace', () => {
141
+ const result = parseOtlpHeaders(' key1 = value1 , key2 = value2 ');
142
+ expect(result).toEqual({
143
+ key1: 'value1',
144
+ key2: 'value2',
145
+ });
146
+ });
147
+ });
148
+
149
+ describe('envToConfig', () => {
150
+ it('should map OTEL_SERVICE_NAME to service', () => {
151
+ const config = envToConfig({
152
+ OTEL_SERVICE_NAME: 'test-service',
153
+ });
154
+
155
+ expect(config.service).toBe('test-service');
156
+ });
157
+
158
+ it('should map OTEL_EXPORTER_OTLP_ENDPOINT to endpoint', () => {
159
+ const config = envToConfig({
160
+ OTEL_EXPORTER_OTLP_ENDPOINT: 'http://localhost:4318',
161
+ });
162
+
163
+ expect(config.endpoint).toBe('http://localhost:4318');
164
+ });
165
+
166
+ it('should map OTEL_EXPORTER_OTLP_PROTOCOL to protocol', () => {
167
+ const config = envToConfig({
168
+ OTEL_EXPORTER_OTLP_PROTOCOL: 'grpc',
169
+ });
170
+
171
+ expect(config.protocol).toBe('grpc');
172
+ });
173
+
174
+ it('should parse OTEL_EXPORTER_OTLP_HEADERS', () => {
175
+ const config = envToConfig({
176
+ OTEL_EXPORTER_OTLP_HEADERS: 'api-key=secret,x-custom=value',
177
+ });
178
+
179
+ expect(config.otlpHeaders).toEqual({
180
+ 'api-key': 'secret',
181
+ 'x-custom': 'value',
182
+ });
183
+ });
184
+
185
+ it('should parse OTEL_RESOURCE_ATTRIBUTES', () => {
186
+ const config = envToConfig({
187
+ OTEL_RESOURCE_ATTRIBUTES:
188
+ 'service.version=1.0.0,deployment.environment=prod',
189
+ });
190
+
191
+ expect(config.resourceAttributes).toEqual({
192
+ 'service.version': '1.0.0',
193
+ 'deployment.environment': 'prod',
194
+ });
195
+ });
196
+
197
+ it('should return empty config for no env vars', () => {
198
+ const config = envToConfig({});
199
+ expect(config).toEqual({});
200
+ });
201
+ });
202
+
203
+ describe('resolveConfigFromEnv', () => {
204
+ it('should return config from env vars', () => {
205
+ process.env.OTEL_SERVICE_NAME = 'test-service';
206
+ process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://localhost:4318';
207
+
208
+ const config = resolveConfigFromEnv();
209
+
210
+ expect(config.service).toBe('test-service');
211
+ expect(config.endpoint).toBe('http://localhost:4318');
212
+ });
213
+
214
+ it('should work with no env vars set', () => {
215
+ const config = resolveConfigFromEnv();
216
+ expect(config).toEqual({});
217
+ });
218
+
219
+ it('should parse complex real-world scenario', () => {
220
+ process.env.OTEL_SERVICE_NAME = 'my-api';
221
+ process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'https://api.honeycomb.io';
222
+ process.env.OTEL_EXPORTER_OTLP_HEADERS =
223
+ 'x-honeycomb-team=abc123,x-honeycomb-dataset=production';
224
+ process.env.OTEL_RESOURCE_ATTRIBUTES =
225
+ 'service.version=1.2.3,deployment.environment=production,team=backend';
226
+ process.env.OTEL_EXPORTER_OTLP_PROTOCOL = 'http';
227
+
228
+ const config = resolveConfigFromEnv();
229
+
230
+ expect(config).toEqual({
231
+ service: 'my-api',
232
+ endpoint: 'https://api.honeycomb.io',
233
+ protocol: 'http',
234
+ otlpHeaders: {
235
+ 'x-honeycomb-team': 'abc123',
236
+ 'x-honeycomb-dataset': 'production',
237
+ },
238
+ resourceAttributes: {
239
+ 'service.version': '1.2.3',
240
+ 'deployment.environment': 'production',
241
+ team: 'backend',
242
+ },
243
+ });
244
+ });
245
+ });
246
+ });
@@ -0,0 +1,158 @@
1
+ import { resolve } from 'node-env-resolver';
2
+ import { string, url, optional } from 'node-env-resolver/validators';
3
+
4
+ /**
5
+ * Standard OpenTelemetry environment variables
6
+ */
7
+ export interface OtelEnvVars {
8
+ OTEL_SERVICE_NAME?: string;
9
+ OTEL_EXPORTER_OTLP_ENDPOINT?: string;
10
+ OTEL_EXPORTER_OTLP_HEADERS?: string;
11
+ OTEL_RESOURCE_ATTRIBUTES?: string;
12
+ OTEL_EXPORTER_OTLP_PROTOCOL?: 'http' | 'grpc';
13
+ }
14
+
15
+ /**
16
+ * Parsed resource attributes as key-value pairs
17
+ */
18
+ export interface ResourceAttributes {
19
+ [key: string]: string;
20
+ }
21
+
22
+ /**
23
+ * Parsed OTLP headers as key-value pairs
24
+ */
25
+ export interface OtlpHeaders {
26
+ [key: string]: string;
27
+ }
28
+
29
+ /**
30
+ * Environment-resolved configuration (subset of AutotelConfig)
31
+ * Defined locally to avoid circular dependency with init.ts
32
+ */
33
+ export interface EnvConfig {
34
+ service?: string;
35
+ endpoint?: string;
36
+ protocol?: 'http' | 'grpc';
37
+ otlpHeaders?: Record<string, string>;
38
+ resourceAttributes?: Record<string, string>;
39
+ }
40
+
41
+ /**
42
+ * Resolve OpenTelemetry environment variables using node-env-resolver
43
+ */
44
+ export function resolveOtelEnv(): OtelEnvVars {
45
+ return resolve({
46
+ OTEL_SERVICE_NAME: string({ optional: true }),
47
+ OTEL_EXPORTER_OTLP_ENDPOINT: url({ optional: true }),
48
+ OTEL_EXPORTER_OTLP_HEADERS: string({ optional: true }),
49
+ OTEL_RESOURCE_ATTRIBUTES: string({ optional: true }),
50
+ OTEL_EXPORTER_OTLP_PROTOCOL: optional(['http', 'grpc'] as const),
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Parse OTEL_RESOURCE_ATTRIBUTES from comma-separated key=value pairs
56
+ * Example: "service.version=1.0.0,deployment.environment=production"
57
+ */
58
+ export function parseResourceAttributes(
59
+ input: string | undefined,
60
+ ): ResourceAttributes {
61
+ if (!input || input.trim() === '') {
62
+ return {};
63
+ }
64
+
65
+ const attributes: ResourceAttributes = {};
66
+ const pairs = input.split(',');
67
+
68
+ for (const pair of pairs) {
69
+ const trimmedPair = pair.trim();
70
+ if (!trimmedPair) continue;
71
+
72
+ const equalIndex = trimmedPair.indexOf('=');
73
+ if (equalIndex === -1) {
74
+ // Invalid format, skip this pair
75
+ continue;
76
+ }
77
+
78
+ const key = trimmedPair.slice(0, equalIndex).trim();
79
+ const value = trimmedPair.slice(equalIndex + 1).trim();
80
+
81
+ if (key && value) {
82
+ attributes[key] = value;
83
+ }
84
+ }
85
+
86
+ return attributes;
87
+ }
88
+
89
+ /**
90
+ * Parse OTEL_EXPORTER_OTLP_HEADERS from comma-separated key=value pairs
91
+ * Example: "api-key=secret123,x-custom-header=value"
92
+ */
93
+ export function parseOtlpHeaders(input: string | undefined): OtlpHeaders {
94
+ if (!input || input.trim() === '') {
95
+ return {};
96
+ }
97
+
98
+ const headers: OtlpHeaders = {};
99
+ const pairs = input.split(',');
100
+
101
+ for (const pair of pairs) {
102
+ const trimmedPair = pair.trim();
103
+ if (!trimmedPair) continue;
104
+
105
+ const equalIndex = trimmedPair.indexOf('=');
106
+ if (equalIndex === -1) {
107
+ // Invalid format, skip this pair
108
+ continue;
109
+ }
110
+
111
+ const key = trimmedPair.slice(0, equalIndex).trim();
112
+ const value = trimmedPair.slice(equalIndex + 1).trim();
113
+
114
+ if (key && value) {
115
+ headers[key] = value;
116
+ }
117
+ }
118
+
119
+ return headers;
120
+ }
121
+
122
+ /**
123
+ * Convert resolved environment variables to config
124
+ */
125
+ export function envToConfig(env: OtelEnvVars): EnvConfig {
126
+ const config: EnvConfig = {};
127
+
128
+ if (env.OTEL_SERVICE_NAME) {
129
+ config.service = env.OTEL_SERVICE_NAME;
130
+ }
131
+
132
+ if (env.OTEL_EXPORTER_OTLP_ENDPOINT) {
133
+ config.endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT;
134
+ }
135
+
136
+ if (env.OTEL_EXPORTER_OTLP_PROTOCOL) {
137
+ config.protocol = env.OTEL_EXPORTER_OTLP_PROTOCOL;
138
+ }
139
+
140
+ if (env.OTEL_EXPORTER_OTLP_HEADERS) {
141
+ config.otlpHeaders = parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS);
142
+ }
143
+
144
+ const resourceAttrs = parseResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES);
145
+ if (Object.keys(resourceAttrs).length > 0) {
146
+ config.resourceAttributes = resourceAttrs;
147
+ }
148
+
149
+ return config;
150
+ }
151
+
152
+ /**
153
+ * Main function to resolve config from environment variables
154
+ */
155
+ export function resolveConfigFromEnv(): EnvConfig {
156
+ const env = resolveOtelEnv();
157
+ return envToConfig(env);
158
+ }