@neat.is/core 0.4.20-dev.20260629 → 0.4.22
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-GNAX2CBF.js → chunk-OQ7CHOY2.js} +859 -159
- package/dist/chunk-OQ7CHOY2.js.map +1 -0
- package/dist/{chunk-62ELQVIP.js → chunk-XRE4OQWJ.js} +57 -9
- package/dist/chunk-XRE4OQWJ.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 +1232 -354
- 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 +884 -150
- 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 +928 -150
- 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 +806 -121
- 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/neatd.cjs
CHANGED
|
@@ -29,6 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
29
29
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
30
|
mod
|
|
31
31
|
));
|
|
32
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
33
|
|
|
33
34
|
// ../../node_modules/tsup/assets/cjs_shims.js
|
|
34
35
|
var getImportMetaUrl, importMetaUrl;
|
|
@@ -41,13 +42,13 @@ var init_cjs_shims = __esm({
|
|
|
41
42
|
});
|
|
42
43
|
|
|
43
44
|
// src/auth.ts
|
|
44
|
-
function
|
|
45
|
+
function isLoopbackHost2(host) {
|
|
45
46
|
if (!host) return false;
|
|
46
47
|
return LOOPBACK_HOSTS.has(host);
|
|
47
48
|
}
|
|
48
49
|
function assertBindAuthority(host, token) {
|
|
49
50
|
if (token && token.length > 0) return;
|
|
50
|
-
if (
|
|
51
|
+
if (isLoopbackHost2(host)) return;
|
|
51
52
|
throw new BindAuthorityError(host);
|
|
52
53
|
}
|
|
53
54
|
function mountBearerAuth(app, opts) {
|
|
@@ -573,7 +574,23 @@ async function buildOtelReceiver(opts) {
|
|
|
573
574
|
};
|
|
574
575
|
return decorated;
|
|
575
576
|
}
|
|
576
|
-
|
|
577
|
+
async function listenSteppingOtlp(app, requestedPort, host) {
|
|
578
|
+
let port = requestedPort;
|
|
579
|
+
for (let attempt = 0; ; attempt++) {
|
|
580
|
+
try {
|
|
581
|
+
return await app.listen({ port, host });
|
|
582
|
+
} catch (err) {
|
|
583
|
+
const code = err.code;
|
|
584
|
+
const canStep = requestedPort !== 0 && code === "EADDRINUSE" && attempt < OTLP_STEP_ATTEMPTS - 1;
|
|
585
|
+
if (!canStep) throw err;
|
|
586
|
+
console.warn(
|
|
587
|
+
`otel: OTLP port ${port} is in use, stepping to ${port + OTLP_STEP_STRIDE}`
|
|
588
|
+
);
|
|
589
|
+
port += OTLP_STEP_STRIDE;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
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;
|
|
577
594
|
var init_otel = __esm({
|
|
578
595
|
"src/otel.ts"() {
|
|
579
596
|
"use strict";
|
|
@@ -589,10 +606,17 @@ var init_otel = __esm({
|
|
|
589
606
|
exportTraceServiceRequestType = null;
|
|
590
607
|
exportTraceServiceResponseType = null;
|
|
591
608
|
cachedProtobufResponseBody = null;
|
|
609
|
+
OTLP_STEP_ATTEMPTS = 8;
|
|
610
|
+
OTLP_STEP_STRIDE = 1;
|
|
592
611
|
}
|
|
593
612
|
});
|
|
594
613
|
|
|
595
614
|
// src/neatd.ts
|
|
615
|
+
var neatd_exports = {};
|
|
616
|
+
__export(neatd_exports, {
|
|
617
|
+
installIngestFaultHandlers: () => installIngestFaultHandlers
|
|
618
|
+
});
|
|
619
|
+
module.exports = __toCommonJS(neatd_exports);
|
|
596
620
|
init_cjs_shims();
|
|
597
621
|
var import_node_fs27 = require("fs");
|
|
598
622
|
var import_node_path44 = __toESM(require("path"), 1);
|
|
@@ -1229,22 +1253,164 @@ var rootCauseShapes = {
|
|
|
1229
1253
|
[import_types.NodeType.ServiceNode]: serviceRootCauseShape,
|
|
1230
1254
|
[import_types.NodeType.FileNode]: fileRootCauseShape
|
|
1231
1255
|
};
|
|
1232
|
-
function getRootCause(graph, errorNodeId, errorEvent) {
|
|
1256
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
1233
1257
|
if (!graph.hasNode(errorNodeId)) return null;
|
|
1234
1258
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
1235
1259
|
const shape = rootCauseShapes[origin.type];
|
|
1236
|
-
if (
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1260
|
+
if (shape) {
|
|
1261
|
+
const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
|
|
1262
|
+
const match = shape(graph, origin, walk);
|
|
1263
|
+
if (match) {
|
|
1264
|
+
const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
|
|
1265
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1266
|
+
rootCauseNode: match.rootCauseNode,
|
|
1267
|
+
rootCauseReason: reason,
|
|
1268
|
+
traversalPath: walk.path,
|
|
1269
|
+
edgeProvenances: walk.edges.map((e) => e.provenance),
|
|
1270
|
+
confidence: confidenceFromMix(walk.edges),
|
|
1271
|
+
fixRecommendation: match.fixRecommendation
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (origin.type === import_types.NodeType.ServiceNode) {
|
|
1276
|
+
const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
|
|
1277
|
+
if (crossService) return crossService;
|
|
1278
|
+
}
|
|
1279
|
+
return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
|
|
1280
|
+
}
|
|
1281
|
+
var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
|
|
1282
|
+
function incidentMatchesNode(ev, nodeId) {
|
|
1283
|
+
return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
|
|
1284
|
+
}
|
|
1285
|
+
function localizeFromIncidents(nodeId, incidents, errorEvent) {
|
|
1286
|
+
const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
|
|
1287
|
+
const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
|
|
1288
|
+
if (relevant.length === 0) return null;
|
|
1289
|
+
const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
|
|
1290
|
+
const attrs = latest.attributes ?? {};
|
|
1291
|
+
const filepath = typeof attrs["code.filepath"] === "string" ? attrs["code.filepath"] : void 0;
|
|
1292
|
+
const lineno = typeof attrs["code.lineno"] === "number" ? attrs["code.lineno"] : void 0;
|
|
1293
|
+
const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
|
|
1294
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
1295
|
+
const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
|
|
1296
|
+
const count = sameMode.length;
|
|
1297
|
+
const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
|
|
1298
|
+
const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
|
|
1299
|
+
if (location) reasonParts.push(`surfaced at ${location}`);
|
|
1300
|
+
const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
|
|
1301
|
+
const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
|
|
1302
|
+
const fileNode = localizesToFile ? latest.affectedNode : void 0;
|
|
1303
|
+
const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
|
|
1304
|
+
return {
|
|
1305
|
+
rootCauseNode: fileNode ?? nodeId,
|
|
1306
|
+
rootCauseReason,
|
|
1307
|
+
...fileNode ? { fileNode } : {},
|
|
1308
|
+
...fixRecommendation ? { fixRecommendation } : {}
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
1312
|
+
const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
|
|
1313
|
+
if (!loc) return null;
|
|
1314
|
+
const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
|
|
1315
|
+
const edgeProvenances = loc.fileNode ? [import_types.Provenance.OBSERVED] : [];
|
|
1241
1316
|
return import_types.RootCauseResultSchema.parse({
|
|
1242
|
-
rootCauseNode:
|
|
1243
|
-
rootCauseReason:
|
|
1244
|
-
traversalPath
|
|
1245
|
-
edgeProvenances
|
|
1246
|
-
confidence:
|
|
1247
|
-
fixRecommendation:
|
|
1317
|
+
rootCauseNode: loc.rootCauseNode,
|
|
1318
|
+
rootCauseReason: loc.rootCauseReason,
|
|
1319
|
+
traversalPath,
|
|
1320
|
+
edgeProvenances,
|
|
1321
|
+
confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
|
|
1322
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
function isFailingCallEdge(e) {
|
|
1326
|
+
return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
1327
|
+
}
|
|
1328
|
+
function callSourcesForService(graph, serviceId3) {
|
|
1329
|
+
const ids = [serviceId3];
|
|
1330
|
+
for (const edgeId of graph.outboundEdges(serviceId3)) {
|
|
1331
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1332
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1333
|
+
const tgt = graph.getNodeAttributes(e.target);
|
|
1334
|
+
if (tgt.type === import_types.NodeType.FileNode) ids.push(e.target);
|
|
1335
|
+
}
|
|
1336
|
+
return ids;
|
|
1337
|
+
}
|
|
1338
|
+
function failingCallDominates(e, id, curEdge, curId) {
|
|
1339
|
+
const ec = e.signal?.errorCount ?? 0;
|
|
1340
|
+
const cc = curEdge.signal?.errorCount ?? 0;
|
|
1341
|
+
if (ec !== cc) return ec > cc;
|
|
1342
|
+
if (import_types.PROV_RANK[e.provenance] !== import_types.PROV_RANK[curEdge.provenance]) {
|
|
1343
|
+
return import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[curEdge.provenance];
|
|
1344
|
+
}
|
|
1345
|
+
return id < curId;
|
|
1346
|
+
}
|
|
1347
|
+
function dominantFailingCall(graph, serviceId3, visited) {
|
|
1348
|
+
let best = null;
|
|
1349
|
+
for (const src of callSourcesForService(graph, serviceId3)) {
|
|
1350
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
1351
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1352
|
+
if (!isFailingCallEdge(e)) continue;
|
|
1353
|
+
if (isFrontierNode(graph, e.target)) continue;
|
|
1354
|
+
const owner = resolveOwningService(graph, e.target);
|
|
1355
|
+
if (!owner || visited.has(owner.id)) continue;
|
|
1356
|
+
if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
|
|
1357
|
+
best = { nextService: owner.id, edge: e };
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
return best;
|
|
1362
|
+
}
|
|
1363
|
+
function followFailingCallChain(graph, originServiceId, maxDepth) {
|
|
1364
|
+
const path45 = [originServiceId];
|
|
1365
|
+
const edges = [];
|
|
1366
|
+
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
1367
|
+
let current = originServiceId;
|
|
1368
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
1369
|
+
const hop = dominantFailingCall(graph, current, visited);
|
|
1370
|
+
if (!hop) break;
|
|
1371
|
+
path45.push(hop.nextService);
|
|
1372
|
+
edges.push(hop.edge);
|
|
1373
|
+
visited.add(hop.nextService);
|
|
1374
|
+
current = hop.nextService;
|
|
1375
|
+
}
|
|
1376
|
+
if (edges.length === 0) return null;
|
|
1377
|
+
return { path: path45, edges, culprit: current };
|
|
1378
|
+
}
|
|
1379
|
+
function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
1380
|
+
const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
|
|
1381
|
+
if (!chain) return null;
|
|
1382
|
+
const culprit = chain.culprit;
|
|
1383
|
+
const path45 = [...chain.path];
|
|
1384
|
+
const edgeProvenances = chain.edges.map((e) => e.provenance);
|
|
1385
|
+
const baseConfidence = confidenceFromMix(chain.edges);
|
|
1386
|
+
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
1387
|
+
const loc = localizeFromIncidents(culprit, incidents, errorEvent);
|
|
1388
|
+
if (loc) {
|
|
1389
|
+
let rootCauseNode = culprit;
|
|
1390
|
+
if (loc.fileNode) {
|
|
1391
|
+
path45.push(loc.fileNode);
|
|
1392
|
+
edgeProvenances.push(import_types.Provenance.OBSERVED);
|
|
1393
|
+
rootCauseNode = loc.fileNode;
|
|
1394
|
+
}
|
|
1395
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1396
|
+
rootCauseNode,
|
|
1397
|
+
rootCauseReason: loc.rootCauseReason,
|
|
1398
|
+
traversalPath: path45,
|
|
1399
|
+
edgeProvenances,
|
|
1400
|
+
confidence,
|
|
1401
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
const lastEdge = chain.edges[chain.edges.length - 1];
|
|
1405
|
+
const errs = lastEdge.signal?.errorCount ?? 0;
|
|
1406
|
+
const culpritName = culprit.replace(/^service:/, "");
|
|
1407
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1408
|
+
rootCauseNode: culprit,
|
|
1409
|
+
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
1410
|
+
traversalPath: path45,
|
|
1411
|
+
edgeProvenances,
|
|
1412
|
+
confidence,
|
|
1413
|
+
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
1248
1414
|
});
|
|
1249
1415
|
}
|
|
1250
1416
|
function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
@@ -1267,14 +1433,14 @@ function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
|
1267
1433
|
});
|
|
1268
1434
|
}
|
|
1269
1435
|
if (frame.distance >= maxDepth) continue;
|
|
1270
|
-
const
|
|
1271
|
-
for (const [
|
|
1272
|
-
if (enqueued.has(
|
|
1273
|
-
enqueued.add(
|
|
1436
|
+
const incoming = bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId));
|
|
1437
|
+
for (const [srcId, edge] of incoming) {
|
|
1438
|
+
if (enqueued.has(srcId)) continue;
|
|
1439
|
+
enqueued.add(srcId);
|
|
1274
1440
|
queue.push({
|
|
1275
|
-
nodeId:
|
|
1441
|
+
nodeId: srcId,
|
|
1276
1442
|
distance: frame.distance + 1,
|
|
1277
|
-
path: [...frame.path,
|
|
1443
|
+
path: [...frame.path, srcId],
|
|
1278
1444
|
pathEdges: [...frame.pathEdges, edge]
|
|
1279
1445
|
});
|
|
1280
1446
|
}
|
|
@@ -1330,6 +1496,62 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
|
|
|
1330
1496
|
total: dependencies.length
|
|
1331
1497
|
});
|
|
1332
1498
|
}
|
|
1499
|
+
function getObservedDependencies(graph, nodeId) {
|
|
1500
|
+
if (!graph.hasNode(nodeId)) {
|
|
1501
|
+
return import_types.ObservedDependenciesResultSchema.parse({
|
|
1502
|
+
origin: nodeId,
|
|
1503
|
+
dependencies: [],
|
|
1504
|
+
observed: false,
|
|
1505
|
+
inboundObservedCount: 0,
|
|
1506
|
+
hasExtractedOutbound: false
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
1510
|
+
const scope = [nodeId];
|
|
1511
|
+
if (attrs.type === import_types.NodeType.ServiceNode) {
|
|
1512
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
1513
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1514
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1515
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
1516
|
+
if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
const dependencies = [];
|
|
1520
|
+
const seenEdge = /* @__PURE__ */ new Set();
|
|
1521
|
+
let hasExtractedOutbound = false;
|
|
1522
|
+
for (const src of scope) {
|
|
1523
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
1524
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1525
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1526
|
+
if (e.provenance === import_types.Provenance.OBSERVED) {
|
|
1527
|
+
if (!seenEdge.has(e.id)) {
|
|
1528
|
+
seenEdge.add(e.id);
|
|
1529
|
+
dependencies.push(e);
|
|
1530
|
+
}
|
|
1531
|
+
} else if (e.provenance === import_types.Provenance.EXTRACTED) {
|
|
1532
|
+
hasExtractedOutbound = true;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
let inboundObservedCount = 0;
|
|
1537
|
+
for (const tgt of scope) {
|
|
1538
|
+
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
1539
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1540
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1541
|
+
if (e.provenance === import_types.Provenance.OBSERVED) inboundObservedCount += 1;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
dependencies.sort(
|
|
1545
|
+
(a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
|
|
1546
|
+
);
|
|
1547
|
+
return import_types.ObservedDependenciesResultSchema.parse({
|
|
1548
|
+
origin: nodeId,
|
|
1549
|
+
dependencies,
|
|
1550
|
+
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
1551
|
+
inboundObservedCount,
|
|
1552
|
+
hasExtractedOutbound
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1333
1555
|
|
|
1334
1556
|
// src/policy.ts
|
|
1335
1557
|
var DEFAULT_ACTION_BY_SEVERITY = {
|
|
@@ -1599,6 +1821,130 @@ function evaluateAllPolicies(graph, policies, ctx) {
|
|
|
1599
1821
|
}
|
|
1600
1822
|
return out;
|
|
1601
1823
|
}
|
|
1824
|
+
function selectApplicablePolicies(graph, policies, nodeId) {
|
|
1825
|
+
if (!graph.hasNode(nodeId)) return [];
|
|
1826
|
+
const node = graph.getNodeAttributes(nodeId);
|
|
1827
|
+
const out = [];
|
|
1828
|
+
for (const policy of policies) {
|
|
1829
|
+
const m = matchPolicyToNode(graph, policy, nodeId, node);
|
|
1830
|
+
if (!m) continue;
|
|
1831
|
+
out.push({
|
|
1832
|
+
policyId: policy.id,
|
|
1833
|
+
policyName: policy.name,
|
|
1834
|
+
...policy.description !== void 0 ? { description: policy.description } : {},
|
|
1835
|
+
severity: policy.severity,
|
|
1836
|
+
onViolation: resolveOnViolation(policy),
|
|
1837
|
+
ruleType: policy.rule.type,
|
|
1838
|
+
match: m.match,
|
|
1839
|
+
reason: m.reason
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
return out;
|
|
1843
|
+
}
|
|
1844
|
+
function requiredProvenanceList(required) {
|
|
1845
|
+
if (Array.isArray(required)) return required.join(" | ");
|
|
1846
|
+
return String(required);
|
|
1847
|
+
}
|
|
1848
|
+
function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
|
|
1849
|
+
const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
|
|
1850
|
+
for (const edgeId of incident) {
|
|
1851
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1852
|
+
if (e.type !== edgeType) continue;
|
|
1853
|
+
if (requiredOtherEnd === void 0) return true;
|
|
1854
|
+
if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
|
|
1855
|
+
}
|
|
1856
|
+
return false;
|
|
1857
|
+
}
|
|
1858
|
+
function blastRadiusSubjectReaching(graph, rule, nodeId) {
|
|
1859
|
+
let found = null;
|
|
1860
|
+
graph.forEachNode((subjId, attrs) => {
|
|
1861
|
+
if (found !== null) return;
|
|
1862
|
+
if (subjId === nodeId) return;
|
|
1863
|
+
if (attrs.type !== rule.nodeType) return;
|
|
1864
|
+
const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
|
|
1865
|
+
if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
|
|
1866
|
+
});
|
|
1867
|
+
return found;
|
|
1868
|
+
}
|
|
1869
|
+
function matchPolicyToNode(graph, policy, nodeId, node) {
|
|
1870
|
+
const rule = policy.rule;
|
|
1871
|
+
switch (rule.type) {
|
|
1872
|
+
case "structural": {
|
|
1873
|
+
if (node.type === rule.fromNodeType) {
|
|
1874
|
+
return {
|
|
1875
|
+
match: "subject",
|
|
1876
|
+
reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
if (node.type === rule.toNodeType) {
|
|
1880
|
+
return {
|
|
1881
|
+
match: "region",
|
|
1882
|
+
reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
return null;
|
|
1886
|
+
}
|
|
1887
|
+
case "ownership": {
|
|
1888
|
+
if (node.type === rule.nodeType) {
|
|
1889
|
+
return {
|
|
1890
|
+
match: "subject",
|
|
1891
|
+
reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
return null;
|
|
1895
|
+
}
|
|
1896
|
+
case "blast-radius": {
|
|
1897
|
+
const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
|
|
1898
|
+
if (node.type === rule.nodeType) {
|
|
1899
|
+
return {
|
|
1900
|
+
match: "subject",
|
|
1901
|
+
reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
|
|
1905
|
+
if (subject) {
|
|
1906
|
+
return {
|
|
1907
|
+
match: "region",
|
|
1908
|
+
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`
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
return null;
|
|
1912
|
+
}
|
|
1913
|
+
case "compatibility": {
|
|
1914
|
+
const kindLabel = rule.kind ?? "all compat shapes";
|
|
1915
|
+
if (node.type === import_types2.NodeType.ServiceNode) {
|
|
1916
|
+
return {
|
|
1917
|
+
match: "subject",
|
|
1918
|
+
reason: `this service's dependencies are compatibility-checked (${kindLabel})`
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
|
|
1922
|
+
if (reachesDriverEngine && node.type === import_types2.NodeType.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, import_types2.EdgeType.CONNECTS_TO)) {
|
|
1923
|
+
return {
|
|
1924
|
+
match: "region",
|
|
1925
|
+
reason: "services connecting to this database have their driver/engine compatibility checked against it"
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
return null;
|
|
1929
|
+
}
|
|
1930
|
+
case "provenance": {
|
|
1931
|
+
const requiredList = requiredProvenanceList(rule.required);
|
|
1932
|
+
if (rule.targetNodeId !== void 0 && nodeId === rule.targetNodeId) {
|
|
1933
|
+
return {
|
|
1934
|
+
match: "subject",
|
|
1935
|
+
reason: `every ${rule.edgeType} edge into ${rule.targetNodeId} must carry ${requiredList} provenance`
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
if (nodeTouchesEdgeType(graph, nodeId, rule.edgeType, rule.targetNodeId)) {
|
|
1939
|
+
return {
|
|
1940
|
+
match: "region",
|
|
1941
|
+
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`
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1602
1948
|
async function loadPolicyFile(policyPath) {
|
|
1603
1949
|
let raw;
|
|
1604
1950
|
try {
|
|
@@ -1685,9 +2031,9 @@ function loadIncidentThresholdsFromEnv() {
|
|
|
1685
2031
|
return DEFAULT_INCIDENT_THRESHOLDS;
|
|
1686
2032
|
}
|
|
1687
2033
|
}
|
|
1688
|
-
function
|
|
2034
|
+
function httpResponseStatusFromAttrs(attrs) {
|
|
1689
2035
|
for (const key of ["http.response.status_code", "http.status_code"]) {
|
|
1690
|
-
const v =
|
|
2036
|
+
const v = attrs[key];
|
|
1691
2037
|
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
1692
2038
|
if (typeof v === "string") {
|
|
1693
2039
|
const n = Number(v);
|
|
@@ -1696,6 +2042,63 @@ function httpResponseStatus(span) {
|
|
|
1696
2042
|
}
|
|
1697
2043
|
return void 0;
|
|
1698
2044
|
}
|
|
2045
|
+
function httpResponseStatus(span) {
|
|
2046
|
+
return httpResponseStatusFromAttrs(span.attributes);
|
|
2047
|
+
}
|
|
2048
|
+
function httpFailureMessageFromAttrs(attrs) {
|
|
2049
|
+
const status2 = httpResponseStatusFromAttrs(attrs);
|
|
2050
|
+
const route = pickAttrFrom(attrs, "http.route", "http.target", "url.path");
|
|
2051
|
+
const method = pickAttrFrom(attrs, "http.request.method", "http.method");
|
|
2052
|
+
const where = route ? `${method ? `${method} ` : ""}${route}` : void 0;
|
|
2053
|
+
if (status2 !== void 0 && where) return `${status2} on ${where}`;
|
|
2054
|
+
if (status2 !== void 0) return `HTTP ${status2}`;
|
|
2055
|
+
if (where) return `error on ${where}`;
|
|
2056
|
+
return void 0;
|
|
2057
|
+
}
|
|
2058
|
+
var GRPC_STATUS_NAMES = {
|
|
2059
|
+
1: "CANCELLED",
|
|
2060
|
+
2: "UNKNOWN",
|
|
2061
|
+
3: "INVALID_ARGUMENT",
|
|
2062
|
+
4: "DEADLINE_EXCEEDED",
|
|
2063
|
+
5: "NOT_FOUND",
|
|
2064
|
+
6: "ALREADY_EXISTS",
|
|
2065
|
+
7: "PERMISSION_DENIED",
|
|
2066
|
+
8: "RESOURCE_EXHAUSTED",
|
|
2067
|
+
9: "FAILED_PRECONDITION",
|
|
2068
|
+
10: "ABORTED",
|
|
2069
|
+
11: "OUT_OF_RANGE",
|
|
2070
|
+
12: "UNIMPLEMENTED",
|
|
2071
|
+
13: "INTERNAL",
|
|
2072
|
+
14: "UNAVAILABLE",
|
|
2073
|
+
15: "DATA_LOSS",
|
|
2074
|
+
16: "UNAUTHENTICATED"
|
|
2075
|
+
};
|
|
2076
|
+
function grpcStatusCodeFromAttrs(attrs) {
|
|
2077
|
+
const v = attrs["rpc.grpc.status_code"];
|
|
2078
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
2079
|
+
if (typeof v === "string") {
|
|
2080
|
+
const n = Number(v);
|
|
2081
|
+
if (Number.isFinite(n)) return n;
|
|
2082
|
+
}
|
|
2083
|
+
return void 0;
|
|
2084
|
+
}
|
|
2085
|
+
function nonHttpFailureMessageFromAttrs(attrs) {
|
|
2086
|
+
const grpc2 = grpcStatusCodeFromAttrs(attrs);
|
|
2087
|
+
if (grpc2 !== void 0 && grpc2 !== 0) {
|
|
2088
|
+
const name = GRPC_STATUS_NAMES[grpc2] ?? `status ${grpc2}`;
|
|
2089
|
+
const detail = pickAttrFrom(attrs, "rpc.grpc.status_message");
|
|
2090
|
+
return detail ? `gRPC ${name}: ${detail}` : `gRPC ${name}`;
|
|
2091
|
+
}
|
|
2092
|
+
const errType = pickAttrFrom(attrs, "error.type");
|
|
2093
|
+
if (errType && errType !== "_OTHER" && !/^\d+$/.test(errType)) {
|
|
2094
|
+
const peer = pickAttrFrom(attrs, "server.address", "net.peer.name", "net.host.name");
|
|
2095
|
+
return peer ? `${errType} connecting to ${peer}` : errType;
|
|
2096
|
+
}
|
|
2097
|
+
return void 0;
|
|
2098
|
+
}
|
|
2099
|
+
function incidentMessage(span) {
|
|
2100
|
+
return span.exception?.message ?? httpFailureMessageFromAttrs(span.attributes) ?? nonHttpFailureMessageFromAttrs(span.attributes) ?? "unknown error";
|
|
2101
|
+
}
|
|
1699
2102
|
function nowIso(ctx) {
|
|
1700
2103
|
return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
|
|
1701
2104
|
}
|
|
@@ -1715,13 +2118,16 @@ function warnNoSourceMaps(serviceName) {
|
|
|
1715
2118
|
`[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.`
|
|
1716
2119
|
);
|
|
1717
2120
|
}
|
|
1718
|
-
function
|
|
2121
|
+
function pickAttrFrom(attrs, ...keys) {
|
|
1719
2122
|
for (const k of keys) {
|
|
1720
|
-
const v =
|
|
2123
|
+
const v = attrs[k];
|
|
1721
2124
|
if (typeof v === "string" && v.length > 0) return v;
|
|
1722
2125
|
}
|
|
1723
2126
|
return void 0;
|
|
1724
2127
|
}
|
|
2128
|
+
function pickAttr(span, ...keys) {
|
|
2129
|
+
return pickAttrFrom(span.attributes, ...keys);
|
|
2130
|
+
}
|
|
1725
2131
|
function hostFromUrl(u) {
|
|
1726
2132
|
if (!u) return void 0;
|
|
1727
2133
|
try {
|
|
@@ -1733,6 +2139,10 @@ function hostFromUrl(u) {
|
|
|
1733
2139
|
function pickAddress(span) {
|
|
1734
2140
|
return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
|
|
1735
2141
|
}
|
|
2142
|
+
function isLoopbackHost(host) {
|
|
2143
|
+
const h = host.toLowerCase();
|
|
2144
|
+
return h === "localhost" || h === "ip6-localhost" || h === "::1" || h === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(h);
|
|
2145
|
+
}
|
|
1736
2146
|
var CODE_FILEPATH_ATTR = "code.filepath";
|
|
1737
2147
|
var CODE_LINENO_ATTR = "code.lineno";
|
|
1738
2148
|
var CODE_FUNCTION_ATTR = "code.function";
|
|
@@ -1840,15 +2250,31 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
|
|
|
1840
2250
|
...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
|
|
1841
2251
|
};
|
|
1842
2252
|
}
|
|
2253
|
+
function reconcileObservedRelPath(graph, serviceName, relPath) {
|
|
2254
|
+
if (graph.hasNode((0, import_types3.fileId)(serviceName, relPath))) return relPath;
|
|
2255
|
+
let best = null;
|
|
2256
|
+
graph.forEachNode((_id, attrs) => {
|
|
2257
|
+
const a = attrs;
|
|
2258
|
+
if (a.type !== import_types3.NodeType.FileNode || a.service !== serviceName) return;
|
|
2259
|
+
if (a.discoveredVia === "otel") return;
|
|
2260
|
+
const p = a.path;
|
|
2261
|
+
if (!p) return;
|
|
2262
|
+
if ((relPath === p || relPath.endsWith(`/${p}`)) && (!best || p.length > best.length)) {
|
|
2263
|
+
best = p;
|
|
2264
|
+
}
|
|
2265
|
+
});
|
|
2266
|
+
return best ?? relPath;
|
|
2267
|
+
}
|
|
1843
2268
|
function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
1844
|
-
const
|
|
2269
|
+
const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
|
|
2270
|
+
const fileNodeId = (0, import_types3.fileId)(serviceName, relPath);
|
|
1845
2271
|
if (!graph.hasNode(fileNodeId)) {
|
|
1846
|
-
const language = languageForExt(
|
|
2272
|
+
const language = languageForExt(relPath);
|
|
1847
2273
|
const node = {
|
|
1848
2274
|
id: fileNodeId,
|
|
1849
2275
|
type: import_types3.NodeType.FileNode,
|
|
1850
2276
|
service: serviceName,
|
|
1851
|
-
path:
|
|
2277
|
+
path: relPath,
|
|
1852
2278
|
...language ? { language } : {},
|
|
1853
2279
|
...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
|
|
1854
2280
|
discoveredVia: "otel"
|
|
@@ -1876,6 +2302,11 @@ function makeInferredEdgeId(type, source, target) {
|
|
|
1876
2302
|
}
|
|
1877
2303
|
var INFERRED_CONFIDENCE = 0.6;
|
|
1878
2304
|
var STITCH_MAX_DEPTH = 2;
|
|
2305
|
+
var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
2306
|
+
import_types3.EdgeType.CALLS,
|
|
2307
|
+
import_types3.EdgeType.CONNECTS_TO,
|
|
2308
|
+
import_types3.EdgeType.DEPENDS_ON
|
|
2309
|
+
]);
|
|
1879
2310
|
var WIRE_SPAN_KIND_CLIENT = 3;
|
|
1880
2311
|
var WIRE_SPAN_KIND_PRODUCER = 4;
|
|
1881
2312
|
function spanMintsObservedEdge(kind) {
|
|
@@ -2049,6 +2480,7 @@ function stitchTrace(graph, sourceServiceId, ts) {
|
|
|
2049
2480
|
for (const edgeId of outbound) {
|
|
2050
2481
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
2051
2482
|
if (edge.provenance !== import_types3.Provenance.EXTRACTED) continue;
|
|
2483
|
+
if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
|
|
2052
2484
|
if (graph.hasEdge((0, import_types3.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
|
|
2053
2485
|
upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
|
|
2054
2486
|
if (!visited.has(edge.target)) {
|
|
@@ -2081,6 +2513,16 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
2081
2513
|
await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(ctx.errorsPath), { recursive: true });
|
|
2082
2514
|
await import_node_fs3.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
2083
2515
|
}
|
|
2516
|
+
function incidentAffectedNode(span, graph, scanPath) {
|
|
2517
|
+
const sid = (0, import_types3.serviceId)(span.service, span.env);
|
|
2518
|
+
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
2519
|
+
const callSite = callSiteFromSpan(span, serviceNode, scanPath);
|
|
2520
|
+
if (callSite) {
|
|
2521
|
+
const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
|
|
2522
|
+
return (0, import_types3.fileId)(span.service, relPath);
|
|
2523
|
+
}
|
|
2524
|
+
return sid;
|
|
2525
|
+
}
|
|
2084
2526
|
function sanitizeAttributes(attrs) {
|
|
2085
2527
|
const out = {};
|
|
2086
2528
|
for (const [k, v] of Object.entries(attrs)) {
|
|
@@ -2089,7 +2531,7 @@ function sanitizeAttributes(attrs) {
|
|
|
2089
2531
|
}
|
|
2090
2532
|
return out;
|
|
2091
2533
|
}
|
|
2092
|
-
function buildErrorEventForReceiver(span) {
|
|
2534
|
+
function buildErrorEventForReceiver(span, graph, scanPath) {
|
|
2093
2535
|
if (span.statusCode !== 2) return null;
|
|
2094
2536
|
const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2095
2537
|
const attrs = sanitizeAttributes(span.attributes);
|
|
@@ -2099,16 +2541,16 @@ function buildErrorEventForReceiver(span) {
|
|
|
2099
2541
|
service: span.service,
|
|
2100
2542
|
traceId: span.traceId,
|
|
2101
2543
|
spanId: span.spanId,
|
|
2102
|
-
errorMessage: span
|
|
2544
|
+
errorMessage: incidentMessage(span),
|
|
2103
2545
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2104
2546
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2105
2547
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
2106
|
-
affectedNode: (
|
|
2548
|
+
affectedNode: incidentAffectedNode(span, graph, scanPath)
|
|
2107
2549
|
};
|
|
2108
2550
|
}
|
|
2109
|
-
function makeErrorSpanWriter(errorsPath) {
|
|
2551
|
+
function makeErrorSpanWriter(errorsPath, graph, scanPath) {
|
|
2110
2552
|
return async (span) => {
|
|
2111
|
-
const ev = buildErrorEventForReceiver(span);
|
|
2553
|
+
const ev = buildErrorEventForReceiver(span, graph, scanPath);
|
|
2112
2554
|
if (!ev) return;
|
|
2113
2555
|
await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(errorsPath), { recursive: true });
|
|
2114
2556
|
await import_node_fs3.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
@@ -2192,7 +2634,10 @@ async function handleSpan(ctx, span) {
|
|
|
2192
2634
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
2193
2635
|
cacheSpanService(span, nowMs, callSite);
|
|
2194
2636
|
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
2195
|
-
const callSiteEvidence = callSite ? {
|
|
2637
|
+
const callSiteEvidence = callSite ? {
|
|
2638
|
+
file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
|
|
2639
|
+
...callSite.line !== void 0 ? { line: callSite.line } : {}
|
|
2640
|
+
} : void 0;
|
|
2196
2641
|
let affectedNode = sourceId;
|
|
2197
2642
|
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
2198
2643
|
if (span.dbSystem) {
|
|
@@ -2214,7 +2659,7 @@ async function handleSpan(ctx, span) {
|
|
|
2214
2659
|
} else {
|
|
2215
2660
|
const host = pickAddress(span);
|
|
2216
2661
|
let resolvedViaAddress = false;
|
|
2217
|
-
if (mintsFromCallerSide && host && host !== span.service) {
|
|
2662
|
+
if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
|
|
2218
2663
|
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
2219
2664
|
if (targetId && targetId !== sourceId) {
|
|
2220
2665
|
upsertObservedEdge(
|
|
@@ -2249,7 +2694,11 @@ async function handleSpan(ctx, span) {
|
|
|
2249
2694
|
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
2250
2695
|
const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
|
|
2251
2696
|
const fallbackEvidence = parent.callSite ? {
|
|
2252
|
-
file:
|
|
2697
|
+
file: reconcileObservedRelPath(
|
|
2698
|
+
ctx.graph,
|
|
2699
|
+
parent.service,
|
|
2700
|
+
parent.callSite.relPath
|
|
2701
|
+
),
|
|
2253
2702
|
...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
|
|
2254
2703
|
} : void 0;
|
|
2255
2704
|
upsertObservedEdge(
|
|
@@ -2274,7 +2723,7 @@ async function handleSpan(ctx, span) {
|
|
|
2274
2723
|
service: span.service,
|
|
2275
2724
|
traceId: span.traceId,
|
|
2276
2725
|
spanId: span.spanId,
|
|
2277
|
-
errorMessage: span
|
|
2726
|
+
errorMessage: incidentMessage(span),
|
|
2278
2727
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2279
2728
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2280
2729
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
@@ -2377,12 +2826,43 @@ async function readStaleEvents(staleEventsPath) {
|
|
|
2377
2826
|
async function readErrorEvents(errorsPath) {
|
|
2378
2827
|
try {
|
|
2379
2828
|
const raw = await import_node_fs3.promises.readFile(errorsPath, "utf8");
|
|
2380
|
-
|
|
2829
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
2830
|
+
return dedupeIncidents(events);
|
|
2381
2831
|
} catch (err) {
|
|
2382
2832
|
if (err.code === "ENOENT") return [];
|
|
2383
2833
|
throw err;
|
|
2384
2834
|
}
|
|
2385
2835
|
}
|
|
2836
|
+
function isSynthesizedHttpIncident(ev) {
|
|
2837
|
+
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
2838
|
+
if (ev.errorType) return false;
|
|
2839
|
+
if (!ev.attributes) return false;
|
|
2840
|
+
const synth = httpFailureMessageFromAttrs(ev.attributes);
|
|
2841
|
+
return synth !== void 0 && synth === ev.errorMessage;
|
|
2842
|
+
}
|
|
2843
|
+
function dedupeIncidents(events) {
|
|
2844
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2845
|
+
const once = [];
|
|
2846
|
+
for (const ev of events) {
|
|
2847
|
+
const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
|
|
2848
|
+
if (key === void 0) {
|
|
2849
|
+
once.push(ev);
|
|
2850
|
+
continue;
|
|
2851
|
+
}
|
|
2852
|
+
if (seen.has(key)) continue;
|
|
2853
|
+
seen.add(key);
|
|
2854
|
+
once.push(ev);
|
|
2855
|
+
}
|
|
2856
|
+
const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
|
|
2857
|
+
const hasRealFailure = /* @__PURE__ */ new Set();
|
|
2858
|
+
for (const ev of once) {
|
|
2859
|
+
if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
|
|
2860
|
+
}
|
|
2861
|
+
return once.filter((ev) => {
|
|
2862
|
+
if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
|
|
2863
|
+
return !hasRealFailure.has(groupKey(ev));
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2386
2866
|
function mergeSnapshot(graph, snapshot) {
|
|
2387
2867
|
const exported = snapshot.graph;
|
|
2388
2868
|
let nodesAdded = 0;
|
|
@@ -2463,10 +2943,17 @@ function isConfigFile(name) {
|
|
|
2463
2943
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
2464
2944
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
2465
2945
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
2946
|
+
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
2466
2947
|
return { match: true, fileType: "env" };
|
|
2467
2948
|
}
|
|
2468
2949
|
return { match: false, fileType: "" };
|
|
2469
2950
|
}
|
|
2951
|
+
function isNeatAuthoredEnvFile(name) {
|
|
2952
|
+
return name === ".env.neat";
|
|
2953
|
+
}
|
|
2954
|
+
function isNeatAuthoredSourceFile(name) {
|
|
2955
|
+
return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
|
|
2956
|
+
}
|
|
2470
2957
|
function isTestPath(filePath) {
|
|
2471
2958
|
const normalised = filePath.replace(/\\/g, "/");
|
|
2472
2959
|
const segments = normalised.split("/");
|
|
@@ -3160,7 +3647,9 @@ async function walkSourceFiles(dir) {
|
|
|
3160
3647
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
3161
3648
|
if (await isPythonVenvDir(full)) continue;
|
|
3162
3649
|
await walk(full);
|
|
3163
|
-
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry2.name))
|
|
3650
|
+
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry2.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
|
|
3651
|
+
// would attribute our instrumentation imports to the user's service.
|
|
3652
|
+
!isNeatAuthoredSourceFile(entry2.name)) {
|
|
3164
3653
|
out.push(full);
|
|
3165
3654
|
}
|
|
3166
3655
|
}
|
|
@@ -3330,11 +3819,28 @@ function collectJsImports(node, out) {
|
|
|
3330
3819
|
if (child) collectJsImports(child, out);
|
|
3331
3820
|
}
|
|
3332
3821
|
}
|
|
3822
|
+
function collectImportedNames(node, out) {
|
|
3823
|
+
if (node.type === "aliased_import") {
|
|
3824
|
+
const nameNode = node.childForFieldName("name");
|
|
3825
|
+
if (nameNode) out.push(nameNode.text);
|
|
3826
|
+
return;
|
|
3827
|
+
}
|
|
3828
|
+
if (node.type === "dotted_name") {
|
|
3829
|
+
out.push(node.text);
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3833
|
+
const child = node.namedChild(i);
|
|
3834
|
+
if (child) collectImportedNames(child, out);
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3333
3837
|
function collectPyImports(node, out) {
|
|
3334
3838
|
if (node.type === "import_from_statement") {
|
|
3335
3839
|
let level = 0;
|
|
3336
3840
|
let modulePath = "";
|
|
3841
|
+
const names = [];
|
|
3337
3842
|
let pastFrom = false;
|
|
3843
|
+
let pastImport = false;
|
|
3338
3844
|
for (let i = 0; i < node.childCount; i++) {
|
|
3339
3845
|
const child = node.child(i);
|
|
3340
3846
|
if (!child) continue;
|
|
@@ -3342,26 +3848,30 @@ function collectPyImports(node, out) {
|
|
|
3342
3848
|
if (child.type === "from") pastFrom = true;
|
|
3343
3849
|
continue;
|
|
3344
3850
|
}
|
|
3345
|
-
if (
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
if (!rc) continue;
|
|
3350
|
-
if (rc.type === "import_prefix") {
|
|
3351
|
-
for (let k = 0; k < rc.childCount; k++) {
|
|
3352
|
-
if (rc.child(k)?.type === ".") level++;
|
|
3353
|
-
}
|
|
3354
|
-
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
3851
|
+
if (!pastImport) {
|
|
3852
|
+
if (child.type === "import") {
|
|
3853
|
+
pastImport = true;
|
|
3854
|
+
continue;
|
|
3355
3855
|
}
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3856
|
+
if (child.type === "relative_import") {
|
|
3857
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
3858
|
+
const rc = child.child(j);
|
|
3859
|
+
if (!rc) continue;
|
|
3860
|
+
if (rc.type === "import_prefix") {
|
|
3861
|
+
for (let k = 0; k < rc.childCount; k++) {
|
|
3862
|
+
if (rc.child(k)?.type === ".") level++;
|
|
3863
|
+
}
|
|
3864
|
+
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
3865
|
+
}
|
|
3866
|
+
} else if (child.type === "dotted_name") {
|
|
3867
|
+
modulePath = child.text;
|
|
3868
|
+
}
|
|
3869
|
+
continue;
|
|
3361
3870
|
}
|
|
3871
|
+
collectImportedNames(child, names);
|
|
3362
3872
|
}
|
|
3363
3873
|
if (level > 0 || modulePath) {
|
|
3364
|
-
out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
3874
|
+
out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
3365
3875
|
}
|
|
3366
3876
|
}
|
|
3367
3877
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -3463,8 +3973,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
|
|
|
3463
3973
|
return null;
|
|
3464
3974
|
}
|
|
3465
3975
|
async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
3466
|
-
if (!imp.modulePath) return null;
|
|
3467
|
-
const relPath = imp.modulePath.split(".").join("/");
|
|
3468
3976
|
let baseDir;
|
|
3469
3977
|
if (imp.level > 0) {
|
|
3470
3978
|
baseDir = import_node_path12.default.dirname(importerPath);
|
|
@@ -3472,13 +3980,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
|
3472
3980
|
} else {
|
|
3473
3981
|
baseDir = serviceDir;
|
|
3474
3982
|
}
|
|
3475
|
-
const
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3983
|
+
const moduleBase = imp.modulePath ? import_node_path12.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
|
|
3984
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
3985
|
+
let needModuleFile = imp.names.length === 0;
|
|
3986
|
+
for (const name of imp.names) {
|
|
3987
|
+
const submoduleFile = import_node_path12.default.join(moduleBase, `${name}.py`);
|
|
3988
|
+
const subpackageInit = import_node_path12.default.join(moduleBase, name, "__init__.py");
|
|
3989
|
+
if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
|
|
3990
|
+
resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, submoduleFile)));
|
|
3991
|
+
} else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
|
|
3992
|
+
resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, subpackageInit)));
|
|
3993
|
+
} else {
|
|
3994
|
+
needModuleFile = true;
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
if (needModuleFile) {
|
|
3998
|
+
const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path12.default.join(moduleBase, "__init__.py")] : [import_node_path12.default.join(moduleBase, "__init__.py")];
|
|
3999
|
+
for (const candidate of moduleFileCandidates) {
|
|
4000
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
4001
|
+
resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, candidate)));
|
|
4002
|
+
break;
|
|
4003
|
+
}
|
|
3479
4004
|
}
|
|
3480
4005
|
}
|
|
3481
|
-
return
|
|
4006
|
+
return [...resolved];
|
|
3482
4007
|
}
|
|
3483
4008
|
function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
|
|
3484
4009
|
const importeeFileId = (0, import_types8.fileId)(serviceName, importeeRelPath);
|
|
@@ -3519,17 +4044,18 @@ async function addImports(graph, services) {
|
|
|
3519
4044
|
continue;
|
|
3520
4045
|
}
|
|
3521
4046
|
for (const imp of pyImports) {
|
|
3522
|
-
const
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
4047
|
+
const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
|
|
4048
|
+
for (const resolved of resolvedPaths) {
|
|
4049
|
+
edgesAdded += emitImportEdge(
|
|
4050
|
+
graph,
|
|
4051
|
+
service.pkg.name,
|
|
4052
|
+
importerFileId,
|
|
4053
|
+
relFile,
|
|
4054
|
+
resolved,
|
|
4055
|
+
imp.line,
|
|
4056
|
+
imp.snippet
|
|
4057
|
+
);
|
|
4058
|
+
}
|
|
3533
4059
|
}
|
|
3534
4060
|
continue;
|
|
3535
4061
|
}
|
|
@@ -4165,6 +4691,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4165
4691
|
edgesAdded++;
|
|
4166
4692
|
}
|
|
4167
4693
|
}
|
|
4694
|
+
if (allConfigs.length === 1) {
|
|
4695
|
+
const primary = allConfigs[0];
|
|
4696
|
+
service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
|
|
4697
|
+
const relPath = import_node_path20.default.relative(scanPath, primary.sourceFile);
|
|
4698
|
+
const cfgId = (0, import_types9.configId)(relPath);
|
|
4699
|
+
if (!graph.hasNode(cfgId)) {
|
|
4700
|
+
const cfgNode = {
|
|
4701
|
+
id: cfgId,
|
|
4702
|
+
type: import_types9.NodeType.ConfigNode,
|
|
4703
|
+
name: import_node_path20.default.basename(primary.sourceFile),
|
|
4704
|
+
path: relPath,
|
|
4705
|
+
fileType: isConfigFile(import_node_path20.default.basename(primary.sourceFile)).fileType || "config"
|
|
4706
|
+
};
|
|
4707
|
+
graph.addNode(cfgId, cfgNode);
|
|
4708
|
+
nodesAdded++;
|
|
4709
|
+
}
|
|
4710
|
+
const cfgEdge = {
|
|
4711
|
+
id: (0, import_types4.extractedEdgeId)(service.node.id, cfgId, import_types9.EdgeType.CONFIGURED_BY),
|
|
4712
|
+
source: service.node.id,
|
|
4713
|
+
target: cfgId,
|
|
4714
|
+
type: import_types9.EdgeType.CONFIGURED_BY,
|
|
4715
|
+
provenance: import_types9.Provenance.EXTRACTED,
|
|
4716
|
+
confidence: (0, import_types9.confidenceForExtracted)("structural"),
|
|
4717
|
+
evidence: { file: toPosix2(relPath) }
|
|
4718
|
+
};
|
|
4719
|
+
if (!graph.hasEdge(cfgEdge.id)) {
|
|
4720
|
+
graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
|
|
4721
|
+
edgesAdded++;
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4168
4724
|
attachIncompatibilities(service, allConfigs);
|
|
4169
4725
|
if (graph.hasNode(service.node.id)) {
|
|
4170
4726
|
const current = graph.getNodeAttributes(service.node.id);
|
|
@@ -4176,6 +4732,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4176
4732
|
if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
|
|
4177
4733
|
delete updated.incompatibilities;
|
|
4178
4734
|
}
|
|
4735
|
+
if (!service.node.dbConnectionTarget) {
|
|
4736
|
+
delete updated.dbConnectionTarget;
|
|
4737
|
+
}
|
|
4179
4738
|
graph.replaceNodeAttributes(service.node.id, updated);
|
|
4180
4739
|
}
|
|
4181
4740
|
}
|
|
@@ -4353,7 +4912,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4353
4912
|
const dedupKey = `${relFile}|${targetId}`;
|
|
4354
4913
|
if (seen.has(dedupKey)) continue;
|
|
4355
4914
|
seen.add(dedupKey);
|
|
4356
|
-
const confidence = (0, import_types11.confidenceForExtracted)("
|
|
4915
|
+
const confidence = (0, import_types11.confidenceForExtracted)("url-literal-service-target");
|
|
4357
4916
|
const ev = {
|
|
4358
4917
|
file: relFile,
|
|
4359
4918
|
line: site.line,
|
|
@@ -4373,7 +4932,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4373
4932
|
target: targetId,
|
|
4374
4933
|
type: import_types11.EdgeType.CALLS,
|
|
4375
4934
|
confidence,
|
|
4376
|
-
confidenceKind: "
|
|
4935
|
+
confidenceKind: "url-literal-service-target",
|
|
4377
4936
|
evidence: ev
|
|
4378
4937
|
});
|
|
4379
4938
|
continue;
|
|
@@ -4879,19 +5438,29 @@ init_cjs_shims();
|
|
|
4879
5438
|
var import_node_path29 = __toESM(require("path"), 1);
|
|
4880
5439
|
var import_node_fs15 = require("fs");
|
|
4881
5440
|
var import_types20 = require("@neat.is/types");
|
|
4882
|
-
function
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
5441
|
+
function readDockerfile(content) {
|
|
5442
|
+
let image = null;
|
|
5443
|
+
const ports = [];
|
|
5444
|
+
let cmd = null;
|
|
5445
|
+
let entrypoint = null;
|
|
5446
|
+
for (const raw of content.split("\n")) {
|
|
4886
5447
|
const line = raw.trim();
|
|
4887
5448
|
if (!line || line.startsWith("#")) continue;
|
|
4888
|
-
if (
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
if (
|
|
4892
|
-
|
|
5449
|
+
if (/^from\s+/i.test(line)) {
|
|
5450
|
+
const candidate = line.split(/\s+/)[1];
|
|
5451
|
+
if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
|
|
5452
|
+
} else if (/^expose\s+/i.test(line)) {
|
|
5453
|
+
for (const token of line.split(/\s+/).slice(1)) {
|
|
5454
|
+
const port = Number.parseInt(token.split("/")[0], 10);
|
|
5455
|
+
if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
|
|
5456
|
+
}
|
|
5457
|
+
} else if (/^entrypoint\s+/i.test(line)) {
|
|
5458
|
+
entrypoint = line;
|
|
5459
|
+
} else if (/^cmd\s+/i.test(line)) {
|
|
5460
|
+
cmd = line;
|
|
5461
|
+
}
|
|
4893
5462
|
}
|
|
4894
|
-
return
|
|
5463
|
+
return { image, ports, entrypoint: entrypoint ?? cmd };
|
|
4895
5464
|
}
|
|
4896
5465
|
async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
4897
5466
|
let nodesAdded = 0;
|
|
@@ -4910,14 +5479,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4910
5479
|
);
|
|
4911
5480
|
continue;
|
|
4912
5481
|
}
|
|
4913
|
-
const
|
|
4914
|
-
if (!image) continue;
|
|
4915
|
-
const node = makeInfraNode("container-image", image);
|
|
5482
|
+
const facts = readDockerfile(content);
|
|
5483
|
+
if (!facts.image) continue;
|
|
5484
|
+
const node = makeInfraNode("container-image", facts.image);
|
|
4916
5485
|
if (!graph.hasNode(node.id)) {
|
|
4917
5486
|
graph.addNode(node.id, node);
|
|
4918
5487
|
nodesAdded++;
|
|
4919
5488
|
}
|
|
4920
5489
|
const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
|
|
5490
|
+
const evidenceFile = toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath));
|
|
4921
5491
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
4922
5492
|
graph,
|
|
4923
5493
|
service.pkg.name,
|
|
@@ -4936,12 +5506,33 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4936
5506
|
provenance: import_types20.Provenance.EXTRACTED,
|
|
4937
5507
|
confidence: (0, import_types20.confidenceForExtracted)("structural"),
|
|
4938
5508
|
evidence: {
|
|
4939
|
-
file:
|
|
5509
|
+
file: evidenceFile,
|
|
5510
|
+
...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
|
|
4940
5511
|
}
|
|
4941
5512
|
};
|
|
4942
5513
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
4943
5514
|
edgesAdded++;
|
|
4944
5515
|
}
|
|
5516
|
+
for (const port of facts.ports) {
|
|
5517
|
+
const portNode = makeInfraNode("port", String(port));
|
|
5518
|
+
if (!graph.hasNode(portNode.id)) {
|
|
5519
|
+
graph.addNode(portNode.id, portNode);
|
|
5520
|
+
nodesAdded++;
|
|
5521
|
+
}
|
|
5522
|
+
const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types20.EdgeType.CONNECTS_TO);
|
|
5523
|
+
if (graph.hasEdge(portEdgeId)) continue;
|
|
5524
|
+
const portEdge = {
|
|
5525
|
+
id: portEdgeId,
|
|
5526
|
+
source: fileNodeId,
|
|
5527
|
+
target: portNode.id,
|
|
5528
|
+
type: import_types20.EdgeType.CONNECTS_TO,
|
|
5529
|
+
provenance: import_types20.Provenance.EXTRACTED,
|
|
5530
|
+
confidence: (0, import_types20.confidenceForExtracted)("structural"),
|
|
5531
|
+
evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
|
|
5532
|
+
};
|
|
5533
|
+
graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
|
|
5534
|
+
edgesAdded++;
|
|
5535
|
+
}
|
|
4945
5536
|
}
|
|
4946
5537
|
return { nodesAdded, edgesAdded };
|
|
4947
5538
|
}
|
|
@@ -4950,7 +5541,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4950
5541
|
init_cjs_shims();
|
|
4951
5542
|
var import_node_fs16 = require("fs");
|
|
4952
5543
|
var import_node_path30 = __toESM(require("path"), 1);
|
|
5544
|
+
var import_types21 = require("@neat.is/types");
|
|
4953
5545
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
5546
|
+
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
4954
5547
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
4955
5548
|
if (depth > max) return [];
|
|
4956
5549
|
const out = [];
|
|
@@ -4967,24 +5560,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
4967
5560
|
}
|
|
4968
5561
|
return out;
|
|
4969
5562
|
}
|
|
5563
|
+
function blockBody(content, from) {
|
|
5564
|
+
const open = content.indexOf("{", from);
|
|
5565
|
+
if (open === -1) return null;
|
|
5566
|
+
let depth = 0;
|
|
5567
|
+
for (let i = open; i < content.length; i++) {
|
|
5568
|
+
const ch = content[i];
|
|
5569
|
+
if (ch === "{") depth++;
|
|
5570
|
+
else if (ch === "}") {
|
|
5571
|
+
depth--;
|
|
5572
|
+
if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
|
|
5573
|
+
}
|
|
5574
|
+
}
|
|
5575
|
+
return null;
|
|
5576
|
+
}
|
|
5577
|
+
function lineAt(content, index) {
|
|
5578
|
+
let line = 1;
|
|
5579
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
5580
|
+
if (content[i] === "\n") line++;
|
|
5581
|
+
}
|
|
5582
|
+
return line;
|
|
5583
|
+
}
|
|
4970
5584
|
async function addTerraformResources(graph, scanPath) {
|
|
4971
5585
|
let nodesAdded = 0;
|
|
5586
|
+
let edgesAdded = 0;
|
|
4972
5587
|
const files = await walkTfFiles(scanPath);
|
|
4973
5588
|
for (const file of files) {
|
|
4974
5589
|
const content = await import_node_fs16.promises.readFile(file, "utf8");
|
|
5590
|
+
const evidenceFile = toPosix2(import_node_path30.default.relative(scanPath, file));
|
|
5591
|
+
const resources = [];
|
|
5592
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
4975
5593
|
RESOURCE_RE.lastIndex = 0;
|
|
4976
5594
|
let m;
|
|
4977
5595
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
4978
|
-
const
|
|
5596
|
+
const type = m[1];
|
|
4979
5597
|
const name = m[2];
|
|
4980
|
-
const node = makeInfraNode(
|
|
5598
|
+
const node = makeInfraNode(type, name, "aws");
|
|
4981
5599
|
if (!graph.hasNode(node.id)) {
|
|
4982
5600
|
graph.addNode(node.id, node);
|
|
4983
5601
|
nodesAdded++;
|
|
4984
5602
|
}
|
|
5603
|
+
const span = blockBody(content, RESOURCE_RE.lastIndex);
|
|
5604
|
+
const resource = {
|
|
5605
|
+
type,
|
|
5606
|
+
name,
|
|
5607
|
+
nodeId: node.id,
|
|
5608
|
+
body: span?.body ?? "",
|
|
5609
|
+
bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
|
|
5610
|
+
};
|
|
5611
|
+
resources.push(resource);
|
|
5612
|
+
byKey.set(`${type}.${name}`, resource);
|
|
5613
|
+
}
|
|
5614
|
+
for (const resource of resources) {
|
|
5615
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5616
|
+
REFERENCE_RE.lastIndex = 0;
|
|
5617
|
+
let ref;
|
|
5618
|
+
while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
|
|
5619
|
+
const key = `${ref[1]}.${ref[2]}`;
|
|
5620
|
+
if (key === `${resource.type}.${resource.name}`) continue;
|
|
5621
|
+
const target = byKey.get(key);
|
|
5622
|
+
if (!target) continue;
|
|
5623
|
+
if (seen.has(target.nodeId)) continue;
|
|
5624
|
+
seen.add(target.nodeId);
|
|
5625
|
+
const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types21.EdgeType.DEPENDS_ON);
|
|
5626
|
+
if (graph.hasEdge(edgeId)) continue;
|
|
5627
|
+
const line = lineAt(content, resource.bodyOffset + ref.index);
|
|
5628
|
+
const edge = {
|
|
5629
|
+
id: edgeId,
|
|
5630
|
+
source: resource.nodeId,
|
|
5631
|
+
target: target.nodeId,
|
|
5632
|
+
type: import_types21.EdgeType.DEPENDS_ON,
|
|
5633
|
+
provenance: import_types21.Provenance.EXTRACTED,
|
|
5634
|
+
confidence: (0, import_types21.confidenceForExtracted)("structural"),
|
|
5635
|
+
evidence: { file: evidenceFile, line, snippet: key }
|
|
5636
|
+
};
|
|
5637
|
+
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
5638
|
+
edgesAdded++;
|
|
5639
|
+
}
|
|
4985
5640
|
}
|
|
4986
5641
|
}
|
|
4987
|
-
return { nodesAdded, edgesAdded
|
|
5642
|
+
return { nodesAdded, edgesAdded };
|
|
4988
5643
|
}
|
|
4989
5644
|
|
|
4990
5645
|
// src/extract/infra/k8s.ts
|
|
@@ -5062,11 +5717,11 @@ var import_node_path33 = __toESM(require("path"), 1);
|
|
|
5062
5717
|
init_cjs_shims();
|
|
5063
5718
|
var import_node_fs18 = require("fs");
|
|
5064
5719
|
var import_node_path32 = __toESM(require("path"), 1);
|
|
5065
|
-
var
|
|
5720
|
+
var import_types22 = require("@neat.is/types");
|
|
5066
5721
|
function dropOrphanedFileNodes(graph) {
|
|
5067
5722
|
const orphans = [];
|
|
5068
5723
|
graph.forEachNode((id, attrs) => {
|
|
5069
|
-
if (attrs.type !==
|
|
5724
|
+
if (attrs.type !== import_types22.NodeType.FileNode) return;
|
|
5070
5725
|
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
5071
5726
|
orphans.push(id);
|
|
5072
5727
|
}
|
|
@@ -5079,7 +5734,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
5079
5734
|
const bases = [scanPath, ...serviceDirs];
|
|
5080
5735
|
graph.forEachEdge((id, attrs) => {
|
|
5081
5736
|
const edge = attrs;
|
|
5082
|
-
if (edge.provenance !==
|
|
5737
|
+
if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
|
|
5083
5738
|
const evidenceFile = edge.evidence?.file;
|
|
5084
5739
|
if (!evidenceFile) return;
|
|
5085
5740
|
if (import_node_path32.default.isAbsolute(evidenceFile)) {
|
|
@@ -5162,7 +5817,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5162
5817
|
init_cjs_shims();
|
|
5163
5818
|
var import_node_fs19 = require("fs");
|
|
5164
5819
|
var import_node_path34 = __toESM(require("path"), 1);
|
|
5165
|
-
var
|
|
5820
|
+
var import_types23 = require("@neat.is/types");
|
|
5166
5821
|
var SCHEMA_VERSION = 4;
|
|
5167
5822
|
function migrateV1ToV2(payload) {
|
|
5168
5823
|
const nodes = payload.graph.nodes;
|
|
@@ -5184,12 +5839,12 @@ function migrateV2ToV3(payload) {
|
|
|
5184
5839
|
for (const edge of edges) {
|
|
5185
5840
|
const attrs = edge.attributes;
|
|
5186
5841
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
5187
|
-
attrs.provenance =
|
|
5842
|
+
attrs.provenance = import_types23.Provenance.OBSERVED;
|
|
5188
5843
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
5189
5844
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
5190
5845
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
5191
5846
|
if (type && source && target) {
|
|
5192
|
-
const newId = (0,
|
|
5847
|
+
const newId = (0, import_types23.observedEdgeId)(source, target, type);
|
|
5193
5848
|
attrs.id = newId;
|
|
5194
5849
|
if (edge.key) edge.key = newId;
|
|
5195
5850
|
}
|
|
@@ -5333,7 +5988,7 @@ var Projects = class {
|
|
|
5333
5988
|
init_cjs_shims();
|
|
5334
5989
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
5335
5990
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
5336
|
-
var
|
|
5991
|
+
var import_types26 = require("@neat.is/types");
|
|
5337
5992
|
|
|
5338
5993
|
// src/extend/index.ts
|
|
5339
5994
|
init_cjs_shims();
|
|
@@ -5652,7 +6307,7 @@ async function rollbackExtension(ctx, args) {
|
|
|
5652
6307
|
|
|
5653
6308
|
// src/divergences.ts
|
|
5654
6309
|
init_cjs_shims();
|
|
5655
|
-
var
|
|
6310
|
+
var import_types24 = require("@neat.is/types");
|
|
5656
6311
|
function bucketKey(source, target, type) {
|
|
5657
6312
|
return `${type}|${source}|${target}`;
|
|
5658
6313
|
}
|
|
@@ -5660,22 +6315,22 @@ function bucketEdges(graph) {
|
|
|
5660
6315
|
const buckets = /* @__PURE__ */ new Map();
|
|
5661
6316
|
graph.forEachEdge((id, attrs) => {
|
|
5662
6317
|
const e = attrs;
|
|
5663
|
-
const parsed = (0,
|
|
6318
|
+
const parsed = (0, import_types24.parseEdgeId)(id);
|
|
5664
6319
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
5665
6320
|
const key = bucketKey(e.source, e.target, e.type);
|
|
5666
6321
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
5667
6322
|
switch (provenance) {
|
|
5668
|
-
case
|
|
6323
|
+
case import_types24.Provenance.EXTRACTED:
|
|
5669
6324
|
cur.extracted = e;
|
|
5670
6325
|
break;
|
|
5671
|
-
case
|
|
6326
|
+
case import_types24.Provenance.OBSERVED:
|
|
5672
6327
|
cur.observed = e;
|
|
5673
6328
|
break;
|
|
5674
|
-
case
|
|
6329
|
+
case import_types24.Provenance.INFERRED:
|
|
5675
6330
|
cur.inferred = e;
|
|
5676
6331
|
break;
|
|
5677
6332
|
default:
|
|
5678
|
-
if (e.provenance ===
|
|
6333
|
+
if (e.provenance === import_types24.Provenance.STALE) cur.stale = e;
|
|
5679
6334
|
}
|
|
5680
6335
|
buckets.set(key, cur);
|
|
5681
6336
|
});
|
|
@@ -5684,7 +6339,7 @@ function bucketEdges(graph) {
|
|
|
5684
6339
|
function nodeIsFrontier(graph, nodeId) {
|
|
5685
6340
|
if (!graph.hasNode(nodeId)) return false;
|
|
5686
6341
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
5687
|
-
return attrs.type ===
|
|
6342
|
+
return attrs.type === import_types24.NodeType.FrontierNode;
|
|
5688
6343
|
}
|
|
5689
6344
|
function clampConfidence(n) {
|
|
5690
6345
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -5703,10 +6358,16 @@ function gradedConfidence(edge) {
|
|
|
5703
6358
|
if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
|
|
5704
6359
|
return clampConfidence(confidenceForEdge(edge));
|
|
5705
6360
|
}
|
|
6361
|
+
var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
6362
|
+
import_types24.EdgeType.CALLS,
|
|
6363
|
+
import_types24.EdgeType.CONNECTS_TO,
|
|
6364
|
+
import_types24.EdgeType.PUBLISHES_TO,
|
|
6365
|
+
import_types24.EdgeType.CONSUMES_FROM
|
|
6366
|
+
]);
|
|
5706
6367
|
function detectMissingDivergences(graph, bucket) {
|
|
5707
6368
|
const out = [];
|
|
5708
|
-
if (bucket.type ===
|
|
5709
|
-
if (bucket.extracted && !bucket.observed) {
|
|
6369
|
+
if (bucket.type === import_types24.EdgeType.CONTAINS) return out;
|
|
6370
|
+
if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
|
|
5710
6371
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
5711
6372
|
out.push({
|
|
5712
6373
|
type: "missing-observed",
|
|
@@ -5746,7 +6407,7 @@ function declaredHostFor(svc) {
|
|
|
5746
6407
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
5747
6408
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5748
6409
|
const e = graph.getEdgeAttributes(edgeId);
|
|
5749
|
-
if (e.type ===
|
|
6410
|
+
if (e.type === import_types24.EdgeType.CONFIGURED_BY && e.provenance === import_types24.Provenance.EXTRACTED) {
|
|
5750
6411
|
return true;
|
|
5751
6412
|
}
|
|
5752
6413
|
}
|
|
@@ -5759,10 +6420,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
5759
6420
|
const out = [];
|
|
5760
6421
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5761
6422
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5762
|
-
if (edge.type !==
|
|
5763
|
-
if (edge.provenance !==
|
|
6423
|
+
if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
|
|
6424
|
+
if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
|
|
5764
6425
|
const target = graph.getNodeAttributes(edge.target);
|
|
5765
|
-
if (target.type !==
|
|
6426
|
+
if (target.type !== import_types24.NodeType.DatabaseNode) continue;
|
|
5766
6427
|
const observedHost = target.host?.trim();
|
|
5767
6428
|
if (!observedHost) continue;
|
|
5768
6429
|
if (observedHost === declaredHost) continue;
|
|
@@ -5784,10 +6445,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
5784
6445
|
const deps = svc.dependencies ?? {};
|
|
5785
6446
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5786
6447
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5787
|
-
if (edge.type !==
|
|
5788
|
-
if (edge.provenance !==
|
|
6448
|
+
if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
|
|
6449
|
+
if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
|
|
5789
6450
|
const target = graph.getNodeAttributes(edge.target);
|
|
5790
|
-
if (target.type !==
|
|
6451
|
+
if (target.type !== import_types24.NodeType.DatabaseNode) continue;
|
|
5791
6452
|
for (const pair of compatPairs()) {
|
|
5792
6453
|
if (pair.engine !== target.engine) continue;
|
|
5793
6454
|
const declared = deps[pair.driver];
|
|
@@ -5840,6 +6501,23 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
5840
6501
|
function involvesNode(d, nodeId) {
|
|
5841
6502
|
return d.source === nodeId || d.target === nodeId;
|
|
5842
6503
|
}
|
|
6504
|
+
function suppressHostMismatchHalves(all) {
|
|
6505
|
+
const observedHalf = /* @__PURE__ */ new Set();
|
|
6506
|
+
const declaredHalf = /* @__PURE__ */ new Set();
|
|
6507
|
+
for (const d of all) {
|
|
6508
|
+
if (d.type !== "host-mismatch") continue;
|
|
6509
|
+
observedHalf.add(`${d.source}->${d.target}`);
|
|
6510
|
+
declaredHalf.add((0, import_types24.databaseId)(d.extractedHost));
|
|
6511
|
+
}
|
|
6512
|
+
if (observedHalf.size === 0) return all;
|
|
6513
|
+
return all.filter((d) => {
|
|
6514
|
+
if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
|
|
6515
|
+
return false;
|
|
6516
|
+
}
|
|
6517
|
+
if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
|
|
6518
|
+
return true;
|
|
6519
|
+
});
|
|
6520
|
+
}
|
|
5843
6521
|
function computeDivergences(graph, opts = {}) {
|
|
5844
6522
|
const all = [];
|
|
5845
6523
|
const buckets = bucketEdges(graph);
|
|
@@ -5848,12 +6526,13 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5848
6526
|
}
|
|
5849
6527
|
graph.forEachNode((nodeId, attrs) => {
|
|
5850
6528
|
const n = attrs;
|
|
5851
|
-
if (n.type !==
|
|
6529
|
+
if (n.type !== import_types24.NodeType.ServiceNode) return;
|
|
5852
6530
|
const svc = n;
|
|
5853
6531
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
5854
6532
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
5855
6533
|
});
|
|
5856
|
-
|
|
6534
|
+
const reconciled = suppressHostMismatchHalves(all);
|
|
6535
|
+
let filtered = reconciled;
|
|
5857
6536
|
if (opts.type) {
|
|
5858
6537
|
const allowed = opts.type;
|
|
5859
6538
|
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
@@ -5881,7 +6560,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5881
6560
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
5882
6561
|
return a.target.localeCompare(b.target);
|
|
5883
6562
|
});
|
|
5884
|
-
return
|
|
6563
|
+
return import_types24.DivergenceResultSchema.parse({
|
|
5885
6564
|
divergences: filtered,
|
|
5886
6565
|
totalAffected: filtered.length,
|
|
5887
6566
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -5970,7 +6649,7 @@ init_cjs_shims();
|
|
|
5970
6649
|
var import_node_fs23 = require("fs");
|
|
5971
6650
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
5972
6651
|
var import_node_path38 = __toESM(require("path"), 1);
|
|
5973
|
-
var
|
|
6652
|
+
var import_types25 = require("@neat.is/types");
|
|
5974
6653
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
5975
6654
|
var LOCK_RETRY_MS = 50;
|
|
5976
6655
|
function neatHome() {
|
|
@@ -6111,10 +6790,10 @@ async function readRegistry() {
|
|
|
6111
6790
|
throw err;
|
|
6112
6791
|
}
|
|
6113
6792
|
const parsed = JSON.parse(raw);
|
|
6114
|
-
return
|
|
6793
|
+
return import_types25.RegistryFileSchema.parse(parsed);
|
|
6115
6794
|
}
|
|
6116
6795
|
async function writeRegistry(reg) {
|
|
6117
|
-
const validated =
|
|
6796
|
+
const validated = import_types25.RegistryFileSchema.parse(reg);
|
|
6118
6797
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
6119
6798
|
}
|
|
6120
6799
|
async function getProject(name) {
|
|
@@ -6385,6 +7064,18 @@ function registerRoutes(scope, ctx) {
|
|
|
6385
7064
|
}
|
|
6386
7065
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
6387
7066
|
});
|
|
7067
|
+
scope.get(
|
|
7068
|
+
"/graph/observed-dependencies/:nodeId",
|
|
7069
|
+
async (req2, reply) => {
|
|
7070
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
7071
|
+
if (!proj) return;
|
|
7072
|
+
const { nodeId } = req2.params;
|
|
7073
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
7074
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
7075
|
+
}
|
|
7076
|
+
return getObservedDependencies(proj.graph, nodeId);
|
|
7077
|
+
}
|
|
7078
|
+
);
|
|
6388
7079
|
scope.get("/graph/divergences", async (req2, reply) => {
|
|
6389
7080
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6390
7081
|
if (!proj) return;
|
|
@@ -6393,11 +7084,11 @@ function registerRoutes(scope, ctx) {
|
|
|
6393
7084
|
const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6394
7085
|
const parsed = [];
|
|
6395
7086
|
for (const c of candidates) {
|
|
6396
|
-
const r =
|
|
7087
|
+
const r = import_types26.DivergenceTypeSchema.safeParse(c);
|
|
6397
7088
|
if (!r.success) {
|
|
6398
7089
|
return reply.code(400).send({
|
|
6399
7090
|
error: `unknown divergence type "${c}"`,
|
|
6400
|
-
allowed:
|
|
7091
|
+
allowed: import_types26.DivergenceTypeSchema.options
|
|
6401
7092
|
});
|
|
6402
7093
|
}
|
|
6403
7094
|
parsed.push(r.data);
|
|
@@ -6445,23 +7136,29 @@ function registerRoutes(scope, ctx) {
|
|
|
6445
7136
|
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
6446
7137
|
return { count: sliced.length, total, events: sliced };
|
|
6447
7138
|
});
|
|
7139
|
+
const incidentHistoryHandler = async (req2, reply) => {
|
|
7140
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
7141
|
+
if (!proj) return;
|
|
7142
|
+
const { nodeId } = req2.params;
|
|
7143
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
7144
|
+
reply.code(404).send({ error: "node not found", id: nodeId });
|
|
7145
|
+
return;
|
|
7146
|
+
}
|
|
7147
|
+
const epath = errorsPathFor(proj);
|
|
7148
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
7149
|
+
const events = await readErrorEvents(epath);
|
|
7150
|
+
const filtered = events.filter(
|
|
7151
|
+
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
7152
|
+
);
|
|
7153
|
+
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
7154
|
+
};
|
|
6448
7155
|
scope.get(
|
|
6449
7156
|
"/incidents/:nodeId",
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6456
|
-
}
|
|
6457
|
-
const epath = errorsPathFor(proj);
|
|
6458
|
-
if (!epath) return { count: 0, total: 0, events: [] };
|
|
6459
|
-
const events = await readErrorEvents(epath);
|
|
6460
|
-
const filtered = events.filter(
|
|
6461
|
-
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
6462
|
-
);
|
|
6463
|
-
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
6464
|
-
}
|
|
7157
|
+
incidentHistoryHandler
|
|
7158
|
+
);
|
|
7159
|
+
scope.get(
|
|
7160
|
+
"/graph/incident-history/:nodeId",
|
|
7161
|
+
incidentHistoryHandler
|
|
6465
7162
|
);
|
|
6466
7163
|
scope.get("/graph/root-cause/:nodeId", async (req2, reply) => {
|
|
6467
7164
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -6470,16 +7167,16 @@ function registerRoutes(scope, ctx) {
|
|
|
6470
7167
|
if (!proj.graph.hasNode(nodeId)) {
|
|
6471
7168
|
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6472
7169
|
}
|
|
6473
|
-
let errorEvent;
|
|
6474
7170
|
const epath = errorsPathFor(proj);
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
7171
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
7172
|
+
let errorEvent;
|
|
7173
|
+
if (req2.query.errorId) {
|
|
7174
|
+
errorEvent = incidents.find((e) => e.id === req2.query.errorId);
|
|
6478
7175
|
if (!errorEvent) {
|
|
6479
7176
|
return reply.code(404).send({ error: "error event not found", id: req2.query.errorId });
|
|
6480
7177
|
}
|
|
6481
7178
|
}
|
|
6482
|
-
const result = getRootCause(proj.graph, nodeId, errorEvent);
|
|
7179
|
+
const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
|
|
6483
7180
|
if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
|
|
6484
7181
|
return result;
|
|
6485
7182
|
});
|
|
@@ -6610,7 +7307,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6610
7307
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
6611
7308
|
let violations = await log.readAll();
|
|
6612
7309
|
if (req2.query.severity) {
|
|
6613
|
-
const sev =
|
|
7310
|
+
const sev = import_types26.PolicySeveritySchema.safeParse(req2.query.severity);
|
|
6614
7311
|
if (!sev.success) {
|
|
6615
7312
|
return reply.code(400).send({
|
|
6616
7313
|
error: "invalid severity",
|
|
@@ -6624,10 +7321,32 @@ function registerRoutes(scope, ctx) {
|
|
|
6624
7321
|
}
|
|
6625
7322
|
return { violations };
|
|
6626
7323
|
});
|
|
7324
|
+
scope.get("/policies/applicable", async (req2, reply) => {
|
|
7325
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
7326
|
+
if (!proj) return;
|
|
7327
|
+
const nodeId = req2.query.node;
|
|
7328
|
+
if (!nodeId) {
|
|
7329
|
+
return reply.code(400).send({ error: 'missing required query param "node"' });
|
|
7330
|
+
}
|
|
7331
|
+
const policyPath = ctx.policyFilePathFor(proj);
|
|
7332
|
+
let policies = [];
|
|
7333
|
+
if (policyPath) {
|
|
7334
|
+
try {
|
|
7335
|
+
policies = await loadPolicyFile(policyPath);
|
|
7336
|
+
} catch (err) {
|
|
7337
|
+
return reply.code(400).send({
|
|
7338
|
+
error: "policy.json failed to parse",
|
|
7339
|
+
details: err.message
|
|
7340
|
+
});
|
|
7341
|
+
}
|
|
7342
|
+
}
|
|
7343
|
+
const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
|
|
7344
|
+
return { node: nodeId, applicable };
|
|
7345
|
+
});
|
|
6627
7346
|
scope.post("/policies/check", async (req2, reply) => {
|
|
6628
7347
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6629
7348
|
if (!proj) return;
|
|
6630
|
-
const parsed =
|
|
7349
|
+
const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
|
|
6631
7350
|
if (!parsed.success) {
|
|
6632
7351
|
return reply.code(400).send({
|
|
6633
7352
|
error: "invalid /policies/check body",
|
|
@@ -6909,6 +7628,7 @@ function unroutedErrorsPath(neatHome3) {
|
|
|
6909
7628
|
}
|
|
6910
7629
|
|
|
6911
7630
|
// src/daemon.ts
|
|
7631
|
+
var import_types27 = require("@neat.is/types");
|
|
6912
7632
|
function daemonJsonPath(scanPath) {
|
|
6913
7633
|
return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
|
|
6914
7634
|
}
|
|
@@ -6962,6 +7682,20 @@ async function clearDaemonRecord(record, home) {
|
|
|
6962
7682
|
} catch {
|
|
6963
7683
|
}
|
|
6964
7684
|
}
|
|
7685
|
+
function reconcileDaemonRecordSync(record, home) {
|
|
7686
|
+
try {
|
|
7687
|
+
const stopped = { ...record, status: "stopped" };
|
|
7688
|
+
const target = daemonJsonPath(record.projectPath);
|
|
7689
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
7690
|
+
(0, import_node_fs25.writeFileSync)(tmp, JSON.stringify(stopped, null, 2) + "\n");
|
|
7691
|
+
(0, import_node_fs25.renameSync)(tmp, target);
|
|
7692
|
+
} catch {
|
|
7693
|
+
}
|
|
7694
|
+
try {
|
|
7695
|
+
(0, import_node_fs25.unlinkSync)(daemonDiscoveryPath(record.project, home));
|
|
7696
|
+
} catch {
|
|
7697
|
+
}
|
|
7698
|
+
}
|
|
6965
7699
|
function teardownSlot(slot) {
|
|
6966
7700
|
try {
|
|
6967
7701
|
slot.stopPersist();
|
|
@@ -7011,6 +7745,19 @@ function isTokenContained(needle, haystack) {
|
|
|
7011
7745
|
const tokens = haystack.split(/[-_]/);
|
|
7012
7746
|
return tokens.includes(needle);
|
|
7013
7747
|
}
|
|
7748
|
+
function serviceNameMatchesProject(serviceName, project) {
|
|
7749
|
+
if (serviceName === project) return true;
|
|
7750
|
+
if (isTokenPrefix(project, serviceName)) return true;
|
|
7751
|
+
if (isTokenContained(project, serviceName)) return true;
|
|
7752
|
+
return false;
|
|
7753
|
+
}
|
|
7754
|
+
function spanBelongsToSingleProject(graph, project, serviceName) {
|
|
7755
|
+
if (!serviceName) return true;
|
|
7756
|
+
if (serviceNameMatchesProject(serviceName, project)) return true;
|
|
7757
|
+
return graph.someNode(
|
|
7758
|
+
(_id, attrs) => attrs.type === import_types27.NodeType.ServiceNode && attrs.name === serviceName
|
|
7759
|
+
);
|
|
7760
|
+
}
|
|
7014
7761
|
async function bootstrapProject(entry2) {
|
|
7015
7762
|
const paths = pathsForProject(entry2.name, import_node_path42.default.join(entry2.path, "neat-out"));
|
|
7016
7763
|
try {
|
|
@@ -7308,7 +8055,9 @@ async function startDaemon(opts = {}) {
|
|
|
7308
8055
|
singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0
|
|
7309
8056
|
});
|
|
7310
8057
|
restAddress = await restApp.listen({ port: restPort, host });
|
|
7311
|
-
console.log(
|
|
8058
|
+
console.log(
|
|
8059
|
+
`neatd: REST listening on http://${host}:${portFromListenAddress(restAddress, restPort)}`
|
|
8060
|
+
);
|
|
7312
8061
|
} catch (err) {
|
|
7313
8062
|
for (const slot of slots.values()) {
|
|
7314
8063
|
teardownSlot(slot);
|
|
@@ -7339,6 +8088,10 @@ async function startDaemon(opts = {}) {
|
|
|
7339
8088
|
warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
|
|
7340
8089
|
return null;
|
|
7341
8090
|
}
|
|
8091
|
+
if (!spanBelongsToSingleProject(slot2.graph, singleProject, serviceName)) {
|
|
8092
|
+
await recordUnroutedSpan(serviceName, traceId);
|
|
8093
|
+
return null;
|
|
8094
|
+
}
|
|
7342
8095
|
return slot2;
|
|
7343
8096
|
}
|
|
7344
8097
|
const liveEntries = await listProjects().catch(() => []);
|
|
@@ -7401,7 +8154,7 @@ async function startDaemon(opts = {}) {
|
|
|
7401
8154
|
onErrorSpanSync: async (span) => {
|
|
7402
8155
|
const slot = await resolveTargetSlot(span.service, span.traceId);
|
|
7403
8156
|
if (!slot) return;
|
|
7404
|
-
await makeErrorSpanWriter(slot.paths.errorsPath)(span);
|
|
8157
|
+
await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
|
|
7405
8158
|
},
|
|
7406
8159
|
// Project-scoped route (issue #367) — the URL already named the
|
|
7407
8160
|
// project. Resolution is a direct slot lookup; service.name resolves
|
|
@@ -7424,10 +8177,10 @@ async function startDaemon(opts = {}) {
|
|
|
7424
8177
|
onProjectErrorSpanSync: async (project, span) => {
|
|
7425
8178
|
const slot = await resolveSlotByName(project, span.service, span.traceId);
|
|
7426
8179
|
if (!slot) return;
|
|
7427
|
-
await makeErrorSpanWriter(slot.paths.errorsPath)(span);
|
|
8180
|
+
await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
|
|
7428
8181
|
}
|
|
7429
8182
|
});
|
|
7430
|
-
otlpAddress = await otlpApp
|
|
8183
|
+
otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
|
|
7431
8184
|
console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);
|
|
7432
8185
|
} catch (err) {
|
|
7433
8186
|
for (const slot of slots.values()) {
|
|
@@ -7582,7 +8335,8 @@ async function startDaemon(opts = {}) {
|
|
|
7582
8335
|
otlpAddress,
|
|
7583
8336
|
bootstrap: tracker,
|
|
7584
8337
|
initialBootstrap,
|
|
7585
|
-
daemonRecord
|
|
8338
|
+
daemonRecord,
|
|
8339
|
+
neatHome: home
|
|
7586
8340
|
};
|
|
7587
8341
|
}
|
|
7588
8342
|
|
|
@@ -7911,6 +8665,20 @@ function webPortFromEnv() {
|
|
|
7911
8665
|
const n = Number.parseInt(raw, 10);
|
|
7912
8666
|
return Number.isFinite(n) ? n : void 0;
|
|
7913
8667
|
}
|
|
8668
|
+
function installIngestFaultHandlers(onFatal = (code) => process.exit(code)) {
|
|
8669
|
+
const logFault = (kind, err) => {
|
|
8670
|
+
const e = err;
|
|
8671
|
+
console.error(`neatd: ${kind}. ${e?.stack ?? e?.message ?? String(err)}`);
|
|
8672
|
+
};
|
|
8673
|
+
process.on(
|
|
8674
|
+
"unhandledRejection",
|
|
8675
|
+
(reason) => logFault("unhandled rejection \u2014 daemon staying up", reason)
|
|
8676
|
+
);
|
|
8677
|
+
process.on("uncaughtException", (err) => {
|
|
8678
|
+
logFault("uncaught exception \u2014 exiting", err);
|
|
8679
|
+
onFatal(1);
|
|
8680
|
+
});
|
|
8681
|
+
}
|
|
7914
8682
|
async function cmdStart() {
|
|
7915
8683
|
const project = process.env.NEAT_PROJECT;
|
|
7916
8684
|
const projectPath = process.env.NEAT_PROJECT_PATH;
|
|
@@ -7925,6 +8693,12 @@ async function cmdStart() {
|
|
|
7925
8693
|
}
|
|
7926
8694
|
throw err;
|
|
7927
8695
|
}
|
|
8696
|
+
installIngestFaultHandlers();
|
|
8697
|
+
process.on("exit", () => {
|
|
8698
|
+
if (handle.daemonRecord) {
|
|
8699
|
+
reconcileDaemonRecordSync(handle.daemonRecord, handle.neatHome);
|
|
8700
|
+
}
|
|
8701
|
+
});
|
|
7928
8702
|
console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`);
|
|
7929
8703
|
console.log(`neatd: registry at ${registryPath()}`);
|
|
7930
8704
|
if (handle.restAddress) console.log(`neatd: REST \u2192 ${handle.restAddress}`);
|
|
@@ -8026,4 +8800,8 @@ if (/[\\/]neatd\.(?:cjs|js)$/.test(entry) || entry.endsWith("/neatd")) {
|
|
|
8026
8800
|
process.exit(1);
|
|
8027
8801
|
});
|
|
8028
8802
|
}
|
|
8803
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
8804
|
+
0 && (module.exports = {
|
|
8805
|
+
installIngestFaultHandlers
|
|
8806
|
+
});
|
|
8029
8807
|
//# sourceMappingURL=neatd.cjs.map
|