@tangle-network/traces 0.23.1 → 1.0.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/README.md +61 -1078
- package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
- package/dist/ask-2KF4KSPB.js +39 -0
- package/dist/check-YF6ZHPGG.js +322 -0
- package/dist/chunk-2XXABIRL.js +25 -0
- package/dist/chunk-4VKPN432.js +138 -0
- package/dist/chunk-6GOG5KIK.js +53 -0
- package/dist/chunk-7USMM4MA.js +5695 -0
- package/dist/chunk-CG5QHQO4.js +492 -0
- package/dist/chunk-CQ6UYZG6.js +1217 -0
- package/dist/chunk-DU534HAX.js +263 -0
- package/dist/chunk-FC4V25CG.js +503 -0
- package/dist/chunk-GLUDGUGJ.js +232 -0
- package/dist/chunk-GNRXKQEQ.js +120 -0
- package/dist/chunk-K2KH2HHD.js +228 -0
- package/dist/chunk-M3W6TZHQ.js +381 -0
- package/dist/chunk-O52XK7OB.js +19 -0
- package/dist/chunk-PYH3PL3N.js +62 -0
- package/dist/chunk-QLVP37YB.js +782 -0
- package/dist/chunk-QUHUMZ4W.js +759 -0
- package/dist/chunk-REO2JQJY.js +535 -0
- package/dist/chunk-RZL5GAWH.js +60 -0
- package/dist/chunk-TOCNGML5.js +273 -0
- package/dist/chunk-UQSAGXPH.js +208 -0
- package/dist/chunk-VFOLC4OX.js +407 -0
- package/dist/chunk-WF7PTDCZ.js +3921 -0
- package/dist/chunk-YAX72TST.js +259 -0
- package/dist/cli.js +2086 -2050
- package/dist/diff-J4RAIOXO.js +16 -0
- package/dist/improvement-LCCVGGUS.js +25 -0
- package/dist/index.d.ts +1235 -4357
- package/dist/index.js +53 -1204
- package/dist/live-XXL7VZWB.js +14 -0
- package/dist/mcp-EYIDKBEK.js +85 -0
- package/dist/report-E3G2T5TJ.js +338 -0
- package/dist/run-watch-RAOOAXOF.js +649 -0
- package/dist/supervisor-report-RMPLLBGZ.js +29 -0
- package/dist/supervisor-run-context-WO6VOTGV.js +30 -0
- package/dist/upload-3R7GRSKS.js +20 -0
- package/dist/verify-safe-C5ZXZOZ2.js +8 -0
- package/docs/analyze.md +76 -0
- package/docs/ask.md +88 -0
- package/docs/bundle.md +40 -0
- package/docs/check.md +77 -0
- package/docs/convert.md +44 -0
- package/docs/develop.md +32 -0
- package/docs/diff.md +38 -0
- package/docs/facts.md +114 -0
- package/docs/mcp.md +36 -0
- package/docs/migrate-1.0.md +33 -0
- package/docs/parsers.md +66 -0
- package/docs/release-1.0.0.md +147 -0
- package/docs/release-1.0.1.md +4 -0
- package/docs/sdk.md +77 -0
- package/docs/select.md +70 -0
- package/docs/skills.md +46 -0
- package/docs/upload.md +47 -0
- package/docs/watch-jsonl.v1.example.jsonl +5 -0
- package/docs/watch.md +66 -0
- package/examples/README.md +9 -0
- package/examples/custom-analyst.ts +65 -74
- package/install.sh +22 -2
- package/package.json +10 -9
- package/skills/inspect-agent-traces/SKILL.md +47 -110
- package/dist/chunk-7DVZCCZI.js +0 -20983
- package/docs/replay-verify.md +0 -123
- package/docs/stream-findings.svg +0 -39
- package/docs/trace-analysts.md +0 -643
- package/examples/custom-backend.ts +0 -21
- package/examples/external-engines.ts +0 -62
- package/examples/improvement-config.mjs +0 -63
- package/examples/observe-and-alert.ts +0 -19
- package/examples/register-harness.ts +0 -26
- package/skills/build-trace-analyst/SKILL.md +0 -76
- package/skills/build-trace-analyst/agents/openai.yaml +0 -4
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TOOL_IO_VALUE_MAX_BYTES,
|
|
3
|
+
boundedString,
|
|
4
|
+
locateSourceObjects,
|
|
5
|
+
parseIsoToEpochMs,
|
|
6
|
+
sourceAttributes,
|
|
7
|
+
toolArgumentsFromAttributes,
|
|
8
|
+
toolIoAttributes
|
|
9
|
+
} from "./chunk-DU534HAX.js";
|
|
10
|
+
|
|
11
|
+
// src/arrays.ts
|
|
12
|
+
function appendAll(target, values) {
|
|
13
|
+
for (const value of values) target.push(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/otlp-input.ts
|
|
17
|
+
import { open as open2, readdir, readFile, stat } from "fs/promises";
|
|
18
|
+
import { basename, join } from "path";
|
|
19
|
+
import {
|
|
20
|
+
ATTR,
|
|
21
|
+
declaredSpanKind,
|
|
22
|
+
resolveSpanKind,
|
|
23
|
+
SPAN_KIND_ATTR_KEYS,
|
|
24
|
+
validateTraceSpans
|
|
25
|
+
} from "@tangle-network/agent-trace-contract";
|
|
26
|
+
import { TOOL_NAME } from "@tangle-network/agent-eval/trace-attributes";
|
|
27
|
+
|
|
28
|
+
// src/json-values.ts
|
|
29
|
+
import { open } from "fs/promises";
|
|
30
|
+
var MAX_STRING_BYTES = TOOL_IO_VALUE_MAX_BYTES;
|
|
31
|
+
var READ_BYTES = 1 << 20;
|
|
32
|
+
var QUOTE = 34;
|
|
33
|
+
var BACKSLASH = 92;
|
|
34
|
+
var NEWLINE = 10;
|
|
35
|
+
var NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/u;
|
|
36
|
+
function isWhitespace(byte) {
|
|
37
|
+
return byte === 32 || byte === 9 || byte === 10 || byte === 13;
|
|
38
|
+
}
|
|
39
|
+
function isBareByte(byte) {
|
|
40
|
+
return byte >= 48 && byte <= 57 || byte === 45 || byte === 43 || byte === 46 || byte >= 97 && byte <= 122 || byte >= 65 && byte <= 90;
|
|
41
|
+
}
|
|
42
|
+
function wholePrefix(raw, limit) {
|
|
43
|
+
let end = Math.min(limit, raw.length);
|
|
44
|
+
for (let i = 0; i < end; ) {
|
|
45
|
+
if (raw[i] !== BACKSLASH) {
|
|
46
|
+
i += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const length = raw[i + 1] === 117 ? 6 : 2;
|
|
50
|
+
if (i + length > end) {
|
|
51
|
+
end = i;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
i += length;
|
|
55
|
+
}
|
|
56
|
+
if (end > 0) {
|
|
57
|
+
let start = end - 1;
|
|
58
|
+
while (start > 0 && (raw[start] & 192) === 128) start -= 1;
|
|
59
|
+
const first = raw[start];
|
|
60
|
+
const size = first >= 240 ? 4 : first >= 224 ? 3 : first >= 192 ? 2 : 1;
|
|
61
|
+
if (start + size > end) end = start;
|
|
62
|
+
}
|
|
63
|
+
return end;
|
|
64
|
+
}
|
|
65
|
+
function decode(raw) {
|
|
66
|
+
return raw.includes(BACKSLASH) ? JSON.parse(`"${raw.toString("utf8")}"`) : raw.toString("utf8");
|
|
67
|
+
}
|
|
68
|
+
var ValueParser = class {
|
|
69
|
+
constructor(onError) {
|
|
70
|
+
this.onError = onError;
|
|
71
|
+
}
|
|
72
|
+
onError;
|
|
73
|
+
state = 0 /* Value */;
|
|
74
|
+
stack = [];
|
|
75
|
+
line = 1;
|
|
76
|
+
valueLine = 1;
|
|
77
|
+
values = 0;
|
|
78
|
+
/** Unknown until the first value ends or crosses a line: true for one document, false for JSONL. */
|
|
79
|
+
document;
|
|
80
|
+
/** The string being read: its raw bytes up to the cap, its length in all, whether it is a key. */
|
|
81
|
+
stringParts = [];
|
|
82
|
+
stringKept = 0;
|
|
83
|
+
stringTotal = 0;
|
|
84
|
+
stringIsKey = false;
|
|
85
|
+
/** Backslashes that ended the previous piece of the string being read. */
|
|
86
|
+
carriedBackslashes = 0;
|
|
87
|
+
bare = "";
|
|
88
|
+
done = [];
|
|
89
|
+
/** Parse `bytes`, which the caller may reuse once this returns, and take the values it completed. */
|
|
90
|
+
feed(bytes) {
|
|
91
|
+
let i = 0;
|
|
92
|
+
while (i < bytes.length) {
|
|
93
|
+
if (this.state === 4 /* String */) {
|
|
94
|
+
i = this.readString(bytes, i);
|
|
95
|
+
} else if (this.state === 5 /* Bare */) {
|
|
96
|
+
const start = i;
|
|
97
|
+
while (i < bytes.length && isBareByte(bytes[i])) i += 1;
|
|
98
|
+
this.bare += bytes.toString("latin1", start, i);
|
|
99
|
+
if (i < bytes.length) this.endBare();
|
|
100
|
+
} else if (this.state === 6 /* SkipLine */) {
|
|
101
|
+
const newline = bytes.indexOf(NEWLINE, i);
|
|
102
|
+
if (newline === -1) break;
|
|
103
|
+
this.line += 1;
|
|
104
|
+
this.state = 0 /* Value */;
|
|
105
|
+
i = newline + 1;
|
|
106
|
+
} else {
|
|
107
|
+
i = this.readStructure(bytes, i);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return this.done.splice(0);
|
|
111
|
+
}
|
|
112
|
+
/** The values the input completed; a value it left open is reported. */
|
|
113
|
+
end() {
|
|
114
|
+
if (this.state === 5 /* Bare */) this.endBare();
|
|
115
|
+
if (this.state !== 6 /* SkipLine */ && (this.state !== 0 /* Value */ || this.stack.length > 0)) {
|
|
116
|
+
this.onError(this.valueLine, "the input ended inside a value");
|
|
117
|
+
}
|
|
118
|
+
return this.done.splice(0);
|
|
119
|
+
}
|
|
120
|
+
reset() {
|
|
121
|
+
this.stack.length = 0;
|
|
122
|
+
this.stringParts = [];
|
|
123
|
+
this.bare = "";
|
|
124
|
+
}
|
|
125
|
+
/** A value that cannot be JSON: report it, and resume on the next line. */
|
|
126
|
+
fail(detail) {
|
|
127
|
+
this.onError(this.valueLine, detail);
|
|
128
|
+
this.reset();
|
|
129
|
+
this.state = 6 /* SkipLine */;
|
|
130
|
+
}
|
|
131
|
+
/** A newline: whitespace in a document; in JSONL, the end of a value left open on its line. */
|
|
132
|
+
newline() {
|
|
133
|
+
const open3 = this.state !== 0 /* Value */ || this.stack.length > 0;
|
|
134
|
+
if (open3 && this.document === void 0 && this.values === 0) this.document = true;
|
|
135
|
+
if (open3 && this.document === false) {
|
|
136
|
+
this.onError(this.valueLine, "the line ended inside a value");
|
|
137
|
+
this.reset();
|
|
138
|
+
this.state = 0 /* Value */;
|
|
139
|
+
}
|
|
140
|
+
this.line += 1;
|
|
141
|
+
}
|
|
142
|
+
readStructure(bytes, at) {
|
|
143
|
+
const byte = bytes[at];
|
|
144
|
+
if (isWhitespace(byte)) {
|
|
145
|
+
if (byte === NEWLINE) this.newline();
|
|
146
|
+
return at + 1;
|
|
147
|
+
}
|
|
148
|
+
if (this.state === 0 /* Value */ && this.stack.length === 0) this.valueLine = this.line;
|
|
149
|
+
if (this.state === 0 /* Value */) return this.startValue(byte, at);
|
|
150
|
+
if (this.state === 2 /* Key */) {
|
|
151
|
+
if (byte === QUOTE) return this.startString(true, at);
|
|
152
|
+
if (byte === 125 && this.closeEmptyObject()) return at + 1;
|
|
153
|
+
this.fail("an object key must be a string");
|
|
154
|
+
return at;
|
|
155
|
+
}
|
|
156
|
+
if (this.state === 3 /* Colon */) {
|
|
157
|
+
if (byte === 58) {
|
|
158
|
+
this.state = 0 /* Value */;
|
|
159
|
+
return at + 1;
|
|
160
|
+
}
|
|
161
|
+
this.fail("a key must be followed by :");
|
|
162
|
+
return at;
|
|
163
|
+
}
|
|
164
|
+
const top = this.stack.at(-1);
|
|
165
|
+
if (byte === 44) {
|
|
166
|
+
this.state = Array.isArray(top.value) ? 0 /* Value */ : 2 /* Key */;
|
|
167
|
+
return at + 1;
|
|
168
|
+
}
|
|
169
|
+
if (byte === 93 && Array.isArray(top.value) || byte === 125 && !Array.isArray(top.value)) {
|
|
170
|
+
this.stack.pop();
|
|
171
|
+
this.complete(top.value);
|
|
172
|
+
return at + 1;
|
|
173
|
+
}
|
|
174
|
+
this.fail("a value must be followed by , or the end of its container");
|
|
175
|
+
return at;
|
|
176
|
+
}
|
|
177
|
+
startValue(byte, at) {
|
|
178
|
+
if (byte === QUOTE) return this.startString(false, at);
|
|
179
|
+
if (byte === 123 || byte === 91) {
|
|
180
|
+
this.stack.push({ value: byte === 123 ? {} : [] });
|
|
181
|
+
this.state = byte === 123 ? 2 /* Key */ : 0 /* Value */;
|
|
182
|
+
return at + 1;
|
|
183
|
+
}
|
|
184
|
+
const top = this.stack.at(-1);
|
|
185
|
+
if (byte === 93 && top && Array.isArray(top.value) && top.value.length === 0) {
|
|
186
|
+
this.stack.pop();
|
|
187
|
+
this.complete(top.value);
|
|
188
|
+
return at + 1;
|
|
189
|
+
}
|
|
190
|
+
if (isBareByte(byte)) {
|
|
191
|
+
this.bare = "";
|
|
192
|
+
this.state = 5 /* Bare */;
|
|
193
|
+
return at;
|
|
194
|
+
}
|
|
195
|
+
this.fail(`unexpected ${JSON.stringify(String.fromCharCode(byte))}`);
|
|
196
|
+
return at;
|
|
197
|
+
}
|
|
198
|
+
/** `}` in the key state closes the object only when it has no key yet: `{}`, not `{"a":1,}`. */
|
|
199
|
+
closeEmptyObject() {
|
|
200
|
+
const top = this.stack.at(-1);
|
|
201
|
+
if (Array.isArray(top.value) || Object.keys(top.value).length > 0) return false;
|
|
202
|
+
this.stack.pop();
|
|
203
|
+
this.complete(top.value);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
startString(isKey, at) {
|
|
207
|
+
this.stringParts = [];
|
|
208
|
+
this.stringKept = 0;
|
|
209
|
+
this.stringTotal = 0;
|
|
210
|
+
this.stringIsKey = isKey;
|
|
211
|
+
this.carriedBackslashes = 0;
|
|
212
|
+
this.state = 4 /* String */;
|
|
213
|
+
return at + 1;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Read string bytes from `at` to the closing quote, or to the end of
|
|
217
|
+
* `bytes`. A quote closes the string when an even number of backslashes
|
|
218
|
+
* precede it, so each quote is looked at once, however long the string.
|
|
219
|
+
*/
|
|
220
|
+
readString(bytes, at) {
|
|
221
|
+
const lineEnd = this.document === false ? bytes.indexOf(NEWLINE, at) : -1;
|
|
222
|
+
const limit = lineEnd === -1 ? bytes.length : lineEnd;
|
|
223
|
+
let from = at;
|
|
224
|
+
for (; ; ) {
|
|
225
|
+
const quote = bytes.indexOf(QUOTE, from);
|
|
226
|
+
if (quote === -1 || quote >= limit) break;
|
|
227
|
+
let backslashes = 0;
|
|
228
|
+
let j = quote - 1;
|
|
229
|
+
while (j >= at && bytes[j] === BACKSLASH) {
|
|
230
|
+
backslashes += 1;
|
|
231
|
+
j -= 1;
|
|
232
|
+
}
|
|
233
|
+
if (j < at) backslashes += this.carriedBackslashes;
|
|
234
|
+
if (backslashes % 2 === 0) {
|
|
235
|
+
this.keep(bytes, at, quote);
|
|
236
|
+
this.endString();
|
|
237
|
+
return quote + 1;
|
|
238
|
+
}
|
|
239
|
+
from = quote + 1;
|
|
240
|
+
}
|
|
241
|
+
this.keep(bytes, at, limit);
|
|
242
|
+
if (lineEnd !== -1) {
|
|
243
|
+
this.newline();
|
|
244
|
+
return lineEnd + 1;
|
|
245
|
+
}
|
|
246
|
+
let run = 0;
|
|
247
|
+
for (let j = limit - 1; j >= at && bytes[j] === BACKSLASH; j -= 1) run += 1;
|
|
248
|
+
this.carriedBackslashes = run === limit - at ? this.carriedBackslashes + run : run;
|
|
249
|
+
return limit;
|
|
250
|
+
}
|
|
251
|
+
/** Keep the string's raw bytes in `[from, to)` up to the cap, and count them all. */
|
|
252
|
+
keep(bytes, from, to) {
|
|
253
|
+
const length = to - from;
|
|
254
|
+
this.stringTotal += length;
|
|
255
|
+
const take = Math.min(MAX_STRING_BYTES - this.stringKept, length);
|
|
256
|
+
if (take <= 0) return;
|
|
257
|
+
this.stringParts.push(Buffer.from(bytes.subarray(from, from + take)));
|
|
258
|
+
this.stringKept += take;
|
|
259
|
+
}
|
|
260
|
+
endString() {
|
|
261
|
+
const raw = this.stringParts.length === 1 ? this.stringParts[0] : Buffer.concat(this.stringParts);
|
|
262
|
+
this.stringParts = [];
|
|
263
|
+
let text;
|
|
264
|
+
try {
|
|
265
|
+
if (this.stringTotal <= MAX_STRING_BYTES) {
|
|
266
|
+
text = decode(raw);
|
|
267
|
+
} else {
|
|
268
|
+
const longest = `
|
|
269
|
+
[truncated ${this.stringTotal} bytes]`;
|
|
270
|
+
const kept = wholePrefix(raw, MAX_STRING_BYTES - Buffer.byteLength(longest));
|
|
271
|
+
text = `${decode(raw.subarray(0, kept))}
|
|
272
|
+
[truncated ${this.stringTotal - kept} bytes]`;
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
this.fail("a string holds an invalid escape");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (this.stringIsKey) {
|
|
279
|
+
this.stack.at(-1).key = text;
|
|
280
|
+
this.state = 3 /* Colon */;
|
|
281
|
+
} else {
|
|
282
|
+
this.complete(text);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
endBare() {
|
|
286
|
+
const text = this.bare;
|
|
287
|
+
this.bare = "";
|
|
288
|
+
if (text === "true" || text === "false" || text === "null") {
|
|
289
|
+
this.complete(text === "null" ? null : text === "true");
|
|
290
|
+
} else if (NUMBER.test(text)) {
|
|
291
|
+
this.complete(Number(text));
|
|
292
|
+
} else {
|
|
293
|
+
this.fail(`${JSON.stringify(text.slice(0, 20))} is not a JSON value`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
complete(value) {
|
|
297
|
+
const top = this.stack.at(-1);
|
|
298
|
+
if (top) {
|
|
299
|
+
if (Array.isArray(top.value)) top.value.push(value);
|
|
300
|
+
else if (top.key === "__proto__") Object.defineProperty(top.value, top.key, { value, enumerable: true, writable: true, configurable: true });
|
|
301
|
+
else top.value[top.key] = value;
|
|
302
|
+
this.state = 1 /* AfterValue */;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
this.done.push({ value, line: this.valueLine });
|
|
306
|
+
this.values += 1;
|
|
307
|
+
this.document ??= false;
|
|
308
|
+
this.state = 0 /* Value */;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
async function* readJsonValues(path, onError) {
|
|
312
|
+
const parser = new ValueParser(onError);
|
|
313
|
+
const handle = await open(path, "r");
|
|
314
|
+
try {
|
|
315
|
+
const buffer = Buffer.allocUnsafe(READ_BYTES);
|
|
316
|
+
for (; ; ) {
|
|
317
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
|
|
318
|
+
if (bytesRead === 0) break;
|
|
319
|
+
yield* parser.feed(bytesRead === buffer.length ? buffer : buffer.subarray(0, bytesRead));
|
|
320
|
+
}
|
|
321
|
+
yield* parser.end();
|
|
322
|
+
} finally {
|
|
323
|
+
await handle.close();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/jsonl.ts
|
|
328
|
+
import { isUtf8 } from "buffer";
|
|
329
|
+
import { createHash } from "crypto";
|
|
330
|
+
import { createReadStream } from "fs";
|
|
331
|
+
var JsonlParseError = class extends SyntaxError {
|
|
332
|
+
sourcePath;
|
|
333
|
+
lineNumber;
|
|
334
|
+
byteOffset;
|
|
335
|
+
byteLength;
|
|
336
|
+
sha256;
|
|
337
|
+
receipt;
|
|
338
|
+
constructor(receipt) {
|
|
339
|
+
super(`Invalid JSONL at ${receipt.sourcePath}:${receipt.lineNumber}`);
|
|
340
|
+
this.name = "JsonlParseError";
|
|
341
|
+
this.sourcePath = receipt.sourcePath;
|
|
342
|
+
this.lineNumber = receipt.lineNumber;
|
|
343
|
+
this.byteOffset = receipt.byteOffset;
|
|
344
|
+
this.byteLength = receipt.byteLength;
|
|
345
|
+
this.sha256 = receipt.sha256;
|
|
346
|
+
this.receipt = receipt;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
function corruptionReceipt(rawLine, path, lineNumber, byteOffset) {
|
|
350
|
+
return {
|
|
351
|
+
receiptVersion: 1,
|
|
352
|
+
kind: "jsonl_corruption",
|
|
353
|
+
status: "degraded_not_lossless",
|
|
354
|
+
sourcePath: path,
|
|
355
|
+
lineNumber,
|
|
356
|
+
byteOffset,
|
|
357
|
+
byteLength: rawLine.length,
|
|
358
|
+
sha256: createHash("sha256").update(rawLine).digest("hex"),
|
|
359
|
+
rawBytes: "local_source_only"
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function parseLine(rawLine, path, lineNumber, byteOffset, options) {
|
|
363
|
+
const jsonBytes = rawLine.at(-1) === 13 ? rawLine.subarray(0, -1) : rawLine;
|
|
364
|
+
if (jsonBytes.length === 0) return void 0;
|
|
365
|
+
if (!isUtf8(jsonBytes)) return handleCorruption(rawLine, path, lineNumber, byteOffset, options);
|
|
366
|
+
try {
|
|
367
|
+
const json = jsonBytes.toString("utf8");
|
|
368
|
+
if (json.trim().length === 0) return void 0;
|
|
369
|
+
const value = JSON.parse(json);
|
|
370
|
+
if (options.captureSources) locateSourceObjects(value, path, rawLine, byteOffset);
|
|
371
|
+
return value;
|
|
372
|
+
} catch {
|
|
373
|
+
return handleCorruption(rawLine, path, lineNumber, byteOffset, options);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function handleCorruption(rawLine, path, lineNumber, byteOffset, options) {
|
|
377
|
+
const receipt = corruptionReceipt(rawLine, path, lineNumber, byteOffset);
|
|
378
|
+
if (options.mode === "recover") {
|
|
379
|
+
options.onCorruption(receipt);
|
|
380
|
+
return void 0;
|
|
381
|
+
}
|
|
382
|
+
throw new JsonlParseError(receipt);
|
|
383
|
+
}
|
|
384
|
+
async function* readJsonl(path, options = { mode: "strict" }) {
|
|
385
|
+
for await (const row of readJsonlLines(path, options)) yield row.value;
|
|
386
|
+
}
|
|
387
|
+
async function* readJsonlLines(path, options = { mode: "strict" }) {
|
|
388
|
+
if (options.mode === "recover" && typeof options.onCorruption !== "function") {
|
|
389
|
+
throw new TypeError("JSONL recover mode requires an onCorruption callback");
|
|
390
|
+
}
|
|
391
|
+
options.signal?.throwIfAborted();
|
|
392
|
+
const input = createReadStream(path, { signal: options.signal });
|
|
393
|
+
let lineNumber = 0;
|
|
394
|
+
let byteOffset = 0;
|
|
395
|
+
let fragments = [];
|
|
396
|
+
try {
|
|
397
|
+
for await (const chunk of input) {
|
|
398
|
+
options.signal?.throwIfAborted();
|
|
399
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
400
|
+
let start = 0;
|
|
401
|
+
let newline = bytes.indexOf(10);
|
|
402
|
+
while (newline !== -1) {
|
|
403
|
+
options.signal?.throwIfAborted();
|
|
404
|
+
fragments.push(bytes.subarray(start, newline));
|
|
405
|
+
const rawLine = fragments.length === 1 ? fragments[0] : Buffer.concat(fragments);
|
|
406
|
+
fragments = [];
|
|
407
|
+
lineNumber += 1;
|
|
408
|
+
const parsed = parseLine(rawLine, path, lineNumber, byteOffset, options);
|
|
409
|
+
if (parsed !== void 0) yield { value: parsed, lineNumber };
|
|
410
|
+
byteOffset += rawLine.length + 1;
|
|
411
|
+
start = newline + 1;
|
|
412
|
+
newline = bytes.indexOf(10, start);
|
|
413
|
+
}
|
|
414
|
+
if (start < bytes.length) fragments.push(bytes.subarray(start));
|
|
415
|
+
}
|
|
416
|
+
if (fragments.length > 0) {
|
|
417
|
+
options.signal?.throwIfAborted();
|
|
418
|
+
const rawLine = fragments.length === 1 ? fragments[0] : Buffer.concat(fragments);
|
|
419
|
+
lineNumber += 1;
|
|
420
|
+
const parsed = parseLine(rawLine, path, lineNumber, byteOffset, options);
|
|
421
|
+
if (parsed !== void 0) yield { value: parsed, lineNumber };
|
|
422
|
+
}
|
|
423
|
+
} finally {
|
|
424
|
+
input.destroy();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function takeJsonl(path, limit, options = { mode: "strict" }) {
|
|
428
|
+
if (!Number.isInteger(limit) || limit < 0) throw new RangeError("limit must be a non-negative integer");
|
|
429
|
+
const rows = [];
|
|
430
|
+
if (limit === 0) return rows;
|
|
431
|
+
for await (const row of readJsonl(path, options)) {
|
|
432
|
+
rows.push(row);
|
|
433
|
+
if (rows.length === limit) break;
|
|
434
|
+
}
|
|
435
|
+
return rows;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/otlp-input.ts
|
|
439
|
+
var OTLP_FILE_EXTENSIONS = [".jsonl", ".ndjson"];
|
|
440
|
+
var OTLP_DIRECTORY_NAME = "otlp";
|
|
441
|
+
var SNIFF_LINES = 5;
|
|
442
|
+
var DURATION_UNMEASURABLE_ATTR = "traces.duration_unmeasurable";
|
|
443
|
+
var RAW_FIELD_ATTRIBUTES = Object.freeze({
|
|
444
|
+
trace_id: "traces.raw_attribute.trace_id",
|
|
445
|
+
end_time: "traces.raw_attribute.end_time",
|
|
446
|
+
"status.code": "traces.raw_attribute.status.code",
|
|
447
|
+
kind: "traces.raw_attribute.openinference.span.kind",
|
|
448
|
+
links: "traces.raw_attribute.links"
|
|
449
|
+
});
|
|
450
|
+
var SUBSTITUTED_FIELDS_ATTR = "traces.substituted_fields";
|
|
451
|
+
var TRACE_ID_MINTED_ATTR = "traces.trace_id_minted";
|
|
452
|
+
var SOURCE_UNREADABLE_ROWS_ATTR = "traces.source.unreadable_rows";
|
|
453
|
+
var SOURCE_UNREADABLE_KINDS_ATTR = "traces.source.unreadable_row_kinds";
|
|
454
|
+
async function jsonlFilesUnder(dir) {
|
|
455
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
456
|
+
const files = [];
|
|
457
|
+
for (const entry of [...entries].sort((left, right) => left.name.localeCompare(right.name))) {
|
|
458
|
+
const path = join(dir, entry.name);
|
|
459
|
+
if (entry.isDirectory()) appendAll(files, await jsonlFilesUnder(path));
|
|
460
|
+
else if (OTLP_FILE_EXTENSIONS.some((ext) => entry.name.toLowerCase().endsWith(ext))) files.push(path);
|
|
461
|
+
}
|
|
462
|
+
return files;
|
|
463
|
+
}
|
|
464
|
+
async function otlpDirectoriesUnder(dir) {
|
|
465
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
466
|
+
const dirs = [];
|
|
467
|
+
for (const entry of [...entries].sort((left, right) => left.name.localeCompare(right.name))) {
|
|
468
|
+
if (!entry.isDirectory()) continue;
|
|
469
|
+
const path = join(dir, entry.name);
|
|
470
|
+
if (entry.name === OTLP_DIRECTORY_NAME) dirs.push(path);
|
|
471
|
+
else appendAll(dirs, await otlpDirectoriesUnder(path));
|
|
472
|
+
}
|
|
473
|
+
return dirs;
|
|
474
|
+
}
|
|
475
|
+
async function sniffOtlpFile(path) {
|
|
476
|
+
if (await isOtlpJsonFile(path)) return { otlp: true };
|
|
477
|
+
let seen = 0;
|
|
478
|
+
let first = null;
|
|
479
|
+
const head = new AbortController();
|
|
480
|
+
try {
|
|
481
|
+
for await (const { value: row } of readJsonlLines(path, {
|
|
482
|
+
mode: "recover",
|
|
483
|
+
onCorruption: () => {
|
|
484
|
+
if (++seen >= SNIFF_LINES) head.abort();
|
|
485
|
+
},
|
|
486
|
+
signal: head.signal
|
|
487
|
+
})) {
|
|
488
|
+
if (isObject(row) && nonEmptyString(row.span_id) !== null && nonEmptyString(row.name) !== null) return { otlp: true };
|
|
489
|
+
if (isStandardOtlpRow(row)) return { otlp: true };
|
|
490
|
+
first ??= { row };
|
|
491
|
+
if (++seen >= SNIFF_LINES) break;
|
|
492
|
+
}
|
|
493
|
+
} catch (error) {
|
|
494
|
+
if (!head.signal.aborted) throw error;
|
|
495
|
+
}
|
|
496
|
+
if (seen === 0) return { otlp: false, reason: "file is empty" };
|
|
497
|
+
return { otlp: false, reason: first === null ? "no line parses as JSON" : describeNonSpanRow(first.row) };
|
|
498
|
+
}
|
|
499
|
+
function describeNonSpanRow(row) {
|
|
500
|
+
if (!isObject(row)) return `rows are ${row === null ? "null" : Array.isArray(row) ? "arrays" : typeof row}, not spans`;
|
|
501
|
+
const marker = nonEmptyString(row.type) ?? nonEmptyString(row.event) ?? nonEmptyString(row.kind);
|
|
502
|
+
const keys = Object.keys(row).slice(0, 4).join(", ");
|
|
503
|
+
return marker !== null ? `rows carry no span_id; they look like \`${marker}\` events (keys: ${keys})` : `rows carry no span_id (keys: ${keys})`;
|
|
504
|
+
}
|
|
505
|
+
async function resolveOtlpInputFiles(path) {
|
|
506
|
+
let info;
|
|
507
|
+
try {
|
|
508
|
+
info = await stat(path);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
if (error.code === "ENOENT") {
|
|
511
|
+
throw new Error(`no OTLP file or directory at ${path}`);
|
|
512
|
+
}
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
if (!info.isDirectory()) return { files: [path], skipped: [] };
|
|
516
|
+
const otlpDirs = basename(path) === OTLP_DIRECTORY_NAME ? [path] : await otlpDirectoriesUnder(path);
|
|
517
|
+
const candidates = otlpDirs.length > 0 ? (await Promise.all(otlpDirs.map(jsonlFilesUnder))).flat() : await jsonlFilesUnder(path);
|
|
518
|
+
if (candidates.length === 0) {
|
|
519
|
+
throw new Error(`no ${OTLP_FILE_EXTENSIONS.join(" / ")} files under ${path}`);
|
|
520
|
+
}
|
|
521
|
+
const files = [];
|
|
522
|
+
const skipped = [];
|
|
523
|
+
for (const candidate of candidates) {
|
|
524
|
+
const sniff = await sniffOtlpFile(candidate);
|
|
525
|
+
if (sniff.otlp) files.push(candidate);
|
|
526
|
+
else skipped.push({ path: candidate, reason: sniff.reason });
|
|
527
|
+
}
|
|
528
|
+
if (files.length === 0) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
`no OTLP span files under ${path}: ${skipped.length} JSONL file(s) hold something else (${skipped.slice(0, 3).map((entry) => `${basename(entry.path)} \u2014 ${entry.reason}`).join("; ")})`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
return { files, skipped };
|
|
534
|
+
}
|
|
535
|
+
function isObject(value) {
|
|
536
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
537
|
+
}
|
|
538
|
+
function canonicalJson(value) {
|
|
539
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
540
|
+
if (isObject(value)) {
|
|
541
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
542
|
+
}
|
|
543
|
+
return JSON.stringify(value) ?? "null";
|
|
544
|
+
}
|
|
545
|
+
function nonEmptyString(value) {
|
|
546
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
547
|
+
}
|
|
548
|
+
function statusCode(value) {
|
|
549
|
+
if (value === "OK" || value === "STATUS_CODE_OK") return "OK";
|
|
550
|
+
if (value === "ERROR" || value === "STATUS_CODE_ERROR") return "ERROR";
|
|
551
|
+
return "UNSET";
|
|
552
|
+
}
|
|
553
|
+
function readStatusCode(declared) {
|
|
554
|
+
const code = statusCode(declared);
|
|
555
|
+
if (declared === `STATUS_CODE_${code}`) return { code, substituted: false };
|
|
556
|
+
return declared === void 0 ? { code, substituted: true } : { code, substituted: true, raw: declared };
|
|
557
|
+
}
|
|
558
|
+
function isToolTelemetrySpan(attributes) {
|
|
559
|
+
const spanType = nonEmptyString(attributes["span.type"])?.toLowerCase();
|
|
560
|
+
return spanType === "tool.execution" || spanType === "tool.blocked_on_user";
|
|
561
|
+
}
|
|
562
|
+
var LOCAL_TOOL_ATTRIBUTES = ["tool_name", "mcp.tool.name", "tool.input", "tool.output"];
|
|
563
|
+
function inferSpanKind(name, attributes) {
|
|
564
|
+
if (isToolTelemetrySpan(attributes)) return "CHAIN";
|
|
565
|
+
const evidence = { ...attributes };
|
|
566
|
+
for (const key of SPAN_KIND_ATTR_KEYS) delete evidence[key];
|
|
567
|
+
const resolved = resolveSpanKind({ name, attributes: evidence });
|
|
568
|
+
if (resolved !== "UNKNOWN") return resolved;
|
|
569
|
+
const spanType = nonEmptyString(attributes["span.type"])?.toLowerCase();
|
|
570
|
+
const lowered = name.toLowerCase();
|
|
571
|
+
if (spanType === "llm_request" || lowered.includes("llm")) return "LLM";
|
|
572
|
+
if (spanType === "tool" || lowered.includes("tool") || LOCAL_TOOL_ATTRIBUTES.some((key) => attributes[key] != null)) {
|
|
573
|
+
return "TOOL";
|
|
574
|
+
}
|
|
575
|
+
if (spanType === "interaction" || lowered.includes("interaction") || lowered.includes("agent")) return "AGENT";
|
|
576
|
+
return "UNKNOWN";
|
|
577
|
+
}
|
|
578
|
+
function capturedToolIo(attributes) {
|
|
579
|
+
const input = attributes["input.value"] ?? attributes["tool.input"] ?? attributes.tool_input ?? attributes["gen_ai.tool.call.arguments"] ?? attributes["tool.arguments"] ?? attributes.tool_arguments ?? attributes.arguments ?? attributes.args ?? attributes.full_command;
|
|
580
|
+
const capture = toolArgumentsFromAttributes({ ...attributes, "input.value": input });
|
|
581
|
+
return toolIoAttributes({
|
|
582
|
+
input: capture.args,
|
|
583
|
+
output: attributes["output.value"] ?? attributes["tool.output"] ?? attributes.tool_output ?? attributes["gen_ai.tool.call.result"],
|
|
584
|
+
argsCaptured: capture.argsCaptured
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
function resolveRowKind(row, attributes, name) {
|
|
588
|
+
const declared = declaredSpanKind({ kind: row.kind, attributes });
|
|
589
|
+
if (declared.kind !== null) return { kind: declared.kind, declaredRaw: declared.raw, substituted: false };
|
|
590
|
+
if (isToolTelemetrySpan(attributes)) return { kind: "CHAIN", declaredRaw: declared.raw, substituted: true };
|
|
591
|
+
return { kind: inferSpanKind(name, attributes), declaredRaw: declared.raw, substituted: true };
|
|
592
|
+
}
|
|
593
|
+
function readLinks(value, traceId) {
|
|
594
|
+
if (!Array.isArray(value)) return [];
|
|
595
|
+
const links = [];
|
|
596
|
+
for (const entry of value) {
|
|
597
|
+
if (!isObject(entry)) continue;
|
|
598
|
+
const spanId = nonEmptyString(entry.span_id);
|
|
599
|
+
if (spanId === null) continue;
|
|
600
|
+
const attributes = isObject(entry.attributes) ? { ...entry.attributes } : void 0;
|
|
601
|
+
links.push({
|
|
602
|
+
trace_id: nonEmptyString(entry.trace_id) ?? traceId,
|
|
603
|
+
span_id: spanId,
|
|
604
|
+
...attributes ? { attributes } : {}
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return links;
|
|
608
|
+
}
|
|
609
|
+
function bridgeContractAttributes(attributes) {
|
|
610
|
+
const toolName = nonEmptyString(attributes[ATTR.toolName]);
|
|
611
|
+
if (toolName !== null && nonEmptyString(attributes[TOOL_NAME]) === null) {
|
|
612
|
+
attributes[TOOL_NAME] = toolName;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function parseTime(value) {
|
|
616
|
+
if (typeof value !== "string" || value.trim().length === 0) return null;
|
|
617
|
+
try {
|
|
618
|
+
return parseIsoToEpochMs(value);
|
|
619
|
+
} catch {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function otlpRowToSpan(row, fallbackTraceId) {
|
|
624
|
+
if (!isObject(row)) {
|
|
625
|
+
const shape = row === null ? "null" : Array.isArray(row) ? "an array" : typeof row;
|
|
626
|
+
return { issue: { kind: "not-an-object", detail: `row is ${shape}` } };
|
|
627
|
+
}
|
|
628
|
+
const spanId = nonEmptyString(row.span_id);
|
|
629
|
+
if (spanId === null) return { issue: { kind: "missing-span-id", detail: "row carries no span_id" } };
|
|
630
|
+
const startMs = parseTime(row.start_time);
|
|
631
|
+
if (startMs === null) {
|
|
632
|
+
return { issue: { kind: "unreadable-timestamp", detail: `span ${spanId} has no readable start_time` } };
|
|
633
|
+
}
|
|
634
|
+
const attributes = isObject(row.attributes) ? { ...row.attributes } : {};
|
|
635
|
+
const resource = isObject(row.resource) ? row.resource : void 0;
|
|
636
|
+
const resourceAttributes = isObject(resource?.attributes) ? resource.attributes : void 0;
|
|
637
|
+
if (resourceAttributes) {
|
|
638
|
+
for (const [key, value] of Object.entries(resourceAttributes)) {
|
|
639
|
+
if (value != null && attributes[key] === void 0) attributes[key] = value;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
const substituted = [];
|
|
643
|
+
const substitute = (field, declared) => {
|
|
644
|
+
substituted.push(field);
|
|
645
|
+
const key = RAW_FIELD_ATTRIBUTES[field];
|
|
646
|
+
if (declared === void 0) delete attributes[key];
|
|
647
|
+
else attributes[key] = declared;
|
|
648
|
+
};
|
|
649
|
+
const declaredTraceId = nonEmptyString(row.trace_id);
|
|
650
|
+
const traceId = declaredTraceId ?? fallbackTraceId;
|
|
651
|
+
if (declaredTraceId === null) {
|
|
652
|
+
substitute("trace_id", row.trace_id);
|
|
653
|
+
attributes[TRACE_ID_MINTED_ATTR] = true;
|
|
654
|
+
}
|
|
655
|
+
const parsedEndMs = parseTime(row.end_time);
|
|
656
|
+
const rawEndMs = parsedEndMs ?? startMs;
|
|
657
|
+
const durationUnmeasurable = rawEndMs < startMs;
|
|
658
|
+
const endMs = durationUnmeasurable ? startMs : rawEndMs;
|
|
659
|
+
if (durationUnmeasurable || parsedEndMs === null) substitute("end_time", row.end_time);
|
|
660
|
+
if (durationUnmeasurable) attributes[DURATION_UNMEASURABLE_ATTR] = true;
|
|
661
|
+
const withheld = durationUnmeasurable ? {
|
|
662
|
+
kind: "negative-duration",
|
|
663
|
+
field: "end_time",
|
|
664
|
+
detail: `span ${spanId} ends ${startMs - rawEndMs}ms before it starts; the span is analysed and its duration is withheld (not repaired), so it appears in every count except the latency ones`
|
|
665
|
+
} : void 0;
|
|
666
|
+
const name = nonEmptyString(row.name) ?? "span";
|
|
667
|
+
bridgeContractAttributes(attributes);
|
|
668
|
+
const { kind, declaredRaw, substituted: kindSubstituted } = resolveRowKind(row, attributes, name);
|
|
669
|
+
if (kindSubstituted) substitute("kind", declaredRaw ?? void 0);
|
|
670
|
+
attributes[ATTR.spanKind] = kind;
|
|
671
|
+
if (kind === "TOOL") Object.assign(attributes, capturedToolIo(attributes));
|
|
672
|
+
const status = isObject(row.status) ? row.status : void 0;
|
|
673
|
+
const statusMessage = nonEmptyString(status?.message);
|
|
674
|
+
const { code: statusCodeValue, substituted: statusSubstituted, raw: rawStatusCode } = readStatusCode(status?.code);
|
|
675
|
+
if (statusSubstituted) substitute("status.code", rawStatusCode);
|
|
676
|
+
const links = readLinks(row.links, traceId);
|
|
677
|
+
if (row.links !== void 0 && canonicalJson(links) !== canonicalJson(row.links)) {
|
|
678
|
+
substitute("links", row.links);
|
|
679
|
+
}
|
|
680
|
+
if (substituted.length > 0) attributes[SUBSTITUTED_FIELDS_ATTR] = substituted.join(",");
|
|
681
|
+
else delete attributes[SUBSTITUTED_FIELDS_ATTR];
|
|
682
|
+
return {
|
|
683
|
+
span: {
|
|
684
|
+
trace_id: traceId,
|
|
685
|
+
span_id: spanId,
|
|
686
|
+
parent_span_id: nonEmptyString(row.parent_span_id),
|
|
687
|
+
name,
|
|
688
|
+
start_time: new Date(startMs).toISOString(),
|
|
689
|
+
end_time: new Date(endMs).toISOString(),
|
|
690
|
+
status: { code: statusCodeValue, ...statusMessage ? { message: statusMessage } : {} },
|
|
691
|
+
attributes,
|
|
692
|
+
...links.length > 0 ? { links } : {}
|
|
693
|
+
},
|
|
694
|
+
...withheld ? { withheld } : {}
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function isOtlpJsonEnvelope(row) {
|
|
698
|
+
return isObject(row) && Array.isArray(row.resourceSpans);
|
|
699
|
+
}
|
|
700
|
+
function isCamelCaseSpanRow(row) {
|
|
701
|
+
return isObject(row) && row.trace_id === void 0 && typeof row.traceId === "string" && (typeof row.spanId === "string" || typeof row.id === "string");
|
|
702
|
+
}
|
|
703
|
+
function isStandardOtlpRow(row) {
|
|
704
|
+
return isOtlpJsonEnvelope(row) || isCamelCaseSpanRow(row);
|
|
705
|
+
}
|
|
706
|
+
var ENVELOPE_START = /^\s*\{\s*"resourceSpans"\s*:/u;
|
|
707
|
+
var ENVELOPE_HEAD_BYTES = 4096;
|
|
708
|
+
async function isOtlpJsonFile(path) {
|
|
709
|
+
const handle = await open2(path, "r");
|
|
710
|
+
try {
|
|
711
|
+
const head = Buffer.alloc(ENVELOPE_HEAD_BYTES);
|
|
712
|
+
const { bytesRead } = await handle.read(head, 0, head.length, 0);
|
|
713
|
+
return ENVELOPE_START.test(head.toString("utf8", 0, bytesRead));
|
|
714
|
+
} finally {
|
|
715
|
+
await handle.close();
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
var OTEL_SPAN_KINDS = ["SPAN_KIND_UNSPECIFIED", "SPAN_KIND_INTERNAL", "SPAN_KIND_SERVER", "SPAN_KIND_CLIENT", "SPAN_KIND_PRODUCER", "SPAN_KIND_CONSUMER"];
|
|
719
|
+
var OTEL_STATUS_CODES = ["STATUS_CODE_UNSET", "STATUS_CODE_OK", "STATUS_CODE_ERROR"];
|
|
720
|
+
var OTEL_SPAN_KIND_ATTR = "otel.span.kind";
|
|
721
|
+
function isPrimitive(value) {
|
|
722
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
723
|
+
}
|
|
724
|
+
function plainAttributeValue(value) {
|
|
725
|
+
if (value === null || value === void 0) return void 0;
|
|
726
|
+
if (typeof value === "string") return boundedString(value);
|
|
727
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
728
|
+
if (Array.isArray(value) && value.every(isPrimitive)) return value.map((item) => typeof item === "string" ? boundedString(item) : item);
|
|
729
|
+
return boundedString(JSON.stringify(value));
|
|
730
|
+
}
|
|
731
|
+
function anyValue(value) {
|
|
732
|
+
if (!isObject(value)) return void 0;
|
|
733
|
+
if (typeof value.stringValue === "string") return boundedString(value.stringValue);
|
|
734
|
+
if (typeof value.boolValue === "boolean") return value.boolValue;
|
|
735
|
+
if (value.intValue !== void 0) {
|
|
736
|
+
const number = Number(value.intValue);
|
|
737
|
+
return Number.isSafeInteger(number) ? number : String(value.intValue);
|
|
738
|
+
}
|
|
739
|
+
if (value.doubleValue !== void 0) {
|
|
740
|
+
const number = Number(value.doubleValue);
|
|
741
|
+
return Number.isFinite(number) ? number : String(value.doubleValue);
|
|
742
|
+
}
|
|
743
|
+
if (isObject(value.arrayValue)) {
|
|
744
|
+
return plainAttributeValue(Array.isArray(value.arrayValue.values) ? value.arrayValue.values.map(anyValue) : []);
|
|
745
|
+
}
|
|
746
|
+
if (isObject(value.kvlistValue)) return plainAttributeValue(attributeBag(value.kvlistValue.values));
|
|
747
|
+
if (typeof value.bytesValue === "string") return value.bytesValue;
|
|
748
|
+
return void 0;
|
|
749
|
+
}
|
|
750
|
+
function attributeBag(value) {
|
|
751
|
+
const bag = {};
|
|
752
|
+
if (Array.isArray(value)) {
|
|
753
|
+
for (const entry of value) {
|
|
754
|
+
if (!isObject(entry) || typeof entry.key !== "string") continue;
|
|
755
|
+
const plain = anyValue(entry.value);
|
|
756
|
+
if (plain !== void 0) bag[entry.key] = plain;
|
|
757
|
+
}
|
|
758
|
+
} else if (isObject(value)) {
|
|
759
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
760
|
+
const plain = plainAttributeValue(entry);
|
|
761
|
+
if (plain !== void 0) bag[key] = plain;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return bag;
|
|
765
|
+
}
|
|
766
|
+
function otlpId(value, bytes) {
|
|
767
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
768
|
+
if (value.length === bytes * 2 && /^[0-9a-f]+$/iu.test(value)) return value.toLowerCase();
|
|
769
|
+
if (/^[A-Za-z0-9+/]+={0,2}$/u.test(value)) {
|
|
770
|
+
const decoded = Buffer.from(value, "base64");
|
|
771
|
+
if (decoded.length === bytes && decoded.toString("base64") === value) return decoded.toString("hex");
|
|
772
|
+
}
|
|
773
|
+
return value;
|
|
774
|
+
}
|
|
775
|
+
function nanosToIso(value) {
|
|
776
|
+
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
777
|
+
try {
|
|
778
|
+
const nanos = BigInt(typeof value === "number" ? Math.trunc(value) : value.trim());
|
|
779
|
+
return nanos > 0n ? new Date(Number(nanos / 1000000n)).toISOString() : void 0;
|
|
780
|
+
} catch {
|
|
781
|
+
return void 0;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function statusRow(code, message) {
|
|
785
|
+
if (code === void 0 || code === null) return void 0;
|
|
786
|
+
const text = nonEmptyString(message);
|
|
787
|
+
return { code: typeof code === "number" ? OTEL_STATUS_CODES[code] ?? code : code, ...text ? { message: boundedString(text) } : {} };
|
|
788
|
+
}
|
|
789
|
+
function contractRowOf(span2, context = {}) {
|
|
790
|
+
const served = span2.spanId === void 0;
|
|
791
|
+
const rawSpanId = served && typeof span2.id === "string" && typeof span2.traceId === "string" && span2.id.startsWith(`${span2.traceId}:`) ? span2.id.slice(span2.traceId.length + 1) : span2.spanId ?? span2.id;
|
|
792
|
+
const traceId = otlpId(span2.traceId, 16);
|
|
793
|
+
const spanId = otlpId(rawSpanId, 8);
|
|
794
|
+
const start = nanosToIso(span2.startTimeUnixNano ?? span2.startUnixNano);
|
|
795
|
+
const end = nanosToIso(span2.endTimeUnixNano ?? span2.endUnixNano);
|
|
796
|
+
const attributes = attributeBag(span2.attributes);
|
|
797
|
+
const otelKind = typeof span2.kind === "number" ? OTEL_SPAN_KINDS[span2.kind] : nonEmptyString(span2.kind) ?? void 0;
|
|
798
|
+
if (otelKind !== void 0 && attributes[OTEL_SPAN_KIND_ATTR] === void 0) attributes[OTEL_SPAN_KIND_ATTR] = otelKind;
|
|
799
|
+
const status = served ? statusRow(span2.statusCode, span2.statusMessage) : statusRow(isObject(span2.status) ? span2.status.code ?? OTEL_STATUS_CODES[0] : OTEL_STATUS_CODES[0], isObject(span2.status) ? span2.status.message : void 0);
|
|
800
|
+
const links = !served && Array.isArray(span2.links) ? span2.links.map((link) => isObject(link) ? {
|
|
801
|
+
...otlpId(link.traceId, 16) === void 0 ? {} : { trace_id: otlpId(link.traceId, 16) },
|
|
802
|
+
...otlpId(link.spanId, 8) === void 0 ? {} : { span_id: otlpId(link.spanId, 8) },
|
|
803
|
+
...link.attributes === void 0 ? {} : { attributes: attributeBag(link.attributes) }
|
|
804
|
+
} : link) : void 0;
|
|
805
|
+
return {
|
|
806
|
+
...traceId === void 0 ? {} : { trace_id: traceId },
|
|
807
|
+
...spanId === void 0 ? {} : { span_id: spanId },
|
|
808
|
+
parent_span_id: otlpId(span2.parentSpanId, 8) ?? null,
|
|
809
|
+
...typeof span2.name === "string" ? { name: span2.name } : {},
|
|
810
|
+
...start === void 0 ? {} : { start_time: start },
|
|
811
|
+
...end === void 0 ? {} : { end_time: end },
|
|
812
|
+
...status === void 0 ? {} : { status },
|
|
813
|
+
attributes,
|
|
814
|
+
...links === void 0 ? {} : { links },
|
|
815
|
+
...context.resource ? { resource: context.resource } : {},
|
|
816
|
+
...context.scope ? { scope: context.scope } : {}
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function* envelopeRows(envelope) {
|
|
820
|
+
for (const resourceSpans of envelope.resourceSpans) {
|
|
821
|
+
if (!isObject(resourceSpans)) {
|
|
822
|
+
yield resourceSpans;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
const resource = isObject(resourceSpans.resource) ? { attributes: attributeBag(resourceSpans.resource.attributes) } : void 0;
|
|
826
|
+
const scopes = resourceSpans.scopeSpans ?? resourceSpans.instrumentationLibrarySpans;
|
|
827
|
+
if (!Array.isArray(scopes)) continue;
|
|
828
|
+
for (const scopeSpans of scopes) {
|
|
829
|
+
if (!isObject(scopeSpans)) {
|
|
830
|
+
yield scopeSpans;
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const declared = scopeSpans.scope ?? scopeSpans.instrumentationLibrary;
|
|
834
|
+
const scope = isObject(declared) ? {
|
|
835
|
+
...typeof declared.name === "string" ? { name: declared.name } : {},
|
|
836
|
+
...typeof declared.version === "string" ? { version: declared.version } : {}
|
|
837
|
+
} : void 0;
|
|
838
|
+
if (!Array.isArray(scopeSpans.spans)) continue;
|
|
839
|
+
for (const span2 of scopeSpans.spans) yield isObject(span2) ? contractRowOf(span2, { resource, scope }) : span2;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function* contractRows(value) {
|
|
844
|
+
if (isOtlpJsonEnvelope(value)) yield* envelopeRows(value);
|
|
845
|
+
else if (isCamelCaseSpanRow(value)) yield contractRowOf(value);
|
|
846
|
+
else yield value;
|
|
847
|
+
}
|
|
848
|
+
var MAX_WHOLE_DOCUMENT_BYTES = 256 * 1024 * 1024;
|
|
849
|
+
async function* fileRows(file, issues) {
|
|
850
|
+
if (await isOtlpJsonFile(file)) {
|
|
851
|
+
for await (const { value, line } of readJsonValues(file, (line2, detail) => issues.push({ kind: "unparseable-line", file, line: line2, detail }))) {
|
|
852
|
+
for (const row of contractRows(value)) yield { value: row, line };
|
|
853
|
+
}
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const firstIssue = issues.length;
|
|
857
|
+
let parsed = 0;
|
|
858
|
+
for await (const { value, lineNumber } of readJsonlLines(file, {
|
|
859
|
+
mode: "recover",
|
|
860
|
+
onCorruption: (receipt) => {
|
|
861
|
+
issues.push({ kind: "unparseable-line", file, line: receipt.lineNumber, detail: `line is not JSON (${receipt.byteLength} bytes)` });
|
|
862
|
+
}
|
|
863
|
+
})) {
|
|
864
|
+
parsed += 1;
|
|
865
|
+
for (const row of contractRows(value)) yield { value: row, line: lineNumber };
|
|
866
|
+
}
|
|
867
|
+
if (parsed > 0 || issues.length === firstIssue || (await stat(file)).size > MAX_WHOLE_DOCUMENT_BYTES) return;
|
|
868
|
+
let document;
|
|
869
|
+
try {
|
|
870
|
+
document = JSON.parse(await readFile(file, "utf8"));
|
|
871
|
+
} catch {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
issues.splice(firstIssue);
|
|
875
|
+
for (const value of Array.isArray(document) ? document : [document]) {
|
|
876
|
+
for (const row of contractRows(value)) yield { value: row, line: 1 };
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
async function readOtlpInput(path) {
|
|
880
|
+
const { files: paths, skipped } = await resolveOtlpInputFiles(path);
|
|
881
|
+
const files = [];
|
|
882
|
+
const rows = [];
|
|
883
|
+
const spans = [];
|
|
884
|
+
const issues = [];
|
|
885
|
+
const withheld = [];
|
|
886
|
+
const seenIdentities = /* @__PURE__ */ new Map();
|
|
887
|
+
for (const file of paths) {
|
|
888
|
+
const fallbackTraceId = `otlp:${basename(file)}`;
|
|
889
|
+
let rowsFromFile = 0;
|
|
890
|
+
let spansFromFile = 0;
|
|
891
|
+
let repeatsFromFile = 0;
|
|
892
|
+
const traceIds = /* @__PURE__ */ new Set();
|
|
893
|
+
for await (const { value: row, line } of fileRows(file, issues)) {
|
|
894
|
+
rowsFromFile += 1;
|
|
895
|
+
rows.push(row);
|
|
896
|
+
const converted = otlpRowToSpan(row, fallbackTraceId);
|
|
897
|
+
if (converted.withheld) withheld.push({ ...converted.withheld, file, line });
|
|
898
|
+
if (converted.span) {
|
|
899
|
+
const identity = JSON.stringify([converted.span.trace_id, converted.span.span_id]);
|
|
900
|
+
const content = canonicalJson(converted.span);
|
|
901
|
+
const seen = seenIdentities.get(identity);
|
|
902
|
+
if (seen !== void 0) {
|
|
903
|
+
if (seen === content) {
|
|
904
|
+
repeatsFromFile += 1;
|
|
905
|
+
traceIds.add(converted.span.trace_id);
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
issues.push({
|
|
909
|
+
kind: "conflicting-span-id",
|
|
910
|
+
file,
|
|
911
|
+
line,
|
|
912
|
+
detail: `span ${converted.span.span_id} in trace ${converted.span.trace_id} was already read with DIFFERENT content; the later row was dropped`
|
|
913
|
+
});
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
seenIdentities.set(identity, content);
|
|
917
|
+
spans.push(converted.span);
|
|
918
|
+
traceIds.add(converted.span.trace_id);
|
|
919
|
+
spansFromFile += 1;
|
|
920
|
+
} else if (converted.issue) {
|
|
921
|
+
issues.push({ ...converted.issue, file, line });
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
files.push({
|
|
925
|
+
path: file,
|
|
926
|
+
rows: rowsFromFile,
|
|
927
|
+
spans: spansFromFile,
|
|
928
|
+
repeats: repeatsFromFile,
|
|
929
|
+
traceIds: [...traceIds]
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
const unreadable = stampUnreadableSourceRows(spans, issues);
|
|
933
|
+
return {
|
|
934
|
+
files,
|
|
935
|
+
skipped,
|
|
936
|
+
rows,
|
|
937
|
+
spans,
|
|
938
|
+
// Cast, not conversion: `validateTraceSpans` trusts nothing about its input
|
|
939
|
+
// and reports whatever it finds, which is exactly what a foreign trace needs.
|
|
940
|
+
validation: validateTraceSpans(rows),
|
|
941
|
+
issues,
|
|
942
|
+
withheld,
|
|
943
|
+
unreadable
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function positiveInteger(value) {
|
|
947
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : 0;
|
|
948
|
+
}
|
|
949
|
+
function stampUnreadableSourceRows(spans, issues) {
|
|
950
|
+
let inherited = 0;
|
|
951
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
952
|
+
for (const span2 of spans) {
|
|
953
|
+
inherited = Math.max(inherited, positiveInteger(span2.attributes[SOURCE_UNREADABLE_ROWS_ATTR]));
|
|
954
|
+
const declared = span2.attributes[SOURCE_UNREADABLE_KINDS_ATTR];
|
|
955
|
+
if (typeof declared === "string") {
|
|
956
|
+
for (const kind of declared.split(",")) if (kind.length > 0) kinds.add(kind);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
for (const issue of issues) kinds.add(issue.kind);
|
|
960
|
+
const total = inherited + issues.length;
|
|
961
|
+
const sortedKinds = [...kinds].sort();
|
|
962
|
+
if (total > 0) {
|
|
963
|
+
for (const span2 of spans) {
|
|
964
|
+
span2.attributes[SOURCE_UNREADABLE_ROWS_ATTR] = total;
|
|
965
|
+
span2.attributes[SOURCE_UNREADABLE_KINDS_ATTR] = sortedKinds.join(",");
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return { rows: total, kinds: sortedKinds, inherited };
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// src/version.ts
|
|
972
|
+
import { readFileSync } from "fs";
|
|
973
|
+
var fromSource;
|
|
974
|
+
function tracesVersion() {
|
|
975
|
+
if (true) return "1.0.1";
|
|
976
|
+
if (fromSource) return fromSource;
|
|
977
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
978
|
+
if (typeof pkg.version !== "string" || !pkg.version) throw new Error("package.json is missing version");
|
|
979
|
+
fromSource = pkg.version;
|
|
980
|
+
return fromSource;
|
|
981
|
+
}
|
|
982
|
+
function tracesCommit() {
|
|
983
|
+
return true ? "1feb8c181d7a" : "source";
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/otlp.ts
|
|
987
|
+
import {
|
|
988
|
+
applyLlmSpanOtlpAttributes,
|
|
989
|
+
LLM_COST_USD,
|
|
990
|
+
LLM_INPUT_TOKENS,
|
|
991
|
+
LLM_MODEL_NAME,
|
|
992
|
+
LLM_OUTPUT_TOKENS,
|
|
993
|
+
OPENINFERENCE_SPAN_KIND,
|
|
994
|
+
TOOL_NAME as TOOL_NAME2
|
|
995
|
+
} from "@tangle-network/agent-eval/trace-attributes";
|
|
996
|
+
|
|
997
|
+
// src/span-validation.ts
|
|
998
|
+
var STATUS_CODES = /* @__PURE__ */ new Set(["OK", "ERROR", "UNSET"]);
|
|
999
|
+
function validateOtlpSpans(input, source = "OTLP spans") {
|
|
1000
|
+
if (!Array.isArray(input)) throw new TypeError(`${source} must be an array`);
|
|
1001
|
+
const spans = [];
|
|
1002
|
+
const identities = /* @__PURE__ */ new Set();
|
|
1003
|
+
for (const [index, value] of input.entries()) {
|
|
1004
|
+
const path = `${source}[${index}]`;
|
|
1005
|
+
const record = requireObject(value, path);
|
|
1006
|
+
const traceId = requireNonEmptyString(record.trace_id, `${path}.trace_id`);
|
|
1007
|
+
const spanId = requireNonEmptyString(record.span_id, `${path}.span_id`);
|
|
1008
|
+
const parentSpanId = record.parent_span_id;
|
|
1009
|
+
if (parentSpanId !== null) {
|
|
1010
|
+
requireNonEmptyString(parentSpanId, `${path}.parent_span_id`);
|
|
1011
|
+
}
|
|
1012
|
+
requireNonEmptyString(record.name, `${path}.name`);
|
|
1013
|
+
const startTime = requireNonEmptyString(record.start_time, `${path}.start_time`);
|
|
1014
|
+
const endTime = requireNonEmptyString(record.end_time, `${path}.end_time`);
|
|
1015
|
+
const startMs = parseTimestamp(startTime, `${path}.start_time`);
|
|
1016
|
+
const endMs = parseTimestamp(endTime, `${path}.end_time`);
|
|
1017
|
+
if (endMs < startMs) {
|
|
1018
|
+
throw new RangeError(
|
|
1019
|
+
`${path}.end_time must not precede start_time (${endTime} < ${startTime})`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
const status = requireObject(record.status, `${path}.status`);
|
|
1023
|
+
if (!STATUS_CODES.has(status.code)) {
|
|
1024
|
+
throw new TypeError(`${path}.status.code must be OK, ERROR, or UNSET`);
|
|
1025
|
+
}
|
|
1026
|
+
if (status.message !== void 0 && typeof status.message !== "string") {
|
|
1027
|
+
throw new TypeError(`${path}.status.message must be a string when present`);
|
|
1028
|
+
}
|
|
1029
|
+
requireObject(record.attributes, `${path}.attributes`);
|
|
1030
|
+
requireLinks(record.links, `${path}.links`);
|
|
1031
|
+
const identity = JSON.stringify([traceId, spanId]);
|
|
1032
|
+
if (identities.has(identity)) {
|
|
1033
|
+
throw new Error(`${source} contains duplicate span identity (${traceId}, ${spanId})`);
|
|
1034
|
+
}
|
|
1035
|
+
identities.add(identity);
|
|
1036
|
+
spans.push(value);
|
|
1037
|
+
}
|
|
1038
|
+
return spans;
|
|
1039
|
+
}
|
|
1040
|
+
function requireLinks(value, path) {
|
|
1041
|
+
if (value === void 0) return;
|
|
1042
|
+
if (!Array.isArray(value)) throw new TypeError(`${path} must be an array when present`);
|
|
1043
|
+
for (const [index, entry] of value.entries()) {
|
|
1044
|
+
const link = requireObject(entry, `${path}[${index}]`);
|
|
1045
|
+
requireNonEmptyString(link.trace_id, `${path}[${index}].trace_id`);
|
|
1046
|
+
requireNonEmptyString(link.span_id, `${path}[${index}].span_id`);
|
|
1047
|
+
if (link.attributes !== void 0) requireObject(link.attributes, `${path}[${index}].attributes`);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
function parseTimestamp(value, path) {
|
|
1051
|
+
try {
|
|
1052
|
+
return parseIsoToEpochMs(value);
|
|
1053
|
+
} catch {
|
|
1054
|
+
throw new TypeError(`${path} must be a finite ISO-8601 or epoch-millis timestamp`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function requireObject(value, path) {
|
|
1058
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1059
|
+
throw new TypeError(`${path} must be an object`);
|
|
1060
|
+
}
|
|
1061
|
+
return value;
|
|
1062
|
+
}
|
|
1063
|
+
function requireNonEmptyString(value, path) {
|
|
1064
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1065
|
+
throw new TypeError(`${path} must be a non-empty string`);
|
|
1066
|
+
}
|
|
1067
|
+
return value;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// src/otlp.ts
|
|
1071
|
+
function span(input) {
|
|
1072
|
+
const attributes = {
|
|
1073
|
+
[OPENINFERENCE_SPAN_KIND]: input.kind
|
|
1074
|
+
};
|
|
1075
|
+
if (input.service != null) attributes["service.name"] = input.service;
|
|
1076
|
+
if (input.agent != null) attributes["agent.name"] = input.agent;
|
|
1077
|
+
if (input.tool != null) attributes[TOOL_NAME2] = input.tool;
|
|
1078
|
+
applyLlmSpanOtlpAttributes(attributes, {
|
|
1079
|
+
model: input.model ?? void 0,
|
|
1080
|
+
inputTokens: input.inputTokens ?? void 0,
|
|
1081
|
+
outputTokens: input.outputTokens ?? void 0,
|
|
1082
|
+
reasoningTokens: input.reasoningTokens ?? void 0,
|
|
1083
|
+
cachedTokens: input.cachedInputTokens ?? void 0,
|
|
1084
|
+
cacheWriteTokens: input.cacheWriteInputTokens ?? void 0,
|
|
1085
|
+
costUsd: input.costUsd ?? void 0
|
|
1086
|
+
});
|
|
1087
|
+
if (input.step != null) attributes.step = input.step;
|
|
1088
|
+
if (input.content != null && input.content.length > 0) attributes["content"] = input.content;
|
|
1089
|
+
if (input.extra) Object.assign(attributes, input.extra);
|
|
1090
|
+
if (input.content) Object.assign(attributes, sourceAttributes("content", input.contentSource));
|
|
1091
|
+
const status = { code: input.status ?? "OK" };
|
|
1092
|
+
if (input.statusMessage && input.statusMessage.length > 0) status.message = input.statusMessage;
|
|
1093
|
+
return {
|
|
1094
|
+
trace_id: input.traceId,
|
|
1095
|
+
span_id: input.spanId,
|
|
1096
|
+
parent_span_id: input.parentSpanId ?? null,
|
|
1097
|
+
name: input.name,
|
|
1098
|
+
start_time: input.startTime,
|
|
1099
|
+
end_time: input.endTime ?? input.startTime,
|
|
1100
|
+
status,
|
|
1101
|
+
attributes,
|
|
1102
|
+
...input.links && input.links.length > 0 ? { links: input.links.map((link) => ({ ...link })) } : {}
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
function producerField(attributes, field) {
|
|
1106
|
+
const declared = attributes[SUBSTITUTED_FIELDS_ATTR];
|
|
1107
|
+
if (typeof declared !== "string" || !declared.split(",").includes(field)) return { substituted: false };
|
|
1108
|
+
const key = RAW_FIELD_ATTRIBUTES[field];
|
|
1109
|
+
return key in attributes ? { substituted: true, wrote: true, value: attributes[key] } : { substituted: true, wrote: false };
|
|
1110
|
+
}
|
|
1111
|
+
var EXPORT_RULE = Object.freeze({
|
|
1112
|
+
trace_id: "analysed",
|
|
1113
|
+
end_time: "faithful",
|
|
1114
|
+
"status.code": "faithful",
|
|
1115
|
+
kind: "declared-or-analysed",
|
|
1116
|
+
links: "faithful"
|
|
1117
|
+
});
|
|
1118
|
+
function producerEntry(attributes, field, key, analysed) {
|
|
1119
|
+
const producer = producerField(attributes, field);
|
|
1120
|
+
if (producer.substituted) {
|
|
1121
|
+
if (producer.wrote && EXPORT_RULE[field] !== "analysed") return { [key]: producer.value };
|
|
1122
|
+
if (EXPORT_RULE[field] === "faithful") return {};
|
|
1123
|
+
}
|
|
1124
|
+
return analysed === void 0 ? {} : { [key]: analysed };
|
|
1125
|
+
}
|
|
1126
|
+
function toOpenInferenceSpan(s) {
|
|
1127
|
+
const a = s.attributes;
|
|
1128
|
+
const attributes = { ...a };
|
|
1129
|
+
attributes["inference.observation_kind"] ??= a[OPENINFERENCE_SPAN_KIND];
|
|
1130
|
+
if (a["agent.name"] != null) attributes["inference.agent_name"] ??= a["agent.name"];
|
|
1131
|
+
if (a[LLM_MODEL_NAME] != null) attributes["inference.llm.model_name"] ??= a[LLM_MODEL_NAME];
|
|
1132
|
+
if (a[LLM_INPUT_TOKENS] != null) attributes["inference.llm.input_tokens"] ??= a[LLM_INPUT_TOKENS];
|
|
1133
|
+
if (a[LLM_OUTPUT_TOKENS] != null) attributes["inference.llm.output_tokens"] ??= a[LLM_OUTPUT_TOKENS];
|
|
1134
|
+
if (a[LLM_COST_USD] != null) attributes["inference.llm.cost.total"] ??= a[LLM_COST_USD];
|
|
1135
|
+
const resourceAttrs = {};
|
|
1136
|
+
if (a["service.name"] != null) resourceAttrs["service.name"] = a["service.name"];
|
|
1137
|
+
if (a["agent.name"] != null) resourceAttrs["agent.name"] = a["agent.name"];
|
|
1138
|
+
for (const k of ["tangle.subject.key", "git.repository", "git.branch", "git.commit", "tangle.cwd", "traces.repo_resolution_source"]) {
|
|
1139
|
+
if (a[k] != null) resourceAttrs[k] = a[k];
|
|
1140
|
+
}
|
|
1141
|
+
for (const k of ["container.image.name", "container.image.digest", "tangle.sandbox.id", "tangle.environment.cwd"]) {
|
|
1142
|
+
if (a[k] != null) resourceAttrs[k] = a[k];
|
|
1143
|
+
}
|
|
1144
|
+
for (const k of ["traces.source.unreadable_rows", "traces.source.unreadable_row_kinds"]) {
|
|
1145
|
+
if (a[k] != null) resourceAttrs[k] = a[k];
|
|
1146
|
+
}
|
|
1147
|
+
resourceAttrs["traces.version"] = tracesVersion();
|
|
1148
|
+
const analysedKind = typeof a[OPENINFERENCE_SPAN_KIND] === "string" ? a[OPENINFERENCE_SPAN_KIND] : "UNKNOWN";
|
|
1149
|
+
return {
|
|
1150
|
+
// Substituted fields carry the PRODUCER's value, or no field at all when the
|
|
1151
|
+
// producer wrote none — see `producerField`. Emitting the analysed value
|
|
1152
|
+
// here is what made a re-export validate cleaner than its own source.
|
|
1153
|
+
...producerEntry(a, "trace_id", "trace_id", s.trace_id),
|
|
1154
|
+
span_id: s.span_id,
|
|
1155
|
+
parent_span_id: s.parent_span_id ?? "",
|
|
1156
|
+
name: s.name,
|
|
1157
|
+
...producerEntry(a, "kind", "kind", analysedKind),
|
|
1158
|
+
start_time: s.start_time,
|
|
1159
|
+
...producerEntry(a, "end_time", "end_time", s.end_time),
|
|
1160
|
+
status: {
|
|
1161
|
+
...producerEntry(a, "status.code", "code", `STATUS_CODE_${s.status.code}`),
|
|
1162
|
+
message: s.status.message ?? ""
|
|
1163
|
+
},
|
|
1164
|
+
resource: { attributes: resourceAttrs },
|
|
1165
|
+
scope: { name: "tangle-traces", version: "" },
|
|
1166
|
+
attributes,
|
|
1167
|
+
...producerEntry(
|
|
1168
|
+
a,
|
|
1169
|
+
"links",
|
|
1170
|
+
"links",
|
|
1171
|
+
s.links && s.links.length > 0 ? s.links.map((link) => ({ ...link })) : void 0
|
|
1172
|
+
)
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
function* iterSerializedSpans(spans) {
|
|
1176
|
+
for (const s of validateOtlpSpans(spans, "serialized spans")) {
|
|
1177
|
+
yield `${JSON.stringify(toOpenInferenceSpan(s))}
|
|
1178
|
+
`;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
var OTLP_WRITE_CHUNK_CHARS = 4 * 1024 * 1024;
|
|
1182
|
+
async function writeOtlpFile(spans, path) {
|
|
1183
|
+
const { open: open3 } = await import("fs/promises");
|
|
1184
|
+
const handle = await open3(path, "w");
|
|
1185
|
+
try {
|
|
1186
|
+
let chunk = "";
|
|
1187
|
+
for (const line of iterSerializedSpans(spans)) {
|
|
1188
|
+
chunk += line;
|
|
1189
|
+
if (chunk.length >= OTLP_WRITE_CHUNK_CHARS) {
|
|
1190
|
+
await handle.write(chunk);
|
|
1191
|
+
chunk = "";
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (chunk.length > 0) await handle.write(chunk);
|
|
1195
|
+
} finally {
|
|
1196
|
+
await handle.close();
|
|
1197
|
+
}
|
|
1198
|
+
return path;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
export {
|
|
1202
|
+
appendAll,
|
|
1203
|
+
readJsonl,
|
|
1204
|
+
takeJsonl,
|
|
1205
|
+
TRACE_ID_MINTED_ATTR,
|
|
1206
|
+
otlpRowToSpan,
|
|
1207
|
+
isStandardOtlpRow,
|
|
1208
|
+
isOtlpJsonFile,
|
|
1209
|
+
readOtlpInput,
|
|
1210
|
+
validateOtlpSpans,
|
|
1211
|
+
tracesVersion,
|
|
1212
|
+
tracesCommit,
|
|
1213
|
+
span,
|
|
1214
|
+
toOpenInferenceSpan,
|
|
1215
|
+
iterSerializedSpans,
|
|
1216
|
+
writeOtlpFile
|
|
1217
|
+
};
|