@neat.is/core 0.2.10 → 0.3.1
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-QYUB3FQL.js → chunk-4ASCXBZF.js} +12 -124
- package/dist/chunk-4ASCXBZF.js.map +1 -0
- package/dist/{chunk-5BQWBQJR.js → chunk-66Z5IEFY.js} +126 -7
- package/dist/chunk-66Z5IEFY.js.map +1 -0
- package/dist/{chunk-JMLZOKCS.js → chunk-FIXKIYNF.js} +818 -18
- package/dist/chunk-FIXKIYNF.js.map +1 -0
- package/dist/chunk-G3PDTGOW.js +125 -0
- package/dist/chunk-G3PDTGOW.js.map +1 -0
- package/dist/cli.cjs +60 -25
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +17 -15
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +186 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -12
- package/dist/index.d.ts +11 -12
- package/dist/index.js +10 -11
- package/dist/neatd.cjs +1886 -34
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +5 -3
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-USFL3OI3.js → otel-grpc-J4O2SIBZ.js} +3 -3
- package/dist/server.cjs +49 -17
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +6 -7
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-5BQWBQJR.js.map +0 -1
- package/dist/chunk-DGUM43GV.js +0 -11
- package/dist/chunk-JMLZOKCS.js.map +0 -1
- package/dist/chunk-QYUB3FQL.js.map +0 -1
- package/dist/chunk-W7ZYJZC7.js +0 -807
- package/dist/chunk-W7ZYJZC7.js.map +0 -1
- package/dist/otel-grpc-USFL3OI3.js.map +0 -1
- /package/dist/{chunk-DGUM43GV.js.map → otel-grpc-J4O2SIBZ.js.map} +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseOtlpRequest
|
|
3
|
+
} from "./chunk-4ASCXBZF.js";
|
|
4
|
+
|
|
5
|
+
// src/otel-grpc.ts
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import * as grpc from "@grpc/grpc-js";
|
|
9
|
+
import * as protoLoader from "@grpc/proto-loader";
|
|
10
|
+
function bytesToHex(buf) {
|
|
11
|
+
if (!buf) return "";
|
|
12
|
+
return Buffer.isBuffer(buf) ? buf.toString("hex") : "";
|
|
13
|
+
}
|
|
14
|
+
function nanosToString(n) {
|
|
15
|
+
if (n === void 0 || n === null) return "0";
|
|
16
|
+
return typeof n === "string" ? n : String(n);
|
|
17
|
+
}
|
|
18
|
+
function reshapeAttributes(attrs) {
|
|
19
|
+
const out = (attrs ?? []).map((kv) => ({
|
|
20
|
+
key: kv.key ?? "",
|
|
21
|
+
value: kv.value ? {
|
|
22
|
+
stringValue: kv.value.string_value,
|
|
23
|
+
boolValue: kv.value.bool_value,
|
|
24
|
+
intValue: kv.value.int_value,
|
|
25
|
+
doubleValue: kv.value.double_value,
|
|
26
|
+
arrayValue: kv.value.array_value ? {
|
|
27
|
+
values: (kv.value.array_value.values ?? []).map((v) => ({
|
|
28
|
+
stringValue: v.string_value,
|
|
29
|
+
boolValue: v.bool_value,
|
|
30
|
+
intValue: v.int_value,
|
|
31
|
+
doubleValue: v.double_value
|
|
32
|
+
}))
|
|
33
|
+
} : void 0
|
|
34
|
+
} : void 0
|
|
35
|
+
}));
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function reshapeGrpcRequest(req) {
|
|
39
|
+
return {
|
|
40
|
+
resourceSpans: (req.resource_spans ?? []).map((rs) => ({
|
|
41
|
+
resource: rs.resource ? { attributes: reshapeAttributes(rs.resource.attributes) } : void 0,
|
|
42
|
+
scopeSpans: (rs.scope_spans ?? []).map((ss) => ({
|
|
43
|
+
spans: (ss.spans ?? []).map((s) => ({
|
|
44
|
+
traceId: bytesToHex(s.trace_id),
|
|
45
|
+
spanId: bytesToHex(s.span_id),
|
|
46
|
+
parentSpanId: s.parent_span_id ? bytesToHex(s.parent_span_id) : void 0,
|
|
47
|
+
name: s.name,
|
|
48
|
+
kind: s.kind,
|
|
49
|
+
startTimeUnixNano: nanosToString(s.start_time_unix_nano),
|
|
50
|
+
endTimeUnixNano: nanosToString(s.end_time_unix_nano),
|
|
51
|
+
attributes: reshapeAttributes(s.attributes),
|
|
52
|
+
events: (s.events ?? []).map((e) => ({
|
|
53
|
+
name: e.name,
|
|
54
|
+
timeUnixNano: nanosToString(e.time_unix_nano),
|
|
55
|
+
attributes: reshapeAttributes(e.attributes)
|
|
56
|
+
})),
|
|
57
|
+
status: s.status ? { code: s.status.code, message: s.status.message } : void 0
|
|
58
|
+
}))
|
|
59
|
+
}))
|
|
60
|
+
}))
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function resolveProtoRoot() {
|
|
64
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
65
|
+
return path.resolve(here, "..", "proto");
|
|
66
|
+
}
|
|
67
|
+
function loadTraceService() {
|
|
68
|
+
const protoRoot = resolveProtoRoot();
|
|
69
|
+
const def = protoLoader.loadSync(
|
|
70
|
+
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
71
|
+
{
|
|
72
|
+
keepCase: true,
|
|
73
|
+
longs: String,
|
|
74
|
+
enums: Number,
|
|
75
|
+
defaults: true,
|
|
76
|
+
oneofs: true,
|
|
77
|
+
includeDirs: [protoRoot]
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
const pkg = grpc.loadPackageDefinition(def);
|
|
81
|
+
return pkg.opentelemetry.proto.collector.trace.v1.TraceService.service;
|
|
82
|
+
}
|
|
83
|
+
async function startOtelGrpcReceiver(opts) {
|
|
84
|
+
const server = new grpc.Server();
|
|
85
|
+
const service = loadTraceService();
|
|
86
|
+
server.addService(service, {
|
|
87
|
+
Export: (call, callback) => {
|
|
88
|
+
void (async () => {
|
|
89
|
+
try {
|
|
90
|
+
const reshaped = reshapeGrpcRequest(call.request ?? {});
|
|
91
|
+
const spans = parseOtlpRequest(reshaped);
|
|
92
|
+
for (const span of spans) {
|
|
93
|
+
await opts.onSpan(span);
|
|
94
|
+
}
|
|
95
|
+
callback(null, { partial_success: {} });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
callback({
|
|
98
|
+
code: grpc.status.INTERNAL,
|
|
99
|
+
message: err instanceof Error ? err.message : String(err)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
const host = opts.host ?? "0.0.0.0";
|
|
106
|
+
const port = opts.port ?? 4317;
|
|
107
|
+
const boundPort = await new Promise((resolve, reject) => {
|
|
108
|
+
server.bindAsync(`${host}:${port}`, grpc.ServerCredentials.createInsecure(), (err, p) => {
|
|
109
|
+
if (err) return reject(err);
|
|
110
|
+
resolve(p);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
address: `${host}:${boundPort}`,
|
|
115
|
+
stop: () => new Promise((resolve) => {
|
|
116
|
+
server.tryShutdown(() => resolve());
|
|
117
|
+
})
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export {
|
|
122
|
+
reshapeGrpcRequest,
|
|
123
|
+
startOtelGrpcReceiver
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=chunk-G3PDTGOW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/otel-grpc.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url'\nimport path from 'node:path'\nimport * as grpc from '@grpc/grpc-js'\nimport * as protoLoader from '@grpc/proto-loader'\nimport {\n parseOtlpRequest,\n type OtlpTracesRequest,\n type ParsedSpan,\n type SpanHandler,\n} from './otel.js'\n\n// OTLP/gRPC receiver. Sits next to buildOtelReceiver (HTTP/JSON) in otel.ts;\n// shares the same parseOtlpRequest decoder so a span looks identical to the\n// downstream onSpan handler whether it came in over JSON or protobuf.\n//\n// Default OFF — opts.enabled (typically NEAT_OTLP_GRPC=true) decides whether\n// server.ts wires this up. We keep gRPC behind a flag so existing HTTP-only\n// deployments don't get a surprise port binding on upgrade.\n\nexport interface BuildOtelGrpcReceiverOptions {\n onSpan: SpanHandler\n}\n\n// proto-loader output for the trace service has fields like resource_spans,\n// scope_spans, span_id, etc. (snake_case keys, since we leave keepCase: true\n// when loading). The HTTP path uses camelCase JSON, so we shape-shift the\n// gRPC payload onto the HTTP shape and let parseOtlpRequest do the rest.\n//\n// All `bytes` fields arrive as Buffers; the HTTP wire format encodes them as\n// hex strings, so we hex-encode for consistency.\n\ninterface GrpcAnyValue {\n string_value?: string\n bool_value?: boolean\n int_value?: string | number\n double_value?: number\n array_value?: { values?: GrpcAnyValue[] }\n // bytes/kvlist fields exist in the proto but the demo doesn't use them.\n}\n\ninterface GrpcKeyValue {\n key?: string\n value?: GrpcAnyValue\n}\n\ninterface GrpcStatus {\n code?: number\n message?: string\n}\n\ninterface GrpcEvent {\n name?: string\n time_unix_nano?: string | number\n attributes?: GrpcKeyValue[]\n}\n\ninterface GrpcSpan {\n trace_id?: Buffer\n span_id?: Buffer\n parent_span_id?: Buffer\n name?: string\n kind?: number\n start_time_unix_nano?: string | number\n end_time_unix_nano?: string | number\n attributes?: GrpcKeyValue[]\n events?: GrpcEvent[]\n status?: GrpcStatus\n}\n\ninterface GrpcScopeSpans {\n spans?: GrpcSpan[]\n}\n\ninterface GrpcResourceSpans {\n resource?: { attributes?: GrpcKeyValue[] }\n scope_spans?: GrpcScopeSpans[]\n}\n\ninterface GrpcExportRequest {\n resource_spans?: GrpcResourceSpans[]\n}\n\nfunction bytesToHex(buf: Buffer | undefined): string {\n if (!buf) return ''\n return Buffer.isBuffer(buf) ? buf.toString('hex') : ''\n}\n\nfunction nanosToString(n: string | number | undefined): string {\n if (n === undefined || n === null) return '0'\n return typeof n === 'string' ? n : String(n)\n}\n\nfunction reshapeAttributes(\n attrs: GrpcKeyValue[] | undefined,\n): OtlpTracesRequest['resourceSpans'] extends Array<infer R>\n ? R extends { resource?: { attributes?: infer A } }\n ? A\n : never\n : never {\n // Map snake_case oneof fields to the camelCase the JSON path expects.\n const out = (attrs ?? []).map((kv) => ({\n key: kv.key ?? '',\n value: kv.value\n ? {\n stringValue: kv.value.string_value,\n boolValue: kv.value.bool_value,\n intValue: kv.value.int_value,\n doubleValue: kv.value.double_value,\n arrayValue: kv.value.array_value\n ? {\n values: (kv.value.array_value.values ?? []).map((v) => ({\n stringValue: v.string_value,\n boolValue: v.bool_value,\n intValue: v.int_value,\n doubleValue: v.double_value,\n })),\n }\n : undefined,\n }\n : undefined,\n }))\n return out as never\n}\n\nexport function reshapeGrpcRequest(req: GrpcExportRequest): OtlpTracesRequest {\n return {\n resourceSpans: (req.resource_spans ?? []).map((rs) => ({\n resource: rs.resource ? { attributes: reshapeAttributes(rs.resource.attributes) } : undefined,\n scopeSpans: (rs.scope_spans ?? []).map((ss) => ({\n spans: (ss.spans ?? []).map((s) => ({\n traceId: bytesToHex(s.trace_id),\n spanId: bytesToHex(s.span_id),\n parentSpanId: s.parent_span_id ? bytesToHex(s.parent_span_id) : undefined,\n name: s.name,\n kind: s.kind,\n startTimeUnixNano: nanosToString(s.start_time_unix_nano),\n endTimeUnixNano: nanosToString(s.end_time_unix_nano),\n attributes: reshapeAttributes(s.attributes),\n events: (s.events ?? []).map((e) => ({\n name: e.name,\n timeUnixNano: nanosToString(e.time_unix_nano),\n attributes: reshapeAttributes(e.attributes),\n })),\n status: s.status ? { code: s.status.code, message: s.status.message } : undefined,\n })),\n })),\n })),\n }\n}\n\n// Find the bundled .proto tree at packages/core/proto/. The dev server runs\n// from the source tree (tsx); the built bundles run from dist/. tsup keeps\n// source layout, so __dirname-relative resolution works for both — we look two\n// levels up from this file.\nfunction resolveProtoRoot(): string {\n // Built output (CJS) sets __dirname natively; ESM build is bundled by tsup\n // and keeps a dirname injection. import.meta.url is the safe bet.\n const here = path.dirname(fileURLToPath(import.meta.url))\n // src/ → packages/core/proto/, dist/ → packages/core/proto/.\n return path.resolve(here, '..', 'proto')\n}\n\nfunction loadTraceService(): grpc.ServiceDefinition {\n const protoRoot = resolveProtoRoot()\n const def = protoLoader.loadSync(\n 'opentelemetry/proto/collector/trace/v1/trace_service.proto',\n {\n keepCase: true,\n longs: String,\n enums: Number,\n defaults: true,\n oneofs: true,\n includeDirs: [protoRoot],\n },\n )\n const pkg = grpc.loadPackageDefinition(def) as unknown as {\n opentelemetry: {\n proto: {\n collector: {\n trace: {\n v1: {\n TraceService: { service: grpc.ServiceDefinition }\n }\n }\n }\n }\n }\n }\n return pkg.opentelemetry.proto.collector.trace.v1.TraceService.service\n}\n\nexport interface OtelGrpcReceiver {\n // Bound address (host:port) once .start() has resolved. Useful for tests.\n address: string\n // Stop accepting new requests, shut down the server.\n stop: () => Promise<void>\n}\n\nexport async function startOtelGrpcReceiver(\n opts: BuildOtelGrpcReceiverOptions & { host?: string; port?: number },\n): Promise<OtelGrpcReceiver> {\n const server = new grpc.Server()\n const service = loadTraceService()\n\n server.addService(service, {\n Export: (\n call: grpc.ServerUnaryCall<GrpcExportRequest, unknown>,\n callback: grpc.sendUnaryData<{ partial_success: object }>,\n ) => {\n void (async () => {\n try {\n const reshaped = reshapeGrpcRequest(call.request ?? {})\n const spans: ParsedSpan[] = parseOtlpRequest(reshaped)\n for (const span of spans) {\n await opts.onSpan(span)\n }\n callback(null, { partial_success: {} })\n } catch (err) {\n callback({\n code: grpc.status.INTERNAL,\n message: err instanceof Error ? err.message : String(err),\n })\n }\n })()\n },\n })\n\n const host = opts.host ?? '0.0.0.0'\n const port = opts.port ?? 4317\n\n const boundPort = await new Promise<number>((resolve, reject) => {\n server.bindAsync(`${host}:${port}`, grpc.ServerCredentials.createInsecure(), (err, p) => {\n if (err) return reject(err)\n resolve(p)\n })\n })\n\n return {\n address: `${host}:${boundPort}`,\n stop: () =>\n new Promise<void>((resolve) => {\n server.tryShutdown(() => resolve())\n }),\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli.cjs
CHANGED
|
@@ -4406,6 +4406,10 @@ async function addProject(opts) {
|
|
|
4406
4406
|
return entry2;
|
|
4407
4407
|
});
|
|
4408
4408
|
}
|
|
4409
|
+
async function getProject(name) {
|
|
4410
|
+
const reg = await readRegistry();
|
|
4411
|
+
return reg.projects.find((p) => p.name === name);
|
|
4412
|
+
}
|
|
4409
4413
|
async function listProjects() {
|
|
4410
4414
|
const reg = await readRegistry();
|
|
4411
4415
|
return reg.projects;
|
|
@@ -4542,9 +4546,15 @@ function registerRoutes(scope, ctx) {
|
|
|
4542
4546
|
scope.get("/health", async (req, reply) => {
|
|
4543
4547
|
const proj = resolveProject(registry, req, reply);
|
|
4544
4548
|
if (!proj) return;
|
|
4549
|
+
const uptimeMs = Date.now() - startedAt;
|
|
4545
4550
|
return {
|
|
4546
|
-
|
|
4551
|
+
ok: true,
|
|
4547
4552
|
project: proj.name,
|
|
4553
|
+
uptimeMs,
|
|
4554
|
+
// Legacy fields kept additively. The web shell's StatusBar reads
|
|
4555
|
+
// nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
|
|
4556
|
+
// the canonical triple and lets the extras pass through.
|
|
4557
|
+
uptime: Math.floor(uptimeMs / 1e3),
|
|
4548
4558
|
nodeCount: proj.graph.order,
|
|
4549
4559
|
edgeCount: proj.graph.size,
|
|
4550
4560
|
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -4564,7 +4574,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4564
4574
|
if (!proj.graph.hasNode(id)) {
|
|
4565
4575
|
return reply.code(404).send({ error: "node not found", id });
|
|
4566
4576
|
}
|
|
4567
|
-
return proj.graph.getNodeAttributes(id);
|
|
4577
|
+
return { node: proj.graph.getNodeAttributes(id) };
|
|
4568
4578
|
}
|
|
4569
4579
|
);
|
|
4570
4580
|
scope.get(
|
|
@@ -4581,12 +4591,12 @@ function registerRoutes(scope, ctx) {
|
|
|
4581
4591
|
return { inbound, outbound };
|
|
4582
4592
|
}
|
|
4583
4593
|
);
|
|
4584
|
-
scope.get("/graph/
|
|
4594
|
+
scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
|
|
4585
4595
|
const proj = resolveProject(registry, req, reply);
|
|
4586
4596
|
if (!proj) return;
|
|
4587
|
-
const {
|
|
4588
|
-
if (!proj.graph.hasNode(
|
|
4589
|
-
return reply.code(404).send({ error: "node not found", id });
|
|
4597
|
+
const { nodeId } = req.params;
|
|
4598
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
4599
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
4590
4600
|
}
|
|
4591
4601
|
const depth = req.query.depth ? Number(req.query.depth) : TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH;
|
|
4592
4602
|
if (!Number.isFinite(depth) || depth < 1 || depth > TRANSITIVE_DEPENDENCIES_MAX_DEPTH) {
|
|
@@ -4594,7 +4604,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4594
4604
|
error: `depth must be an integer in [1, ${TRANSITIVE_DEPENDENCIES_MAX_DEPTH}]`
|
|
4595
4605
|
});
|
|
4596
4606
|
}
|
|
4597
|
-
return getTransitiveDependencies(proj.graph,
|
|
4607
|
+
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
4598
4608
|
});
|
|
4599
4609
|
scope.get("/graph/divergences", async (req, reply) => {
|
|
4600
4610
|
const proj = resolveProject(registry, req, reply);
|
|
@@ -4635,19 +4645,26 @@ function registerRoutes(scope, ctx) {
|
|
|
4635
4645
|
const proj = resolveProject(registry, req, reply);
|
|
4636
4646
|
if (!proj) return;
|
|
4637
4647
|
const epath = errorsPathFor(proj);
|
|
4638
|
-
if (!epath) return [];
|
|
4639
|
-
|
|
4648
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
4649
|
+
const events = await readErrorEvents(epath);
|
|
4650
|
+
const total = events.length;
|
|
4651
|
+
const limit = req.query.limit ? Number(req.query.limit) : 50;
|
|
4652
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
|
|
4653
|
+
const sliced = events.slice(0, safeLimit);
|
|
4654
|
+
return { count: sliced.length, total, events: sliced };
|
|
4640
4655
|
});
|
|
4641
|
-
scope.get("/
|
|
4656
|
+
scope.get("/stale-events", async (req, reply) => {
|
|
4642
4657
|
const proj = resolveProject(registry, req, reply);
|
|
4643
4658
|
if (!proj) return;
|
|
4644
4659
|
const spath = staleEventsPathFor(proj);
|
|
4645
|
-
if (!spath) return [];
|
|
4660
|
+
if (!spath) return { count: 0, total: 0, events: [] };
|
|
4646
4661
|
const events = await readStaleEvents(spath);
|
|
4647
4662
|
const filtered = req.query.edgeType ? events.filter((e) => e.edgeType === req.query.edgeType) : events;
|
|
4648
4663
|
const ordered = [...filtered].reverse();
|
|
4664
|
+
const total = ordered.length;
|
|
4649
4665
|
const limit = req.query.limit ? Number(req.query.limit) : 50;
|
|
4650
|
-
|
|
4666
|
+
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
4667
|
+
return { count: sliced.length, total, events: sliced };
|
|
4651
4668
|
});
|
|
4652
4669
|
scope.get(
|
|
4653
4670
|
"/incidents/:nodeId",
|
|
@@ -4659,14 +4676,15 @@ function registerRoutes(scope, ctx) {
|
|
|
4659
4676
|
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
4660
4677
|
}
|
|
4661
4678
|
const epath = errorsPathFor(proj);
|
|
4662
|
-
if (!epath) return [];
|
|
4679
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
4663
4680
|
const events = await readErrorEvents(epath);
|
|
4664
|
-
|
|
4681
|
+
const filtered = events.filter(
|
|
4665
4682
|
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
4666
4683
|
);
|
|
4684
|
+
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
4667
4685
|
}
|
|
4668
4686
|
);
|
|
4669
|
-
scope.get("/
|
|
4687
|
+
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
4670
4688
|
const proj = resolveProject(registry, req, reply);
|
|
4671
4689
|
if (!proj) return;
|
|
4672
4690
|
const { nodeId } = req.params;
|
|
@@ -4686,7 +4704,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4686
4704
|
if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
|
|
4687
4705
|
return result;
|
|
4688
4706
|
});
|
|
4689
|
-
scope.get("/
|
|
4707
|
+
scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
|
|
4690
4708
|
const proj = resolveProject(registry, req, reply);
|
|
4691
4709
|
if (!proj) return;
|
|
4692
4710
|
const { nodeId } = req.params;
|
|
@@ -4796,7 +4814,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4796
4814
|
if (req.query.policyId) {
|
|
4797
4815
|
violations = violations.filter((v) => v.policyId === req.query.policyId);
|
|
4798
4816
|
}
|
|
4799
|
-
return violations;
|
|
4817
|
+
return { violations };
|
|
4800
4818
|
});
|
|
4801
4819
|
scope.post("/policies/check", async (req, reply) => {
|
|
4802
4820
|
const proj = resolveProject(registry, req, reply);
|
|
@@ -4875,6 +4893,20 @@ async function buildApi(opts) {
|
|
|
4875
4893
|
});
|
|
4876
4894
|
}
|
|
4877
4895
|
});
|
|
4896
|
+
app.get("/projects/:project", async (req, reply) => {
|
|
4897
|
+
try {
|
|
4898
|
+
const entry2 = await getProject(req.params.project);
|
|
4899
|
+
if (!entry2) {
|
|
4900
|
+
return reply.code(404).send({ error: "project not found", project: req.params.project });
|
|
4901
|
+
}
|
|
4902
|
+
return { project: entry2 };
|
|
4903
|
+
} catch (err) {
|
|
4904
|
+
return reply.code(500).send({
|
|
4905
|
+
error: "failed to read project registry",
|
|
4906
|
+
details: err.message
|
|
4907
|
+
});
|
|
4908
|
+
}
|
|
4909
|
+
});
|
|
4878
4910
|
registerRoutes(app, routeCtx);
|
|
4879
4911
|
await app.register(
|
|
4880
4912
|
async (scope) => {
|
|
@@ -5962,7 +5994,7 @@ async function runRootCause(client, input) {
|
|
|
5962
5994
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
5963
5995
|
const path38 = projectPath(
|
|
5964
5996
|
input.project,
|
|
5965
|
-
`/
|
|
5997
|
+
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
5966
5998
|
);
|
|
5967
5999
|
try {
|
|
5968
6000
|
const result = await client.get(path38);
|
|
@@ -5993,7 +6025,7 @@ async function runBlastRadius(client, input) {
|
|
|
5993
6025
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
5994
6026
|
const path38 = projectPath(
|
|
5995
6027
|
input.project,
|
|
5996
|
-
`/
|
|
6028
|
+
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
5997
6029
|
);
|
|
5998
6030
|
try {
|
|
5999
6031
|
const result = await client.get(path38);
|
|
@@ -6032,7 +6064,7 @@ async function runDependencies(client, input) {
|
|
|
6032
6064
|
const depth = input.depth ?? 3;
|
|
6033
6065
|
const path38 = projectPath(
|
|
6034
6066
|
input.project,
|
|
6035
|
-
`/graph/
|
|
6067
|
+
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
6036
6068
|
);
|
|
6037
6069
|
try {
|
|
6038
6070
|
const result = await client.get(path38);
|
|
@@ -6118,7 +6150,8 @@ function formatDuration(ms) {
|
|
|
6118
6150
|
async function runIncidents(client, input) {
|
|
6119
6151
|
const path38 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
6120
6152
|
try {
|
|
6121
|
-
const
|
|
6153
|
+
const body = await client.get(path38);
|
|
6154
|
+
const events = body.events;
|
|
6122
6155
|
if (events.length === 0) {
|
|
6123
6156
|
return {
|
|
6124
6157
|
summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
|
|
@@ -6132,7 +6165,7 @@ async function runIncidents(client, input) {
|
|
|
6132
6165
|
}
|
|
6133
6166
|
const target = input.nodeId ?? "the project";
|
|
6134
6167
|
return {
|
|
6135
|
-
summary: `${target} has ${
|
|
6168
|
+
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
6136
6169
|
block: blockLines.join("\n"),
|
|
6137
6170
|
provenance: import_types22.Provenance.OBSERVED
|
|
6138
6171
|
};
|
|
@@ -6228,9 +6261,10 @@ async function runStaleEdges(client, input) {
|
|
|
6228
6261
|
if (input.limit !== void 0) params.set("limit", String(input.limit));
|
|
6229
6262
|
if (input.edgeType) params.set("edgeType", input.edgeType);
|
|
6230
6263
|
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
6231
|
-
const
|
|
6232
|
-
projectPath(input.project, `/
|
|
6264
|
+
const body = await client.get(
|
|
6265
|
+
projectPath(input.project, `/stale-events${qs}`)
|
|
6233
6266
|
);
|
|
6267
|
+
const events = body.events;
|
|
6234
6268
|
if (events.length === 0) {
|
|
6235
6269
|
return {
|
|
6236
6270
|
summary: input.edgeType ? `No stale ${input.edgeType} edges recorded.` : "No stale-edge transitions recorded yet."
|
|
@@ -6264,9 +6298,10 @@ async function runPolicies(client, input) {
|
|
|
6264
6298
|
const params = new URLSearchParams();
|
|
6265
6299
|
if (input.policyId) params.set("policyId", input.policyId);
|
|
6266
6300
|
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
6267
|
-
|
|
6301
|
+
const body = await client.get(
|
|
6268
6302
|
projectPath(input.project, `/policies/violations${qs}`)
|
|
6269
6303
|
);
|
|
6304
|
+
violations = body.violations;
|
|
6270
6305
|
allowed = violations.every((v) => v.onViolation !== "block");
|
|
6271
6306
|
}
|
|
6272
6307
|
if (input.nodeId) {
|