Skip to content
104 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import test from "node:test";
3
4import { decodeLogs, decodeMetrics, decodeTraces, ProtoError } from "./proto.ts";
5
6// A small protobuf writer, enough to build the messages SDKs send.
7const varint = (n: bigint | number): number[] => {
8 let v = BigInt(n);
9 if (v < 0n) v = BigInt.asUintN(64, v);
10 const out: number[] = [];
11 do {
12 let byte = Number(v & 0x7fn);
13 v >>= 7n;
14 if (v > 0n) byte |= 0x80;
15 out.push(byte);
16 } while (v > 0n);
17 return out;
18};
19const key = (no: number, wire: number) => varint((no << 3) | wire);
20const bytes = (no: number, b: number[] | Uint8Array) => [...key(no, 2), ...varint(b.length), ...b];
21const str = (no: number, s: string) => bytes(no, [...new TextEncoder().encode(s)]);
22const msg = (no: number, ...parts: number[][]) => bytes(no, parts.flat());
23const vint = (no: number, n: number | bigint) => [...key(no, 0), ...varint(n)];
24const fixed64 = (no: number, n: bigint) => {
25 const b = new Uint8Array(8);
26 new DataView(b.buffer).setBigUint64(0, n, true);
27 return [...key(no, 1), ...b];
28};
29const double = (no: number, n: number) => {
30 const b = new Uint8Array(8);
31 new DataView(b.buffer).setFloat64(0, n, true);
32 return [...key(no, 1), ...b];
33};
34const hex = (h: string) => h.match(/../g)!.map((x) => parseInt(x, 16));
35const kv = (no: number, k: string, value: number[]) => msg(no, str(1, k), msg(2, value));
36
37const TRACE = "4bf92f3577b34da6a3ce929d0e0e4736";
38const SPAN = "00f067aa0ba902b7";
39
40test("a trace export in protobuf reads as OTLP/JSON does", () => {
41 const span = [
42 ...bytes(1, hex(TRACE)),
43 ...bytes(2, hex(SPAN)),
44 ...str(5, "GET /users/:id"),
45 ...vint(6, 2),
46 ...fixed64(7, 1_760_000_000_000_000_000n),
47 ...fixed64(8, 1_760_000_000_250_000_000n),
48 ...kv(9, "http.response.status_code", vint(3, 200)),
49 ...kv(9, "http.route", str(1, "/users/:id")),
50 ...kv(9, "ratio", double(4, 0.5)),
51 ...kv(9, "ok", vint(2, 1)),
52 ...kv(9, "neg", vint(3, -5n)),
53 ...msg(11, fixed64(1, 1_760_000_000_100_000_000n), str(2, "exception"), kv(3, "exception.type", str(1, "TypeError"))),
54 ...msg(15, str(2, "boom"), vint(3, 2)),
55 // A field this reader does not know is skipped.
56 ...vint(99, 7),
57 ];
58 const request = msg(1, msg(1, kv(1, "service.name", str(1, "api"))), msg(2, msg(1, str(1, "scope")), msg(2, span)));
59 const out = decodeTraces(Uint8Array.from(request));
60 const s = out.resourceSpans![0].scopeSpans![0].spans![0];
61 assert.equal(out.resourceSpans![0].resource!.attributes![0].key, "service.name");
62 assert.equal(s.traceId, TRACE);
63 assert.equal(s.spanId, SPAN);
64 assert.equal(s.name, "GET /users/:id");
65 assert.equal(s.kind, 2);
66 assert.equal(s.startTimeUnixNano, "1760000000000000000");
67 assert.equal(s.endTimeUnixNano, "1760000000250000000");
68 assert.deepEqual(s.attributes![0], { key: "http.response.status_code", value: { intValue: "200" } });
69 assert.deepEqual(s.attributes![2].value, { doubleValue: 0.5 });
70 assert.deepEqual(s.attributes![3].value, { boolValue: true });
71 assert.deepEqual(s.attributes![4].value, { intValue: "-5" });
72 assert.equal(s.events![0].name, "exception");
73 assert.deepEqual(s.status, { message: "boom", code: 2 });
74});
75
76test("logs and metrics in protobuf", () => {
77 const record = [...fixed64(1, 1_760_000_000_000_000_000n), ...vint(2, 17), ...str(3, "ERROR"), ...msg(5, str(1, "it broke")), ...bytes(9, hex(TRACE))];
78 const logs = decodeLogs(Uint8Array.from(msg(1, msg(2, msg(2, record)))));
79 const r = logs.resourceLogs![0].scopeLogs![0].logRecords![0];
80 assert.equal(r.severityNumber, 17);
81 assert.deepEqual(r.body, { stringValue: "it broke" });
82 assert.equal(r.traceId, TRACE);
83
84 // A histogram with packed buckets and bounds.
85 const counts = new Uint8Array(24);
86 [1n, 2n, 3n].forEach((n, i) => new DataView(counts.buffer).setBigUint64(i * 8, n, true));
87 const bounds = new Uint8Array(16);
88 [10, 100].forEach((n, i) => new DataView(bounds.buffer).setFloat64(i * 8, n, true));
89 const point = [...fixed64(3, 1_760_000_000_000_000_000n), ...fixed64(4, 6n), ...double(5, 300), ...bytes(6, counts), ...bytes(7, bounds)];
90 const gauge = msg(5, msg(1, fixed64(3, 1_760_000_000_000_000_000n), double(4, 42)));
91 const metrics = decodeMetrics(
92 Uint8Array.from(msg(1, msg(2, msg(2, str(1, "http.server.duration"), str(3, "ms"), msg(9, msg(1, point), vint(2, 2))), msg(2, str(1, "queue.depth"), gauge)))),
93 );
94 const [h, g] = metrics.resourceMetrics![0].scopeMetrics![0].metrics!;
95 assert.equal(h.name, "http.server.duration");
96 assert.deepEqual(h.histogram!.dataPoints![0].bucketCounts, ["1", "2", "3"]);
97 assert.deepEqual(h.histogram!.dataPoints![0].explicitBounds, [10, 100]);
98 assert.equal(h.histogram!.aggregationTemporality, 2);
99 assert.equal(g.gauge!.dataPoints![0].asDouble, 42);
100});
101
102test("a message cut short is refused, not read as garbage", () => {
103 assert.throws(() => decodeTraces(Uint8Array.from([0x0a, 0x10, 0x01])), ProtoError);
104});