@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
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
startStalenessLoop,
|
|
21
21
|
touchLastSeen,
|
|
22
22
|
writeAtomically
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-3PXRQH53.js";
|
|
24
24
|
import {
|
|
25
25
|
assertBindAuthority,
|
|
26
26
|
buildOtelReceiver,
|
|
@@ -891,4 +891,4 @@ export {
|
|
|
891
891
|
resolveHost,
|
|
892
892
|
startDaemon
|
|
893
893
|
};
|
|
894
|
-
//# sourceMappingURL=chunk-
|
|
894
|
+
//# sourceMappingURL=chunk-OJ6BW63H.js.map
|
package/dist/cli.cjs
CHANGED
|
@@ -1445,7 +1445,7 @@ var rootCauseShapes = {
|
|
|
1445
1445
|
[import_types.NodeType.FileNode]: fileRootCauseShape,
|
|
1446
1446
|
[import_types.NodeType.SymbolNode]: symbolRootCauseShape
|
|
1447
1447
|
};
|
|
1448
|
-
function
|
|
1448
|
+
function legacyRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
1449
1449
|
if (!graph.hasNode(errorNodeId)) return null;
|
|
1450
1450
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
1451
1451
|
const shape = rootCauseShapes[origin.type];
|
|
@@ -1702,7 +1702,9 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
1702
1702
|
dependencies: [],
|
|
1703
1703
|
observed: false,
|
|
1704
1704
|
inboundObservedCount: 0,
|
|
1705
|
-
hasExtractedOutbound: false
|
|
1705
|
+
hasExtractedOutbound: false,
|
|
1706
|
+
inboundVolume: 0,
|
|
1707
|
+
window: "lifetime"
|
|
1706
1708
|
});
|
|
1707
1709
|
}
|
|
1708
1710
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
@@ -1733,11 +1735,19 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
1733
1735
|
}
|
|
1734
1736
|
}
|
|
1735
1737
|
let inboundObservedCount = 0;
|
|
1738
|
+
let inboundVolume = 0;
|
|
1739
|
+
let inboundLastObserved;
|
|
1736
1740
|
for (const tgt of scope) {
|
|
1737
1741
|
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
1738
1742
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1739
1743
|
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1740
|
-
if (e.provenance === import_types.Provenance.OBSERVED)
|
|
1744
|
+
if (e.provenance === import_types.Provenance.OBSERVED) {
|
|
1745
|
+
inboundObservedCount += 1;
|
|
1746
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 1;
|
|
1747
|
+
if (e.lastObserved && (!inboundLastObserved || e.lastObserved > inboundLastObserved)) {
|
|
1748
|
+
inboundLastObserved = e.lastObserved;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1741
1751
|
}
|
|
1742
1752
|
}
|
|
1743
1753
|
dependencies.sort(
|
|
@@ -1748,7 +1758,291 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
1748
1758
|
dependencies,
|
|
1749
1759
|
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
1750
1760
|
inboundObservedCount,
|
|
1751
|
-
hasExtractedOutbound
|
|
1761
|
+
hasExtractedOutbound,
|
|
1762
|
+
// The signal is cumulative, so the honest window label is "lifetime" (ADR-190).
|
|
1763
|
+
inboundVolume,
|
|
1764
|
+
window: "lifetime",
|
|
1765
|
+
...inboundLastObserved ? { inboundLastObserved } : {}
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
var SATURATION_P95_MS = 1e3;
|
|
1769
|
+
function nodeScope(graph, nodeId) {
|
|
1770
|
+
const scope = [nodeId];
|
|
1771
|
+
if (!graph.hasNode(nodeId)) return scope;
|
|
1772
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
1773
|
+
if (attrs.type === import_types.NodeType.ServiceNode) {
|
|
1774
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
1775
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1776
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1777
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
1778
|
+
if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return scope;
|
|
1782
|
+
}
|
|
1783
|
+
function incidentCountForNode(nodeId, incidents) {
|
|
1784
|
+
if (!incidents || incidents.length === 0) return 0;
|
|
1785
|
+
return incidents.filter((ev) => incidentMatchesNode(ev, nodeId)).length;
|
|
1786
|
+
}
|
|
1787
|
+
function nodeContext(graph, nodeId, incidents, now = Date.now()) {
|
|
1788
|
+
const scope = nodeScope(graph, nodeId);
|
|
1789
|
+
let errorsFromCallers = 0;
|
|
1790
|
+
let inboundVolume = 0;
|
|
1791
|
+
let outboundVolume = 0;
|
|
1792
|
+
let outboundErrors = 0;
|
|
1793
|
+
let latestInboundMs;
|
|
1794
|
+
let latencyP95Ms;
|
|
1795
|
+
let stale = false;
|
|
1796
|
+
for (const n of scope) {
|
|
1797
|
+
if (!graph.hasNode(n)) continue;
|
|
1798
|
+
for (const edgeId of graph.inboundEdges(n)) {
|
|
1799
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1800
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1801
|
+
errorsFromCallers += e.signal?.errorCount ?? 0;
|
|
1802
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
1803
|
+
if (e.provenance === import_types.Provenance.STALE) stale = true;
|
|
1804
|
+
const p95 = e.signal?.latencyMs?.p95;
|
|
1805
|
+
if (p95 !== void 0) latencyP95Ms = Math.max(latencyP95Ms ?? 0, p95);
|
|
1806
|
+
if (e.lastObserved) {
|
|
1807
|
+
const t = Date.parse(e.lastObserved);
|
|
1808
|
+
if (Number.isFinite(t)) latestInboundMs = Math.max(latestInboundMs ?? 0, t);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
1812
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1813
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1814
|
+
outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
1815
|
+
if (e.type === import_types.EdgeType.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
|
|
1816
|
+
if (e.provenance === import_types.Provenance.STALE) stale = true;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
const errorsEmittedHere = incidentCountForNode(nodeId, incidents) + outboundErrors;
|
|
1820
|
+
const lastObservedAgeMs = latestInboundMs !== void 0 ? Math.max(0, now - latestInboundMs) : void 0;
|
|
1821
|
+
return {
|
|
1822
|
+
errorsEmittedHere,
|
|
1823
|
+
errorsFromCallers,
|
|
1824
|
+
callCount: inboundVolume,
|
|
1825
|
+
outboundVolume,
|
|
1826
|
+
...lastObservedAgeMs !== void 0 ? { lastObservedAgeMs } : {},
|
|
1827
|
+
...latencyP95Ms !== void 0 ? { latencyP95Ms } : {},
|
|
1828
|
+
stale
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
function isSaturated(ctx) {
|
|
1832
|
+
return ctx.latencyP95Ms !== void 0 && ctx.latencyP95Ms >= SATURATION_P95_MS;
|
|
1833
|
+
}
|
|
1834
|
+
function classifyNode(ctx) {
|
|
1835
|
+
if (ctx.errorsEmittedHere > 0) {
|
|
1836
|
+
if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
|
|
1837
|
+
return "symptom-only";
|
|
1838
|
+
}
|
|
1839
|
+
return "primary-failure";
|
|
1840
|
+
}
|
|
1841
|
+
if (ctx.errorsFromCallers > 0) return "symptom-only";
|
|
1842
|
+
return "unrelated";
|
|
1843
|
+
}
|
|
1844
|
+
function isVictimSeed(ctx) {
|
|
1845
|
+
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
1846
|
+
}
|
|
1847
|
+
function grainOf(graph, nodeId) {
|
|
1848
|
+
if (!graph.hasNode(nodeId)) return "unknown";
|
|
1849
|
+
const t = graph.getNodeAttributes(nodeId).type;
|
|
1850
|
+
if (t === import_types.NodeType.ServiceNode) return "service";
|
|
1851
|
+
if (t === import_types.NodeType.FileNode) return "file";
|
|
1852
|
+
if (t === import_types.NodeType.SymbolNode) return "symbol";
|
|
1853
|
+
return t;
|
|
1854
|
+
}
|
|
1855
|
+
function findPath(graph, from, to, direction, maxDepth) {
|
|
1856
|
+
if (!graph.hasNode(from) || !graph.hasNode(to)) return null;
|
|
1857
|
+
if (from === to) return { nodes: [from], edges: [] };
|
|
1858
|
+
const queue = [{ nodeId: from, depth: 0, nodes: [from], edges: [] }];
|
|
1859
|
+
const enqueued = /* @__PURE__ */ new Set([from]);
|
|
1860
|
+
while (queue.length > 0) {
|
|
1861
|
+
const frame = queue.shift();
|
|
1862
|
+
if (frame.depth >= maxDepth) continue;
|
|
1863
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
|
|
1864
|
+
const neighbours = [...best.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
1865
|
+
for (const [nid, edge] of neighbours) {
|
|
1866
|
+
if (nid === to) return { nodes: [...frame.nodes, nid], edges: [...frame.edges, edge] };
|
|
1867
|
+
if (enqueued.has(nid)) continue;
|
|
1868
|
+
enqueued.add(nid);
|
|
1869
|
+
queue.push({
|
|
1870
|
+
nodeId: nid,
|
|
1871
|
+
depth: frame.depth + 1,
|
|
1872
|
+
nodes: [...frame.nodes, nid],
|
|
1873
|
+
edges: [...frame.edges, edge]
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
return null;
|
|
1878
|
+
}
|
|
1879
|
+
var EMPTY_CONTEXT = {
|
|
1880
|
+
errorsEmittedHere: 0,
|
|
1881
|
+
errorsFromCallers: 0,
|
|
1882
|
+
callCount: 0,
|
|
1883
|
+
outboundVolume: 0,
|
|
1884
|
+
stale: false
|
|
1885
|
+
};
|
|
1886
|
+
function expandNode(graph, nodeId, direction, incidents, now = Date.now()) {
|
|
1887
|
+
if (!graph.hasNode(nodeId)) {
|
|
1888
|
+
return import_types.ExpandResultSchema.parse({
|
|
1889
|
+
origin: nodeId,
|
|
1890
|
+
direction,
|
|
1891
|
+
node: { id: nodeId, classification: "unrelated", context: EMPTY_CONTEXT },
|
|
1892
|
+
neighbours: []
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
const ctx = nodeContext(graph, nodeId, incidents, now);
|
|
1896
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(nodeId));
|
|
1897
|
+
const neighbours = [];
|
|
1898
|
+
for (const [nid, edge] of best) {
|
|
1899
|
+
if (edge.type === import_types.EdgeType.CONTAINS) continue;
|
|
1900
|
+
const nctx = nodeContext(graph, nid, incidents, now);
|
|
1901
|
+
neighbours.push({
|
|
1902
|
+
node: nid,
|
|
1903
|
+
edgeType: edge.type,
|
|
1904
|
+
provenance: edge.provenance,
|
|
1905
|
+
classification: classifyNode(nctx),
|
|
1906
|
+
context: nctx
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
neighbours.sort((a, b) => a.node.localeCompare(b.node));
|
|
1910
|
+
return import_types.ExpandResultSchema.parse({
|
|
1911
|
+
origin: nodeId,
|
|
1912
|
+
direction,
|
|
1913
|
+
node: { id: nodeId, classification: classifyNode(ctx), context: ctx },
|
|
1914
|
+
neighbours
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
function relate(graph, a, b, maxDepth = ROOT_CAUSE_MAX_DEPTH) {
|
|
1918
|
+
const buildPath = (fp) => ({
|
|
1919
|
+
nodes: fp.nodes,
|
|
1920
|
+
edgeTypes: fp.edges.map((e) => e.type),
|
|
1921
|
+
provenance: fp.edges.map((e) => e.provenance),
|
|
1922
|
+
grain: fp.nodes.map((n) => grainOf(graph, n)),
|
|
1923
|
+
// The failure runs end to end when every hop carries error / latency / alert
|
|
1924
|
+
// signal — that is what turns reachability into cause-confirmation.
|
|
1925
|
+
carriesSignal: fp.edges.length > 0 && fp.edges.every(
|
|
1926
|
+
(e) => (e.signal?.errorCount ?? 0) > 0 || e.signal?.latencyMs !== void 0 || e.signal?.anomalous !== void 0
|
|
1927
|
+
)
|
|
1928
|
+
});
|
|
1929
|
+
if (!graph.hasNode(a) || !graph.hasNode(b)) {
|
|
1930
|
+
return import_types.RelateResultSchema.parse({
|
|
1931
|
+
a,
|
|
1932
|
+
b,
|
|
1933
|
+
related: false,
|
|
1934
|
+
direction: null,
|
|
1935
|
+
paths: [],
|
|
1936
|
+
note: !graph.hasNode(a) ? `node not found: ${a}` : `node not found: ${b}`
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
const down = findPath(graph, a, b, "down", maxDepth);
|
|
1940
|
+
const up = findPath(graph, a, b, "up", maxDepth);
|
|
1941
|
+
if (!down && !up) {
|
|
1942
|
+
return import_types.RelateResultSchema.parse({
|
|
1943
|
+
a,
|
|
1944
|
+
b,
|
|
1945
|
+
related: false,
|
|
1946
|
+
direction: null,
|
|
1947
|
+
paths: [],
|
|
1948
|
+
note: `no path within ${maxDepth} hops`
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
const paths = [];
|
|
1952
|
+
const direction = down ? "a->b" : "b->a";
|
|
1953
|
+
if (down) paths.push(buildPath(down));
|
|
1954
|
+
if (up) paths.push(buildPath(up));
|
|
1955
|
+
const endpointsFine = grainOf(graph, a) !== "service" && grainOf(graph, b) !== "service";
|
|
1956
|
+
const grainGap = endpointsFine && paths[0].grain.some((g) => g === "service");
|
|
1957
|
+
return import_types.RelateResultSchema.parse({
|
|
1958
|
+
a,
|
|
1959
|
+
b,
|
|
1960
|
+
related: true,
|
|
1961
|
+
direction,
|
|
1962
|
+
paths,
|
|
1963
|
+
...grainGap ? { grainGap: true } : {}
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
function findLoadOrigin(graph, alertNodeId, incidents, now) {
|
|
1967
|
+
const upstream = getBlastRadius(graph, alertNodeId).affectedNodes.map((n) => n.nodeId);
|
|
1968
|
+
let best = null;
|
|
1969
|
+
for (const nid of upstream) {
|
|
1970
|
+
if (nid === alertNodeId) continue;
|
|
1971
|
+
const ctx = nodeContext(graph, nid, incidents, now);
|
|
1972
|
+
if (ctx.outboundVolume === 0) continue;
|
|
1973
|
+
const isSource = ctx.callCount === 0;
|
|
1974
|
+
const better = !best || (isSource !== best.isSource ? isSource : ctx.outboundVolume !== best.ctx.outboundVolume ? ctx.outboundVolume > best.ctx.outboundVolume : nid < best.node);
|
|
1975
|
+
if (better) best = { node: nid, ctx, isSource };
|
|
1976
|
+
}
|
|
1977
|
+
return best ? { node: best.node, ctx: best.ctx } : null;
|
|
1978
|
+
}
|
|
1979
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents, opts) {
|
|
1980
|
+
const legacy = legacyRootCause(graph, errorNodeId, errorEvent, incidents);
|
|
1981
|
+
if (!legacy) return null;
|
|
1982
|
+
const navigation = opts?.navigation ?? process.env.NEAT_RCA_NAVIGATION !== "0";
|
|
1983
|
+
if (!navigation) return legacy;
|
|
1984
|
+
return enrichWithNavigation(graph, errorNodeId, legacy, incidents, opts?.now ?? Date.now());
|
|
1985
|
+
}
|
|
1986
|
+
function enrichWithNavigation(graph, errorNodeId, legacy, incidents, now) {
|
|
1987
|
+
const seedNode = legacy.rootCauseNode;
|
|
1988
|
+
const seedCtx = graph.hasNode(seedNode) ? nodeContext(graph, seedNode, incidents, now) : null;
|
|
1989
|
+
const lastProv = legacy.edgeProvenances[legacy.edgeProvenances.length - 1];
|
|
1990
|
+
const candidates = [];
|
|
1991
|
+
if (seedCtx && isVictimSeed(seedCtx)) {
|
|
1992
|
+
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
1993
|
+
if (origin) {
|
|
1994
|
+
const path84 = findPath(graph, errorNodeId, origin.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
1995
|
+
const originConfidence = confidenceFromMix(path84?.edges ?? [], now);
|
|
1996
|
+
candidates.push({
|
|
1997
|
+
node: origin.node,
|
|
1998
|
+
classification: "primary-failure",
|
|
1999
|
+
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.`,
|
|
2000
|
+
context: origin.ctx,
|
|
2001
|
+
confidence: Math.max(0.3, Math.min(0.8, originConfidence || 0.5)),
|
|
2002
|
+
provenance: import_types.Provenance.OBSERVED
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
const staleNote = seedCtx.stale ? "; the node has gone STALE" : "";
|
|
2006
|
+
const satNote = isSaturated(seedCtx) ? `; inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
2007
|
+
candidates.push({
|
|
2008
|
+
node: seedNode,
|
|
2009
|
+
classification: "symptom-only",
|
|
2010
|
+
reason: `Errors arrive from callers (${seedCtx.errorsFromCallers}) but none originate here${staleNote}${satNote} \u2014 a downstream victim of load, not the fault.`,
|
|
2011
|
+
context: seedCtx,
|
|
2012
|
+
confidence: Math.min(legacy.confidence, 0.4),
|
|
2013
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
2014
|
+
});
|
|
2015
|
+
} else {
|
|
2016
|
+
candidates.push({
|
|
2017
|
+
node: seedNode,
|
|
2018
|
+
classification: "primary-failure",
|
|
2019
|
+
reason: legacy.rootCauseReason,
|
|
2020
|
+
context: seedCtx ?? EMPTY_CONTEXT,
|
|
2021
|
+
confidence: legacy.confidence,
|
|
2022
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
const top = candidates[0];
|
|
2026
|
+
let traversalPath = legacy.traversalPath;
|
|
2027
|
+
let edgeProvenances = legacy.edgeProvenances;
|
|
2028
|
+
if (top.node !== seedNode) {
|
|
2029
|
+
const path84 = findPath(graph, errorNodeId, top.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
2030
|
+
if (path84) {
|
|
2031
|
+
traversalPath = path84.nodes;
|
|
2032
|
+
edgeProvenances = path84.edges.map((e) => e.provenance);
|
|
2033
|
+
} else {
|
|
2034
|
+
traversalPath = [errorNodeId, top.node];
|
|
2035
|
+
edgeProvenances = [top.provenance ?? import_types.Provenance.OBSERVED];
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
return import_types.RootCauseResultSchema.parse({
|
|
2039
|
+
rootCauseNode: top.node,
|
|
2040
|
+
rootCauseReason: top.reason,
|
|
2041
|
+
traversalPath,
|
|
2042
|
+
edgeProvenances,
|
|
2043
|
+
confidence: top.confidence,
|
|
2044
|
+
...legacy.fixRecommendation ? { fixRecommendation: legacy.fixRecommendation } : {},
|
|
2045
|
+
candidates
|
|
1752
2046
|
});
|
|
1753
2047
|
}
|
|
1754
2048
|
|
|
@@ -4643,6 +4937,63 @@ function columnIsObserved(col) {
|
|
|
4643
4937
|
return col.provenances.includes(import_types7.Provenance.OBSERVED);
|
|
4644
4938
|
}
|
|
4645
4939
|
|
|
4940
|
+
// src/latency-digest.ts
|
|
4941
|
+
init_cjs_shims();
|
|
4942
|
+
var SUB = 16;
|
|
4943
|
+
var MIN_EXP = -10;
|
|
4944
|
+
var MAX_EXP = 22;
|
|
4945
|
+
var OCTAVES = MAX_EXP - MIN_EXP + 1;
|
|
4946
|
+
var OVERFLOW_INDEX = OCTAVES * SUB + 1;
|
|
4947
|
+
function latencyBucketIndex(ms) {
|
|
4948
|
+
if (!Number.isFinite(ms) || ms <= 0) return 0;
|
|
4949
|
+
const e = Math.floor(Math.log2(ms));
|
|
4950
|
+
if (e < MIN_EXP) return 0;
|
|
4951
|
+
if (e > MAX_EXP) return OVERFLOW_INDEX;
|
|
4952
|
+
const base = 2 ** e;
|
|
4953
|
+
const raw = Math.floor((ms / base - 1) * SUB);
|
|
4954
|
+
const s = raw < 0 ? 0 : raw >= SUB ? SUB - 1 : raw;
|
|
4955
|
+
return (e - MIN_EXP) * SUB + s + 1;
|
|
4956
|
+
}
|
|
4957
|
+
function bucketRepresentativeMs(index) {
|
|
4958
|
+
if (index <= 0) return 0;
|
|
4959
|
+
if (index >= OVERFLOW_INDEX) return 2 ** (MAX_EXP + 1);
|
|
4960
|
+
const zeroBased = index - 1;
|
|
4961
|
+
const e = MIN_EXP + Math.floor(zeroBased / SUB);
|
|
4962
|
+
const s = zeroBased % SUB;
|
|
4963
|
+
const base = 2 ** e;
|
|
4964
|
+
const lower = base * (1 + s / SUB);
|
|
4965
|
+
const upper = base * (1 + (s + 1) / SUB);
|
|
4966
|
+
return (lower + upper) / 2;
|
|
4967
|
+
}
|
|
4968
|
+
function recordLatency(hist, ms) {
|
|
4969
|
+
const key = String(latencyBucketIndex(ms));
|
|
4970
|
+
hist[key] = (hist[key] ?? 0) + 1;
|
|
4971
|
+
return hist;
|
|
4972
|
+
}
|
|
4973
|
+
function quantile(hist, q) {
|
|
4974
|
+
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]);
|
|
4975
|
+
let total = 0;
|
|
4976
|
+
for (const [, c] of entries) total += c;
|
|
4977
|
+
if (total === 0) return 0;
|
|
4978
|
+
const rank = Math.max(1, Math.ceil(q * total));
|
|
4979
|
+
let cumulative = 0;
|
|
4980
|
+
for (const [idx, c] of entries) {
|
|
4981
|
+
cumulative += c;
|
|
4982
|
+
if (cumulative >= rank) return bucketRepresentativeMs(idx);
|
|
4983
|
+
}
|
|
4984
|
+
return bucketRepresentativeMs(entries[entries.length - 1][0]);
|
|
4985
|
+
}
|
|
4986
|
+
function round(ms) {
|
|
4987
|
+
return Math.round(ms * 100) / 100;
|
|
4988
|
+
}
|
|
4989
|
+
function latencyPercentiles(hist) {
|
|
4990
|
+
if (!hist) return void 0;
|
|
4991
|
+
let total = 0;
|
|
4992
|
+
for (const c of Object.values(hist)) total += c;
|
|
4993
|
+
if (total === 0) return void 0;
|
|
4994
|
+
return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
|
|
4995
|
+
}
|
|
4996
|
+
|
|
4646
4997
|
// src/ingest.ts
|
|
4647
4998
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
4648
4999
|
var DAY_MS = 24 * HOUR_MS;
|
|
@@ -5338,7 +5689,7 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
5338
5689
|
graph.addNode(id, node);
|
|
5339
5690
|
return id;
|
|
5340
5691
|
}
|
|
5341
|
-
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
|
|
5692
|
+
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence, durationMs) {
|
|
5342
5693
|
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
5343
5694
|
const grain = source.startsWith("file:") || source.startsWith("symbol:") ? "file" : "service";
|
|
5344
5695
|
const id = makeObservedEdgeId(type, source, target);
|
|
@@ -5346,10 +5697,15 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
5346
5697
|
const existing = graph.getEdgeAttributes(id);
|
|
5347
5698
|
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
5348
5699
|
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
5700
|
+
const latencyHist2 = durationMs !== void 0 ? recordLatency({ ...existing.signal?.latencyHist ?? {} }, durationMs) : existing.signal?.latencyHist;
|
|
5701
|
+
const latencyMs2 = latencyPercentiles(latencyHist2) ?? existing.signal?.latencyMs;
|
|
5349
5702
|
const newSignal = {
|
|
5350
5703
|
spanCount: newSpanCount,
|
|
5351
5704
|
errorCount: newErrorCount,
|
|
5352
|
-
lastObservedAgeMs: 0
|
|
5705
|
+
lastObservedAgeMs: 0,
|
|
5706
|
+
...latencyHist2 ? { latencyHist: latencyHist2 } : {},
|
|
5707
|
+
...latencyMs2 ? { latencyMs: latencyMs2 } : {},
|
|
5708
|
+
...existing.signal?.anomalous !== void 0 ? { anomalous: existing.signal.anomalous } : {}
|
|
5353
5709
|
};
|
|
5354
5710
|
const updated = {
|
|
5355
5711
|
...existing,
|
|
@@ -5364,10 +5720,14 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
5364
5720
|
graph.replaceEdgeAttributes(id, updated);
|
|
5365
5721
|
return { edge: updated, created: false };
|
|
5366
5722
|
}
|
|
5723
|
+
const latencyHist = durationMs !== void 0 ? recordLatency({}, durationMs) : void 0;
|
|
5724
|
+
const latencyMs = latencyHist ? latencyPercentiles(latencyHist) : void 0;
|
|
5367
5725
|
const signal = {
|
|
5368
5726
|
spanCount: 1,
|
|
5369
5727
|
errorCount: isError ? 1 : 0,
|
|
5370
|
-
lastObservedAgeMs: 0
|
|
5728
|
+
lastObservedAgeMs: 0,
|
|
5729
|
+
...latencyHist ? { latencyHist } : {},
|
|
5730
|
+
...latencyMs ? { latencyMs } : {}
|
|
5371
5731
|
};
|
|
5372
5732
|
const edge = {
|
|
5373
5733
|
id,
|
|
@@ -5592,6 +5952,7 @@ async function handleSpan(ctx, span) {
|
|
|
5592
5952
|
}
|
|
5593
5953
|
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
5594
5954
|
const isError = span.statusCode === 2;
|
|
5955
|
+
const durationMs = span.durationNanos > 0n ? Number(span.durationNanos) / 1e6 : void 0;
|
|
5595
5956
|
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
5596
5957
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
5597
5958
|
cacheSpanService(span, nowMs, callSite);
|
|
@@ -5630,7 +5991,8 @@ async function handleSpan(ctx, span) {
|
|
|
5630
5991
|
targetId,
|
|
5631
5992
|
ts,
|
|
5632
5993
|
isError,
|
|
5633
|
-
callSiteEvidence
|
|
5994
|
+
callSiteEvidence,
|
|
5995
|
+
durationMs
|
|
5634
5996
|
);
|
|
5635
5997
|
if (result) affectedNode = targetId;
|
|
5636
5998
|
if (span.dbSystem === "mongodb" && span.dbCollection) {
|
|
@@ -5642,7 +6004,8 @@ async function handleSpan(ctx, span) {
|
|
|
5642
6004
|
collectionId,
|
|
5643
6005
|
ts,
|
|
5644
6006
|
isError,
|
|
5645
|
-
callSiteEvidence
|
|
6007
|
+
callSiteEvidence,
|
|
6008
|
+
durationMs
|
|
5646
6009
|
);
|
|
5647
6010
|
}
|
|
5648
6011
|
if (span.dbTable) {
|
|
@@ -5654,7 +6017,8 @@ async function handleSpan(ctx, span) {
|
|
|
5654
6017
|
tableId,
|
|
5655
6018
|
ts,
|
|
5656
6019
|
isError,
|
|
5657
|
-
callSiteEvidence
|
|
6020
|
+
callSiteEvidence,
|
|
6021
|
+
durationMs
|
|
5658
6022
|
);
|
|
5659
6023
|
mergeObservedColumns(ctx.graph, tableId, span.dbColumns);
|
|
5660
6024
|
}
|
|
@@ -5673,7 +6037,8 @@ async function handleSpan(ctx, span) {
|
|
|
5673
6037
|
targetId,
|
|
5674
6038
|
ts,
|
|
5675
6039
|
isError,
|
|
5676
|
-
callSiteEvidence
|
|
6040
|
+
callSiteEvidence,
|
|
6041
|
+
durationMs
|
|
5677
6042
|
);
|
|
5678
6043
|
if (result) affectedNode = targetId;
|
|
5679
6044
|
} else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
|
|
@@ -5690,7 +6055,8 @@ async function handleSpan(ctx, span) {
|
|
|
5690
6055
|
targetId,
|
|
5691
6056
|
ts,
|
|
5692
6057
|
isError,
|
|
5693
|
-
callSiteEvidence
|
|
6058
|
+
callSiteEvidence,
|
|
6059
|
+
durationMs
|
|
5694
6060
|
);
|
|
5695
6061
|
if (result) affectedNode = targetId;
|
|
5696
6062
|
} else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
|
|
@@ -5702,7 +6068,8 @@ async function handleSpan(ctx, span) {
|
|
|
5702
6068
|
targetId,
|
|
5703
6069
|
ts,
|
|
5704
6070
|
isError,
|
|
5705
|
-
callSiteEvidence
|
|
6071
|
+
callSiteEvidence,
|
|
6072
|
+
durationMs
|
|
5706
6073
|
);
|
|
5707
6074
|
if (result) affectedNode = targetId;
|
|
5708
6075
|
} else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
|
|
@@ -5718,7 +6085,8 @@ async function handleSpan(ctx, span) {
|
|
|
5718
6085
|
targetId,
|
|
5719
6086
|
ts,
|
|
5720
6087
|
isError,
|
|
5721
|
-
callSiteEvidence
|
|
6088
|
+
callSiteEvidence,
|
|
6089
|
+
durationMs
|
|
5722
6090
|
);
|
|
5723
6091
|
if (result) affectedNode = targetId;
|
|
5724
6092
|
} else {
|
|
@@ -5734,7 +6102,8 @@ async function handleSpan(ctx, span) {
|
|
|
5734
6102
|
targetId,
|
|
5735
6103
|
ts,
|
|
5736
6104
|
isError,
|
|
5737
|
-
callSiteEvidence
|
|
6105
|
+
callSiteEvidence,
|
|
6106
|
+
durationMs
|
|
5738
6107
|
);
|
|
5739
6108
|
affectedNode = targetId;
|
|
5740
6109
|
resolvedViaAddress = true;
|
|
@@ -5747,7 +6116,8 @@ async function handleSpan(ctx, span) {
|
|
|
5747
6116
|
frontierNodeId,
|
|
5748
6117
|
ts,
|
|
5749
6118
|
isError,
|
|
5750
|
-
callSiteEvidence
|
|
6119
|
+
callSiteEvidence,
|
|
6120
|
+
durationMs
|
|
5751
6121
|
);
|
|
5752
6122
|
affectedNode = frontierNodeId;
|
|
5753
6123
|
resolvedViaAddress = true;
|
|
@@ -5773,7 +6143,8 @@ async function handleSpan(ctx, span) {
|
|
|
5773
6143
|
sourceId,
|
|
5774
6144
|
ts,
|
|
5775
6145
|
isError,
|
|
5776
|
-
fallbackEvidence
|
|
6146
|
+
fallbackEvidence,
|
|
6147
|
+
durationMs
|
|
5777
6148
|
);
|
|
5778
6149
|
}
|
|
5779
6150
|
}
|
|
@@ -5787,7 +6158,7 @@ async function handleSpan(ctx, span) {
|
|
|
5787
6158
|
);
|
|
5788
6159
|
if (routeNodeId) {
|
|
5789
6160
|
const routeSvc = ctx.graph.getNodeAttributes(routeNodeId).service;
|
|
5790
|
-
upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError);
|
|
6161
|
+
upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError, void 0, durationMs);
|
|
5791
6162
|
}
|
|
5792
6163
|
}
|
|
5793
6164
|
if (span.statusCode === 2) {
|
|
@@ -18919,6 +19290,34 @@ function registerRoutes(scope, ctx) {
|
|
|
18919
19290
|
}
|
|
18920
19291
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
18921
19292
|
});
|
|
19293
|
+
scope.get("/graph/expand/:nodeId", async (req, reply) => {
|
|
19294
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
19295
|
+
if (!proj) return;
|
|
19296
|
+
const { nodeId } = req.params;
|
|
19297
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
19298
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
19299
|
+
}
|
|
19300
|
+
const direction = req.query.direction;
|
|
19301
|
+
if (direction !== "up" && direction !== "down") {
|
|
19302
|
+
return reply.code(400).send({ error: 'direction must be "up" or "down"' });
|
|
19303
|
+
}
|
|
19304
|
+
const epath = errorsPathFor(proj);
|
|
19305
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
19306
|
+
return expandNode(proj.graph, nodeId, direction, incidents);
|
|
19307
|
+
});
|
|
19308
|
+
scope.get("/graph/relate", async (req, reply) => {
|
|
19309
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
19310
|
+
if (!proj) return;
|
|
19311
|
+
const { a, b } = req.query;
|
|
19312
|
+
if (!a || !b) {
|
|
19313
|
+
return reply.code(400).send({ error: "both a and b query params are required" });
|
|
19314
|
+
}
|
|
19315
|
+
const maxDepth = req.query.maxDepth ? Number(req.query.maxDepth) : void 0;
|
|
19316
|
+
if (maxDepth !== void 0 && (!Number.isFinite(maxDepth) || maxDepth < 1)) {
|
|
19317
|
+
return reply.code(400).send({ error: "maxDepth must be a positive integer" });
|
|
19318
|
+
}
|
|
19319
|
+
return relate(proj.graph, a, b, maxDepth);
|
|
19320
|
+
});
|
|
18922
19321
|
scope.get("/search", async (req, reply) => {
|
|
18923
19322
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18924
19323
|
if (!proj) return;
|