@neat.is/core 0.7.10 → 0.8.0
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/{chunk-6T7ZHODF.js → chunk-3PXRQH53.js} +420 -20
- package/dist/chunk-3PXRQH53.js.map +1 -0
- package/dist/{chunk-D4OX6MUX.js → chunk-OJ6BW63H.js} +2 -2
- package/dist/cli.cjs +417 -18
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.cjs +417 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +417 -18
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +417 -18
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-6T7ZHODF.js.map +0 -1
- /package/dist/{chunk-D4OX6MUX.js.map → chunk-OJ6BW63H.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
resolveHost,
|
|
7
7
|
resolveNeatVersion,
|
|
8
8
|
writeDaemonRecord
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-OJ6BW63H.js";
|
|
10
10
|
import {
|
|
11
11
|
buildSearchIndex
|
|
12
12
|
} from "./chunk-BC53SCT7.js";
|
|
@@ -74,7 +74,7 @@ import {
|
|
|
74
74
|
startStalenessLoop,
|
|
75
75
|
upsertConnectorEntry,
|
|
76
76
|
validateConnectorEntry
|
|
77
|
-
} from "./chunk-
|
|
77
|
+
} from "./chunk-3PXRQH53.js";
|
|
78
78
|
import {
|
|
79
79
|
startOtelGrpcReceiver
|
|
80
80
|
} from "./chunk-FCO5Z3RW.js";
|
package/dist/index.cjs
CHANGED
|
@@ -1437,7 +1437,7 @@ var rootCauseShapes = {
|
|
|
1437
1437
|
[import_types.NodeType.FileNode]: fileRootCauseShape,
|
|
1438
1438
|
[import_types.NodeType.SymbolNode]: symbolRootCauseShape
|
|
1439
1439
|
};
|
|
1440
|
-
function
|
|
1440
|
+
function legacyRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
1441
1441
|
if (!graph.hasNode(errorNodeId)) return null;
|
|
1442
1442
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
1443
1443
|
const shape = rootCauseShapes[origin.type];
|
|
@@ -1694,7 +1694,9 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
1694
1694
|
dependencies: [],
|
|
1695
1695
|
observed: false,
|
|
1696
1696
|
inboundObservedCount: 0,
|
|
1697
|
-
hasExtractedOutbound: false
|
|
1697
|
+
hasExtractedOutbound: false,
|
|
1698
|
+
inboundVolume: 0,
|
|
1699
|
+
window: "lifetime"
|
|
1698
1700
|
});
|
|
1699
1701
|
}
|
|
1700
1702
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
@@ -1725,11 +1727,19 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
1725
1727
|
}
|
|
1726
1728
|
}
|
|
1727
1729
|
let inboundObservedCount = 0;
|
|
1730
|
+
let inboundVolume = 0;
|
|
1731
|
+
let inboundLastObserved;
|
|
1728
1732
|
for (const tgt of scope) {
|
|
1729
1733
|
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
1730
1734
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1731
1735
|
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1732
|
-
if (e.provenance === import_types.Provenance.OBSERVED)
|
|
1736
|
+
if (e.provenance === import_types.Provenance.OBSERVED) {
|
|
1737
|
+
inboundObservedCount += 1;
|
|
1738
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 1;
|
|
1739
|
+
if (e.lastObserved && (!inboundLastObserved || e.lastObserved > inboundLastObserved)) {
|
|
1740
|
+
inboundLastObserved = e.lastObserved;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1733
1743
|
}
|
|
1734
1744
|
}
|
|
1735
1745
|
dependencies.sort(
|
|
@@ -1740,7 +1750,291 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
1740
1750
|
dependencies,
|
|
1741
1751
|
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
1742
1752
|
inboundObservedCount,
|
|
1743
|
-
hasExtractedOutbound
|
|
1753
|
+
hasExtractedOutbound,
|
|
1754
|
+
// The signal is cumulative, so the honest window label is "lifetime" (ADR-190).
|
|
1755
|
+
inboundVolume,
|
|
1756
|
+
window: "lifetime",
|
|
1757
|
+
...inboundLastObserved ? { inboundLastObserved } : {}
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
var SATURATION_P95_MS = 1e3;
|
|
1761
|
+
function nodeScope(graph, nodeId) {
|
|
1762
|
+
const scope = [nodeId];
|
|
1763
|
+
if (!graph.hasNode(nodeId)) return scope;
|
|
1764
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
1765
|
+
if (attrs.type === import_types.NodeType.ServiceNode) {
|
|
1766
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
1767
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1768
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1769
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
1770
|
+
if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
return scope;
|
|
1774
|
+
}
|
|
1775
|
+
function incidentCountForNode(nodeId, incidents) {
|
|
1776
|
+
if (!incidents || incidents.length === 0) return 0;
|
|
1777
|
+
return incidents.filter((ev) => incidentMatchesNode(ev, nodeId)).length;
|
|
1778
|
+
}
|
|
1779
|
+
function nodeContext(graph, nodeId, incidents, now = Date.now()) {
|
|
1780
|
+
const scope = nodeScope(graph, nodeId);
|
|
1781
|
+
let errorsFromCallers = 0;
|
|
1782
|
+
let inboundVolume = 0;
|
|
1783
|
+
let outboundVolume = 0;
|
|
1784
|
+
let outboundErrors = 0;
|
|
1785
|
+
let latestInboundMs;
|
|
1786
|
+
let latencyP95Ms;
|
|
1787
|
+
let stale = false;
|
|
1788
|
+
for (const n of scope) {
|
|
1789
|
+
if (!graph.hasNode(n)) continue;
|
|
1790
|
+
for (const edgeId of graph.inboundEdges(n)) {
|
|
1791
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1792
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1793
|
+
errorsFromCallers += e.signal?.errorCount ?? 0;
|
|
1794
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
1795
|
+
if (e.provenance === import_types.Provenance.STALE) stale = true;
|
|
1796
|
+
const p95 = e.signal?.latencyMs?.p95;
|
|
1797
|
+
if (p95 !== void 0) latencyP95Ms = Math.max(latencyP95Ms ?? 0, p95);
|
|
1798
|
+
if (e.lastObserved) {
|
|
1799
|
+
const t = Date.parse(e.lastObserved);
|
|
1800
|
+
if (Number.isFinite(t)) latestInboundMs = Math.max(latestInboundMs ?? 0, t);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
1804
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1805
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1806
|
+
outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
1807
|
+
if (e.type === import_types.EdgeType.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
|
|
1808
|
+
if (e.provenance === import_types.Provenance.STALE) stale = true;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
const errorsEmittedHere = incidentCountForNode(nodeId, incidents) + outboundErrors;
|
|
1812
|
+
const lastObservedAgeMs = latestInboundMs !== void 0 ? Math.max(0, now - latestInboundMs) : void 0;
|
|
1813
|
+
return {
|
|
1814
|
+
errorsEmittedHere,
|
|
1815
|
+
errorsFromCallers,
|
|
1816
|
+
callCount: inboundVolume,
|
|
1817
|
+
outboundVolume,
|
|
1818
|
+
...lastObservedAgeMs !== void 0 ? { lastObservedAgeMs } : {},
|
|
1819
|
+
...latencyP95Ms !== void 0 ? { latencyP95Ms } : {},
|
|
1820
|
+
stale
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
function isSaturated(ctx) {
|
|
1824
|
+
return ctx.latencyP95Ms !== void 0 && ctx.latencyP95Ms >= SATURATION_P95_MS;
|
|
1825
|
+
}
|
|
1826
|
+
function classifyNode(ctx) {
|
|
1827
|
+
if (ctx.errorsEmittedHere > 0) {
|
|
1828
|
+
if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
|
|
1829
|
+
return "symptom-only";
|
|
1830
|
+
}
|
|
1831
|
+
return "primary-failure";
|
|
1832
|
+
}
|
|
1833
|
+
if (ctx.errorsFromCallers > 0) return "symptom-only";
|
|
1834
|
+
return "unrelated";
|
|
1835
|
+
}
|
|
1836
|
+
function isVictimSeed(ctx) {
|
|
1837
|
+
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
1838
|
+
}
|
|
1839
|
+
function grainOf(graph, nodeId) {
|
|
1840
|
+
if (!graph.hasNode(nodeId)) return "unknown";
|
|
1841
|
+
const t = graph.getNodeAttributes(nodeId).type;
|
|
1842
|
+
if (t === import_types.NodeType.ServiceNode) return "service";
|
|
1843
|
+
if (t === import_types.NodeType.FileNode) return "file";
|
|
1844
|
+
if (t === import_types.NodeType.SymbolNode) return "symbol";
|
|
1845
|
+
return t;
|
|
1846
|
+
}
|
|
1847
|
+
function findPath(graph, from, to, direction, maxDepth) {
|
|
1848
|
+
if (!graph.hasNode(from) || !graph.hasNode(to)) return null;
|
|
1849
|
+
if (from === to) return { nodes: [from], edges: [] };
|
|
1850
|
+
const queue = [{ nodeId: from, depth: 0, nodes: [from], edges: [] }];
|
|
1851
|
+
const enqueued = /* @__PURE__ */ new Set([from]);
|
|
1852
|
+
while (queue.length > 0) {
|
|
1853
|
+
const frame = queue.shift();
|
|
1854
|
+
if (frame.depth >= maxDepth) continue;
|
|
1855
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
|
|
1856
|
+
const neighbours = [...best.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
1857
|
+
for (const [nid, edge] of neighbours) {
|
|
1858
|
+
if (nid === to) return { nodes: [...frame.nodes, nid], edges: [...frame.edges, edge] };
|
|
1859
|
+
if (enqueued.has(nid)) continue;
|
|
1860
|
+
enqueued.add(nid);
|
|
1861
|
+
queue.push({
|
|
1862
|
+
nodeId: nid,
|
|
1863
|
+
depth: frame.depth + 1,
|
|
1864
|
+
nodes: [...frame.nodes, nid],
|
|
1865
|
+
edges: [...frame.edges, edge]
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
return null;
|
|
1870
|
+
}
|
|
1871
|
+
var EMPTY_CONTEXT = {
|
|
1872
|
+
errorsEmittedHere: 0,
|
|
1873
|
+
errorsFromCallers: 0,
|
|
1874
|
+
callCount: 0,
|
|
1875
|
+
outboundVolume: 0,
|
|
1876
|
+
stale: false
|
|
1877
|
+
};
|
|
1878
|
+
function expandNode(graph, nodeId, direction, incidents, now = Date.now()) {
|
|
1879
|
+
if (!graph.hasNode(nodeId)) {
|
|
1880
|
+
return import_types.ExpandResultSchema.parse({
|
|
1881
|
+
origin: nodeId,
|
|
1882
|
+
direction,
|
|
1883
|
+
node: { id: nodeId, classification: "unrelated", context: EMPTY_CONTEXT },
|
|
1884
|
+
neighbours: []
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
const ctx = nodeContext(graph, nodeId, incidents, now);
|
|
1888
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(nodeId));
|
|
1889
|
+
const neighbours = [];
|
|
1890
|
+
for (const [nid, edge] of best) {
|
|
1891
|
+
if (edge.type === import_types.EdgeType.CONTAINS) continue;
|
|
1892
|
+
const nctx = nodeContext(graph, nid, incidents, now);
|
|
1893
|
+
neighbours.push({
|
|
1894
|
+
node: nid,
|
|
1895
|
+
edgeType: edge.type,
|
|
1896
|
+
provenance: edge.provenance,
|
|
1897
|
+
classification: classifyNode(nctx),
|
|
1898
|
+
context: nctx
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
neighbours.sort((a, b) => a.node.localeCompare(b.node));
|
|
1902
|
+
return import_types.ExpandResultSchema.parse({
|
|
1903
|
+
origin: nodeId,
|
|
1904
|
+
direction,
|
|
1905
|
+
node: { id: nodeId, classification: classifyNode(ctx), context: ctx },
|
|
1906
|
+
neighbours
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
function relate(graph, a, b, maxDepth = ROOT_CAUSE_MAX_DEPTH) {
|
|
1910
|
+
const buildPath = (fp) => ({
|
|
1911
|
+
nodes: fp.nodes,
|
|
1912
|
+
edgeTypes: fp.edges.map((e) => e.type),
|
|
1913
|
+
provenance: fp.edges.map((e) => e.provenance),
|
|
1914
|
+
grain: fp.nodes.map((n) => grainOf(graph, n)),
|
|
1915
|
+
// The failure runs end to end when every hop carries error / latency / alert
|
|
1916
|
+
// signal — that is what turns reachability into cause-confirmation.
|
|
1917
|
+
carriesSignal: fp.edges.length > 0 && fp.edges.every(
|
|
1918
|
+
(e) => (e.signal?.errorCount ?? 0) > 0 || e.signal?.latencyMs !== void 0 || e.signal?.anomalous !== void 0
|
|
1919
|
+
)
|
|
1920
|
+
});
|
|
1921
|
+
if (!graph.hasNode(a) || !graph.hasNode(b)) {
|
|
1922
|
+
return import_types.RelateResultSchema.parse({
|
|
1923
|
+
a,
|
|
1924
|
+
b,
|
|
1925
|
+
related: false,
|
|
1926
|
+
direction: null,
|
|
1927
|
+
paths: [],
|
|
1928
|
+
note: !graph.hasNode(a) ? `node not found: ${a}` : `node not found: ${b}`
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
const down = findPath(graph, a, b, "down", maxDepth);
|
|
1932
|
+
const up = findPath(graph, a, b, "up", maxDepth);
|
|
1933
|
+
if (!down && !up) {
|
|
1934
|
+
return import_types.RelateResultSchema.parse({
|
|
1935
|
+
a,
|
|
1936
|
+
b,
|
|
1937
|
+
related: false,
|
|
1938
|
+
direction: null,
|
|
1939
|
+
paths: [],
|
|
1940
|
+
note: `no path within ${maxDepth} hops`
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
const paths = [];
|
|
1944
|
+
const direction = down ? "a->b" : "b->a";
|
|
1945
|
+
if (down) paths.push(buildPath(down));
|
|
1946
|
+
if (up) paths.push(buildPath(up));
|
|
1947
|
+
const endpointsFine = grainOf(graph, a) !== "service" && grainOf(graph, b) !== "service";
|
|
1948
|
+
const grainGap = endpointsFine && paths[0].grain.some((g) => g === "service");
|
|
1949
|
+
return import_types.RelateResultSchema.parse({
|
|
1950
|
+
a,
|
|
1951
|
+
b,
|
|
1952
|
+
related: true,
|
|
1953
|
+
direction,
|
|
1954
|
+
paths,
|
|
1955
|
+
...grainGap ? { grainGap: true } : {}
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
function findLoadOrigin(graph, alertNodeId, incidents, now) {
|
|
1959
|
+
const upstream = getBlastRadius(graph, alertNodeId).affectedNodes.map((n) => n.nodeId);
|
|
1960
|
+
let best = null;
|
|
1961
|
+
for (const nid of upstream) {
|
|
1962
|
+
if (nid === alertNodeId) continue;
|
|
1963
|
+
const ctx = nodeContext(graph, nid, incidents, now);
|
|
1964
|
+
if (ctx.outboundVolume === 0) continue;
|
|
1965
|
+
const isSource = ctx.callCount === 0;
|
|
1966
|
+
const better = !best || (isSource !== best.isSource ? isSource : ctx.outboundVolume !== best.ctx.outboundVolume ? ctx.outboundVolume > best.ctx.outboundVolume : nid < best.node);
|
|
1967
|
+
if (better) best = { node: nid, ctx, isSource };
|
|
1968
|
+
}
|
|
1969
|
+
return best ? { node: best.node, ctx: best.ctx } : null;
|
|
1970
|
+
}
|
|
1971
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents, opts) {
|
|
1972
|
+
const legacy = legacyRootCause(graph, errorNodeId, errorEvent, incidents);
|
|
1973
|
+
if (!legacy) return null;
|
|
1974
|
+
const navigation = opts?.navigation ?? process.env.NEAT_RCA_NAVIGATION !== "0";
|
|
1975
|
+
if (!navigation) return legacy;
|
|
1976
|
+
return enrichWithNavigation(graph, errorNodeId, legacy, incidents, opts?.now ?? Date.now());
|
|
1977
|
+
}
|
|
1978
|
+
function enrichWithNavigation(graph, errorNodeId, legacy, incidents, now) {
|
|
1979
|
+
const seedNode = legacy.rootCauseNode;
|
|
1980
|
+
const seedCtx = graph.hasNode(seedNode) ? nodeContext(graph, seedNode, incidents, now) : null;
|
|
1981
|
+
const lastProv = legacy.edgeProvenances[legacy.edgeProvenances.length - 1];
|
|
1982
|
+
const candidates = [];
|
|
1983
|
+
if (seedCtx && isVictimSeed(seedCtx)) {
|
|
1984
|
+
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
1985
|
+
if (origin) {
|
|
1986
|
+
const path68 = findPath(graph, errorNodeId, origin.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
1987
|
+
const originConfidence = confidenceFromMix(path68?.edges ?? [], now);
|
|
1988
|
+
candidates.push({
|
|
1989
|
+
node: origin.node,
|
|
1990
|
+
classification: "primary-failure",
|
|
1991
|
+
reason: `Highest-volume upstream source (${origin.ctx.outboundVolume} observed outbound calls) driving a saturated/stale subgraph; the alerting path decays downstream into a starved victim rather than a fault at the callee.`,
|
|
1992
|
+
context: origin.ctx,
|
|
1993
|
+
confidence: Math.max(0.3, Math.min(0.8, originConfidence || 0.5)),
|
|
1994
|
+
provenance: import_types.Provenance.OBSERVED
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
const staleNote = seedCtx.stale ? "; the node has gone STALE" : "";
|
|
1998
|
+
const satNote = isSaturated(seedCtx) ? `; inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
1999
|
+
candidates.push({
|
|
2000
|
+
node: seedNode,
|
|
2001
|
+
classification: "symptom-only",
|
|
2002
|
+
reason: `Errors arrive from callers (${seedCtx.errorsFromCallers}) but none originate here${staleNote}${satNote} \u2014 a downstream victim of load, not the fault.`,
|
|
2003
|
+
context: seedCtx,
|
|
2004
|
+
confidence: Math.min(legacy.confidence, 0.4),
|
|
2005
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
2006
|
+
});
|
|
2007
|
+
} else {
|
|
2008
|
+
candidates.push({
|
|
2009
|
+
node: seedNode,
|
|
2010
|
+
classification: "primary-failure",
|
|
2011
|
+
reason: legacy.rootCauseReason,
|
|
2012
|
+
context: seedCtx ?? EMPTY_CONTEXT,
|
|
2013
|
+
confidence: legacy.confidence,
|
|
2014
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
const top = candidates[0];
|
|
2018
|
+
let traversalPath = legacy.traversalPath;
|
|
2019
|
+
let edgeProvenances = legacy.edgeProvenances;
|
|
2020
|
+
if (top.node !== seedNode) {
|
|
2021
|
+
const path68 = findPath(graph, errorNodeId, top.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
2022
|
+
if (path68) {
|
|
2023
|
+
traversalPath = path68.nodes;
|
|
2024
|
+
edgeProvenances = path68.edges.map((e) => e.provenance);
|
|
2025
|
+
} else {
|
|
2026
|
+
traversalPath = [errorNodeId, top.node];
|
|
2027
|
+
edgeProvenances = [top.provenance ?? import_types.Provenance.OBSERVED];
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
return import_types.RootCauseResultSchema.parse({
|
|
2031
|
+
rootCauseNode: top.node,
|
|
2032
|
+
rootCauseReason: top.reason,
|
|
2033
|
+
traversalPath,
|
|
2034
|
+
edgeProvenances,
|
|
2035
|
+
confidence: top.confidence,
|
|
2036
|
+
...legacy.fixRecommendation ? { fixRecommendation: legacy.fixRecommendation } : {},
|
|
2037
|
+
candidates
|
|
1744
2038
|
});
|
|
1745
2039
|
}
|
|
1746
2040
|
|
|
@@ -4623,6 +4917,63 @@ function columnIsObserved(col) {
|
|
|
4623
4917
|
return col.provenances.includes(import_types7.Provenance.OBSERVED);
|
|
4624
4918
|
}
|
|
4625
4919
|
|
|
4920
|
+
// src/latency-digest.ts
|
|
4921
|
+
init_cjs_shims();
|
|
4922
|
+
var SUB = 16;
|
|
4923
|
+
var MIN_EXP = -10;
|
|
4924
|
+
var MAX_EXP = 22;
|
|
4925
|
+
var OCTAVES = MAX_EXP - MIN_EXP + 1;
|
|
4926
|
+
var OVERFLOW_INDEX = OCTAVES * SUB + 1;
|
|
4927
|
+
function latencyBucketIndex(ms) {
|
|
4928
|
+
if (!Number.isFinite(ms) || ms <= 0) return 0;
|
|
4929
|
+
const e = Math.floor(Math.log2(ms));
|
|
4930
|
+
if (e < MIN_EXP) return 0;
|
|
4931
|
+
if (e > MAX_EXP) return OVERFLOW_INDEX;
|
|
4932
|
+
const base = 2 ** e;
|
|
4933
|
+
const raw = Math.floor((ms / base - 1) * SUB);
|
|
4934
|
+
const s = raw < 0 ? 0 : raw >= SUB ? SUB - 1 : raw;
|
|
4935
|
+
return (e - MIN_EXP) * SUB + s + 1;
|
|
4936
|
+
}
|
|
4937
|
+
function bucketRepresentativeMs(index) {
|
|
4938
|
+
if (index <= 0) return 0;
|
|
4939
|
+
if (index >= OVERFLOW_INDEX) return 2 ** (MAX_EXP + 1);
|
|
4940
|
+
const zeroBased = index - 1;
|
|
4941
|
+
const e = MIN_EXP + Math.floor(zeroBased / SUB);
|
|
4942
|
+
const s = zeroBased % SUB;
|
|
4943
|
+
const base = 2 ** e;
|
|
4944
|
+
const lower = base * (1 + s / SUB);
|
|
4945
|
+
const upper = base * (1 + (s + 1) / SUB);
|
|
4946
|
+
return (lower + upper) / 2;
|
|
4947
|
+
}
|
|
4948
|
+
function recordLatency(hist, ms) {
|
|
4949
|
+
const key = String(latencyBucketIndex(ms));
|
|
4950
|
+
hist[key] = (hist[key] ?? 0) + 1;
|
|
4951
|
+
return hist;
|
|
4952
|
+
}
|
|
4953
|
+
function quantile(hist, q) {
|
|
4954
|
+
const entries = Object.entries(hist).map(([k, c]) => [Number(k), c]).filter(([idx, c]) => Number.isFinite(idx) && c > 0).sort((a, b) => a[0] - b[0]);
|
|
4955
|
+
let total = 0;
|
|
4956
|
+
for (const [, c] of entries) total += c;
|
|
4957
|
+
if (total === 0) return 0;
|
|
4958
|
+
const rank = Math.max(1, Math.ceil(q * total));
|
|
4959
|
+
let cumulative = 0;
|
|
4960
|
+
for (const [idx, c] of entries) {
|
|
4961
|
+
cumulative += c;
|
|
4962
|
+
if (cumulative >= rank) return bucketRepresentativeMs(idx);
|
|
4963
|
+
}
|
|
4964
|
+
return bucketRepresentativeMs(entries[entries.length - 1][0]);
|
|
4965
|
+
}
|
|
4966
|
+
function round(ms) {
|
|
4967
|
+
return Math.round(ms * 100) / 100;
|
|
4968
|
+
}
|
|
4969
|
+
function latencyPercentiles(hist) {
|
|
4970
|
+
if (!hist) return void 0;
|
|
4971
|
+
let total = 0;
|
|
4972
|
+
for (const c of Object.values(hist)) total += c;
|
|
4973
|
+
if (total === 0) return void 0;
|
|
4974
|
+
return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
|
|
4975
|
+
}
|
|
4976
|
+
|
|
4626
4977
|
// src/ingest.ts
|
|
4627
4978
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
4628
4979
|
var DAY_MS = 24 * HOUR_MS;
|
|
@@ -5318,7 +5669,7 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
5318
5669
|
graph.addNode(id, node);
|
|
5319
5670
|
return id;
|
|
5320
5671
|
}
|
|
5321
|
-
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
|
|
5672
|
+
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence, durationMs) {
|
|
5322
5673
|
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
5323
5674
|
const grain = source.startsWith("file:") || source.startsWith("symbol:") ? "file" : "service";
|
|
5324
5675
|
const id = makeObservedEdgeId(type, source, target);
|
|
@@ -5326,10 +5677,15 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
5326
5677
|
const existing = graph.getEdgeAttributes(id);
|
|
5327
5678
|
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
5328
5679
|
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
5680
|
+
const latencyHist2 = durationMs !== void 0 ? recordLatency({ ...existing.signal?.latencyHist ?? {} }, durationMs) : existing.signal?.latencyHist;
|
|
5681
|
+
const latencyMs2 = latencyPercentiles(latencyHist2) ?? existing.signal?.latencyMs;
|
|
5329
5682
|
const newSignal = {
|
|
5330
5683
|
spanCount: newSpanCount,
|
|
5331
5684
|
errorCount: newErrorCount,
|
|
5332
|
-
lastObservedAgeMs: 0
|
|
5685
|
+
lastObservedAgeMs: 0,
|
|
5686
|
+
...latencyHist2 ? { latencyHist: latencyHist2 } : {},
|
|
5687
|
+
...latencyMs2 ? { latencyMs: latencyMs2 } : {},
|
|
5688
|
+
...existing.signal?.anomalous !== void 0 ? { anomalous: existing.signal.anomalous } : {}
|
|
5333
5689
|
};
|
|
5334
5690
|
const updated = {
|
|
5335
5691
|
...existing,
|
|
@@ -5344,10 +5700,14 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
5344
5700
|
graph.replaceEdgeAttributes(id, updated);
|
|
5345
5701
|
return { edge: updated, created: false };
|
|
5346
5702
|
}
|
|
5703
|
+
const latencyHist = durationMs !== void 0 ? recordLatency({}, durationMs) : void 0;
|
|
5704
|
+
const latencyMs = latencyHist ? latencyPercentiles(latencyHist) : void 0;
|
|
5347
5705
|
const signal = {
|
|
5348
5706
|
spanCount: 1,
|
|
5349
5707
|
errorCount: isError ? 1 : 0,
|
|
5350
|
-
lastObservedAgeMs: 0
|
|
5708
|
+
lastObservedAgeMs: 0,
|
|
5709
|
+
...latencyHist ? { latencyHist } : {},
|
|
5710
|
+
...latencyMs ? { latencyMs } : {}
|
|
5351
5711
|
};
|
|
5352
5712
|
const edge = {
|
|
5353
5713
|
id,
|
|
@@ -5572,6 +5932,7 @@ async function handleSpan(ctx, span) {
|
|
|
5572
5932
|
}
|
|
5573
5933
|
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
5574
5934
|
const isError = span.statusCode === 2;
|
|
5935
|
+
const durationMs = span.durationNanos > 0n ? Number(span.durationNanos) / 1e6 : void 0;
|
|
5575
5936
|
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
5576
5937
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
5577
5938
|
cacheSpanService(span, nowMs, callSite);
|
|
@@ -5610,7 +5971,8 @@ async function handleSpan(ctx, span) {
|
|
|
5610
5971
|
targetId,
|
|
5611
5972
|
ts,
|
|
5612
5973
|
isError,
|
|
5613
|
-
callSiteEvidence
|
|
5974
|
+
callSiteEvidence,
|
|
5975
|
+
durationMs
|
|
5614
5976
|
);
|
|
5615
5977
|
if (result) affectedNode = targetId;
|
|
5616
5978
|
if (span.dbSystem === "mongodb" && span.dbCollection) {
|
|
@@ -5622,7 +5984,8 @@ async function handleSpan(ctx, span) {
|
|
|
5622
5984
|
collectionId,
|
|
5623
5985
|
ts,
|
|
5624
5986
|
isError,
|
|
5625
|
-
callSiteEvidence
|
|
5987
|
+
callSiteEvidence,
|
|
5988
|
+
durationMs
|
|
5626
5989
|
);
|
|
5627
5990
|
}
|
|
5628
5991
|
if (span.dbTable) {
|
|
@@ -5634,7 +5997,8 @@ async function handleSpan(ctx, span) {
|
|
|
5634
5997
|
tableId,
|
|
5635
5998
|
ts,
|
|
5636
5999
|
isError,
|
|
5637
|
-
callSiteEvidence
|
|
6000
|
+
callSiteEvidence,
|
|
6001
|
+
durationMs
|
|
5638
6002
|
);
|
|
5639
6003
|
mergeObservedColumns(ctx.graph, tableId, span.dbColumns);
|
|
5640
6004
|
}
|
|
@@ -5653,7 +6017,8 @@ async function handleSpan(ctx, span) {
|
|
|
5653
6017
|
targetId,
|
|
5654
6018
|
ts,
|
|
5655
6019
|
isError,
|
|
5656
|
-
callSiteEvidence
|
|
6020
|
+
callSiteEvidence,
|
|
6021
|
+
durationMs
|
|
5657
6022
|
);
|
|
5658
6023
|
if (result) affectedNode = targetId;
|
|
5659
6024
|
} else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
|
|
@@ -5670,7 +6035,8 @@ async function handleSpan(ctx, span) {
|
|
|
5670
6035
|
targetId,
|
|
5671
6036
|
ts,
|
|
5672
6037
|
isError,
|
|
5673
|
-
callSiteEvidence
|
|
6038
|
+
callSiteEvidence,
|
|
6039
|
+
durationMs
|
|
5674
6040
|
);
|
|
5675
6041
|
if (result) affectedNode = targetId;
|
|
5676
6042
|
} else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
|
|
@@ -5682,7 +6048,8 @@ async function handleSpan(ctx, span) {
|
|
|
5682
6048
|
targetId,
|
|
5683
6049
|
ts,
|
|
5684
6050
|
isError,
|
|
5685
|
-
callSiteEvidence
|
|
6051
|
+
callSiteEvidence,
|
|
6052
|
+
durationMs
|
|
5686
6053
|
);
|
|
5687
6054
|
if (result) affectedNode = targetId;
|
|
5688
6055
|
} else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
|
|
@@ -5698,7 +6065,8 @@ async function handleSpan(ctx, span) {
|
|
|
5698
6065
|
targetId,
|
|
5699
6066
|
ts,
|
|
5700
6067
|
isError,
|
|
5701
|
-
callSiteEvidence
|
|
6068
|
+
callSiteEvidence,
|
|
6069
|
+
durationMs
|
|
5702
6070
|
);
|
|
5703
6071
|
if (result) affectedNode = targetId;
|
|
5704
6072
|
} else {
|
|
@@ -5714,7 +6082,8 @@ async function handleSpan(ctx, span) {
|
|
|
5714
6082
|
targetId,
|
|
5715
6083
|
ts,
|
|
5716
6084
|
isError,
|
|
5717
|
-
callSiteEvidence
|
|
6085
|
+
callSiteEvidence,
|
|
6086
|
+
durationMs
|
|
5718
6087
|
);
|
|
5719
6088
|
affectedNode = targetId;
|
|
5720
6089
|
resolvedViaAddress = true;
|
|
@@ -5727,7 +6096,8 @@ async function handleSpan(ctx, span) {
|
|
|
5727
6096
|
frontierNodeId,
|
|
5728
6097
|
ts,
|
|
5729
6098
|
isError,
|
|
5730
|
-
callSiteEvidence
|
|
6099
|
+
callSiteEvidence,
|
|
6100
|
+
durationMs
|
|
5731
6101
|
);
|
|
5732
6102
|
affectedNode = frontierNodeId;
|
|
5733
6103
|
resolvedViaAddress = true;
|
|
@@ -5753,7 +6123,8 @@ async function handleSpan(ctx, span) {
|
|
|
5753
6123
|
sourceId,
|
|
5754
6124
|
ts,
|
|
5755
6125
|
isError,
|
|
5756
|
-
fallbackEvidence
|
|
6126
|
+
fallbackEvidence,
|
|
6127
|
+
durationMs
|
|
5757
6128
|
);
|
|
5758
6129
|
}
|
|
5759
6130
|
}
|
|
@@ -5767,7 +6138,7 @@ async function handleSpan(ctx, span) {
|
|
|
5767
6138
|
);
|
|
5768
6139
|
if (routeNodeId) {
|
|
5769
6140
|
const routeSvc = ctx.graph.getNodeAttributes(routeNodeId).service;
|
|
5770
|
-
upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError);
|
|
6141
|
+
upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError, void 0, durationMs);
|
|
5771
6142
|
}
|
|
5772
6143
|
}
|
|
5773
6144
|
if (span.statusCode === 2) {
|
|
@@ -18513,6 +18884,34 @@ function registerRoutes(scope, ctx) {
|
|
|
18513
18884
|
}
|
|
18514
18885
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
18515
18886
|
});
|
|
18887
|
+
scope.get("/graph/expand/:nodeId", async (req, reply) => {
|
|
18888
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18889
|
+
if (!proj) return;
|
|
18890
|
+
const { nodeId } = req.params;
|
|
18891
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
18892
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
18893
|
+
}
|
|
18894
|
+
const direction = req.query.direction;
|
|
18895
|
+
if (direction !== "up" && direction !== "down") {
|
|
18896
|
+
return reply.code(400).send({ error: 'direction must be "up" or "down"' });
|
|
18897
|
+
}
|
|
18898
|
+
const epath = errorsPathFor(proj);
|
|
18899
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
18900
|
+
return expandNode(proj.graph, nodeId, direction, incidents);
|
|
18901
|
+
});
|
|
18902
|
+
scope.get("/graph/relate", async (req, reply) => {
|
|
18903
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18904
|
+
if (!proj) return;
|
|
18905
|
+
const { a, b } = req.query;
|
|
18906
|
+
if (!a || !b) {
|
|
18907
|
+
return reply.code(400).send({ error: "both a and b query params are required" });
|
|
18908
|
+
}
|
|
18909
|
+
const maxDepth = req.query.maxDepth ? Number(req.query.maxDepth) : void 0;
|
|
18910
|
+
if (maxDepth !== void 0 && (!Number.isFinite(maxDepth) || maxDepth < 1)) {
|
|
18911
|
+
return reply.code(400).send({ error: "maxDepth must be a positive integer" });
|
|
18912
|
+
}
|
|
18913
|
+
return relate(proj.graph, a, b, maxDepth);
|
|
18914
|
+
});
|
|
18516
18915
|
scope.get("/search", async (req, reply) => {
|
|
18517
18916
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18518
18917
|
if (!proj) return;
|