@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/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");
|
|
@@ -2136,6 +2578,22 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
|
|
|
2136
2578
|
};
|
|
2137
2579
|
await appendErrorEvent(ctx, ev);
|
|
2138
2580
|
}
|
|
2581
|
+
async function recordExceptionIncident(ctx, span, ts) {
|
|
2582
|
+
const attrs = sanitizeAttributes(span.attributes);
|
|
2583
|
+
const ev = {
|
|
2584
|
+
id: `${span.traceId}:${span.spanId}`,
|
|
2585
|
+
timestamp: ts,
|
|
2586
|
+
service: span.service,
|
|
2587
|
+
traceId: span.traceId,
|
|
2588
|
+
spanId: span.spanId,
|
|
2589
|
+
errorMessage: incidentMessage(span),
|
|
2590
|
+
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2591
|
+
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2592
|
+
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
2593
|
+
affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
|
|
2594
|
+
};
|
|
2595
|
+
await appendErrorEvent(ctx, ev);
|
|
2596
|
+
}
|
|
2139
2597
|
async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status2) {
|
|
2140
2598
|
const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
|
|
2141
2599
|
if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
|
|
@@ -2192,7 +2650,10 @@ async function handleSpan(ctx, span) {
|
|
|
2192
2650
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
2193
2651
|
cacheSpanService(span, nowMs, callSite);
|
|
2194
2652
|
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
2195
|
-
const callSiteEvidence = callSite ? {
|
|
2653
|
+
const callSiteEvidence = callSite ? {
|
|
2654
|
+
file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
|
|
2655
|
+
...callSite.line !== void 0 ? { line: callSite.line } : {}
|
|
2656
|
+
} : void 0;
|
|
2196
2657
|
let affectedNode = sourceId;
|
|
2197
2658
|
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
2198
2659
|
if (span.dbSystem) {
|
|
@@ -2214,7 +2675,7 @@ async function handleSpan(ctx, span) {
|
|
|
2214
2675
|
} else {
|
|
2215
2676
|
const host = pickAddress(span);
|
|
2216
2677
|
let resolvedViaAddress = false;
|
|
2217
|
-
if (mintsFromCallerSide && host && host !== span.service) {
|
|
2678
|
+
if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
|
|
2218
2679
|
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
2219
2680
|
if (targetId && targetId !== sourceId) {
|
|
2220
2681
|
upsertObservedEdge(
|
|
@@ -2249,7 +2710,11 @@ async function handleSpan(ctx, span) {
|
|
|
2249
2710
|
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
2250
2711
|
const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
|
|
2251
2712
|
const fallbackEvidence = parent.callSite ? {
|
|
2252
|
-
file:
|
|
2713
|
+
file: reconcileObservedRelPath(
|
|
2714
|
+
ctx.graph,
|
|
2715
|
+
parent.service,
|
|
2716
|
+
parent.callSite.relPath
|
|
2717
|
+
),
|
|
2253
2718
|
...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
|
|
2254
2719
|
} : void 0;
|
|
2255
2720
|
upsertObservedEdge(
|
|
@@ -2274,7 +2739,7 @@ async function handleSpan(ctx, span) {
|
|
|
2274
2739
|
service: span.service,
|
|
2275
2740
|
traceId: span.traceId,
|
|
2276
2741
|
spanId: span.spanId,
|
|
2277
|
-
errorMessage: span
|
|
2742
|
+
errorMessage: incidentMessage(span),
|
|
2278
2743
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2279
2744
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2280
2745
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
@@ -2285,7 +2750,9 @@ async function handleSpan(ctx, span) {
|
|
|
2285
2750
|
}
|
|
2286
2751
|
if (span.statusCode !== 2) {
|
|
2287
2752
|
const status2 = httpResponseStatus(span);
|
|
2288
|
-
if (
|
|
2753
|
+
if (span.exception) {
|
|
2754
|
+
await recordExceptionIncident(ctx, span, ts);
|
|
2755
|
+
} else if (status2 !== void 0 && status2 >= 500) {
|
|
2289
2756
|
await recordFailingResponseIncident(ctx, span, sourceId, ts, status2, 1);
|
|
2290
2757
|
} else if (status2 !== void 0 && status2 >= 400 && spanMintsObservedEdge(span.kind)) {
|
|
2291
2758
|
await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status2);
|
|
@@ -2377,12 +2844,43 @@ async function readStaleEvents(staleEventsPath) {
|
|
|
2377
2844
|
async function readErrorEvents(errorsPath) {
|
|
2378
2845
|
try {
|
|
2379
2846
|
const raw = await import_node_fs3.promises.readFile(errorsPath, "utf8");
|
|
2380
|
-
|
|
2847
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
2848
|
+
return dedupeIncidents(events);
|
|
2381
2849
|
} catch (err) {
|
|
2382
2850
|
if (err.code === "ENOENT") return [];
|
|
2383
2851
|
throw err;
|
|
2384
2852
|
}
|
|
2385
2853
|
}
|
|
2854
|
+
function isSynthesizedHttpIncident(ev) {
|
|
2855
|
+
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
2856
|
+
if (ev.errorType) return false;
|
|
2857
|
+
if (!ev.attributes) return false;
|
|
2858
|
+
const synth = httpFailureMessageFromAttrs(ev.attributes);
|
|
2859
|
+
return synth !== void 0 && synth === ev.errorMessage;
|
|
2860
|
+
}
|
|
2861
|
+
function dedupeIncidents(events) {
|
|
2862
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2863
|
+
const once = [];
|
|
2864
|
+
for (const ev of events) {
|
|
2865
|
+
const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
|
|
2866
|
+
if (key === void 0) {
|
|
2867
|
+
once.push(ev);
|
|
2868
|
+
continue;
|
|
2869
|
+
}
|
|
2870
|
+
if (seen.has(key)) continue;
|
|
2871
|
+
seen.add(key);
|
|
2872
|
+
once.push(ev);
|
|
2873
|
+
}
|
|
2874
|
+
const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
|
|
2875
|
+
const hasRealFailure = /* @__PURE__ */ new Set();
|
|
2876
|
+
for (const ev of once) {
|
|
2877
|
+
if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
|
|
2878
|
+
}
|
|
2879
|
+
return once.filter((ev) => {
|
|
2880
|
+
if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
|
|
2881
|
+
return !hasRealFailure.has(groupKey(ev));
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2386
2884
|
function mergeSnapshot(graph, snapshot) {
|
|
2387
2885
|
const exported = snapshot.graph;
|
|
2388
2886
|
let nodesAdded = 0;
|
|
@@ -2463,10 +2961,17 @@ function isConfigFile(name) {
|
|
|
2463
2961
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
2464
2962
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
2465
2963
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
2964
|
+
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
2466
2965
|
return { match: true, fileType: "env" };
|
|
2467
2966
|
}
|
|
2468
2967
|
return { match: false, fileType: "" };
|
|
2469
2968
|
}
|
|
2969
|
+
function isNeatAuthoredEnvFile(name) {
|
|
2970
|
+
return name === ".env.neat";
|
|
2971
|
+
}
|
|
2972
|
+
function isNeatAuthoredSourceFile(name) {
|
|
2973
|
+
return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
|
|
2974
|
+
}
|
|
2470
2975
|
function isTestPath(filePath) {
|
|
2471
2976
|
const normalised = filePath.replace(/\\/g, "/");
|
|
2472
2977
|
const segments = normalised.split("/");
|
|
@@ -3160,7 +3665,9 @@ async function walkSourceFiles(dir) {
|
|
|
3160
3665
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
3161
3666
|
if (await isPythonVenvDir(full)) continue;
|
|
3162
3667
|
await walk(full);
|
|
3163
|
-
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry2.name))
|
|
3668
|
+
} 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
|
|
3669
|
+
// would attribute our instrumentation imports to the user's service.
|
|
3670
|
+
!isNeatAuthoredSourceFile(entry2.name)) {
|
|
3164
3671
|
out.push(full);
|
|
3165
3672
|
}
|
|
3166
3673
|
}
|
|
@@ -3330,11 +3837,28 @@ function collectJsImports(node, out) {
|
|
|
3330
3837
|
if (child) collectJsImports(child, out);
|
|
3331
3838
|
}
|
|
3332
3839
|
}
|
|
3840
|
+
function collectImportedNames(node, out) {
|
|
3841
|
+
if (node.type === "aliased_import") {
|
|
3842
|
+
const nameNode = node.childForFieldName("name");
|
|
3843
|
+
if (nameNode) out.push(nameNode.text);
|
|
3844
|
+
return;
|
|
3845
|
+
}
|
|
3846
|
+
if (node.type === "dotted_name") {
|
|
3847
|
+
out.push(node.text);
|
|
3848
|
+
return;
|
|
3849
|
+
}
|
|
3850
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3851
|
+
const child = node.namedChild(i);
|
|
3852
|
+
if (child) collectImportedNames(child, out);
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3333
3855
|
function collectPyImports(node, out) {
|
|
3334
3856
|
if (node.type === "import_from_statement") {
|
|
3335
3857
|
let level = 0;
|
|
3336
3858
|
let modulePath = "";
|
|
3859
|
+
const names = [];
|
|
3337
3860
|
let pastFrom = false;
|
|
3861
|
+
let pastImport = false;
|
|
3338
3862
|
for (let i = 0; i < node.childCount; i++) {
|
|
3339
3863
|
const child = node.child(i);
|
|
3340
3864
|
if (!child) continue;
|
|
@@ -3342,26 +3866,30 @@ function collectPyImports(node, out) {
|
|
|
3342
3866
|
if (child.type === "from") pastFrom = true;
|
|
3343
3867
|
continue;
|
|
3344
3868
|
}
|
|
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;
|
|
3869
|
+
if (!pastImport) {
|
|
3870
|
+
if (child.type === "import") {
|
|
3871
|
+
pastImport = true;
|
|
3872
|
+
continue;
|
|
3355
3873
|
}
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3874
|
+
if (child.type === "relative_import") {
|
|
3875
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
3876
|
+
const rc = child.child(j);
|
|
3877
|
+
if (!rc) continue;
|
|
3878
|
+
if (rc.type === "import_prefix") {
|
|
3879
|
+
for (let k = 0; k < rc.childCount; k++) {
|
|
3880
|
+
if (rc.child(k)?.type === ".") level++;
|
|
3881
|
+
}
|
|
3882
|
+
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
3883
|
+
}
|
|
3884
|
+
} else if (child.type === "dotted_name") {
|
|
3885
|
+
modulePath = child.text;
|
|
3886
|
+
}
|
|
3887
|
+
continue;
|
|
3361
3888
|
}
|
|
3889
|
+
collectImportedNames(child, names);
|
|
3362
3890
|
}
|
|
3363
3891
|
if (level > 0 || modulePath) {
|
|
3364
|
-
out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
3892
|
+
out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
3365
3893
|
}
|
|
3366
3894
|
}
|
|
3367
3895
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -3463,8 +3991,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
|
|
|
3463
3991
|
return null;
|
|
3464
3992
|
}
|
|
3465
3993
|
async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
3466
|
-
if (!imp.modulePath) return null;
|
|
3467
|
-
const relPath = imp.modulePath.split(".").join("/");
|
|
3468
3994
|
let baseDir;
|
|
3469
3995
|
if (imp.level > 0) {
|
|
3470
3996
|
baseDir = import_node_path12.default.dirname(importerPath);
|
|
@@ -3472,13 +3998,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
|
3472
3998
|
} else {
|
|
3473
3999
|
baseDir = serviceDir;
|
|
3474
4000
|
}
|
|
3475
|
-
const
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
4001
|
+
const moduleBase = imp.modulePath ? import_node_path12.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
|
|
4002
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
4003
|
+
let needModuleFile = imp.names.length === 0;
|
|
4004
|
+
for (const name of imp.names) {
|
|
4005
|
+
const submoduleFile = import_node_path12.default.join(moduleBase, `${name}.py`);
|
|
4006
|
+
const subpackageInit = import_node_path12.default.join(moduleBase, name, "__init__.py");
|
|
4007
|
+
if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
|
|
4008
|
+
resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, submoduleFile)));
|
|
4009
|
+
} else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
|
|
4010
|
+
resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, subpackageInit)));
|
|
4011
|
+
} else {
|
|
4012
|
+
needModuleFile = true;
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
if (needModuleFile) {
|
|
4016
|
+
const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path12.default.join(moduleBase, "__init__.py")] : [import_node_path12.default.join(moduleBase, "__init__.py")];
|
|
4017
|
+
for (const candidate of moduleFileCandidates) {
|
|
4018
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
4019
|
+
resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, candidate)));
|
|
4020
|
+
break;
|
|
4021
|
+
}
|
|
3479
4022
|
}
|
|
3480
4023
|
}
|
|
3481
|
-
return
|
|
4024
|
+
return [...resolved];
|
|
3482
4025
|
}
|
|
3483
4026
|
function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
|
|
3484
4027
|
const importeeFileId = (0, import_types8.fileId)(serviceName, importeeRelPath);
|
|
@@ -3519,17 +4062,18 @@ async function addImports(graph, services) {
|
|
|
3519
4062
|
continue;
|
|
3520
4063
|
}
|
|
3521
4064
|
for (const imp of pyImports) {
|
|
3522
|
-
const
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
4065
|
+
const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
|
|
4066
|
+
for (const resolved of resolvedPaths) {
|
|
4067
|
+
edgesAdded += emitImportEdge(
|
|
4068
|
+
graph,
|
|
4069
|
+
service.pkg.name,
|
|
4070
|
+
importerFileId,
|
|
4071
|
+
relFile,
|
|
4072
|
+
resolved,
|
|
4073
|
+
imp.line,
|
|
4074
|
+
imp.snippet
|
|
4075
|
+
);
|
|
4076
|
+
}
|
|
3533
4077
|
}
|
|
3534
4078
|
continue;
|
|
3535
4079
|
}
|
|
@@ -4165,6 +4709,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4165
4709
|
edgesAdded++;
|
|
4166
4710
|
}
|
|
4167
4711
|
}
|
|
4712
|
+
if (allConfigs.length === 1) {
|
|
4713
|
+
const primary = allConfigs[0];
|
|
4714
|
+
service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
|
|
4715
|
+
const relPath = import_node_path20.default.relative(scanPath, primary.sourceFile);
|
|
4716
|
+
const cfgId = (0, import_types9.configId)(relPath);
|
|
4717
|
+
if (!graph.hasNode(cfgId)) {
|
|
4718
|
+
const cfgNode = {
|
|
4719
|
+
id: cfgId,
|
|
4720
|
+
type: import_types9.NodeType.ConfigNode,
|
|
4721
|
+
name: import_node_path20.default.basename(primary.sourceFile),
|
|
4722
|
+
path: relPath,
|
|
4723
|
+
fileType: isConfigFile(import_node_path20.default.basename(primary.sourceFile)).fileType || "config"
|
|
4724
|
+
};
|
|
4725
|
+
graph.addNode(cfgId, cfgNode);
|
|
4726
|
+
nodesAdded++;
|
|
4727
|
+
}
|
|
4728
|
+
const cfgEdge = {
|
|
4729
|
+
id: (0, import_types4.extractedEdgeId)(service.node.id, cfgId, import_types9.EdgeType.CONFIGURED_BY),
|
|
4730
|
+
source: service.node.id,
|
|
4731
|
+
target: cfgId,
|
|
4732
|
+
type: import_types9.EdgeType.CONFIGURED_BY,
|
|
4733
|
+
provenance: import_types9.Provenance.EXTRACTED,
|
|
4734
|
+
confidence: (0, import_types9.confidenceForExtracted)("structural"),
|
|
4735
|
+
evidence: { file: toPosix2(relPath) }
|
|
4736
|
+
};
|
|
4737
|
+
if (!graph.hasEdge(cfgEdge.id)) {
|
|
4738
|
+
graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
|
|
4739
|
+
edgesAdded++;
|
|
4740
|
+
}
|
|
4741
|
+
}
|
|
4168
4742
|
attachIncompatibilities(service, allConfigs);
|
|
4169
4743
|
if (graph.hasNode(service.node.id)) {
|
|
4170
4744
|
const current = graph.getNodeAttributes(service.node.id);
|
|
@@ -4176,6 +4750,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4176
4750
|
if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
|
|
4177
4751
|
delete updated.incompatibilities;
|
|
4178
4752
|
}
|
|
4753
|
+
if (!service.node.dbConnectionTarget) {
|
|
4754
|
+
delete updated.dbConnectionTarget;
|
|
4755
|
+
}
|
|
4179
4756
|
graph.replaceNodeAttributes(service.node.id, updated);
|
|
4180
4757
|
}
|
|
4181
4758
|
}
|
|
@@ -4353,7 +4930,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4353
4930
|
const dedupKey = `${relFile}|${targetId}`;
|
|
4354
4931
|
if (seen.has(dedupKey)) continue;
|
|
4355
4932
|
seen.add(dedupKey);
|
|
4356
|
-
const confidence = (0, import_types11.confidenceForExtracted)("
|
|
4933
|
+
const confidence = (0, import_types11.confidenceForExtracted)("url-literal-service-target");
|
|
4357
4934
|
const ev = {
|
|
4358
4935
|
file: relFile,
|
|
4359
4936
|
line: site.line,
|
|
@@ -4373,7 +4950,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4373
4950
|
target: targetId,
|
|
4374
4951
|
type: import_types11.EdgeType.CALLS,
|
|
4375
4952
|
confidence,
|
|
4376
|
-
confidenceKind: "
|
|
4953
|
+
confidenceKind: "url-literal-service-target",
|
|
4377
4954
|
evidence: ev
|
|
4378
4955
|
});
|
|
4379
4956
|
continue;
|
|
@@ -4879,19 +5456,29 @@ init_cjs_shims();
|
|
|
4879
5456
|
var import_node_path29 = __toESM(require("path"), 1);
|
|
4880
5457
|
var import_node_fs15 = require("fs");
|
|
4881
5458
|
var import_types20 = require("@neat.is/types");
|
|
4882
|
-
function
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
5459
|
+
function readDockerfile(content) {
|
|
5460
|
+
let image = null;
|
|
5461
|
+
const ports = [];
|
|
5462
|
+
let cmd = null;
|
|
5463
|
+
let entrypoint = null;
|
|
5464
|
+
for (const raw of content.split("\n")) {
|
|
4886
5465
|
const line = raw.trim();
|
|
4887
5466
|
if (!line || line.startsWith("#")) continue;
|
|
4888
|
-
if (
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
if (
|
|
4892
|
-
|
|
5467
|
+
if (/^from\s+/i.test(line)) {
|
|
5468
|
+
const candidate = line.split(/\s+/)[1];
|
|
5469
|
+
if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
|
|
5470
|
+
} else if (/^expose\s+/i.test(line)) {
|
|
5471
|
+
for (const token of line.split(/\s+/).slice(1)) {
|
|
5472
|
+
const port = Number.parseInt(token.split("/")[0], 10);
|
|
5473
|
+
if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
|
|
5474
|
+
}
|
|
5475
|
+
} else if (/^entrypoint\s+/i.test(line)) {
|
|
5476
|
+
entrypoint = line;
|
|
5477
|
+
} else if (/^cmd\s+/i.test(line)) {
|
|
5478
|
+
cmd = line;
|
|
5479
|
+
}
|
|
4893
5480
|
}
|
|
4894
|
-
return
|
|
5481
|
+
return { image, ports, entrypoint: entrypoint ?? cmd };
|
|
4895
5482
|
}
|
|
4896
5483
|
async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
4897
5484
|
let nodesAdded = 0;
|
|
@@ -4910,14 +5497,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4910
5497
|
);
|
|
4911
5498
|
continue;
|
|
4912
5499
|
}
|
|
4913
|
-
const
|
|
4914
|
-
if (!image) continue;
|
|
4915
|
-
const node = makeInfraNode("container-image", image);
|
|
5500
|
+
const facts = readDockerfile(content);
|
|
5501
|
+
if (!facts.image) continue;
|
|
5502
|
+
const node = makeInfraNode("container-image", facts.image);
|
|
4916
5503
|
if (!graph.hasNode(node.id)) {
|
|
4917
5504
|
graph.addNode(node.id, node);
|
|
4918
5505
|
nodesAdded++;
|
|
4919
5506
|
}
|
|
4920
5507
|
const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
|
|
5508
|
+
const evidenceFile = toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath));
|
|
4921
5509
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
4922
5510
|
graph,
|
|
4923
5511
|
service.pkg.name,
|
|
@@ -4936,12 +5524,33 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4936
5524
|
provenance: import_types20.Provenance.EXTRACTED,
|
|
4937
5525
|
confidence: (0, import_types20.confidenceForExtracted)("structural"),
|
|
4938
5526
|
evidence: {
|
|
4939
|
-
file:
|
|
5527
|
+
file: evidenceFile,
|
|
5528
|
+
...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
|
|
4940
5529
|
}
|
|
4941
5530
|
};
|
|
4942
5531
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
4943
5532
|
edgesAdded++;
|
|
4944
5533
|
}
|
|
5534
|
+
for (const port of facts.ports) {
|
|
5535
|
+
const portNode = makeInfraNode("port", String(port));
|
|
5536
|
+
if (!graph.hasNode(portNode.id)) {
|
|
5537
|
+
graph.addNode(portNode.id, portNode);
|
|
5538
|
+
nodesAdded++;
|
|
5539
|
+
}
|
|
5540
|
+
const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types20.EdgeType.CONNECTS_TO);
|
|
5541
|
+
if (graph.hasEdge(portEdgeId)) continue;
|
|
5542
|
+
const portEdge = {
|
|
5543
|
+
id: portEdgeId,
|
|
5544
|
+
source: fileNodeId,
|
|
5545
|
+
target: portNode.id,
|
|
5546
|
+
type: import_types20.EdgeType.CONNECTS_TO,
|
|
5547
|
+
provenance: import_types20.Provenance.EXTRACTED,
|
|
5548
|
+
confidence: (0, import_types20.confidenceForExtracted)("structural"),
|
|
5549
|
+
evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
|
|
5550
|
+
};
|
|
5551
|
+
graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
|
|
5552
|
+
edgesAdded++;
|
|
5553
|
+
}
|
|
4945
5554
|
}
|
|
4946
5555
|
return { nodesAdded, edgesAdded };
|
|
4947
5556
|
}
|
|
@@ -4950,7 +5559,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4950
5559
|
init_cjs_shims();
|
|
4951
5560
|
var import_node_fs16 = require("fs");
|
|
4952
5561
|
var import_node_path30 = __toESM(require("path"), 1);
|
|
5562
|
+
var import_types21 = require("@neat.is/types");
|
|
4953
5563
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
5564
|
+
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
4954
5565
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
4955
5566
|
if (depth > max) return [];
|
|
4956
5567
|
const out = [];
|
|
@@ -4967,24 +5578,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
4967
5578
|
}
|
|
4968
5579
|
return out;
|
|
4969
5580
|
}
|
|
5581
|
+
function blockBody(content, from) {
|
|
5582
|
+
const open = content.indexOf("{", from);
|
|
5583
|
+
if (open === -1) return null;
|
|
5584
|
+
let depth = 0;
|
|
5585
|
+
for (let i = open; i < content.length; i++) {
|
|
5586
|
+
const ch = content[i];
|
|
5587
|
+
if (ch === "{") depth++;
|
|
5588
|
+
else if (ch === "}") {
|
|
5589
|
+
depth--;
|
|
5590
|
+
if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
return null;
|
|
5594
|
+
}
|
|
5595
|
+
function lineAt(content, index) {
|
|
5596
|
+
let line = 1;
|
|
5597
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
5598
|
+
if (content[i] === "\n") line++;
|
|
5599
|
+
}
|
|
5600
|
+
return line;
|
|
5601
|
+
}
|
|
4970
5602
|
async function addTerraformResources(graph, scanPath) {
|
|
4971
5603
|
let nodesAdded = 0;
|
|
5604
|
+
let edgesAdded = 0;
|
|
4972
5605
|
const files = await walkTfFiles(scanPath);
|
|
4973
5606
|
for (const file of files) {
|
|
4974
5607
|
const content = await import_node_fs16.promises.readFile(file, "utf8");
|
|
5608
|
+
const evidenceFile = toPosix2(import_node_path30.default.relative(scanPath, file));
|
|
5609
|
+
const resources = [];
|
|
5610
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
4975
5611
|
RESOURCE_RE.lastIndex = 0;
|
|
4976
5612
|
let m;
|
|
4977
5613
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
4978
|
-
const
|
|
5614
|
+
const type = m[1];
|
|
4979
5615
|
const name = m[2];
|
|
4980
|
-
const node = makeInfraNode(
|
|
5616
|
+
const node = makeInfraNode(type, name, "aws");
|
|
4981
5617
|
if (!graph.hasNode(node.id)) {
|
|
4982
5618
|
graph.addNode(node.id, node);
|
|
4983
5619
|
nodesAdded++;
|
|
4984
5620
|
}
|
|
5621
|
+
const span = blockBody(content, RESOURCE_RE.lastIndex);
|
|
5622
|
+
const resource = {
|
|
5623
|
+
type,
|
|
5624
|
+
name,
|
|
5625
|
+
nodeId: node.id,
|
|
5626
|
+
body: span?.body ?? "",
|
|
5627
|
+
bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
|
|
5628
|
+
};
|
|
5629
|
+
resources.push(resource);
|
|
5630
|
+
byKey.set(`${type}.${name}`, resource);
|
|
5631
|
+
}
|
|
5632
|
+
for (const resource of resources) {
|
|
5633
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5634
|
+
REFERENCE_RE.lastIndex = 0;
|
|
5635
|
+
let ref;
|
|
5636
|
+
while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
|
|
5637
|
+
const key = `${ref[1]}.${ref[2]}`;
|
|
5638
|
+
if (key === `${resource.type}.${resource.name}`) continue;
|
|
5639
|
+
const target = byKey.get(key);
|
|
5640
|
+
if (!target) continue;
|
|
5641
|
+
if (seen.has(target.nodeId)) continue;
|
|
5642
|
+
seen.add(target.nodeId);
|
|
5643
|
+
const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types21.EdgeType.DEPENDS_ON);
|
|
5644
|
+
if (graph.hasEdge(edgeId)) continue;
|
|
5645
|
+
const line = lineAt(content, resource.bodyOffset + ref.index);
|
|
5646
|
+
const edge = {
|
|
5647
|
+
id: edgeId,
|
|
5648
|
+
source: resource.nodeId,
|
|
5649
|
+
target: target.nodeId,
|
|
5650
|
+
type: import_types21.EdgeType.DEPENDS_ON,
|
|
5651
|
+
provenance: import_types21.Provenance.EXTRACTED,
|
|
5652
|
+
confidence: (0, import_types21.confidenceForExtracted)("structural"),
|
|
5653
|
+
evidence: { file: evidenceFile, line, snippet: key }
|
|
5654
|
+
};
|
|
5655
|
+
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
5656
|
+
edgesAdded++;
|
|
5657
|
+
}
|
|
4985
5658
|
}
|
|
4986
5659
|
}
|
|
4987
|
-
return { nodesAdded, edgesAdded
|
|
5660
|
+
return { nodesAdded, edgesAdded };
|
|
4988
5661
|
}
|
|
4989
5662
|
|
|
4990
5663
|
// src/extract/infra/k8s.ts
|
|
@@ -5062,11 +5735,11 @@ var import_node_path33 = __toESM(require("path"), 1);
|
|
|
5062
5735
|
init_cjs_shims();
|
|
5063
5736
|
var import_node_fs18 = require("fs");
|
|
5064
5737
|
var import_node_path32 = __toESM(require("path"), 1);
|
|
5065
|
-
var
|
|
5738
|
+
var import_types22 = require("@neat.is/types");
|
|
5066
5739
|
function dropOrphanedFileNodes(graph) {
|
|
5067
5740
|
const orphans = [];
|
|
5068
5741
|
graph.forEachNode((id, attrs) => {
|
|
5069
|
-
if (attrs.type !==
|
|
5742
|
+
if (attrs.type !== import_types22.NodeType.FileNode) return;
|
|
5070
5743
|
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
5071
5744
|
orphans.push(id);
|
|
5072
5745
|
}
|
|
@@ -5079,7 +5752,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
5079
5752
|
const bases = [scanPath, ...serviceDirs];
|
|
5080
5753
|
graph.forEachEdge((id, attrs) => {
|
|
5081
5754
|
const edge = attrs;
|
|
5082
|
-
if (edge.provenance !==
|
|
5755
|
+
if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
|
|
5083
5756
|
const evidenceFile = edge.evidence?.file;
|
|
5084
5757
|
if (!evidenceFile) return;
|
|
5085
5758
|
if (import_node_path32.default.isAbsolute(evidenceFile)) {
|
|
@@ -5162,7 +5835,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5162
5835
|
init_cjs_shims();
|
|
5163
5836
|
var import_node_fs19 = require("fs");
|
|
5164
5837
|
var import_node_path34 = __toESM(require("path"), 1);
|
|
5165
|
-
var
|
|
5838
|
+
var import_types23 = require("@neat.is/types");
|
|
5166
5839
|
var SCHEMA_VERSION = 4;
|
|
5167
5840
|
function migrateV1ToV2(payload) {
|
|
5168
5841
|
const nodes = payload.graph.nodes;
|
|
@@ -5184,12 +5857,12 @@ function migrateV2ToV3(payload) {
|
|
|
5184
5857
|
for (const edge of edges) {
|
|
5185
5858
|
const attrs = edge.attributes;
|
|
5186
5859
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
5187
|
-
attrs.provenance =
|
|
5860
|
+
attrs.provenance = import_types23.Provenance.OBSERVED;
|
|
5188
5861
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
5189
5862
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
5190
5863
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
5191
5864
|
if (type && source && target) {
|
|
5192
|
-
const newId = (0,
|
|
5865
|
+
const newId = (0, import_types23.observedEdgeId)(source, target, type);
|
|
5193
5866
|
attrs.id = newId;
|
|
5194
5867
|
if (edge.key) edge.key = newId;
|
|
5195
5868
|
}
|
|
@@ -5333,7 +6006,7 @@ var Projects = class {
|
|
|
5333
6006
|
init_cjs_shims();
|
|
5334
6007
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
5335
6008
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
5336
|
-
var
|
|
6009
|
+
var import_types26 = require("@neat.is/types");
|
|
5337
6010
|
|
|
5338
6011
|
// src/extend/index.ts
|
|
5339
6012
|
init_cjs_shims();
|
|
@@ -5652,7 +6325,7 @@ async function rollbackExtension(ctx, args) {
|
|
|
5652
6325
|
|
|
5653
6326
|
// src/divergences.ts
|
|
5654
6327
|
init_cjs_shims();
|
|
5655
|
-
var
|
|
6328
|
+
var import_types24 = require("@neat.is/types");
|
|
5656
6329
|
function bucketKey(source, target, type) {
|
|
5657
6330
|
return `${type}|${source}|${target}`;
|
|
5658
6331
|
}
|
|
@@ -5660,22 +6333,22 @@ function bucketEdges(graph) {
|
|
|
5660
6333
|
const buckets = /* @__PURE__ */ new Map();
|
|
5661
6334
|
graph.forEachEdge((id, attrs) => {
|
|
5662
6335
|
const e = attrs;
|
|
5663
|
-
const parsed = (0,
|
|
6336
|
+
const parsed = (0, import_types24.parseEdgeId)(id);
|
|
5664
6337
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
5665
6338
|
const key = bucketKey(e.source, e.target, e.type);
|
|
5666
6339
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
5667
6340
|
switch (provenance) {
|
|
5668
|
-
case
|
|
6341
|
+
case import_types24.Provenance.EXTRACTED:
|
|
5669
6342
|
cur.extracted = e;
|
|
5670
6343
|
break;
|
|
5671
|
-
case
|
|
6344
|
+
case import_types24.Provenance.OBSERVED:
|
|
5672
6345
|
cur.observed = e;
|
|
5673
6346
|
break;
|
|
5674
|
-
case
|
|
6347
|
+
case import_types24.Provenance.INFERRED:
|
|
5675
6348
|
cur.inferred = e;
|
|
5676
6349
|
break;
|
|
5677
6350
|
default:
|
|
5678
|
-
if (e.provenance ===
|
|
6351
|
+
if (e.provenance === import_types24.Provenance.STALE) cur.stale = e;
|
|
5679
6352
|
}
|
|
5680
6353
|
buckets.set(key, cur);
|
|
5681
6354
|
});
|
|
@@ -5684,7 +6357,7 @@ function bucketEdges(graph) {
|
|
|
5684
6357
|
function nodeIsFrontier(graph, nodeId) {
|
|
5685
6358
|
if (!graph.hasNode(nodeId)) return false;
|
|
5686
6359
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
5687
|
-
return attrs.type ===
|
|
6360
|
+
return attrs.type === import_types24.NodeType.FrontierNode;
|
|
5688
6361
|
}
|
|
5689
6362
|
function clampConfidence(n) {
|
|
5690
6363
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -5703,10 +6376,16 @@ function gradedConfidence(edge) {
|
|
|
5703
6376
|
if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
|
|
5704
6377
|
return clampConfidence(confidenceForEdge(edge));
|
|
5705
6378
|
}
|
|
6379
|
+
var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
6380
|
+
import_types24.EdgeType.CALLS,
|
|
6381
|
+
import_types24.EdgeType.CONNECTS_TO,
|
|
6382
|
+
import_types24.EdgeType.PUBLISHES_TO,
|
|
6383
|
+
import_types24.EdgeType.CONSUMES_FROM
|
|
6384
|
+
]);
|
|
5706
6385
|
function detectMissingDivergences(graph, bucket) {
|
|
5707
6386
|
const out = [];
|
|
5708
|
-
if (bucket.type ===
|
|
5709
|
-
if (bucket.extracted && !bucket.observed) {
|
|
6387
|
+
if (bucket.type === import_types24.EdgeType.CONTAINS) return out;
|
|
6388
|
+
if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
|
|
5710
6389
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
5711
6390
|
out.push({
|
|
5712
6391
|
type: "missing-observed",
|
|
@@ -5746,7 +6425,7 @@ function declaredHostFor(svc) {
|
|
|
5746
6425
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
5747
6426
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5748
6427
|
const e = graph.getEdgeAttributes(edgeId);
|
|
5749
|
-
if (e.type ===
|
|
6428
|
+
if (e.type === import_types24.EdgeType.CONFIGURED_BY && e.provenance === import_types24.Provenance.EXTRACTED) {
|
|
5750
6429
|
return true;
|
|
5751
6430
|
}
|
|
5752
6431
|
}
|
|
@@ -5759,10 +6438,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
5759
6438
|
const out = [];
|
|
5760
6439
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5761
6440
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5762
|
-
if (edge.type !==
|
|
5763
|
-
if (edge.provenance !==
|
|
6441
|
+
if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
|
|
6442
|
+
if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
|
|
5764
6443
|
const target = graph.getNodeAttributes(edge.target);
|
|
5765
|
-
if (target.type !==
|
|
6444
|
+
if (target.type !== import_types24.NodeType.DatabaseNode) continue;
|
|
5766
6445
|
const observedHost = target.host?.trim();
|
|
5767
6446
|
if (!observedHost) continue;
|
|
5768
6447
|
if (observedHost === declaredHost) continue;
|
|
@@ -5784,10 +6463,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
5784
6463
|
const deps = svc.dependencies ?? {};
|
|
5785
6464
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5786
6465
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5787
|
-
if (edge.type !==
|
|
5788
|
-
if (edge.provenance !==
|
|
6466
|
+
if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
|
|
6467
|
+
if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
|
|
5789
6468
|
const target = graph.getNodeAttributes(edge.target);
|
|
5790
|
-
if (target.type !==
|
|
6469
|
+
if (target.type !== import_types24.NodeType.DatabaseNode) continue;
|
|
5791
6470
|
for (const pair of compatPairs()) {
|
|
5792
6471
|
if (pair.engine !== target.engine) continue;
|
|
5793
6472
|
const declared = deps[pair.driver];
|
|
@@ -5840,6 +6519,23 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
5840
6519
|
function involvesNode(d, nodeId) {
|
|
5841
6520
|
return d.source === nodeId || d.target === nodeId;
|
|
5842
6521
|
}
|
|
6522
|
+
function suppressHostMismatchHalves(all) {
|
|
6523
|
+
const observedHalf = /* @__PURE__ */ new Set();
|
|
6524
|
+
const declaredHalf = /* @__PURE__ */ new Set();
|
|
6525
|
+
for (const d of all) {
|
|
6526
|
+
if (d.type !== "host-mismatch") continue;
|
|
6527
|
+
observedHalf.add(`${d.source}->${d.target}`);
|
|
6528
|
+
declaredHalf.add((0, import_types24.databaseId)(d.extractedHost));
|
|
6529
|
+
}
|
|
6530
|
+
if (observedHalf.size === 0) return all;
|
|
6531
|
+
return all.filter((d) => {
|
|
6532
|
+
if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
|
|
6533
|
+
return false;
|
|
6534
|
+
}
|
|
6535
|
+
if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
|
|
6536
|
+
return true;
|
|
6537
|
+
});
|
|
6538
|
+
}
|
|
5843
6539
|
function computeDivergences(graph, opts = {}) {
|
|
5844
6540
|
const all = [];
|
|
5845
6541
|
const buckets = bucketEdges(graph);
|
|
@@ -5848,12 +6544,13 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5848
6544
|
}
|
|
5849
6545
|
graph.forEachNode((nodeId, attrs) => {
|
|
5850
6546
|
const n = attrs;
|
|
5851
|
-
if (n.type !==
|
|
6547
|
+
if (n.type !== import_types24.NodeType.ServiceNode) return;
|
|
5852
6548
|
const svc = n;
|
|
5853
6549
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
5854
6550
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
5855
6551
|
});
|
|
5856
|
-
|
|
6552
|
+
const reconciled = suppressHostMismatchHalves(all);
|
|
6553
|
+
let filtered = reconciled;
|
|
5857
6554
|
if (opts.type) {
|
|
5858
6555
|
const allowed = opts.type;
|
|
5859
6556
|
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
@@ -5881,7 +6578,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5881
6578
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
5882
6579
|
return a.target.localeCompare(b.target);
|
|
5883
6580
|
});
|
|
5884
|
-
return
|
|
6581
|
+
return import_types24.DivergenceResultSchema.parse({
|
|
5885
6582
|
divergences: filtered,
|
|
5886
6583
|
totalAffected: filtered.length,
|
|
5887
6584
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -5970,7 +6667,7 @@ init_cjs_shims();
|
|
|
5970
6667
|
var import_node_fs23 = require("fs");
|
|
5971
6668
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
5972
6669
|
var import_node_path38 = __toESM(require("path"), 1);
|
|
5973
|
-
var
|
|
6670
|
+
var import_types25 = require("@neat.is/types");
|
|
5974
6671
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
5975
6672
|
var LOCK_RETRY_MS = 50;
|
|
5976
6673
|
function neatHome() {
|
|
@@ -6111,10 +6808,10 @@ async function readRegistry() {
|
|
|
6111
6808
|
throw err;
|
|
6112
6809
|
}
|
|
6113
6810
|
const parsed = JSON.parse(raw);
|
|
6114
|
-
return
|
|
6811
|
+
return import_types25.RegistryFileSchema.parse(parsed);
|
|
6115
6812
|
}
|
|
6116
6813
|
async function writeRegistry(reg) {
|
|
6117
|
-
const validated =
|
|
6814
|
+
const validated = import_types25.RegistryFileSchema.parse(reg);
|
|
6118
6815
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
6119
6816
|
}
|
|
6120
6817
|
async function getProject(name) {
|
|
@@ -6385,6 +7082,18 @@ function registerRoutes(scope, ctx) {
|
|
|
6385
7082
|
}
|
|
6386
7083
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
6387
7084
|
});
|
|
7085
|
+
scope.get(
|
|
7086
|
+
"/graph/observed-dependencies/:nodeId",
|
|
7087
|
+
async (req2, reply) => {
|
|
7088
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
7089
|
+
if (!proj) return;
|
|
7090
|
+
const { nodeId } = req2.params;
|
|
7091
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
7092
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
7093
|
+
}
|
|
7094
|
+
return getObservedDependencies(proj.graph, nodeId);
|
|
7095
|
+
}
|
|
7096
|
+
);
|
|
6388
7097
|
scope.get("/graph/divergences", async (req2, reply) => {
|
|
6389
7098
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6390
7099
|
if (!proj) return;
|
|
@@ -6393,11 +7102,11 @@ function registerRoutes(scope, ctx) {
|
|
|
6393
7102
|
const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6394
7103
|
const parsed = [];
|
|
6395
7104
|
for (const c of candidates) {
|
|
6396
|
-
const r =
|
|
7105
|
+
const r = import_types26.DivergenceTypeSchema.safeParse(c);
|
|
6397
7106
|
if (!r.success) {
|
|
6398
7107
|
return reply.code(400).send({
|
|
6399
7108
|
error: `unknown divergence type "${c}"`,
|
|
6400
|
-
allowed:
|
|
7109
|
+
allowed: import_types26.DivergenceTypeSchema.options
|
|
6401
7110
|
});
|
|
6402
7111
|
}
|
|
6403
7112
|
parsed.push(r.data);
|
|
@@ -6445,23 +7154,29 @@ function registerRoutes(scope, ctx) {
|
|
|
6445
7154
|
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
6446
7155
|
return { count: sliced.length, total, events: sliced };
|
|
6447
7156
|
});
|
|
7157
|
+
const incidentHistoryHandler = async (req2, reply) => {
|
|
7158
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
7159
|
+
if (!proj) return;
|
|
7160
|
+
const { nodeId } = req2.params;
|
|
7161
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
7162
|
+
reply.code(404).send({ error: "node not found", id: nodeId });
|
|
7163
|
+
return;
|
|
7164
|
+
}
|
|
7165
|
+
const epath = errorsPathFor(proj);
|
|
7166
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
7167
|
+
const events = await readErrorEvents(epath);
|
|
7168
|
+
const filtered = events.filter(
|
|
7169
|
+
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
7170
|
+
);
|
|
7171
|
+
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
7172
|
+
};
|
|
6448
7173
|
scope.get(
|
|
6449
7174
|
"/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
|
-
}
|
|
7175
|
+
incidentHistoryHandler
|
|
7176
|
+
);
|
|
7177
|
+
scope.get(
|
|
7178
|
+
"/graph/incident-history/:nodeId",
|
|
7179
|
+
incidentHistoryHandler
|
|
6465
7180
|
);
|
|
6466
7181
|
scope.get("/graph/root-cause/:nodeId", async (req2, reply) => {
|
|
6467
7182
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -6470,16 +7185,16 @@ function registerRoutes(scope, ctx) {
|
|
|
6470
7185
|
if (!proj.graph.hasNode(nodeId)) {
|
|
6471
7186
|
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6472
7187
|
}
|
|
6473
|
-
let errorEvent;
|
|
6474
7188
|
const epath = errorsPathFor(proj);
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
7189
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
7190
|
+
let errorEvent;
|
|
7191
|
+
if (req2.query.errorId) {
|
|
7192
|
+
errorEvent = incidents.find((e) => e.id === req2.query.errorId);
|
|
6478
7193
|
if (!errorEvent) {
|
|
6479
7194
|
return reply.code(404).send({ error: "error event not found", id: req2.query.errorId });
|
|
6480
7195
|
}
|
|
6481
7196
|
}
|
|
6482
|
-
const result = getRootCause(proj.graph, nodeId, errorEvent);
|
|
7197
|
+
const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
|
|
6483
7198
|
if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
|
|
6484
7199
|
return result;
|
|
6485
7200
|
});
|
|
@@ -6610,7 +7325,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6610
7325
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
6611
7326
|
let violations = await log.readAll();
|
|
6612
7327
|
if (req2.query.severity) {
|
|
6613
|
-
const sev =
|
|
7328
|
+
const sev = import_types26.PolicySeveritySchema.safeParse(req2.query.severity);
|
|
6614
7329
|
if (!sev.success) {
|
|
6615
7330
|
return reply.code(400).send({
|
|
6616
7331
|
error: "invalid severity",
|
|
@@ -6624,10 +7339,32 @@ function registerRoutes(scope, ctx) {
|
|
|
6624
7339
|
}
|
|
6625
7340
|
return { violations };
|
|
6626
7341
|
});
|
|
7342
|
+
scope.get("/policies/applicable", async (req2, reply) => {
|
|
7343
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
7344
|
+
if (!proj) return;
|
|
7345
|
+
const nodeId = req2.query.node;
|
|
7346
|
+
if (!nodeId) {
|
|
7347
|
+
return reply.code(400).send({ error: 'missing required query param "node"' });
|
|
7348
|
+
}
|
|
7349
|
+
const policyPath = ctx.policyFilePathFor(proj);
|
|
7350
|
+
let policies = [];
|
|
7351
|
+
if (policyPath) {
|
|
7352
|
+
try {
|
|
7353
|
+
policies = await loadPolicyFile(policyPath);
|
|
7354
|
+
} catch (err) {
|
|
7355
|
+
return reply.code(400).send({
|
|
7356
|
+
error: "policy.json failed to parse",
|
|
7357
|
+
details: err.message
|
|
7358
|
+
});
|
|
7359
|
+
}
|
|
7360
|
+
}
|
|
7361
|
+
const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
|
|
7362
|
+
return { node: nodeId, applicable };
|
|
7363
|
+
});
|
|
6627
7364
|
scope.post("/policies/check", async (req2, reply) => {
|
|
6628
7365
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6629
7366
|
if (!proj) return;
|
|
6630
|
-
const parsed =
|
|
7367
|
+
const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
|
|
6631
7368
|
if (!parsed.success) {
|
|
6632
7369
|
return reply.code(400).send({
|
|
6633
7370
|
error: "invalid /policies/check body",
|
|
@@ -6909,6 +7646,7 @@ function unroutedErrorsPath(neatHome3) {
|
|
|
6909
7646
|
}
|
|
6910
7647
|
|
|
6911
7648
|
// src/daemon.ts
|
|
7649
|
+
var import_types27 = require("@neat.is/types");
|
|
6912
7650
|
function daemonJsonPath(scanPath) {
|
|
6913
7651
|
return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
|
|
6914
7652
|
}
|
|
@@ -6962,6 +7700,20 @@ async function clearDaemonRecord(record, home) {
|
|
|
6962
7700
|
} catch {
|
|
6963
7701
|
}
|
|
6964
7702
|
}
|
|
7703
|
+
function reconcileDaemonRecordSync(record, home) {
|
|
7704
|
+
try {
|
|
7705
|
+
const stopped = { ...record, status: "stopped" };
|
|
7706
|
+
const target = daemonJsonPath(record.projectPath);
|
|
7707
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
7708
|
+
(0, import_node_fs25.writeFileSync)(tmp, JSON.stringify(stopped, null, 2) + "\n");
|
|
7709
|
+
(0, import_node_fs25.renameSync)(tmp, target);
|
|
7710
|
+
} catch {
|
|
7711
|
+
}
|
|
7712
|
+
try {
|
|
7713
|
+
(0, import_node_fs25.unlinkSync)(daemonDiscoveryPath(record.project, home));
|
|
7714
|
+
} catch {
|
|
7715
|
+
}
|
|
7716
|
+
}
|
|
6965
7717
|
function teardownSlot(slot) {
|
|
6966
7718
|
try {
|
|
6967
7719
|
slot.stopPersist();
|
|
@@ -7011,6 +7763,19 @@ function isTokenContained(needle, haystack) {
|
|
|
7011
7763
|
const tokens = haystack.split(/[-_]/);
|
|
7012
7764
|
return tokens.includes(needle);
|
|
7013
7765
|
}
|
|
7766
|
+
function serviceNameMatchesProject(serviceName, project) {
|
|
7767
|
+
if (serviceName === project) return true;
|
|
7768
|
+
if (isTokenPrefix(project, serviceName)) return true;
|
|
7769
|
+
if (isTokenContained(project, serviceName)) return true;
|
|
7770
|
+
return false;
|
|
7771
|
+
}
|
|
7772
|
+
function spanBelongsToSingleProject(graph, project, serviceName) {
|
|
7773
|
+
if (!serviceName) return true;
|
|
7774
|
+
if (serviceNameMatchesProject(serviceName, project)) return true;
|
|
7775
|
+
return graph.someNode(
|
|
7776
|
+
(_id, attrs) => attrs.type === import_types27.NodeType.ServiceNode && attrs.name === serviceName
|
|
7777
|
+
);
|
|
7778
|
+
}
|
|
7014
7779
|
async function bootstrapProject(entry2) {
|
|
7015
7780
|
const paths = pathsForProject(entry2.name, import_node_path42.default.join(entry2.path, "neat-out"));
|
|
7016
7781
|
try {
|
|
@@ -7308,7 +8073,9 @@ async function startDaemon(opts = {}) {
|
|
|
7308
8073
|
singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0
|
|
7309
8074
|
});
|
|
7310
8075
|
restAddress = await restApp.listen({ port: restPort, host });
|
|
7311
|
-
console.log(
|
|
8076
|
+
console.log(
|
|
8077
|
+
`neatd: REST listening on http://${host}:${portFromListenAddress(restAddress, restPort)}`
|
|
8078
|
+
);
|
|
7312
8079
|
} catch (err) {
|
|
7313
8080
|
for (const slot of slots.values()) {
|
|
7314
8081
|
teardownSlot(slot);
|
|
@@ -7339,6 +8106,10 @@ async function startDaemon(opts = {}) {
|
|
|
7339
8106
|
warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
|
|
7340
8107
|
return null;
|
|
7341
8108
|
}
|
|
8109
|
+
if (!spanBelongsToSingleProject(slot2.graph, singleProject, serviceName)) {
|
|
8110
|
+
await recordUnroutedSpan(serviceName, traceId);
|
|
8111
|
+
return null;
|
|
8112
|
+
}
|
|
7342
8113
|
return slot2;
|
|
7343
8114
|
}
|
|
7344
8115
|
const liveEntries = await listProjects().catch(() => []);
|
|
@@ -7401,7 +8172,7 @@ async function startDaemon(opts = {}) {
|
|
|
7401
8172
|
onErrorSpanSync: async (span) => {
|
|
7402
8173
|
const slot = await resolveTargetSlot(span.service, span.traceId);
|
|
7403
8174
|
if (!slot) return;
|
|
7404
|
-
await makeErrorSpanWriter(slot.paths.errorsPath)(span);
|
|
8175
|
+
await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
|
|
7405
8176
|
},
|
|
7406
8177
|
// Project-scoped route (issue #367) — the URL already named the
|
|
7407
8178
|
// project. Resolution is a direct slot lookup; service.name resolves
|
|
@@ -7424,10 +8195,10 @@ async function startDaemon(opts = {}) {
|
|
|
7424
8195
|
onProjectErrorSpanSync: async (project, span) => {
|
|
7425
8196
|
const slot = await resolveSlotByName(project, span.service, span.traceId);
|
|
7426
8197
|
if (!slot) return;
|
|
7427
|
-
await makeErrorSpanWriter(slot.paths.errorsPath)(span);
|
|
8198
|
+
await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
|
|
7428
8199
|
}
|
|
7429
8200
|
});
|
|
7430
|
-
otlpAddress = await otlpApp
|
|
8201
|
+
otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
|
|
7431
8202
|
console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);
|
|
7432
8203
|
} catch (err) {
|
|
7433
8204
|
for (const slot of slots.values()) {
|
|
@@ -7582,7 +8353,8 @@ async function startDaemon(opts = {}) {
|
|
|
7582
8353
|
otlpAddress,
|
|
7583
8354
|
bootstrap: tracker,
|
|
7584
8355
|
initialBootstrap,
|
|
7585
|
-
daemonRecord
|
|
8356
|
+
daemonRecord,
|
|
8357
|
+
neatHome: home
|
|
7586
8358
|
};
|
|
7587
8359
|
}
|
|
7588
8360
|
|
|
@@ -7911,6 +8683,20 @@ function webPortFromEnv() {
|
|
|
7911
8683
|
const n = Number.parseInt(raw, 10);
|
|
7912
8684
|
return Number.isFinite(n) ? n : void 0;
|
|
7913
8685
|
}
|
|
8686
|
+
function installIngestFaultHandlers(onFatal = (code) => process.exit(code)) {
|
|
8687
|
+
const logFault = (kind, err) => {
|
|
8688
|
+
const e = err;
|
|
8689
|
+
console.error(`neatd: ${kind}. ${e?.stack ?? e?.message ?? String(err)}`);
|
|
8690
|
+
};
|
|
8691
|
+
process.on(
|
|
8692
|
+
"unhandledRejection",
|
|
8693
|
+
(reason) => logFault("unhandled rejection \u2014 daemon staying up", reason)
|
|
8694
|
+
);
|
|
8695
|
+
process.on("uncaughtException", (err) => {
|
|
8696
|
+
logFault("uncaught exception \u2014 exiting", err);
|
|
8697
|
+
onFatal(1);
|
|
8698
|
+
});
|
|
8699
|
+
}
|
|
7914
8700
|
async function cmdStart() {
|
|
7915
8701
|
const project = process.env.NEAT_PROJECT;
|
|
7916
8702
|
const projectPath = process.env.NEAT_PROJECT_PATH;
|
|
@@ -7925,6 +8711,12 @@ async function cmdStart() {
|
|
|
7925
8711
|
}
|
|
7926
8712
|
throw err;
|
|
7927
8713
|
}
|
|
8714
|
+
installIngestFaultHandlers();
|
|
8715
|
+
process.on("exit", () => {
|
|
8716
|
+
if (handle.daemonRecord) {
|
|
8717
|
+
reconcileDaemonRecordSync(handle.daemonRecord, handle.neatHome);
|
|
8718
|
+
}
|
|
8719
|
+
});
|
|
7928
8720
|
console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`);
|
|
7929
8721
|
console.log(`neatd: registry at ${registryPath()}`);
|
|
7930
8722
|
if (handle.restAddress) console.log(`neatd: REST \u2192 ${handle.restAddress}`);
|
|
@@ -8026,4 +8818,8 @@ if (/[\\/]neatd\.(?:cjs|js)$/.test(entry) || entry.endsWith("/neatd")) {
|
|
|
8026
8818
|
process.exit(1);
|
|
8027
8819
|
});
|
|
8028
8820
|
}
|
|
8821
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
8822
|
+
0 && (module.exports = {
|
|
8823
|
+
installIngestFaultHandlers
|
|
8824
|
+
});
|
|
8029
8825
|
//# sourceMappingURL=neatd.cjs.map
|