autotel-terminal 17.0.4 → 17.0.5
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/dist/cli.cjs +240 -13
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +240 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +213 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +213 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +83 -10
- package/src/lib/dashboard-keymap.test.ts +18 -0
- package/src/lib/dashboard-keymap.ts +14 -1
- package/src/lib/topology-render.test.ts +319 -0
- package/src/lib/topology-render.ts +96 -0
- package/src/otlp-http-json.test.ts +131 -0
- package/src/otlp-http-json.ts +52 -7
- package/src/span-stream.ts +57 -1
package/dist/cli.cjs
CHANGED
|
@@ -111,6 +111,25 @@ function createTerminalSpanStream(processor) {
|
|
|
111
111
|
const startTime = timeToMs(span.startTime);
|
|
112
112
|
const endTime = timeToMs(span.endTime);
|
|
113
113
|
const durationMs = endTime - startTime;
|
|
114
|
+
const resourceAttrs = span.resource?.attributes ?? {};
|
|
115
|
+
const spanAttrs = span.attributes;
|
|
116
|
+
const mergedAttrs = {};
|
|
117
|
+
for (const [k, v] of Object.entries(resourceAttrs)) {
|
|
118
|
+
mergedAttrs[k] = v;
|
|
119
|
+
}
|
|
120
|
+
for (const [k, v] of Object.entries(spanAttrs)) {
|
|
121
|
+
mergedAttrs[k] = v;
|
|
122
|
+
}
|
|
123
|
+
const spanEvents = span.events?.length ? span.events.map((e) => ({
|
|
124
|
+
name: e.name,
|
|
125
|
+
timeMs: timeToMs(e.time),
|
|
126
|
+
attributes: e.attributes
|
|
127
|
+
})) : void 0;
|
|
128
|
+
const spanLinks = span.links?.length ? span.links.map((l) => ({
|
|
129
|
+
traceId: l.context.traceId,
|
|
130
|
+
spanId: l.context.spanId,
|
|
131
|
+
attributes: l.attributes
|
|
132
|
+
})) : void 0;
|
|
114
133
|
const event = {
|
|
115
134
|
name: span.name,
|
|
116
135
|
spanId: spanContext.spanId,
|
|
@@ -121,7 +140,9 @@ function createTerminalSpanStream(processor) {
|
|
|
121
140
|
durationMs,
|
|
122
141
|
status: mapStatus(span.status.code),
|
|
123
142
|
kind: mapKind(span.kind),
|
|
124
|
-
attributes:
|
|
143
|
+
attributes: mergedAttrs,
|
|
144
|
+
...spanEvents ? { events: spanEvents } : {},
|
|
145
|
+
...spanLinks ? { links: spanLinks } : {}
|
|
125
146
|
};
|
|
126
147
|
callback(event);
|
|
127
148
|
});
|
|
@@ -561,6 +582,123 @@ function buildErrorSummaries(traceSummaries) {
|
|
|
561
582
|
return out;
|
|
562
583
|
}
|
|
563
584
|
|
|
585
|
+
// src/lib/topology-model.ts
|
|
586
|
+
function getServiceName2(span) {
|
|
587
|
+
const attrs = span.attributes ?? {};
|
|
588
|
+
const serviceName = attrs["service.name"] ?? attrs["resource.service.name"];
|
|
589
|
+
return serviceName || "unknown";
|
|
590
|
+
}
|
|
591
|
+
function getPeerService(span) {
|
|
592
|
+
const attrs = span.attributes ?? {};
|
|
593
|
+
const peerService = attrs["peer.service"] ?? attrs["db.system"] ?? attrs["messaging.system"] ?? attrs["http.host"];
|
|
594
|
+
return peerService ?? null;
|
|
595
|
+
}
|
|
596
|
+
function buildServiceGraph(spans) {
|
|
597
|
+
const byService = /* @__PURE__ */ new Map();
|
|
598
|
+
for (const span of spans) {
|
|
599
|
+
const svc = getServiceName2(span);
|
|
600
|
+
const entry = byService.get(svc) ?? {
|
|
601
|
+
durations: [],
|
|
602
|
+
spanCount: 0,
|
|
603
|
+
errorCount: 0
|
|
604
|
+
};
|
|
605
|
+
entry.spanCount += 1;
|
|
606
|
+
entry.durations.push(span.durationMs);
|
|
607
|
+
if (span.status === "ERROR") entry.errorCount += 1;
|
|
608
|
+
byService.set(svc, entry);
|
|
609
|
+
}
|
|
610
|
+
const services = [];
|
|
611
|
+
for (const [serviceName, { durations, spanCount, errorCount }] of byService) {
|
|
612
|
+
if (spanCount === 0) continue;
|
|
613
|
+
const avgDurationMs = durations.reduce((acc, d) => acc + d, 0) / durations.length;
|
|
614
|
+
const sorted = durations.toSorted((a, b) => a - b);
|
|
615
|
+
const p95Index = Math.floor(sorted.length * 0.95);
|
|
616
|
+
const p95DurationMs = sorted[p95Index] ?? sorted.at(-1) ?? 0;
|
|
617
|
+
services.push({
|
|
618
|
+
serviceName,
|
|
619
|
+
spanCount,
|
|
620
|
+
errorCount,
|
|
621
|
+
avgDurationMs,
|
|
622
|
+
p95DurationMs
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
const byEdge = /* @__PURE__ */ new Map();
|
|
626
|
+
for (const span of spans) {
|
|
627
|
+
const to = getPeerService(span);
|
|
628
|
+
if (!to) continue;
|
|
629
|
+
const from = getServiceName2(span);
|
|
630
|
+
const key = `${from}\u2192${to}`;
|
|
631
|
+
const entry = byEdge.get(key) ?? { spanCount: 0, errorCount: 0 };
|
|
632
|
+
entry.spanCount += 1;
|
|
633
|
+
if (span.status === "ERROR") entry.errorCount += 1;
|
|
634
|
+
byEdge.set(key, entry);
|
|
635
|
+
}
|
|
636
|
+
const edges = [];
|
|
637
|
+
for (const [key, { spanCount, errorCount }] of byEdge) {
|
|
638
|
+
const [fromService, toService] = key.split("\u2192");
|
|
639
|
+
edges.push({ fromService, toService, spanCount, errorCount });
|
|
640
|
+
}
|
|
641
|
+
services.sort((a, b) => b.spanCount - a.spanCount);
|
|
642
|
+
edges.sort((a, b) => b.spanCount - a.spanCount);
|
|
643
|
+
return { services, edges };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/lib/topology-render.ts
|
|
647
|
+
function renderTopologyAscii(graph) {
|
|
648
|
+
if (graph.services.length === 0) {
|
|
649
|
+
return [" No services detected yet."];
|
|
650
|
+
}
|
|
651
|
+
const targetServices = new Set(graph.edges.map((e) => e.toService));
|
|
652
|
+
const roots = graph.services.filter(
|
|
653
|
+
(s) => !targetServices.has(s.serviceName)
|
|
654
|
+
);
|
|
655
|
+
const rootList = roots.length > 0 ? roots : [...graph.services];
|
|
656
|
+
const lines = [];
|
|
657
|
+
for (const root of rootList) {
|
|
658
|
+
renderNode(graph, root.serviceName, "", true, lines, /* @__PURE__ */ new Set());
|
|
659
|
+
}
|
|
660
|
+
return lines;
|
|
661
|
+
}
|
|
662
|
+
function renderNode(graph, serviceName, prefix, isRoot, lines, ancestors) {
|
|
663
|
+
const svc = graph.services.find((s) => s.serviceName === serviceName);
|
|
664
|
+
const label = svc ? formatServiceLine(svc) : `[${serviceName}]`;
|
|
665
|
+
if (isRoot) {
|
|
666
|
+
lines.push(label);
|
|
667
|
+
}
|
|
668
|
+
if (ancestors.has(serviceName)) return;
|
|
669
|
+
const pathAncestors = new Set(ancestors);
|
|
670
|
+
pathAncestors.add(serviceName);
|
|
671
|
+
const outgoing = graph.edges.filter((e) => e.fromService === serviceName);
|
|
672
|
+
for (let i = 0; i < outgoing.length; i++) {
|
|
673
|
+
const edge = outgoing[i];
|
|
674
|
+
const isLast = i === outgoing.length - 1;
|
|
675
|
+
const connector = isLast ? "\u2514" : "\u251C";
|
|
676
|
+
const childPrefix = isLast ? " " : "\u2502 ";
|
|
677
|
+
const downstream = graph.services.find(
|
|
678
|
+
(s) => s.serviceName === edge.toService
|
|
679
|
+
);
|
|
680
|
+
const edgeLabel = formatEdgeLabel(edge);
|
|
681
|
+
const downstreamLabel = downstream ? formatServiceLine(downstream) : `[${edge.toService}]`;
|
|
682
|
+
lines.push(`${prefix} ${connector}\u2500\u2500${edgeLabel}\u2500\u2500\u2192 ${downstreamLabel}`);
|
|
683
|
+
renderNode(
|
|
684
|
+
graph,
|
|
685
|
+
edge.toService,
|
|
686
|
+
prefix + " " + childPrefix,
|
|
687
|
+
false,
|
|
688
|
+
lines,
|
|
689
|
+
pathAncestors
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function formatServiceLine(svc) {
|
|
694
|
+
const errPart = svc.errorCount > 0 ? ` \xB7 ${svc.errorCount} err` : "";
|
|
695
|
+
return `[${svc.serviceName}] ${svc.spanCount} spans${errPart} \xB7 p95 ${formatDurationMs(svc.p95DurationMs)}`;
|
|
696
|
+
}
|
|
697
|
+
function formatEdgeLabel(edge) {
|
|
698
|
+
const errPart = edge.errorCount > 0 ? `, ${edge.errorCount} err` : "";
|
|
699
|
+
return `(${edge.spanCount}${errPart})`;
|
|
700
|
+
}
|
|
701
|
+
|
|
564
702
|
// src/lib/export-model.ts
|
|
565
703
|
function exportTraceToJson(trace, logs) {
|
|
566
704
|
const exported = {
|
|
@@ -1078,6 +1216,8 @@ function Dashboard({
|
|
|
1078
1216
|
[spansForSelectedService]
|
|
1079
1217
|
);
|
|
1080
1218
|
const selectedTraceSummary = drilldownTraceId == null ? filteredSummaries[selected] ?? null : filteredSummaries.find((t2) => t2.traceId === drilldownTraceId) ?? null;
|
|
1219
|
+
const serviceGraph = react.useMemo(() => buildServiceGraph(spans), [spans]);
|
|
1220
|
+
const topologyLines = react.useMemo(() => renderTopologyAscii(serviceGraph), [serviceGraph]);
|
|
1081
1221
|
const errorSummaries = react.useMemo(
|
|
1082
1222
|
() => buildErrorSummaries(traceSummaries),
|
|
1083
1223
|
[traceSummaries]
|
|
@@ -1484,6 +1624,13 @@ Currently viewing trace ${drilldownTraceId}. This trace has ${drilldownSpans.len
|
|
|
1484
1624
|
setDrilldownSelectedIndex(0);
|
|
1485
1625
|
setDrilldownScrollOffset(0);
|
|
1486
1626
|
}
|
|
1627
|
+
if (input === "G") {
|
|
1628
|
+
setViewMode((m) => m === "topology" ? "trace" : "topology");
|
|
1629
|
+
setSelected(0);
|
|
1630
|
+
setDrilldownTraceId(null);
|
|
1631
|
+
setDrilldownSelectedIndex(0);
|
|
1632
|
+
setDrilldownScrollOffset(0);
|
|
1633
|
+
}
|
|
1487
1634
|
if (input === "c") {
|
|
1488
1635
|
setSpans([]);
|
|
1489
1636
|
setLogs([]);
|
|
@@ -1625,7 +1772,7 @@ ${json}
|
|
|
1625
1772
|
{ isActive: isRawModeSupported }
|
|
1626
1773
|
);
|
|
1627
1774
|
const headerRight = recording ? "[Recording]" : paused ? "[Paused]" : "[Live]";
|
|
1628
|
-
const headerModeLabel = viewMode === "trace" ? "traces" : viewMode === "span" ? "spans" : viewMode === "log" ? "logs" : viewMode === "service-summary" ? "services" : "errors";
|
|
1775
|
+
const headerModeLabel = viewMode === "trace" ? "traces" : viewMode === "span" ? "spans" : viewMode === "log" ? "logs" : viewMode === "service-summary" ? "services" : viewMode === "topology" ? "topology" : "errors";
|
|
1629
1776
|
const showNewError = newErrorCount > 0;
|
|
1630
1777
|
function renderTreeRow(node, index) {
|
|
1631
1778
|
const isSel = drilldownTraceId != null && index === drilldownSelectedIndex;
|
|
@@ -1639,8 +1786,9 @@ ${json}
|
|
|
1639
1786
|
flexDirection: "row",
|
|
1640
1787
|
children: [
|
|
1641
1788
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { backgroundColor: isSel ? "blue" : void 0, color: isSel ? "white" : void 0, children: isSel ? "\u25B8 " : " " }),
|
|
1789
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: node.span.status === "ERROR" ? "red" : void 0, children: node.span.status === "ERROR" ? "\u2717" : " " }),
|
|
1642
1790
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: prefix }),
|
|
1643
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? statusColor : void 0, children: truncate(node.span.name,
|
|
1791
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? statusColor : void 0, children: truncate(node.span.name, 23) }),
|
|
1644
1792
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: svcColor, children: [
|
|
1645
1793
|
" ",
|
|
1646
1794
|
truncate(svcName, 10)
|
|
@@ -1706,12 +1854,20 @@ ${json}
|
|
|
1706
1854
|
] })
|
|
1707
1855
|
] }) : /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", justifyContent: "space-between", children: [
|
|
1708
1856
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: drilldownTraceId == null ? "\u2191/\u2193 select \u2022 Enter open \u2022 Tab cycle tabs \u2022 Esc back \u2022 T trace \u2022 L logs \u2022 a AI \u2022 ? help" : "\u2191/\u2193 select \u2022 Tab cycle tabs \u2022 Esc back \u2022 T trace \u2022 L logs \u2022 a AI \u2022 ? help" }),
|
|
1709
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: viewMode === "trace" ? `traces ${filteredSummaries.length}/${traceSummaries.length}` : viewMode === "span" ? `spans ${filteredSpans.length}/${spans.length}` : viewMode === "service-summary" ? `services ${serviceStats.length}` : viewMode === "errors" ? `errors ${filteredErrorSummaries.length}/${errorSummaries.length}` : `logs ${filteredLogs.length}/${logs.length}` })
|
|
1857
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: viewMode === "trace" ? `traces ${filteredSummaries.length}/${traceSummaries.length}` : viewMode === "span" ? `spans ${filteredSpans.length}/${spans.length}` : viewMode === "service-summary" ? `services ${serviceStats.length}` : viewMode === "errors" ? `errors ${filteredErrorSummaries.length}/${errorSummaries.length}` : viewMode === "topology" ? `services ${serviceGraph.services.length} \xB7 edges ${serviceGraph.edges.length}` : `logs ${filteredLogs.length}/${logs.length}` })
|
|
1710
1858
|
] }),
|
|
1711
|
-
showHelp && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Views: t/l/v/E \u2022 Search: / \u2022 Filters: e/S/R/H/f/x \u2022 Capture: p/r/J \u2022 AI: a \u2022 Clear: c" })
|
|
1859
|
+
showHelp && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Views: t/l/v/E/G \u2022 Search: / \u2022 Filters: e/S/R/H/f/x \u2022 Capture: p/r/J \u2022 AI: a \u2022 Clear: c" })
|
|
1712
1860
|
] }),
|
|
1713
1861
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginBottom: 0, children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: spanFilters.serviceName || spanFilters.route || spanFilters.statusGroup !== "all" || spanFilters.traceId ? `filters:${spanFilters.serviceName ? ` service=${spanFilters.serviceName}` : ""}${spanFilters.route ? ` route=${spanFilters.route}` : ""}${spanFilters.statusGroup && spanFilters.statusGroup !== "all" ? ` status=${spanFilters.statusGroup}` : ""}${spanFilters.traceId ? ` trace=${spanFilters.traceId.slice(0, 8)}\u2026` : ""}` : "filters: none" }) }),
|
|
1714
|
-
|
|
1862
|
+
viewMode === "topology" && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
|
|
1863
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Service Topology" }),
|
|
1864
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Press G to toggle \xB7 Shows service dependencies from span data" }),
|
|
1865
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", marginTop: 1, children: topologyLines.map((line, i) => {
|
|
1866
|
+
const hasErr = line.includes(" err");
|
|
1867
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: hasErr ? "red" : void 0, children: line }, `topo-${i}`);
|
|
1868
|
+
}) })
|
|
1869
|
+
] }),
|
|
1870
|
+
viewMode !== "topology" && (drilldownTraceId != null ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, paddingY: 0, children: [
|
|
1715
1871
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 0, flexDirection: "column", children: [
|
|
1716
1872
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { bold: true, children: [
|
|
1717
1873
|
"Trace ",
|
|
@@ -1810,7 +1966,8 @@ ${json}
|
|
|
1810
1966
|
const svcColor = getServiceColor(svcName);
|
|
1811
1967
|
const kindStr = (s.kind ?? "").padEnd(KIND_COL);
|
|
1812
1968
|
const svcStr = truncate(svcName, SERVICE_COL - 2).padEnd(SERVICE_COL);
|
|
1813
|
-
const
|
|
1969
|
+
const errorMark = s.status === "ERROR" ? "\u2717" : " ";
|
|
1970
|
+
const namePart = `${indent}${truncate(s.name, nameWidth - 1)}`.padEnd(NAME_COL - 1);
|
|
1814
1971
|
const bar = buildWaterfallBar(
|
|
1815
1972
|
s.startTime,
|
|
1816
1973
|
s.durationMs,
|
|
@@ -1821,6 +1978,7 @@ ${json}
|
|
|
1821
1978
|
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
1822
1979
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { backgroundColor: isSel ? "blue" : void 0, color: isSel ? "white" : void 0, children: [
|
|
1823
1980
|
isSel ? "\u25B8" : " ",
|
|
1981
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: s.status === "ERROR" ? "red" : void 0, children: errorMark }),
|
|
1824
1982
|
namePart
|
|
1825
1983
|
] }),
|
|
1826
1984
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: svcColor, children: [
|
|
@@ -1927,6 +2085,53 @@ ${json}
|
|
|
1927
2085
|
": ",
|
|
1928
2086
|
truncate(String(v), 28)
|
|
1929
2087
|
] }, k))
|
|
2088
|
+
] }),
|
|
2089
|
+
span.events && span.events.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", marginTop: 0, children: [
|
|
2090
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { bold: true, dimColor: true, children: [
|
|
2091
|
+
"Events (",
|
|
2092
|
+
span.events.length,
|
|
2093
|
+
")"
|
|
2094
|
+
] }),
|
|
2095
|
+
span.events.slice(0, 5).map((ev, i) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
2096
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: ev.name === "exception" ? "red" : "yellow", children: [
|
|
2097
|
+
" ",
|
|
2098
|
+
"\u25C6",
|
|
2099
|
+
" ",
|
|
2100
|
+
truncate(ev.name, 20)
|
|
2101
|
+
] }),
|
|
2102
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2103
|
+
" ",
|
|
2104
|
+
"+",
|
|
2105
|
+
formatDurationMs(ev.timeMs - span.startTime)
|
|
2106
|
+
] }),
|
|
2107
|
+
ev.attributes && Object.keys(ev.attributes).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2108
|
+
" ",
|
|
2109
|
+
Object.entries(ev.attributes).slice(0, 2).map(([k, v]) => `${k}=${String(v)}`).join(" ")
|
|
2110
|
+
] })
|
|
2111
|
+
] }, `ev-${i}`))
|
|
2112
|
+
] }),
|
|
2113
|
+
span.links && span.links.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", marginTop: 0, children: [
|
|
2114
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { bold: true, dimColor: true, children: [
|
|
2115
|
+
"Links (",
|
|
2116
|
+
span.links.length,
|
|
2117
|
+
")"
|
|
2118
|
+
] }),
|
|
2119
|
+
span.links.slice(0, 5).map((lnk, i) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
2120
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "cyan", children: [
|
|
2121
|
+
" ",
|
|
2122
|
+
"\u2192",
|
|
2123
|
+
" trace:",
|
|
2124
|
+
lnk.traceId.slice(0, 8),
|
|
2125
|
+
"\u2026",
|
|
2126
|
+
" span:",
|
|
2127
|
+
lnk.spanId.slice(0, 8),
|
|
2128
|
+
"\u2026"
|
|
2129
|
+
] }),
|
|
2130
|
+
lnk.attributes && Object.keys(lnk.attributes).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2131
|
+
" ",
|
|
2132
|
+
Object.entries(lnk.attributes).slice(0, 2).map(([k, v]) => `${k}=${String(v)}`).join(" ")
|
|
2133
|
+
] })
|
|
2134
|
+
] }, `lnk-${i}`))
|
|
1930
2135
|
] })
|
|
1931
2136
|
] });
|
|
1932
2137
|
})(),
|
|
@@ -2407,7 +2612,7 @@ ${json}
|
|
|
2407
2612
|
] })
|
|
2408
2613
|
}
|
|
2409
2614
|
)
|
|
2410
|
-
] }),
|
|
2615
|
+
] })),
|
|
2411
2616
|
showStats && /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2412
2617
|
"Spans: ",
|
|
2413
2618
|
stats.total,
|
|
@@ -2603,6 +2808,8 @@ function* extractSpans(payload) {
|
|
|
2603
2808
|
if (!Array.isArray(resourceSpans)) return;
|
|
2604
2809
|
for (const resourceSpan of resourceSpans) {
|
|
2605
2810
|
if (!resourceSpan || typeof resourceSpan !== "object") continue;
|
|
2811
|
+
const resource = resourceSpan.resource;
|
|
2812
|
+
const resourceAttrs = attrsToRecord(resource?.attributes);
|
|
2606
2813
|
const scopeSpans = resourceSpan.scopeSpans;
|
|
2607
2814
|
if (!Array.isArray(scopeSpans)) continue;
|
|
2608
2815
|
for (const scopeSpan of scopeSpans) {
|
|
@@ -2611,15 +2818,33 @@ function* extractSpans(payload) {
|
|
|
2611
2818
|
if (!Array.isArray(spans)) continue;
|
|
2612
2819
|
for (const span of spans) {
|
|
2613
2820
|
if (span && typeof span === "object") {
|
|
2614
|
-
yield span;
|
|
2821
|
+
yield { span, resourceAttrs };
|
|
2615
2822
|
}
|
|
2616
2823
|
}
|
|
2617
2824
|
}
|
|
2618
2825
|
}
|
|
2619
2826
|
}
|
|
2620
|
-
function otlpSpanToTerminalEvent(span) {
|
|
2827
|
+
function otlpSpanToTerminalEvent(span, resourceAttrs = {}) {
|
|
2621
2828
|
const startTime = toMs(span.startTimeUnixNano);
|
|
2622
2829
|
const endTime = toMs(span.endTimeUnixNano);
|
|
2830
|
+
const spanAttrs = attrsToRecord(span.attributes);
|
|
2831
|
+
const mergedAttrs = {};
|
|
2832
|
+
for (const [k, v] of Object.entries(resourceAttrs)) {
|
|
2833
|
+
mergedAttrs[k] = v;
|
|
2834
|
+
}
|
|
2835
|
+
for (const [k, v] of Object.entries(spanAttrs)) {
|
|
2836
|
+
mergedAttrs[k] = v;
|
|
2837
|
+
}
|
|
2838
|
+
const parsedEvents = span.events?.length ? span.events.map((e) => ({
|
|
2839
|
+
name: e.name || "",
|
|
2840
|
+
timeMs: toMs(e.timeUnixNano),
|
|
2841
|
+
attributes: attrsToRecord(e.attributes)
|
|
2842
|
+
})) : void 0;
|
|
2843
|
+
const parsedLinks = span.links?.length ? span.links.map((l) => ({
|
|
2844
|
+
traceId: normalizeHexId(l.traceId, 32),
|
|
2845
|
+
spanId: normalizeHexId(l.spanId, 16),
|
|
2846
|
+
attributes: attrsToRecord(l.attributes)
|
|
2847
|
+
})) : void 0;
|
|
2623
2848
|
return {
|
|
2624
2849
|
name: span.name || "unnamed",
|
|
2625
2850
|
spanId: normalizeHexId(span.spanId, 16),
|
|
@@ -2630,7 +2855,9 @@ function otlpSpanToTerminalEvent(span) {
|
|
|
2630
2855
|
durationMs: Math.max(0, endTime - startTime),
|
|
2631
2856
|
status: mapStatus2(span.status?.code),
|
|
2632
2857
|
kind: mapKind2(span.kind),
|
|
2633
|
-
attributes:
|
|
2858
|
+
attributes: mergedAttrs,
|
|
2859
|
+
...parsedEvents ? { events: parsedEvents } : {},
|
|
2860
|
+
...parsedLinks ? { links: parsedLinks } : {}
|
|
2634
2861
|
};
|
|
2635
2862
|
}
|
|
2636
2863
|
async function readJsonBody(req) {
|
|
@@ -2655,8 +2882,8 @@ function sendJson(res, status, data) {
|
|
|
2655
2882
|
}
|
|
2656
2883
|
function parseOtlpEvents(payload) {
|
|
2657
2884
|
const events = [];
|
|
2658
|
-
for (const span of extractSpans(payload)) {
|
|
2659
|
-
events.push(otlpSpanToTerminalEvent(span));
|
|
2885
|
+
for (const { span, resourceAttrs } of extractSpans(payload)) {
|
|
2886
|
+
events.push(otlpSpanToTerminalEvent(span, resourceAttrs));
|
|
2660
2887
|
}
|
|
2661
2888
|
return events;
|
|
2662
2889
|
}
|