pi-mono/packages/chord/test/examples/61-tutorial/trace.ts
Mario Zechner ff33ef42c4 feat(chord): telemetry from pages and workers stored by the server, who a span was for
The four telemetry gaps (rows on commits, chord.host, microsecond times, a read-only store) and: pages record and
send span batches over their link to the server's ingest object (checked, capped, rate-limited, stamped browser and
with the link's principal); workers send theirs too, so the server is the store's only writer; principalAttributes
lets an app choose what a span records about its principal. The tutorial shows a real page's click traced to the
worker, with the trace viewer.
2026-11-04 11:59:14 +00:00

48 lines
2.7 KiB
TypeScript

// The todo app's trace reader: a script reading the telemetry store of a data directory (`telemetry.sqlite`) through
// the store's read interface, while the app runs or after. Without a trace id it prints the newest traces (those whose
// first span is `--name`, and with a span for `--principal`, when given) and the bytes on the links; with one, that
// trace as the renderer prints it.
// Run from packages/chord:
// node --conditions=source --experimental-strip-types test/examples/61-tutorial/trace.ts <data directory> [--name <span>] [--principal <key>]
// node --conditions=source --experimental-strip-types test/examples/61-tutorial/trace.ts <data directory> <trace id>
import { parseArgs } from "node:util";
import { renderTrace } from "@earendil-works/pi-telemetry/render";
import { openTelemetryStore } from "../../../src/telemetry-sqlite/node.ts";
import { telemetryFile } from "./src/telemetry.ts";
const { values, positionals } = parseArgs({
allowPositionals: true,
options: { name: { type: "string" }, principal: { type: "string" } },
});
const [dataDir, id] = positionals;
if (dataDir === undefined) {
throw new Error("usage: trace.ts <data directory> [--name <span>] [--principal <key>] [<trace id>]");
}
// The shared file, read-only: the app's processes may be writing it meanwhile.
const store = await openTelemetryStore(telemetryFile(dataDir));
try {
if (id !== undefined) {
const trace = await store.getTrace(id);
console.log(trace === undefined ? `no trace ${id}` : renderTrace(trace.spans));
} else {
// Complete traces, newest first; a trace's first span (its root, by start time) names it, and the app's
// `principalAttributes` hook named whom each span was for.
const attributes = values.principal === undefined ? undefined : { "chord.principal": values.principal };
const { traces } = await store.findTraces({ name: values.name, attributes, limit: 10 });
for (const { traceId, spans } of traces) {
const start = Math.min(...spans.map((span) => span.startedMs));
const length = Math.max(...spans.map((span) => span.endedMs)) - start;
const failed = spans.some((span) => span.status.status === "error") ? " error" : "";
console.log(`${traceId} ${spans[0]!.name} ${spans.length} spans ${length.toFixed(2)} ms${failed}`);
}
// Metrics are stored per series and 10 s: a series is one set of attributes, here the direction.
for (const direction of ["sent", "received"]) {
const rows = await store.findMetrics({ name: "chord.link.bytes", attributes: { direction } });
const bytes = rows.reduce((sum, row) => sum + row.sum, 0);
const messages = rows.reduce((sum, row) => sum + row.count, 0);
console.log(`chord.link.bytes ${direction}: ${bytes} bytes in ${messages} messages`);
}
}
} finally {
await store.close();
}