@rivetkit/traces 2.0.4-rc.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/LICENSE +203 -0
- package/dist/schemas/v1.ts +653 -0
- package/dist/tsup/chunk-2D7JND4Z.js +63 -0
- package/dist/tsup/chunk-2D7JND4Z.js.map +1 -0
- package/dist/tsup/chunk-7RQXHEKZ.js +541 -0
- package/dist/tsup/chunk-7RQXHEKZ.js.map +1 -0
- package/dist/tsup/chunk-DXS2HLRN.cjs +63 -0
- package/dist/tsup/chunk-DXS2HLRN.cjs.map +1 -0
- package/dist/tsup/chunk-QOSSO6CN.cjs +541 -0
- package/dist/tsup/chunk-QOSSO6CN.cjs.map +1 -0
- package/dist/tsup/chunk-UNGPFJ4C.js +417 -0
- package/dist/tsup/chunk-UNGPFJ4C.js.map +1 -0
- package/dist/tsup/chunk-ZTVH74GC.cjs +417 -0
- package/dist/tsup/chunk-ZTVH74GC.cjs.map +1 -0
- package/dist/tsup/encoding.cjs +20 -0
- package/dist/tsup/encoding.cjs.map +1 -0
- package/dist/tsup/encoding.d.cts +6 -0
- package/dist/tsup/encoding.d.ts +6 -0
- package/dist/tsup/encoding.js +20 -0
- package/dist/tsup/encoding.js.map +1 -0
- package/dist/tsup/index.browser.cjs +15 -0
- package/dist/tsup/index.browser.cjs.map +1 -0
- package/dist/tsup/index.browser.d.cts +7 -0
- package/dist/tsup/index.browser.d.ts +7 -0
- package/dist/tsup/index.browser.js +15 -0
- package/dist/tsup/index.browser.js.map +1 -0
- package/dist/tsup/index.cjs +921 -0
- package/dist/tsup/index.cjs.map +1 -0
- package/dist/tsup/index.d.cts +9 -0
- package/dist/tsup/index.d.ts +9 -0
- package/dist/tsup/index.js +921 -0
- package/dist/tsup/index.js.map +1 -0
- package/dist/tsup/noop-CcgjEgCu.d.cts +99 -0
- package/dist/tsup/noop-D-YAZiGa.d.ts +99 -0
- package/dist/tsup/otlp-Da4Yz0xC.d.cts +81 -0
- package/dist/tsup/otlp-Da4Yz0xC.d.ts +81 -0
- package/dist/tsup/otlp-entry.cjs +16 -0
- package/dist/tsup/otlp-entry.cjs.map +1 -0
- package/dist/tsup/otlp-entry.d.cts +10 -0
- package/dist/tsup/otlp-entry.d.ts +10 -0
- package/dist/tsup/otlp-entry.js +16 -0
- package/dist/tsup/otlp-entry.js.map +1 -0
- package/dist/tsup/v1-DovAIc7f.d.cts +118 -0
- package/dist/tsup/v1-DovAIc7f.d.ts +118 -0
- package/package.json +74 -0
- package/schemas/v1.bare +177 -0
- package/schemas/versioned.ts +99 -0
- package/src/encoding.ts +18 -0
- package/src/index.browser.ts +13 -0
- package/src/index.ts +31 -0
- package/src/noop.ts +81 -0
- package/src/otlp-entry.ts +18 -0
- package/src/otlp.ts +158 -0
- package/src/read-range.ts +502 -0
- package/src/traces.ts +1186 -0
- package/src/types.ts +94 -0
package/src/traces.ts
ADDED
|
@@ -0,0 +1,1186 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { performance } from "node:perf_hooks";
|
|
5
|
+
import { decode as decodeCbor, encode as encodeCbor } from "cbor-x";
|
|
6
|
+
import { pack, unpack } from "fdb-tuple";
|
|
7
|
+
import {
|
|
8
|
+
CHUNK_VERSIONED,
|
|
9
|
+
CURRENT_VERSION,
|
|
10
|
+
encodeRecord,
|
|
11
|
+
type ActiveSpanRef,
|
|
12
|
+
type Attributes,
|
|
13
|
+
type Chunk,
|
|
14
|
+
type KeyValue,
|
|
15
|
+
type Record as TraceRecord,
|
|
16
|
+
type RecordBody,
|
|
17
|
+
type SpanEnd,
|
|
18
|
+
type SpanEvent,
|
|
19
|
+
type SpanId,
|
|
20
|
+
type SpanLink,
|
|
21
|
+
type SpanRecordKey,
|
|
22
|
+
type SpanSnapshot,
|
|
23
|
+
type SpanStart,
|
|
24
|
+
type SpanStatus,
|
|
25
|
+
SpanStatusCode,
|
|
26
|
+
type SpanUpdate,
|
|
27
|
+
type StringId,
|
|
28
|
+
type TraceId,
|
|
29
|
+
} from "../schemas/versioned.js";
|
|
30
|
+
import {
|
|
31
|
+
hexFromBytes,
|
|
32
|
+
type OtlpExportTraceServiceRequestJson,
|
|
33
|
+
type OtlpResource,
|
|
34
|
+
} from "./otlp.js";
|
|
35
|
+
import { readRangeWireToOtlp } from "./read-range.js";
|
|
36
|
+
import type {
|
|
37
|
+
EndSpanOptions,
|
|
38
|
+
EventOptions,
|
|
39
|
+
ReadRangeOptions,
|
|
40
|
+
ReadRangeResult,
|
|
41
|
+
ReadRangeWire,
|
|
42
|
+
SpanHandle,
|
|
43
|
+
SpanStatusInput,
|
|
44
|
+
StartSpanOptions,
|
|
45
|
+
Traces,
|
|
46
|
+
TracesDriver,
|
|
47
|
+
TracesOptions,
|
|
48
|
+
UpdateSpanOptions,
|
|
49
|
+
} from "./types.js";
|
|
50
|
+
|
|
51
|
+
// OTLP v1 JSON reference: https://opentelemetry.io/docs/specs/otlp/
|
|
52
|
+
// Span data model reference: https://opentelemetry.io/docs/specs/otel/trace/api/
|
|
53
|
+
|
|
54
|
+
const KEY_PREFIX = {
|
|
55
|
+
DATA: 1,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const MAX_CHUNK_ID = 0xffff_ffff;
|
|
59
|
+
const AFTER_MAX_CHUNK_ID = 0x1_0000_0000;
|
|
60
|
+
|
|
61
|
+
const DEFAULT_BUCKET_SIZE_SEC = 3600;
|
|
62
|
+
const DEFAULT_TARGET_CHUNK_BYTES = 512 * 1024;
|
|
63
|
+
const DEFAULT_MAX_CHUNK_BYTES = 1024 * 1024;
|
|
64
|
+
const DEFAULT_MAX_CHUNK_AGE_MS = 5000;
|
|
65
|
+
const DEFAULT_SNAPSHOT_INTERVAL_MS = 300_000;
|
|
66
|
+
const DEFAULT_SNAPSHOT_BYTES_THRESHOLD = 256 * 1024;
|
|
67
|
+
const DEFAULT_MAX_READ_LIMIT = 10_000;
|
|
68
|
+
const DEFAULT_MAX_ACTIVE_SPANS = 10_000;
|
|
69
|
+
|
|
70
|
+
const SPAN_ID_BYTES = 8;
|
|
71
|
+
const TRACE_ID_BYTES = 16;
|
|
72
|
+
|
|
73
|
+
type AttributeMap = Map<string, unknown>;
|
|
74
|
+
|
|
75
|
+
type SpanState = {
|
|
76
|
+
spanId: SpanId;
|
|
77
|
+
traceId: TraceId;
|
|
78
|
+
parentSpanId: SpanId | null;
|
|
79
|
+
name: string;
|
|
80
|
+
kind: number;
|
|
81
|
+
traceState: string | null;
|
|
82
|
+
flags: number;
|
|
83
|
+
attributes: AttributeMap;
|
|
84
|
+
droppedAttributesCount: number;
|
|
85
|
+
links: LinkState[];
|
|
86
|
+
droppedLinksCount: number;
|
|
87
|
+
status: SpanStatus | null;
|
|
88
|
+
startTimeUnixNs: bigint;
|
|
89
|
+
depth: number;
|
|
90
|
+
bytesSinceSnapshot: number;
|
|
91
|
+
lastSnapshotMonoMs: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type LinkState = {
|
|
95
|
+
traceId: TraceId;
|
|
96
|
+
spanId: SpanId;
|
|
97
|
+
traceState: string | null;
|
|
98
|
+
attributes: AttributeMap;
|
|
99
|
+
droppedAttributesCount: number;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
type ChunkState = {
|
|
103
|
+
bucketStartSec: number;
|
|
104
|
+
chunkId: number;
|
|
105
|
+
baseUnixNs: bigint;
|
|
106
|
+
strings: string[];
|
|
107
|
+
stringIds: Map<string, number>;
|
|
108
|
+
records: TraceRecord[];
|
|
109
|
+
sizeBytes: number;
|
|
110
|
+
createdAtMonoMs: number;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
type PendingChunk = {
|
|
114
|
+
key: Uint8Array;
|
|
115
|
+
bucketStartSec: number;
|
|
116
|
+
chunkId: number;
|
|
117
|
+
chunk: Chunk;
|
|
118
|
+
bytes: Uint8Array;
|
|
119
|
+
maxRecordNs: bigint;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const spanContext = new AsyncLocalStorage<SpanHandle | null>();
|
|
123
|
+
|
|
124
|
+
function spanKey(spanId: Uint8Array | SpanId): string {
|
|
125
|
+
return hexFromBytes(normalizeBytes(spanId));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
129
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
130
|
+
copy.set(bytes);
|
|
131
|
+
return copy.buffer;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function toUint8Array(buffer: ArrayBuffer): Uint8Array {
|
|
135
|
+
return new Uint8Array(buffer);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeBytes(input: Uint8Array | ArrayBuffer): Uint8Array {
|
|
139
|
+
return input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createTraces(
|
|
143
|
+
options: TracesOptions<OtlpResource>,
|
|
144
|
+
): Traces<OtlpExportTraceServiceRequestJson> {
|
|
145
|
+
const driver = options.driver;
|
|
146
|
+
const bucketSizeSec = options.bucketSizeSec ?? DEFAULT_BUCKET_SIZE_SEC;
|
|
147
|
+
const maxChunkBytes = options.maxChunkBytes ?? DEFAULT_MAX_CHUNK_BYTES;
|
|
148
|
+
const targetChunkBytes = Math.min(
|
|
149
|
+
options.targetChunkBytes ?? DEFAULT_TARGET_CHUNK_BYTES,
|
|
150
|
+
maxChunkBytes,
|
|
151
|
+
);
|
|
152
|
+
const maxChunkAgeMs = options.maxChunkAgeMs ?? DEFAULT_MAX_CHUNK_AGE_MS;
|
|
153
|
+
const snapshotIntervalMs =
|
|
154
|
+
options.snapshotIntervalMs ?? DEFAULT_SNAPSHOT_INTERVAL_MS;
|
|
155
|
+
const snapshotBytesThreshold =
|
|
156
|
+
options.snapshotBytesThreshold ?? DEFAULT_SNAPSHOT_BYTES_THRESHOLD;
|
|
157
|
+
const maxActiveSpans = options.maxActiveSpans ?? DEFAULT_MAX_ACTIVE_SPANS;
|
|
158
|
+
const maxReadLimit = options.maxReadLimit ?? DEFAULT_MAX_READ_LIMIT;
|
|
159
|
+
const resource = options.resource;
|
|
160
|
+
|
|
161
|
+
const timeAnchor = {
|
|
162
|
+
unixMs: Date.now(),
|
|
163
|
+
monoMs: performance.now(),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const activeSpans = new Map<string, SpanState>();
|
|
167
|
+
const activeSpanRefs = new Map<string, ActiveSpanRef>();
|
|
168
|
+
const pendingChunks: PendingChunk[] = [];
|
|
169
|
+
let writeChain = Promise.resolve();
|
|
170
|
+
const bucketChunkCounters = new Map<number, number>();
|
|
171
|
+
|
|
172
|
+
function nowUnixMs(): number {
|
|
173
|
+
return timeAnchor.unixMs + (performance.now() - timeAnchor.monoMs);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function nowUnixNs(anchor: { unixMs: number; monoMs: number }): bigint {
|
|
177
|
+
const unixMs = anchor.unixMs + (performance.now() - anchor.monoMs);
|
|
178
|
+
const wholeMs = Math.floor(unixMs);
|
|
179
|
+
const fracMs = unixMs - wholeMs;
|
|
180
|
+
return BigInt(wholeMs) * 1_000_000n + BigInt(Math.floor(fracMs * 1_000_000));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function createChunkState(bucketStartSec: number): ChunkState {
|
|
184
|
+
return {
|
|
185
|
+
bucketStartSec,
|
|
186
|
+
chunkId: nextChunkId(bucketStartSec),
|
|
187
|
+
baseUnixNs: BigInt(bucketStartSec) * 1_000_000_000n,
|
|
188
|
+
strings: [],
|
|
189
|
+
stringIds: new Map(),
|
|
190
|
+
records: [],
|
|
191
|
+
sizeBytes: 0,
|
|
192
|
+
createdAtMonoMs: performance.now(),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function nextChunkId(bucketStartSec: number): number {
|
|
197
|
+
const current = bucketChunkCounters.get(bucketStartSec) ?? 0;
|
|
198
|
+
bucketChunkCounters.set(bucketStartSec, current + 1);
|
|
199
|
+
return current;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const currentChunk = createChunkState(
|
|
203
|
+
computeBucketStartSec(nowUnixNs(timeAnchor), bucketSizeSec),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
function computeBucketStartSec(
|
|
207
|
+
absoluteUnixNs: bigint,
|
|
208
|
+
bucketSize: number,
|
|
209
|
+
): number {
|
|
210
|
+
const sec = absoluteUnixNs / 1_000_000_000n;
|
|
211
|
+
const bucket = sec / BigInt(bucketSize);
|
|
212
|
+
return Number(bucket * BigInt(bucketSize));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function internString(value: string): StringId {
|
|
216
|
+
const existing = currentChunk.stringIds.get(value);
|
|
217
|
+
if (existing !== undefined) {
|
|
218
|
+
return existing;
|
|
219
|
+
}
|
|
220
|
+
const id = currentChunk.strings.length;
|
|
221
|
+
currentChunk.strings.push(value);
|
|
222
|
+
currentChunk.stringIds.set(value, id);
|
|
223
|
+
return id;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function encodeAttributes(
|
|
227
|
+
attributes?: Record<string, unknown>,
|
|
228
|
+
): { attributes: Attributes; dropped: number } {
|
|
229
|
+
const list: KeyValue[] = [];
|
|
230
|
+
let dropped = 0;
|
|
231
|
+
if (!attributes) {
|
|
232
|
+
return { attributes: list, dropped };
|
|
233
|
+
}
|
|
234
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
235
|
+
const sanitized = sanitizeAttributeValue(value);
|
|
236
|
+
if (sanitized === undefined) {
|
|
237
|
+
dropped++;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const encoded = encodeCbor(sanitized);
|
|
242
|
+
list.push({ key: internString(key), value: toArrayBuffer(encoded) });
|
|
243
|
+
} catch {
|
|
244
|
+
dropped++;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { attributes: list, dropped };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function sanitizeAttributeValue(value: unknown): unknown | undefined {
|
|
251
|
+
if (value === undefined || typeof value === "function") {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
if (typeof value === "symbol") {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
if (value instanceof Map) {
|
|
258
|
+
const obj: Record<string, unknown> = {};
|
|
259
|
+
for (const [key, mapValue] of value.entries()) {
|
|
260
|
+
if (typeof key !== "string") {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
const sanitized = sanitizeAttributeValue(mapValue);
|
|
264
|
+
if (sanitized !== undefined) {
|
|
265
|
+
obj[key] = sanitized;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return obj;
|
|
269
|
+
}
|
|
270
|
+
if (Array.isArray(value)) {
|
|
271
|
+
return value
|
|
272
|
+
.map((entry) => sanitizeAttributeValue(entry))
|
|
273
|
+
.filter((entry) => entry !== undefined);
|
|
274
|
+
}
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function encodeLinks(
|
|
279
|
+
links?: StartSpanOptions["links"],
|
|
280
|
+
): { links: SpanLink[]; dropped: number } {
|
|
281
|
+
const result: SpanLink[] = [];
|
|
282
|
+
let dropped = 0;
|
|
283
|
+
if (!links) {
|
|
284
|
+
return { links: result, dropped };
|
|
285
|
+
}
|
|
286
|
+
for (const link of links) {
|
|
287
|
+
const { attributes, dropped: droppedAttributes } = encodeAttributes(
|
|
288
|
+
link.attributes,
|
|
289
|
+
);
|
|
290
|
+
result.push({
|
|
291
|
+
traceId: toArrayBuffer(link.traceId),
|
|
292
|
+
spanId: toArrayBuffer(link.spanId),
|
|
293
|
+
traceState: link.traceState ?? null,
|
|
294
|
+
attributes,
|
|
295
|
+
droppedAttributesCount: droppedAttributes,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return { links: result, dropped };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function createSpanStartRecord(
|
|
302
|
+
spanId: SpanId,
|
|
303
|
+
traceId: TraceId,
|
|
304
|
+
name: string,
|
|
305
|
+
options: StartSpanOptions | undefined,
|
|
306
|
+
parentSpanId: SpanId | null,
|
|
307
|
+
): SpanStart {
|
|
308
|
+
const { attributes, dropped } = encodeAttributes(options?.attributes);
|
|
309
|
+
const { links, dropped: droppedLinks } = encodeLinks(options?.links);
|
|
310
|
+
return {
|
|
311
|
+
traceId,
|
|
312
|
+
spanId,
|
|
313
|
+
parentSpanId,
|
|
314
|
+
name: internString(name),
|
|
315
|
+
kind: options?.kind ?? 0,
|
|
316
|
+
traceState: options?.traceState ?? null,
|
|
317
|
+
flags: options?.flags ?? 0,
|
|
318
|
+
attributes,
|
|
319
|
+
droppedAttributesCount: dropped,
|
|
320
|
+
links,
|
|
321
|
+
droppedLinksCount: droppedLinks,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function createSpanUpdateRecord(
|
|
326
|
+
spanId: SpanId,
|
|
327
|
+
options: UpdateSpanOptions,
|
|
328
|
+
): SpanUpdate {
|
|
329
|
+
const { attributes, dropped } = encodeAttributes(options.attributes);
|
|
330
|
+
return {
|
|
331
|
+
spanId,
|
|
332
|
+
attributes,
|
|
333
|
+
droppedAttributesCount: dropped,
|
|
334
|
+
status: options.status ? toBareStatus(options.status) : null,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function createSpanEventRecord(
|
|
339
|
+
spanId: SpanId,
|
|
340
|
+
name: string,
|
|
341
|
+
options: EventOptions | undefined,
|
|
342
|
+
): SpanEvent {
|
|
343
|
+
const { attributes, dropped } = encodeAttributes(options?.attributes);
|
|
344
|
+
return {
|
|
345
|
+
spanId,
|
|
346
|
+
name: internString(name),
|
|
347
|
+
attributes,
|
|
348
|
+
droppedAttributesCount: dropped,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function createSpanEndRecord(
|
|
353
|
+
spanId: SpanId,
|
|
354
|
+
options: EndSpanOptions | undefined,
|
|
355
|
+
): SpanEnd {
|
|
356
|
+
return {
|
|
357
|
+
spanId,
|
|
358
|
+
status: options?.status ? toBareStatus(options.status) : null,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function createSpanSnapshotRecord(state: SpanState): SpanSnapshot {
|
|
363
|
+
const { attributes, dropped } = encodeAttributeMap(state.attributes);
|
|
364
|
+
const { links, dropped: droppedLinks } = encodeLinkState(state.links);
|
|
365
|
+
return {
|
|
366
|
+
traceId: state.traceId,
|
|
367
|
+
spanId: state.spanId,
|
|
368
|
+
parentSpanId: state.parentSpanId,
|
|
369
|
+
name: internString(state.name),
|
|
370
|
+
kind: state.kind,
|
|
371
|
+
startTimeUnixNs: state.startTimeUnixNs,
|
|
372
|
+
traceState: state.traceState,
|
|
373
|
+
flags: state.flags,
|
|
374
|
+
attributes,
|
|
375
|
+
droppedAttributesCount: state.droppedAttributesCount + dropped,
|
|
376
|
+
links,
|
|
377
|
+
droppedLinksCount: state.droppedLinksCount + droppedLinks,
|
|
378
|
+
status: state.status,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function encodeAttributeMap(
|
|
383
|
+
attributes: AttributeMap,
|
|
384
|
+
): { attributes: Attributes; dropped: number } {
|
|
385
|
+
const list: KeyValue[] = [];
|
|
386
|
+
let dropped = 0;
|
|
387
|
+
for (const [key, value] of attributes.entries()) {
|
|
388
|
+
const sanitized = sanitizeAttributeValue(value);
|
|
389
|
+
if (sanitized === undefined) {
|
|
390
|
+
dropped++;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
const encoded = encodeCbor(sanitized);
|
|
395
|
+
list.push({ key: internString(key), value: toArrayBuffer(encoded) });
|
|
396
|
+
} catch {
|
|
397
|
+
dropped++;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { attributes: list, dropped };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function buildAttributeMapFromInput(
|
|
404
|
+
attributes?: Record<string, unknown>,
|
|
405
|
+
): AttributeMap {
|
|
406
|
+
const map = new Map<string, unknown>();
|
|
407
|
+
if (!attributes) {
|
|
408
|
+
return map;
|
|
409
|
+
}
|
|
410
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
411
|
+
const sanitized = sanitizeAttributeValue(value);
|
|
412
|
+
if (sanitized !== undefined) {
|
|
413
|
+
map.set(key, sanitized);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return map;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function decodeAttributeList(
|
|
420
|
+
attributes: Attributes,
|
|
421
|
+
strings: readonly string[],
|
|
422
|
+
): AttributeMap {
|
|
423
|
+
const map = new Map<string, unknown>();
|
|
424
|
+
for (const kv of attributes) {
|
|
425
|
+
const key = strings[kv.key] ?? "";
|
|
426
|
+
try {
|
|
427
|
+
map.set(key, decodeCbor(toUint8Array(kv.value)) as unknown);
|
|
428
|
+
} catch {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return map;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function decodeLinks(
|
|
436
|
+
links: readonly SpanLink[],
|
|
437
|
+
strings: readonly string[],
|
|
438
|
+
): LinkState[] {
|
|
439
|
+
return links.map((link) => ({
|
|
440
|
+
traceId: link.traceId,
|
|
441
|
+
spanId: link.spanId,
|
|
442
|
+
traceState: link.traceState,
|
|
443
|
+
attributes: decodeAttributeList(link.attributes, strings),
|
|
444
|
+
droppedAttributesCount: link.droppedAttributesCount,
|
|
445
|
+
}));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function encodeLinkState(
|
|
449
|
+
links: LinkState[],
|
|
450
|
+
): { links: SpanLink[]; dropped: number } {
|
|
451
|
+
const result: SpanLink[] = [];
|
|
452
|
+
let dropped = 0;
|
|
453
|
+
for (const link of links) {
|
|
454
|
+
const { attributes, dropped: droppedAttributes } = encodeAttributeMap(
|
|
455
|
+
link.attributes,
|
|
456
|
+
);
|
|
457
|
+
result.push({
|
|
458
|
+
traceId: link.traceId,
|
|
459
|
+
spanId: link.spanId,
|
|
460
|
+
traceState: link.traceState,
|
|
461
|
+
attributes,
|
|
462
|
+
droppedAttributesCount: droppedAttributes,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return { links: result, dropped };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function appendRecord(
|
|
469
|
+
buildBody: () => RecordBody,
|
|
470
|
+
providedTimeUnixMs?: number,
|
|
471
|
+
): { recordIndex: number; encodedBytes: number; body: RecordBody } {
|
|
472
|
+
const absoluteUnixNs =
|
|
473
|
+
providedTimeUnixMs !== undefined
|
|
474
|
+
? BigInt(Math.floor(providedTimeUnixMs)) * 1_000_000n
|
|
475
|
+
: nowUnixNs(timeAnchor);
|
|
476
|
+
const recordBucketStart = computeBucketStartSec(
|
|
477
|
+
absoluteUnixNs,
|
|
478
|
+
bucketSizeSec,
|
|
479
|
+
);
|
|
480
|
+
if (recordBucketStart !== currentChunk.bucketStartSec) {
|
|
481
|
+
flushChunk();
|
|
482
|
+
resetChunkState(recordBucketStart);
|
|
483
|
+
}
|
|
484
|
+
if (performance.now() - currentChunk.createdAtMonoMs >= maxChunkAgeMs) {
|
|
485
|
+
flushChunk();
|
|
486
|
+
resetChunkState(recordBucketStart);
|
|
487
|
+
}
|
|
488
|
+
let body = buildBody();
|
|
489
|
+
const timeOffsetNs = absoluteUnixNs - currentChunk.baseUnixNs;
|
|
490
|
+
let record: TraceRecord = { timeOffsetNs, body };
|
|
491
|
+
let encodedRecord = encodeRecord(record);
|
|
492
|
+
if (encodedRecord.length > maxChunkBytes) {
|
|
493
|
+
throw new Error("Record exceeds maxChunkBytes");
|
|
494
|
+
}
|
|
495
|
+
if (currentChunk.sizeBytes + encodedRecord.length > targetChunkBytes) {
|
|
496
|
+
flushChunk();
|
|
497
|
+
resetChunkState(recordBucketStart);
|
|
498
|
+
body = buildBody();
|
|
499
|
+
record = { timeOffsetNs, body };
|
|
500
|
+
encodedRecord = encodeRecord(record);
|
|
501
|
+
if (encodedRecord.length > maxChunkBytes) {
|
|
502
|
+
throw new Error("Record exceeds maxChunkBytes");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
currentChunk.records.push(record);
|
|
506
|
+
currentChunk.sizeBytes += encodedRecord.length;
|
|
507
|
+
const recordIndex = currentChunk.records.length - 1;
|
|
508
|
+
return { recordIndex, encodedBytes: encodedRecord.length, body };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function flushChunk(): boolean {
|
|
512
|
+
if (currentChunk.records.length === 0) {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
const chunk: Chunk = {
|
|
516
|
+
baseUnixNs: currentChunk.baseUnixNs,
|
|
517
|
+
strings: currentChunk.strings,
|
|
518
|
+
records: currentChunk.records,
|
|
519
|
+
activeSpans: Array.from(activeSpanRefs.values()),
|
|
520
|
+
};
|
|
521
|
+
const bytes = CHUNK_VERSIONED.serializeWithEmbeddedVersion(
|
|
522
|
+
chunk,
|
|
523
|
+
CURRENT_VERSION,
|
|
524
|
+
);
|
|
525
|
+
const key = buildChunkKey(currentChunk.bucketStartSec, currentChunk.chunkId);
|
|
526
|
+
const maxRecordNs =
|
|
527
|
+
chunk.records.length > 0
|
|
528
|
+
? chunk.baseUnixNs +
|
|
529
|
+
chunk.records[chunk.records.length - 1].timeOffsetNs
|
|
530
|
+
: chunk.baseUnixNs;
|
|
531
|
+
const pending: PendingChunk = {
|
|
532
|
+
key,
|
|
533
|
+
bucketStartSec: currentChunk.bucketStartSec,
|
|
534
|
+
chunkId: currentChunk.chunkId,
|
|
535
|
+
chunk,
|
|
536
|
+
bytes,
|
|
537
|
+
maxRecordNs,
|
|
538
|
+
};
|
|
539
|
+
pendingChunks.push(pending);
|
|
540
|
+
enqueueWrite(pending);
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function enqueueWrite(pending: PendingChunk): void {
|
|
545
|
+
writeChain = writeChain.then(async () => {
|
|
546
|
+
await driver.set(pending.key, pending.bytes);
|
|
547
|
+
const index = pendingChunks.indexOf(pending);
|
|
548
|
+
if (index !== -1) {
|
|
549
|
+
pendingChunks.splice(index, 1);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function resetChunkState(bucketStartSec: number): void {
|
|
555
|
+
currentChunk.bucketStartSec = bucketStartSec;
|
|
556
|
+
currentChunk.chunkId = nextChunkId(bucketStartSec);
|
|
557
|
+
currentChunk.baseUnixNs = BigInt(bucketStartSec) * 1_000_000_000n;
|
|
558
|
+
currentChunk.strings = [];
|
|
559
|
+
currentChunk.stringIds = new Map();
|
|
560
|
+
currentChunk.records = [];
|
|
561
|
+
currentChunk.sizeBytes = 0;
|
|
562
|
+
currentChunk.createdAtMonoMs = performance.now();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function enforceMaxActiveSpans(): void {
|
|
566
|
+
if (activeSpans.size <= maxActiveSpans) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const candidates = Array.from(activeSpans.values()).sort((a, b) => {
|
|
570
|
+
if (a.depth !== b.depth) {
|
|
571
|
+
return b.depth - a.depth;
|
|
572
|
+
}
|
|
573
|
+
if (a.startTimeUnixNs > b.startTimeUnixNs) {
|
|
574
|
+
return -1;
|
|
575
|
+
}
|
|
576
|
+
if (a.startTimeUnixNs < b.startTimeUnixNs) {
|
|
577
|
+
return 1;
|
|
578
|
+
}
|
|
579
|
+
return 0;
|
|
580
|
+
});
|
|
581
|
+
for (const span of candidates) {
|
|
582
|
+
dropSpan(span.spanId);
|
|
583
|
+
if (activeSpans.size <= maxActiveSpans) {
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function dropSpan(spanId: SpanId | Uint8Array): void {
|
|
590
|
+
const key = spanKey(spanId);
|
|
591
|
+
activeSpans.delete(key);
|
|
592
|
+
activeSpanRefs.delete(key);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function assertActive(handle: SpanHandle): void {
|
|
596
|
+
if (!isActive(handle)) {
|
|
597
|
+
throw new Error("Span handle is not active");
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function isActive(handle: SpanHandle): boolean {
|
|
602
|
+
return activeSpans.has(spanKey(handle.spanId));
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function startSpan(name: string, options?: StartSpanOptions): SpanHandle {
|
|
606
|
+
const parent = options?.parent ?? getCurrentSpan();
|
|
607
|
+
if (parent) {
|
|
608
|
+
assertActive(parent);
|
|
609
|
+
}
|
|
610
|
+
const spanIdBytes = randomBytes(SPAN_ID_BYTES);
|
|
611
|
+
const traceIdBytes = parent ? parent.traceId : randomBytes(TRACE_ID_BYTES);
|
|
612
|
+
const spanId = toArrayBuffer(spanIdBytes);
|
|
613
|
+
const traceId = toArrayBuffer(traceIdBytes);
|
|
614
|
+
const parentSpanId = parent ? toArrayBuffer(parent.spanId) : null;
|
|
615
|
+
const { recordIndex, encodedBytes, body } = appendRecord(() => ({
|
|
616
|
+
tag: "SpanStart",
|
|
617
|
+
val: createSpanStartRecord(
|
|
618
|
+
spanId,
|
|
619
|
+
traceId,
|
|
620
|
+
name,
|
|
621
|
+
options,
|
|
622
|
+
parentSpanId,
|
|
623
|
+
),
|
|
624
|
+
}));
|
|
625
|
+
const spanStart = body.val as SpanStart;
|
|
626
|
+
const key = spanKey(spanId);
|
|
627
|
+
const startKey: SpanRecordKey = {
|
|
628
|
+
prefix: KEY_PREFIX.DATA,
|
|
629
|
+
bucketStartSec: BigInt(currentChunk.bucketStartSec),
|
|
630
|
+
chunkId: currentChunk.chunkId,
|
|
631
|
+
recordIndex,
|
|
632
|
+
};
|
|
633
|
+
activeSpanRefs.set(key, {
|
|
634
|
+
spanId,
|
|
635
|
+
startKey,
|
|
636
|
+
latestSnapshotKey: null,
|
|
637
|
+
});
|
|
638
|
+
const depth = computeSpanDepth(parentSpanId);
|
|
639
|
+
activeSpans.set(key, {
|
|
640
|
+
spanId,
|
|
641
|
+
traceId,
|
|
642
|
+
parentSpanId,
|
|
643
|
+
name,
|
|
644
|
+
kind: options?.kind ?? 0,
|
|
645
|
+
traceState: options?.traceState ?? null,
|
|
646
|
+
flags: options?.flags ?? 0,
|
|
647
|
+
attributes: buildAttributeMapFromInput(options?.attributes),
|
|
648
|
+
droppedAttributesCount: spanStart.droppedAttributesCount,
|
|
649
|
+
links: decodeLinks(spanStart.links, currentChunk.strings),
|
|
650
|
+
droppedLinksCount: spanStart.droppedLinksCount,
|
|
651
|
+
status: null,
|
|
652
|
+
startTimeUnixNs:
|
|
653
|
+
currentChunk.baseUnixNs + currentChunk.records[recordIndex].timeOffsetNs,
|
|
654
|
+
depth,
|
|
655
|
+
bytesSinceSnapshot: encodedBytes,
|
|
656
|
+
lastSnapshotMonoMs: performance.now(),
|
|
657
|
+
});
|
|
658
|
+
enforceMaxActiveSpans();
|
|
659
|
+
return {
|
|
660
|
+
spanId: spanIdBytes,
|
|
661
|
+
traceId: traceIdBytes,
|
|
662
|
+
isActive: () => activeSpans.has(key),
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function updateSpan(handle: SpanHandle, options: UpdateSpanOptions): void {
|
|
667
|
+
if (!options.attributes && !options.status) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
assertActive(handle);
|
|
671
|
+
const { encodedBytes, body } = appendRecord(() => ({
|
|
672
|
+
tag: "SpanUpdate",
|
|
673
|
+
val: createSpanUpdateRecord(toArrayBuffer(handle.spanId), options),
|
|
674
|
+
}));
|
|
675
|
+
const spanUpdate = body.val as SpanUpdate;
|
|
676
|
+
const state = activeSpans.get(spanKey(handle.spanId));
|
|
677
|
+
if (!state) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (options.attributes) {
|
|
681
|
+
const updates = buildAttributeMapFromInput(options.attributes);
|
|
682
|
+
for (const [key, value] of updates.entries()) {
|
|
683
|
+
state.attributes.set(key, value);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
state.droppedAttributesCount += spanUpdate.droppedAttributesCount;
|
|
687
|
+
if (options.status) {
|
|
688
|
+
state.status = toBareStatus(options.status);
|
|
689
|
+
}
|
|
690
|
+
state.bytesSinceSnapshot += encodedBytes;
|
|
691
|
+
maybeSnapshot(handle.spanId, state);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function setAttributes(
|
|
695
|
+
handle: SpanHandle,
|
|
696
|
+
attributes: Record<string, unknown>,
|
|
697
|
+
): void {
|
|
698
|
+
updateSpan(handle, { attributes });
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function setStatus(handle: SpanHandle, status: SpanStatusInput): void {
|
|
702
|
+
updateSpan(handle, { status });
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function emitEvent(
|
|
706
|
+
handle: SpanHandle,
|
|
707
|
+
name: string,
|
|
708
|
+
options?: EventOptions,
|
|
709
|
+
): void {
|
|
710
|
+
assertActive(handle);
|
|
711
|
+
const { encodedBytes } = appendRecord(
|
|
712
|
+
() => ({
|
|
713
|
+
tag: "SpanEvent",
|
|
714
|
+
val: createSpanEventRecord(toArrayBuffer(handle.spanId), name, options),
|
|
715
|
+
}),
|
|
716
|
+
options?.timeUnixMs,
|
|
717
|
+
);
|
|
718
|
+
const state = activeSpans.get(spanKey(handle.spanId));
|
|
719
|
+
if (state) {
|
|
720
|
+
state.bytesSinceSnapshot += encodedBytes;
|
|
721
|
+
maybeSnapshot(handle.spanId, state);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function endSpan(handle: SpanHandle, options?: EndSpanOptions): void {
|
|
726
|
+
assertActive(handle);
|
|
727
|
+
appendRecord(() => ({
|
|
728
|
+
tag: "SpanEnd",
|
|
729
|
+
val: createSpanEndRecord(toArrayBuffer(handle.spanId), options),
|
|
730
|
+
}));
|
|
731
|
+
dropSpan(handle.spanId);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function maybeSnapshot(spanId: SpanId | Uint8Array, state: SpanState): void {
|
|
735
|
+
if (
|
|
736
|
+
state.bytesSinceSnapshot < snapshotBytesThreshold &&
|
|
737
|
+
performance.now() - state.lastSnapshotMonoMs < snapshotIntervalMs
|
|
738
|
+
) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const { recordIndex } = appendRecord(() => ({
|
|
742
|
+
tag: "SpanSnapshot",
|
|
743
|
+
val: createSpanSnapshotRecord(state),
|
|
744
|
+
}));
|
|
745
|
+
const key = spanKey(spanId);
|
|
746
|
+
const ref = activeSpanRefs.get(key);
|
|
747
|
+
if (ref) {
|
|
748
|
+
activeSpanRefs.set(key, {
|
|
749
|
+
...ref,
|
|
750
|
+
latestSnapshotKey: {
|
|
751
|
+
prefix: KEY_PREFIX.DATA,
|
|
752
|
+
bucketStartSec: BigInt(currentChunk.bucketStartSec),
|
|
753
|
+
chunkId: currentChunk.chunkId,
|
|
754
|
+
recordIndex,
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
state.bytesSinceSnapshot = 0;
|
|
759
|
+
state.lastSnapshotMonoMs = performance.now();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function flush(): Promise<boolean> {
|
|
763
|
+
const didFlush = flushChunk();
|
|
764
|
+
if (didFlush) {
|
|
765
|
+
resetChunkState(currentChunk.bucketStartSec);
|
|
766
|
+
}
|
|
767
|
+
await writeChain;
|
|
768
|
+
return didFlush;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function withSpan<T>(handle: SpanHandle, fn: () => T): T {
|
|
772
|
+
return spanContext.run(handle, fn);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function getCurrentSpan(): SpanHandle | null {
|
|
776
|
+
const handle = spanContext.getStore() ?? null;
|
|
777
|
+
if (!handle) {
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
return isActive(handle) ? handle : null;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async function readRangeWire(
|
|
784
|
+
options: ReadRangeOptions,
|
|
785
|
+
): Promise<ReadRangeWire> {
|
|
786
|
+
const startMs = Math.floor(options.startMs);
|
|
787
|
+
const endMs = Math.floor(options.endMs);
|
|
788
|
+
if (options.limit <= 0 || endMs <= startMs) {
|
|
789
|
+
return {
|
|
790
|
+
startTimeMs: BigInt(startMs),
|
|
791
|
+
endTimeMs: BigInt(endMs),
|
|
792
|
+
limit: 0,
|
|
793
|
+
clamped: false,
|
|
794
|
+
baseChunks: [],
|
|
795
|
+
chunks: [],
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
const limitWasClamped = options.limit > maxReadLimit;
|
|
799
|
+
const limit = Math.min(options.limit, maxReadLimit);
|
|
800
|
+
const startNs = BigInt(startMs) * 1_000_000n;
|
|
801
|
+
const endNs = BigInt(endMs) * 1_000_000n;
|
|
802
|
+
|
|
803
|
+
const previousChunk = await findPreviousChunk(startNs, bucketSizeSec);
|
|
804
|
+
const activeRefs = previousChunk?.activeSpans ?? [];
|
|
805
|
+
const baseChunks: Chunk[] = [];
|
|
806
|
+
for (const ref of activeRefs) {
|
|
807
|
+
const baseRecord = await loadBaseRecord(ref);
|
|
808
|
+
if (!baseRecord) {
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
const baseUnixNs =
|
|
812
|
+
baseRecord.absNs - baseRecord.record.timeOffsetNs;
|
|
813
|
+
baseChunks.push({
|
|
814
|
+
baseUnixNs,
|
|
815
|
+
strings: baseRecord.strings,
|
|
816
|
+
records: [baseRecord.record],
|
|
817
|
+
activeSpans: [],
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const chunks: Chunk[] = [];
|
|
822
|
+
const diskChunks = await listRangeChunks(startNs, endNs, bucketSizeSec);
|
|
823
|
+
for (const chunk of diskChunks) {
|
|
824
|
+
const filtered = filterChunkRecords(chunk.chunk, startNs, endNs);
|
|
825
|
+
if (filtered) {
|
|
826
|
+
chunks.push(filtered);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
for (const pending of pendingChunks) {
|
|
830
|
+
const filtered = filterChunkRecords(pending.chunk, startNs, endNs);
|
|
831
|
+
if (filtered) {
|
|
832
|
+
chunks.push(filtered);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
const currentFiltered = filterChunkRecords(
|
|
836
|
+
currentChunkAsChunk(),
|
|
837
|
+
startNs,
|
|
838
|
+
endNs,
|
|
839
|
+
);
|
|
840
|
+
if (currentFiltered) {
|
|
841
|
+
chunks.push(currentFiltered);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const reachedSpanLimit = countUniqueSpanIds(chunks, limit);
|
|
845
|
+
return {
|
|
846
|
+
startTimeMs: BigInt(startMs),
|
|
847
|
+
endTimeMs: BigInt(endMs),
|
|
848
|
+
limit,
|
|
849
|
+
clamped: limitWasClamped || reachedSpanLimit,
|
|
850
|
+
baseChunks,
|
|
851
|
+
chunks,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
async function readRange(
|
|
856
|
+
options: ReadRangeOptions,
|
|
857
|
+
): Promise<ReadRangeResult<OtlpExportTraceServiceRequestJson>> {
|
|
858
|
+
const wire = await readRangeWire(options);
|
|
859
|
+
return readRangeWireToOtlp(wire, resource);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function filterChunkRecords(
|
|
863
|
+
chunk: Chunk,
|
|
864
|
+
startNs: bigint,
|
|
865
|
+
endNs: bigint,
|
|
866
|
+
): Chunk | null {
|
|
867
|
+
const filtered: TraceRecord[] = [];
|
|
868
|
+
for (const record of chunk.records) {
|
|
869
|
+
const absNs = chunk.baseUnixNs + record.timeOffsetNs;
|
|
870
|
+
if (absNs < startNs || absNs >= endNs) {
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
filtered.push(record);
|
|
874
|
+
}
|
|
875
|
+
if (filtered.length === 0) {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
baseUnixNs: chunk.baseUnixNs,
|
|
880
|
+
strings: chunk.strings,
|
|
881
|
+
records: filtered,
|
|
882
|
+
activeSpans: chunk.activeSpans,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function countUniqueSpanIds(chunks: Chunk[], limit: number): boolean {
|
|
887
|
+
if (limit <= 0) {
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
const seen = new Set<string>();
|
|
891
|
+
for (const chunk of chunks) {
|
|
892
|
+
for (const record of chunk.records) {
|
|
893
|
+
const key = spanKey(recordSpanId(record.body));
|
|
894
|
+
if (seen.has(key)) {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
if (seen.size >= limit) {
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
seen.add(key);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function recordSpanId(body: RecordBody): SpanId {
|
|
907
|
+
switch (body.tag) {
|
|
908
|
+
case "SpanStart":
|
|
909
|
+
return body.val.spanId;
|
|
910
|
+
case "SpanEvent":
|
|
911
|
+
return body.val.spanId;
|
|
912
|
+
case "SpanUpdate":
|
|
913
|
+
return body.val.spanId;
|
|
914
|
+
case "SpanEnd":
|
|
915
|
+
return body.val.spanId;
|
|
916
|
+
case "SpanSnapshot":
|
|
917
|
+
return body.val.spanId;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function currentChunkAsChunk(): Chunk {
|
|
922
|
+
return {
|
|
923
|
+
baseUnixNs: currentChunk.baseUnixNs,
|
|
924
|
+
strings: currentChunk.strings,
|
|
925
|
+
records: currentChunk.records,
|
|
926
|
+
activeSpans: Array.from(activeSpanRefs.values()),
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
async function listRangeChunks(
|
|
931
|
+
startNs: bigint,
|
|
932
|
+
endNs: bigint,
|
|
933
|
+
bucketSize: number,
|
|
934
|
+
): Promise<Array<{ key: Uint8Array; chunk: Chunk }>> {
|
|
935
|
+
const startBucket = computeBucketStartSec(startNs, bucketSize);
|
|
936
|
+
const endBucket = computeBucketStartSec(endNs, bucketSize);
|
|
937
|
+
const startKey = buildChunkKey(startBucket, 0);
|
|
938
|
+
const endKey = buildChunkKey(endBucket + bucketSize, 0);
|
|
939
|
+
const entries = await driver.listRange(startKey, endKey);
|
|
940
|
+
const output: Array<{ key: Uint8Array; chunk: Chunk }> = [];
|
|
941
|
+
for (const entry of entries) {
|
|
942
|
+
const chunk = deserializeChunkSafe(entry.value);
|
|
943
|
+
if (!chunk) {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
output.push({ key: entry.key, chunk });
|
|
947
|
+
}
|
|
948
|
+
return output;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
async function findPreviousChunk(
|
|
952
|
+
startNs: bigint,
|
|
953
|
+
bucketSize: number,
|
|
954
|
+
): Promise<Chunk | null> {
|
|
955
|
+
const startBucket = computeBucketStartSec(startNs, bucketSize);
|
|
956
|
+
let cursor = {
|
|
957
|
+
bucketStartSec: startBucket,
|
|
958
|
+
chunkId: AFTER_MAX_CHUNK_ID,
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
while (true) {
|
|
962
|
+
const pendingCandidate = findLatestPendingBefore(cursor);
|
|
963
|
+
const diskCandidate = await findLatestDiskBefore(cursor);
|
|
964
|
+
const candidate = selectLatestCandidate(
|
|
965
|
+
pendingCandidate,
|
|
966
|
+
diskCandidate,
|
|
967
|
+
);
|
|
968
|
+
if (!candidate) {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
if (candidate.maxRecordNs < startNs) {
|
|
972
|
+
return candidate.chunk;
|
|
973
|
+
}
|
|
974
|
+
cursor = {
|
|
975
|
+
bucketStartSec: candidate.bucketStartSec,
|
|
976
|
+
chunkId: candidate.chunkId,
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function findLatestPendingBefore(cursor: {
|
|
982
|
+
bucketStartSec: number;
|
|
983
|
+
chunkId: number;
|
|
984
|
+
}): PendingChunk | null {
|
|
985
|
+
let best: PendingChunk | null = null;
|
|
986
|
+
for (const pending of pendingChunks) {
|
|
987
|
+
if (compareChunkKey(pending, cursor) >= 0) {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (!best || compareChunkKey(pending, best) > 0) {
|
|
991
|
+
best = pending;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return best;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
async function findLatestDiskBefore(cursor: {
|
|
998
|
+
bucketStartSec: number;
|
|
999
|
+
chunkId: number;
|
|
1000
|
+
}): Promise<PendingChunk | null> {
|
|
1001
|
+
const startKey = buildChunkKey(0, 0);
|
|
1002
|
+
let endKey = buildChunkKey(cursor.bucketStartSec, cursor.chunkId);
|
|
1003
|
+
|
|
1004
|
+
while (true) {
|
|
1005
|
+
const entries = await driver.listRange(startKey, endKey, {
|
|
1006
|
+
reverse: true,
|
|
1007
|
+
limit: 10,
|
|
1008
|
+
});
|
|
1009
|
+
if (entries.length === 0) {
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
for (const entry of entries) {
|
|
1013
|
+
const chunk = deserializeChunkSafe(entry.value);
|
|
1014
|
+
if (!chunk) {
|
|
1015
|
+
endKey = entry.key;
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
const { bucketStartSec, chunkId } = decodeChunkKey(entry.key);
|
|
1019
|
+
const maxRecordNs =
|
|
1020
|
+
chunk.records.length > 0
|
|
1021
|
+
? chunk.baseUnixNs +
|
|
1022
|
+
chunk.records[chunk.records.length - 1].timeOffsetNs
|
|
1023
|
+
: chunk.baseUnixNs;
|
|
1024
|
+
return {
|
|
1025
|
+
key: entry.key,
|
|
1026
|
+
bucketStartSec,
|
|
1027
|
+
chunkId,
|
|
1028
|
+
chunk,
|
|
1029
|
+
bytes: entry.value,
|
|
1030
|
+
maxRecordNs,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function selectLatestCandidate(
|
|
1037
|
+
pending: PendingChunk | null,
|
|
1038
|
+
disk: PendingChunk | null,
|
|
1039
|
+
): PendingChunk | null {
|
|
1040
|
+
if (pending && disk) {
|
|
1041
|
+
return compareChunkKey(pending, disk) >= 0 ? pending : disk;
|
|
1042
|
+
}
|
|
1043
|
+
return pending ?? disk;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function compareChunkKey(
|
|
1047
|
+
a: { bucketStartSec: number; chunkId: number },
|
|
1048
|
+
b: { bucketStartSec: number; chunkId: number },
|
|
1049
|
+
): number {
|
|
1050
|
+
if (a.bucketStartSec !== b.bucketStartSec) {
|
|
1051
|
+
return a.bucketStartSec - b.bucketStartSec;
|
|
1052
|
+
}
|
|
1053
|
+
return a.chunkId - b.chunkId;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function decodeChunkKey(key: Uint8Array): {
|
|
1057
|
+
bucketStartSec: number;
|
|
1058
|
+
chunkId: number;
|
|
1059
|
+
} {
|
|
1060
|
+
const tuple = unpack(Buffer.from(key)) as [number, number, number];
|
|
1061
|
+
return { bucketStartSec: tuple[1], chunkId: tuple[2] };
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function buildChunkKey(bucketStartSec: number, chunkId: number): Uint8Array {
|
|
1065
|
+
return pack([KEY_PREFIX.DATA, bucketStartSec, chunkId]);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function deserializeChunkSafe(bytes: Uint8Array): Chunk | null {
|
|
1069
|
+
try {
|
|
1070
|
+
return CHUNK_VERSIONED.deserializeWithEmbeddedVersion(bytes);
|
|
1071
|
+
} catch {
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
async function loadBaseRecord(
|
|
1077
|
+
ref: ActiveSpanRef,
|
|
1078
|
+
): Promise<
|
|
1079
|
+
{ record: TraceRecord; strings: readonly string[]; absNs: bigint } | null
|
|
1080
|
+
> {
|
|
1081
|
+
const key = ref.latestSnapshotKey ?? ref.startKey;
|
|
1082
|
+
const bucketStartSec = toNumber(key.bucketStartSec);
|
|
1083
|
+
const fromMemory = findChunkInMemory(bucketStartSec, key.chunkId);
|
|
1084
|
+
if (fromMemory) {
|
|
1085
|
+
const record = fromMemory.records[key.recordIndex];
|
|
1086
|
+
if (!record) {
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
const absNs = fromMemory.baseUnixNs + record.timeOffsetNs;
|
|
1090
|
+
return { record, strings: fromMemory.strings, absNs };
|
|
1091
|
+
}
|
|
1092
|
+
const chunkKey = buildChunkKey(bucketStartSec, key.chunkId);
|
|
1093
|
+
const bytes = await driver.get(chunkKey);
|
|
1094
|
+
if (!bytes) {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
const chunk = deserializeChunkSafe(bytes);
|
|
1098
|
+
if (!chunk) {
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
const record = chunk.records[key.recordIndex];
|
|
1102
|
+
if (!record) {
|
|
1103
|
+
return null;
|
|
1104
|
+
}
|
|
1105
|
+
const absNs = chunk.baseUnixNs + record.timeOffsetNs;
|
|
1106
|
+
return { record, strings: chunk.strings, absNs };
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function findChunkInMemory(
|
|
1110
|
+
bucketStartSec: number,
|
|
1111
|
+
chunkId: number,
|
|
1112
|
+
): Chunk | null {
|
|
1113
|
+
if (
|
|
1114
|
+
currentChunk.bucketStartSec === bucketStartSec &&
|
|
1115
|
+
currentChunk.chunkId === chunkId
|
|
1116
|
+
) {
|
|
1117
|
+
return currentChunkAsChunk();
|
|
1118
|
+
}
|
|
1119
|
+
const pending = pendingChunks.find(
|
|
1120
|
+
(candidate) =>
|
|
1121
|
+
candidate.bucketStartSec === bucketStartSec &&
|
|
1122
|
+
candidate.chunkId === chunkId,
|
|
1123
|
+
);
|
|
1124
|
+
return pending?.chunk ?? null;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function toNumber(value: bigint): number {
|
|
1128
|
+
const asNumber = Number(value);
|
|
1129
|
+
if (!Number.isSafeInteger(asNumber)) {
|
|
1130
|
+
throw new Error("Value exceeds safe integer range");
|
|
1131
|
+
}
|
|
1132
|
+
return asNumber;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function computeSpanDepth(parentSpanId: SpanId | null): number {
|
|
1136
|
+
if (!parentSpanId) {
|
|
1137
|
+
return 0;
|
|
1138
|
+
}
|
|
1139
|
+
const parent = activeSpans.get(spanKey(parentSpanId));
|
|
1140
|
+
if (!parent) {
|
|
1141
|
+
return 0;
|
|
1142
|
+
}
|
|
1143
|
+
return parent.depth + 1;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function randomSpanId(): SpanId {
|
|
1147
|
+
return toArrayBuffer(randomBytes(SPAN_ID_BYTES));
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function randomTraceId(): TraceId {
|
|
1151
|
+
return toArrayBuffer(randomBytes(TRACE_ID_BYTES));
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function toBareStatus(status: SpanStatusInput): SpanStatus {
|
|
1155
|
+
return {
|
|
1156
|
+
code: toBareStatusCode(status.code),
|
|
1157
|
+
message: status.message ?? null,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function toBareStatusCode(code: SpanStatusInput["code"]): SpanStatusCode {
|
|
1162
|
+
switch (code) {
|
|
1163
|
+
case "OK":
|
|
1164
|
+
return SpanStatusCode.OK;
|
|
1165
|
+
case "ERROR":
|
|
1166
|
+
return SpanStatusCode.ERROR;
|
|
1167
|
+
case "UNSET":
|
|
1168
|
+
default:
|
|
1169
|
+
return SpanStatusCode.UNSET;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
return {
|
|
1174
|
+
startSpan,
|
|
1175
|
+
updateSpan,
|
|
1176
|
+
setAttributes,
|
|
1177
|
+
setStatus,
|
|
1178
|
+
endSpan,
|
|
1179
|
+
emitEvent,
|
|
1180
|
+
withSpan,
|
|
1181
|
+
getCurrentSpan,
|
|
1182
|
+
flush,
|
|
1183
|
+
readRange,
|
|
1184
|
+
readRangeWire,
|
|
1185
|
+
};
|
|
1186
|
+
}
|