@neat.is/core 0.4.20-dev.20260630 → 0.4.23
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-MTXF77TN.js → chunk-GAFTW2OX.js} +2 -2
- package/dist/{chunk-62ELQVIP.js → chunk-IABNGQT2.js} +57 -9
- package/dist/chunk-IABNGQT2.js.map +1 -0
- package/dist/{chunk-GNAX2CBF.js → chunk-O25KZNZK.js} +878 -160
- package/dist/chunk-O25KZNZK.js.map +1 -0
- package/dist/{chunk-BGPWBRLU.js → chunk-ZX7PCMGZ.js} +21 -2
- package/dist/{chunk-BGPWBRLU.js.map → chunk-ZX7PCMGZ.js.map} +1 -1
- package/dist/cli.cjs +1251 -355
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +2 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +164 -60
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +903 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +947 -151
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.d.cts +17 -0
- package/dist/neatd.d.ts +17 -0
- package/dist/neatd.js +27 -3
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-I67ONCRM.js → otel-grpc-QYHUJ4OY.js} +3 -3
- package/dist/server.cjs +835 -122
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/proto/opentelemetry/proto/trace/v1/trace.proto +8 -2
- package/dist/chunk-62ELQVIP.js.map +0 -1
- package/dist/chunk-GNAX2CBF.js.map +0 -1
- /package/dist/{chunk-MTXF77TN.js.map → chunk-GAFTW2OX.js.map} +0 -0
- /package/dist/{otel-grpc-I67ONCRM.js.map → otel-grpc-QYHUJ4OY.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -41,13 +41,13 @@ var init_cjs_shims = __esm({
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
// src/auth.ts
|
|
44
|
-
function
|
|
44
|
+
function isLoopbackHost2(host) {
|
|
45
45
|
if (!host) return false;
|
|
46
46
|
return LOOPBACK_HOSTS.has(host);
|
|
47
47
|
}
|
|
48
48
|
function assertBindAuthority(host, token) {
|
|
49
49
|
if (token && token.length > 0) return;
|
|
50
|
-
if (
|
|
50
|
+
if (isLoopbackHost2(host)) return;
|
|
51
51
|
throw new BindAuthorityError(host);
|
|
52
52
|
}
|
|
53
53
|
function mountBearerAuth(app, opts) {
|
|
@@ -573,6 +573,22 @@ async function buildOtelReceiver(opts) {
|
|
|
573
573
|
};
|
|
574
574
|
return decorated;
|
|
575
575
|
}
|
|
576
|
+
async function listenSteppingOtlp(app, requestedPort, host) {
|
|
577
|
+
let port = requestedPort;
|
|
578
|
+
for (let attempt = 0; ; attempt++) {
|
|
579
|
+
try {
|
|
580
|
+
return await app.listen({ port, host });
|
|
581
|
+
} catch (err) {
|
|
582
|
+
const code = err.code;
|
|
583
|
+
const canStep = requestedPort !== 0 && code === "EADDRINUSE" && attempt < OTLP_STEP_ATTEMPTS - 1;
|
|
584
|
+
if (!canStep) throw err;
|
|
585
|
+
console.warn(
|
|
586
|
+
`otel: OTLP port ${port} is in use, stepping to ${port + OTLP_STEP_STRIDE}`
|
|
587
|
+
);
|
|
588
|
+
port += OTLP_STEP_STRIDE;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
576
592
|
function logSpanHandler(span) {
|
|
577
593
|
const parent = span.parentSpanId ? span.parentSpanId.slice(0, 8) : "<root>";
|
|
578
594
|
const status2 = span.statusCode === 2 ? "ERROR" : "OK";
|
|
@@ -581,7 +597,7 @@ function logSpanHandler(span) {
|
|
|
581
597
|
`otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
|
|
582
598
|
);
|
|
583
599
|
}
|
|
584
|
-
var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
600
|
+
var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody, OTLP_STEP_ATTEMPTS, OTLP_STEP_STRIDE;
|
|
585
601
|
var init_otel = __esm({
|
|
586
602
|
"src/otel.ts"() {
|
|
587
603
|
"use strict";
|
|
@@ -597,6 +613,8 @@ var init_otel = __esm({
|
|
|
597
613
|
exportTraceServiceRequestType = null;
|
|
598
614
|
exportTraceServiceResponseType = null;
|
|
599
615
|
cachedProtobufResponseBody = null;
|
|
616
|
+
OTLP_STEP_ATTEMPTS = 8;
|
|
617
|
+
OTLP_STEP_STRIDE = 1;
|
|
600
618
|
}
|
|
601
619
|
});
|
|
602
620
|
|
|
@@ -1272,22 +1290,164 @@ var rootCauseShapes = {
|
|
|
1272
1290
|
[import_types.NodeType.ServiceNode]: serviceRootCauseShape,
|
|
1273
1291
|
[import_types.NodeType.FileNode]: fileRootCauseShape
|
|
1274
1292
|
};
|
|
1275
|
-
function getRootCause(graph, errorNodeId, errorEvent) {
|
|
1293
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
1276
1294
|
if (!graph.hasNode(errorNodeId)) return null;
|
|
1277
1295
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
1278
1296
|
const shape = rootCauseShapes[origin.type];
|
|
1279
|
-
if (
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1297
|
+
if (shape) {
|
|
1298
|
+
const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
|
|
1299
|
+
const match = shape(graph, origin, walk);
|
|
1300
|
+
if (match) {
|
|
1301
|
+
const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
|
|
1302
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1303
|
+
rootCauseNode: match.rootCauseNode,
|
|
1304
|
+
rootCauseReason: reason,
|
|
1305
|
+
traversalPath: walk.path,
|
|
1306
|
+
edgeProvenances: walk.edges.map((e) => e.provenance),
|
|
1307
|
+
confidence: confidenceFromMix(walk.edges),
|
|
1308
|
+
fixRecommendation: match.fixRecommendation
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
if (origin.type === import_types.NodeType.ServiceNode) {
|
|
1313
|
+
const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
|
|
1314
|
+
if (crossService) return crossService;
|
|
1315
|
+
}
|
|
1316
|
+
return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
|
|
1317
|
+
}
|
|
1318
|
+
var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
|
|
1319
|
+
function incidentMatchesNode(ev, nodeId) {
|
|
1320
|
+
return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
|
|
1321
|
+
}
|
|
1322
|
+
function localizeFromIncidents(nodeId, incidents, errorEvent) {
|
|
1323
|
+
const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
|
|
1324
|
+
const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
|
|
1325
|
+
if (relevant.length === 0) return null;
|
|
1326
|
+
const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
|
|
1327
|
+
const attrs = latest.attributes ?? {};
|
|
1328
|
+
const filepath = typeof attrs["code.filepath"] === "string" ? attrs["code.filepath"] : void 0;
|
|
1329
|
+
const lineno = typeof attrs["code.lineno"] === "number" ? attrs["code.lineno"] : void 0;
|
|
1330
|
+
const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
|
|
1331
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
1332
|
+
const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
|
|
1333
|
+
const count = sameMode.length;
|
|
1334
|
+
const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
|
|
1335
|
+
const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
|
|
1336
|
+
if (location) reasonParts.push(`surfaced at ${location}`);
|
|
1337
|
+
const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
|
|
1338
|
+
const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
|
|
1339
|
+
const fileNode = localizesToFile ? latest.affectedNode : void 0;
|
|
1340
|
+
const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
|
|
1341
|
+
return {
|
|
1342
|
+
rootCauseNode: fileNode ?? nodeId,
|
|
1343
|
+
rootCauseReason,
|
|
1344
|
+
...fileNode ? { fileNode } : {},
|
|
1345
|
+
...fixRecommendation ? { fixRecommendation } : {}
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
1349
|
+
const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
|
|
1350
|
+
if (!loc) return null;
|
|
1351
|
+
const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
|
|
1352
|
+
const edgeProvenances = loc.fileNode ? [import_types.Provenance.OBSERVED] : [];
|
|
1284
1353
|
return import_types.RootCauseResultSchema.parse({
|
|
1285
|
-
rootCauseNode:
|
|
1286
|
-
rootCauseReason:
|
|
1287
|
-
traversalPath
|
|
1288
|
-
edgeProvenances
|
|
1289
|
-
confidence:
|
|
1290
|
-
fixRecommendation:
|
|
1354
|
+
rootCauseNode: loc.rootCauseNode,
|
|
1355
|
+
rootCauseReason: loc.rootCauseReason,
|
|
1356
|
+
traversalPath,
|
|
1357
|
+
edgeProvenances,
|
|
1358
|
+
confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
|
|
1359
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
function isFailingCallEdge(e) {
|
|
1363
|
+
return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
1364
|
+
}
|
|
1365
|
+
function callSourcesForService(graph, serviceId3) {
|
|
1366
|
+
const ids = [serviceId3];
|
|
1367
|
+
for (const edgeId of graph.outboundEdges(serviceId3)) {
|
|
1368
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1369
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1370
|
+
const tgt = graph.getNodeAttributes(e.target);
|
|
1371
|
+
if (tgt.type === import_types.NodeType.FileNode) ids.push(e.target);
|
|
1372
|
+
}
|
|
1373
|
+
return ids;
|
|
1374
|
+
}
|
|
1375
|
+
function failingCallDominates(e, id, curEdge, curId) {
|
|
1376
|
+
const ec = e.signal?.errorCount ?? 0;
|
|
1377
|
+
const cc = curEdge.signal?.errorCount ?? 0;
|
|
1378
|
+
if (ec !== cc) return ec > cc;
|
|
1379
|
+
if (import_types.PROV_RANK[e.provenance] !== import_types.PROV_RANK[curEdge.provenance]) {
|
|
1380
|
+
return import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[curEdge.provenance];
|
|
1381
|
+
}
|
|
1382
|
+
return id < curId;
|
|
1383
|
+
}
|
|
1384
|
+
function dominantFailingCall(graph, serviceId3, visited) {
|
|
1385
|
+
let best = null;
|
|
1386
|
+
for (const src of callSourcesForService(graph, serviceId3)) {
|
|
1387
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
1388
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1389
|
+
if (!isFailingCallEdge(e)) continue;
|
|
1390
|
+
if (isFrontierNode(graph, e.target)) continue;
|
|
1391
|
+
const owner = resolveOwningService(graph, e.target);
|
|
1392
|
+
if (!owner || visited.has(owner.id)) continue;
|
|
1393
|
+
if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
|
|
1394
|
+
best = { nextService: owner.id, edge: e };
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return best;
|
|
1399
|
+
}
|
|
1400
|
+
function followFailingCallChain(graph, originServiceId, maxDepth) {
|
|
1401
|
+
const path43 = [originServiceId];
|
|
1402
|
+
const edges = [];
|
|
1403
|
+
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
1404
|
+
let current = originServiceId;
|
|
1405
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
1406
|
+
const hop = dominantFailingCall(graph, current, visited);
|
|
1407
|
+
if (!hop) break;
|
|
1408
|
+
path43.push(hop.nextService);
|
|
1409
|
+
edges.push(hop.edge);
|
|
1410
|
+
visited.add(hop.nextService);
|
|
1411
|
+
current = hop.nextService;
|
|
1412
|
+
}
|
|
1413
|
+
if (edges.length === 0) return null;
|
|
1414
|
+
return { path: path43, edges, culprit: current };
|
|
1415
|
+
}
|
|
1416
|
+
function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
1417
|
+
const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
|
|
1418
|
+
if (!chain) return null;
|
|
1419
|
+
const culprit = chain.culprit;
|
|
1420
|
+
const path43 = [...chain.path];
|
|
1421
|
+
const edgeProvenances = chain.edges.map((e) => e.provenance);
|
|
1422
|
+
const baseConfidence = confidenceFromMix(chain.edges);
|
|
1423
|
+
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
1424
|
+
const loc = localizeFromIncidents(culprit, incidents, errorEvent);
|
|
1425
|
+
if (loc) {
|
|
1426
|
+
let rootCauseNode = culprit;
|
|
1427
|
+
if (loc.fileNode) {
|
|
1428
|
+
path43.push(loc.fileNode);
|
|
1429
|
+
edgeProvenances.push(import_types.Provenance.OBSERVED);
|
|
1430
|
+
rootCauseNode = loc.fileNode;
|
|
1431
|
+
}
|
|
1432
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1433
|
+
rootCauseNode,
|
|
1434
|
+
rootCauseReason: loc.rootCauseReason,
|
|
1435
|
+
traversalPath: path43,
|
|
1436
|
+
edgeProvenances,
|
|
1437
|
+
confidence,
|
|
1438
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
const lastEdge = chain.edges[chain.edges.length - 1];
|
|
1442
|
+
const errs = lastEdge.signal?.errorCount ?? 0;
|
|
1443
|
+
const culpritName = culprit.replace(/^service:/, "");
|
|
1444
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1445
|
+
rootCauseNode: culprit,
|
|
1446
|
+
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
1447
|
+
traversalPath: path43,
|
|
1448
|
+
edgeProvenances,
|
|
1449
|
+
confidence,
|
|
1450
|
+
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
1291
1451
|
});
|
|
1292
1452
|
}
|
|
1293
1453
|
function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
@@ -1310,14 +1470,14 @@ function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
|
1310
1470
|
});
|
|
1311
1471
|
}
|
|
1312
1472
|
if (frame.distance >= maxDepth) continue;
|
|
1313
|
-
const
|
|
1314
|
-
for (const [
|
|
1315
|
-
if (enqueued.has(
|
|
1316
|
-
enqueued.add(
|
|
1473
|
+
const incoming = bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId));
|
|
1474
|
+
for (const [srcId, edge] of incoming) {
|
|
1475
|
+
if (enqueued.has(srcId)) continue;
|
|
1476
|
+
enqueued.add(srcId);
|
|
1317
1477
|
queue.push({
|
|
1318
|
-
nodeId:
|
|
1478
|
+
nodeId: srcId,
|
|
1319
1479
|
distance: frame.distance + 1,
|
|
1320
|
-
path: [...frame.path,
|
|
1480
|
+
path: [...frame.path, srcId],
|
|
1321
1481
|
pathEdges: [...frame.pathEdges, edge]
|
|
1322
1482
|
});
|
|
1323
1483
|
}
|
|
@@ -1373,6 +1533,62 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
|
|
|
1373
1533
|
total: dependencies.length
|
|
1374
1534
|
});
|
|
1375
1535
|
}
|
|
1536
|
+
function getObservedDependencies(graph, nodeId) {
|
|
1537
|
+
if (!graph.hasNode(nodeId)) {
|
|
1538
|
+
return import_types.ObservedDependenciesResultSchema.parse({
|
|
1539
|
+
origin: nodeId,
|
|
1540
|
+
dependencies: [],
|
|
1541
|
+
observed: false,
|
|
1542
|
+
inboundObservedCount: 0,
|
|
1543
|
+
hasExtractedOutbound: false
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
1547
|
+
const scope = [nodeId];
|
|
1548
|
+
if (attrs.type === import_types.NodeType.ServiceNode) {
|
|
1549
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
1550
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1551
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1552
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
1553
|
+
if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
const dependencies = [];
|
|
1557
|
+
const seenEdge = /* @__PURE__ */ new Set();
|
|
1558
|
+
let hasExtractedOutbound = false;
|
|
1559
|
+
for (const src of scope) {
|
|
1560
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
1561
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1562
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1563
|
+
if (e.provenance === import_types.Provenance.OBSERVED) {
|
|
1564
|
+
if (!seenEdge.has(e.id)) {
|
|
1565
|
+
seenEdge.add(e.id);
|
|
1566
|
+
dependencies.push(e);
|
|
1567
|
+
}
|
|
1568
|
+
} else if (e.provenance === import_types.Provenance.EXTRACTED) {
|
|
1569
|
+
hasExtractedOutbound = true;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
let inboundObservedCount = 0;
|
|
1574
|
+
for (const tgt of scope) {
|
|
1575
|
+
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
1576
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1577
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1578
|
+
if (e.provenance === import_types.Provenance.OBSERVED) inboundObservedCount += 1;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
dependencies.sort(
|
|
1582
|
+
(a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
|
|
1583
|
+
);
|
|
1584
|
+
return import_types.ObservedDependenciesResultSchema.parse({
|
|
1585
|
+
origin: nodeId,
|
|
1586
|
+
dependencies,
|
|
1587
|
+
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
1588
|
+
inboundObservedCount,
|
|
1589
|
+
hasExtractedOutbound
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1376
1592
|
|
|
1377
1593
|
// src/policy.ts
|
|
1378
1594
|
var DEFAULT_ACTION_BY_SEVERITY = {
|
|
@@ -1642,6 +1858,130 @@ function evaluateAllPolicies(graph, policies, ctx) {
|
|
|
1642
1858
|
}
|
|
1643
1859
|
return out;
|
|
1644
1860
|
}
|
|
1861
|
+
function selectApplicablePolicies(graph, policies, nodeId) {
|
|
1862
|
+
if (!graph.hasNode(nodeId)) return [];
|
|
1863
|
+
const node = graph.getNodeAttributes(nodeId);
|
|
1864
|
+
const out = [];
|
|
1865
|
+
for (const policy of policies) {
|
|
1866
|
+
const m = matchPolicyToNode(graph, policy, nodeId, node);
|
|
1867
|
+
if (!m) continue;
|
|
1868
|
+
out.push({
|
|
1869
|
+
policyId: policy.id,
|
|
1870
|
+
policyName: policy.name,
|
|
1871
|
+
...policy.description !== void 0 ? { description: policy.description } : {},
|
|
1872
|
+
severity: policy.severity,
|
|
1873
|
+
onViolation: resolveOnViolation(policy),
|
|
1874
|
+
ruleType: policy.rule.type,
|
|
1875
|
+
match: m.match,
|
|
1876
|
+
reason: m.reason
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
return out;
|
|
1880
|
+
}
|
|
1881
|
+
function requiredProvenanceList(required) {
|
|
1882
|
+
if (Array.isArray(required)) return required.join(" | ");
|
|
1883
|
+
return String(required);
|
|
1884
|
+
}
|
|
1885
|
+
function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
|
|
1886
|
+
const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
|
|
1887
|
+
for (const edgeId of incident) {
|
|
1888
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1889
|
+
if (e.type !== edgeType) continue;
|
|
1890
|
+
if (requiredOtherEnd === void 0) return true;
|
|
1891
|
+
if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
|
|
1892
|
+
}
|
|
1893
|
+
return false;
|
|
1894
|
+
}
|
|
1895
|
+
function blastRadiusSubjectReaching(graph, rule, nodeId) {
|
|
1896
|
+
let found = null;
|
|
1897
|
+
graph.forEachNode((subjId, attrs) => {
|
|
1898
|
+
if (found !== null) return;
|
|
1899
|
+
if (subjId === nodeId) return;
|
|
1900
|
+
if (attrs.type !== rule.nodeType) return;
|
|
1901
|
+
const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
|
|
1902
|
+
if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
|
|
1903
|
+
});
|
|
1904
|
+
return found;
|
|
1905
|
+
}
|
|
1906
|
+
function matchPolicyToNode(graph, policy, nodeId, node) {
|
|
1907
|
+
const rule = policy.rule;
|
|
1908
|
+
switch (rule.type) {
|
|
1909
|
+
case "structural": {
|
|
1910
|
+
if (node.type === rule.fromNodeType) {
|
|
1911
|
+
return {
|
|
1912
|
+
match: "subject",
|
|
1913
|
+
reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
if (node.type === rule.toNodeType) {
|
|
1917
|
+
return {
|
|
1918
|
+
match: "region",
|
|
1919
|
+
reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
return null;
|
|
1923
|
+
}
|
|
1924
|
+
case "ownership": {
|
|
1925
|
+
if (node.type === rule.nodeType) {
|
|
1926
|
+
return {
|
|
1927
|
+
match: "subject",
|
|
1928
|
+
reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
return null;
|
|
1932
|
+
}
|
|
1933
|
+
case "blast-radius": {
|
|
1934
|
+
const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
|
|
1935
|
+
if (node.type === rule.nodeType) {
|
|
1936
|
+
return {
|
|
1937
|
+
match: "subject",
|
|
1938
|
+
reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
|
|
1942
|
+
if (subject) {
|
|
1943
|
+
return {
|
|
1944
|
+
match: "region",
|
|
1945
|
+
reason: `this node is in the blast radius of ${subject} (a ${rule.nodeType} held to a blast radius of ${rule.maxAffected}${depthLabel}) \u2014 it breaks if that ${rule.nodeType} changes`
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
return null;
|
|
1949
|
+
}
|
|
1950
|
+
case "compatibility": {
|
|
1951
|
+
const kindLabel = rule.kind ?? "all compat shapes";
|
|
1952
|
+
if (node.type === import_types2.NodeType.ServiceNode) {
|
|
1953
|
+
return {
|
|
1954
|
+
match: "subject",
|
|
1955
|
+
reason: `this service's dependencies are compatibility-checked (${kindLabel})`
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
|
|
1959
|
+
if (reachesDriverEngine && node.type === import_types2.NodeType.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, import_types2.EdgeType.CONNECTS_TO)) {
|
|
1960
|
+
return {
|
|
1961
|
+
match: "region",
|
|
1962
|
+
reason: "services connecting to this database have their driver/engine compatibility checked against it"
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
return null;
|
|
1966
|
+
}
|
|
1967
|
+
case "provenance": {
|
|
1968
|
+
const requiredList = requiredProvenanceList(rule.required);
|
|
1969
|
+
if (rule.targetNodeId !== void 0 && nodeId === rule.targetNodeId) {
|
|
1970
|
+
return {
|
|
1971
|
+
match: "subject",
|
|
1972
|
+
reason: `every ${rule.edgeType} edge into ${rule.targetNodeId} must carry ${requiredList} provenance`
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
if (nodeTouchesEdgeType(graph, nodeId, rule.edgeType, rule.targetNodeId)) {
|
|
1976
|
+
return {
|
|
1977
|
+
match: "region",
|
|
1978
|
+
reason: rule.targetNodeId !== void 0 ? `this node sits on a ${rule.edgeType} edge to ${rule.targetNodeId}, which must carry ${requiredList} provenance` : `this node sits on a ${rule.edgeType} edge, which must carry ${requiredList} provenance`
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
return null;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1645
1985
|
async function loadPolicyFile(policyPath) {
|
|
1646
1986
|
let raw;
|
|
1647
1987
|
try {
|
|
@@ -1750,9 +2090,9 @@ function loadIncidentThresholdsFromEnv() {
|
|
|
1750
2090
|
return DEFAULT_INCIDENT_THRESHOLDS;
|
|
1751
2091
|
}
|
|
1752
2092
|
}
|
|
1753
|
-
function
|
|
2093
|
+
function httpResponseStatusFromAttrs(attrs) {
|
|
1754
2094
|
for (const key of ["http.response.status_code", "http.status_code"]) {
|
|
1755
|
-
const v =
|
|
2095
|
+
const v = attrs[key];
|
|
1756
2096
|
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
1757
2097
|
if (typeof v === "string") {
|
|
1758
2098
|
const n = Number(v);
|
|
@@ -1761,6 +2101,63 @@ function httpResponseStatus(span) {
|
|
|
1761
2101
|
}
|
|
1762
2102
|
return void 0;
|
|
1763
2103
|
}
|
|
2104
|
+
function httpResponseStatus(span) {
|
|
2105
|
+
return httpResponseStatusFromAttrs(span.attributes);
|
|
2106
|
+
}
|
|
2107
|
+
function httpFailureMessageFromAttrs(attrs) {
|
|
2108
|
+
const status2 = httpResponseStatusFromAttrs(attrs);
|
|
2109
|
+
const route = pickAttrFrom(attrs, "http.route", "http.target", "url.path");
|
|
2110
|
+
const method = pickAttrFrom(attrs, "http.request.method", "http.method");
|
|
2111
|
+
const where = route ? `${method ? `${method} ` : ""}${route}` : void 0;
|
|
2112
|
+
if (status2 !== void 0 && where) return `${status2} on ${where}`;
|
|
2113
|
+
if (status2 !== void 0) return `HTTP ${status2}`;
|
|
2114
|
+
if (where) return `error on ${where}`;
|
|
2115
|
+
return void 0;
|
|
2116
|
+
}
|
|
2117
|
+
var GRPC_STATUS_NAMES = {
|
|
2118
|
+
1: "CANCELLED",
|
|
2119
|
+
2: "UNKNOWN",
|
|
2120
|
+
3: "INVALID_ARGUMENT",
|
|
2121
|
+
4: "DEADLINE_EXCEEDED",
|
|
2122
|
+
5: "NOT_FOUND",
|
|
2123
|
+
6: "ALREADY_EXISTS",
|
|
2124
|
+
7: "PERMISSION_DENIED",
|
|
2125
|
+
8: "RESOURCE_EXHAUSTED",
|
|
2126
|
+
9: "FAILED_PRECONDITION",
|
|
2127
|
+
10: "ABORTED",
|
|
2128
|
+
11: "OUT_OF_RANGE",
|
|
2129
|
+
12: "UNIMPLEMENTED",
|
|
2130
|
+
13: "INTERNAL",
|
|
2131
|
+
14: "UNAVAILABLE",
|
|
2132
|
+
15: "DATA_LOSS",
|
|
2133
|
+
16: "UNAUTHENTICATED"
|
|
2134
|
+
};
|
|
2135
|
+
function grpcStatusCodeFromAttrs(attrs) {
|
|
2136
|
+
const v = attrs["rpc.grpc.status_code"];
|
|
2137
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
2138
|
+
if (typeof v === "string") {
|
|
2139
|
+
const n = Number(v);
|
|
2140
|
+
if (Number.isFinite(n)) return n;
|
|
2141
|
+
}
|
|
2142
|
+
return void 0;
|
|
2143
|
+
}
|
|
2144
|
+
function nonHttpFailureMessageFromAttrs(attrs) {
|
|
2145
|
+
const grpc2 = grpcStatusCodeFromAttrs(attrs);
|
|
2146
|
+
if (grpc2 !== void 0 && grpc2 !== 0) {
|
|
2147
|
+
const name = GRPC_STATUS_NAMES[grpc2] ?? `status ${grpc2}`;
|
|
2148
|
+
const detail = pickAttrFrom(attrs, "rpc.grpc.status_message");
|
|
2149
|
+
return detail ? `gRPC ${name}: ${detail}` : `gRPC ${name}`;
|
|
2150
|
+
}
|
|
2151
|
+
const errType = pickAttrFrom(attrs, "error.type");
|
|
2152
|
+
if (errType && errType !== "_OTHER" && !/^\d+$/.test(errType)) {
|
|
2153
|
+
const peer = pickAttrFrom(attrs, "server.address", "net.peer.name", "net.host.name");
|
|
2154
|
+
return peer ? `${errType} connecting to ${peer}` : errType;
|
|
2155
|
+
}
|
|
2156
|
+
return void 0;
|
|
2157
|
+
}
|
|
2158
|
+
function incidentMessage(span) {
|
|
2159
|
+
return span.exception?.message ?? httpFailureMessageFromAttrs(span.attributes) ?? nonHttpFailureMessageFromAttrs(span.attributes) ?? "unknown error";
|
|
2160
|
+
}
|
|
1764
2161
|
function nowIso(ctx) {
|
|
1765
2162
|
return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
|
|
1766
2163
|
}
|
|
@@ -1780,13 +2177,16 @@ function warnNoSourceMaps(serviceName) {
|
|
|
1780
2177
|
`[neat] ${serviceName}: no .map files found under dist/; observed file edges will land on dist paths, not src. Set sourceMap: true in tsconfig to enable file-level reconciliation.`
|
|
1781
2178
|
);
|
|
1782
2179
|
}
|
|
1783
|
-
function
|
|
2180
|
+
function pickAttrFrom(attrs, ...keys) {
|
|
1784
2181
|
for (const k of keys) {
|
|
1785
|
-
const v =
|
|
2182
|
+
const v = attrs[k];
|
|
1786
2183
|
if (typeof v === "string" && v.length > 0) return v;
|
|
1787
2184
|
}
|
|
1788
2185
|
return void 0;
|
|
1789
2186
|
}
|
|
2187
|
+
function pickAttr(span, ...keys) {
|
|
2188
|
+
return pickAttrFrom(span.attributes, ...keys);
|
|
2189
|
+
}
|
|
1790
2190
|
function hostFromUrl(u) {
|
|
1791
2191
|
if (!u) return void 0;
|
|
1792
2192
|
try {
|
|
@@ -1798,6 +2198,10 @@ function hostFromUrl(u) {
|
|
|
1798
2198
|
function pickAddress(span) {
|
|
1799
2199
|
return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
|
|
1800
2200
|
}
|
|
2201
|
+
function isLoopbackHost(host) {
|
|
2202
|
+
const h = host.toLowerCase();
|
|
2203
|
+
return h === "localhost" || h === "ip6-localhost" || h === "::1" || h === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(h);
|
|
2204
|
+
}
|
|
1801
2205
|
var CODE_FILEPATH_ATTR = "code.filepath";
|
|
1802
2206
|
var CODE_LINENO_ATTR = "code.lineno";
|
|
1803
2207
|
var CODE_FUNCTION_ATTR = "code.function";
|
|
@@ -1905,15 +2309,31 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
|
|
|
1905
2309
|
...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
|
|
1906
2310
|
};
|
|
1907
2311
|
}
|
|
2312
|
+
function reconcileObservedRelPath(graph, serviceName, relPath) {
|
|
2313
|
+
if (graph.hasNode((0, import_types3.fileId)(serviceName, relPath))) return relPath;
|
|
2314
|
+
let best = null;
|
|
2315
|
+
graph.forEachNode((_id, attrs) => {
|
|
2316
|
+
const a = attrs;
|
|
2317
|
+
if (a.type !== import_types3.NodeType.FileNode || a.service !== serviceName) return;
|
|
2318
|
+
if (a.discoveredVia === "otel") return;
|
|
2319
|
+
const p = a.path;
|
|
2320
|
+
if (!p) return;
|
|
2321
|
+
if ((relPath === p || relPath.endsWith(`/${p}`)) && (!best || p.length > best.length)) {
|
|
2322
|
+
best = p;
|
|
2323
|
+
}
|
|
2324
|
+
});
|
|
2325
|
+
return best ?? relPath;
|
|
2326
|
+
}
|
|
1908
2327
|
function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
1909
|
-
const
|
|
2328
|
+
const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
|
|
2329
|
+
const fileNodeId = (0, import_types3.fileId)(serviceName, relPath);
|
|
1910
2330
|
if (!graph.hasNode(fileNodeId)) {
|
|
1911
|
-
const language = languageForExt(
|
|
2331
|
+
const language = languageForExt(relPath);
|
|
1912
2332
|
const node = {
|
|
1913
2333
|
id: fileNodeId,
|
|
1914
2334
|
type: import_types3.NodeType.FileNode,
|
|
1915
2335
|
service: serviceName,
|
|
1916
|
-
path:
|
|
2336
|
+
path: relPath,
|
|
1917
2337
|
...language ? { language } : {},
|
|
1918
2338
|
...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
|
|
1919
2339
|
discoveredVia: "otel"
|
|
@@ -1941,6 +2361,11 @@ function makeInferredEdgeId(type, source, target) {
|
|
|
1941
2361
|
}
|
|
1942
2362
|
var INFERRED_CONFIDENCE = 0.6;
|
|
1943
2363
|
var STITCH_MAX_DEPTH = 2;
|
|
2364
|
+
var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
2365
|
+
import_types3.EdgeType.CALLS,
|
|
2366
|
+
import_types3.EdgeType.CONNECTS_TO,
|
|
2367
|
+
import_types3.EdgeType.DEPENDS_ON
|
|
2368
|
+
]);
|
|
1944
2369
|
var WIRE_SPAN_KIND_CLIENT = 3;
|
|
1945
2370
|
var WIRE_SPAN_KIND_PRODUCER = 4;
|
|
1946
2371
|
function spanMintsObservedEdge(kind) {
|
|
@@ -2114,6 +2539,7 @@ function stitchTrace(graph, sourceServiceId, ts) {
|
|
|
2114
2539
|
for (const edgeId of outbound) {
|
|
2115
2540
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
2116
2541
|
if (edge.provenance !== import_types3.Provenance.EXTRACTED) continue;
|
|
2542
|
+
if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
|
|
2117
2543
|
if (graph.hasEdge((0, import_types3.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
|
|
2118
2544
|
upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
|
|
2119
2545
|
if (!visited.has(edge.target)) {
|
|
@@ -2146,6 +2572,16 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
2146
2572
|
await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(ctx.errorsPath), { recursive: true });
|
|
2147
2573
|
await import_node_fs3.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
2148
2574
|
}
|
|
2575
|
+
function incidentAffectedNode(span, graph, scanPath) {
|
|
2576
|
+
const sid = (0, import_types3.serviceId)(span.service, span.env);
|
|
2577
|
+
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
2578
|
+
const callSite = callSiteFromSpan(span, serviceNode, scanPath);
|
|
2579
|
+
if (callSite) {
|
|
2580
|
+
const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
|
|
2581
|
+
return (0, import_types3.fileId)(span.service, relPath);
|
|
2582
|
+
}
|
|
2583
|
+
return sid;
|
|
2584
|
+
}
|
|
2149
2585
|
function sanitizeAttributes(attrs) {
|
|
2150
2586
|
const out = {};
|
|
2151
2587
|
for (const [k, v] of Object.entries(attrs)) {
|
|
@@ -2154,7 +2590,7 @@ function sanitizeAttributes(attrs) {
|
|
|
2154
2590
|
}
|
|
2155
2591
|
return out;
|
|
2156
2592
|
}
|
|
2157
|
-
function buildErrorEventForReceiver(span) {
|
|
2593
|
+
function buildErrorEventForReceiver(span, graph, scanPath) {
|
|
2158
2594
|
if (span.statusCode !== 2) return null;
|
|
2159
2595
|
const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2160
2596
|
const attrs = sanitizeAttributes(span.attributes);
|
|
@@ -2164,16 +2600,16 @@ function buildErrorEventForReceiver(span) {
|
|
|
2164
2600
|
service: span.service,
|
|
2165
2601
|
traceId: span.traceId,
|
|
2166
2602
|
spanId: span.spanId,
|
|
2167
|
-
errorMessage: span
|
|
2603
|
+
errorMessage: incidentMessage(span),
|
|
2168
2604
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2169
2605
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2170
2606
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
2171
|
-
affectedNode: (
|
|
2607
|
+
affectedNode: incidentAffectedNode(span, graph, scanPath)
|
|
2172
2608
|
};
|
|
2173
2609
|
}
|
|
2174
|
-
function makeErrorSpanWriter(errorsPath) {
|
|
2610
|
+
function makeErrorSpanWriter(errorsPath, graph, scanPath) {
|
|
2175
2611
|
return async (span) => {
|
|
2176
|
-
const ev = buildErrorEventForReceiver(span);
|
|
2612
|
+
const ev = buildErrorEventForReceiver(span, graph, scanPath);
|
|
2177
2613
|
if (!ev) return;
|
|
2178
2614
|
await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(errorsPath), { recursive: true });
|
|
2179
2615
|
await import_node_fs3.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
@@ -2201,6 +2637,22 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
|
|
|
2201
2637
|
};
|
|
2202
2638
|
await appendErrorEvent(ctx, ev);
|
|
2203
2639
|
}
|
|
2640
|
+
async function recordExceptionIncident(ctx, span, ts) {
|
|
2641
|
+
const attrs = sanitizeAttributes(span.attributes);
|
|
2642
|
+
const ev = {
|
|
2643
|
+
id: `${span.traceId}:${span.spanId}`,
|
|
2644
|
+
timestamp: ts,
|
|
2645
|
+
service: span.service,
|
|
2646
|
+
traceId: span.traceId,
|
|
2647
|
+
spanId: span.spanId,
|
|
2648
|
+
errorMessage: incidentMessage(span),
|
|
2649
|
+
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2650
|
+
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2651
|
+
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
2652
|
+
affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
|
|
2653
|
+
};
|
|
2654
|
+
await appendErrorEvent(ctx, ev);
|
|
2655
|
+
}
|
|
2204
2656
|
async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status2) {
|
|
2205
2657
|
const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
|
|
2206
2658
|
if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
|
|
@@ -2257,7 +2709,10 @@ async function handleSpan(ctx, span) {
|
|
|
2257
2709
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
2258
2710
|
cacheSpanService(span, nowMs, callSite);
|
|
2259
2711
|
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
2260
|
-
const callSiteEvidence = callSite ? {
|
|
2712
|
+
const callSiteEvidence = callSite ? {
|
|
2713
|
+
file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
|
|
2714
|
+
...callSite.line !== void 0 ? { line: callSite.line } : {}
|
|
2715
|
+
} : void 0;
|
|
2261
2716
|
let affectedNode = sourceId;
|
|
2262
2717
|
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
2263
2718
|
if (span.dbSystem) {
|
|
@@ -2279,7 +2734,7 @@ async function handleSpan(ctx, span) {
|
|
|
2279
2734
|
} else {
|
|
2280
2735
|
const host = pickAddress(span);
|
|
2281
2736
|
let resolvedViaAddress = false;
|
|
2282
|
-
if (mintsFromCallerSide && host && host !== span.service) {
|
|
2737
|
+
if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
|
|
2283
2738
|
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
2284
2739
|
if (targetId && targetId !== sourceId) {
|
|
2285
2740
|
upsertObservedEdge(
|
|
@@ -2314,7 +2769,11 @@ async function handleSpan(ctx, span) {
|
|
|
2314
2769
|
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
2315
2770
|
const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
|
|
2316
2771
|
const fallbackEvidence = parent.callSite ? {
|
|
2317
|
-
file:
|
|
2772
|
+
file: reconcileObservedRelPath(
|
|
2773
|
+
ctx.graph,
|
|
2774
|
+
parent.service,
|
|
2775
|
+
parent.callSite.relPath
|
|
2776
|
+
),
|
|
2318
2777
|
...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
|
|
2319
2778
|
} : void 0;
|
|
2320
2779
|
upsertObservedEdge(
|
|
@@ -2339,7 +2798,7 @@ async function handleSpan(ctx, span) {
|
|
|
2339
2798
|
service: span.service,
|
|
2340
2799
|
traceId: span.traceId,
|
|
2341
2800
|
spanId: span.spanId,
|
|
2342
|
-
errorMessage: span
|
|
2801
|
+
errorMessage: incidentMessage(span),
|
|
2343
2802
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2344
2803
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2345
2804
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
@@ -2350,7 +2809,9 @@ async function handleSpan(ctx, span) {
|
|
|
2350
2809
|
}
|
|
2351
2810
|
if (span.statusCode !== 2) {
|
|
2352
2811
|
const status2 = httpResponseStatus(span);
|
|
2353
|
-
if (
|
|
2812
|
+
if (span.exception) {
|
|
2813
|
+
await recordExceptionIncident(ctx, span, ts);
|
|
2814
|
+
} else if (status2 !== void 0 && status2 >= 500) {
|
|
2354
2815
|
await recordFailingResponseIncident(ctx, span, sourceId, ts, status2, 1);
|
|
2355
2816
|
} else if (status2 !== void 0 && status2 >= 400 && spanMintsObservedEdge(span.kind)) {
|
|
2356
2817
|
await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status2);
|
|
@@ -2515,12 +2976,43 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
2515
2976
|
async function readErrorEvents(errorsPath) {
|
|
2516
2977
|
try {
|
|
2517
2978
|
const raw = await import_node_fs3.promises.readFile(errorsPath, "utf8");
|
|
2518
|
-
|
|
2979
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
2980
|
+
return dedupeIncidents(events);
|
|
2519
2981
|
} catch (err) {
|
|
2520
2982
|
if (err.code === "ENOENT") return [];
|
|
2521
2983
|
throw err;
|
|
2522
2984
|
}
|
|
2523
2985
|
}
|
|
2986
|
+
function isSynthesizedHttpIncident(ev) {
|
|
2987
|
+
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
2988
|
+
if (ev.errorType) return false;
|
|
2989
|
+
if (!ev.attributes) return false;
|
|
2990
|
+
const synth = httpFailureMessageFromAttrs(ev.attributes);
|
|
2991
|
+
return synth !== void 0 && synth === ev.errorMessage;
|
|
2992
|
+
}
|
|
2993
|
+
function dedupeIncidents(events) {
|
|
2994
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2995
|
+
const once = [];
|
|
2996
|
+
for (const ev of events) {
|
|
2997
|
+
const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
|
|
2998
|
+
if (key === void 0) {
|
|
2999
|
+
once.push(ev);
|
|
3000
|
+
continue;
|
|
3001
|
+
}
|
|
3002
|
+
if (seen.has(key)) continue;
|
|
3003
|
+
seen.add(key);
|
|
3004
|
+
once.push(ev);
|
|
3005
|
+
}
|
|
3006
|
+
const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
|
|
3007
|
+
const hasRealFailure = /* @__PURE__ */ new Set();
|
|
3008
|
+
for (const ev of once) {
|
|
3009
|
+
if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
|
|
3010
|
+
}
|
|
3011
|
+
return once.filter((ev) => {
|
|
3012
|
+
if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
|
|
3013
|
+
return !hasRealFailure.has(groupKey(ev));
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
2524
3016
|
function mergeSnapshot(graph, snapshot) {
|
|
2525
3017
|
const exported = snapshot.graph;
|
|
2526
3018
|
let nodesAdded = 0;
|
|
@@ -2601,10 +3093,17 @@ function isConfigFile(name) {
|
|
|
2601
3093
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
2602
3094
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
2603
3095
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
3096
|
+
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
2604
3097
|
return { match: true, fileType: "env" };
|
|
2605
3098
|
}
|
|
2606
3099
|
return { match: false, fileType: "" };
|
|
2607
3100
|
}
|
|
3101
|
+
function isNeatAuthoredEnvFile(name) {
|
|
3102
|
+
return name === ".env.neat";
|
|
3103
|
+
}
|
|
3104
|
+
function isNeatAuthoredSourceFile(name) {
|
|
3105
|
+
return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
|
|
3106
|
+
}
|
|
2608
3107
|
function isTestPath(filePath) {
|
|
2609
3108
|
const normalised = filePath.replace(/\\/g, "/");
|
|
2610
3109
|
const segments = normalised.split("/");
|
|
@@ -3298,7 +3797,9 @@ async function walkSourceFiles(dir) {
|
|
|
3298
3797
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3299
3798
|
if (await isPythonVenvDir(full)) continue;
|
|
3300
3799
|
await walk(full);
|
|
3301
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry.name))
|
|
3800
|
+
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
|
|
3801
|
+
// would attribute our instrumentation imports to the user's service.
|
|
3802
|
+
!isNeatAuthoredSourceFile(entry.name)) {
|
|
3302
3803
|
out.push(full);
|
|
3303
3804
|
}
|
|
3304
3805
|
}
|
|
@@ -3468,11 +3969,28 @@ function collectJsImports(node, out) {
|
|
|
3468
3969
|
if (child) collectJsImports(child, out);
|
|
3469
3970
|
}
|
|
3470
3971
|
}
|
|
3972
|
+
function collectImportedNames(node, out) {
|
|
3973
|
+
if (node.type === "aliased_import") {
|
|
3974
|
+
const nameNode = node.childForFieldName("name");
|
|
3975
|
+
if (nameNode) out.push(nameNode.text);
|
|
3976
|
+
return;
|
|
3977
|
+
}
|
|
3978
|
+
if (node.type === "dotted_name") {
|
|
3979
|
+
out.push(node.text);
|
|
3980
|
+
return;
|
|
3981
|
+
}
|
|
3982
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3983
|
+
const child = node.namedChild(i);
|
|
3984
|
+
if (child) collectImportedNames(child, out);
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3471
3987
|
function collectPyImports(node, out) {
|
|
3472
3988
|
if (node.type === "import_from_statement") {
|
|
3473
3989
|
let level = 0;
|
|
3474
3990
|
let modulePath = "";
|
|
3991
|
+
const names = [];
|
|
3475
3992
|
let pastFrom = false;
|
|
3993
|
+
let pastImport = false;
|
|
3476
3994
|
for (let i = 0; i < node.childCount; i++) {
|
|
3477
3995
|
const child = node.child(i);
|
|
3478
3996
|
if (!child) continue;
|
|
@@ -3480,26 +3998,30 @@ function collectPyImports(node, out) {
|
|
|
3480
3998
|
if (child.type === "from") pastFrom = true;
|
|
3481
3999
|
continue;
|
|
3482
4000
|
}
|
|
3483
|
-
if (
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
if (!rc) continue;
|
|
3488
|
-
if (rc.type === "import_prefix") {
|
|
3489
|
-
for (let k = 0; k < rc.childCount; k++) {
|
|
3490
|
-
if (rc.child(k)?.type === ".") level++;
|
|
3491
|
-
}
|
|
3492
|
-
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
4001
|
+
if (!pastImport) {
|
|
4002
|
+
if (child.type === "import") {
|
|
4003
|
+
pastImport = true;
|
|
4004
|
+
continue;
|
|
3493
4005
|
}
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
4006
|
+
if (child.type === "relative_import") {
|
|
4007
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
4008
|
+
const rc = child.child(j);
|
|
4009
|
+
if (!rc) continue;
|
|
4010
|
+
if (rc.type === "import_prefix") {
|
|
4011
|
+
for (let k = 0; k < rc.childCount; k++) {
|
|
4012
|
+
if (rc.child(k)?.type === ".") level++;
|
|
4013
|
+
}
|
|
4014
|
+
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
4015
|
+
}
|
|
4016
|
+
} else if (child.type === "dotted_name") {
|
|
4017
|
+
modulePath = child.text;
|
|
4018
|
+
}
|
|
4019
|
+
continue;
|
|
3499
4020
|
}
|
|
4021
|
+
collectImportedNames(child, names);
|
|
3500
4022
|
}
|
|
3501
4023
|
if (level > 0 || modulePath) {
|
|
3502
|
-
out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
4024
|
+
out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
3503
4025
|
}
|
|
3504
4026
|
}
|
|
3505
4027
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -3601,8 +4123,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
|
|
|
3601
4123
|
return null;
|
|
3602
4124
|
}
|
|
3603
4125
|
async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
3604
|
-
if (!imp.modulePath) return null;
|
|
3605
|
-
const relPath = imp.modulePath.split(".").join("/");
|
|
3606
4126
|
let baseDir;
|
|
3607
4127
|
if (imp.level > 0) {
|
|
3608
4128
|
baseDir = import_node_path12.default.dirname(importerPath);
|
|
@@ -3610,13 +4130,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
|
3610
4130
|
} else {
|
|
3611
4131
|
baseDir = serviceDir;
|
|
3612
4132
|
}
|
|
3613
|
-
const
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
4133
|
+
const moduleBase = imp.modulePath ? import_node_path12.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
|
|
4134
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
4135
|
+
let needModuleFile = imp.names.length === 0;
|
|
4136
|
+
for (const name of imp.names) {
|
|
4137
|
+
const submoduleFile = import_node_path12.default.join(moduleBase, `${name}.py`);
|
|
4138
|
+
const subpackageInit = import_node_path12.default.join(moduleBase, name, "__init__.py");
|
|
4139
|
+
if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
|
|
4140
|
+
resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, submoduleFile)));
|
|
4141
|
+
} else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
|
|
4142
|
+
resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, subpackageInit)));
|
|
4143
|
+
} else {
|
|
4144
|
+
needModuleFile = true;
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
if (needModuleFile) {
|
|
4148
|
+
const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path12.default.join(moduleBase, "__init__.py")] : [import_node_path12.default.join(moduleBase, "__init__.py")];
|
|
4149
|
+
for (const candidate of moduleFileCandidates) {
|
|
4150
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
4151
|
+
resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, candidate)));
|
|
4152
|
+
break;
|
|
4153
|
+
}
|
|
3617
4154
|
}
|
|
3618
4155
|
}
|
|
3619
|
-
return
|
|
4156
|
+
return [...resolved];
|
|
3620
4157
|
}
|
|
3621
4158
|
function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
|
|
3622
4159
|
const importeeFileId = (0, import_types8.fileId)(serviceName, importeeRelPath);
|
|
@@ -3657,17 +4194,18 @@ async function addImports(graph, services) {
|
|
|
3657
4194
|
continue;
|
|
3658
4195
|
}
|
|
3659
4196
|
for (const imp of pyImports) {
|
|
3660
|
-
const
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
4197
|
+
const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
|
|
4198
|
+
for (const resolved of resolvedPaths) {
|
|
4199
|
+
edgesAdded += emitImportEdge(
|
|
4200
|
+
graph,
|
|
4201
|
+
service.pkg.name,
|
|
4202
|
+
importerFileId,
|
|
4203
|
+
relFile,
|
|
4204
|
+
resolved,
|
|
4205
|
+
imp.line,
|
|
4206
|
+
imp.snippet
|
|
4207
|
+
);
|
|
4208
|
+
}
|
|
3671
4209
|
}
|
|
3672
4210
|
continue;
|
|
3673
4211
|
}
|
|
@@ -4303,6 +4841,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4303
4841
|
edgesAdded++;
|
|
4304
4842
|
}
|
|
4305
4843
|
}
|
|
4844
|
+
if (allConfigs.length === 1) {
|
|
4845
|
+
const primary = allConfigs[0];
|
|
4846
|
+
service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
|
|
4847
|
+
const relPath = import_node_path20.default.relative(scanPath, primary.sourceFile);
|
|
4848
|
+
const cfgId = (0, import_types9.configId)(relPath);
|
|
4849
|
+
if (!graph.hasNode(cfgId)) {
|
|
4850
|
+
const cfgNode = {
|
|
4851
|
+
id: cfgId,
|
|
4852
|
+
type: import_types9.NodeType.ConfigNode,
|
|
4853
|
+
name: import_node_path20.default.basename(primary.sourceFile),
|
|
4854
|
+
path: relPath,
|
|
4855
|
+
fileType: isConfigFile(import_node_path20.default.basename(primary.sourceFile)).fileType || "config"
|
|
4856
|
+
};
|
|
4857
|
+
graph.addNode(cfgId, cfgNode);
|
|
4858
|
+
nodesAdded++;
|
|
4859
|
+
}
|
|
4860
|
+
const cfgEdge = {
|
|
4861
|
+
id: (0, import_types4.extractedEdgeId)(service.node.id, cfgId, import_types9.EdgeType.CONFIGURED_BY),
|
|
4862
|
+
source: service.node.id,
|
|
4863
|
+
target: cfgId,
|
|
4864
|
+
type: import_types9.EdgeType.CONFIGURED_BY,
|
|
4865
|
+
provenance: import_types9.Provenance.EXTRACTED,
|
|
4866
|
+
confidence: (0, import_types9.confidenceForExtracted)("structural"),
|
|
4867
|
+
evidence: { file: toPosix2(relPath) }
|
|
4868
|
+
};
|
|
4869
|
+
if (!graph.hasEdge(cfgEdge.id)) {
|
|
4870
|
+
graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
|
|
4871
|
+
edgesAdded++;
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4306
4874
|
attachIncompatibilities(service, allConfigs);
|
|
4307
4875
|
if (graph.hasNode(service.node.id)) {
|
|
4308
4876
|
const current = graph.getNodeAttributes(service.node.id);
|
|
@@ -4314,6 +4882,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4314
4882
|
if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
|
|
4315
4883
|
delete updated.incompatibilities;
|
|
4316
4884
|
}
|
|
4885
|
+
if (!service.node.dbConnectionTarget) {
|
|
4886
|
+
delete updated.dbConnectionTarget;
|
|
4887
|
+
}
|
|
4317
4888
|
graph.replaceNodeAttributes(service.node.id, updated);
|
|
4318
4889
|
}
|
|
4319
4890
|
}
|
|
@@ -4491,7 +5062,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4491
5062
|
const dedupKey = `${relFile}|${targetId}`;
|
|
4492
5063
|
if (seen.has(dedupKey)) continue;
|
|
4493
5064
|
seen.add(dedupKey);
|
|
4494
|
-
const confidence = (0, import_types11.confidenceForExtracted)("
|
|
5065
|
+
const confidence = (0, import_types11.confidenceForExtracted)("url-literal-service-target");
|
|
4495
5066
|
const ev = {
|
|
4496
5067
|
file: relFile,
|
|
4497
5068
|
line: site.line,
|
|
@@ -4511,7 +5082,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4511
5082
|
target: targetId,
|
|
4512
5083
|
type: import_types11.EdgeType.CALLS,
|
|
4513
5084
|
confidence,
|
|
4514
|
-
confidenceKind: "
|
|
5085
|
+
confidenceKind: "url-literal-service-target",
|
|
4515
5086
|
evidence: ev
|
|
4516
5087
|
});
|
|
4517
5088
|
continue;
|
|
@@ -5017,19 +5588,29 @@ init_cjs_shims();
|
|
|
5017
5588
|
var import_node_path29 = __toESM(require("path"), 1);
|
|
5018
5589
|
var import_node_fs15 = require("fs");
|
|
5019
5590
|
var import_types20 = require("@neat.is/types");
|
|
5020
|
-
function
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5591
|
+
function readDockerfile(content) {
|
|
5592
|
+
let image = null;
|
|
5593
|
+
const ports = [];
|
|
5594
|
+
let cmd = null;
|
|
5595
|
+
let entrypoint = null;
|
|
5596
|
+
for (const raw of content.split("\n")) {
|
|
5024
5597
|
const line = raw.trim();
|
|
5025
5598
|
if (!line || line.startsWith("#")) continue;
|
|
5026
|
-
if (
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
if (
|
|
5030
|
-
|
|
5599
|
+
if (/^from\s+/i.test(line)) {
|
|
5600
|
+
const candidate = line.split(/\s+/)[1];
|
|
5601
|
+
if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
|
|
5602
|
+
} else if (/^expose\s+/i.test(line)) {
|
|
5603
|
+
for (const token of line.split(/\s+/).slice(1)) {
|
|
5604
|
+
const port = Number.parseInt(token.split("/")[0], 10);
|
|
5605
|
+
if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
|
|
5606
|
+
}
|
|
5607
|
+
} else if (/^entrypoint\s+/i.test(line)) {
|
|
5608
|
+
entrypoint = line;
|
|
5609
|
+
} else if (/^cmd\s+/i.test(line)) {
|
|
5610
|
+
cmd = line;
|
|
5611
|
+
}
|
|
5031
5612
|
}
|
|
5032
|
-
return
|
|
5613
|
+
return { image, ports, entrypoint: entrypoint ?? cmd };
|
|
5033
5614
|
}
|
|
5034
5615
|
async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
5035
5616
|
let nodesAdded = 0;
|
|
@@ -5048,14 +5629,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5048
5629
|
);
|
|
5049
5630
|
continue;
|
|
5050
5631
|
}
|
|
5051
|
-
const
|
|
5052
|
-
if (!image) continue;
|
|
5053
|
-
const node = makeInfraNode("container-image", image);
|
|
5632
|
+
const facts = readDockerfile(content);
|
|
5633
|
+
if (!facts.image) continue;
|
|
5634
|
+
const node = makeInfraNode("container-image", facts.image);
|
|
5054
5635
|
if (!graph.hasNode(node.id)) {
|
|
5055
5636
|
graph.addNode(node.id, node);
|
|
5056
5637
|
nodesAdded++;
|
|
5057
5638
|
}
|
|
5058
5639
|
const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
|
|
5640
|
+
const evidenceFile = toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath));
|
|
5059
5641
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
5060
5642
|
graph,
|
|
5061
5643
|
service.pkg.name,
|
|
@@ -5074,12 +5656,33 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5074
5656
|
provenance: import_types20.Provenance.EXTRACTED,
|
|
5075
5657
|
confidence: (0, import_types20.confidenceForExtracted)("structural"),
|
|
5076
5658
|
evidence: {
|
|
5077
|
-
file:
|
|
5659
|
+
file: evidenceFile,
|
|
5660
|
+
...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
|
|
5078
5661
|
}
|
|
5079
5662
|
};
|
|
5080
5663
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
5081
5664
|
edgesAdded++;
|
|
5082
5665
|
}
|
|
5666
|
+
for (const port of facts.ports) {
|
|
5667
|
+
const portNode = makeInfraNode("port", String(port));
|
|
5668
|
+
if (!graph.hasNode(portNode.id)) {
|
|
5669
|
+
graph.addNode(portNode.id, portNode);
|
|
5670
|
+
nodesAdded++;
|
|
5671
|
+
}
|
|
5672
|
+
const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types20.EdgeType.CONNECTS_TO);
|
|
5673
|
+
if (graph.hasEdge(portEdgeId)) continue;
|
|
5674
|
+
const portEdge = {
|
|
5675
|
+
id: portEdgeId,
|
|
5676
|
+
source: fileNodeId,
|
|
5677
|
+
target: portNode.id,
|
|
5678
|
+
type: import_types20.EdgeType.CONNECTS_TO,
|
|
5679
|
+
provenance: import_types20.Provenance.EXTRACTED,
|
|
5680
|
+
confidence: (0, import_types20.confidenceForExtracted)("structural"),
|
|
5681
|
+
evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
|
|
5682
|
+
};
|
|
5683
|
+
graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
|
|
5684
|
+
edgesAdded++;
|
|
5685
|
+
}
|
|
5083
5686
|
}
|
|
5084
5687
|
return { nodesAdded, edgesAdded };
|
|
5085
5688
|
}
|
|
@@ -5088,7 +5691,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5088
5691
|
init_cjs_shims();
|
|
5089
5692
|
var import_node_fs16 = require("fs");
|
|
5090
5693
|
var import_node_path30 = __toESM(require("path"), 1);
|
|
5694
|
+
var import_types21 = require("@neat.is/types");
|
|
5091
5695
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
5696
|
+
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
5092
5697
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
5093
5698
|
if (depth > max) return [];
|
|
5094
5699
|
const out = [];
|
|
@@ -5105,24 +5710,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
5105
5710
|
}
|
|
5106
5711
|
return out;
|
|
5107
5712
|
}
|
|
5713
|
+
function blockBody(content, from) {
|
|
5714
|
+
const open = content.indexOf("{", from);
|
|
5715
|
+
if (open === -1) return null;
|
|
5716
|
+
let depth = 0;
|
|
5717
|
+
for (let i = open; i < content.length; i++) {
|
|
5718
|
+
const ch = content[i];
|
|
5719
|
+
if (ch === "{") depth++;
|
|
5720
|
+
else if (ch === "}") {
|
|
5721
|
+
depth--;
|
|
5722
|
+
if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
return null;
|
|
5726
|
+
}
|
|
5727
|
+
function lineAt(content, index) {
|
|
5728
|
+
let line = 1;
|
|
5729
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
5730
|
+
if (content[i] === "\n") line++;
|
|
5731
|
+
}
|
|
5732
|
+
return line;
|
|
5733
|
+
}
|
|
5108
5734
|
async function addTerraformResources(graph, scanPath) {
|
|
5109
5735
|
let nodesAdded = 0;
|
|
5736
|
+
let edgesAdded = 0;
|
|
5110
5737
|
const files = await walkTfFiles(scanPath);
|
|
5111
5738
|
for (const file of files) {
|
|
5112
5739
|
const content = await import_node_fs16.promises.readFile(file, "utf8");
|
|
5740
|
+
const evidenceFile = toPosix2(import_node_path30.default.relative(scanPath, file));
|
|
5741
|
+
const resources = [];
|
|
5742
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
5113
5743
|
RESOURCE_RE.lastIndex = 0;
|
|
5114
5744
|
let m;
|
|
5115
5745
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
5116
|
-
const
|
|
5746
|
+
const type = m[1];
|
|
5117
5747
|
const name = m[2];
|
|
5118
|
-
const node = makeInfraNode(
|
|
5748
|
+
const node = makeInfraNode(type, name, "aws");
|
|
5119
5749
|
if (!graph.hasNode(node.id)) {
|
|
5120
5750
|
graph.addNode(node.id, node);
|
|
5121
5751
|
nodesAdded++;
|
|
5122
5752
|
}
|
|
5753
|
+
const span = blockBody(content, RESOURCE_RE.lastIndex);
|
|
5754
|
+
const resource = {
|
|
5755
|
+
type,
|
|
5756
|
+
name,
|
|
5757
|
+
nodeId: node.id,
|
|
5758
|
+
body: span?.body ?? "",
|
|
5759
|
+
bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
|
|
5760
|
+
};
|
|
5761
|
+
resources.push(resource);
|
|
5762
|
+
byKey.set(`${type}.${name}`, resource);
|
|
5763
|
+
}
|
|
5764
|
+
for (const resource of resources) {
|
|
5765
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5766
|
+
REFERENCE_RE.lastIndex = 0;
|
|
5767
|
+
let ref;
|
|
5768
|
+
while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
|
|
5769
|
+
const key = `${ref[1]}.${ref[2]}`;
|
|
5770
|
+
if (key === `${resource.type}.${resource.name}`) continue;
|
|
5771
|
+
const target = byKey.get(key);
|
|
5772
|
+
if (!target) continue;
|
|
5773
|
+
if (seen.has(target.nodeId)) continue;
|
|
5774
|
+
seen.add(target.nodeId);
|
|
5775
|
+
const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types21.EdgeType.DEPENDS_ON);
|
|
5776
|
+
if (graph.hasEdge(edgeId)) continue;
|
|
5777
|
+
const line = lineAt(content, resource.bodyOffset + ref.index);
|
|
5778
|
+
const edge = {
|
|
5779
|
+
id: edgeId,
|
|
5780
|
+
source: resource.nodeId,
|
|
5781
|
+
target: target.nodeId,
|
|
5782
|
+
type: import_types21.EdgeType.DEPENDS_ON,
|
|
5783
|
+
provenance: import_types21.Provenance.EXTRACTED,
|
|
5784
|
+
confidence: (0, import_types21.confidenceForExtracted)("structural"),
|
|
5785
|
+
evidence: { file: evidenceFile, line, snippet: key }
|
|
5786
|
+
};
|
|
5787
|
+
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
5788
|
+
edgesAdded++;
|
|
5789
|
+
}
|
|
5123
5790
|
}
|
|
5124
5791
|
}
|
|
5125
|
-
return { nodesAdded, edgesAdded
|
|
5792
|
+
return { nodesAdded, edgesAdded };
|
|
5126
5793
|
}
|
|
5127
5794
|
|
|
5128
5795
|
// src/extract/infra/k8s.ts
|
|
@@ -5200,11 +5867,11 @@ var import_node_path33 = __toESM(require("path"), 1);
|
|
|
5200
5867
|
init_cjs_shims();
|
|
5201
5868
|
var import_node_fs18 = require("fs");
|
|
5202
5869
|
var import_node_path32 = __toESM(require("path"), 1);
|
|
5203
|
-
var
|
|
5870
|
+
var import_types22 = require("@neat.is/types");
|
|
5204
5871
|
function dropOrphanedFileNodes(graph) {
|
|
5205
5872
|
const orphans = [];
|
|
5206
5873
|
graph.forEachNode((id, attrs) => {
|
|
5207
|
-
if (attrs.type !==
|
|
5874
|
+
if (attrs.type !== import_types22.NodeType.FileNode) return;
|
|
5208
5875
|
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
5209
5876
|
orphans.push(id);
|
|
5210
5877
|
}
|
|
@@ -5217,7 +5884,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
5217
5884
|
const bases = [scanPath, ...serviceDirs];
|
|
5218
5885
|
graph.forEachEdge((id, attrs) => {
|
|
5219
5886
|
const edge = attrs;
|
|
5220
|
-
if (edge.provenance !==
|
|
5887
|
+
if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
|
|
5221
5888
|
const evidenceFile = edge.evidence?.file;
|
|
5222
5889
|
if (!evidenceFile) return;
|
|
5223
5890
|
if (import_node_path32.default.isAbsolute(evidenceFile)) {
|
|
@@ -5300,7 +5967,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5300
5967
|
init_cjs_shims();
|
|
5301
5968
|
var import_node_fs19 = require("fs");
|
|
5302
5969
|
var import_node_path34 = __toESM(require("path"), 1);
|
|
5303
|
-
var
|
|
5970
|
+
var import_types23 = require("@neat.is/types");
|
|
5304
5971
|
var SCHEMA_VERSION = 4;
|
|
5305
5972
|
function migrateV1ToV2(payload) {
|
|
5306
5973
|
const nodes = payload.graph.nodes;
|
|
@@ -5322,12 +5989,12 @@ function migrateV2ToV3(payload) {
|
|
|
5322
5989
|
for (const edge of edges) {
|
|
5323
5990
|
const attrs = edge.attributes;
|
|
5324
5991
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
5325
|
-
attrs.provenance =
|
|
5992
|
+
attrs.provenance = import_types23.Provenance.OBSERVED;
|
|
5326
5993
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
5327
5994
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
5328
5995
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
5329
5996
|
if (type && source && target) {
|
|
5330
|
-
const newId = (0,
|
|
5997
|
+
const newId = (0, import_types23.observedEdgeId)(source, target, type);
|
|
5331
5998
|
attrs.id = newId;
|
|
5332
5999
|
if (edge.key) edge.key = newId;
|
|
5333
6000
|
}
|
|
@@ -5419,7 +6086,7 @@ function startPersistLoop(graph, outPath, opts = {}) {
|
|
|
5419
6086
|
init_cjs_shims();
|
|
5420
6087
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
5421
6088
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
5422
|
-
var
|
|
6089
|
+
var import_types26 = require("@neat.is/types");
|
|
5423
6090
|
|
|
5424
6091
|
// src/extend/index.ts
|
|
5425
6092
|
init_cjs_shims();
|
|
@@ -5738,7 +6405,7 @@ async function rollbackExtension(ctx, args) {
|
|
|
5738
6405
|
|
|
5739
6406
|
// src/divergences.ts
|
|
5740
6407
|
init_cjs_shims();
|
|
5741
|
-
var
|
|
6408
|
+
var import_types24 = require("@neat.is/types");
|
|
5742
6409
|
function bucketKey(source, target, type) {
|
|
5743
6410
|
return `${type}|${source}|${target}`;
|
|
5744
6411
|
}
|
|
@@ -5746,22 +6413,22 @@ function bucketEdges(graph) {
|
|
|
5746
6413
|
const buckets = /* @__PURE__ */ new Map();
|
|
5747
6414
|
graph.forEachEdge((id, attrs) => {
|
|
5748
6415
|
const e = attrs;
|
|
5749
|
-
const parsed = (0,
|
|
6416
|
+
const parsed = (0, import_types24.parseEdgeId)(id);
|
|
5750
6417
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
5751
6418
|
const key = bucketKey(e.source, e.target, e.type);
|
|
5752
6419
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
5753
6420
|
switch (provenance) {
|
|
5754
|
-
case
|
|
6421
|
+
case import_types24.Provenance.EXTRACTED:
|
|
5755
6422
|
cur.extracted = e;
|
|
5756
6423
|
break;
|
|
5757
|
-
case
|
|
6424
|
+
case import_types24.Provenance.OBSERVED:
|
|
5758
6425
|
cur.observed = e;
|
|
5759
6426
|
break;
|
|
5760
|
-
case
|
|
6427
|
+
case import_types24.Provenance.INFERRED:
|
|
5761
6428
|
cur.inferred = e;
|
|
5762
6429
|
break;
|
|
5763
6430
|
default:
|
|
5764
|
-
if (e.provenance ===
|
|
6431
|
+
if (e.provenance === import_types24.Provenance.STALE) cur.stale = e;
|
|
5765
6432
|
}
|
|
5766
6433
|
buckets.set(key, cur);
|
|
5767
6434
|
});
|
|
@@ -5770,7 +6437,7 @@ function bucketEdges(graph) {
|
|
|
5770
6437
|
function nodeIsFrontier(graph, nodeId) {
|
|
5771
6438
|
if (!graph.hasNode(nodeId)) return false;
|
|
5772
6439
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
5773
|
-
return attrs.type ===
|
|
6440
|
+
return attrs.type === import_types24.NodeType.FrontierNode;
|
|
5774
6441
|
}
|
|
5775
6442
|
function clampConfidence(n) {
|
|
5776
6443
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -5789,10 +6456,16 @@ function gradedConfidence(edge) {
|
|
|
5789
6456
|
if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
|
|
5790
6457
|
return clampConfidence(confidenceForEdge(edge));
|
|
5791
6458
|
}
|
|
6459
|
+
var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
6460
|
+
import_types24.EdgeType.CALLS,
|
|
6461
|
+
import_types24.EdgeType.CONNECTS_TO,
|
|
6462
|
+
import_types24.EdgeType.PUBLISHES_TO,
|
|
6463
|
+
import_types24.EdgeType.CONSUMES_FROM
|
|
6464
|
+
]);
|
|
5792
6465
|
function detectMissingDivergences(graph, bucket) {
|
|
5793
6466
|
const out = [];
|
|
5794
|
-
if (bucket.type ===
|
|
5795
|
-
if (bucket.extracted && !bucket.observed) {
|
|
6467
|
+
if (bucket.type === import_types24.EdgeType.CONTAINS) return out;
|
|
6468
|
+
if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
|
|
5796
6469
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
5797
6470
|
out.push({
|
|
5798
6471
|
type: "missing-observed",
|
|
@@ -5832,7 +6505,7 @@ function declaredHostFor(svc) {
|
|
|
5832
6505
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
5833
6506
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5834
6507
|
const e = graph.getEdgeAttributes(edgeId);
|
|
5835
|
-
if (e.type ===
|
|
6508
|
+
if (e.type === import_types24.EdgeType.CONFIGURED_BY && e.provenance === import_types24.Provenance.EXTRACTED) {
|
|
5836
6509
|
return true;
|
|
5837
6510
|
}
|
|
5838
6511
|
}
|
|
@@ -5845,10 +6518,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
5845
6518
|
const out = [];
|
|
5846
6519
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5847
6520
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5848
|
-
if (edge.type !==
|
|
5849
|
-
if (edge.provenance !==
|
|
6521
|
+
if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
|
|
6522
|
+
if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
|
|
5850
6523
|
const target = graph.getNodeAttributes(edge.target);
|
|
5851
|
-
if (target.type !==
|
|
6524
|
+
if (target.type !== import_types24.NodeType.DatabaseNode) continue;
|
|
5852
6525
|
const observedHost = target.host?.trim();
|
|
5853
6526
|
if (!observedHost) continue;
|
|
5854
6527
|
if (observedHost === declaredHost) continue;
|
|
@@ -5870,10 +6543,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
5870
6543
|
const deps = svc.dependencies ?? {};
|
|
5871
6544
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5872
6545
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5873
|
-
if (edge.type !==
|
|
5874
|
-
if (edge.provenance !==
|
|
6546
|
+
if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
|
|
6547
|
+
if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
|
|
5875
6548
|
const target = graph.getNodeAttributes(edge.target);
|
|
5876
|
-
if (target.type !==
|
|
6549
|
+
if (target.type !== import_types24.NodeType.DatabaseNode) continue;
|
|
5877
6550
|
for (const pair of compatPairs()) {
|
|
5878
6551
|
if (pair.engine !== target.engine) continue;
|
|
5879
6552
|
const declared = deps[pair.driver];
|
|
@@ -5926,6 +6599,23 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
5926
6599
|
function involvesNode(d, nodeId) {
|
|
5927
6600
|
return d.source === nodeId || d.target === nodeId;
|
|
5928
6601
|
}
|
|
6602
|
+
function suppressHostMismatchHalves(all) {
|
|
6603
|
+
const observedHalf = /* @__PURE__ */ new Set();
|
|
6604
|
+
const declaredHalf = /* @__PURE__ */ new Set();
|
|
6605
|
+
for (const d of all) {
|
|
6606
|
+
if (d.type !== "host-mismatch") continue;
|
|
6607
|
+
observedHalf.add(`${d.source}->${d.target}`);
|
|
6608
|
+
declaredHalf.add((0, import_types24.databaseId)(d.extractedHost));
|
|
6609
|
+
}
|
|
6610
|
+
if (observedHalf.size === 0) return all;
|
|
6611
|
+
return all.filter((d) => {
|
|
6612
|
+
if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
|
|
6613
|
+
return false;
|
|
6614
|
+
}
|
|
6615
|
+
if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
|
|
6616
|
+
return true;
|
|
6617
|
+
});
|
|
6618
|
+
}
|
|
5929
6619
|
function computeDivergences(graph, opts = {}) {
|
|
5930
6620
|
const all = [];
|
|
5931
6621
|
const buckets = bucketEdges(graph);
|
|
@@ -5934,12 +6624,13 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5934
6624
|
}
|
|
5935
6625
|
graph.forEachNode((nodeId, attrs) => {
|
|
5936
6626
|
const n = attrs;
|
|
5937
|
-
if (n.type !==
|
|
6627
|
+
if (n.type !== import_types24.NodeType.ServiceNode) return;
|
|
5938
6628
|
const svc = n;
|
|
5939
6629
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
5940
6630
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
5941
6631
|
});
|
|
5942
|
-
|
|
6632
|
+
const reconciled = suppressHostMismatchHalves(all);
|
|
6633
|
+
let filtered = reconciled;
|
|
5943
6634
|
if (opts.type) {
|
|
5944
6635
|
const allowed = opts.type;
|
|
5945
6636
|
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
@@ -5967,7 +6658,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5967
6658
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
5968
6659
|
return a.target.localeCompare(b.target);
|
|
5969
6660
|
});
|
|
5970
|
-
return
|
|
6661
|
+
return import_types24.DivergenceResultSchema.parse({
|
|
5971
6662
|
divergences: filtered,
|
|
5972
6663
|
totalAffected: filtered.length,
|
|
5973
6664
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -6108,7 +6799,7 @@ init_cjs_shims();
|
|
|
6108
6799
|
var import_node_fs23 = require("fs");
|
|
6109
6800
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
6110
6801
|
var import_node_path38 = __toESM(require("path"), 1);
|
|
6111
|
-
var
|
|
6802
|
+
var import_types25 = require("@neat.is/types");
|
|
6112
6803
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
6113
6804
|
var LOCK_RETRY_MS = 50;
|
|
6114
6805
|
function neatHome() {
|
|
@@ -6257,10 +6948,10 @@ async function readRegistry() {
|
|
|
6257
6948
|
throw err;
|
|
6258
6949
|
}
|
|
6259
6950
|
const parsed = JSON.parse(raw);
|
|
6260
|
-
return
|
|
6951
|
+
return import_types25.RegistryFileSchema.parse(parsed);
|
|
6261
6952
|
}
|
|
6262
6953
|
async function writeRegistry(reg) {
|
|
6263
|
-
const validated =
|
|
6954
|
+
const validated = import_types25.RegistryFileSchema.parse(reg);
|
|
6264
6955
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
6265
6956
|
}
|
|
6266
6957
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -6581,6 +7272,18 @@ function registerRoutes(scope, ctx) {
|
|
|
6581
7272
|
}
|
|
6582
7273
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
6583
7274
|
});
|
|
7275
|
+
scope.get(
|
|
7276
|
+
"/graph/observed-dependencies/:nodeId",
|
|
7277
|
+
async (req, reply) => {
|
|
7278
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7279
|
+
if (!proj) return;
|
|
7280
|
+
const { nodeId } = req.params;
|
|
7281
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
7282
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
7283
|
+
}
|
|
7284
|
+
return getObservedDependencies(proj.graph, nodeId);
|
|
7285
|
+
}
|
|
7286
|
+
);
|
|
6584
7287
|
scope.get("/graph/divergences", async (req, reply) => {
|
|
6585
7288
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6586
7289
|
if (!proj) return;
|
|
@@ -6589,11 +7292,11 @@ function registerRoutes(scope, ctx) {
|
|
|
6589
7292
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6590
7293
|
const parsed = [];
|
|
6591
7294
|
for (const c of candidates) {
|
|
6592
|
-
const r =
|
|
7295
|
+
const r = import_types26.DivergenceTypeSchema.safeParse(c);
|
|
6593
7296
|
if (!r.success) {
|
|
6594
7297
|
return reply.code(400).send({
|
|
6595
7298
|
error: `unknown divergence type "${c}"`,
|
|
6596
|
-
allowed:
|
|
7299
|
+
allowed: import_types26.DivergenceTypeSchema.options
|
|
6597
7300
|
});
|
|
6598
7301
|
}
|
|
6599
7302
|
parsed.push(r.data);
|
|
@@ -6641,23 +7344,29 @@ function registerRoutes(scope, ctx) {
|
|
|
6641
7344
|
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
6642
7345
|
return { count: sliced.length, total, events: sliced };
|
|
6643
7346
|
});
|
|
7347
|
+
const incidentHistoryHandler = async (req, reply) => {
|
|
7348
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7349
|
+
if (!proj) return;
|
|
7350
|
+
const { nodeId } = req.params;
|
|
7351
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
7352
|
+
reply.code(404).send({ error: "node not found", id: nodeId });
|
|
7353
|
+
return;
|
|
7354
|
+
}
|
|
7355
|
+
const epath = errorsPathFor(proj);
|
|
7356
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
7357
|
+
const events = await readErrorEvents(epath);
|
|
7358
|
+
const filtered = events.filter(
|
|
7359
|
+
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
7360
|
+
);
|
|
7361
|
+
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
7362
|
+
};
|
|
6644
7363
|
scope.get(
|
|
6645
7364
|
"/incidents/:nodeId",
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6652
|
-
}
|
|
6653
|
-
const epath = errorsPathFor(proj);
|
|
6654
|
-
if (!epath) return { count: 0, total: 0, events: [] };
|
|
6655
|
-
const events = await readErrorEvents(epath);
|
|
6656
|
-
const filtered = events.filter(
|
|
6657
|
-
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
6658
|
-
);
|
|
6659
|
-
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
6660
|
-
}
|
|
7365
|
+
incidentHistoryHandler
|
|
7366
|
+
);
|
|
7367
|
+
scope.get(
|
|
7368
|
+
"/graph/incident-history/:nodeId",
|
|
7369
|
+
incidentHistoryHandler
|
|
6661
7370
|
);
|
|
6662
7371
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
6663
7372
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -6666,16 +7375,16 @@ function registerRoutes(scope, ctx) {
|
|
|
6666
7375
|
if (!proj.graph.hasNode(nodeId)) {
|
|
6667
7376
|
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6668
7377
|
}
|
|
6669
|
-
let errorEvent;
|
|
6670
7378
|
const epath = errorsPathFor(proj);
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
7379
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
7380
|
+
let errorEvent;
|
|
7381
|
+
if (req.query.errorId) {
|
|
7382
|
+
errorEvent = incidents.find((e) => e.id === req.query.errorId);
|
|
6674
7383
|
if (!errorEvent) {
|
|
6675
7384
|
return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
|
|
6676
7385
|
}
|
|
6677
7386
|
}
|
|
6678
|
-
const result = getRootCause(proj.graph, nodeId, errorEvent);
|
|
7387
|
+
const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
|
|
6679
7388
|
if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
|
|
6680
7389
|
return result;
|
|
6681
7390
|
});
|
|
@@ -6806,7 +7515,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6806
7515
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
6807
7516
|
let violations = await log.readAll();
|
|
6808
7517
|
if (req.query.severity) {
|
|
6809
|
-
const sev =
|
|
7518
|
+
const sev = import_types26.PolicySeveritySchema.safeParse(req.query.severity);
|
|
6810
7519
|
if (!sev.success) {
|
|
6811
7520
|
return reply.code(400).send({
|
|
6812
7521
|
error: "invalid severity",
|
|
@@ -6820,10 +7529,32 @@ function registerRoutes(scope, ctx) {
|
|
|
6820
7529
|
}
|
|
6821
7530
|
return { violations };
|
|
6822
7531
|
});
|
|
7532
|
+
scope.get("/policies/applicable", async (req, reply) => {
|
|
7533
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7534
|
+
if (!proj) return;
|
|
7535
|
+
const nodeId = req.query.node;
|
|
7536
|
+
if (!nodeId) {
|
|
7537
|
+
return reply.code(400).send({ error: 'missing required query param "node"' });
|
|
7538
|
+
}
|
|
7539
|
+
const policyPath = ctx.policyFilePathFor(proj);
|
|
7540
|
+
let policies = [];
|
|
7541
|
+
if (policyPath) {
|
|
7542
|
+
try {
|
|
7543
|
+
policies = await loadPolicyFile(policyPath);
|
|
7544
|
+
} catch (err) {
|
|
7545
|
+
return reply.code(400).send({
|
|
7546
|
+
error: "policy.json failed to parse",
|
|
7547
|
+
details: err.message
|
|
7548
|
+
});
|
|
7549
|
+
}
|
|
7550
|
+
}
|
|
7551
|
+
const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
|
|
7552
|
+
return { node: nodeId, applicable };
|
|
7553
|
+
});
|
|
6823
7554
|
scope.post("/policies/check", async (req, reply) => {
|
|
6824
7555
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6825
7556
|
if (!proj) return;
|
|
6826
|
-
const parsed =
|
|
7557
|
+
const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
6827
7558
|
if (!parsed.success) {
|
|
6828
7559
|
return reply.code(400).send({
|
|
6829
7560
|
error: "invalid /policies/check body",
|
|
@@ -7113,6 +7844,7 @@ function unroutedErrorsPath(neatHome2) {
|
|
|
7113
7844
|
}
|
|
7114
7845
|
|
|
7115
7846
|
// src/daemon.ts
|
|
7847
|
+
var import_types27 = require("@neat.is/types");
|
|
7116
7848
|
function daemonJsonPath(scanPath) {
|
|
7117
7849
|
return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
|
|
7118
7850
|
}
|
|
@@ -7215,6 +7947,19 @@ function isTokenContained(needle, haystack) {
|
|
|
7215
7947
|
const tokens = haystack.split(/[-_]/);
|
|
7216
7948
|
return tokens.includes(needle);
|
|
7217
7949
|
}
|
|
7950
|
+
function serviceNameMatchesProject(serviceName, project) {
|
|
7951
|
+
if (serviceName === project) return true;
|
|
7952
|
+
if (isTokenPrefix(project, serviceName)) return true;
|
|
7953
|
+
if (isTokenContained(project, serviceName)) return true;
|
|
7954
|
+
return false;
|
|
7955
|
+
}
|
|
7956
|
+
function spanBelongsToSingleProject(graph, project, serviceName) {
|
|
7957
|
+
if (!serviceName) return true;
|
|
7958
|
+
if (serviceNameMatchesProject(serviceName, project)) return true;
|
|
7959
|
+
return graph.someNode(
|
|
7960
|
+
(_id, attrs) => attrs.type === import_types27.NodeType.ServiceNode && attrs.name === serviceName
|
|
7961
|
+
);
|
|
7962
|
+
}
|
|
7218
7963
|
async function bootstrapProject(entry) {
|
|
7219
7964
|
const paths = pathsForProject(entry.name, import_node_path42.default.join(entry.path, "neat-out"));
|
|
7220
7965
|
try {
|
|
@@ -7512,7 +8257,9 @@ async function startDaemon(opts = {}) {
|
|
|
7512
8257
|
singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0
|
|
7513
8258
|
});
|
|
7514
8259
|
restAddress = await restApp.listen({ port: restPort, host });
|
|
7515
|
-
console.log(
|
|
8260
|
+
console.log(
|
|
8261
|
+
`neatd: REST listening on http://${host}:${portFromListenAddress(restAddress, restPort)}`
|
|
8262
|
+
);
|
|
7516
8263
|
} catch (err) {
|
|
7517
8264
|
for (const slot of slots.values()) {
|
|
7518
8265
|
teardownSlot(slot);
|
|
@@ -7543,6 +8290,10 @@ async function startDaemon(opts = {}) {
|
|
|
7543
8290
|
warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
|
|
7544
8291
|
return null;
|
|
7545
8292
|
}
|
|
8293
|
+
if (!spanBelongsToSingleProject(slot2.graph, singleProject, serviceName)) {
|
|
8294
|
+
await recordUnroutedSpan(serviceName, traceId);
|
|
8295
|
+
return null;
|
|
8296
|
+
}
|
|
7546
8297
|
return slot2;
|
|
7547
8298
|
}
|
|
7548
8299
|
const liveEntries = await listProjects().catch(() => []);
|
|
@@ -7605,7 +8356,7 @@ async function startDaemon(opts = {}) {
|
|
|
7605
8356
|
onErrorSpanSync: async (span) => {
|
|
7606
8357
|
const slot = await resolveTargetSlot(span.service, span.traceId);
|
|
7607
8358
|
if (!slot) return;
|
|
7608
|
-
await makeErrorSpanWriter(slot.paths.errorsPath)(span);
|
|
8359
|
+
await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
|
|
7609
8360
|
},
|
|
7610
8361
|
// Project-scoped route (issue #367) — the URL already named the
|
|
7611
8362
|
// project. Resolution is a direct slot lookup; service.name resolves
|
|
@@ -7628,10 +8379,10 @@ async function startDaemon(opts = {}) {
|
|
|
7628
8379
|
onProjectErrorSpanSync: async (project, span) => {
|
|
7629
8380
|
const slot = await resolveSlotByName(project, span.service, span.traceId);
|
|
7630
8381
|
if (!slot) return;
|
|
7631
|
-
await makeErrorSpanWriter(slot.paths.errorsPath)(span);
|
|
8382
|
+
await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
|
|
7632
8383
|
}
|
|
7633
8384
|
});
|
|
7634
|
-
otlpAddress = await otlpApp
|
|
8385
|
+
otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
|
|
7635
8386
|
console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);
|
|
7636
8387
|
} catch (err) {
|
|
7637
8388
|
for (const slot of slots.values()) {
|
|
@@ -7786,7 +8537,8 @@ async function startDaemon(opts = {}) {
|
|
|
7786
8537
|
otlpAddress,
|
|
7787
8538
|
bootstrap: tracker,
|
|
7788
8539
|
initialBootstrap,
|
|
7789
|
-
daemonRecord
|
|
8540
|
+
daemonRecord,
|
|
8541
|
+
neatHome: home
|
|
7790
8542
|
};
|
|
7791
8543
|
}
|
|
7792
8544
|
// Annotate the CommonJS export names for ESM import in node:
|