@struktur/telemetry 2.1.1
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/package.json +36 -0
- package/src/adapters/langfuse/LangfuseAdapter.ts +319 -0
- package/src/adapters/langfuse/index.ts +6 -0
- package/src/adapters/phoenix/PhoenixAdapter.ts +338 -0
- package/src/adapters/phoenix/index.ts +6 -0
- package/src/factory.ts +133 -0
- package/src/index.ts +55 -0
- package/src/types.ts +453 -0
- package/tests/adapters/langfuse.test.ts +118 -0
- package/tests/adapters/phoenix.test.ts +132 -0
- package/tests/factory.test.ts +93 -0
- package/tests/types.test.ts +248 -0
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@struktur/telemetry",
|
|
3
|
+
"version": "2.1.1",
|
|
4
|
+
"license": "FSL-1.1-MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./types": "./src/types.ts",
|
|
11
|
+
"./factory": "./src/factory.ts",
|
|
12
|
+
"./adapters/phoenix": "./src/adapters/phoenix/index.ts",
|
|
13
|
+
"./adapters/langfuse": "./src/adapters/langfuse/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "bun test",
|
|
17
|
+
"typecheck": "tsc --noEmit"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@struktur/sdk": "2.1.0"
|
|
21
|
+
},
|
|
22
|
+
"optionalDependencies": {
|
|
23
|
+
"@arizeai/phoenix-otel": "^0.4.2",
|
|
24
|
+
"@arizeai/openinference-core": "^1.0.0",
|
|
25
|
+
"@arizeai/openinference-semantic-conventions": "^1.0.0",
|
|
26
|
+
"@langfuse/otel": "^2.0.0",
|
|
27
|
+
"@opentelemetry/api": "^1.9.0",
|
|
28
|
+
"@opentelemetry/sdk-node": "^0.200.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"typescript": "^5"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/bun": "latest"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Langfuse telemetry adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements TelemetryAdapter for Langfuse using their OpenTelemetry SDK.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
TelemetryAdapter,
|
|
9
|
+
SpanContext,
|
|
10
|
+
Span,
|
|
11
|
+
SpanResult,
|
|
12
|
+
TelemetryEvent,
|
|
13
|
+
TelemetryContext,
|
|
14
|
+
LangfuseConfig,
|
|
15
|
+
LLMCallEvent,
|
|
16
|
+
ValidationEvent,
|
|
17
|
+
ChunkEvent,
|
|
18
|
+
ToolCallEvent,
|
|
19
|
+
MergeEvent,
|
|
20
|
+
ParseEvent,
|
|
21
|
+
} from "../../types.js";
|
|
22
|
+
|
|
23
|
+
type OtelSpan = {
|
|
24
|
+
spanContext: () => { spanId: string; traceId: string };
|
|
25
|
+
setStatus: (status: { code: number; message?: string }) => void;
|
|
26
|
+
setAttribute: (key: string, value: string | number | boolean | undefined) => void;
|
|
27
|
+
setAttributes: (attrs: Record<string, string | number | boolean>) => void;
|
|
28
|
+
recordException: (error: Error) => void;
|
|
29
|
+
end: () => void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Langfuse telemetry adapter using OpenTelemetry
|
|
34
|
+
*/
|
|
35
|
+
export class LangfuseAdapter implements TelemetryAdapter {
|
|
36
|
+
readonly name = "langfuse";
|
|
37
|
+
readonly version = "1.0.0";
|
|
38
|
+
|
|
39
|
+
private config: LangfuseConfig;
|
|
40
|
+
private sdk: { shutdown: () => Promise<void> } | null = null;
|
|
41
|
+
private activeSpans = new Map<string, OtelSpan>();
|
|
42
|
+
private otelApi: typeof import("@opentelemetry/api") | null = null;
|
|
43
|
+
|
|
44
|
+
constructor(config: LangfuseConfig) {
|
|
45
|
+
this.config = {
|
|
46
|
+
baseUrl: "https://cloud.langfuse.com",
|
|
47
|
+
...config,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async initialize(): Promise<void> {
|
|
52
|
+
// Dynamically import Langfuse OTel SDK
|
|
53
|
+
const [{ LangfuseSpanProcessor }, { NodeSDK }, otelApi] = await Promise.all([
|
|
54
|
+
import("@langfuse/otel"),
|
|
55
|
+
import("@opentelemetry/sdk-node"),
|
|
56
|
+
import("@opentelemetry/api"),
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
this.otelApi = otelApi;
|
|
60
|
+
|
|
61
|
+
const processor = new LangfuseSpanProcessor({
|
|
62
|
+
publicKey: this.config.publicKey,
|
|
63
|
+
secretKey: this.config.secretKey,
|
|
64
|
+
baseUrl: this.config.baseUrl,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const sdk = new NodeSDK({
|
|
68
|
+
spanProcessors: [processor],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
sdk.start();
|
|
72
|
+
this.sdk = sdk;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async shutdown(): Promise<void> {
|
|
76
|
+
if (this.sdk) {
|
|
77
|
+
await this.sdk.shutdown();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
startSpan(context: SpanContext): Span {
|
|
82
|
+
if (!this.otelApi) {
|
|
83
|
+
throw new Error("LangfuseAdapter not initialized");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const tracer = this.otelApi.trace.getTracer("struktur");
|
|
87
|
+
|
|
88
|
+
const otelSpan = tracer.startSpan(context.name, {
|
|
89
|
+
attributes: {
|
|
90
|
+
"observation.type": context.kind.toLowerCase(),
|
|
91
|
+
...context.attributes,
|
|
92
|
+
},
|
|
93
|
+
}) as OtelSpan;
|
|
94
|
+
|
|
95
|
+
const spanContext = otelSpan.spanContext();
|
|
96
|
+
const span: Span = {
|
|
97
|
+
id: spanContext.spanId,
|
|
98
|
+
traceId: spanContext.traceId,
|
|
99
|
+
name: context.name,
|
|
100
|
+
kind: context.kind,
|
|
101
|
+
startTime: context.startTime ?? Date.now(),
|
|
102
|
+
parentId: context.parentSpan?.id,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
this.activeSpans.set(span.id, otelSpan);
|
|
106
|
+
return span;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
endSpan(span: Span, result?: SpanResult): void {
|
|
110
|
+
const otelSpan = this.activeSpans.get(span.id);
|
|
111
|
+
if (!otelSpan) return;
|
|
112
|
+
|
|
113
|
+
if (result) {
|
|
114
|
+
otelSpan.setStatus({
|
|
115
|
+
code: result.status === "ok" ? 1 : 2,
|
|
116
|
+
message: result.error?.message,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (result.output !== undefined) {
|
|
120
|
+
try {
|
|
121
|
+
const outputStr = typeof result.output === "string"
|
|
122
|
+
? result.output
|
|
123
|
+
: JSON.stringify(result.output);
|
|
124
|
+
otelSpan.setAttribute("output", outputStr);
|
|
125
|
+
} catch {
|
|
126
|
+
otelSpan.setAttribute("output", "[object]");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (result.latencyMs !== undefined) {
|
|
131
|
+
otelSpan.setAttribute("latency_ms", result.latencyMs);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
otelSpan.end();
|
|
136
|
+
this.activeSpans.delete(span.id);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
recordEvent(span: Span, event: TelemetryEvent): void {
|
|
140
|
+
const otelSpan = this.activeSpans.get(span.id);
|
|
141
|
+
if (!otelSpan) return;
|
|
142
|
+
|
|
143
|
+
switch (event.type) {
|
|
144
|
+
case "llm_call":
|
|
145
|
+
this.recordLLMCall(otelSpan, event);
|
|
146
|
+
break;
|
|
147
|
+
case "validation":
|
|
148
|
+
this.recordValidation(otelSpan, event);
|
|
149
|
+
break;
|
|
150
|
+
case "chunk":
|
|
151
|
+
this.recordChunk(otelSpan, event);
|
|
152
|
+
break;
|
|
153
|
+
case "tool_call":
|
|
154
|
+
this.recordToolCall(otelSpan, event);
|
|
155
|
+
break;
|
|
156
|
+
case "merge":
|
|
157
|
+
this.recordMerge(otelSpan, event);
|
|
158
|
+
break;
|
|
159
|
+
case "parse":
|
|
160
|
+
this.recordParse(otelSpan, event);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
setAttributes(span: Span, attributes: Record<string, unknown>): void {
|
|
166
|
+
const otelSpan = this.activeSpans.get(span.id);
|
|
167
|
+
if (!otelSpan) return;
|
|
168
|
+
|
|
169
|
+
const stringAttrs: Record<string, string | number | boolean> = {};
|
|
170
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
171
|
+
if (value !== undefined && value !== null) {
|
|
172
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
173
|
+
stringAttrs[key] = value;
|
|
174
|
+
} else {
|
|
175
|
+
try {
|
|
176
|
+
stringAttrs[key] = JSON.stringify(value);
|
|
177
|
+
} catch {
|
|
178
|
+
stringAttrs[key] = String(value);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
otelSpan.setAttributes(stringAttrs);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
setContext(_context: TelemetryContext): void {
|
|
188
|
+
// Langfuse supports session_id, user_id, metadata, tags via span attributes
|
|
189
|
+
// These would be set on individual spans
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private recordLLMCall(span: OtelSpan, event: LLMCallEvent): void {
|
|
193
|
+
const attrs: Record<string, string | number | boolean> = {
|
|
194
|
+
model: event.model,
|
|
195
|
+
provider: event.provider,
|
|
196
|
+
input: JSON.stringify(event.input.messages),
|
|
197
|
+
temperature: event.input.temperature ?? "",
|
|
198
|
+
max_tokens: event.input.maxTokens ?? "",
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
if (event.output) {
|
|
202
|
+
attrs.output = event.output.content;
|
|
203
|
+
|
|
204
|
+
if (event.output.usage) {
|
|
205
|
+
attrs["usage.input"] = event.output.usage.input;
|
|
206
|
+
attrs["usage.output"] = event.output.usage.output;
|
|
207
|
+
attrs["usage.total"] = event.output.usage.total;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
attrs.latency_ms = event.latencyMs;
|
|
212
|
+
|
|
213
|
+
if (event.error) {
|
|
214
|
+
attrs.error = event.error.message;
|
|
215
|
+
span.recordException(event.error);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
span.setAttributes(attrs);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private recordValidation(span: OtelSpan, event: ValidationEvent): void {
|
|
222
|
+
const attrs: Record<string, string | number | boolean> = {
|
|
223
|
+
attempt: event.attempt,
|
|
224
|
+
max_attempts: event.maxAttempts,
|
|
225
|
+
success: event.success,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
if (event.errors && event.errors.length > 0) {
|
|
229
|
+
attrs.errors = JSON.stringify(event.errors);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (event.latencyMs !== undefined) {
|
|
233
|
+
attrs.latency_ms = event.latencyMs;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
span.setAttributes(attrs);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private recordChunk(span: OtelSpan, event: ChunkEvent): void {
|
|
240
|
+
const attrs: Record<string, string | number | boolean> = {
|
|
241
|
+
chunk_index: event.chunkIndex,
|
|
242
|
+
chunk_total: event.totalChunks,
|
|
243
|
+
chunk_tokens: event.tokens,
|
|
244
|
+
chunk_images: event.images,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (event.content) {
|
|
248
|
+
attrs.chunk_content = event.content.slice(0, 1000);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
span.setAttributes(attrs);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private recordToolCall(span: OtelSpan, event: ToolCallEvent): void {
|
|
255
|
+
const attrs: Record<string, string | number | boolean> = {
|
|
256
|
+
tool_name: event.toolName,
|
|
257
|
+
tool_args: JSON.stringify(event.args),
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
if (event.result !== undefined) {
|
|
261
|
+
try {
|
|
262
|
+
attrs.tool_result = JSON.stringify(event.result);
|
|
263
|
+
} catch {
|
|
264
|
+
attrs.tool_result = "[object]";
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (event.error) {
|
|
269
|
+
attrs.tool_error = event.error.message;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (event.latencyMs !== undefined) {
|
|
273
|
+
attrs.latency_ms = event.latencyMs;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
span.setAttributes(attrs);
|
|
277
|
+
|
|
278
|
+
if (event.error) {
|
|
279
|
+
span.recordException(event.error);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private recordMerge(span: OtelSpan, event: MergeEvent): void {
|
|
284
|
+
const attrs: Record<string, string | number | boolean> = {
|
|
285
|
+
strategy: event.strategy,
|
|
286
|
+
input_count: event.inputCount,
|
|
287
|
+
output_count: event.outputCount,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
if (event.deduped !== undefined) {
|
|
291
|
+
attrs.deduped = event.deduped;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
span.setAttributes(attrs);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private recordParse(span: OtelSpan, event: ParseEvent): void {
|
|
298
|
+
span.setAttributes({
|
|
299
|
+
mime_type: event.mimeType,
|
|
300
|
+
parser: event.parser,
|
|
301
|
+
input_size: event.inputSize,
|
|
302
|
+
output_tokens: event.outputTokens,
|
|
303
|
+
output_images: event.outputImages,
|
|
304
|
+
latency_ms: event.latencyMs,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Create a Langfuse telemetry adapter
|
|
311
|
+
*
|
|
312
|
+
* @param config - Langfuse configuration
|
|
313
|
+
* @returns Langfuse telemetry adapter
|
|
314
|
+
*/
|
|
315
|
+
export function createLangfuseAdapter(config: LangfuseConfig): LangfuseAdapter {
|
|
316
|
+
return new LangfuseAdapter(config);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export type { LangfuseConfig };
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phoenix (Arize) telemetry adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements TelemetryAdapter for Phoenix using OpenTelemetry and
|
|
5
|
+
* OpenInference semantic conventions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
TelemetryAdapter,
|
|
10
|
+
SpanContext,
|
|
11
|
+
Span,
|
|
12
|
+
SpanResult,
|
|
13
|
+
TelemetryEvent,
|
|
14
|
+
TelemetryContext,
|
|
15
|
+
PhoenixConfig,
|
|
16
|
+
LLMCallEvent,
|
|
17
|
+
ValidationEvent,
|
|
18
|
+
ChunkEvent,
|
|
19
|
+
ToolCallEvent,
|
|
20
|
+
MergeEvent,
|
|
21
|
+
ParseEvent,
|
|
22
|
+
TokenUsage,
|
|
23
|
+
} from "../../types.js";
|
|
24
|
+
|
|
25
|
+
type OtelSpan = {
|
|
26
|
+
spanContext: () => { spanId: string; traceId: string };
|
|
27
|
+
setStatus: (status: { code: number; message?: string }) => void;
|
|
28
|
+
setAttribute: (key: string, value: string | number | boolean | undefined) => void;
|
|
29
|
+
setAttributes: (attrs: Record<string, string | number | boolean>) => void;
|
|
30
|
+
recordException: (error: Error) => void;
|
|
31
|
+
end: () => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Phoenix telemetry adapter using OpenTelemetry
|
|
36
|
+
*/
|
|
37
|
+
export class PhoenixAdapter implements TelemetryAdapter {
|
|
38
|
+
readonly name = "phoenix";
|
|
39
|
+
readonly version = "1.0.0";
|
|
40
|
+
|
|
41
|
+
private config: PhoenixConfig;
|
|
42
|
+
private tracerProvider: { forceFlush?: () => Promise<void> } | null = null;
|
|
43
|
+
private activeSpans = new Map<string, OtelSpan>();
|
|
44
|
+
private otelApi: typeof import("@opentelemetry/api") | null = null;
|
|
45
|
+
private phoenixOtel: typeof import("@arizeai/phoenix-otel") | null = null;
|
|
46
|
+
|
|
47
|
+
constructor(config: PhoenixConfig) {
|
|
48
|
+
this.config = {
|
|
49
|
+
url: "http://localhost:6006",
|
|
50
|
+
batch: true,
|
|
51
|
+
...config,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async initialize(): Promise<void> {
|
|
56
|
+
// Dynamically import OTel dependencies
|
|
57
|
+
const [{ register }, otelApi] = await Promise.all([
|
|
58
|
+
import("@arizeai/phoenix-otel"),
|
|
59
|
+
import("@opentelemetry/api"),
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
this.otelApi = otelApi;
|
|
63
|
+
this.phoenixOtel = { register } as typeof import("@arizeai/phoenix-otel");
|
|
64
|
+
|
|
65
|
+
this.tracerProvider = register({
|
|
66
|
+
projectName: this.config.projectName,
|
|
67
|
+
url: this.config.url,
|
|
68
|
+
apiKey: this.config.apiKey,
|
|
69
|
+
batch: this.config.batch,
|
|
70
|
+
headers: this.config.headers,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async shutdown(): Promise<void> {
|
|
75
|
+
if (this.tracerProvider?.forceFlush) {
|
|
76
|
+
await this.tracerProvider.forceFlush();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
startSpan(context: SpanContext): Span {
|
|
81
|
+
if (!this.otelApi) {
|
|
82
|
+
throw new Error("PhoenixAdapter not initialized");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const tracer = this.otelApi.trace.getTracer("struktur");
|
|
86
|
+
|
|
87
|
+
const spanKind = context.kind;
|
|
88
|
+
const otelSpan = tracer.startSpan(context.name, {
|
|
89
|
+
attributes: {
|
|
90
|
+
"openinference.span.kind": spanKind,
|
|
91
|
+
...context.attributes,
|
|
92
|
+
},
|
|
93
|
+
}) as OtelSpan;
|
|
94
|
+
|
|
95
|
+
const spanContext = otelSpan.spanContext();
|
|
96
|
+
const span: Span = {
|
|
97
|
+
id: spanContext.spanId,
|
|
98
|
+
traceId: spanContext.traceId,
|
|
99
|
+
name: context.name,
|
|
100
|
+
kind: context.kind,
|
|
101
|
+
startTime: context.startTime ?? Date.now(),
|
|
102
|
+
parentId: context.parentSpan?.id,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
this.activeSpans.set(span.id, otelSpan);
|
|
106
|
+
return span;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
endSpan(span: Span, result?: SpanResult): void {
|
|
110
|
+
const otelSpan = this.activeSpans.get(span.id);
|
|
111
|
+
if (!otelSpan) return;
|
|
112
|
+
|
|
113
|
+
if (result) {
|
|
114
|
+
// OTel status codes: 1 = OK, 2 = ERROR
|
|
115
|
+
otelSpan.setStatus({
|
|
116
|
+
code: result.status === "ok" ? 1 : 2,
|
|
117
|
+
message: result.error?.message,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (result.output !== undefined) {
|
|
121
|
+
try {
|
|
122
|
+
const outputStr = typeof result.output === "string"
|
|
123
|
+
? result.output
|
|
124
|
+
: JSON.stringify(result.output);
|
|
125
|
+
otelSpan.setAttribute("output.value", outputStr);
|
|
126
|
+
} catch {
|
|
127
|
+
otelSpan.setAttribute("output.value", "[object]");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (result.latencyMs !== undefined) {
|
|
132
|
+
otelSpan.setAttribute("latency_ms", result.latencyMs);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
otelSpan.end();
|
|
137
|
+
this.activeSpans.delete(span.id);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
recordEvent(span: Span, event: TelemetryEvent): void {
|
|
141
|
+
const otelSpan = this.activeSpans.get(span.id);
|
|
142
|
+
if (!otelSpan) return;
|
|
143
|
+
|
|
144
|
+
switch (event.type) {
|
|
145
|
+
case "llm_call":
|
|
146
|
+
this.recordLLMCall(otelSpan, event);
|
|
147
|
+
break;
|
|
148
|
+
case "validation":
|
|
149
|
+
this.recordValidation(otelSpan, event);
|
|
150
|
+
break;
|
|
151
|
+
case "chunk":
|
|
152
|
+
this.recordChunk(otelSpan, event);
|
|
153
|
+
break;
|
|
154
|
+
case "tool_call":
|
|
155
|
+
this.recordToolCall(otelSpan, event);
|
|
156
|
+
break;
|
|
157
|
+
case "merge":
|
|
158
|
+
this.recordMerge(otelSpan, event);
|
|
159
|
+
break;
|
|
160
|
+
case "parse":
|
|
161
|
+
this.recordParse(otelSpan, event);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
setAttributes(span: Span, attributes: Record<string, unknown>): void {
|
|
167
|
+
const otelSpan = this.activeSpans.get(span.id);
|
|
168
|
+
if (!otelSpan) return;
|
|
169
|
+
|
|
170
|
+
const stringAttrs: Record<string, string | number | boolean> = {};
|
|
171
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
172
|
+
if (value !== undefined && value !== null) {
|
|
173
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
174
|
+
stringAttrs[key] = value;
|
|
175
|
+
} else {
|
|
176
|
+
try {
|
|
177
|
+
stringAttrs[key] = JSON.stringify(value);
|
|
178
|
+
} catch {
|
|
179
|
+
stringAttrs[key] = String(value);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
otelSpan.setAttributes(stringAttrs);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
setContext(_context: TelemetryContext): void {
|
|
189
|
+
// Phoenix/OpenInference supports context via OTel context propagation
|
|
190
|
+
// This would require setting up context managers
|
|
191
|
+
// For now, attributes can be set on spans directly
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private recordLLMCall(span: OtelSpan, event: LLMCallEvent): void {
|
|
195
|
+
const attrs: Record<string, string | number | boolean> = {
|
|
196
|
+
"llm.model_name": event.model,
|
|
197
|
+
"llm.provider": event.provider,
|
|
198
|
+
"llm.temperature": event.input.temperature ?? "",
|
|
199
|
+
"llm.max_tokens": event.input.maxTokens ?? "",
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Record input messages
|
|
203
|
+
if (event.input.messages.length > 0) {
|
|
204
|
+
attrs["llm.input_messages"] = JSON.stringify(event.input.messages);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Record schema if present
|
|
208
|
+
if (event.input.schema) {
|
|
209
|
+
try {
|
|
210
|
+
attrs["llm.schema"] = JSON.stringify(event.input.schema);
|
|
211
|
+
} catch {
|
|
212
|
+
// Ignore schema serialization errors
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Record output
|
|
217
|
+
if (event.output) {
|
|
218
|
+
attrs["output.value"] = event.output.content;
|
|
219
|
+
attrs["llm.structured_output"] = event.output.structured ?? false;
|
|
220
|
+
|
|
221
|
+
// Record token usage
|
|
222
|
+
if (event.output.usage) {
|
|
223
|
+
this.setTokenUsageAttrs(attrs, event.output.usage);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
attrs["latency_ms"] = event.latencyMs;
|
|
228
|
+
|
|
229
|
+
if (event.error) {
|
|
230
|
+
span.recordException(event.error);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
span.setAttributes(attrs);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private setTokenUsageAttrs(attrs: Record<string, string | number | boolean>, usage: TokenUsage): void {
|
|
237
|
+
attrs["llm.token_count.prompt"] = usage.input;
|
|
238
|
+
attrs["llm.token_count.completion"] = usage.output;
|
|
239
|
+
attrs["llm.token_count.total"] = usage.total;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private recordValidation(span: OtelSpan, event: ValidationEvent): void {
|
|
243
|
+
const attrs: Record<string, string | number | boolean> = {
|
|
244
|
+
"validation.attempt": event.attempt,
|
|
245
|
+
"validation.max_attempts": event.maxAttempts,
|
|
246
|
+
"validation.success": event.success,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if (event.errors && event.errors.length > 0) {
|
|
250
|
+
attrs["validation.errors"] = JSON.stringify(event.errors);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (event.latencyMs !== undefined) {
|
|
254
|
+
attrs["latency_ms"] = event.latencyMs;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
span.setAttributes(attrs);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private recordChunk(span: OtelSpan, event: ChunkEvent): void {
|
|
261
|
+
span.setAttributes({
|
|
262
|
+
"chunk.index": event.chunkIndex,
|
|
263
|
+
"chunk.total": event.totalChunks,
|
|
264
|
+
"chunk.tokens": event.tokens,
|
|
265
|
+
"chunk.images": event.images,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (event.content) {
|
|
269
|
+
span.setAttribute("chunk.content_preview", event.content.slice(0, 1000));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private recordToolCall(span: OtelSpan, event: ToolCallEvent): void {
|
|
274
|
+
const attrs: Record<string, string | number | boolean> = {
|
|
275
|
+
"tool.name": event.toolName,
|
|
276
|
+
"tool.args": JSON.stringify(event.args),
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
if (event.result !== undefined) {
|
|
280
|
+
try {
|
|
281
|
+
attrs["tool.result"] = JSON.stringify(event.result);
|
|
282
|
+
} catch {
|
|
283
|
+
attrs["tool.result"] = "[object]";
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (event.error) {
|
|
288
|
+
attrs["tool.error"] = event.error.message;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (event.latencyMs !== undefined) {
|
|
292
|
+
attrs["latency_ms"] = event.latencyMs;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
span.setAttributes(attrs);
|
|
296
|
+
|
|
297
|
+
if (event.error) {
|
|
298
|
+
span.recordException(event.error);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private recordMerge(span: OtelSpan, event: MergeEvent): void {
|
|
303
|
+
const attrs: Record<string, string | number | boolean> = {
|
|
304
|
+
"merge.strategy": event.strategy,
|
|
305
|
+
"merge.input_count": event.inputCount,
|
|
306
|
+
"merge.output_count": event.outputCount,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
if (event.deduped !== undefined) {
|
|
310
|
+
attrs["merge.deduped"] = event.deduped;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
span.setAttributes(attrs);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private recordParse(span: OtelSpan, event: ParseEvent): void {
|
|
317
|
+
span.setAttributes({
|
|
318
|
+
"parse.mime_type": event.mimeType,
|
|
319
|
+
"parse.parser": event.parser,
|
|
320
|
+
"parse.input_size": event.inputSize,
|
|
321
|
+
"parse.output_tokens": event.outputTokens,
|
|
322
|
+
"parse.output_images": event.outputImages,
|
|
323
|
+
"latency_ms": event.latencyMs,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Create a Phoenix telemetry adapter
|
|
330
|
+
*
|
|
331
|
+
* @param config - Phoenix configuration
|
|
332
|
+
* @returns Phoenix telemetry adapter
|
|
333
|
+
*/
|
|
334
|
+
export function createPhoenixAdapter(config: PhoenixConfig): PhoenixAdapter {
|
|
335
|
+
return new PhoenixAdapter(config);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export type { PhoenixConfig };
|