autotel 3.7.0 → 4.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 (132) hide show
  1. package/README.md +26 -1
  2. package/dist/{attributes-ksn4HVbd.js → attributes-CmYpdqCN.js} +2 -11
  3. package/dist/attributes-CmYpdqCN.js.map +1 -0
  4. package/dist/{attributes-D3etyRVc.cjs → attributes-PZ5doLgw.cjs} +2 -11
  5. package/dist/attributes-PZ5doLgw.cjs.map +1 -0
  6. package/dist/attributes.cjs +1 -1
  7. package/dist/attributes.d.cts +2 -2
  8. package/dist/attributes.d.ts +2 -2
  9. package/dist/attributes.js +1 -1
  10. package/dist/auto.cjs +2 -2
  11. package/dist/auto.js +1 -1
  12. package/dist/correlation-id.cjs +1 -1
  13. package/dist/correlation-id.js +1 -1
  14. package/dist/decorators.cjs +1 -1
  15. package/dist/decorators.js +1 -1
  16. package/dist/{event-Dlqr4ZNL.cjs → event-BhHREDJk.cjs} +3 -3
  17. package/dist/{event-Dlqr4ZNL.cjs.map → event-BhHREDJk.cjs.map} +1 -1
  18. package/dist/{event-_58ryBjh.js → event-ByBTV9M2.js} +3 -3
  19. package/dist/{event-_58ryBjh.js.map → event-ByBTV9M2.js.map} +1 -1
  20. package/dist/event.cjs +1 -1
  21. package/dist/event.js +1 -1
  22. package/dist/{functional-BGkT8J-h.js → functional-DtI0u4vx.js} +19 -19
  23. package/dist/functional-DtI0u4vx.js.map +1 -0
  24. package/dist/{functional-C4CzoVrX.cjs → functional-zpzNLhky.cjs} +4 -4
  25. package/dist/{functional-C4CzoVrX.cjs.map → functional-zpzNLhky.cjs.map} +1 -1
  26. package/dist/functional.cjs +1 -1
  27. package/dist/functional.js +1 -1
  28. package/dist/http.cjs +1 -1
  29. package/dist/http.js +1 -1
  30. package/dist/{index-CX0aG1Uh.d.ts → index-Ck06vlW2.d.ts} +2 -32
  31. package/dist/index-Ck06vlW2.d.ts.map +1 -0
  32. package/dist/{index-DIWZFKUS.d.cts → index-eKuioqT1.d.cts} +2 -32
  33. package/dist/index-eKuioqT1.d.cts.map +1 -0
  34. package/dist/index.cjs +7 -351
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +4 -172
  37. package/dist/index.d.cts.map +1 -1
  38. package/dist/index.d.ts +4 -172
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +9 -338
  41. package/dist/index.js.map +1 -1
  42. package/dist/{init-DJQOdVlN.d.ts → init-B7u-DjxM.d.ts} +57 -2
  43. package/dist/init-B7u-DjxM.d.ts.map +1 -0
  44. package/dist/{init-DvapOXCc.cjs → init-BX7AmFRl.cjs} +40 -21
  45. package/dist/init-BX7AmFRl.cjs.map +1 -0
  46. package/dist/{init-Ch6t7MNI.js → init-D-jnNMix.js} +39 -20
  47. package/dist/init-D-jnNMix.js.map +1 -0
  48. package/dist/{init-CNp-ee80.d.cts → init-DSrRmVnz.d.cts} +57 -2
  49. package/dist/init-DSrRmVnz.d.cts.map +1 -0
  50. package/dist/instrumentation.cjs +1 -1
  51. package/dist/instrumentation.js +1 -1
  52. package/dist/logger-D3Ej3DII.js +446 -0
  53. package/dist/logger-D3Ej3DII.js.map +1 -0
  54. package/dist/logger-thMPLpOG.cjs +487 -0
  55. package/dist/logger-thMPLpOG.cjs.map +1 -0
  56. package/dist/logger.cjs +8 -236
  57. package/dist/logger.js +2 -204
  58. package/dist/messaging.cjs +1 -1
  59. package/dist/messaging.js +1 -1
  60. package/dist/{registry-DfXA3R1L.js → registry-DVSmWg6Y.js} +2 -11
  61. package/dist/registry-DVSmWg6Y.js.map +1 -0
  62. package/dist/{registry-JZg2J3RZ.cjs → registry-DYgvb62e.cjs} +1 -16
  63. package/dist/registry-DYgvb62e.cjs.map +1 -0
  64. package/dist/semantic-conventions.cjs +1 -1
  65. package/dist/semantic-conventions.js +1 -1
  66. package/dist/semantic-helpers.cjs +1 -114
  67. package/dist/semantic-helpers.cjs.map +1 -1
  68. package/dist/semantic-helpers.d.cts +1 -114
  69. package/dist/semantic-helpers.d.cts.map +1 -1
  70. package/dist/semantic-helpers.d.ts +1 -114
  71. package/dist/semantic-helpers.d.ts.map +1 -1
  72. package/dist/semantic-helpers.js +2 -114
  73. package/dist/semantic-helpers.js.map +1 -1
  74. package/dist/{track-3HY4NGV-.cjs → track-D59FfpL0.cjs} +2 -2
  75. package/dist/{track-3HY4NGV-.cjs.map → track-D59FfpL0.cjs.map} +1 -1
  76. package/dist/{track-nsKVy-pj.js → track-wc0HafS_.js} +6 -6
  77. package/dist/track-wc0HafS_.js.map +1 -0
  78. package/dist/webhook.cjs +1 -1
  79. package/dist/webhook.js +1 -1
  80. package/dist/workflow-distributed.cjs +1 -1
  81. package/dist/workflow-distributed.js +1 -1
  82. package/dist/workflow.cjs +1 -1
  83. package/dist/workflow.js +1 -1
  84. package/dist/{yaml-config-B3dQ82GR.cjs → yaml-config-Ck2uB0Dp.cjs} +2 -1
  85. package/dist/yaml-config-Ck2uB0Dp.cjs.map +1 -0
  86. package/dist/yaml-config.cjs +1 -1
  87. package/dist/yaml-config.d.cts +7 -1
  88. package/dist/yaml-config.d.cts.map +1 -1
  89. package/dist/yaml-config.d.ts +7 -1
  90. package/dist/yaml-config.d.ts.map +1 -1
  91. package/dist/yaml-config.js +1 -0
  92. package/dist/yaml-config.js.map +1 -1
  93. package/package.json +1 -1
  94. package/skills/analyze-traces/SKILL.md +14 -12
  95. package/skills/autotel-core/SKILL.md +2 -0
  96. package/skills/autotel-instrumentation/SKILL.md +25 -0
  97. package/skills/debug-missing-spans/SKILL.md +3 -1
  98. package/skills/migrate-to-autotel/SKILL.md +24 -23
  99. package/skills/review-otel-patterns/SKILL.md +9 -6
  100. package/skills/tune-sampling/SKILL.md +8 -3
  101. package/src/attributes/builders.ts +2 -20
  102. package/src/attributes/index.ts +0 -1
  103. package/src/attributes/registry.ts +2 -9
  104. package/src/attributes/types.ts +0 -8
  105. package/src/index.ts +4 -41
  106. package/src/init.customization.test.ts +71 -0
  107. package/src/init.ts +167 -40
  108. package/src/semantic-helpers.test.ts +2 -87
  109. package/src/semantic-helpers.ts +0 -146
  110. package/src/yaml-config.test.ts +36 -0
  111. package/src/yaml-config.ts +10 -1
  112. package/dist/attributes-D3etyRVc.cjs.map +0 -1
  113. package/dist/attributes-ksn4HVbd.js.map +0 -1
  114. package/dist/functional-BGkT8J-h.js.map +0 -1
  115. package/dist/index-CX0aG1Uh.d.ts.map +0 -1
  116. package/dist/index-DIWZFKUS.d.cts.map +0 -1
  117. package/dist/init-CNp-ee80.d.cts.map +0 -1
  118. package/dist/init-Ch6t7MNI.js.map +0 -1
  119. package/dist/init-DJQOdVlN.d.ts.map +0 -1
  120. package/dist/init-DvapOXCc.cjs.map +0 -1
  121. package/dist/logger.cjs.map +0 -1
  122. package/dist/logger.js.map +0 -1
  123. package/dist/registry-DfXA3R1L.js.map +0 -1
  124. package/dist/registry-JZg2J3RZ.cjs.map +0 -1
  125. package/dist/track-nsKVy-pj.js.map +0 -1
  126. package/dist/yaml-config-B3dQ82GR.cjs.map +0 -1
  127. package/src/gen-ai-cost.test.ts +0 -81
  128. package/src/gen-ai-cost.ts +0 -145
  129. package/src/gen-ai-events.test.ts +0 -135
  130. package/src/gen-ai-events.ts +0 -208
  131. package/src/gen-ai-metrics.test.ts +0 -96
  132. package/src/gen-ai-metrics.ts +0 -128
@@ -68,7 +68,7 @@ import { init } from 'autotel';
68
68
 
69
69
  init({
70
70
  service: 'my-app',
71
- exporter: { url: process.env.OTEL_ENDPOINT! },
71
+ endpoint: process.env.OTEL_ENDPOINT!,
72
72
  });
73
73
  ```
74
74
 
@@ -78,7 +78,7 @@ init({
78
78
 
79
79
  - `useLogger().set({ … })` flattens onto the active span — no more `span.setAttribute('user.id', id)` boilerplate.
80
80
  - `attributeRedactor: 'default'` — PII masking for free in production.
81
- - `composeSpanProcessors([])` for multi-backend tee.
81
+ - `destinations: [...]` for straightforward OTLP multi-backend fan-out.
82
82
  - Cloudflare Workers + Edge support out of the box (`defineWorkerFetch`).
83
83
 
84
84
  ### What stays the same
@@ -121,20 +121,27 @@ init({
121
121
 
122
122
  ### Step 3: Decide on errors-only vs full tracing
123
123
 
124
- Sentry shines at errors; for tracing you may want to fan out to Honeycomb or Grafana Tempo. Use `composeSpanProcessors`:
124
+ Sentry shines at errors; for tracing you may want to fan out to Honeycomb or Grafana Tempo. If both targets are plain OTLP backends, prefer `destinations`:
125
125
 
126
126
  ```typescript
127
- spanProcessors: composeSpanProcessors([
128
- new BatchSpanProcessor(new SentrySpanExporter({ dsn })),
129
- new BatchSpanProcessor(
130
- new OTLPHttpJsonExporter({
131
- url: honeycombUrl,
127
+ init({
128
+ service: 'my-app',
129
+ destinations: [
130
+ {
131
+ endpoint: grafanaUrl,
132
+ headers: 'Authorization=Basic ...',
133
+ },
134
+ {
135
+ endpoint: honeycombUrl,
132
136
  headers: { 'x-honeycomb-team': key },
133
- }),
134
- ),
135
- ]);
137
+ signals: ['traces'],
138
+ },
139
+ ],
140
+ });
136
141
  ```
137
142
 
143
+ If one destination needs a non-OTLP exporter or custom filtering, drop to `composeSpanProcessors`.
144
+
138
145
  ## From Datadog APM
139
146
 
140
147
  Datadog APM uses its own format and proprietary tracer. The migration path is OTLP → Datadog OTLP intake, then sunset `dd-trace`.
@@ -150,10 +157,8 @@ import { init } from 'autotel';
150
157
 
151
158
  init({
152
159
  service: 'my-app',
153
- exporter: {
154
- url: 'https://trace.agent.datadoghq.com/api/v0.4/traces',
155
- headers: { 'dd-api-key': process.env.DD_API_KEY! },
156
- },
160
+ endpoint: 'https://trace.agent.datadoghq.com/api/v0.4/traces',
161
+ headers: { 'dd-api-key': process.env.DD_API_KEY! },
157
162
  });
158
163
  ```
159
164
 
@@ -191,10 +196,8 @@ Drop it from `start` script and `NODE_OPTIONS`.
191
196
  ```typescript
192
197
  init({
193
198
  service: 'my-app',
194
- exporter: {
195
- url: 'https://otlp.nr-data.net/v1/traces',
196
- headers: { 'api-key': process.env.NEW_RELIC_LICENSE_KEY! },
197
- },
199
+ endpoint: 'https://otlp.nr-data.net/v1/traces',
200
+ headers: { 'api-key': process.env.NEW_RELIC_LICENSE_KEY! },
198
201
  });
199
202
  ```
200
203
 
@@ -216,10 +219,8 @@ Beelines for Node was Honeycomb's pre-OTel SDK. Migration is straightforward —
216
219
  ```typescript
217
220
  init({
218
221
  service: 'my-app',
219
- exporter: {
220
- url: 'https://api.honeycomb.io/v1/traces',
221
- headers: { 'x-honeycomb-team': process.env.HONEYCOMB_API_KEY! },
222
- },
222
+ endpoint: 'https://api.honeycomb.io',
223
+ headers: { 'x-honeycomb-team': process.env.HONEYCOMB_API_KEY! },
223
224
  });
224
225
  ```
225
226
 
@@ -74,7 +74,7 @@ export function register() {
74
74
  if (process.env.NEXT_RUNTIME === 'nodejs') {
75
75
  init({
76
76
  service: 'my-app',
77
- exporter: { url: process.env.OTLP_ENDPOINT! },
77
+ endpoint: process.env.OTLP_ENDPOINT!,
78
78
  sampling: { rates: { server: 25, client: 5 } },
79
79
  attributeRedactor: 'default',
80
80
  });
@@ -201,7 +201,7 @@ export const handler = withLambda(async (event) => {
201
201
  ```typescript
202
202
  import { init, trace } from 'autotel';
203
203
 
204
- init({ service: 'my-worker', exporter: { url: process.env.OTLP_ENDPOINT! } });
204
+ init({ service: 'my-worker', endpoint: process.env.OTLP_ENDPOINT! });
205
205
 
206
206
  const processJob = trace(async (job: Job) => {
207
207
  // span auto-named after the function
@@ -219,7 +219,8 @@ All options work with `init()`, framework adapters, and `wrapModule` / `defineWo
219
219
  | Option | Type | Default | Description |
220
220
  | --------------------------------------- | --------------------------------------------------------------- | ----------------- | --------------------------------------------------------------- |
221
221
  | `service` / `service.name` | `string` | `'app'` | Service name in `service.name` resource attribute |
222
- | `exporter` | `{ url, headers?, protocol? }` | — | OTLP HTTP/JSON or HTTP/protobuf endpoint |
222
+ | `endpoint` | `string` | — | Single OTLP destination shorthand |
223
+ | `destinations` | `Array<{ endpoint, headers?, protocol?, signals? }>` | — | Declarative OTLP fan-out to multiple backends |
223
224
  | `spanProcessors` | `SpanProcessor[]` | — | Use **instead of** `exporter` for full control |
224
225
  | `sampling.rates` | `{ server?: number, client?: number, internal?: number }` | `100%` | Head sampling per span kind (0–100%) |
225
226
  | `sampling.tail` | `TailSampleFn` | — | Keep traces matching predicate (e.g. errors, slow) |
@@ -291,7 +292,7 @@ Switch backends with **no code changes** — autotel speaks OTLP HTTP/JSON and H
291
292
  | New Relic | `https://otlp.nr-data.net/v1/traces` | `{ 'api-key': '<key>' }` |
292
293
  | Local Jaeger / Tempo / Collector | `http://localhost:4318/v1/traces` | — |
293
294
 
294
- Use `init({ exporter: { url, headers } })`. Multiple destinations? Use `composeSpanProcessors([batchA, batchB])` (see Composition below).
295
+ Use `init({ endpoint, headers })` for one backend. For multiple OTLP backends, prefer `init({ destinations: [...] })`. Drop to `composeSpanProcessors([batchA, batchB])` only when you need custom processor-level control.
295
296
 
296
297
  ---
297
298
 
@@ -385,7 +386,9 @@ Compose them at build time with `composeSpanProcessors([...])` — no boilerplat
385
386
 
386
387
  ## AI SDK integration (gen-ai semantic conventions)
387
388
 
388
- autotel implements the **OTel gen-ai semantic conventions** out of the box. Token usage, tool calls, model info, latency, cost — captured as standard attributes (`gen_ai.usage.input_tokens`, `gen_ai.tool.name`, `gen_ai.response.finish_reason`, …) so any backend that understands OTel can render LLM telemetry without custom mapping.
389
+ autotel implements the **OTel gen-ai semantic conventions** out of the box. Token usage, tool calls, model info, latency, cost — captured as standard attributes (`gen_ai.usage.input_tokens`, `gen_ai.tool.name`, `gen_ai.response.finish_reasons`, …) so any backend that understands OTel can render LLM telemetry without custom mapping.
390
+
391
+ > Node.js apps get the same canonical `gen_ai.*` conventions (plus cost, metric views, and agent governance) from the `autotel-genai` package — `traceGenAI` / `recordGenAiUsage` from `autotel-genai/trace` and `genAiMetricViews` from `autotel-genai/metrics`. `withAiTelemetry` below is the edge-runtime entry point.
389
392
 
390
393
  ```typescript
391
394
  import { trace } from 'autotel';
@@ -402,7 +405,7 @@ const handler = trace(async (req) => {
402
405
  });
403
406
  ```
404
407
 
405
- Captured attributes per call: `gen_ai.system`, `gen_ai.request.model`, `gen_ai.usage.{input,output,reasoning,cache_read}_tokens`, `gen_ai.response.finish_reason`, `gen_ai.response.id`, plus per-tool spans with `gen_ai.tool.name`, `gen_ai.tool.duration`. Cost estimation comes for free if you pass a pricing map to `withAiTelemetry`.
408
+ Captured attributes per call: `gen_ai.provider.name`, `gen_ai.request.model`, `gen_ai.usage.input_tokens` / `output_tokens` / `reasoning.output_tokens` / `cache_read.input_tokens`, `gen_ai.response.finish_reasons`, `gen_ai.response.id`, plus per-tool spans with `gen_ai.tool.name`. Cost estimation (`gen_ai.usage.cost.usd`) comes for free if you pass a pricing map to `withAiTelemetry`.
406
409
 
407
410
  Anti-patterns to detect:
408
411
 
@@ -108,7 +108,12 @@ const tail = new TailSamplingProcessor({
108
108
 
109
109
  // 4. Always keep AI traces (rare + expensive — full visibility helps)
110
110
  if (
111
- trace.spans.some((s) => typeof s.attributes['gen_ai.system'] === 'string')
111
+ trace.spans.some(
112
+ (s) =>
113
+ typeof s.attributes['gen_ai.provider.name'] === 'string' ||
114
+ // legacy: third-party instrumentations may still emit gen_ai.system
115
+ typeof s.attributes['gen_ai.system'] === 'string',
116
+ )
112
117
  )
113
118
  return true;
114
119
 
@@ -142,8 +147,8 @@ keep: (trace) => {
142
147
  const cost = trace.spans.reduce(
143
148
  (acc, s) =>
144
149
  acc +
145
- (typeof s.attributes['gen_ai.cost.usd'] === 'number'
146
- ? (s.attributes['gen_ai.cost.usd'] as number)
150
+ (typeof s.attributes['gen_ai.usage.cost.usd'] === 'number'
151
+ ? (s.attributes['gen_ai.usage.cost.usd'] as number)
147
152
  : 0),
148
153
  0,
149
154
  );
@@ -38,7 +38,6 @@ import {
38
38
  FaaSAttributes,
39
39
  FeatureFlagAttributes,
40
40
  MessagingAttributes,
41
- GenAIAttributes,
42
41
  RPCAttributes,
43
42
  GraphQLAttributes,
44
43
  OTelAttributes,
@@ -477,25 +476,8 @@ export const attrs = {
477
476
  },
478
477
  },
479
478
 
480
- genAI: {
481
- system: (value: string) => ({ [GenAIAttributes.system]: value }),
482
- requestModel: (value: string) => ({
483
- [GenAIAttributes.requestModel]: value,
484
- }),
485
- responseModel: (value: string) => ({
486
- [GenAIAttributes.responseModel]: value,
487
- }),
488
- operationName: (value: 'chat' | 'completion' | 'embedding') => ({
489
- [GenAIAttributes.operationName]: value,
490
- }),
491
- usagePromptTokens: (value: number) => ({
492
- [GenAIAttributes.usagePromptTokens]: value,
493
- }),
494
- usageCompletionTokens: (value: number) => ({
495
- [GenAIAttributes.usageCompletionTokens]: value,
496
- }),
497
- provider: (value: string) => ({ [GenAIAttributes.provider]: value }),
498
- },
479
+ // GenAI/LLM attribute builders moved to the `autotel-genai` package
480
+ // (canonical `gen_ai.*` semantic conventions).
499
481
 
500
482
  rpc: {
501
483
  system: (value: string) => ({ [RPCAttributes.system]: value }),
@@ -70,7 +70,6 @@ export type {
70
70
  K8sAttrs,
71
71
  FaaSAttrs,
72
72
  ThreadAttrs,
73
- GenAIAttrs,
74
73
  RPCAttrs,
75
74
  GraphQLAttrs,
76
75
  ClientAttrs,
@@ -155,15 +155,8 @@ export const MessagingAttributes = {
155
155
  consumerGroup: 'messaging.consumer.group' as const,
156
156
  } as const;
157
157
 
158
- export const GenAIAttributes = {
159
- system: 'gen.ai.system' as const,
160
- requestModel: 'gen.ai.request.model' as const,
161
- responseModel: 'gen.ai.response.model' as const,
162
- operationName: 'gen.ai.operation.name' as const,
163
- usagePromptTokens: 'gen.ai.usage.prompt_tokens' as const,
164
- usageCompletionTokens: 'gen.ai.usage.completion_tokens' as const,
165
- provider: 'gen.ai.provider' as const,
166
- } as const;
158
+ // GenAI attribute registry moved to the `autotel-genai` package, which uses the
159
+ // canonical `gen_ai.*` namespace (these legacy `gen.ai.*` keys were non-spec).
167
160
 
168
161
  export const RPCAttributes = {
169
162
  system: 'rpc.system' as const,
@@ -155,14 +155,6 @@ export interface ThreadAttrs {
155
155
  name?: string;
156
156
  }
157
157
 
158
- export interface GenAIAttrs {
159
- system?: string;
160
- requestModel?: string;
161
- responseModel?: string;
162
- operationName?: 'chat' | 'completion' | 'embedding';
163
- provider?: string;
164
- }
165
-
166
158
  export interface RPCAttrs {
167
159
  system?: string;
168
160
  service?: string;
package/src/index.ts CHANGED
@@ -241,43 +241,9 @@ export {
241
241
  createObservableGauge,
242
242
  } from './metric-helpers';
243
243
 
244
- // LLM-tuned histogram buckets pass genAiMetricViews() to your
245
- // MeterProvider so gen_ai.* histograms have useful resolution.
246
- export {
247
- GEN_AI_DURATION_BUCKETS_SECONDS,
248
- GEN_AI_TOKEN_USAGE_BUCKETS,
249
- GEN_AI_COST_USD_BUCKETS,
250
- genAiMetricViews,
251
- llmHistogramAdvice,
252
- } from './gen-ai-metrics';
253
-
254
- // OTel GenAI span event helpers — record prompt-sent / response-received
255
- // / retry / tool-call / stream-first-token as timestamped events aligned
256
- // with the published GenAI semantic conventions.
257
- export {
258
- recordPromptSent,
259
- recordResponseReceived,
260
- recordRetry,
261
- recordToolCall,
262
- recordStreamFirstToken,
263
- type PromptSentEvent,
264
- type ResponseReceivedEvent,
265
- type RetryEvent,
266
- type ToolCallEvent,
267
- type StreamFirstTokenEvent,
268
- } from './gen-ai-events';
269
-
270
- // Per-model LLM cost estimation — estimate USD cost from token usage and
271
- // record it as the gen_ai.usage.cost.usd span attribute.
272
- export {
273
- estimateLLMCost,
274
- recordLLMCost,
275
- MODEL_PRICING,
276
- GEN_AI_COST_ATTRIBUTE,
277
- type ModelPricing,
278
- type TokenUsage,
279
- type EstimateCostOptions,
280
- } from './gen-ai-cost';
244
+ // GenAI / LLM instrumentation (cost, token usage, metric buckets, span event
245
+ // helpers, traceLLM) lives in the dedicated `autotel-genai` package — canonical
246
+ // `gen_ai.*` semantic conventions. Core stays generic and AI-free.
281
247
 
282
248
  // Tracer helpers for custom spans
283
249
  export {
@@ -303,13 +269,11 @@ export {
303
269
  getAutotelTracer,
304
270
  } from './tracer-provider';
305
271
 
306
- // Semantic convention helpers
272
+ // Semantic convention helpers (GenAI/LLM helpers moved to `autotel-genai`).
307
273
  export {
308
- traceLLM,
309
274
  traceDB,
310
275
  traceHTTP,
311
276
  traceMessaging,
312
- type LLMConfig,
313
277
  type DBConfig,
314
278
  type HTTPConfig,
315
279
  type MessagingConfig,
@@ -389,7 +353,6 @@ export {
389
353
  type K8sAttrs,
390
354
  type FaaSAttrs,
391
355
  type ThreadAttrs,
392
- type GenAIAttrs,
393
356
  type RPCAttrs,
394
357
  type GraphQLAttrs,
395
358
  type ClientAttrs,
@@ -325,6 +325,77 @@ describe('init() customization', () => {
325
325
  });
326
326
  });
327
327
 
328
+ it('supports declarative multi-destination OTLP fan-out', async () => {
329
+ const {
330
+ init,
331
+ traceExporterOptions,
332
+ metricExporterOptions,
333
+ logExporterOptions,
334
+ metricReaderOptions,
335
+ } = await loadInitWithMocks();
336
+
337
+ init({
338
+ service: 'fanout-app',
339
+ logs: true,
340
+ destinations: [
341
+ {
342
+ endpoint: 'https://otlp-gateway.grafana.net/otlp',
343
+ headers: { Authorization: 'Basic grafana' },
344
+ },
345
+ {
346
+ endpoint: 'https://api.honeycomb.io',
347
+ headers: { 'x-honeycomb-team': 'hny' },
348
+ signals: ['traces'],
349
+ },
350
+ ],
351
+ });
352
+
353
+ expect(traceExporterOptions).toHaveLength(2);
354
+ expect(traceExporterOptions[0]).toMatchObject({
355
+ url: 'https://otlp-gateway.grafana.net/otlp/v1/traces',
356
+ headers: { Authorization: 'Basic grafana' },
357
+ });
358
+ expect(traceExporterOptions[1]).toMatchObject({
359
+ url: 'https://api.honeycomb.io/v1/traces',
360
+ headers: { 'x-honeycomb-team': 'hny' },
361
+ });
362
+
363
+ expect(metricExporterOptions).toHaveLength(1);
364
+ expect(metricExporterOptions[0]).toMatchObject({
365
+ url: 'https://otlp-gateway.grafana.net/otlp/v1/metrics',
366
+ });
367
+ expect(metricReaderOptions).toHaveLength(1);
368
+
369
+ expect(logExporterOptions).toHaveLength(1);
370
+ expect(logExporterOptions[0]).toMatchObject({
371
+ url: 'https://otlp-gateway.grafana.net/otlp/v1/logs',
372
+ });
373
+ });
374
+
375
+ it('lets destinations inherit top-level protocol and headers', async () => {
376
+ const { init, traceExporterOptions, metricExporterOptions } =
377
+ await loadInitWithMocks();
378
+
379
+ init({
380
+ service: 'fanout-inherited',
381
+ protocol: 'http',
382
+ headers: 'Authorization=Bearer shared',
383
+ destinations: [
384
+ { endpoint: 'https://grafana.example.com/otlp' },
385
+ { endpoint: 'https://honeycomb.example.com' },
386
+ ],
387
+ });
388
+
389
+ expect(traceExporterOptions).toHaveLength(2);
390
+ expect(traceExporterOptions[0]).toMatchObject({
391
+ headers: { Authorization: 'Bearer shared' },
392
+ });
393
+ expect(traceExporterOptions[1]).toMatchObject({
394
+ headers: { Authorization: 'Bearer shared' },
395
+ });
396
+ expect(metricExporterOptions).toHaveLength(2);
397
+ });
398
+
328
399
  it('resolves sampling preset shorthand to a sampler instance', async () => {
329
400
  const { init, getDefaultSampler } = await loadInitWithMocks();
330
401
 
package/src/init.ts CHANGED
@@ -126,6 +126,33 @@ type OTLPExporterConfig = {
126
126
  concurrencyLimit?: number;
127
127
  };
128
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
+
129
156
  // Lazy-load gRPC exporters (optional peer dependencies)
130
157
  let OTLPTraceExporterGRPC:
131
158
  | (new (config: OTLPExporterConfig) => SpanExporter)
@@ -415,11 +442,45 @@ export interface AutotelConfig {
415
442
 
416
443
  /**
417
444
  * OTLP endpoint for traces/metrics/logs
445
+ * Single-destination shorthand. For multi-backend OTLP fan-out, use
446
+ * `destinations` instead.
418
447
  * Only used if you don't provide custom exporters/processors
419
448
  * @default process.env.OTLP_ENDPOINT || 'http://localhost:4318'
420
449
  */
421
450
  endpoint?: string;
422
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
+
423
484
  /**
424
485
  * Custom span processors for traces (supports multiple processors)
425
486
  * Allows you to use any backend: Jaeger, Zipkin, Datadog, New Relic, etc.
@@ -1406,6 +1467,45 @@ function normalizeOtlpHeaders(
1406
1467
  return parsed;
1407
1468
  }
1408
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
+
1409
1509
  /**
1410
1510
  * Initialize autotel - Write Once, Observe Everywhere
1411
1511
  *
@@ -1533,7 +1633,6 @@ export function init(cfg: AutotelConfig): void {
1533
1633
  // Initialize OpenTelemetry
1534
1634
  // Only use endpoint if explicitly configured (no default fallback)
1535
1635
  let endpoint = mergedConfig.endpoint ?? devtoolsConfig.endpoint;
1536
- const otlpHeaders = normalizeOtlpHeaders(mergedConfig.headers);
1537
1636
  const version = mergedConfig.version || detectVersion();
1538
1637
  const environment =
1539
1638
  mergedConfig.environment || process.env.NODE_ENV || 'development';
@@ -1600,8 +1699,7 @@ export function init(cfg: AutotelConfig): void {
1600
1699
  );
1601
1700
  }
1602
1701
 
1603
- // Resolve OTLP protocol (http or grpc)
1604
- const protocol = resolveProtocol(mergedConfig.protocol);
1702
+ const otlpDestinations = resolveOtlpDestinations(mergedConfig, endpoint);
1605
1703
 
1606
1704
  // Backward-compatible singular aliases. Plural forms take precedence when both are provided.
1607
1705
  const configuredSpanProcessors =
@@ -1643,16 +1741,23 @@ export function init(cfg: AutotelConfig): void {
1643
1741
  new TailSamplingSpanProcessor(new BatchSpanProcessor(exporter)),
1644
1742
  );
1645
1743
  }
1646
- } else if (endpoint) {
1647
- // Default: OTLP with tail sampling (only if endpoint is configured)
1648
- const traceExporter = createTraceExporter(protocol, {
1649
- url: formatEndpointUrl(endpoint, 'traces', protocol),
1650
- headers: otlpHeaders,
1651
- });
1652
-
1653
- spanProcessors.push(
1654
- new TailSamplingSpanProcessor(new BatchSpanProcessor(traceExporter)),
1655
- );
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
+ }
1656
1761
  }
1657
1762
  // If no endpoint and no custom processors/exporters, array remains empty
1658
1763
  // SDK will still work but won't export traces
@@ -1760,18 +1865,25 @@ export function init(cfg: AutotelConfig): void {
1760
1865
  if (configuredMetricReaders && configuredMetricReaders.length > 0) {
1761
1866
  // User provided custom metric readers
1762
1867
  metricReaders.push(...configuredMetricReaders);
1763
- } else if (metricsEnabled && endpoint) {
1764
- // Default: OTLP metrics exporter (only if endpoint is configured)
1765
- const metricExporter = createMetricExporter(protocol, {
1766
- url: formatEndpointUrl(endpoint, 'metrics', protocol),
1767
- headers: otlpHeaders,
1768
- });
1769
-
1770
- metricReaders.push(
1771
- new PeriodicExportingMetricReader({
1772
- exporter: metricExporter,
1773
- }),
1774
- );
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
+ }
1775
1887
  }
1776
1888
 
1777
1889
  let logRecordProcessors: LogRecordProcessor[] | undefined;
@@ -1782,24 +1894,39 @@ export function init(cfg: AutotelConfig): void {
1782
1894
  logRecordProcessors = [...configuredLogRecordProcessors];
1783
1895
  }
1784
1896
 
1785
- // Auto-configure OTLP log exporter when logs are enabled and endpoint is set
1786
- if (logsEnabled && endpoint) {
1787
- const logExporter = createLogExporter(protocol, {
1788
- url: formatEndpointUrl(endpoint, 'logs', protocol),
1789
- headers: otlpHeaders,
1790
- });
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
+ });
1791
1910
 
1792
- let processor: LogRecordProcessor = new BatchLogRecordProcessor(
1793
- logExporter,
1794
- );
1795
- if (_stringRedactor) {
1796
- processor = new RedactingLogRecordProcessor(processor, _stringRedactor);
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);
1797
1921
  }
1798
- if (!logRecordProcessors) {
1799
- logRecordProcessors = [];
1922
+
1923
+ if (
1924
+ otlpDestinations.some((destination) =>
1925
+ destinationSupportsSignal(destination, 'logs'),
1926
+ )
1927
+ ) {
1928
+ logger.info({}, '[autotel] OTLP log exporter configured');
1800
1929
  }
1801
- logRecordProcessors.push(processor);
1802
- logger.info({}, '[autotel] OTLP log exporter configured');
1803
1930
  }
1804
1931
 
1805
1932
  // PostHog OTLP logs integration