@dudousxd/nestjs-telescope
Version:
Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.
109 lines • 5.12 kB
JavaScript
// packages/core/src/metrics/waterfall.ts
import { EntryType } from '../entry/entry.js';
function asRecord(content) {
return typeof content === 'object' && content !== null
? content
: null;
}
/** A human label for a span, derived from the entry's content by type. */
function labelFor(entry) {
const record = asRecord(entry.content);
if (record === null)
return entry.type;
if (typeof record.uri === 'string') {
return typeof record.method === 'string' ? `${record.method} ${record.uri}` : record.uri;
}
if (typeof record.sql === 'string')
return record.sql;
if (typeof record.queue === 'string' && typeof record.name === 'string') {
return `${record.queue}:${record.name}`;
}
if (entry.type === EntryType.HttpClient && typeof record.url === 'string') {
return typeof record.method === 'string' ? `${record.method} ${record.url}` : record.url;
}
// Diagnostics span entries (`@dudousxd/nestjs-diagnostics-telescope`) carry the
// emitting lib + event name, e.g. `agent` + `llm.turn` → "agent:llm.turn".
if (typeof record.lib === 'string' && typeof record.event === 'string') {
return `${record.lib}:${record.event}`;
}
if (typeof record.name === 'string')
return record.name;
return entry.type;
}
/**
* Reconstruct a nested span waterfall from a batch/trace's entries.
*
* DESIGN: the entry model carries `traceId`/`spanId` but NOT a parent-span
* pointer, so we cannot rebuild the exact OTel span tree from explicit links.
* Instead — exactly as Sentry/Tempo do when parent links are missing — we infer
* nesting from **time-interval containment**: a span is a child of the tightest
* enclosing span whose `[start, end]` strictly contains it. `sequence` provides a
* stable tie-break for spans that start at the same instant. This yields the
* familiar request → query/http_client → nested-op nesting from data we already
* capture, with zero new watcher overhead.
*
* Returns `null` for an empty input. Pure.
*/
export function buildWaterfall(entries) {
if (entries.length === 0)
return null;
const intervals = entries.map((entry) => {
const startMs = entry.createdAt.getTime();
const durationMs = typeof entry.durationMs === 'number' ? Math.max(0, entry.durationMs) : 0;
return { entry, startMs, durationMs, endMs: startMs + durationMs };
});
const traceStartMs = Math.min(...intervals.map((i) => i.startMs));
const traceEndMs = Math.max(...intervals.map((i) => i.endMs));
const totalDurationMs = Math.max(0, traceEndMs - traceStartMs);
// Order so that enclosing spans come first: earliest start, then the LONGER
// span first (a parent starts no later and ends no earlier than its child),
// then sequence for a stable tie-break.
const ordered = [...intervals].sort((a, b) => a.startMs - b.startMs || b.durationMs - a.durationMs || a.entry.sequence - b.entry.sequence);
const nodeById = new Map();
const roots = [];
// A stack of currently-open ancestors, innermost last. We pop ancestors whose
// interval does not contain the current span, then attach to the top of stack.
const openStack = [];
for (const interval of ordered) {
while (openStack.length > 0) {
const top = openStack[openStack.length - 1];
if (top !== undefined && contains(top.interval, interval))
break;
openStack.pop();
}
const parent = openStack[openStack.length - 1];
const depth = parent === undefined ? 0 : parent.node.depth + 1;
const node = {
id: interval.entry.id,
type: interval.entry.type,
label: labelFor(interval.entry),
offsetMs: interval.startMs - traceStartMs,
durationMs: interval.durationMs,
depth,
sequence: interval.entry.sequence,
children: [],
};
nodeById.set(node.id, node);
if (parent === undefined)
roots.push(node);
else
parent.node.children.push(node);
openStack.push({ interval, node });
}
// Order children (and roots) by start offset, then sequence.
const byOffset = (a, b) => a.offsetMs - b.offsetMs || a.sequence - b.sequence;
for (const node of nodeById.values())
node.children.sort(byOffset);
roots.sort(byOffset);
return { traceStartMs, totalDurationMs, spans: roots };
}
/** True when `outer` strictly encloses `inner` — it contains the interval AND is
* genuinely larger on at least one side. Two spans with the SAME interval are
* therefore siblings, not nested (a span never "contains" its identical twin),
* while a zero-width child inside a parent's bounds still nests. */
function contains(outer, inner) {
const encloses = outer.startMs <= inner.startMs && outer.endMs >= inner.endMs;
const strictlyLarger = outer.startMs < inner.startMs || outer.endMs > inner.endMs;
return encloses && strictlyLarger;
}
//# sourceMappingURL=waterfall.js.map