@telemetry-dev/eve 0.1.0 → 0.1.2
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 +59 -18
- package/dist/index.d.mts +16 -3
- package/dist/index.mjs +178 -77
- package/package.json +4 -4
- package/src/client.ts +163 -62
- package/src/config.ts +31 -7
- package/src/hook.ts +43 -55
- package/src/index.ts +1 -0
- package/src/instrumentation.ts +12 -9
- package/src/otel.ts +34 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ lifecycle-hook, and HTTP-client surfaces.
|
|
|
10
10
|
npm install @telemetry-dev/eve eve
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Requires `eve >=0.
|
|
13
|
+
Requires `eve >=0.47.0 <1` and Node.js 24 or newer.
|
|
14
14
|
|
|
15
15
|
## Environment
|
|
16
16
|
|
|
@@ -93,17 +93,21 @@ const client = wrapEveClient(new Client({ host: "http://127.0.0.1:3000" }), {
|
|
|
93
93
|
agentName: "support-agent",
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
const session = client.
|
|
97
|
-
|
|
96
|
+
const { response, session } = await client.sessions.create({
|
|
97
|
+
message: "Summarize this incident.",
|
|
98
|
+
});
|
|
98
99
|
const result = await response.result();
|
|
100
|
+
const followUp = await session.send("What do we do next?");
|
|
99
101
|
```
|
|
100
102
|
|
|
101
|
-
`wrapEveClient()`
|
|
102
|
-
records the outgoing message or HITL
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
`wrapEveClient()` makes one caller-side `invoke_agent` span for each turn: `sessions.create()`,
|
|
104
|
+
`ClientSession.send()`, and `ClientSession.respond()`. It records the outgoing message or the HITL
|
|
105
|
+
input responses as span input, sets `gen_ai.conversation.id` from
|
|
106
|
+
the session state or Eve's response session id (so all turns of one session share one trace), streams
|
|
107
|
+
TTFT from the first message/reasoning delta, accumulates usage from `step.completed`, records final
|
|
108
|
+
message/result output, and marks failures/cancellations as errors.
|
|
105
109
|
|
|
106
|
-
`ClientSession.stream()`, `Client.info()`, and `Client.health()` are
|
|
110
|
+
`ClientSession.stream()`, `sessions.attach()`, `Client.info()`, and `Client.health()` are not spanned.
|
|
107
111
|
Attach-streams are unbounded and the probe routes are not agent turns.
|
|
108
112
|
|
|
109
113
|
## Trace shape
|
|
@@ -120,25 +124,62 @@ A typical turn contains:
|
|
|
120
124
|
context and any custom runtime context you return from `step.started`.
|
|
121
125
|
- Lifecycle logs joined to the same session via `gen_ai.conversation.id`.
|
|
122
126
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
`spanFilter: () => true` to export
|
|
127
|
+
By default, only AI spans are exported: tracer scopes `eve`, `eve.agent`, `gen_ai`, and this SDK. Spans from
|
|
128
|
+
other libraries on the same global tracer provider (for example better-auth, Nitro, or eve's
|
|
129
|
+
`workflow` engine) are dropped. Pass `spanFilter: () => true` to export every span.
|
|
126
130
|
|
|
127
131
|
## Content capture
|
|
128
132
|
|
|
129
|
-
Server-side model content
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
Server-side model content depends on Eve's trace policy and the destination capture settings.
|
|
134
|
+
`telemetryDevInstrumentation()` maps `captureInput` / `captureOutput` to `recordInputs` /
|
|
135
|
+
`recordOutputs`. The defaults are `true`, but these flags cannot override Eve's trace policy.
|
|
136
|
+
In Eve 0.47.0 and 0.50.0, the default policy records content only for a `public` audience,
|
|
137
|
+
even if `EVE_DEV=1`. HTTP clients, web chat, and schedules have an `unknown` audience.
|
|
138
|
+
|
|
139
|
+
For approved local content capture, use Eve's provider layout.
|
|
140
|
+
Remove `agent/instrumentation.ts`.
|
|
141
|
+
Set `experimental.instrumentationProviders` to `true` in the Eve configuration.
|
|
142
|
+
Add these two files:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// agent/instrumentation/otel.ts
|
|
146
|
+
import { otel } from "eve/instrumentation/otel";
|
|
147
|
+
|
|
148
|
+
export default otel({ tracePolicy: () => true });
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// agent/instrumentation/telemetry-dev.ts
|
|
153
|
+
import { telemetryDevOtelIntegration } from "@telemetry-dev/eve";
|
|
154
|
+
|
|
155
|
+
export default telemetryDevOtelIntegration();
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
This policy selects Eve's audience-aware capture.
|
|
159
|
+
For local `unknown`-audience channels, run `eve dev` or set `EVE_DEV=1` in the Eve server process.
|
|
160
|
+
Without `EVE_DEV=1`, local unknown-audience channels export metadata only.
|
|
161
|
+
Local private channels always export metadata only.
|
|
162
|
+
Destination settings and forwarded parent policies can decrease content capture.
|
|
163
|
+
|
|
164
|
+
The legacy single-file `telemetryDevInstrumentation()` API has no `tracePolicy` option.
|
|
165
|
+
Thus, it cannot record model content for new unknown-audience sessions on these Eve versions.
|
|
166
|
+
The single-file layout remains suitable for metadata-only traces.
|
|
167
|
+
Do not combine the two layouts.
|
|
168
|
+
This package cannot restore content that Eve removed.
|
|
169
|
+
|
|
170
|
+
Caller-side wrapper content follows the telemetry.dev SDK `captureInput` / `captureOutput`
|
|
171
|
+
settings. The SDK mask and truncation options apply to captured client-wrapper inputs, outputs,
|
|
172
|
+
and logs.
|
|
132
173
|
|
|
133
174
|
## Limitations
|
|
134
175
|
|
|
135
176
|
- Pass options to exactly one entry point in a process. Initialization is one-shot so duplicate entry
|
|
136
177
|
points do not replace the first configuration.
|
|
137
178
|
- `ClientSession.stream()` is not spanned.
|
|
138
|
-
-
|
|
139
|
-
|
|
140
|
-
- The client wrapper
|
|
141
|
-
|
|
179
|
+
- The client wrapper exports a total cost only after an error-free terminal event. Each completed step must report a finite, non-negative `usage.costUsd` value, including zero. Otherwise, the wrapper omits the total but still records token usage. Model information is also necessary for server-side pricing.
|
|
180
|
+
- When caller and server spans share a trace and matching Eve turn metadata, the caller cost replaces the server's main model-call costs. Tool and subagent costs stay separate. Without that match, the two reported totals stay unchanged.
|
|
181
|
+
- The client wrapper ends its `invoke_agent` span after stream use or an abort signal. Without these actions, the span stays open.
|
|
182
|
+
- Eve 0.50 gives `telemetryDevOtelIntegration()` no tracer-provider or parent-context hook. Thus, Eve provider spans cannot join the deterministic session trace.
|
|
142
183
|
- Inline subagent child streams (`subagent.event`) are not fanned out in full; the hook logs
|
|
143
184
|
`subagent.started`, `subagent.called`, `subagent.completed`, and child failure events.
|
|
144
185
|
- Eve workflow `$eve.*` run tags are a Vercel-dashboard surface and are not visible to OpenTelemetry,
|
package/dist/index.d.mts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { ClientOverrides, ClientOverrides as ClientOverrides$1, TelemetryOptions } from "@telemetry-dev/sdk";
|
|
1
|
+
import { ClientOverrides, ClientOverrides as ClientOverrides$1, TelemetryOptions, TelemetrySpanProcessorOptions } from "@telemetry-dev/sdk";
|
|
2
2
|
import { Client } from "eve/client";
|
|
3
|
+
import { OtelIntegration } from "eve/instrumentation/otel";
|
|
3
4
|
import { HookDefinition } from "eve/hooks";
|
|
4
5
|
import { InstrumentationDefinition, InstrumentationEvents, InstrumentationRuntimeContext } from "eve/instrumentation";
|
|
5
6
|
|
|
6
7
|
//#region src/config.d.ts
|
|
7
8
|
/** Options accepted by every @telemetry-dev/eve entry point. */
|
|
8
|
-
type TelemetryDevEveOptions = Omit<TelemetryOptions, "registerGlobal">;
|
|
9
|
+
type TelemetryDevEveOptions = Omit<TelemetryOptions, "registerGlobal" | "sdkName">;
|
|
9
10
|
//#endregion
|
|
10
11
|
//#region src/client.d.ts
|
|
11
12
|
interface WrapEveClientOptions extends TelemetryDevEveOptions {
|
|
@@ -34,4 +35,16 @@ interface TelemetryDevInstrumentationOptions extends TelemetryDevEveOptions {
|
|
|
34
35
|
}
|
|
35
36
|
declare function telemetryDevInstrumentation(options?: TelemetryDevInstrumentationOptions, overrides?: ClientOverrides$1): InstrumentationDefinition;
|
|
36
37
|
//#endregion
|
|
37
|
-
|
|
38
|
+
//#region src/otel.d.ts
|
|
39
|
+
interface TelemetryDevOtelIntegrationOptions extends Omit<TelemetrySpanProcessorOptions, "spanExporter"> {
|
|
40
|
+
recordInputs?: boolean;
|
|
41
|
+
recordOutputs?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* telemetry.dev as a destination in eve's `agent/instrumentation/` provider layout
|
|
45
|
+
* (`experimental.instrumentationProviders`). Export it from one file in that directory.
|
|
46
|
+
* eve owns the tracer provider there, so this attaches a processor instead of registering one.
|
|
47
|
+
*/
|
|
48
|
+
declare function telemetryDevOtelIntegration(options?: TelemetryDevOtelIntegrationOptions): OtelIntegration;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { type TelemetryDevEveOptions, type TelemetryDevInstrumentationOptions, type TelemetryDevOtelIntegrationOptions, type WrapEveClientOptions, telemetryDevHook, telemetryDevInstrumentation, telemetryDevOtelIntegration, wrapEveClient };
|
package/dist/index.mjs
CHANGED
|
@@ -1,36 +1,49 @@
|
|
|
1
|
-
import { init, log, startSpan } from "@telemetry-dev/sdk";
|
|
1
|
+
import { SCOPE_NAME, TelemetrySpanProcessor, init, log, startSpan } from "@telemetry-dev/sdk";
|
|
2
2
|
import { MessageResponse, isCurrentTurnBoundaryEvent } from "eve/client";
|
|
3
|
+
import { otelIntegration } from "eve/instrumentation/otel";
|
|
3
4
|
//#region src/config.ts
|
|
4
5
|
let initialized = false;
|
|
6
|
+
const AI_SCOPES = {
|
|
7
|
+
eve: true,
|
|
8
|
+
"eve.agent": true,
|
|
9
|
+
gen_ai: true,
|
|
10
|
+
[SCOPE_NAME]: true
|
|
11
|
+
};
|
|
12
|
+
const isAiScope = (scope) => Object.hasOwn(AI_SCOPES, scope);
|
|
5
13
|
/**
|
|
6
14
|
* Initializes the telemetry.dev SDK exactly once per process for this integration.
|
|
7
|
-
* registerGlobal:true lets eve's tracers
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
15
|
+
* registerGlobal:true lets eve's tracers resolve to the SDK provider. spanFilter defaults to
|
|
16
|
+
* exporting only AI scopes (see AI_SCOPES); pass spanFilter: () => true to export every span.
|
|
17
|
+
* eve starts each `ai.eve.turn` under a workflow-engine span of its own trace, so the turn is
|
|
18
|
+
* re-rooted under the session parent and every turn of a session lands in one trace.
|
|
11
19
|
*/
|
|
12
20
|
function ensureInit(options = {}, overrides) {
|
|
13
21
|
if (initialized) return;
|
|
14
22
|
initialized = true;
|
|
15
23
|
init({
|
|
16
24
|
...options,
|
|
25
|
+
sdkName: "@telemetry-dev/eve",
|
|
17
26
|
registerGlobal: true,
|
|
18
|
-
spanFilter: options.spanFilter ?? ((span) => span.instrumentationScope.name
|
|
27
|
+
spanFilter: options.spanFilter ?? ((span) => isAiScope(span.instrumentationScope.name)),
|
|
28
|
+
sessionRootOf: (name, attributes) => {
|
|
29
|
+
const sessionId = attributes["eve.session.id"];
|
|
30
|
+
return name === "ai.eve.turn" && typeof sessionId === "string" ? sessionId : void 0;
|
|
31
|
+
}
|
|
19
32
|
}, overrides);
|
|
20
33
|
}
|
|
21
34
|
//#endregion
|
|
22
35
|
//#region src/client.ts
|
|
23
36
|
function asRecord$1(value) {
|
|
24
|
-
if (value === null ||
|
|
37
|
+
if (value === null || Array.isArray(value) || !(value instanceof Object)) return void 0;
|
|
25
38
|
return value;
|
|
26
39
|
}
|
|
27
40
|
function stringField$1(record, key) {
|
|
28
41
|
const value = record?.[key];
|
|
29
|
-
return
|
|
42
|
+
return value?.constructor === String && value.length > 0 ? value : void 0;
|
|
30
43
|
}
|
|
31
44
|
function numberField$1(record, key) {
|
|
32
45
|
const value = record?.[key];
|
|
33
|
-
return
|
|
46
|
+
return value?.constructor === Number ? value : void 0;
|
|
34
47
|
}
|
|
35
48
|
function eventData$1(event) {
|
|
36
49
|
if (!("data" in event)) return {};
|
|
@@ -51,12 +64,9 @@ function failureError(code, message) {
|
|
|
51
64
|
if (code) error.name = code;
|
|
52
65
|
return error;
|
|
53
66
|
}
|
|
54
|
-
function inputForSpan(payload) {
|
|
55
|
-
return payload.message ?? payload.inputResponses;
|
|
56
|
-
}
|
|
57
67
|
function bindOrReturn(target, prop) {
|
|
58
|
-
const value =
|
|
59
|
-
return
|
|
68
|
+
const value = target[prop];
|
|
69
|
+
return value instanceof Function ? value.bind(target) : value;
|
|
60
70
|
}
|
|
61
71
|
function addLifecycleEvent(span, event) {
|
|
62
72
|
const data = eventData$1(event);
|
|
@@ -84,16 +94,21 @@ function addLifecycleEvent(span, event) {
|
|
|
84
94
|
default: return;
|
|
85
95
|
}
|
|
86
96
|
}
|
|
87
|
-
async function* instrumentedStream(response, span, start,
|
|
97
|
+
async function* instrumentedStream(response, span, start, signal, endCancelled) {
|
|
88
98
|
const usage = {};
|
|
89
99
|
let sawUsage = false;
|
|
90
100
|
let output;
|
|
91
101
|
let finishReason;
|
|
102
|
+
let costUsd;
|
|
103
|
+
let allCostsKnown = true;
|
|
92
104
|
let timeToFirstChunkMs;
|
|
93
105
|
let error;
|
|
106
|
+
let done = false;
|
|
94
107
|
let sawTerminalEvent = false;
|
|
95
108
|
const finish = () => {
|
|
96
|
-
if (
|
|
109
|
+
if (done) return;
|
|
110
|
+
done = true;
|
|
111
|
+
if (!sawTerminalEvent && error === void 0) if (signal?.aborted) {
|
|
97
112
|
error = failureError("cancelled", "cancelled");
|
|
98
113
|
finishReason ??= "cancelled";
|
|
99
114
|
} else {
|
|
@@ -103,14 +118,25 @@ async function* instrumentedStream(response, span, start, payload) {
|
|
|
103
118
|
span.end({
|
|
104
119
|
usage: sawUsage ? usage : void 0,
|
|
105
120
|
finishReason,
|
|
121
|
+
costUsd: sawTerminalEvent && error === void 0 && allCostsKnown && Number.isFinite(costUsd) ? costUsd : void 0,
|
|
106
122
|
output,
|
|
107
123
|
timeToFirstChunkMs,
|
|
108
|
-
error
|
|
124
|
+
error: error instanceof Error ? error : error === void 0 ? void 0 : /* @__PURE__ */ new Error("error")
|
|
109
125
|
});
|
|
110
126
|
};
|
|
111
127
|
try {
|
|
112
128
|
for await (const event of response) {
|
|
113
129
|
const data = eventData$1(event);
|
|
130
|
+
if (event.type === "turn.started") {
|
|
131
|
+
const turnTrace = asRecord$1(data.trace);
|
|
132
|
+
const traceId = stringField$1(turnTrace, "traceId");
|
|
133
|
+
const spanId = stringField$1(turnTrace, "spanId");
|
|
134
|
+
const turnId = stringField$1(data, "turnId");
|
|
135
|
+
if (traceId && spanId && turnId) {
|
|
136
|
+
span.span.setAttribute("td.eve.turn_root", `${traceId}/${spanId}`);
|
|
137
|
+
span.span.setAttribute("eve.turn.id", turnId);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
114
140
|
if (isCurrentTurnBoundaryEvent(event)) sawTerminalEvent = true;
|
|
115
141
|
if (timeToFirstChunkMs === void 0 && (event.type === "message.appended" || event.type === "reasoning.appended")) timeToFirstChunkMs = performance.now() - start;
|
|
116
142
|
if (event.type === "step.completed") {
|
|
@@ -119,6 +145,9 @@ async function* instrumentedStream(response, span, start, payload) {
|
|
|
119
145
|
sawUsage = addUsage(usage, "outputTokens", numberField$1(eventUsage, "outputTokens")) || sawUsage;
|
|
120
146
|
sawUsage = addUsage(usage, "cacheReadInputTokens", numberField$1(eventUsage, "cacheReadTokens")) || sawUsage;
|
|
121
147
|
sawUsage = addUsage(usage, "cacheCreationInputTokens", numberField$1(eventUsage, "cacheWriteTokens")) || sawUsage;
|
|
148
|
+
const stepCost = numberField$1(eventUsage, "costUsd");
|
|
149
|
+
if (stepCost === void 0 || !Number.isFinite(stepCost) || stepCost < 0) allCostsKnown = false;
|
|
150
|
+
else costUsd = (costUsd ?? 0) + stepCost;
|
|
122
151
|
finishReason = stringField$1(data, "finishReason") ?? finishReason;
|
|
123
152
|
}
|
|
124
153
|
if (event.type === "message.completed" && stringField$1(data, "finishReason") !== "tool-calls") output = data.message;
|
|
@@ -131,49 +160,104 @@ async function* instrumentedStream(response, span, start, payload) {
|
|
|
131
160
|
}
|
|
132
161
|
finishReason = "error";
|
|
133
162
|
}
|
|
163
|
+
if (event.type === "turn.cancelled") {
|
|
164
|
+
error = failureError("cancelled", "cancelled");
|
|
165
|
+
finishReason = "cancelled";
|
|
166
|
+
}
|
|
134
167
|
addLifecycleEvent(span, event);
|
|
135
168
|
yield event;
|
|
136
169
|
}
|
|
137
170
|
} catch (caught) {
|
|
138
|
-
if (
|
|
171
|
+
if (signal?.aborted) {
|
|
139
172
|
error = failureError("cancelled", "cancelled");
|
|
140
173
|
finishReason ??= "cancelled";
|
|
141
|
-
} else error = caught;
|
|
174
|
+
} else error = caught instanceof Error ? caught : new Error(String(caught));
|
|
142
175
|
throw caught;
|
|
143
176
|
} finally {
|
|
144
177
|
finish();
|
|
178
|
+
if (endCancelled) signal?.removeEventListener("abort", endCancelled);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function tracedTurn(options, input, turnOptions, knownSessionId, send) {
|
|
182
|
+
const startTime = /* @__PURE__ */ new Date();
|
|
183
|
+
const start = performance.now();
|
|
184
|
+
const signal = turnOptions?.signal;
|
|
185
|
+
const spanFor = (sessionId) => startSpan(options.spanName ?? "invoke_agent", {
|
|
186
|
+
type: "agent",
|
|
187
|
+
agentName: options.agentName,
|
|
188
|
+
input,
|
|
189
|
+
startTime,
|
|
190
|
+
attributes: sessionId === void 0 ? void 0 : { "gen_ai.conversation.id": sessionId }
|
|
191
|
+
});
|
|
192
|
+
let response;
|
|
193
|
+
try {
|
|
194
|
+
response = await send();
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const span = spanFor(knownSessionId);
|
|
197
|
+
if (signal?.aborted) span.end({
|
|
198
|
+
error: failureError("cancelled", "cancelled"),
|
|
199
|
+
finishReason: "cancelled"
|
|
200
|
+
});
|
|
201
|
+
else span.end({ error: error instanceof Error ? error : new Error(String(error)) });
|
|
202
|
+
throw error;
|
|
145
203
|
}
|
|
204
|
+
const span = spanFor(response.sessionId);
|
|
205
|
+
let streamStarted = false;
|
|
206
|
+
let endedBeforeStream = false;
|
|
207
|
+
const endCancelled = () => {
|
|
208
|
+
if (streamStarted) return;
|
|
209
|
+
endedBeforeStream = true;
|
|
210
|
+
span.end({
|
|
211
|
+
error: failureError("cancelled", "cancelled"),
|
|
212
|
+
finishReason: "cancelled"
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
if (signal?.aborted) endCancelled();
|
|
216
|
+
else signal?.addEventListener("abort", endCancelled, { once: true });
|
|
217
|
+
return new MessageResponse({
|
|
218
|
+
cancelTurn: () => response.cancel(),
|
|
219
|
+
createStream: () => {
|
|
220
|
+
streamStarted = true;
|
|
221
|
+
if (endedBeforeStream) return (async function* () {
|
|
222
|
+
yield* response;
|
|
223
|
+
})();
|
|
224
|
+
return instrumentedStream(response, span, start, signal, endCancelled);
|
|
225
|
+
},
|
|
226
|
+
sessionId: response.sessionId
|
|
227
|
+
});
|
|
146
228
|
}
|
|
147
229
|
function wrapSession(session, options) {
|
|
148
230
|
return new Proxy(session, { get(target, prop) {
|
|
149
231
|
if (prop === "send") {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
createStream: () => instrumentedStream(response, span, start, payload)
|
|
174
|
-
});
|
|
232
|
+
const send = (message, turnOptions) => tracedTurn(options, message, turnOptions, target.state.sessionId, () => target.send(message, turnOptions));
|
|
233
|
+
return send;
|
|
234
|
+
}
|
|
235
|
+
if (prop === "respond") {
|
|
236
|
+
const respond = (inputResponses, turnOptions) => tracedTurn(options, inputResponses, turnOptions, target.state.sessionId, () => target.respond(inputResponses, turnOptions));
|
|
237
|
+
return respond;
|
|
238
|
+
}
|
|
239
|
+
return bindOrReturn(target, prop);
|
|
240
|
+
} });
|
|
241
|
+
}
|
|
242
|
+
function wrapSessions(sessions, options) {
|
|
243
|
+
return new Proxy(sessions, { get(target, prop) {
|
|
244
|
+
if (prop === "create") {
|
|
245
|
+
const create = async (input) => {
|
|
246
|
+
let session;
|
|
247
|
+
return {
|
|
248
|
+
response: await tracedTurn(options, input.message, input, void 0, async () => {
|
|
249
|
+
const created = await target.create(input);
|
|
250
|
+
session = created.session;
|
|
251
|
+
return created.response;
|
|
252
|
+
}),
|
|
253
|
+
session: wrapSession(session, options)
|
|
254
|
+
};
|
|
175
255
|
};
|
|
176
|
-
return
|
|
256
|
+
return create;
|
|
257
|
+
}
|
|
258
|
+
if (prop === "attach") {
|
|
259
|
+
const attach = (sessionId, attachOptions) => wrapSession(target.attach(sessionId, attachOptions), options);
|
|
260
|
+
return attach;
|
|
177
261
|
}
|
|
178
262
|
return bindOrReturn(target, prop);
|
|
179
263
|
} });
|
|
@@ -181,15 +265,16 @@ function wrapSession(session, options) {
|
|
|
181
265
|
function wrapEveClient(client, options = {}, overrides) {
|
|
182
266
|
const { agentName: _agentName, spanName: _spanName, ...sdkOptions } = options;
|
|
183
267
|
ensureInit(sdkOptions, overrides);
|
|
268
|
+
const sessions = wrapSessions(client.sessions, options);
|
|
184
269
|
return new Proxy(client, { get(target, prop) {
|
|
185
|
-
if (prop === "
|
|
270
|
+
if (prop === "sessions") return sessions;
|
|
186
271
|
return bindOrReturn(target, prop);
|
|
187
272
|
} });
|
|
188
273
|
}
|
|
189
274
|
//#endregion
|
|
190
275
|
//#region src/hook.ts
|
|
191
276
|
function asRecord(value) {
|
|
192
|
-
if (value === null ||
|
|
277
|
+
if (value === null || Array.isArray(value) || !(value instanceof Object)) return void 0;
|
|
193
278
|
return value;
|
|
194
279
|
}
|
|
195
280
|
function eventData(event) {
|
|
@@ -198,11 +283,11 @@ function eventData(event) {
|
|
|
198
283
|
}
|
|
199
284
|
function stringField(record, key) {
|
|
200
285
|
const value = record?.[key];
|
|
201
|
-
return
|
|
286
|
+
return value?.constructor === String && value.length > 0 ? value : void 0;
|
|
202
287
|
}
|
|
203
288
|
function numberField(record, key) {
|
|
204
289
|
const value = record?.[key];
|
|
205
|
-
return
|
|
290
|
+
return value?.constructor === Number ? value : void 0;
|
|
206
291
|
}
|
|
207
292
|
function jsonField(record, key) {
|
|
208
293
|
const value = record?.[key];
|
|
@@ -239,7 +324,7 @@ function reportError$1(onError, error) {
|
|
|
239
324
|
} catch {}
|
|
240
325
|
}
|
|
241
326
|
function emit(event, ctx, level, message, attributes = {}, baseEvent = event) {
|
|
242
|
-
const at = event.meta
|
|
327
|
+
const at = event.meta.at;
|
|
243
328
|
log(message, {
|
|
244
329
|
level,
|
|
245
330
|
eventName: event.type,
|
|
@@ -263,10 +348,11 @@ function logEvent(event, ctx) {
|
|
|
263
348
|
case "session.started": {
|
|
264
349
|
const runtime = asRecord(data.runtime);
|
|
265
350
|
const invocation = asRecord(data.invocation);
|
|
351
|
+
const trace = asRecord(data.trace);
|
|
266
352
|
emit(event, ctx, "info", "Session started", {
|
|
267
353
|
"eve.version": stringField(runtime, "eveVersion"),
|
|
268
|
-
"gen_ai.request.model": stringField(runtime, "modelId"),
|
|
269
354
|
"eve.agent.id": stringField(runtime, "agentId"),
|
|
355
|
+
"eve.trace.id": stringField(trace, "traceId"),
|
|
270
356
|
"eve.parent.session_id": stringField(invocation, "parentSessionId"),
|
|
271
357
|
"eve.parent.call_id": stringField(invocation, "parentCallId"),
|
|
272
358
|
"eve.parent.turn_id": stringField(invocation, "parentTurnId"),
|
|
@@ -280,6 +366,9 @@ function logEvent(event, ctx) {
|
|
|
280
366
|
case "message.received":
|
|
281
367
|
emit(event, ctx, "debug", "User message received");
|
|
282
368
|
return;
|
|
369
|
+
case "step.started":
|
|
370
|
+
emit(event, ctx, "debug", "Step started", { "gen_ai.request.model": stringField(data, "modelId") });
|
|
371
|
+
return;
|
|
283
372
|
case "step.completed": {
|
|
284
373
|
const usage = asRecord(data.usage);
|
|
285
374
|
emit(event, ctx, "info", stepCompletedMessage(data), {
|
|
@@ -335,27 +424,15 @@ function logEvent(event, ctx) {
|
|
|
335
424
|
"eve.subagent.name": stringField(data, "subagentName"),
|
|
336
425
|
"gen_ai.tool.call.id": stringField(data, "callId")
|
|
337
426
|
};
|
|
338
|
-
if (childType === "step.failed") {
|
|
339
|
-
emit(event, ctx, "error", `Subagent step failed: ${stringField(childData, "message") ?? ""}`, {
|
|
340
|
-
...attrs,
|
|
341
|
-
"error.code": stringField(childData, "code"),
|
|
342
|
-
"eve.error.details": jsonField(childData, "details")
|
|
343
|
-
}, child);
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
if (childType === "turn.failed") {
|
|
347
|
-
emit(event, ctx, "error", `Subagent turn failed: ${stringField(childData, "message") ?? ""}`, {
|
|
348
|
-
...attrs,
|
|
349
|
-
"error.code": stringField(childData, "code"),
|
|
350
|
-
"eve.error.details": jsonField(childData, "details")
|
|
351
|
-
}, child);
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
if (childType === "session.failed") emit(event, ctx, "error", `Subagent session failed: ${stringField(childData, "message") ?? ""}`, {
|
|
427
|
+
if (childType === "step.failed" || childType === "turn.failed" || childType === "session.failed") emit(event, ctx, "error", `Subagent ${childType.replace(".", " ")}: ${stringField(childData, "message") ?? ""}`, {
|
|
355
428
|
...attrs,
|
|
356
429
|
"error.code": stringField(childData, "code"),
|
|
357
430
|
"eve.error.details": jsonField(childData, "details")
|
|
358
|
-
},
|
|
431
|
+
}, {
|
|
432
|
+
...child,
|
|
433
|
+
data: childData,
|
|
434
|
+
type: childType
|
|
435
|
+
});
|
|
359
436
|
return;
|
|
360
437
|
}
|
|
361
438
|
case "subagent.called": {
|
|
@@ -388,6 +465,9 @@ function logEvent(event, ctx) {
|
|
|
388
465
|
case "turn.completed":
|
|
389
466
|
emit(event, ctx, "info", "Turn completed");
|
|
390
467
|
return;
|
|
468
|
+
case "turn.cancelled":
|
|
469
|
+
emit(event, ctx, "warn", "Turn cancelled");
|
|
470
|
+
return;
|
|
391
471
|
case "turn.failed":
|
|
392
472
|
emit(event, ctx, "error", `Turn failed: ${stringField(data, "message") ?? ""}`, {
|
|
393
473
|
"error.code": stringField(data, "code"),
|
|
@@ -415,13 +495,13 @@ function telemetryDevHook(options = {}, overrides) {
|
|
|
415
495
|
ensureInit(options, overrides);
|
|
416
496
|
logEvent(event, ctx);
|
|
417
497
|
} catch (error) {
|
|
418
|
-
reportError$1(options.onError, error);
|
|
498
|
+
reportError$1(options.onError, error instanceof Error ? error : new Error(String(error)));
|
|
419
499
|
}
|
|
420
500
|
} } };
|
|
421
501
|
}
|
|
422
502
|
//#endregion
|
|
423
503
|
//#region src/instrumentation.ts
|
|
424
|
-
const envServiceName = () =>
|
|
504
|
+
const envServiceName = () => globalThis.process !== void 0 ? process.env.OTEL_SERVICE_NAME : void 0;
|
|
425
505
|
function reportError(onError, error) {
|
|
426
506
|
try {
|
|
427
507
|
onError?.(error);
|
|
@@ -439,20 +519,23 @@ function telemetryDevInstrumentation(options = {}, overrides) {
|
|
|
439
519
|
serviceName: sdkOptions.serviceName ?? envServiceName() ?? agentName
|
|
440
520
|
}, overrides);
|
|
441
521
|
} catch (error) {
|
|
442
|
-
reportError(sdkOptions.onError, error);
|
|
522
|
+
reportError(sdkOptions.onError, error instanceof Error ? error : new Error(String(error)));
|
|
443
523
|
}
|
|
444
524
|
},
|
|
445
525
|
events: { "step.started"(input) {
|
|
446
526
|
const ctx = { ...runtimeContext };
|
|
447
527
|
const userId = input.session.auth.initiator?.principalId ?? input.session.auth.current?.principalId;
|
|
448
|
-
|
|
528
|
+
const runtimeContextWithUser = userId ? {
|
|
529
|
+
...ctx,
|
|
530
|
+
"user.id": userId
|
|
531
|
+
} : ctx;
|
|
449
532
|
try {
|
|
450
533
|
const userResult = stepStarted?.(input);
|
|
451
|
-
if (userResult?.runtimeContext) Object.assign(
|
|
534
|
+
if (userResult?.runtimeContext) Object.assign(runtimeContextWithUser, userResult.runtimeContext);
|
|
452
535
|
} catch (error) {
|
|
453
|
-
reportError(sdkOptions.onError, error);
|
|
536
|
+
reportError(sdkOptions.onError, error instanceof Error ? error : new Error(String(error)));
|
|
454
537
|
}
|
|
455
|
-
return Object.keys(
|
|
538
|
+
return Object.keys(runtimeContextWithUser).length > 0 ? { runtimeContext: runtimeContextWithUser } : void 0;
|
|
456
539
|
} }
|
|
457
540
|
};
|
|
458
541
|
if (functionId !== void 0) return {
|
|
@@ -462,4 +545,22 @@ function telemetryDevInstrumentation(options = {}, overrides) {
|
|
|
462
545
|
return definition;
|
|
463
546
|
}
|
|
464
547
|
//#endregion
|
|
465
|
-
|
|
548
|
+
//#region src/otel.ts
|
|
549
|
+
/**
|
|
550
|
+
* telemetry.dev as a destination in eve's `agent/instrumentation/` provider layout
|
|
551
|
+
* (`experimental.instrumentationProviders`). Export it from one file in that directory.
|
|
552
|
+
* eve owns the tracer provider there, so this attaches a processor instead of registering one.
|
|
553
|
+
*/
|
|
554
|
+
function telemetryDevOtelIntegration(options = {}) {
|
|
555
|
+
const { recordInputs, recordOutputs, ...processorOptions } = options;
|
|
556
|
+
return otelIntegration({
|
|
557
|
+
recordInputs,
|
|
558
|
+
recordOutputs,
|
|
559
|
+
spanProcessors: [new TelemetrySpanProcessor({
|
|
560
|
+
...processorOptions,
|
|
561
|
+
spanFilter: processorOptions.spanFilter ?? ((span) => isAiScope(span.instrumentationScope.name))
|
|
562
|
+
})]
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
//#endregion
|
|
566
|
+
export { telemetryDevHook, telemetryDevInstrumentation, telemetryDevOtelIntegration, wrapEveClient };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telemetry-dev/eve",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Eve (Vercel agent framework) telemetry integration for telemetry.dev.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@opentelemetry/api": "^1.9.1",
|
|
40
|
-
"@telemetry-dev/sdk": "^0.1.
|
|
40
|
+
"@telemetry-dev/sdk": "^0.1.2"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@opentelemetry/sdk-logs": "^0.218.0",
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
"@types/node": "^25.5.0",
|
|
46
46
|
"@typescript/native-preview": "7.0.0-dev.20260328.1",
|
|
47
47
|
"ai": "7.0.0",
|
|
48
|
-
"eve": "0.
|
|
48
|
+
"eve": "0.50.0",
|
|
49
49
|
"typescript": "^6.0.2",
|
|
50
50
|
"vite-plus": "0.1.20",
|
|
51
51
|
"vitest": "npm:@voidzero-dev/vite-plus-test@0.1.20",
|
|
52
52
|
"zod": "^4.4.3"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"eve": ">=0.
|
|
55
|
+
"eve": ">=0.47.0 <1"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "pnpm exec vp pack",
|
package/src/client.ts
CHANGED
|
@@ -8,10 +8,11 @@ import {
|
|
|
8
8
|
import type {
|
|
9
9
|
Client,
|
|
10
10
|
ClientSession,
|
|
11
|
-
|
|
11
|
+
ClientSessions,
|
|
12
|
+
InputResponse,
|
|
13
|
+
MessageStreamEvent,
|
|
12
14
|
SendTurnInput,
|
|
13
|
-
|
|
14
|
-
SessionState,
|
|
15
|
+
SendTurnOptions,
|
|
15
16
|
} from "eve/client";
|
|
16
17
|
import { isCurrentTurnBoundaryEvent, MessageResponse } from "eve/client";
|
|
17
18
|
|
|
@@ -24,24 +25,33 @@ export interface WrapEveClientOptions extends TelemetryDevEveOptions {
|
|
|
24
25
|
spanName?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
type EventRecord = { [key: string]: EventValue };
|
|
29
|
+
type EventValue =
|
|
30
|
+
| string
|
|
31
|
+
| number
|
|
32
|
+
| boolean
|
|
33
|
+
| null
|
|
34
|
+
| undefined
|
|
35
|
+
| readonly EventValue[]
|
|
36
|
+
| EventRecord;
|
|
37
|
+
function asRecord(value: EventValue): EventRecord | undefined {
|
|
38
|
+
if (value === null || Array.isArray(value) || !(value instanceof Object)) return undefined;
|
|
39
|
+
return value as EventRecord;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
|
-
function stringField(record:
|
|
42
|
+
function stringField(record: EventRecord | undefined, key: string): string | undefined {
|
|
33
43
|
const value = record?.[key];
|
|
34
|
-
return
|
|
44
|
+
return value?.constructor === String && value.length > 0 ? value : undefined;
|
|
35
45
|
}
|
|
36
46
|
|
|
37
|
-
function numberField(record:
|
|
47
|
+
function numberField(record: EventRecord | undefined, key: string): number | undefined {
|
|
38
48
|
const value = record?.[key];
|
|
39
|
-
return
|
|
49
|
+
return value?.constructor === Number ? value : undefined;
|
|
40
50
|
}
|
|
41
51
|
|
|
42
|
-
function eventData(event:
|
|
52
|
+
function eventData(event: MessageStreamEvent): EventRecord {
|
|
43
53
|
if (!("data" in event)) return {};
|
|
44
|
-
return asRecord(event.data) ?? {};
|
|
54
|
+
return asRecord(event.data as EventValue) ?? {};
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
function eventAttributes(
|
|
@@ -66,16 +76,12 @@ function failureError(code: string | undefined, message: string | undefined): Er
|
|
|
66
76
|
return error;
|
|
67
77
|
}
|
|
68
78
|
|
|
69
|
-
function
|
|
70
|
-
|
|
79
|
+
function bindOrReturn<T extends object>(target: T, prop: string | symbol) {
|
|
80
|
+
const value = target[prop as keyof T];
|
|
81
|
+
return value instanceof Function ? value.bind(target) : value;
|
|
71
82
|
}
|
|
72
83
|
|
|
73
|
-
function
|
|
74
|
-
const value = Reflect.get(target, prop, target);
|
|
75
|
-
return typeof value === "function" ? value.bind(target) : value;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function addLifecycleEvent(span: SpanHandle, event: HandleMessageStreamEvent): void {
|
|
84
|
+
function addLifecycleEvent(span: SpanHandle, event: MessageStreamEvent): void {
|
|
79
85
|
const data = eventData(event);
|
|
80
86
|
switch (event.type) {
|
|
81
87
|
case "subagent.called": {
|
|
@@ -116,19 +122,25 @@ async function* instrumentedStream<TOutput>(
|
|
|
116
122
|
response: MessageResponse<TOutput>,
|
|
117
123
|
span: SpanHandle,
|
|
118
124
|
start: number,
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
signal: AbortSignal | undefined,
|
|
126
|
+
endCancelled: (() => void) | undefined,
|
|
127
|
+
): AsyncGenerator<MessageStreamEvent> {
|
|
121
128
|
const usage: TokenUsage = {};
|
|
122
129
|
let sawUsage = false;
|
|
123
130
|
let output: unknown;
|
|
124
131
|
let finishReason: string | undefined;
|
|
132
|
+
let costUsd: number | undefined;
|
|
133
|
+
let allCostsKnown = true;
|
|
125
134
|
let timeToFirstChunkMs: number | undefined;
|
|
126
135
|
let error: unknown;
|
|
136
|
+
let done = false;
|
|
127
137
|
let sawTerminalEvent = false;
|
|
128
138
|
|
|
129
139
|
const finish = (): void => {
|
|
140
|
+
if (done) return;
|
|
141
|
+
done = true;
|
|
130
142
|
if (!sawTerminalEvent && error === undefined) {
|
|
131
|
-
if (
|
|
143
|
+
if (signal?.aborted) {
|
|
132
144
|
error = failureError("cancelled", "cancelled");
|
|
133
145
|
finishReason ??= "cancelled";
|
|
134
146
|
} else {
|
|
@@ -139,15 +151,29 @@ async function* instrumentedStream<TOutput>(
|
|
|
139
151
|
span.end({
|
|
140
152
|
usage: sawUsage ? usage : undefined,
|
|
141
153
|
finishReason,
|
|
154
|
+
costUsd:
|
|
155
|
+
sawTerminalEvent && error === undefined && allCostsKnown && Number.isFinite(costUsd)
|
|
156
|
+
? costUsd
|
|
157
|
+
: undefined,
|
|
142
158
|
output,
|
|
143
159
|
timeToFirstChunkMs,
|
|
144
|
-
error,
|
|
160
|
+
error: error instanceof Error ? error : error === undefined ? undefined : new Error("error"),
|
|
145
161
|
});
|
|
146
162
|
};
|
|
147
163
|
|
|
148
164
|
try {
|
|
149
165
|
for await (const event of response) {
|
|
150
166
|
const data = eventData(event);
|
|
167
|
+
if (event.type === "turn.started") {
|
|
168
|
+
const turnTrace = asRecord(data.trace);
|
|
169
|
+
const traceId = stringField(turnTrace, "traceId");
|
|
170
|
+
const spanId = stringField(turnTrace, "spanId");
|
|
171
|
+
const turnId = stringField(data, "turnId");
|
|
172
|
+
if (traceId && spanId && turnId) {
|
|
173
|
+
span.span.setAttribute("td.eve.turn_root", `${traceId}/${spanId}`);
|
|
174
|
+
span.span.setAttribute("eve.turn.id", turnId);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
151
177
|
if (isCurrentTurnBoundaryEvent(event)) sawTerminalEvent = true;
|
|
152
178
|
if (
|
|
153
179
|
timeToFirstChunkMs === undefined &&
|
|
@@ -171,6 +197,12 @@ async function* instrumentedStream<TOutput>(
|
|
|
171
197
|
"cacheCreationInputTokens",
|
|
172
198
|
numberField(eventUsage, "cacheWriteTokens"),
|
|
173
199
|
) || sawUsage;
|
|
200
|
+
const stepCost = numberField(eventUsage, "costUsd");
|
|
201
|
+
if (stepCost === undefined || !Number.isFinite(stepCost) || stepCost < 0) {
|
|
202
|
+
allCostsKnown = false;
|
|
203
|
+
} else {
|
|
204
|
+
costUsd = (costUsd ?? 0) + stepCost;
|
|
205
|
+
}
|
|
174
206
|
finishReason = stringField(data, "finishReason") ?? finishReason;
|
|
175
207
|
}
|
|
176
208
|
|
|
@@ -195,63 +227,134 @@ async function* instrumentedStream<TOutput>(
|
|
|
195
227
|
}
|
|
196
228
|
finishReason = "error";
|
|
197
229
|
}
|
|
230
|
+
if (event.type === "turn.cancelled") {
|
|
231
|
+
error = failureError("cancelled", "cancelled");
|
|
232
|
+
finishReason = "cancelled";
|
|
233
|
+
}
|
|
198
234
|
|
|
199
235
|
addLifecycleEvent(span, event);
|
|
200
236
|
yield event;
|
|
201
237
|
}
|
|
202
238
|
} catch (caught) {
|
|
203
|
-
if (
|
|
239
|
+
if (signal?.aborted) {
|
|
204
240
|
error = failureError("cancelled", "cancelled");
|
|
205
241
|
finishReason ??= "cancelled";
|
|
206
242
|
} else {
|
|
207
|
-
error = caught;
|
|
243
|
+
error = caught instanceof Error ? caught : new Error(String(caught));
|
|
208
244
|
}
|
|
209
245
|
throw caught;
|
|
210
246
|
} finally {
|
|
211
247
|
finish();
|
|
248
|
+
if (endCancelled) signal?.removeEventListener("abort", endCancelled);
|
|
212
249
|
}
|
|
213
250
|
}
|
|
214
251
|
|
|
252
|
+
async function tracedTurn<TOutput>(
|
|
253
|
+
options: WrapEveClientOptions,
|
|
254
|
+
input: SendTurnInput["message"] | readonly InputResponse[],
|
|
255
|
+
turnOptions: SendTurnOptions<TOutput> | undefined,
|
|
256
|
+
knownSessionId: string | undefined,
|
|
257
|
+
send: () => Promise<MessageResponse<TOutput>>,
|
|
258
|
+
): Promise<MessageResponse<TOutput>> {
|
|
259
|
+
// The session id is only known after the POST (first turn), so the span starts once the
|
|
260
|
+
// request resolves with the original start time; that lets it join the session trace.
|
|
261
|
+
const startTime = new Date();
|
|
262
|
+
const start = performance.now();
|
|
263
|
+
const signal = turnOptions?.signal;
|
|
264
|
+
const spanFor = (sessionId: string | undefined) =>
|
|
265
|
+
startSpan(options.spanName ?? "invoke_agent", {
|
|
266
|
+
type: "agent",
|
|
267
|
+
agentName: options.agentName,
|
|
268
|
+
input,
|
|
269
|
+
startTime,
|
|
270
|
+
attributes: sessionId === undefined ? undefined : { "gen_ai.conversation.id": sessionId },
|
|
271
|
+
});
|
|
272
|
+
let response: MessageResponse<TOutput>;
|
|
273
|
+
try {
|
|
274
|
+
response = await send();
|
|
275
|
+
} catch (error) {
|
|
276
|
+
const span = spanFor(knownSessionId);
|
|
277
|
+
if (signal?.aborted) {
|
|
278
|
+
span.end({ error: failureError("cancelled", "cancelled"), finishReason: "cancelled" });
|
|
279
|
+
} else {
|
|
280
|
+
span.end({ error: error instanceof Error ? error : new Error(String(error)) });
|
|
281
|
+
}
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
const span = spanFor(response.sessionId);
|
|
285
|
+
let streamStarted = false;
|
|
286
|
+
let endedBeforeStream = false;
|
|
287
|
+
const endCancelled = () => {
|
|
288
|
+
if (streamStarted) return;
|
|
289
|
+
endedBeforeStream = true;
|
|
290
|
+
span.end({ error: failureError("cancelled", "cancelled"), finishReason: "cancelled" });
|
|
291
|
+
};
|
|
292
|
+
if (signal?.aborted) {
|
|
293
|
+
endCancelled();
|
|
294
|
+
} else {
|
|
295
|
+
signal?.addEventListener("abort", endCancelled, { once: true });
|
|
296
|
+
}
|
|
297
|
+
// MessageResponse's constructor is @internal in eve; re-wrapping the stream has no
|
|
298
|
+
// public alternative. cancel() forwards to the original response, whose turn id
|
|
299
|
+
// resolves because instrumentedStream consumes it.
|
|
300
|
+
return new MessageResponse<TOutput>({
|
|
301
|
+
cancelTurn: () => response.cancel(),
|
|
302
|
+
createStream: () => {
|
|
303
|
+
streamStarted = true;
|
|
304
|
+
// The span already ended on abort; hand back the raw stream instead of ending it twice.
|
|
305
|
+
if (endedBeforeStream)
|
|
306
|
+
return (async function* () {
|
|
307
|
+
yield* response;
|
|
308
|
+
})();
|
|
309
|
+
return instrumentedStream(response, span, start, signal, endCancelled);
|
|
310
|
+
},
|
|
311
|
+
sessionId: response.sessionId,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
215
315
|
function wrapSession(session: ClientSession, options: WrapEveClientOptions): ClientSession {
|
|
216
316
|
return new Proxy(session, {
|
|
217
317
|
get(target, prop) {
|
|
218
318
|
if (prop === "send") {
|
|
219
|
-
const
|
|
319
|
+
const send: ClientSession["send"] = (message, turnOptions) =>
|
|
320
|
+
tracedTurn(options, message, turnOptions, target.state.sessionId, () =>
|
|
321
|
+
target.send(message, turnOptions),
|
|
322
|
+
);
|
|
323
|
+
return send;
|
|
324
|
+
}
|
|
325
|
+
if (prop === "respond") {
|
|
326
|
+
const respond: ClientSession["respond"] = (inputResponses, turnOptions) =>
|
|
327
|
+
tracedTurn(options, inputResponses, turnOptions, target.state.sessionId, () =>
|
|
328
|
+
target.respond(inputResponses, turnOptions),
|
|
329
|
+
);
|
|
330
|
+
return respond;
|
|
331
|
+
}
|
|
332
|
+
return bindOrReturn(target, prop);
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function wrapSessions(sessions: ClientSessions, options: WrapEveClientOptions): ClientSessions {
|
|
338
|
+
return new Proxy(sessions, {
|
|
339
|
+
get(target, prop) {
|
|
340
|
+
if (prop === "create") {
|
|
341
|
+
const create: ClientSessions["create"] = async <TOutput = unknown>(
|
|
220
342
|
input: SendTurnInput<TOutput>,
|
|
221
343
|
) => {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
input: inputForSpan(payload),
|
|
228
|
-
});
|
|
229
|
-
const start = performance.now();
|
|
230
|
-
let response: MessageResponse<TOutput>;
|
|
231
|
-
try {
|
|
232
|
-
response = await target.send(input);
|
|
233
|
-
} catch (error) {
|
|
234
|
-
if (payload.signal?.aborted) {
|
|
235
|
-
span.end({
|
|
236
|
-
error: failureError("cancelled", "cancelled"),
|
|
237
|
-
finishReason: "cancelled",
|
|
238
|
-
});
|
|
239
|
-
} else {
|
|
240
|
-
span.end({ error });
|
|
241
|
-
}
|
|
242
|
-
throw error;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
span.span.setAttribute("gen_ai.conversation.id", response.sessionId);
|
|
246
|
-
// MessageResponse's constructor is @internal in eve; re-wrapping the stream has no
|
|
247
|
-
// public alternative. Revisit on eve upgrades.
|
|
248
|
-
return new MessageResponse<TOutput>({
|
|
249
|
-
continuationToken: response.continuationToken,
|
|
250
|
-
sessionId: response.sessionId,
|
|
251
|
-
createStream: () => instrumentedStream(response, span, start, payload),
|
|
344
|
+
let session: ClientSession | undefined;
|
|
345
|
+
const response = await tracedTurn(options, input.message, input, undefined, async () => {
|
|
346
|
+
const created = await target.create(input);
|
|
347
|
+
session = created.session;
|
|
348
|
+
return created.response;
|
|
252
349
|
});
|
|
350
|
+
return { response, session: wrapSession(session!, options) };
|
|
253
351
|
};
|
|
254
|
-
return
|
|
352
|
+
return create;
|
|
353
|
+
}
|
|
354
|
+
if (prop === "attach") {
|
|
355
|
+
const attach: ClientSessions["attach"] = (sessionId, attachOptions) =>
|
|
356
|
+
wrapSession(target.attach(sessionId, attachOptions), options);
|
|
357
|
+
return attach;
|
|
255
358
|
}
|
|
256
359
|
return bindOrReturn(target, prop);
|
|
257
360
|
},
|
|
@@ -265,12 +368,10 @@ export function wrapEveClient<C extends Client>(
|
|
|
265
368
|
): C {
|
|
266
369
|
const { agentName: _agentName, spanName: _spanName, ...sdkOptions } = options;
|
|
267
370
|
ensureInit(sdkOptions, overrides);
|
|
268
|
-
|
|
371
|
+
const sessions = wrapSessions(client.sessions, options);
|
|
269
372
|
return new Proxy(client, {
|
|
270
373
|
get(target, prop) {
|
|
271
|
-
if (prop === "
|
|
272
|
-
return (state?: SessionState | string) => wrapSession(target.session(state), options);
|
|
273
|
-
}
|
|
374
|
+
if (prop === "sessions") return sessions;
|
|
274
375
|
return bindOrReturn(target, prop);
|
|
275
376
|
},
|
|
276
377
|
}) as C;
|
package/src/config.ts
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
init,
|
|
3
|
+
SCOPE_NAME,
|
|
4
|
+
shutdown,
|
|
5
|
+
type ClientOverrides,
|
|
6
|
+
type TelemetryOptions,
|
|
7
|
+
} from "@telemetry-dev/sdk";
|
|
2
8
|
|
|
3
9
|
/** Options accepted by every @telemetry-dev/eve entry point. */
|
|
4
|
-
export type TelemetryDevEveOptions = Omit<TelemetryOptions, "registerGlobal">;
|
|
10
|
+
export type TelemetryDevEveOptions = Omit<TelemetryOptions, "registerGlobal" | "sdkName">;
|
|
5
11
|
|
|
6
12
|
export type { ClientOverrides };
|
|
7
13
|
|
|
8
14
|
let initialized = false;
|
|
9
15
|
|
|
16
|
+
// eve's agent spans ("eve" in agent/instrumentation.ts, "eve.agent" in the provider layout), the
|
|
17
|
+
// AI SDK's model-call and tool spans ("gen_ai", emitted by @ai-sdk/otel), and this SDK's own
|
|
18
|
+
// client-wrapper spans. Everything else on the global provider (better-auth, Nitro, eve's
|
|
19
|
+
// "workflow" engine) is not AI telemetry.
|
|
20
|
+
const AI_SCOPES = {
|
|
21
|
+
eve: true,
|
|
22
|
+
"eve.agent": true,
|
|
23
|
+
gen_ai: true,
|
|
24
|
+
[SCOPE_NAME]: true,
|
|
25
|
+
} satisfies Record<string, true>;
|
|
26
|
+
|
|
27
|
+
export const isAiScope = (scope: string): boolean => Object.hasOwn(AI_SCOPES, scope);
|
|
28
|
+
|
|
10
29
|
/**
|
|
11
30
|
* Initializes the telemetry.dev SDK exactly once per process for this integration.
|
|
12
|
-
* registerGlobal:true lets eve's tracers
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
31
|
+
* registerGlobal:true lets eve's tracers resolve to the SDK provider. spanFilter defaults to
|
|
32
|
+
* exporting only AI scopes (see AI_SCOPES); pass spanFilter: () => true to export every span.
|
|
33
|
+
* eve starts each `ai.eve.turn` under a workflow-engine span of its own trace, so the turn is
|
|
34
|
+
* re-rooted under the session parent and every turn of a session lands in one trace.
|
|
16
35
|
*/
|
|
17
36
|
export function ensureInit(
|
|
18
37
|
options: TelemetryDevEveOptions = {},
|
|
@@ -23,8 +42,13 @@ export function ensureInit(
|
|
|
23
42
|
init(
|
|
24
43
|
{
|
|
25
44
|
...options,
|
|
45
|
+
sdkName: "@telemetry-dev/eve",
|
|
26
46
|
registerGlobal: true,
|
|
27
|
-
spanFilter: options.spanFilter ?? ((span) => span.instrumentationScope.name
|
|
47
|
+
spanFilter: options.spanFilter ?? ((span) => isAiScope(span.instrumentationScope.name)),
|
|
48
|
+
sessionRootOf: (name, attributes) => {
|
|
49
|
+
const sessionId = attributes["eve.session.id"];
|
|
50
|
+
return name === "ai.eve.turn" && typeof sessionId === "string" ? sessionId : undefined;
|
|
51
|
+
},
|
|
28
52
|
},
|
|
29
53
|
overrides,
|
|
30
54
|
);
|
package/src/hook.ts
CHANGED
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
import { log, type LogLevel } from "@telemetry-dev/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { MessageStreamEvent } from "eve/client";
|
|
3
3
|
import type { HookContext, HookDefinition } from "eve/hooks";
|
|
4
4
|
|
|
5
5
|
import { ensureInit, type ClientOverrides, type TelemetryDevEveOptions } from "./config.ts";
|
|
6
6
|
|
|
7
|
-
type Attrs =
|
|
7
|
+
type Attrs = { [key: string]: Value };
|
|
8
|
+
type Value = string | number | boolean | null | undefined | readonly Value[] | Attrs;
|
|
8
9
|
|
|
9
|
-
function asRecord(value:
|
|
10
|
-
if (value === null ||
|
|
11
|
-
return value as
|
|
10
|
+
function asRecord(value: Value): Attrs | undefined {
|
|
11
|
+
if (value === null || Array.isArray(value) || !(value instanceof Object)) return undefined;
|
|
12
|
+
return value as Attrs;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
function eventData(event:
|
|
15
|
+
function eventData(event: MessageStreamEvent): Attrs {
|
|
15
16
|
if (!("data" in event)) return {};
|
|
16
|
-
return asRecord(event.data) ?? {};
|
|
17
|
+
return asRecord(event.data as Value) ?? {};
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function stringField(record:
|
|
20
|
+
function stringField(record: Attrs | undefined, key: string): string | undefined {
|
|
20
21
|
const value = record?.[key];
|
|
21
|
-
return
|
|
22
|
+
return value?.constructor === String && value.length > 0 ? value : undefined;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
function numberField(record:
|
|
25
|
+
function numberField(record: Attrs | undefined, key: string): number | undefined {
|
|
25
26
|
const value = record?.[key];
|
|
26
|
-
return
|
|
27
|
+
return value?.constructor === Number ? value : undefined;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
function jsonField(record:
|
|
30
|
+
function jsonField(record: Attrs | undefined, key: string): string | undefined {
|
|
30
31
|
const value = record?.[key];
|
|
31
32
|
if (value === undefined) return undefined;
|
|
32
33
|
try {
|
|
@@ -36,7 +37,7 @@ function jsonField(record: Record<string, unknown> | undefined, key: string): st
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
function baseAttributes(event:
|
|
40
|
+
function baseAttributes(event: MessageStreamEvent, ctx: HookContext) {
|
|
40
41
|
const data = eventData(event);
|
|
41
42
|
return {
|
|
42
43
|
"gen_ai.conversation.id": ctx.session.id,
|
|
@@ -48,7 +49,7 @@ function baseAttributes(event: HandleMessageStreamEvent, ctx: HookContext): Attr
|
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
function toolAttrs(result:
|
|
52
|
+
function toolAttrs(result: Value) {
|
|
52
53
|
const record = asRecord(result);
|
|
53
54
|
const subagentName = stringField(record, "subagentName");
|
|
54
55
|
return {
|
|
@@ -59,7 +60,7 @@ function toolAttrs(result: unknown): Attrs {
|
|
|
59
60
|
"eve.subagent.name": subagentName,
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
|
-
function reportError(onError: ((error:
|
|
63
|
+
function reportError(onError: ((error: Error) => void) | undefined, error: Error): void {
|
|
63
64
|
try {
|
|
64
65
|
onError?.(error);
|
|
65
66
|
} catch {
|
|
@@ -68,14 +69,14 @@ function reportError(onError: ((error: unknown) => void) | undefined, error: unk
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
function emit(
|
|
71
|
-
event:
|
|
72
|
+
event: MessageStreamEvent,
|
|
72
73
|
ctx: HookContext,
|
|
73
74
|
level: LogLevel,
|
|
74
75
|
message: string,
|
|
75
76
|
attributes: Attrs = {},
|
|
76
|
-
baseEvent:
|
|
77
|
+
baseEvent: MessageStreamEvent = event,
|
|
77
78
|
): void {
|
|
78
|
-
const at = event.meta
|
|
79
|
+
const at = event.meta.at;
|
|
79
80
|
log(message, {
|
|
80
81
|
level,
|
|
81
82
|
eventName: event.type,
|
|
@@ -84,7 +85,7 @@ function emit(
|
|
|
84
85
|
});
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
function stepCompletedMessage(data:
|
|
88
|
+
function stepCompletedMessage(data: Attrs): string {
|
|
88
89
|
const finishReason = stringField(data, "finishReason") ?? "unknown";
|
|
89
90
|
const usage = asRecord(data.usage);
|
|
90
91
|
const inputTokens = numberField(usage, "inputTokens");
|
|
@@ -96,17 +97,18 @@ function stepCompletedMessage(data: Record<string, unknown>): string {
|
|
|
96
97
|
return `Step completed (${finishReason})${suffix}`;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
function logEvent(event:
|
|
100
|
+
function logEvent(event: MessageStreamEvent, ctx: HookContext): void {
|
|
100
101
|
const data = eventData(event);
|
|
101
102
|
|
|
102
103
|
switch (event.type) {
|
|
103
104
|
case "session.started": {
|
|
104
105
|
const runtime = asRecord(data.runtime);
|
|
105
106
|
const invocation = asRecord(data.invocation);
|
|
107
|
+
const trace = asRecord(data.trace);
|
|
106
108
|
emit(event, ctx, "info", "Session started", {
|
|
107
109
|
"eve.version": stringField(runtime, "eveVersion"),
|
|
108
|
-
"gen_ai.request.model": stringField(runtime, "modelId"),
|
|
109
110
|
"eve.agent.id": stringField(runtime, "agentId"),
|
|
111
|
+
"eve.trace.id": stringField(trace, "traceId"),
|
|
110
112
|
"eve.parent.session_id": stringField(invocation, "parentSessionId"),
|
|
111
113
|
"eve.parent.call_id": stringField(invocation, "parentCallId"),
|
|
112
114
|
"eve.parent.turn_id": stringField(invocation, "parentTurnId"),
|
|
@@ -120,6 +122,11 @@ function logEvent(event: HandleMessageStreamEvent, ctx: HookContext): void {
|
|
|
120
122
|
case "message.received":
|
|
121
123
|
emit(event, ctx, "debug", "User message received");
|
|
122
124
|
return;
|
|
125
|
+
case "step.started":
|
|
126
|
+
emit(event, ctx, "debug", "Step started", {
|
|
127
|
+
"gen_ai.request.model": stringField(data, "modelId"),
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
123
130
|
case "step.completed": {
|
|
124
131
|
const usage = asRecord(data.usage);
|
|
125
132
|
emit(event, ctx, "info", stepCompletedMessage(data), {
|
|
@@ -186,55 +193,33 @@ function logEvent(event: HandleMessageStreamEvent, ctx: HookContext): void {
|
|
|
186
193
|
});
|
|
187
194
|
return;
|
|
188
195
|
case "subagent.event": {
|
|
189
|
-
const child = asRecord(data.event);
|
|
196
|
+
const child = asRecord(data.event as Value);
|
|
190
197
|
const childData = asRecord(child?.data);
|
|
191
198
|
const childType = stringField(child, "type");
|
|
192
199
|
const attrs = {
|
|
193
200
|
"eve.subagent.name": stringField(data, "subagentName"),
|
|
194
201
|
"gen_ai.tool.call.id": stringField(data, "callId"),
|
|
195
202
|
};
|
|
196
|
-
if (
|
|
203
|
+
if (
|
|
204
|
+
childType === "step.failed" ||
|
|
205
|
+
childType === "turn.failed" ||
|
|
206
|
+
childType === "session.failed"
|
|
207
|
+
) {
|
|
197
208
|
emit(
|
|
198
209
|
event,
|
|
199
210
|
ctx,
|
|
200
211
|
"error",
|
|
201
|
-
`Subagent
|
|
212
|
+
`Subagent ${childType.replace(".", " ")}: ${stringField(childData, "message") ?? ""}`,
|
|
202
213
|
{
|
|
203
214
|
...attrs,
|
|
204
215
|
"error.code": stringField(childData, "code"),
|
|
205
216
|
"eve.error.details": jsonField(childData, "details"),
|
|
206
217
|
},
|
|
207
|
-
child as unknown as HandleMessageStreamEvent,
|
|
208
|
-
);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (childType === "turn.failed") {
|
|
212
|
-
emit(
|
|
213
|
-
event,
|
|
214
|
-
ctx,
|
|
215
|
-
"error",
|
|
216
|
-
`Subagent turn failed: ${stringField(childData, "message") ?? ""}`,
|
|
217
218
|
{
|
|
218
|
-
...
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
},
|
|
222
|
-
child as unknown as HandleMessageStreamEvent,
|
|
223
|
-
);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
if (childType === "session.failed") {
|
|
227
|
-
emit(
|
|
228
|
-
event,
|
|
229
|
-
ctx,
|
|
230
|
-
"error",
|
|
231
|
-
`Subagent session failed: ${stringField(childData, "message") ?? ""}`,
|
|
232
|
-
{
|
|
233
|
-
...attrs,
|
|
234
|
-
"error.code": stringField(childData, "code"),
|
|
235
|
-
"eve.error.details": jsonField(childData, "details"),
|
|
236
|
-
},
|
|
237
|
-
child as unknown as HandleMessageStreamEvent,
|
|
219
|
+
...child,
|
|
220
|
+
data: childData,
|
|
221
|
+
type: childType,
|
|
222
|
+
} as MessageStreamEvent,
|
|
238
223
|
);
|
|
239
224
|
}
|
|
240
225
|
return;
|
|
@@ -271,6 +256,9 @@ function logEvent(event: HandleMessageStreamEvent, ctx: HookContext): void {
|
|
|
271
256
|
case "turn.completed":
|
|
272
257
|
emit(event, ctx, "info", "Turn completed");
|
|
273
258
|
return;
|
|
259
|
+
case "turn.cancelled":
|
|
260
|
+
emit(event, ctx, "warn", "Turn cancelled");
|
|
261
|
+
return;
|
|
274
262
|
case "turn.failed":
|
|
275
263
|
emit(event, ctx, "error", `Turn failed: ${stringField(data, "message") ?? ""}`, {
|
|
276
264
|
"error.code": stringField(data, "code"),
|
|
@@ -305,7 +293,7 @@ export function telemetryDevHook(
|
|
|
305
293
|
ensureInit(options, overrides);
|
|
306
294
|
logEvent(event, ctx);
|
|
307
295
|
} catch (error) {
|
|
308
|
-
reportError(options.onError, error);
|
|
296
|
+
reportError(options.onError, error instanceof Error ? error : new Error(String(error)));
|
|
309
297
|
}
|
|
310
298
|
},
|
|
311
299
|
},
|
package/src/index.ts
CHANGED
package/src/instrumentation.ts
CHANGED
|
@@ -20,8 +20,8 @@ export interface TelemetryDevInstrumentationOptions extends TelemetryDevEveOptio
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const envServiceName = (): string | undefined =>
|
|
23
|
-
|
|
24
|
-
function reportError(onError: ((error:
|
|
23
|
+
globalThis.process !== undefined ? process.env.OTEL_SERVICE_NAME : undefined;
|
|
24
|
+
function reportError(onError: ((error: Error) => void) | undefined, error: Error): void {
|
|
25
25
|
try {
|
|
26
26
|
onError?.(error);
|
|
27
27
|
} catch {
|
|
@@ -49,27 +49,30 @@ export function telemetryDevInstrumentation(
|
|
|
49
49
|
overrides,
|
|
50
50
|
);
|
|
51
51
|
} catch (error) {
|
|
52
|
-
reportError(sdkOptions.onError, error);
|
|
52
|
+
reportError(sdkOptions.onError, error instanceof Error ? error : new Error(String(error)));
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
events: {
|
|
56
56
|
"step.started"(input) {
|
|
57
|
-
const ctx
|
|
57
|
+
const ctx = { ...runtimeContext } satisfies InstrumentationRuntimeContext;
|
|
58
58
|
const userId =
|
|
59
59
|
input.session.auth.initiator?.principalId ?? input.session.auth.current?.principalId;
|
|
60
|
-
|
|
60
|
+
const runtimeContextWithUser = userId ? { ...ctx, "user.id": userId } : ctx;
|
|
61
61
|
|
|
62
62
|
try {
|
|
63
63
|
const userResult = stepStarted?.(input);
|
|
64
64
|
if (userResult?.runtimeContext) {
|
|
65
|
-
Object.assign(
|
|
65
|
+
Object.assign(runtimeContextWithUser, userResult.runtimeContext);
|
|
66
66
|
}
|
|
67
67
|
} catch (error) {
|
|
68
|
-
reportError(
|
|
68
|
+
reportError(
|
|
69
|
+
sdkOptions.onError,
|
|
70
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
71
|
+
);
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
return Object.keys(
|
|
72
|
-
? { runtimeContext:
|
|
74
|
+
return Object.keys(runtimeContextWithUser).length > 0
|
|
75
|
+
? { runtimeContext: runtimeContextWithUser }
|
|
73
76
|
: undefined;
|
|
74
77
|
},
|
|
75
78
|
},
|
package/src/otel.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { TelemetrySpanProcessor, type TelemetrySpanProcessorOptions } from "@telemetry-dev/sdk";
|
|
2
|
+
import { otelIntegration, type OtelIntegration } from "eve/instrumentation/otel";
|
|
3
|
+
|
|
4
|
+
import { isAiScope } from "./config.ts";
|
|
5
|
+
|
|
6
|
+
export interface TelemetryDevOtelIntegrationOptions extends Omit<
|
|
7
|
+
TelemetrySpanProcessorOptions,
|
|
8
|
+
"spanExporter"
|
|
9
|
+
> {
|
|
10
|
+
recordInputs?: boolean;
|
|
11
|
+
recordOutputs?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* telemetry.dev as a destination in eve's `agent/instrumentation/` provider layout
|
|
16
|
+
* (`experimental.instrumentationProviders`). Export it from one file in that directory.
|
|
17
|
+
* eve owns the tracer provider there, so this attaches a processor instead of registering one.
|
|
18
|
+
*/
|
|
19
|
+
export function telemetryDevOtelIntegration(
|
|
20
|
+
options: TelemetryDevOtelIntegrationOptions = {},
|
|
21
|
+
): OtelIntegration {
|
|
22
|
+
const { recordInputs, recordOutputs, ...processorOptions } = options;
|
|
23
|
+
return otelIntegration({
|
|
24
|
+
recordInputs,
|
|
25
|
+
recordOutputs,
|
|
26
|
+
spanProcessors: [
|
|
27
|
+
new TelemetrySpanProcessor({
|
|
28
|
+
...processorOptions,
|
|
29
|
+
spanFilter:
|
|
30
|
+
processorOptions.spanFilter ?? ((span) => isAiScope(span.instrumentationScope.name)),
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
}
|