@observyze/sdk 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/LICENSE +21 -0
- package/README.md +10 -175
- package/dist/chunk-YRMQCX2P.mjs +372 -0
- package/dist/index-DEorAmFu.d.mts +405 -0
- package/dist/index-DEorAmFu.d.ts +405 -0
- package/dist/index.d.mts +60 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +1029 -0
- package/dist/index.mjs +673 -0
- package/dist/opentelemetry/index.d.mts +1 -0
- package/dist/opentelemetry/index.d.ts +1 -0
- package/dist/opentelemetry/index.js +335 -0
- package/dist/opentelemetry/index.mjs +6 -0
- package/package.json +15 -8
- package/INSTRUMENTATION_SUMMARY.md +0 -184
- package/examples/auto-instrumentation.ts +0 -210
- package/src/client.ts +0 -578
- package/src/index.ts +0 -21
- package/src/instrumentation/README.md +0 -227
- package/src/instrumentation/anthropic.ts +0 -233
- package/src/instrumentation/index.ts +0 -43
- package/src/instrumentation/openai.ts +0 -193
- package/src/trace.ts +0 -242
- package/src/types.ts +0 -102
- package/tsconfig.json +0 -14
package/dist/index.js
ADDED
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __esm = (fn, res) => function __init() {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
};
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
|
+
|
|
23
|
+
// src/types.ts
|
|
24
|
+
var SpanType, TraceStatus;
|
|
25
|
+
var init_types = __esm({
|
|
26
|
+
"src/types.ts"() {
|
|
27
|
+
"use strict";
|
|
28
|
+
SpanType = /* @__PURE__ */ ((SpanType3) => {
|
|
29
|
+
SpanType3["LLM"] = "llm";
|
|
30
|
+
SpanType3["TOOL"] = "tool";
|
|
31
|
+
SpanType3["AGENT"] = "agent";
|
|
32
|
+
SpanType3["CHAIN"] = "chain";
|
|
33
|
+
SpanType3["RETRIEVAL"] = "retrieval";
|
|
34
|
+
return SpanType3;
|
|
35
|
+
})(SpanType || {});
|
|
36
|
+
TraceStatus = /* @__PURE__ */ ((TraceStatus2) => {
|
|
37
|
+
TraceStatus2["SUCCESS"] = "success";
|
|
38
|
+
TraceStatus2["ERROR"] = "error";
|
|
39
|
+
TraceStatus2["TIMEOUT"] = "timeout";
|
|
40
|
+
TraceStatus2["RUNNING"] = "running";
|
|
41
|
+
return TraceStatus2;
|
|
42
|
+
})(TraceStatus || {});
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// src/instrumentation/openai.ts
|
|
47
|
+
function wrapOpenAI(client, nwClient) {
|
|
48
|
+
const anyClient = client;
|
|
49
|
+
const originalCreate = client.chat.completions.create.bind(client.chat.completions);
|
|
50
|
+
client.chat.completions.create = async function(params, options) {
|
|
51
|
+
const trace = nwClient.startTrace(`openai.chat.completions.create`, {
|
|
52
|
+
provider: "openai",
|
|
53
|
+
model: params.model
|
|
54
|
+
});
|
|
55
|
+
const span = trace.startSpan("chat.completions.create", "llm" /* LLM */);
|
|
56
|
+
span.setMetadata("model", params.model);
|
|
57
|
+
span.setMetadata("provider", "openai");
|
|
58
|
+
if (params.temperature !== void 0) span.setMetadata("temperature", params.temperature);
|
|
59
|
+
if (params.max_tokens !== void 0) span.setMetadata("max_tokens", params.max_tokens);
|
|
60
|
+
span.setInput({
|
|
61
|
+
model: params.model,
|
|
62
|
+
messages: params.messages,
|
|
63
|
+
temperature: params.temperature,
|
|
64
|
+
max_tokens: params.max_tokens
|
|
65
|
+
});
|
|
66
|
+
const startTime = Date.now();
|
|
67
|
+
try {
|
|
68
|
+
const response = await originalCreate(params, options);
|
|
69
|
+
if (params.stream) {
|
|
70
|
+
return wrapOpenAIStream(response, span, trace, startTime);
|
|
71
|
+
}
|
|
72
|
+
const completionResponse = response;
|
|
73
|
+
const latency = Date.now() - startTime;
|
|
74
|
+
span.setOutput({
|
|
75
|
+
id: completionResponse.id,
|
|
76
|
+
model: completionResponse.model,
|
|
77
|
+
choices: completionResponse.choices
|
|
78
|
+
});
|
|
79
|
+
if (completionResponse.usage) {
|
|
80
|
+
span.setTokens({
|
|
81
|
+
input: completionResponse.usage.prompt_tokens,
|
|
82
|
+
output: completionResponse.usage.completion_tokens,
|
|
83
|
+
total: completionResponse.usage.total_tokens
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
span.setMetadata("latency_ms", latency);
|
|
87
|
+
span.end();
|
|
88
|
+
trace.end();
|
|
89
|
+
return response;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const latency = Date.now() - startTime;
|
|
92
|
+
span.setMetadata("latency_ms", latency);
|
|
93
|
+
span.setError(error);
|
|
94
|
+
span.end();
|
|
95
|
+
trace.end();
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
return client;
|
|
100
|
+
}
|
|
101
|
+
function wrapOpenAIStream(stream, span, trace, startTime) {
|
|
102
|
+
const bufferedChunks = [];
|
|
103
|
+
let streamId = "";
|
|
104
|
+
let streamModel = "";
|
|
105
|
+
return {
|
|
106
|
+
[Symbol.asyncIterator]: async function* () {
|
|
107
|
+
try {
|
|
108
|
+
for await (const chunk of stream) {
|
|
109
|
+
if (chunk.id) streamId = chunk.id;
|
|
110
|
+
if (chunk.model) streamModel = chunk.model;
|
|
111
|
+
const delta = chunk.choices[0]?.delta;
|
|
112
|
+
if (delta?.content) {
|
|
113
|
+
bufferedChunks.push(delta.content);
|
|
114
|
+
}
|
|
115
|
+
yield chunk;
|
|
116
|
+
}
|
|
117
|
+
const latency = Date.now() - startTime;
|
|
118
|
+
const completeOutput = bufferedChunks.join("");
|
|
119
|
+
span.setOutput({
|
|
120
|
+
id: streamId,
|
|
121
|
+
model: streamModel,
|
|
122
|
+
content: completeOutput
|
|
123
|
+
});
|
|
124
|
+
span.setMetadata("latency_ms", latency);
|
|
125
|
+
span.setMetadata("streaming", true);
|
|
126
|
+
span.end();
|
|
127
|
+
trace.end();
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const latency = Date.now() - startTime;
|
|
130
|
+
span.setMetadata("latency_ms", latency);
|
|
131
|
+
span.setError(error);
|
|
132
|
+
span.end();
|
|
133
|
+
trace.end();
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
var init_openai = __esm({
|
|
140
|
+
"src/instrumentation/openai.ts"() {
|
|
141
|
+
"use strict";
|
|
142
|
+
init_types();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// src/instrumentation/anthropic.ts
|
|
147
|
+
function wrapAnthropic(client, nwClient) {
|
|
148
|
+
const anyClient = client;
|
|
149
|
+
const originalCreate = client.messages.create.bind(client.messages);
|
|
150
|
+
client.messages.create = async function(params, options) {
|
|
151
|
+
const trace = nwClient.startTrace(`anthropic.messages.create`, {
|
|
152
|
+
provider: "anthropic",
|
|
153
|
+
model: params.model
|
|
154
|
+
});
|
|
155
|
+
const span = trace.startSpan("messages.create", "llm" /* LLM */);
|
|
156
|
+
span.setMetadata("model", params.model);
|
|
157
|
+
span.setMetadata("provider", "anthropic");
|
|
158
|
+
if (params.temperature !== void 0) span.setMetadata("temperature", params.temperature);
|
|
159
|
+
if (params.max_tokens !== void 0) span.setMetadata("max_tokens", params.max_tokens);
|
|
160
|
+
if (params.system !== void 0) span.setMetadata("system", params.system);
|
|
161
|
+
span.setInput({
|
|
162
|
+
model: params.model,
|
|
163
|
+
messages: params.messages,
|
|
164
|
+
max_tokens: params.max_tokens,
|
|
165
|
+
temperature: params.temperature,
|
|
166
|
+
system: params.system
|
|
167
|
+
});
|
|
168
|
+
const startTime = Date.now();
|
|
169
|
+
try {
|
|
170
|
+
const response = await originalCreate(params, options);
|
|
171
|
+
if (params.stream) {
|
|
172
|
+
return wrapAnthropicStream(response, span, trace, startTime);
|
|
173
|
+
}
|
|
174
|
+
const messageResponse = response;
|
|
175
|
+
const latency = Date.now() - startTime;
|
|
176
|
+
span.setOutput({
|
|
177
|
+
id: messageResponse.id,
|
|
178
|
+
model: messageResponse.model,
|
|
179
|
+
role: messageResponse.role,
|
|
180
|
+
content: messageResponse.content,
|
|
181
|
+
stop_reason: messageResponse.stop_reason
|
|
182
|
+
});
|
|
183
|
+
if (messageResponse.usage) {
|
|
184
|
+
span.setTokens({
|
|
185
|
+
input: messageResponse.usage.input_tokens,
|
|
186
|
+
output: messageResponse.usage.output_tokens,
|
|
187
|
+
total: messageResponse.usage.input_tokens + messageResponse.usage.output_tokens
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
span.setMetadata("latency_ms", latency);
|
|
191
|
+
span.end();
|
|
192
|
+
trace.end();
|
|
193
|
+
return response;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
const latency = Date.now() - startTime;
|
|
196
|
+
span.setMetadata("latency_ms", latency);
|
|
197
|
+
span.setError(error);
|
|
198
|
+
span.end();
|
|
199
|
+
trace.end();
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
return client;
|
|
204
|
+
}
|
|
205
|
+
function wrapAnthropicStream(stream, span, trace, startTime) {
|
|
206
|
+
const bufferedChunks = [];
|
|
207
|
+
let messageId = "";
|
|
208
|
+
let messageModel = "";
|
|
209
|
+
let stopReason = null;
|
|
210
|
+
let inputTokens = 0;
|
|
211
|
+
let outputTokens = 0;
|
|
212
|
+
return {
|
|
213
|
+
[Symbol.asyncIterator]: async function* () {
|
|
214
|
+
try {
|
|
215
|
+
for await (const event of stream) {
|
|
216
|
+
if (event.type === "message_start" && event.message) {
|
|
217
|
+
messageId = event.message.id;
|
|
218
|
+
messageModel = event.message.model;
|
|
219
|
+
if (event.message.usage) {
|
|
220
|
+
inputTokens = event.message.usage.input_tokens;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (event.type === "content_block_delta" && event.delta?.text) {
|
|
224
|
+
bufferedChunks.push(event.delta.text);
|
|
225
|
+
}
|
|
226
|
+
if (event.type === "message_delta" && event.delta) {
|
|
227
|
+
if (event.delta.stop_reason) {
|
|
228
|
+
stopReason = event.delta.stop_reason;
|
|
229
|
+
}
|
|
230
|
+
if (event.usage?.output_tokens) {
|
|
231
|
+
outputTokens = event.usage.output_tokens;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
yield event;
|
|
235
|
+
}
|
|
236
|
+
const latency = Date.now() - startTime;
|
|
237
|
+
const completeOutput = bufferedChunks.join("");
|
|
238
|
+
span.setOutput({
|
|
239
|
+
id: messageId,
|
|
240
|
+
model: messageModel,
|
|
241
|
+
content: completeOutput,
|
|
242
|
+
stop_reason: stopReason
|
|
243
|
+
});
|
|
244
|
+
if (inputTokens > 0 || outputTokens > 0) {
|
|
245
|
+
span.setTokens({
|
|
246
|
+
input: inputTokens,
|
|
247
|
+
output: outputTokens,
|
|
248
|
+
total: inputTokens + outputTokens
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
span.setMetadata("latency_ms", latency);
|
|
252
|
+
span.setMetadata("streaming", true);
|
|
253
|
+
span.end();
|
|
254
|
+
trace.end();
|
|
255
|
+
} catch (error) {
|
|
256
|
+
const latency = Date.now() - startTime;
|
|
257
|
+
span.setMetadata("latency_ms", latency);
|
|
258
|
+
span.setError(error);
|
|
259
|
+
span.end();
|
|
260
|
+
trace.end();
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
var init_anthropic = __esm({
|
|
267
|
+
"src/instrumentation/anthropic.ts"() {
|
|
268
|
+
"use strict";
|
|
269
|
+
init_types();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// src/instrumentation/index.ts
|
|
274
|
+
var instrumentation_exports = {};
|
|
275
|
+
__export(instrumentation_exports, {
|
|
276
|
+
wrap: () => wrap,
|
|
277
|
+
wrapAnthropic: () => wrapAnthropic,
|
|
278
|
+
wrapOpenAI: () => wrapOpenAI
|
|
279
|
+
});
|
|
280
|
+
function wrap(client, nwClient) {
|
|
281
|
+
if ("chat" in client && client.chat && "completions" in client.chat) {
|
|
282
|
+
return wrapOpenAI(client, nwClient);
|
|
283
|
+
}
|
|
284
|
+
if ("messages" in client && client.messages && "create" in client.messages) {
|
|
285
|
+
return wrapAnthropic(client, nwClient);
|
|
286
|
+
}
|
|
287
|
+
throw new Error(
|
|
288
|
+
"Observyze SDK: Unsupported client type. Supported clients: OpenAI, Anthropic"
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
var init_instrumentation = __esm({
|
|
292
|
+
"src/instrumentation/index.ts"() {
|
|
293
|
+
"use strict";
|
|
294
|
+
init_openai();
|
|
295
|
+
init_anthropic();
|
|
296
|
+
init_openai();
|
|
297
|
+
init_anthropic();
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// src/index.ts
|
|
302
|
+
var index_exports = {};
|
|
303
|
+
__export(index_exports, {
|
|
304
|
+
ObservyzeClient: () => ObservyzeClient,
|
|
305
|
+
ObservyzeSpanExporter: () => ObservyzeSpanExporter,
|
|
306
|
+
Span: () => Span,
|
|
307
|
+
SpanType: () => SpanType,
|
|
308
|
+
Trace: () => Trace,
|
|
309
|
+
TraceStatus: () => TraceStatus,
|
|
310
|
+
wrap: () => wrap,
|
|
311
|
+
wrapAnthropic: () => wrapAnthropic,
|
|
312
|
+
wrapOpenAI: () => wrapOpenAI
|
|
313
|
+
});
|
|
314
|
+
module.exports = __toCommonJS(index_exports);
|
|
315
|
+
|
|
316
|
+
// src/trace.ts
|
|
317
|
+
init_types();
|
|
318
|
+
var import_crypto = require("crypto");
|
|
319
|
+
function generateId() {
|
|
320
|
+
return `${Date.now()}-${(0, import_crypto.randomUUID)().substring(0, 8)}`;
|
|
321
|
+
}
|
|
322
|
+
var Span = class {
|
|
323
|
+
data;
|
|
324
|
+
startTime;
|
|
325
|
+
constructor(name, type, parentSpanId) {
|
|
326
|
+
this.startTime = Date.now();
|
|
327
|
+
this.data = {
|
|
328
|
+
span_id: generateId(),
|
|
329
|
+
parent_span_id: parentSpanId,
|
|
330
|
+
name,
|
|
331
|
+
type,
|
|
332
|
+
start_time: new Date(this.startTime),
|
|
333
|
+
end_time: new Date(this.startTime),
|
|
334
|
+
// Will be updated on end()
|
|
335
|
+
duration_ms: 0,
|
|
336
|
+
input: null,
|
|
337
|
+
output: null,
|
|
338
|
+
metadata: {}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Set the input data for this span
|
|
343
|
+
*/
|
|
344
|
+
setInput(input) {
|
|
345
|
+
this.data.input = input;
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Set the output data for this span
|
|
350
|
+
*/
|
|
351
|
+
setOutput(output) {
|
|
352
|
+
this.data.output = output;
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Record an error that occurred during span execution
|
|
357
|
+
*/
|
|
358
|
+
setError(error) {
|
|
359
|
+
this.data.error = {
|
|
360
|
+
message: error.message,
|
|
361
|
+
stack: error.stack,
|
|
362
|
+
code: error.code
|
|
363
|
+
};
|
|
364
|
+
return this;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Set metadata for this span
|
|
368
|
+
*/
|
|
369
|
+
setMetadata(key, value) {
|
|
370
|
+
this.data.metadata[key] = value;
|
|
371
|
+
return this;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Set multiple metadata fields at once
|
|
375
|
+
*/
|
|
376
|
+
setMetadataAll(metadata) {
|
|
377
|
+
this.data.metadata = { ...this.data.metadata, ...metadata };
|
|
378
|
+
return this;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Set token usage information
|
|
382
|
+
*/
|
|
383
|
+
setTokens(tokens) {
|
|
384
|
+
this.data.tokens = tokens;
|
|
385
|
+
return this;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* End the span and calculate duration
|
|
389
|
+
*/
|
|
390
|
+
end() {
|
|
391
|
+
const endTime = Date.now();
|
|
392
|
+
this.data.end_time = new Date(endTime);
|
|
393
|
+
this.data.duration_ms = endTime - this.startTime;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Get the span ID
|
|
397
|
+
*/
|
|
398
|
+
get id() {
|
|
399
|
+
return this.data.span_id;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Get the span data for serialization
|
|
403
|
+
*/
|
|
404
|
+
toJSON() {
|
|
405
|
+
return { ...this.data };
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
var Trace = class {
|
|
409
|
+
data;
|
|
410
|
+
startTime;
|
|
411
|
+
spans = [];
|
|
412
|
+
ended = false;
|
|
413
|
+
constructor(name, organizationId, projectId) {
|
|
414
|
+
this.startTime = Date.now();
|
|
415
|
+
this.data = {
|
|
416
|
+
trace_id: generateId(),
|
|
417
|
+
organization_id: organizationId,
|
|
418
|
+
project_id: projectId,
|
|
419
|
+
name,
|
|
420
|
+
status: "running" /* RUNNING */,
|
|
421
|
+
start_time: new Date(this.startTime),
|
|
422
|
+
end_time: new Date(this.startTime),
|
|
423
|
+
// Will be updated on end()
|
|
424
|
+
duration_ms: 0,
|
|
425
|
+
metadata: {},
|
|
426
|
+
spans: [],
|
|
427
|
+
tags: []
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Start a new span within this trace
|
|
432
|
+
*/
|
|
433
|
+
startSpan(name, type, parentSpanId) {
|
|
434
|
+
if (this.ended) {
|
|
435
|
+
throw new Error("Cannot start span on an ended trace");
|
|
436
|
+
}
|
|
437
|
+
const span = new Span(name, type, parentSpanId);
|
|
438
|
+
this.spans.push(span);
|
|
439
|
+
return span;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Add metadata to the trace
|
|
443
|
+
*/
|
|
444
|
+
setMetadata(key, value) {
|
|
445
|
+
this.data.metadata[key] = value;
|
|
446
|
+
return this;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Set multiple metadata fields at once
|
|
450
|
+
*/
|
|
451
|
+
setMetadataAll(metadata) {
|
|
452
|
+
this.data.metadata = { ...this.data.metadata, ...metadata };
|
|
453
|
+
return this;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Add tags to the trace
|
|
457
|
+
*/
|
|
458
|
+
addTag(tag) {
|
|
459
|
+
if (!this.data.tags.includes(tag)) {
|
|
460
|
+
this.data.tags.push(tag);
|
|
461
|
+
}
|
|
462
|
+
return this;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Add multiple tags at once
|
|
466
|
+
*/
|
|
467
|
+
addTags(tags) {
|
|
468
|
+
tags.forEach((tag) => this.addTag(tag));
|
|
469
|
+
return this;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Set the user ID associated with this trace
|
|
473
|
+
*/
|
|
474
|
+
setUserId(userId) {
|
|
475
|
+
this.data.user_id = userId;
|
|
476
|
+
return this;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Set the session ID associated with this trace
|
|
480
|
+
*/
|
|
481
|
+
setSessionId(sessionId) {
|
|
482
|
+
this.data.session_id = sessionId;
|
|
483
|
+
return this;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* End the trace with a final status
|
|
487
|
+
*/
|
|
488
|
+
end(status = "success" /* SUCCESS */) {
|
|
489
|
+
if (this.ended) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const endTime = Date.now();
|
|
493
|
+
this.data.end_time = new Date(endTime);
|
|
494
|
+
this.data.duration_ms = endTime - this.startTime;
|
|
495
|
+
this.data.status = status;
|
|
496
|
+
this.data.spans = this.spans.map((span) => span.toJSON());
|
|
497
|
+
this.ended = true;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Get the trace ID
|
|
501
|
+
*/
|
|
502
|
+
get id() {
|
|
503
|
+
return this.data.trace_id;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Check if the trace has ended
|
|
507
|
+
*/
|
|
508
|
+
get isEnded() {
|
|
509
|
+
return this.ended;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Get the trace data for serialization
|
|
513
|
+
*/
|
|
514
|
+
toJSON() {
|
|
515
|
+
return { ...this.data };
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// src/client.ts
|
|
520
|
+
init_types();
|
|
521
|
+
var DEFAULT_CONFIG = {
|
|
522
|
+
endpoint: "https://api.observyze.com",
|
|
523
|
+
batchSize: 100,
|
|
524
|
+
flushInterval: 5e3,
|
|
525
|
+
enableAutoInstrumentation: true,
|
|
526
|
+
debug: false,
|
|
527
|
+
dryRun: false,
|
|
528
|
+
enablePiiRedaction: true
|
|
529
|
+
};
|
|
530
|
+
var ObservyzeClient = class _ObservyzeClient {
|
|
531
|
+
config;
|
|
532
|
+
traceBuffer = [];
|
|
533
|
+
flushTimer = null;
|
|
534
|
+
isShuttingDown = false;
|
|
535
|
+
MAX_QUEUE_SIZE = 1e3;
|
|
536
|
+
RETRY_DELAYS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
|
|
537
|
+
// ms: 1s → 2s → 4s → 8s → 16s → 30s
|
|
538
|
+
constructor(config) {
|
|
539
|
+
if (!config.apiKey) {
|
|
540
|
+
throw new Error("Observyze SDK: apiKey is required");
|
|
541
|
+
}
|
|
542
|
+
this.config = {
|
|
543
|
+
...DEFAULT_CONFIG,
|
|
544
|
+
...config,
|
|
545
|
+
organizationId: config.organizationId || "",
|
|
546
|
+
projectId: config.projectId || ""
|
|
547
|
+
};
|
|
548
|
+
this.startFlushTimer();
|
|
549
|
+
if (this.config.debug) {
|
|
550
|
+
console.log("[Observyze SDK] Initialized with config:", {
|
|
551
|
+
endpoint: this.config.endpoint,
|
|
552
|
+
batchSize: this.config.batchSize,
|
|
553
|
+
flushInterval: this.config.flushInterval,
|
|
554
|
+
dryRun: this.config.dryRun
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Start a new trace
|
|
560
|
+
*/
|
|
561
|
+
startTrace(name, metadata) {
|
|
562
|
+
const trace = new Trace(
|
|
563
|
+
name,
|
|
564
|
+
this.config.organizationId,
|
|
565
|
+
this.config.projectId
|
|
566
|
+
);
|
|
567
|
+
if (metadata) {
|
|
568
|
+
trace.setMetadataAll(metadata);
|
|
569
|
+
}
|
|
570
|
+
const originalEnd = trace.end.bind(trace);
|
|
571
|
+
trace.end = (status = "success" /* SUCCESS */) => {
|
|
572
|
+
originalEnd(status);
|
|
573
|
+
this.bufferTrace(trace);
|
|
574
|
+
};
|
|
575
|
+
return trace;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Buffer a completed trace for batch sending
|
|
579
|
+
*/
|
|
580
|
+
bufferTrace(trace) {
|
|
581
|
+
if (!trace.isEnded) {
|
|
582
|
+
if (this.config.debug) {
|
|
583
|
+
console.warn("[Observyze SDK] Attempted to buffer a trace that has not ended");
|
|
584
|
+
}
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (this.traceBuffer.length >= this.MAX_QUEUE_SIZE) {
|
|
588
|
+
if (this.config.debug) {
|
|
589
|
+
console.warn(`[Observyze SDK] Queue at max capacity (${this.MAX_QUEUE_SIZE}), dropping oldest trace`);
|
|
590
|
+
}
|
|
591
|
+
this.traceBuffer.shift();
|
|
592
|
+
}
|
|
593
|
+
this.traceBuffer.push(trace);
|
|
594
|
+
if (this.config.debug) {
|
|
595
|
+
console.log(`[Observyze SDK] Buffered trace ${trace.id} (${this.traceBuffer.length}/${this.config.batchSize})`);
|
|
596
|
+
}
|
|
597
|
+
if (this.traceBuffer.length >= this.config.batchSize) {
|
|
598
|
+
this.flush().catch((err) => {
|
|
599
|
+
console.error("[Observyze SDK] Error flushing buffer:", err);
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Start the auto-flush timer
|
|
605
|
+
*/
|
|
606
|
+
startFlushTimer() {
|
|
607
|
+
if (this.flushTimer) {
|
|
608
|
+
clearInterval(this.flushTimer);
|
|
609
|
+
}
|
|
610
|
+
this.flushTimer = setInterval(() => {
|
|
611
|
+
if (this.traceBuffer.length > 0) {
|
|
612
|
+
this.flush().catch((err) => {
|
|
613
|
+
console.error("[Observyze SDK] Error in auto-flush:", err);
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}, this.config.flushInterval);
|
|
617
|
+
if (this.flushTimer.unref) {
|
|
618
|
+
this.flushTimer.unref();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Flush all buffered traces to the Ingestion Service
|
|
623
|
+
*/
|
|
624
|
+
async flush() {
|
|
625
|
+
if (this.traceBuffer.length === 0) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const tracesToSend = this.traceBuffer.splice(0, this.config.batchSize);
|
|
629
|
+
if (this.config.debug) {
|
|
630
|
+
console.log(`[Observyze SDK] Flushing ${tracesToSend.length} traces`);
|
|
631
|
+
}
|
|
632
|
+
if (this.config.dryRun) {
|
|
633
|
+
if (this.config.debug) {
|
|
634
|
+
console.log("[Observyze SDK] Dry-run mode: traces not sent");
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
await this.sendWithRetry(tracesToSend);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
const remainingSpace = this.MAX_QUEUE_SIZE - this.traceBuffer.length;
|
|
642
|
+
if (remainingSpace > 0) {
|
|
643
|
+
this.traceBuffer.unshift(...tracesToSend.slice(0, remainingSpace));
|
|
644
|
+
if (this.config.debug) {
|
|
645
|
+
console.log(`[Observyze SDK] Re-queued ${Math.min(tracesToSend.length, remainingSpace)} traces after failure`);
|
|
646
|
+
}
|
|
647
|
+
} else {
|
|
648
|
+
if (this.config.debug) {
|
|
649
|
+
console.warn(`[Observyze SDK] Queue full, dropped ${tracesToSend.length} traces`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (this.config.debug) {
|
|
653
|
+
console.error("[Observyze SDK] Failed to send traces after retries:", error);
|
|
654
|
+
}
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Send traces with exponential backoff retry
|
|
660
|
+
*/
|
|
661
|
+
async sendWithRetry(traces) {
|
|
662
|
+
let lastError = null;
|
|
663
|
+
for (let attempt = 0; attempt < this.RETRY_DELAYS.length + 1; attempt++) {
|
|
664
|
+
try {
|
|
665
|
+
const response = await fetch(`${this.config.endpoint}/api/v1/ingest/batch`, {
|
|
666
|
+
method: "POST",
|
|
667
|
+
headers: {
|
|
668
|
+
"Content-Type": "application/json",
|
|
669
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
670
|
+
},
|
|
671
|
+
body: JSON.stringify({
|
|
672
|
+
traces: traces.map((trace) => {
|
|
673
|
+
const json = trace.toJSON();
|
|
674
|
+
if (this.config.enablePiiRedaction) {
|
|
675
|
+
json.spans = this.sanitizePII(json.spans);
|
|
676
|
+
}
|
|
677
|
+
return json;
|
|
678
|
+
})
|
|
679
|
+
})
|
|
680
|
+
});
|
|
681
|
+
if (!response.ok) {
|
|
682
|
+
const errorBody = await response.text();
|
|
683
|
+
throw new Error(`Ingestion failed: ${response.status} ${errorBody}`);
|
|
684
|
+
}
|
|
685
|
+
if (this.config.debug) {
|
|
686
|
+
console.log(`[Observyze SDK] Successfully sent ${traces.length} traces${attempt > 0 ? ` (after ${attempt} retries)` : ""}`);
|
|
687
|
+
}
|
|
688
|
+
return;
|
|
689
|
+
} catch (error) {
|
|
690
|
+
lastError = error;
|
|
691
|
+
if (attempt >= this.RETRY_DELAYS.length) {
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
const delay = this.RETRY_DELAYS[attempt];
|
|
695
|
+
if (this.config.debug) {
|
|
696
|
+
console.warn(`[Observyze SDK] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, error);
|
|
697
|
+
}
|
|
698
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
throw lastError || new Error("Failed to send traces after all retries");
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Shutdown the SDK and flush remaining traces
|
|
705
|
+
*/
|
|
706
|
+
async shutdown() {
|
|
707
|
+
if (this.isShuttingDown) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
this.isShuttingDown = true;
|
|
711
|
+
if (this.config.debug) {
|
|
712
|
+
console.log("[Observyze SDK] Shutting down...");
|
|
713
|
+
}
|
|
714
|
+
if (this.flushTimer) {
|
|
715
|
+
clearInterval(this.flushTimer);
|
|
716
|
+
this.flushTimer = null;
|
|
717
|
+
}
|
|
718
|
+
try {
|
|
719
|
+
await this.flush();
|
|
720
|
+
} catch (error) {
|
|
721
|
+
console.error("[Observyze SDK] Error during shutdown flush:", error);
|
|
722
|
+
}
|
|
723
|
+
if (this.config.debug) {
|
|
724
|
+
console.log("[Observyze SDK] Shutdown complete");
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Get current buffer size
|
|
729
|
+
*/
|
|
730
|
+
get bufferSize() {
|
|
731
|
+
return this.traceBuffer.length;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Get SDK configuration
|
|
735
|
+
*/
|
|
736
|
+
getConfig() {
|
|
737
|
+
return { ...this.config };
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Wrap an LLM client (OpenAI, Anthropic) to enable auto-instrumentation
|
|
741
|
+
*
|
|
742
|
+
* @example
|
|
743
|
+
* ```typescript
|
|
744
|
+
* import OpenAI from 'openai'
|
|
745
|
+
* import { ObservyzeClient } from '@observyze/sdk'
|
|
746
|
+
*
|
|
747
|
+
* const nw = new ObservyzeClient({ apiKey: 'your-api-key' })
|
|
748
|
+
* const openai = new OpenAI({ apiKey: 'openai-key' })
|
|
749
|
+
*
|
|
750
|
+
* // Wrap the client to enable auto-instrumentation
|
|
751
|
+
* nw.wrap(openai)
|
|
752
|
+
*
|
|
753
|
+
* // All calls are now automatically traced
|
|
754
|
+
* const response = await openai.chat.completions.create({
|
|
755
|
+
* model: 'gpt-4',
|
|
756
|
+
* messages: [{ role: 'user', content: 'Hello!' }]
|
|
757
|
+
* })
|
|
758
|
+
* ```
|
|
759
|
+
*/
|
|
760
|
+
wrap(client) {
|
|
761
|
+
const { wrap: wrapClient } = (init_instrumentation(), __toCommonJS(instrumentation_exports));
|
|
762
|
+
return wrapClient(client, this);
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Sync local agent .history file to Observyze cloud
|
|
766
|
+
* Parses JSON/NDJSON agent history and sends to ingestion endpoint.
|
|
767
|
+
*/
|
|
768
|
+
async syncLocalHistory(filePath) {
|
|
769
|
+
try {
|
|
770
|
+
if (typeof process === "undefined" || !process.versions?.node) {
|
|
771
|
+
throw new Error("syncLocalHistory is only available in Node.js environments");
|
|
772
|
+
}
|
|
773
|
+
const fs = require("fs");
|
|
774
|
+
const path = require("path");
|
|
775
|
+
const fullPath = path.resolve(process.cwd(), filePath);
|
|
776
|
+
if (!fs.existsSync(fullPath)) {
|
|
777
|
+
throw new Error(`History file not found: ${fullPath}`);
|
|
778
|
+
}
|
|
779
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
780
|
+
let items = [];
|
|
781
|
+
try {
|
|
782
|
+
items = JSON.parse(content);
|
|
783
|
+
} catch (e) {
|
|
784
|
+
items = content.split("\n").filter((l) => l.trim()).map((l) => JSON.parse(l));
|
|
785
|
+
}
|
|
786
|
+
if (!Array.isArray(items)) {
|
|
787
|
+
items = [items];
|
|
788
|
+
}
|
|
789
|
+
if (this.config.debug) {
|
|
790
|
+
console.log(`[Observyze SDK] Syncing ${items.length} traces from ${filePath}`);
|
|
791
|
+
}
|
|
792
|
+
for (let i = 0; i < items.length; i += this.config.batchSize) {
|
|
793
|
+
const batch = items.slice(i, i + this.config.batchSize);
|
|
794
|
+
const response = await fetch(`${this.config.endpoint}/api/v1/ingest/batch`, {
|
|
795
|
+
method: "POST",
|
|
796
|
+
headers: {
|
|
797
|
+
"Content-Type": "application/json",
|
|
798
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
799
|
+
},
|
|
800
|
+
body: JSON.stringify({ traces: batch })
|
|
801
|
+
});
|
|
802
|
+
if (!response.ok) {
|
|
803
|
+
const errorBody = await response.text();
|
|
804
|
+
throw new Error(`Batch sync failed: ${response.status} ${errorBody}`);
|
|
805
|
+
}
|
|
806
|
+
if (this.config.debug) {
|
|
807
|
+
console.log(`[Observyze SDK] Synced batch of ${batch.length} traces from local history`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
} catch (err) {
|
|
811
|
+
console.error("[Observyze SDK] Failed to sync local history:", err);
|
|
812
|
+
throw err;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Industry-grade PII Redaction (Compliance & RBAC)
|
|
817
|
+
*
|
|
818
|
+
* Recursively scrubs PII from trace data before transmission to the cloud.
|
|
819
|
+
* Coverage: emails, JWTs, bearer tokens, AWS/API keys, SSNs, credit cards,
|
|
820
|
+
* phone numbers (US + E.164), IPv4/IPv6, passport numbers, ZIP codes, plus
|
|
821
|
+
* key-based redaction for sensitive JSON fields (password, token, api_key, etc.).
|
|
822
|
+
*
|
|
823
|
+
* Design:
|
|
824
|
+
* - Pure function, never mutates the original object
|
|
825
|
+
* - Depth-limited to 16 levels to prevent stack overflow on deep agent outputs
|
|
826
|
+
* - Key-aware: sensitive key names are fully redacted regardless of value format
|
|
827
|
+
*/
|
|
828
|
+
static PII_PATTERNS = [
|
|
829
|
+
{ pattern: /\b[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}\b/gi, label: "[EMAIL_REDACTED]" },
|
|
830
|
+
{ pattern: /\beyJ[A-Za-z0-9_\-]+\.eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\b/g, label: "[JWT_REDACTED]" },
|
|
831
|
+
{ pattern: /\b(Bearer|Token|Basic)\s+[A-Za-z0-9\-.~+\/]+=*\b/gi, label: "[AUTH_TOKEN_REDACTED]" },
|
|
832
|
+
{ pattern: /\b(AKIA|ASIA|AROA|ANPA|ANVA|AIDA)[A-Z0-9]{16}\b/g, label: "[AWS_KEY_REDACTED]" },
|
|
833
|
+
{ pattern: /\b(sk-[A-Za-z0-9\-]{20,}|pk-[A-Za-z0-9\-]{20,}|ob_[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{36,}|gho_[A-Za-z0-9]{36,})\b/g, label: "[API_KEY_REDACTED]" },
|
|
834
|
+
{ pattern: /\b\d{3}-\d{2}-\d{4}\b/g, label: "[SSN_REDACTED]" },
|
|
835
|
+
{ pattern: /\b(?:\d[ \-]?){13,18}\d\b/g, label: "[CC_REDACTED]" },
|
|
836
|
+
{ pattern: /(?:\+1[\s.\-]?)?\(?\d{3}\)?[\s.\-]?\d{3}[\s.\-]?\d{4}\b/g, label: "[PHONE_REDACTED]" },
|
|
837
|
+
{ pattern: /\+\d{1,3}[\s.\-]?\(?\d{1,4}\)?[\s.\-]?\d{1,4}[\s.\-]?\d{1,9}/g, label: "[PHONE_REDACTED]" },
|
|
838
|
+
{ pattern: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g, label: "[IP_REDACTED]" },
|
|
839
|
+
{ pattern: /\b(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}\b/g, label: "[IP_REDACTED]" }
|
|
840
|
+
];
|
|
841
|
+
static SENSITIVE_KEYS = /* @__PURE__ */ new Set([
|
|
842
|
+
"password",
|
|
843
|
+
"passwd",
|
|
844
|
+
"secret",
|
|
845
|
+
"token",
|
|
846
|
+
"apikey",
|
|
847
|
+
"api_key",
|
|
848
|
+
"accesstoken",
|
|
849
|
+
"access_token",
|
|
850
|
+
"refreshtoken",
|
|
851
|
+
"refresh_token",
|
|
852
|
+
"authorization",
|
|
853
|
+
"auth",
|
|
854
|
+
"credential",
|
|
855
|
+
"credentials",
|
|
856
|
+
"private_key",
|
|
857
|
+
"privatekey",
|
|
858
|
+
"client_secret",
|
|
859
|
+
"clientsecret",
|
|
860
|
+
"ssn",
|
|
861
|
+
"social_security",
|
|
862
|
+
"dob",
|
|
863
|
+
"date_of_birth",
|
|
864
|
+
"dateofbirth",
|
|
865
|
+
"passport",
|
|
866
|
+
"passport_number",
|
|
867
|
+
"credit_card",
|
|
868
|
+
"creditcard",
|
|
869
|
+
"card_number",
|
|
870
|
+
"cardnumber",
|
|
871
|
+
"cvv",
|
|
872
|
+
"cvc",
|
|
873
|
+
"pin",
|
|
874
|
+
"bank_account",
|
|
875
|
+
"routing_number"
|
|
876
|
+
]);
|
|
877
|
+
static isSensitiveKey(key) {
|
|
878
|
+
const normalized = key.toLowerCase().replace(/-/g, "_");
|
|
879
|
+
if (_ObservyzeClient.SENSITIVE_KEYS.has(normalized)) return true;
|
|
880
|
+
const segments = normalized.split("_");
|
|
881
|
+
for (const segment of segments) {
|
|
882
|
+
if (_ObservyzeClient.SENSITIVE_KEYS.has(segment)) return true;
|
|
883
|
+
}
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
sanitizePII(data, depth = 0) {
|
|
887
|
+
if (depth > 16) return "[MAX_DEPTH_EXCEEDED]";
|
|
888
|
+
if (data === null || data === void 0) return data;
|
|
889
|
+
if (typeof data === "string") {
|
|
890
|
+
let result = data;
|
|
891
|
+
for (const { pattern, label } of _ObservyzeClient.PII_PATTERNS) {
|
|
892
|
+
pattern.lastIndex = 0;
|
|
893
|
+
result = result.replace(pattern, label);
|
|
894
|
+
}
|
|
895
|
+
return result;
|
|
896
|
+
}
|
|
897
|
+
if (typeof data === "number" || typeof data === "boolean") return data;
|
|
898
|
+
if (Array.isArray(data)) return data.map((item) => this.sanitizePII(item, depth + 1));
|
|
899
|
+
if (typeof data === "object") {
|
|
900
|
+
const sanitized = {};
|
|
901
|
+
for (const [k, v] of Object.entries(data)) {
|
|
902
|
+
sanitized[k] = _ObservyzeClient.isSensitiveKey(k) ? "[REDACTED]" : this.sanitizePII(v, depth + 1);
|
|
903
|
+
}
|
|
904
|
+
return sanitized;
|
|
905
|
+
}
|
|
906
|
+
return data;
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
// src/index.ts
|
|
911
|
+
init_types();
|
|
912
|
+
init_instrumentation();
|
|
913
|
+
|
|
914
|
+
// src/opentelemetry/exporter.ts
|
|
915
|
+
init_types();
|
|
916
|
+
var ObservyzeSpanExporter = class {
|
|
917
|
+
client;
|
|
918
|
+
config;
|
|
919
|
+
constructor(client, config) {
|
|
920
|
+
this.client = client;
|
|
921
|
+
this.config = {
|
|
922
|
+
serviceName: config?.serviceName || "unknown-service",
|
|
923
|
+
projectId: config?.projectId || "",
|
|
924
|
+
defaultSpanType: config?.defaultSpanType || "llm",
|
|
925
|
+
headers: config?.headers || {}
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Export spans — called by OTel SDK when spans are ready.
|
|
930
|
+
* Converts OTel spans to Observyze traces and buffers them.
|
|
931
|
+
*/
|
|
932
|
+
async export(spans, resultCallback) {
|
|
933
|
+
if (!spans || spans.length === 0) {
|
|
934
|
+
resultCallback({ code: 0 });
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
try {
|
|
938
|
+
const organizationId = this.client.config?.organizationId || "";
|
|
939
|
+
const projectId = this.config.projectId || this.client.config?.projectId || "";
|
|
940
|
+
for (const span of spans) {
|
|
941
|
+
const spanContext = span.spanContext();
|
|
942
|
+
const traceId = spanContext?.traceId || span.spanId();
|
|
943
|
+
const trace = new Trace(
|
|
944
|
+
span.name || "otel-span",
|
|
945
|
+
organizationId,
|
|
946
|
+
projectId
|
|
947
|
+
);
|
|
948
|
+
trace.setMetadata("source", "opentelemetry");
|
|
949
|
+
trace.setMetadata("otel.trace_id", traceId);
|
|
950
|
+
trace.setMetadata("otel.span_id", spanContext?.spanId || "");
|
|
951
|
+
trace.setMetadata("service.name", this.config.serviceName);
|
|
952
|
+
trace.setMetadataAll(span.attributes || {});
|
|
953
|
+
if (span.resource?.attributes) {
|
|
954
|
+
trace.setMetadataAll(span.resource.attributes);
|
|
955
|
+
}
|
|
956
|
+
const input = span.attributes?.["gen_ai.prompt.0.content"] || span.attributes?.["gen_ai.completion.0.content"] || span.attributes?.["llm.input"] || void 0;
|
|
957
|
+
const output = span.attributes?.["gen_ai.completion.0.content"] || span.attributes?.["llm.output"] || void 0;
|
|
958
|
+
const model = span.attributes?.["gen_ai.request.model"] || span.attributes?.["llm.model"] || void 0;
|
|
959
|
+
const provider = span.attributes?.["gen_ai.request.provider"] || span.attributes?.["llm.provider"] || void 0;
|
|
960
|
+
const inputTokens = typeof span.attributes?.["gen_ai.usage.input_tokens"] === "number" ? span.attributes["gen_ai.usage.input_tokens"] : void 0;
|
|
961
|
+
const outputTokens = typeof span.attributes?.["gen_ai.usage.output_tokens"] === "number" ? span.attributes["gen_ai.usage.output_tokens"] : void 0;
|
|
962
|
+
trace.setMetadata("provider", provider || "unknown");
|
|
963
|
+
trace.setMetadata("model", model || "unknown");
|
|
964
|
+
const nsDuration = span.duration;
|
|
965
|
+
const durationMs = nsDuration ? Math.round(nsDuration / 1e6) : 0;
|
|
966
|
+
trace.setMetadata("latency_ms", durationMs);
|
|
967
|
+
trace.setMetadata("otel.duration_ns", nsDuration);
|
|
968
|
+
const oSpan = trace.startSpan(span.name || "otel-operation", this.config.defaultSpanType);
|
|
969
|
+
if (input) oSpan.setInput(input);
|
|
970
|
+
if (output) oSpan.setOutput(output);
|
|
971
|
+
if (inputTokens || outputTokens) {
|
|
972
|
+
oSpan.setTokens({
|
|
973
|
+
input: inputTokens || 0,
|
|
974
|
+
output: outputTokens || 0,
|
|
975
|
+
total: (inputTokens || 0) + (outputTokens || 0)
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
if (model) oSpan.setMetadata("model", model);
|
|
979
|
+
if (provider) oSpan.setMetadata("provider", provider);
|
|
980
|
+
if (span.attributes) oSpan.setMetadataAll(span.attributes);
|
|
981
|
+
const status = span.status;
|
|
982
|
+
const statusCode = status?.code;
|
|
983
|
+
if (statusCode === 2) {
|
|
984
|
+
oSpan.setError(new Error(status?.message || "OTel span error"));
|
|
985
|
+
trace.end("error" /* ERROR */);
|
|
986
|
+
} else {
|
|
987
|
+
trace.end("success" /* SUCCESS */);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
resultCallback({ code: 0 });
|
|
991
|
+
} catch (error) {
|
|
992
|
+
resultCallback({
|
|
993
|
+
code: 1,
|
|
994
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Called when the exporter is shut down.
|
|
1000
|
+
* Flushes any remaining buffered traces via the SDK client.
|
|
1001
|
+
*/
|
|
1002
|
+
async shutdown() {
|
|
1003
|
+
try {
|
|
1004
|
+
await this.client.flush();
|
|
1005
|
+
} catch {
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Called by the OTel SDK to force-export buffered spans.
|
|
1010
|
+
*/
|
|
1011
|
+
async forceFlush() {
|
|
1012
|
+
try {
|
|
1013
|
+
await this.client.flush();
|
|
1014
|
+
} catch {
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1019
|
+
0 && (module.exports = {
|
|
1020
|
+
ObservyzeClient,
|
|
1021
|
+
ObservyzeSpanExporter,
|
|
1022
|
+
Span,
|
|
1023
|
+
SpanType,
|
|
1024
|
+
Trace,
|
|
1025
|
+
TraceStatus,
|
|
1026
|
+
wrap,
|
|
1027
|
+
wrapAnthropic,
|
|
1028
|
+
wrapOpenAI
|
|
1029
|
+
});
|