@arnilo/prism-observability-opentelemetry 0.0.4 → 0.0.6

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/CHANGELOG.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [0.0.6] - 2026-07-19
4
+
5
+ - Released with the exact 0.0.6 first-party package graph.
6
+
7
+ ## [0.0.5] - 2026-07-16
8
+
9
+ - Added safe run-feedback/evaluation span events or ended-run spans plus low-cardinality counters; comments, tag values, scorer/evaluation IDs, and arbitrary metadata are not accepted by telemetry handlers.
4
10
 
5
11
  ## [0.0.4] - 2026-07-14
6
12
 
package/README.md CHANGED
@@ -36,7 +36,10 @@ Set `enabled: false` or omit both `tracer` and `meter` for a no-op adapter.
36
36
 
37
37
  - Metadata-only spans: no prompts, tool arguments, or credentials.
38
38
  - High-cardinality IDs (`sessionId`, `runId`, `requestId`, `toolCallId`) are span attributes, not metric labels.
39
- - Metric labels are limited to `provider_id`, `outcome`, `status`, and token `kind`.
39
+ - Metric labels are limited to `provider_id`, `outcome`, `status`, token `kind`, and feedback rating bucket/link presence; comments, tags, and linked IDs never become labels.
40
+ - `handleRunFeedback()` / `handleEvaluation()` add safe scalar metadata to the active run span or a short post-run span.
41
+ - `prism.provider.tokens` records turns and `prism.run.tokens` records aggregates.
42
+ - Run errors and detach close attributable outstanding spans exactly once.
40
43
  - Exporter failures are isolated: instrumentation catches errors and forwards them to `onExporterError` without affecting the run.
41
44
 
42
45
  ## Profile inclusion
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { createInMemoryTelemetry, createOpenTelemetryInstrumentation, wrapOpenTelemetryApi, } from "./instrumentation.js";
2
- export type { InMemoryTelemetry, OpenTelemetryInstrumentation, OpenTelemetryInstrumentationOptions, PrismCounter, PrismHistogram, PrismMeter, PrismSpan, PrismSpanStatus, PrismTracer, RecordedMetric, RecordedSpan, } from "./instrumentation.js";
2
+ export type { EvaluationTelemetry, InMemoryTelemetry, OpenTelemetryInstrumentation, OpenTelemetryInstrumentationOptions, PrismCounter, PrismHistogram, PrismMeter, PrismSpan, PrismSpanStatus, PrismTracer, RunFeedbackTelemetry, RecordedMetric, RecordedSpan, } from "./instrumentation.js";
3
3
  export declare const packageName = "@arnilo/prism-observability-opentelemetry";
@@ -3,6 +3,7 @@ export type PrismSpanStatus = "ok" | "error";
3
3
  export interface PrismSpan {
4
4
  setAttribute(key: string, value: string | number | boolean): void;
5
5
  setStatus(code: PrismSpanStatus, message?: string): void;
6
+ addEvent?(name: string, attributes?: Readonly<Record<string, string | number | boolean>>): void;
6
7
  end(): void;
7
8
  }
8
9
  export interface PrismTracer {
@@ -31,10 +32,26 @@ export interface OpenTelemetryInstrumentationOptions {
31
32
  readonly meter?: PrismMeter;
32
33
  readonly onExporterError?: (error: unknown) => void;
33
34
  }
35
+ export interface RunFeedbackTelemetry {
36
+ readonly runId: string;
37
+ readonly rating?: number;
38
+ readonly hasComment: boolean;
39
+ readonly tagCount: number;
40
+ readonly scorerCount: number;
41
+ readonly evaluationCount: number;
42
+ }
43
+ export interface EvaluationTelemetry {
44
+ readonly runId: string;
45
+ readonly status: "scored" | "skipped" | "failed";
46
+ readonly score?: number;
47
+ readonly hasReason: boolean;
48
+ }
34
49
  export interface OpenTelemetryInstrumentation {
35
50
  readonly enabled: boolean;
36
51
  handleAgentEvent(event: AgentEvent): void;
37
- attachSession(session: Pick<AgentSession, "subscribe">): () => void;
52
+ handleRunFeedback(feedback: RunFeedbackTelemetry): void;
53
+ handleEvaluation(evaluation: EvaluationTelemetry): void;
54
+ attachSession(session: Pick<AgentSession, "id" | "subscribe">): () => void;
38
55
  }
39
56
  export interface RecordedSpan {
40
57
  readonly name: string;
@@ -44,6 +61,10 @@ export interface RecordedSpan {
44
61
  readonly message?: string;
45
62
  };
46
63
  readonly ended: boolean;
64
+ readonly events: readonly {
65
+ readonly name: string;
66
+ readonly attributes: Record<string, string | number | boolean>;
67
+ }[];
47
68
  }
48
69
  export interface RecordedMetric {
49
70
  readonly name: string;
@@ -66,6 +87,7 @@ interface OpenTelemetrySpan {
66
87
  code: number;
67
88
  message?: string;
68
89
  }): void;
90
+ addEvent?(name: string, attributes?: Record<string, string | number | boolean>): void;
69
91
  end(): void;
70
92
  }
71
93
  interface OpenTelemetryTracer {
@@ -9,7 +9,7 @@ export function createInMemoryTelemetry() {
9
9
  const metrics = [];
10
10
  const tracer = {
11
11
  startSpan(name, options) {
12
- const record = { name, attributes: { ...(options?.attributes ?? {}) }, ended: false };
12
+ const record = { name, attributes: { ...(options?.attributes ?? {}) }, ended: false, events: [] };
13
13
  spans.push(record);
14
14
  return {
15
15
  setAttribute(key, value) {
@@ -18,6 +18,9 @@ export function createInMemoryTelemetry() {
18
18
  setStatus(code, message) {
19
19
  record.status = { code, message };
20
20
  },
21
+ addEvent(name, attributes) {
22
+ record.events.push({ name, attributes: { ...(attributes ?? {}) } });
23
+ },
21
24
  end() {
22
25
  record.ended = true;
23
26
  },
@@ -68,7 +71,10 @@ export function createOpenTelemetryInstrumentation(options = {}) {
68
71
  description: "Tool execution latency",
69
72
  unit: "ms",
70
73
  });
71
- const tokenCounter = meter?.createCounter("prism.provider.tokens", { description: "Provider token usage" });
74
+ const providerTokenCounter = meter?.createCounter("prism.provider.tokens", { description: "Provider-turn token usage" });
75
+ const runTokenCounter = meter?.createCounter("prism.run.tokens", { description: "Aggregate agent-run token usage" });
76
+ const feedbackCounter = meter?.createCounter("prism.run.feedback", { description: "Run feedback count" });
77
+ const evaluationCounter = meter?.createCounter("prism.run.evaluation", { description: "Run evaluation count" });
72
78
  const activeSpans = new Map();
73
79
  const safe = (fn) => {
74
80
  if (!enabled)
@@ -81,12 +87,22 @@ export function createOpenTelemetryInstrumentation(options = {}) {
81
87
  }
82
88
  };
83
89
  const endSpan = (key, status, message) => {
84
- const span = activeSpans.get(key);
85
- if (!span)
90
+ const active = activeSpans.get(key);
91
+ if (!active)
86
92
  return;
87
- span.setStatus(status, message);
88
- span.end();
89
93
  activeSpans.delete(key);
94
+ try {
95
+ active.span.setStatus(status, message);
96
+ }
97
+ finally {
98
+ active.span.end();
99
+ }
100
+ };
101
+ const endMatchingSpans = (predicate, message) => {
102
+ for (const [key, active] of activeSpans) {
103
+ if (predicate(active))
104
+ endSpan(key, "error", message);
105
+ }
90
106
  };
91
107
  const handleAgentEvent = (event) => {
92
108
  if (!enabled)
@@ -97,21 +113,28 @@ export function createOpenTelemetryInstrumentation(options = {}) {
97
113
  if (!tracer)
98
114
  return;
99
115
  const key = spanKey("agent", event.runId);
100
- activeSpans.set(key, tracer.startSpan("prism.agent.run", {
101
- attributes: {
102
- "prism.session_id": event.sessionId,
103
- "prism.run_id": event.runId,
104
- },
105
- }));
116
+ endSpan(key, "error", "Duplicate agent start");
117
+ activeSpans.set(key, {
118
+ sessionId: event.sessionId,
119
+ runId: event.runId,
120
+ span: tracer.startSpan("prism.agent.run", {
121
+ attributes: {
122
+ "prism.session_id": event.sessionId,
123
+ "prism.run_id": event.runId,
124
+ },
125
+ }),
126
+ });
106
127
  });
107
128
  break;
108
129
  case "agent_finished":
109
130
  safe(() => {
110
131
  endSpan(spanKey("agent", event.runId), "ok");
111
- if (!event.usage || !tokenCounter)
132
+ if (!event.usage || !runTokenCounter)
112
133
  return;
113
- tokenCounter.add(event.usage.inputTokens ?? 0, metricAttrs({ kind: "input", scope: "agent" }));
114
- tokenCounter.add(event.usage.outputTokens ?? 0, metricAttrs({ kind: "output", scope: "agent" }));
134
+ if (event.usage.inputTokens !== undefined)
135
+ runTokenCounter.add(event.usage.inputTokens, metricAttrs({ kind: "input" }));
136
+ if (event.usage.outputTokens !== undefined)
137
+ runTokenCounter.add(event.usage.outputTokens, metricAttrs({ kind: "output" }));
115
138
  });
116
139
  break;
117
140
  case "provider_turn_started":
@@ -120,49 +143,57 @@ export function createOpenTelemetryInstrumentation(options = {}) {
120
143
  return;
121
144
  const attempt = event.metadata.attempt ?? 1;
122
145
  const key = spanKey("provider", event.runId, event.turn, attempt);
123
- activeSpans.set(key, tracer.startSpan("prism.provider.turn", {
124
- attributes: {
125
- "prism.session_id": event.sessionId,
126
- "prism.run_id": event.runId,
127
- "prism.turn": event.turn,
128
- "prism.provider_id": event.metadata.providerId,
129
- "prism.model": event.metadata.model.model,
130
- ...(event.metadata.requestId ? { "prism.request_id": event.metadata.requestId } : {}),
131
- ...(event.metadata.attempt ? { "prism.attempt": event.metadata.attempt } : {}),
132
- },
133
- }));
146
+ endSpan(key, "error", "Duplicate provider turn start");
147
+ activeSpans.set(key, {
148
+ sessionId: event.sessionId,
149
+ runId: event.runId,
150
+ span: tracer.startSpan("prism.provider.turn", {
151
+ attributes: {
152
+ "prism.session_id": event.sessionId,
153
+ "prism.run_id": event.runId,
154
+ "prism.turn": event.turn,
155
+ "prism.provider_id": event.metadata.providerId,
156
+ "prism.model": event.metadata.model.model,
157
+ ...(event.metadata.requestId ? { "prism.request_id": event.metadata.requestId } : {}),
158
+ ...(event.metadata.attempt ? { "prism.attempt": event.metadata.attempt } : {}),
159
+ },
160
+ }),
161
+ });
134
162
  });
135
163
  break;
136
164
  case "provider_turn_finished":
137
165
  safe(() => {
138
166
  const attempt = event.metadata.attempt ?? 1;
139
167
  const key = spanKey("provider", event.runId, event.turn, attempt);
140
- const span = activeSpans.get(key);
141
- if (span) {
142
- if (event.metadata.latencyMs !== undefined)
143
- span.setAttribute("prism.latency_ms", event.metadata.latencyMs);
144
- if (event.metadata.httpStatus !== undefined)
145
- span.setAttribute("http.status_code", event.metadata.httpStatus);
146
- if (event.error)
147
- span.setStatus("error", event.error.message);
148
- else
149
- span.setStatus("ok");
150
- span.end();
151
- activeSpans.delete(key);
168
+ const active = activeSpans.get(key);
169
+ if (active) {
170
+ try {
171
+ if (event.metadata.latencyMs !== undefined)
172
+ active.span.setAttribute("prism.latency_ms", event.metadata.latencyMs);
173
+ if (event.metadata.httpStatus !== undefined)
174
+ active.span.setAttribute("http.status_code", event.metadata.httpStatus);
175
+ }
176
+ finally {
177
+ endSpan(key, event.error ? "error" : "ok", event.error?.message);
178
+ }
152
179
  }
153
180
  providerDuration?.record(event.metadata.latencyMs ?? 0, metricAttrs({
154
181
  provider_id: event.metadata.providerId,
155
182
  outcome: event.error ? "error" : "success",
156
183
  }));
157
- if (!event.usage || !tokenCounter)
184
+ if (!event.usage || !providerTokenCounter)
158
185
  return;
159
- tokenCounter.add(event.usage.inputTokens ?? 0, metricAttrs({ provider_id: event.metadata.providerId, kind: "input" }));
160
- tokenCounter.add(event.usage.outputTokens ?? 0, metricAttrs({ provider_id: event.metadata.providerId, kind: "output" }));
186
+ if (event.usage.inputTokens !== undefined) {
187
+ providerTokenCounter.add(event.usage.inputTokens, metricAttrs({ provider_id: event.metadata.providerId, kind: "input" }));
188
+ }
189
+ if (event.usage.outputTokens !== undefined) {
190
+ providerTokenCounter.add(event.usage.outputTokens, metricAttrs({ provider_id: event.metadata.providerId, kind: "output" }));
191
+ }
161
192
  if (event.usage.cacheReadTokens) {
162
- tokenCounter.add(event.usage.cacheReadTokens, metricAttrs({ provider_id: event.metadata.providerId, kind: "cache_read" }));
193
+ providerTokenCounter.add(event.usage.cacheReadTokens, metricAttrs({ provider_id: event.metadata.providerId, kind: "cache_read" }));
163
194
  }
164
195
  if (event.usage.cacheWriteTokens) {
165
- tokenCounter.add(event.usage.cacheWriteTokens, metricAttrs({ provider_id: event.metadata.providerId, kind: "cache_write" }));
196
+ providerTokenCounter.add(event.usage.cacheWriteTokens, metricAttrs({ provider_id: event.metadata.providerId, kind: "cache_write" }));
166
197
  }
167
198
  });
168
199
  break;
@@ -170,15 +201,20 @@ export function createOpenTelemetryInstrumentation(options = {}) {
170
201
  safe(() => {
171
202
  if (!tracer)
172
203
  return;
173
- const key = spanKey("tool", event.call.id);
174
- activeSpans.set(key, tracer.startSpan("prism.tool.execute", {
175
- attributes: {
176
- "prism.session_id": event.sessionId,
177
- "prism.run_id": event.runId,
178
- "prism.tool_call_id": event.call.id,
179
- "prism.tool_name": event.call.name,
180
- },
181
- }));
204
+ const key = spanKey("tool", event.runId, event.call.id);
205
+ endSpan(key, "error", "Duplicate tool start");
206
+ activeSpans.set(key, {
207
+ sessionId: event.sessionId,
208
+ runId: event.runId,
209
+ span: tracer.startSpan("prism.tool.execute", {
210
+ attributes: {
211
+ "prism.session_id": event.sessionId,
212
+ "prism.run_id": event.runId,
213
+ "prism.tool_call_id": event.call.id,
214
+ "prism.tool_name": event.call.name,
215
+ },
216
+ }),
217
+ });
182
218
  });
183
219
  break;
184
220
  case "tool_execution_finished":
@@ -190,28 +226,78 @@ export function createOpenTelemetryInstrumentation(options = {}) {
190
226
  : event.type === "tool_execution_error"
191
227
  ? event.call.id
192
228
  : event.toolCallId;
193
- const key = spanKey("tool", toolCallId);
194
- const span = activeSpans.get(key);
195
- if (span) {
196
- span.setAttribute("prism.tool_status", event.metadata.status);
197
- span.setAttribute("prism.duration_ms", event.metadata.durationMs);
198
- if (event.type === "tool_execution_finished")
199
- span.setStatus("ok");
200
- else
201
- span.setStatus("error", event.error.message);
202
- span.end();
203
- activeSpans.delete(key);
229
+ const key = spanKey("tool", event.runId, toolCallId);
230
+ const active = activeSpans.get(key);
231
+ if (active) {
232
+ try {
233
+ active.span.setAttribute("prism.tool_status", event.metadata.status);
234
+ active.span.setAttribute("prism.duration_ms", event.metadata.durationMs);
235
+ }
236
+ finally {
237
+ endSpan(key, event.type === "tool_execution_finished" ? "ok" : "error", event.type === "tool_execution_finished" ? undefined : event.error.message);
238
+ }
204
239
  }
205
240
  toolDuration?.record(event.metadata.durationMs, metricAttrs({ status: event.metadata.status }));
206
241
  });
207
242
  break;
243
+ case "error":
244
+ safe(() => {
245
+ if (event.runId)
246
+ endMatchingSpans((active) => active.runId === event.runId, event.error.message);
247
+ else if (event.sessionId)
248
+ endMatchingSpans((active) => active.sessionId === event.sessionId, event.error.message);
249
+ });
250
+ break;
208
251
  default:
209
252
  break;
210
253
  }
211
254
  };
255
+ const recordRunMetadata = (runId, eventName, spanName, attributes) => safe(() => {
256
+ const active = activeSpans.get(spanKey("agent", runId));
257
+ if (active?.span.addEvent) {
258
+ active.span.addEvent(eventName, attributes);
259
+ return;
260
+ }
261
+ if (!tracer)
262
+ return;
263
+ const span = tracer.startSpan(spanName, { attributes: { "prism.run_id": runId, ...attributes } });
264
+ span.setStatus("ok");
265
+ span.end();
266
+ });
212
267
  return {
213
268
  enabled,
214
269
  handleAgentEvent,
270
+ handleRunFeedback(feedback) {
271
+ const rating = feedback.rating !== undefined && Number.isFinite(feedback.rating) && feedback.rating >= -1 && feedback.rating <= 1
272
+ ? feedback.rating
273
+ : undefined;
274
+ const tagCount = boundedCount(feedback.tagCount);
275
+ const scorerCount = boundedCount(feedback.scorerCount);
276
+ const evaluationCount = boundedCount(feedback.evaluationCount);
277
+ const attributes = {
278
+ "prism.feedback.rating": rating ?? 0,
279
+ "prism.feedback.has_rating": rating !== undefined,
280
+ "prism.feedback.has_comment": Boolean(feedback.hasComment),
281
+ "prism.feedback.tag_count": tagCount,
282
+ "prism.feedback.scorer_count": scorerCount,
283
+ "prism.feedback.evaluation_count": evaluationCount,
284
+ };
285
+ recordRunMetadata(feedback.runId, "prism.run.feedback", "prism.run.feedback", attributes);
286
+ safe(() => feedbackCounter?.add(1, metricAttrs({
287
+ rating: rating === undefined ? "none" : rating > 0 ? "positive" : rating < 0 ? "negative" : "neutral",
288
+ linked_evaluation: evaluationCount > 0 ? "true" : "false",
289
+ })));
290
+ },
291
+ handleEvaluation(evaluation) {
292
+ const attributes = {
293
+ "prism.evaluation.status": evaluation.status,
294
+ "prism.evaluation.has_score": evaluation.score !== undefined && Number.isFinite(evaluation.score),
295
+ "prism.evaluation.has_reason": Boolean(evaluation.hasReason),
296
+ ...(evaluation.score !== undefined && Number.isFinite(evaluation.score) ? { "prism.evaluation.score": evaluation.score } : {}),
297
+ };
298
+ recordRunMetadata(evaluation.runId, "prism.run.evaluation", "prism.run.evaluation", attributes);
299
+ safe(() => evaluationCounter?.add(1, metricAttrs({ status: evaluation.status })));
300
+ },
215
301
  attachSession(session) {
216
302
  if (!enabled)
217
303
  return () => { };
@@ -222,7 +308,7 @@ export function createOpenTelemetryInstrumentation(options = {}) {
222
308
  try {
223
309
  while (active) {
224
310
  const next = await iterator.next();
225
- if (next.done)
311
+ if (!active || next.done)
226
312
  break;
227
313
  handleAgentEvent(next.value);
228
314
  }
@@ -234,10 +320,14 @@ export function createOpenTelemetryInstrumentation(options = {}) {
234
320
  return () => {
235
321
  active = false;
236
322
  void iterator.return?.();
323
+ safe(() => endMatchingSpans((span) => span.sessionId === session.id, "Instrumentation detached"));
237
324
  };
238
325
  },
239
326
  };
240
327
  }
328
+ function boundedCount(value) {
329
+ return Number.isSafeInteger(value) && value > 0 ? Math.min(value, 64) : 0;
330
+ }
241
331
  const OTEL_STATUS_OK = 1;
242
332
  const OTEL_STATUS_ERROR = 2;
243
333
  export function wrapOpenTelemetryApi(tracer, meter) {
@@ -252,6 +342,9 @@ export function wrapOpenTelemetryApi(tracer, meter) {
252
342
  setStatus(code, message) {
253
343
  span.setStatus({ code: code === "ok" ? OTEL_STATUS_OK : OTEL_STATUS_ERROR, message });
254
344
  },
345
+ addEvent(name, attributes) {
346
+ span.addEvent?.(name, attributes ? { ...attributes } : undefined);
347
+ },
255
348
  end() {
256
349
  span.end();
257
350
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnilo/prism-observability-opentelemetry",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Optional OpenTelemetry adapter for Prism agent events.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,7 +25,7 @@
25
25
  "pack:dry-run": "npm pack --dry-run"
26
26
  },
27
27
  "peerDependencies": {
28
- "@arnilo/prism": "0.0.4"
28
+ "@arnilo/prism": "0.0.6"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@arnilo/prism": "file:../..",