@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
|
@@ -3341,6 +3341,62 @@ function columnIsObserved(col) {
|
|
|
3341
3341
|
return col.provenances.includes(Provenance4.OBSERVED);
|
|
3342
3342
|
}
|
|
3343
3343
|
|
|
3344
|
+
// src/latency-digest.ts
|
|
3345
|
+
var SUB = 16;
|
|
3346
|
+
var MIN_EXP = -10;
|
|
3347
|
+
var MAX_EXP = 22;
|
|
3348
|
+
var OCTAVES = MAX_EXP - MIN_EXP + 1;
|
|
3349
|
+
var OVERFLOW_INDEX = OCTAVES * SUB + 1;
|
|
3350
|
+
function latencyBucketIndex(ms) {
|
|
3351
|
+
if (!Number.isFinite(ms) || ms <= 0) return 0;
|
|
3352
|
+
const e = Math.floor(Math.log2(ms));
|
|
3353
|
+
if (e < MIN_EXP) return 0;
|
|
3354
|
+
if (e > MAX_EXP) return OVERFLOW_INDEX;
|
|
3355
|
+
const base = 2 ** e;
|
|
3356
|
+
const raw = Math.floor((ms / base - 1) * SUB);
|
|
3357
|
+
const s = raw < 0 ? 0 : raw >= SUB ? SUB - 1 : raw;
|
|
3358
|
+
return (e - MIN_EXP) * SUB + s + 1;
|
|
3359
|
+
}
|
|
3360
|
+
function bucketRepresentativeMs(index) {
|
|
3361
|
+
if (index <= 0) return 0;
|
|
3362
|
+
if (index >= OVERFLOW_INDEX) return 2 ** (MAX_EXP + 1);
|
|
3363
|
+
const zeroBased = index - 1;
|
|
3364
|
+
const e = MIN_EXP + Math.floor(zeroBased / SUB);
|
|
3365
|
+
const s = zeroBased % SUB;
|
|
3366
|
+
const base = 2 ** e;
|
|
3367
|
+
const lower = base * (1 + s / SUB);
|
|
3368
|
+
const upper = base * (1 + (s + 1) / SUB);
|
|
3369
|
+
return (lower + upper) / 2;
|
|
3370
|
+
}
|
|
3371
|
+
function recordLatency(hist, ms) {
|
|
3372
|
+
const key = String(latencyBucketIndex(ms));
|
|
3373
|
+
hist[key] = (hist[key] ?? 0) + 1;
|
|
3374
|
+
return hist;
|
|
3375
|
+
}
|
|
3376
|
+
function quantile(hist, q) {
|
|
3377
|
+
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]);
|
|
3378
|
+
let total = 0;
|
|
3379
|
+
for (const [, c] of entries) total += c;
|
|
3380
|
+
if (total === 0) return 0;
|
|
3381
|
+
const rank = Math.max(1, Math.ceil(q * total));
|
|
3382
|
+
let cumulative = 0;
|
|
3383
|
+
for (const [idx, c] of entries) {
|
|
3384
|
+
cumulative += c;
|
|
3385
|
+
if (cumulative >= rank) return bucketRepresentativeMs(idx);
|
|
3386
|
+
}
|
|
3387
|
+
return bucketRepresentativeMs(entries[entries.length - 1][0]);
|
|
3388
|
+
}
|
|
3389
|
+
function round(ms) {
|
|
3390
|
+
return Math.round(ms * 100) / 100;
|
|
3391
|
+
}
|
|
3392
|
+
function latencyPercentiles(hist) {
|
|
3393
|
+
if (!hist) return void 0;
|
|
3394
|
+
let total = 0;
|
|
3395
|
+
for (const c of Object.values(hist)) total += c;
|
|
3396
|
+
if (total === 0) return void 0;
|
|
3397
|
+
return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3344
3400
|
// src/ingest.ts
|
|
3345
3401
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
3346
3402
|
var DAY_MS = 24 * HOUR_MS;
|
|
@@ -4036,7 +4092,7 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
4036
4092
|
graph.addNode(id, node);
|
|
4037
4093
|
return id;
|
|
4038
4094
|
}
|
|
4039
|
-
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
|
|
4095
|
+
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence, durationMs) {
|
|
4040
4096
|
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
4041
4097
|
const grain = source.startsWith("file:") || source.startsWith("symbol:") ? "file" : "service";
|
|
4042
4098
|
const id = makeObservedEdgeId(type, source, target);
|
|
@@ -4044,10 +4100,15 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
4044
4100
|
const existing = graph.getEdgeAttributes(id);
|
|
4045
4101
|
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
4046
4102
|
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
4103
|
+
const latencyHist2 = durationMs !== void 0 ? recordLatency({ ...existing.signal?.latencyHist ?? {} }, durationMs) : existing.signal?.latencyHist;
|
|
4104
|
+
const latencyMs2 = latencyPercentiles(latencyHist2) ?? existing.signal?.latencyMs;
|
|
4047
4105
|
const newSignal = {
|
|
4048
4106
|
spanCount: newSpanCount,
|
|
4049
4107
|
errorCount: newErrorCount,
|
|
4050
|
-
lastObservedAgeMs: 0
|
|
4108
|
+
lastObservedAgeMs: 0,
|
|
4109
|
+
...latencyHist2 ? { latencyHist: latencyHist2 } : {},
|
|
4110
|
+
...latencyMs2 ? { latencyMs: latencyMs2 } : {},
|
|
4111
|
+
...existing.signal?.anomalous !== void 0 ? { anomalous: existing.signal.anomalous } : {}
|
|
4051
4112
|
};
|
|
4052
4113
|
const updated = {
|
|
4053
4114
|
...existing,
|
|
@@ -4062,10 +4123,14 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
4062
4123
|
graph.replaceEdgeAttributes(id, updated);
|
|
4063
4124
|
return { edge: updated, created: false };
|
|
4064
4125
|
}
|
|
4126
|
+
const latencyHist = durationMs !== void 0 ? recordLatency({}, durationMs) : void 0;
|
|
4127
|
+
const latencyMs = latencyHist ? latencyPercentiles(latencyHist) : void 0;
|
|
4065
4128
|
const signal = {
|
|
4066
4129
|
spanCount: 1,
|
|
4067
4130
|
errorCount: isError ? 1 : 0,
|
|
4068
|
-
lastObservedAgeMs: 0
|
|
4131
|
+
lastObservedAgeMs: 0,
|
|
4132
|
+
...latencyHist ? { latencyHist } : {},
|
|
4133
|
+
...latencyMs ? { latencyMs } : {}
|
|
4069
4134
|
};
|
|
4070
4135
|
const edge = {
|
|
4071
4136
|
id,
|
|
@@ -4290,6 +4355,7 @@ async function handleSpan(ctx, span) {
|
|
|
4290
4355
|
}
|
|
4291
4356
|
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
4292
4357
|
const isError = span.statusCode === 2;
|
|
4358
|
+
const durationMs = span.durationNanos > 0n ? Number(span.durationNanos) / 1e6 : void 0;
|
|
4293
4359
|
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
4294
4360
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
4295
4361
|
cacheSpanService(span, nowMs, callSite);
|
|
@@ -4328,7 +4394,8 @@ async function handleSpan(ctx, span) {
|
|
|
4328
4394
|
targetId,
|
|
4329
4395
|
ts,
|
|
4330
4396
|
isError,
|
|
4331
|
-
callSiteEvidence
|
|
4397
|
+
callSiteEvidence,
|
|
4398
|
+
durationMs
|
|
4332
4399
|
);
|
|
4333
4400
|
if (result) affectedNode = targetId;
|
|
4334
4401
|
if (span.dbSystem === "mongodb" && span.dbCollection) {
|
|
@@ -4340,7 +4407,8 @@ async function handleSpan(ctx, span) {
|
|
|
4340
4407
|
collectionId,
|
|
4341
4408
|
ts,
|
|
4342
4409
|
isError,
|
|
4343
|
-
callSiteEvidence
|
|
4410
|
+
callSiteEvidence,
|
|
4411
|
+
durationMs
|
|
4344
4412
|
);
|
|
4345
4413
|
}
|
|
4346
4414
|
if (span.dbTable) {
|
|
@@ -4352,7 +4420,8 @@ async function handleSpan(ctx, span) {
|
|
|
4352
4420
|
tableId,
|
|
4353
4421
|
ts,
|
|
4354
4422
|
isError,
|
|
4355
|
-
callSiteEvidence
|
|
4423
|
+
callSiteEvidence,
|
|
4424
|
+
durationMs
|
|
4356
4425
|
);
|
|
4357
4426
|
mergeObservedColumns(ctx.graph, tableId, span.dbColumns);
|
|
4358
4427
|
}
|
|
@@ -4371,7 +4440,8 @@ async function handleSpan(ctx, span) {
|
|
|
4371
4440
|
targetId,
|
|
4372
4441
|
ts,
|
|
4373
4442
|
isError,
|
|
4374
|
-
callSiteEvidence
|
|
4443
|
+
callSiteEvidence,
|
|
4444
|
+
durationMs
|
|
4375
4445
|
);
|
|
4376
4446
|
if (result) affectedNode = targetId;
|
|
4377
4447
|
} else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
|
|
@@ -4388,7 +4458,8 @@ async function handleSpan(ctx, span) {
|
|
|
4388
4458
|
targetId,
|
|
4389
4459
|
ts,
|
|
4390
4460
|
isError,
|
|
4391
|
-
callSiteEvidence
|
|
4461
|
+
callSiteEvidence,
|
|
4462
|
+
durationMs
|
|
4392
4463
|
);
|
|
4393
4464
|
if (result) affectedNode = targetId;
|
|
4394
4465
|
} else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
|
|
@@ -4400,7 +4471,8 @@ async function handleSpan(ctx, span) {
|
|
|
4400
4471
|
targetId,
|
|
4401
4472
|
ts,
|
|
4402
4473
|
isError,
|
|
4403
|
-
callSiteEvidence
|
|
4474
|
+
callSiteEvidence,
|
|
4475
|
+
durationMs
|
|
4404
4476
|
);
|
|
4405
4477
|
if (result) affectedNode = targetId;
|
|
4406
4478
|
} else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
|
|
@@ -4416,7 +4488,8 @@ async function handleSpan(ctx, span) {
|
|
|
4416
4488
|
targetId,
|
|
4417
4489
|
ts,
|
|
4418
4490
|
isError,
|
|
4419
|
-
callSiteEvidence
|
|
4491
|
+
callSiteEvidence,
|
|
4492
|
+
durationMs
|
|
4420
4493
|
);
|
|
4421
4494
|
if (result) affectedNode = targetId;
|
|
4422
4495
|
} else {
|
|
@@ -4432,7 +4505,8 @@ async function handleSpan(ctx, span) {
|
|
|
4432
4505
|
targetId,
|
|
4433
4506
|
ts,
|
|
4434
4507
|
isError,
|
|
4435
|
-
callSiteEvidence
|
|
4508
|
+
callSiteEvidence,
|
|
4509
|
+
durationMs
|
|
4436
4510
|
);
|
|
4437
4511
|
affectedNode = targetId;
|
|
4438
4512
|
resolvedViaAddress = true;
|
|
@@ -4445,7 +4519,8 @@ async function handleSpan(ctx, span) {
|
|
|
4445
4519
|
frontierNodeId,
|
|
4446
4520
|
ts,
|
|
4447
4521
|
isError,
|
|
4448
|
-
callSiteEvidence
|
|
4522
|
+
callSiteEvidence,
|
|
4523
|
+
durationMs
|
|
4449
4524
|
);
|
|
4450
4525
|
affectedNode = frontierNodeId;
|
|
4451
4526
|
resolvedViaAddress = true;
|
|
@@ -4471,7 +4546,8 @@ async function handleSpan(ctx, span) {
|
|
|
4471
4546
|
sourceId,
|
|
4472
4547
|
ts,
|
|
4473
4548
|
isError,
|
|
4474
|
-
fallbackEvidence
|
|
4549
|
+
fallbackEvidence,
|
|
4550
|
+
durationMs
|
|
4475
4551
|
);
|
|
4476
4552
|
}
|
|
4477
4553
|
}
|
|
@@ -4485,7 +4561,7 @@ async function handleSpan(ctx, span) {
|
|
|
4485
4561
|
);
|
|
4486
4562
|
if (routeNodeId) {
|
|
4487
4563
|
const routeSvc = ctx.graph.getNodeAttributes(routeNodeId).service;
|
|
4488
|
-
upsertObservedEdge(ctx.graph, EdgeType5.CONTAINS, serviceId(routeSvc), routeNodeId, ts, isError);
|
|
4564
|
+
upsertObservedEdge(ctx.graph, EdgeType5.CONTAINS, serviceId(routeSvc), routeNodeId, ts, isError, void 0, durationMs);
|
|
4489
4565
|
}
|
|
4490
4566
|
}
|
|
4491
4567
|
if (span.statusCode === 2) {
|
|
@@ -4774,10 +4850,12 @@ function mergeSnapshot(graph, snapshot) {
|
|
|
4774
4850
|
import {
|
|
4775
4851
|
BlastRadiusResultSchema,
|
|
4776
4852
|
EdgeType as EdgeType6,
|
|
4853
|
+
ExpandResultSchema,
|
|
4777
4854
|
NodeType as NodeType5,
|
|
4778
4855
|
ObservedDependenciesResultSchema,
|
|
4779
4856
|
PROV_RANK,
|
|
4780
4857
|
Provenance as Provenance6,
|
|
4858
|
+
RelateResultSchema,
|
|
4781
4859
|
RootCauseResultSchema,
|
|
4782
4860
|
TransitiveDependenciesResultSchema
|
|
4783
4861
|
} from "@neat.is/types";
|
|
@@ -4994,7 +5072,7 @@ var rootCauseShapes = {
|
|
|
4994
5072
|
[NodeType5.FileNode]: fileRootCauseShape,
|
|
4995
5073
|
[NodeType5.SymbolNode]: symbolRootCauseShape
|
|
4996
5074
|
};
|
|
4997
|
-
function
|
|
5075
|
+
function legacyRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
4998
5076
|
if (!graph.hasNode(errorNodeId)) return null;
|
|
4999
5077
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
5000
5078
|
const shape = rootCauseShapes[origin.type];
|
|
@@ -5251,7 +5329,9 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
5251
5329
|
dependencies: [],
|
|
5252
5330
|
observed: false,
|
|
5253
5331
|
inboundObservedCount: 0,
|
|
5254
|
-
hasExtractedOutbound: false
|
|
5332
|
+
hasExtractedOutbound: false,
|
|
5333
|
+
inboundVolume: 0,
|
|
5334
|
+
window: "lifetime"
|
|
5255
5335
|
});
|
|
5256
5336
|
}
|
|
5257
5337
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
@@ -5282,11 +5362,19 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
5282
5362
|
}
|
|
5283
5363
|
}
|
|
5284
5364
|
let inboundObservedCount = 0;
|
|
5365
|
+
let inboundVolume = 0;
|
|
5366
|
+
let inboundLastObserved;
|
|
5285
5367
|
for (const tgt of scope) {
|
|
5286
5368
|
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
5287
5369
|
const e = graph.getEdgeAttributes(edgeId);
|
|
5288
5370
|
if (e.type === EdgeType6.CONTAINS) continue;
|
|
5289
|
-
if (e.provenance === Provenance6.OBSERVED)
|
|
5371
|
+
if (e.provenance === Provenance6.OBSERVED) {
|
|
5372
|
+
inboundObservedCount += 1;
|
|
5373
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 1;
|
|
5374
|
+
if (e.lastObserved && (!inboundLastObserved || e.lastObserved > inboundLastObserved)) {
|
|
5375
|
+
inboundLastObserved = e.lastObserved;
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5290
5378
|
}
|
|
5291
5379
|
}
|
|
5292
5380
|
dependencies.sort(
|
|
@@ -5297,7 +5385,291 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
5297
5385
|
dependencies,
|
|
5298
5386
|
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
5299
5387
|
inboundObservedCount,
|
|
5300
|
-
hasExtractedOutbound
|
|
5388
|
+
hasExtractedOutbound,
|
|
5389
|
+
// The signal is cumulative, so the honest window label is "lifetime" (ADR-190).
|
|
5390
|
+
inboundVolume,
|
|
5391
|
+
window: "lifetime",
|
|
5392
|
+
...inboundLastObserved ? { inboundLastObserved } : {}
|
|
5393
|
+
});
|
|
5394
|
+
}
|
|
5395
|
+
var SATURATION_P95_MS = 1e3;
|
|
5396
|
+
function nodeScope(graph, nodeId) {
|
|
5397
|
+
const scope = [nodeId];
|
|
5398
|
+
if (!graph.hasNode(nodeId)) return scope;
|
|
5399
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
5400
|
+
if (attrs.type === NodeType5.ServiceNode) {
|
|
5401
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
5402
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
5403
|
+
if (e.type !== EdgeType6.CONTAINS) continue;
|
|
5404
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
5405
|
+
if (owned.type === NodeType5.FileNode) scope.push(e.target);
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
return scope;
|
|
5409
|
+
}
|
|
5410
|
+
function incidentCountForNode(nodeId, incidents) {
|
|
5411
|
+
if (!incidents || incidents.length === 0) return 0;
|
|
5412
|
+
return incidents.filter((ev) => incidentMatchesNode(ev, nodeId)).length;
|
|
5413
|
+
}
|
|
5414
|
+
function nodeContext(graph, nodeId, incidents, now = Date.now()) {
|
|
5415
|
+
const scope = nodeScope(graph, nodeId);
|
|
5416
|
+
let errorsFromCallers = 0;
|
|
5417
|
+
let inboundVolume = 0;
|
|
5418
|
+
let outboundVolume = 0;
|
|
5419
|
+
let outboundErrors = 0;
|
|
5420
|
+
let latestInboundMs;
|
|
5421
|
+
let latencyP95Ms;
|
|
5422
|
+
let stale = false;
|
|
5423
|
+
for (const n of scope) {
|
|
5424
|
+
if (!graph.hasNode(n)) continue;
|
|
5425
|
+
for (const edgeId of graph.inboundEdges(n)) {
|
|
5426
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
5427
|
+
if (e.type === EdgeType6.CONTAINS) continue;
|
|
5428
|
+
errorsFromCallers += e.signal?.errorCount ?? 0;
|
|
5429
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
5430
|
+
if (e.provenance === Provenance6.STALE) stale = true;
|
|
5431
|
+
const p95 = e.signal?.latencyMs?.p95;
|
|
5432
|
+
if (p95 !== void 0) latencyP95Ms = Math.max(latencyP95Ms ?? 0, p95);
|
|
5433
|
+
if (e.lastObserved) {
|
|
5434
|
+
const t = Date.parse(e.lastObserved);
|
|
5435
|
+
if (Number.isFinite(t)) latestInboundMs = Math.max(latestInboundMs ?? 0, t);
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
5438
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
5439
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
5440
|
+
if (e.type === EdgeType6.CONTAINS) continue;
|
|
5441
|
+
outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
5442
|
+
if (e.type === EdgeType6.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
|
|
5443
|
+
if (e.provenance === Provenance6.STALE) stale = true;
|
|
5444
|
+
}
|
|
5445
|
+
}
|
|
5446
|
+
const errorsEmittedHere = incidentCountForNode(nodeId, incidents) + outboundErrors;
|
|
5447
|
+
const lastObservedAgeMs = latestInboundMs !== void 0 ? Math.max(0, now - latestInboundMs) : void 0;
|
|
5448
|
+
return {
|
|
5449
|
+
errorsEmittedHere,
|
|
5450
|
+
errorsFromCallers,
|
|
5451
|
+
callCount: inboundVolume,
|
|
5452
|
+
outboundVolume,
|
|
5453
|
+
...lastObservedAgeMs !== void 0 ? { lastObservedAgeMs } : {},
|
|
5454
|
+
...latencyP95Ms !== void 0 ? { latencyP95Ms } : {},
|
|
5455
|
+
stale
|
|
5456
|
+
};
|
|
5457
|
+
}
|
|
5458
|
+
function isSaturated(ctx) {
|
|
5459
|
+
return ctx.latencyP95Ms !== void 0 && ctx.latencyP95Ms >= SATURATION_P95_MS;
|
|
5460
|
+
}
|
|
5461
|
+
function classifyNode(ctx) {
|
|
5462
|
+
if (ctx.errorsEmittedHere > 0) {
|
|
5463
|
+
if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
|
|
5464
|
+
return "symptom-only";
|
|
5465
|
+
}
|
|
5466
|
+
return "primary-failure";
|
|
5467
|
+
}
|
|
5468
|
+
if (ctx.errorsFromCallers > 0) return "symptom-only";
|
|
5469
|
+
return "unrelated";
|
|
5470
|
+
}
|
|
5471
|
+
function isVictimSeed(ctx) {
|
|
5472
|
+
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
5473
|
+
}
|
|
5474
|
+
function grainOf(graph, nodeId) {
|
|
5475
|
+
if (!graph.hasNode(nodeId)) return "unknown";
|
|
5476
|
+
const t = graph.getNodeAttributes(nodeId).type;
|
|
5477
|
+
if (t === NodeType5.ServiceNode) return "service";
|
|
5478
|
+
if (t === NodeType5.FileNode) return "file";
|
|
5479
|
+
if (t === NodeType5.SymbolNode) return "symbol";
|
|
5480
|
+
return t;
|
|
5481
|
+
}
|
|
5482
|
+
function findPath(graph, from, to, direction, maxDepth) {
|
|
5483
|
+
if (!graph.hasNode(from) || !graph.hasNode(to)) return null;
|
|
5484
|
+
if (from === to) return { nodes: [from], edges: [] };
|
|
5485
|
+
const queue = [{ nodeId: from, depth: 0, nodes: [from], edges: [] }];
|
|
5486
|
+
const enqueued = /* @__PURE__ */ new Set([from]);
|
|
5487
|
+
while (queue.length > 0) {
|
|
5488
|
+
const frame = queue.shift();
|
|
5489
|
+
if (frame.depth >= maxDepth) continue;
|
|
5490
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
|
|
5491
|
+
const neighbours = [...best.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
5492
|
+
for (const [nid, edge] of neighbours) {
|
|
5493
|
+
if (nid === to) return { nodes: [...frame.nodes, nid], edges: [...frame.edges, edge] };
|
|
5494
|
+
if (enqueued.has(nid)) continue;
|
|
5495
|
+
enqueued.add(nid);
|
|
5496
|
+
queue.push({
|
|
5497
|
+
nodeId: nid,
|
|
5498
|
+
depth: frame.depth + 1,
|
|
5499
|
+
nodes: [...frame.nodes, nid],
|
|
5500
|
+
edges: [...frame.edges, edge]
|
|
5501
|
+
});
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
return null;
|
|
5505
|
+
}
|
|
5506
|
+
var EMPTY_CONTEXT = {
|
|
5507
|
+
errorsEmittedHere: 0,
|
|
5508
|
+
errorsFromCallers: 0,
|
|
5509
|
+
callCount: 0,
|
|
5510
|
+
outboundVolume: 0,
|
|
5511
|
+
stale: false
|
|
5512
|
+
};
|
|
5513
|
+
function expandNode(graph, nodeId, direction, incidents, now = Date.now()) {
|
|
5514
|
+
if (!graph.hasNode(nodeId)) {
|
|
5515
|
+
return ExpandResultSchema.parse({
|
|
5516
|
+
origin: nodeId,
|
|
5517
|
+
direction,
|
|
5518
|
+
node: { id: nodeId, classification: "unrelated", context: EMPTY_CONTEXT },
|
|
5519
|
+
neighbours: []
|
|
5520
|
+
});
|
|
5521
|
+
}
|
|
5522
|
+
const ctx = nodeContext(graph, nodeId, incidents, now);
|
|
5523
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(nodeId));
|
|
5524
|
+
const neighbours = [];
|
|
5525
|
+
for (const [nid, edge] of best) {
|
|
5526
|
+
if (edge.type === EdgeType6.CONTAINS) continue;
|
|
5527
|
+
const nctx = nodeContext(graph, nid, incidents, now);
|
|
5528
|
+
neighbours.push({
|
|
5529
|
+
node: nid,
|
|
5530
|
+
edgeType: edge.type,
|
|
5531
|
+
provenance: edge.provenance,
|
|
5532
|
+
classification: classifyNode(nctx),
|
|
5533
|
+
context: nctx
|
|
5534
|
+
});
|
|
5535
|
+
}
|
|
5536
|
+
neighbours.sort((a, b) => a.node.localeCompare(b.node));
|
|
5537
|
+
return ExpandResultSchema.parse({
|
|
5538
|
+
origin: nodeId,
|
|
5539
|
+
direction,
|
|
5540
|
+
node: { id: nodeId, classification: classifyNode(ctx), context: ctx },
|
|
5541
|
+
neighbours
|
|
5542
|
+
});
|
|
5543
|
+
}
|
|
5544
|
+
function relate(graph, a, b, maxDepth = ROOT_CAUSE_MAX_DEPTH) {
|
|
5545
|
+
const buildPath = (fp) => ({
|
|
5546
|
+
nodes: fp.nodes,
|
|
5547
|
+
edgeTypes: fp.edges.map((e) => e.type),
|
|
5548
|
+
provenance: fp.edges.map((e) => e.provenance),
|
|
5549
|
+
grain: fp.nodes.map((n) => grainOf(graph, n)),
|
|
5550
|
+
// The failure runs end to end when every hop carries error / latency / alert
|
|
5551
|
+
// signal — that is what turns reachability into cause-confirmation.
|
|
5552
|
+
carriesSignal: fp.edges.length > 0 && fp.edges.every(
|
|
5553
|
+
(e) => (e.signal?.errorCount ?? 0) > 0 || e.signal?.latencyMs !== void 0 || e.signal?.anomalous !== void 0
|
|
5554
|
+
)
|
|
5555
|
+
});
|
|
5556
|
+
if (!graph.hasNode(a) || !graph.hasNode(b)) {
|
|
5557
|
+
return RelateResultSchema.parse({
|
|
5558
|
+
a,
|
|
5559
|
+
b,
|
|
5560
|
+
related: false,
|
|
5561
|
+
direction: null,
|
|
5562
|
+
paths: [],
|
|
5563
|
+
note: !graph.hasNode(a) ? `node not found: ${a}` : `node not found: ${b}`
|
|
5564
|
+
});
|
|
5565
|
+
}
|
|
5566
|
+
const down = findPath(graph, a, b, "down", maxDepth);
|
|
5567
|
+
const up = findPath(graph, a, b, "up", maxDepth);
|
|
5568
|
+
if (!down && !up) {
|
|
5569
|
+
return RelateResultSchema.parse({
|
|
5570
|
+
a,
|
|
5571
|
+
b,
|
|
5572
|
+
related: false,
|
|
5573
|
+
direction: null,
|
|
5574
|
+
paths: [],
|
|
5575
|
+
note: `no path within ${maxDepth} hops`
|
|
5576
|
+
});
|
|
5577
|
+
}
|
|
5578
|
+
const paths = [];
|
|
5579
|
+
const direction = down ? "a->b" : "b->a";
|
|
5580
|
+
if (down) paths.push(buildPath(down));
|
|
5581
|
+
if (up) paths.push(buildPath(up));
|
|
5582
|
+
const endpointsFine = grainOf(graph, a) !== "service" && grainOf(graph, b) !== "service";
|
|
5583
|
+
const grainGap = endpointsFine && paths[0].grain.some((g) => g === "service");
|
|
5584
|
+
return RelateResultSchema.parse({
|
|
5585
|
+
a,
|
|
5586
|
+
b,
|
|
5587
|
+
related: true,
|
|
5588
|
+
direction,
|
|
5589
|
+
paths,
|
|
5590
|
+
...grainGap ? { grainGap: true } : {}
|
|
5591
|
+
});
|
|
5592
|
+
}
|
|
5593
|
+
function findLoadOrigin(graph, alertNodeId, incidents, now) {
|
|
5594
|
+
const upstream = getBlastRadius(graph, alertNodeId).affectedNodes.map((n) => n.nodeId);
|
|
5595
|
+
let best = null;
|
|
5596
|
+
for (const nid of upstream) {
|
|
5597
|
+
if (nid === alertNodeId) continue;
|
|
5598
|
+
const ctx = nodeContext(graph, nid, incidents, now);
|
|
5599
|
+
if (ctx.outboundVolume === 0) continue;
|
|
5600
|
+
const isSource = ctx.callCount === 0;
|
|
5601
|
+
const better = !best || (isSource !== best.isSource ? isSource : ctx.outboundVolume !== best.ctx.outboundVolume ? ctx.outboundVolume > best.ctx.outboundVolume : nid < best.node);
|
|
5602
|
+
if (better) best = { node: nid, ctx, isSource };
|
|
5603
|
+
}
|
|
5604
|
+
return best ? { node: best.node, ctx: best.ctx } : null;
|
|
5605
|
+
}
|
|
5606
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents, opts) {
|
|
5607
|
+
const legacy = legacyRootCause(graph, errorNodeId, errorEvent, incidents);
|
|
5608
|
+
if (!legacy) return null;
|
|
5609
|
+
const navigation = opts?.navigation ?? process.env.NEAT_RCA_NAVIGATION !== "0";
|
|
5610
|
+
if (!navigation) return legacy;
|
|
5611
|
+
return enrichWithNavigation(graph, errorNodeId, legacy, incidents, opts?.now ?? Date.now());
|
|
5612
|
+
}
|
|
5613
|
+
function enrichWithNavigation(graph, errorNodeId, legacy, incidents, now) {
|
|
5614
|
+
const seedNode = legacy.rootCauseNode;
|
|
5615
|
+
const seedCtx = graph.hasNode(seedNode) ? nodeContext(graph, seedNode, incidents, now) : null;
|
|
5616
|
+
const lastProv = legacy.edgeProvenances[legacy.edgeProvenances.length - 1];
|
|
5617
|
+
const candidates = [];
|
|
5618
|
+
if (seedCtx && isVictimSeed(seedCtx)) {
|
|
5619
|
+
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
5620
|
+
if (origin) {
|
|
5621
|
+
const path64 = findPath(graph, errorNodeId, origin.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
5622
|
+
const originConfidence = confidenceFromMix(path64?.edges ?? [], now);
|
|
5623
|
+
candidates.push({
|
|
5624
|
+
node: origin.node,
|
|
5625
|
+
classification: "primary-failure",
|
|
5626
|
+
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.`,
|
|
5627
|
+
context: origin.ctx,
|
|
5628
|
+
confidence: Math.max(0.3, Math.min(0.8, originConfidence || 0.5)),
|
|
5629
|
+
provenance: Provenance6.OBSERVED
|
|
5630
|
+
});
|
|
5631
|
+
}
|
|
5632
|
+
const staleNote = seedCtx.stale ? "; the node has gone STALE" : "";
|
|
5633
|
+
const satNote = isSaturated(seedCtx) ? `; inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
5634
|
+
candidates.push({
|
|
5635
|
+
node: seedNode,
|
|
5636
|
+
classification: "symptom-only",
|
|
5637
|
+
reason: `Errors arrive from callers (${seedCtx.errorsFromCallers}) but none originate here${staleNote}${satNote} \u2014 a downstream victim of load, not the fault.`,
|
|
5638
|
+
context: seedCtx,
|
|
5639
|
+
confidence: Math.min(legacy.confidence, 0.4),
|
|
5640
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
5641
|
+
});
|
|
5642
|
+
} else {
|
|
5643
|
+
candidates.push({
|
|
5644
|
+
node: seedNode,
|
|
5645
|
+
classification: "primary-failure",
|
|
5646
|
+
reason: legacy.rootCauseReason,
|
|
5647
|
+
context: seedCtx ?? EMPTY_CONTEXT,
|
|
5648
|
+
confidence: legacy.confidence,
|
|
5649
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
5650
|
+
});
|
|
5651
|
+
}
|
|
5652
|
+
const top = candidates[0];
|
|
5653
|
+
let traversalPath = legacy.traversalPath;
|
|
5654
|
+
let edgeProvenances = legacy.edgeProvenances;
|
|
5655
|
+
if (top.node !== seedNode) {
|
|
5656
|
+
const path64 = findPath(graph, errorNodeId, top.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
5657
|
+
if (path64) {
|
|
5658
|
+
traversalPath = path64.nodes;
|
|
5659
|
+
edgeProvenances = path64.edges.map((e) => e.provenance);
|
|
5660
|
+
} else {
|
|
5661
|
+
traversalPath = [errorNodeId, top.node];
|
|
5662
|
+
edgeProvenances = [top.provenance ?? Provenance6.OBSERVED];
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
return RootCauseResultSchema.parse({
|
|
5666
|
+
rootCauseNode: top.node,
|
|
5667
|
+
rootCauseReason: top.reason,
|
|
5668
|
+
traversalPath,
|
|
5669
|
+
edgeProvenances,
|
|
5670
|
+
confidence: top.confidence,
|
|
5671
|
+
...legacy.fixRecommendation ? { fixRecommendation: legacy.fixRecommendation } : {},
|
|
5672
|
+
candidates
|
|
5301
5673
|
});
|
|
5302
5674
|
}
|
|
5303
5675
|
|
|
@@ -17956,6 +18328,34 @@ function registerRoutes(scope, ctx) {
|
|
|
17956
18328
|
}
|
|
17957
18329
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
17958
18330
|
});
|
|
18331
|
+
scope.get("/graph/expand/:nodeId", async (req, reply) => {
|
|
18332
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18333
|
+
if (!proj) return;
|
|
18334
|
+
const { nodeId } = req.params;
|
|
18335
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
18336
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
18337
|
+
}
|
|
18338
|
+
const direction = req.query.direction;
|
|
18339
|
+
if (direction !== "up" && direction !== "down") {
|
|
18340
|
+
return reply.code(400).send({ error: 'direction must be "up" or "down"' });
|
|
18341
|
+
}
|
|
18342
|
+
const epath = errorsPathFor(proj);
|
|
18343
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
18344
|
+
return expandNode(proj.graph, nodeId, direction, incidents);
|
|
18345
|
+
});
|
|
18346
|
+
scope.get("/graph/relate", async (req, reply) => {
|
|
18347
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18348
|
+
if (!proj) return;
|
|
18349
|
+
const { a, b } = req.query;
|
|
18350
|
+
if (!a || !b) {
|
|
18351
|
+
return reply.code(400).send({ error: "both a and b query params are required" });
|
|
18352
|
+
}
|
|
18353
|
+
const maxDepth = req.query.maxDepth ? Number(req.query.maxDepth) : void 0;
|
|
18354
|
+
if (maxDepth !== void 0 && (!Number.isFinite(maxDepth) || maxDepth < 1)) {
|
|
18355
|
+
return reply.code(400).send({ error: "maxDepth must be a positive integer" });
|
|
18356
|
+
}
|
|
18357
|
+
return relate(proj.graph, a, b, maxDepth);
|
|
18358
|
+
});
|
|
17959
18359
|
scope.get("/search", async (req, reply) => {
|
|
17960
18360
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
17961
18361
|
if (!proj) return;
|
|
@@ -18449,8 +18849,8 @@ export {
|
|
|
18449
18849
|
emitNeatEvent,
|
|
18450
18850
|
attachGraphToEventBus,
|
|
18451
18851
|
confidenceForEdge,
|
|
18452
|
-
getRootCause,
|
|
18453
18852
|
getBlastRadius,
|
|
18853
|
+
getRootCause,
|
|
18454
18854
|
evaluateAllPolicies,
|
|
18455
18855
|
loadPolicyFile,
|
|
18456
18856
|
PolicyViolationsLog,
|
|
@@ -18524,4 +18924,4 @@ export {
|
|
|
18524
18924
|
deprovisionConnector,
|
|
18525
18925
|
buildApi
|
|
18526
18926
|
};
|
|
18527
|
-
//# sourceMappingURL=chunk-
|
|
18927
|
+
//# sourceMappingURL=chunk-3PXRQH53.js.map
|