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.
- package/README.md +26 -1
- package/dist/{attributes-ksn4HVbd.js → attributes-CmYpdqCN.js} +2 -11
- package/dist/attributes-CmYpdqCN.js.map +1 -0
- package/dist/{attributes-D3etyRVc.cjs → attributes-PZ5doLgw.cjs} +2 -11
- package/dist/attributes-PZ5doLgw.cjs.map +1 -0
- package/dist/attributes.cjs +1 -1
- package/dist/attributes.d.cts +2 -2
- package/dist/attributes.d.ts +2 -2
- package/dist/attributes.js +1 -1
- package/dist/auto.cjs +2 -2
- package/dist/auto.js +1 -1
- package/dist/correlation-id.cjs +1 -1
- package/dist/correlation-id.js +1 -1
- package/dist/decorators.cjs +1 -1
- package/dist/decorators.js +1 -1
- package/dist/{event-Dlqr4ZNL.cjs → event-BhHREDJk.cjs} +3 -3
- package/dist/{event-Dlqr4ZNL.cjs.map → event-BhHREDJk.cjs.map} +1 -1
- package/dist/{event-_58ryBjh.js → event-ByBTV9M2.js} +3 -3
- package/dist/{event-_58ryBjh.js.map → event-ByBTV9M2.js.map} +1 -1
- package/dist/event.cjs +1 -1
- package/dist/event.js +1 -1
- package/dist/{functional-BGkT8J-h.js → functional-DtI0u4vx.js} +19 -19
- package/dist/functional-DtI0u4vx.js.map +1 -0
- package/dist/{functional-C4CzoVrX.cjs → functional-zpzNLhky.cjs} +4 -4
- package/dist/{functional-C4CzoVrX.cjs.map → functional-zpzNLhky.cjs.map} +1 -1
- package/dist/functional.cjs +1 -1
- package/dist/functional.js +1 -1
- package/dist/http.cjs +1 -1
- package/dist/http.js +1 -1
- package/dist/{index-CX0aG1Uh.d.ts → index-Ck06vlW2.d.ts} +2 -32
- package/dist/index-Ck06vlW2.d.ts.map +1 -0
- package/dist/{index-DIWZFKUS.d.cts → index-eKuioqT1.d.cts} +2 -32
- package/dist/index-eKuioqT1.d.cts.map +1 -0
- package/dist/index.cjs +7 -351
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -172
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +4 -172
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -338
- package/dist/index.js.map +1 -1
- package/dist/{init-DJQOdVlN.d.ts → init-B7u-DjxM.d.ts} +57 -2
- package/dist/init-B7u-DjxM.d.ts.map +1 -0
- package/dist/{init-DvapOXCc.cjs → init-BX7AmFRl.cjs} +40 -21
- package/dist/init-BX7AmFRl.cjs.map +1 -0
- package/dist/{init-Ch6t7MNI.js → init-D-jnNMix.js} +39 -20
- package/dist/init-D-jnNMix.js.map +1 -0
- package/dist/{init-CNp-ee80.d.cts → init-DSrRmVnz.d.cts} +57 -2
- package/dist/init-DSrRmVnz.d.cts.map +1 -0
- package/dist/instrumentation.cjs +1 -1
- package/dist/instrumentation.js +1 -1
- package/dist/logger-D3Ej3DII.js +446 -0
- package/dist/logger-D3Ej3DII.js.map +1 -0
- package/dist/logger-thMPLpOG.cjs +487 -0
- package/dist/logger-thMPLpOG.cjs.map +1 -0
- package/dist/logger.cjs +8 -236
- package/dist/logger.js +2 -204
- package/dist/messaging.cjs +1 -1
- package/dist/messaging.js +1 -1
- package/dist/{registry-DfXA3R1L.js → registry-DVSmWg6Y.js} +2 -11
- package/dist/registry-DVSmWg6Y.js.map +1 -0
- package/dist/{registry-JZg2J3RZ.cjs → registry-DYgvb62e.cjs} +1 -16
- package/dist/registry-DYgvb62e.cjs.map +1 -0
- package/dist/semantic-conventions.cjs +1 -1
- package/dist/semantic-conventions.js +1 -1
- package/dist/semantic-helpers.cjs +1 -114
- package/dist/semantic-helpers.cjs.map +1 -1
- package/dist/semantic-helpers.d.cts +1 -114
- package/dist/semantic-helpers.d.cts.map +1 -1
- package/dist/semantic-helpers.d.ts +1 -114
- package/dist/semantic-helpers.d.ts.map +1 -1
- package/dist/semantic-helpers.js +2 -114
- package/dist/semantic-helpers.js.map +1 -1
- package/dist/{track-3HY4NGV-.cjs → track-D59FfpL0.cjs} +2 -2
- package/dist/{track-3HY4NGV-.cjs.map → track-D59FfpL0.cjs.map} +1 -1
- package/dist/{track-nsKVy-pj.js → track-wc0HafS_.js} +6 -6
- package/dist/track-wc0HafS_.js.map +1 -0
- package/dist/webhook.cjs +1 -1
- package/dist/webhook.js +1 -1
- package/dist/workflow-distributed.cjs +1 -1
- package/dist/workflow-distributed.js +1 -1
- package/dist/workflow.cjs +1 -1
- package/dist/workflow.js +1 -1
- package/dist/{yaml-config-B3dQ82GR.cjs → yaml-config-Ck2uB0Dp.cjs} +2 -1
- package/dist/yaml-config-Ck2uB0Dp.cjs.map +1 -0
- package/dist/yaml-config.cjs +1 -1
- package/dist/yaml-config.d.cts +7 -1
- package/dist/yaml-config.d.cts.map +1 -1
- package/dist/yaml-config.d.ts +7 -1
- package/dist/yaml-config.d.ts.map +1 -1
- package/dist/yaml-config.js +1 -0
- package/dist/yaml-config.js.map +1 -1
- package/package.json +1 -1
- package/skills/analyze-traces/SKILL.md +14 -12
- package/skills/autotel-core/SKILL.md +2 -0
- package/skills/autotel-instrumentation/SKILL.md +25 -0
- package/skills/debug-missing-spans/SKILL.md +3 -1
- package/skills/migrate-to-autotel/SKILL.md +24 -23
- package/skills/review-otel-patterns/SKILL.md +9 -6
- package/skills/tune-sampling/SKILL.md +8 -3
- package/src/attributes/builders.ts +2 -20
- package/src/attributes/index.ts +0 -1
- package/src/attributes/registry.ts +2 -9
- package/src/attributes/types.ts +0 -8
- package/src/index.ts +4 -41
- package/src/init.customization.test.ts +71 -0
- package/src/init.ts +167 -40
- package/src/semantic-helpers.test.ts +2 -87
- package/src/semantic-helpers.ts +0 -146
- package/src/yaml-config.test.ts +36 -0
- package/src/yaml-config.ts +10 -1
- package/dist/attributes-D3etyRVc.cjs.map +0 -1
- package/dist/attributes-ksn4HVbd.js.map +0 -1
- package/dist/functional-BGkT8J-h.js.map +0 -1
- package/dist/index-CX0aG1Uh.d.ts.map +0 -1
- package/dist/index-DIWZFKUS.d.cts.map +0 -1
- package/dist/init-CNp-ee80.d.cts.map +0 -1
- package/dist/init-Ch6t7MNI.js.map +0 -1
- package/dist/init-DJQOdVlN.d.ts.map +0 -1
- package/dist/init-DvapOXCc.cjs.map +0 -1
- package/dist/logger.cjs.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/registry-DfXA3R1L.js.map +0 -1
- package/dist/registry-JZg2J3RZ.cjs.map +0 -1
- package/dist/track-nsKVy-pj.js.map +0 -1
- package/dist/yaml-config-B3dQ82GR.cjs.map +0 -1
- package/src/gen-ai-cost.test.ts +0 -81
- package/src/gen-ai-cost.ts +0 -145
- package/src/gen-ai-events.test.ts +0 -135
- package/src/gen-ai-events.ts +0 -208
- package/src/gen-ai-metrics.test.ts +0 -96
- 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
|
-
|
|
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
|
-
- `
|
|
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.
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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',
|
|
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
|
-
| `
|
|
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({
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
481
|
-
|
|
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 }),
|
package/src/attributes/index.ts
CHANGED
|
@@ -155,15 +155,8 @@ export const MessagingAttributes = {
|
|
|
155
155
|
consumerGroup: 'messaging.consumer.group' as const,
|
|
156
156
|
} as const;
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
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,
|
package/src/attributes/types.ts
CHANGED
|
@@ -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
|
|
245
|
-
//
|
|
246
|
-
|
|
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
|
-
|
|
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
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
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
|
|
1786
|
-
if (logsEnabled
|
|
1787
|
-
const
|
|
1788
|
-
|
|
1789
|
-
|
|
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
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
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
|
-
|
|
1799
|
-
|
|
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
|