@kb-labs/rest-api-app 2.117.0 → 2.118.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/index.js +173 -182
- package/dist/index.js.map +1 -1
- package/package.json +22 -22
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import Fastify from 'fastify';
|
|
|
3
3
|
import { platform } from '@kb-labs/core-runtime';
|
|
4
4
|
import { EventEmitter } from 'events';
|
|
5
5
|
import { performance, monitorEventLoopDelay } from 'perf_hooks';
|
|
6
|
-
import { OperationMetricsTracker, getListenOptions, registerOpenAPI, createHttpLogger, createServiceObservabilityDescribe, createServiceObservabilityHealth } from '@kb-labs/shared-http';
|
|
6
|
+
import { OperationMetricsTracker, getListenOptions, registerOpenAPI, createHttpLogger, createSseStream, createServiceObservabilityDescribe, createServiceObservabilityHealth } from '@kb-labs/shared-http';
|
|
7
7
|
import { Registry, Histogram, Counter, Gauge } from 'prom-client';
|
|
8
8
|
import * as os from 'os';
|
|
9
9
|
import { hostname } from 'os';
|
|
@@ -12,7 +12,7 @@ import * as process2 from 'process';
|
|
|
12
12
|
import { createRegistry, mergeOpenAPISpecs } from '@kb-labs/core-registry';
|
|
13
13
|
import { validateManifest } from '@kb-labs/plugin-contracts';
|
|
14
14
|
import { mountRoutes } from '@kb-labs/plugin-execution/http';
|
|
15
|
-
import { mountWebSocketChannels } from '@kb-labs/plugin-execution';
|
|
15
|
+
import { mountEventStreams, mountWebSocketChannels } from '@kb-labs/plugin-execution';
|
|
16
16
|
import { InProcessBackend } from '@kb-labs/plugin-execution-factory';
|
|
17
17
|
import { logDiagnosticEvent } from '@kb-labs/core-platform';
|
|
18
18
|
import * as path from 'path';
|
|
@@ -823,8 +823,6 @@ function buildRegistrySseAuthHook(config) {
|
|
|
823
823
|
});
|
|
824
824
|
};
|
|
825
825
|
}
|
|
826
|
-
|
|
827
|
-
// src/routes/events.ts
|
|
828
826
|
async function registerEventRoutes(server, basePath, registry, readiness, eventHub, config) {
|
|
829
827
|
const endpoint = `${basePath}/events/registry`;
|
|
830
828
|
const authHook = buildRegistrySseAuthHook(config);
|
|
@@ -833,25 +831,18 @@ async function registerEventRoutes(server, basePath, registry, readiness, eventH
|
|
|
833
831
|
url: endpoint,
|
|
834
832
|
onRequest: authHook ? [authHook] : void 0,
|
|
835
833
|
handler: async (request, reply) => {
|
|
836
|
-
reply.hijack();
|
|
837
834
|
const origin = request.headers.origin;
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
reply.raw.setHeader("Connection", "keep-alive");
|
|
845
|
-
reply.raw.flushHeaders?.();
|
|
846
|
-
reply.raw.write(": connected\n\n");
|
|
835
|
+
const stream = createSseStream(request, reply, {
|
|
836
|
+
logger: request.kbLogger ?? platform.logger,
|
|
837
|
+
serviceId: "rest",
|
|
838
|
+
route: endpoint,
|
|
839
|
+
headers: origin === "http://localhost:3000" || origin === "http://localhost:5173" ? { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true" } : void 0
|
|
840
|
+
});
|
|
847
841
|
const send = (event) => {
|
|
848
|
-
|
|
849
|
-
`);
|
|
850
|
-
reply.raw.write(`data: ${JSON.stringify(event)}
|
|
851
|
-
|
|
852
|
-
`);
|
|
842
|
+
stream.send(event.type, event);
|
|
853
843
|
};
|
|
854
844
|
const unsubscribe = eventHub.subscribe(send);
|
|
845
|
+
stream.onCleanup(unsubscribe);
|
|
855
846
|
const snapshot = registry.snapshot();
|
|
856
847
|
const checksumAlgorithm = snapshot.checksumAlgorithm === "sha256" ? "sha256" : void 0;
|
|
857
848
|
send({
|
|
@@ -899,20 +890,12 @@ async function registerEventRoutes(server, basePath, registry, readiness, eventH
|
|
|
899
890
|
request.kbLogger.warn("Failed to fetch system health for SSE client", { err: error });
|
|
900
891
|
}
|
|
901
892
|
});
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
if (request.raw.socket && !request.raw.socket.writable) {
|
|
909
|
-
void healthPromise.then(() => {
|
|
910
|
-
unsubscribe();
|
|
911
|
-
reply.raw.end();
|
|
912
|
-
resolve2();
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
});
|
|
893
|
+
if (request.raw.socket && !request.raw.socket.writable) {
|
|
894
|
+
await healthPromise;
|
|
895
|
+
stream.close("test_complete");
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
await stream.closed;
|
|
916
899
|
}
|
|
917
900
|
});
|
|
918
901
|
}
|
|
@@ -2052,7 +2035,7 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2052
2035
|
const backend = platform.executionBackend;
|
|
2053
2036
|
const wsBackend = new InProcessBackend({ platform: platform });
|
|
2054
2037
|
const mountableManifests = manifests.filter(
|
|
2055
|
-
(entry) => entry.manifest.rest?.routes && entry.manifest.rest.routes.length > 0 || entry.manifest.ws?.channels && entry.manifest.ws.channels.length > 0
|
|
2038
|
+
(entry) => entry.manifest.rest?.routes && entry.manifest.rest.routes.length > 0 || entry.manifest.sse?.streams && entry.manifest.sse.streams.length > 0 || entry.manifest.ws?.channels && entry.manifest.ws.channels.length > 0
|
|
2056
2039
|
);
|
|
2057
2040
|
platform.logger.info("Plugins selected for route mounting", {
|
|
2058
2041
|
mountable: mountableManifests.length,
|
|
@@ -2074,6 +2057,18 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2074
2057
|
}
|
|
2075
2058
|
}
|
|
2076
2059
|
}
|
|
2060
|
+
if (entry.manifest.sse?.streams) {
|
|
2061
|
+
for (const stream of entry.manifest.sse.streams) {
|
|
2062
|
+
const handlerFile = stream.handler.split("#")[0];
|
|
2063
|
+
if (handlerFile) {
|
|
2064
|
+
const pluginDistRoot = path.join(entry.pluginRoot, "dist");
|
|
2065
|
+
handlerChecks.push({
|
|
2066
|
+
key: `${entry.manifest.id}::SSE ${stream.path}`,
|
|
2067
|
+
filePath: path.resolve(pluginDistRoot, handlerFile)
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2077
2072
|
}
|
|
2078
2073
|
const handlerAccessResults = await Promise.all(
|
|
2079
2074
|
handlerChecks.map(async (check) => {
|
|
@@ -2125,6 +2120,25 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2125
2120
|
}
|
|
2126
2121
|
}
|
|
2127
2122
|
}
|
|
2123
|
+
if (manifest.sse?.streams) {
|
|
2124
|
+
for (const stream of manifest.sse.streams) {
|
|
2125
|
+
const handlerFile = stream.handler.split("#")[0];
|
|
2126
|
+
if (!handlerFile) {
|
|
2127
|
+
restValidationErrors.push(
|
|
2128
|
+
`Event stream ${stream.path}: Invalid handler reference "${stream.handler}"`
|
|
2129
|
+
);
|
|
2130
|
+
continue;
|
|
2131
|
+
}
|
|
2132
|
+
const check = handlerExistsMap.get(
|
|
2133
|
+
`${manifest.id}::SSE ${stream.path}`
|
|
2134
|
+
);
|
|
2135
|
+
if (check && !check.exists) {
|
|
2136
|
+
restValidationErrors.push(
|
|
2137
|
+
`Event stream ${stream.path}: Handler file not found: ${check.filePath}`
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2128
2142
|
if (restValidationErrors.length > 0) {
|
|
2129
2143
|
const reasonCode = inferRouteValidationReasonCode(restValidationErrors);
|
|
2130
2144
|
logDiagnosticEvent(platform.logger, {
|
|
@@ -2161,11 +2175,17 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2161
2175
|
return match ? `${match[1]} ${match[2]}` : null;
|
|
2162
2176
|
}).filter(Boolean)
|
|
2163
2177
|
);
|
|
2164
|
-
const validRoutes = manifest.rest
|
|
2178
|
+
const validRoutes = (manifest.rest?.routes ?? []).filter((route) => {
|
|
2165
2179
|
const routeKey = `${route.method} ${route.path}`;
|
|
2166
2180
|
return !errorPaths.has(routeKey);
|
|
2167
2181
|
});
|
|
2168
|
-
|
|
2182
|
+
const streamErrorPaths = new Set(
|
|
2183
|
+
restValidationErrors.map((error) => error.match(/Event stream\s+([^\s:]+)/)?.[1]).filter((value) => Boolean(value))
|
|
2184
|
+
);
|
|
2185
|
+
const validStreams = (manifest.sse?.streams ?? []).filter(
|
|
2186
|
+
(stream) => !streamErrorPaths.has(stream.path)
|
|
2187
|
+
);
|
|
2188
|
+
if (validRoutes.length === 0 && validStreams.length === 0) {
|
|
2169
2189
|
restDomainOperationMetrics.recordOperation(
|
|
2170
2190
|
"plugin.routes.mount",
|
|
2171
2191
|
0,
|
|
@@ -2193,11 +2213,17 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2193
2213
|
});
|
|
2194
2214
|
continue;
|
|
2195
2215
|
}
|
|
2196
|
-
manifest.rest
|
|
2216
|
+
if (manifest.rest) {
|
|
2217
|
+
manifest.rest.routes = validRoutes;
|
|
2218
|
+
}
|
|
2219
|
+
if (manifest.sse) {
|
|
2220
|
+
manifest.sse.streams = validStreams;
|
|
2221
|
+
}
|
|
2197
2222
|
platform.logger.info("Filtered routes, mounting valid ones", {
|
|
2198
2223
|
plugin: `${manifest.id}@${manifest.version}`,
|
|
2199
|
-
totalRoutes:
|
|
2224
|
+
totalRoutes: validRoutes.length + restValidationErrors.length,
|
|
2200
2225
|
validRoutes: validRoutes.length,
|
|
2226
|
+
validStreams: validStreams.length,
|
|
2201
2227
|
skippedRoutes: restValidationErrors.length
|
|
2202
2228
|
});
|
|
2203
2229
|
}
|
|
@@ -2218,13 +2244,28 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2218
2244
|
} else {
|
|
2219
2245
|
pluginBasePath = `${config.basePath}/plugins/${manifest.id}`;
|
|
2220
2246
|
}
|
|
2247
|
+
let sseBasePath = pluginBasePath;
|
|
2248
|
+
if (manifest.sse?.basePath) {
|
|
2249
|
+
if (manifest.sse.basePath.startsWith("/api/")) {
|
|
2250
|
+
sseBasePath = manifest.sse.basePath;
|
|
2251
|
+
} else if (manifest.sse.basePath.startsWith("/v1/")) {
|
|
2252
|
+
sseBasePath = manifest.sse.basePath.replace(
|
|
2253
|
+
/^\/v1/,
|
|
2254
|
+
config.basePath
|
|
2255
|
+
);
|
|
2256
|
+
} else {
|
|
2257
|
+
sseBasePath = `${config.basePath}${manifest.sse.basePath}`;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2221
2260
|
platform.logger.info("Mounting plugin routes", {
|
|
2222
2261
|
plugin: `${manifest.id}@${manifest.version}`,
|
|
2223
2262
|
configBasePath: config.basePath,
|
|
2224
2263
|
manifestBasePath: manifest.rest?.basePath,
|
|
2225
2264
|
pluginBasePath,
|
|
2265
|
+
sseBasePath,
|
|
2226
2266
|
pluginRoot,
|
|
2227
|
-
routes: manifest.rest?.routes?.length ?? 0
|
|
2267
|
+
routes: manifest.rest?.routes?.length ?? 0,
|
|
2268
|
+
streams: manifest.sse?.streams?.length ?? 0
|
|
2228
2269
|
});
|
|
2229
2270
|
await mountRoutes(server, manifest, {
|
|
2230
2271
|
backend,
|
|
@@ -2233,8 +2274,28 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2233
2274
|
basePath: pluginBasePath,
|
|
2234
2275
|
defaultTimeoutMs: gatewayTimeoutMs
|
|
2235
2276
|
});
|
|
2277
|
+
let streamsCount = 0;
|
|
2278
|
+
if (manifest.sse?.streams && manifest.sse.streams.length > 0) {
|
|
2279
|
+
const streamsResult = await mountEventStreams(server, manifest, {
|
|
2280
|
+
backend: wsBackend,
|
|
2281
|
+
logger: platform.logger,
|
|
2282
|
+
serviceId: "rest",
|
|
2283
|
+
pluginRoot: pluginDistRoot,
|
|
2284
|
+
workspaceRoot,
|
|
2285
|
+
basePath: sseBasePath,
|
|
2286
|
+
defaultTimeoutMs: manifest.sse.defaults?.timeoutMs ?? gatewayTimeoutMs,
|
|
2287
|
+
defaultKeepAliveMs: manifest.sse.defaults?.keepAliveMs
|
|
2288
|
+
});
|
|
2289
|
+
streamsCount = streamsResult.mounted;
|
|
2290
|
+
if (streamsResult.errors.length > 0) {
|
|
2291
|
+
platform.logger.warn("SSE stream mounting had errors", {
|
|
2292
|
+
plugin: `${manifest.id}@${manifest.version}`,
|
|
2293
|
+
errors: streamsResult.errors
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2236
2297
|
const duration = performance.now() - start;
|
|
2237
|
-
const routesCount = manifest.rest?.routes?.length ?? 0;
|
|
2298
|
+
const routesCount = (manifest.rest?.routes?.length ?? 0) + streamsCount;
|
|
2238
2299
|
for (const route of manifest.rest?.routes ?? []) {
|
|
2239
2300
|
metricsCollector.registerRouteBudget(
|
|
2240
2301
|
route.method,
|
|
@@ -2261,6 +2322,7 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2261
2322
|
plugin: `${manifest.id}@${manifest.version}`,
|
|
2262
2323
|
pluginBasePath,
|
|
2263
2324
|
routesCount,
|
|
2325
|
+
streamsCount,
|
|
2264
2326
|
durationMs: Number(duration.toFixed(2))
|
|
2265
2327
|
});
|
|
2266
2328
|
let channelsCount = 0;
|
|
@@ -2930,38 +2992,27 @@ async function registerWorkflowRoutes(server, config) {
|
|
|
2930
2992
|
url: `${basePath}/workflows/runs/:runId/events`,
|
|
2931
2993
|
handler: async (request, reply) => {
|
|
2932
2994
|
const runId = getRunId(request.params);
|
|
2933
|
-
reply.hijack();
|
|
2934
|
-
const raw = reply.raw;
|
|
2935
2995
|
const origin = request.headers.origin;
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
raw.flushHeaders?.();
|
|
2944
|
-
raw.write(": connected\n\n");
|
|
2996
|
+
const stream = createSseStream(request, reply, {
|
|
2997
|
+
logger: request.kbLogger ?? platform.logger,
|
|
2998
|
+
serviceId: "rest",
|
|
2999
|
+
route: `${basePath}/workflows/runs/:runId/events`,
|
|
3000
|
+
keepAliveMs: KEEP_ALIVE_MS,
|
|
3001
|
+
headers: typeof origin === "string" && (origin === "http://localhost:3000" || origin === "http://localhost:5173") ? { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true" } : void 0
|
|
3002
|
+
});
|
|
2945
3003
|
const sentIds = /* @__PURE__ */ new Set();
|
|
2946
3004
|
const sendEvent = (type, payload, timestamp) => {
|
|
2947
|
-
if (raw.writableEnded) {
|
|
2948
|
-
return;
|
|
2949
|
-
}
|
|
2950
3005
|
const data = { type, runId, payload, timestamp: timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
|
|
2951
3006
|
const dedup = `${type}:${data.timestamp}`;
|
|
2952
3007
|
if (sentIds.has(dedup)) {
|
|
2953
3008
|
return;
|
|
2954
3009
|
}
|
|
2955
3010
|
sentIds.add(dedup);
|
|
2956
|
-
|
|
2957
|
-
`);
|
|
2958
|
-
raw.write(`data: ${JSON.stringify(data)}
|
|
2959
|
-
|
|
2960
|
-
`);
|
|
3011
|
+
stream.send("workflow.event", data);
|
|
2961
3012
|
};
|
|
2962
3013
|
const alreadyFinished = await replayHistory(runId, sendEvent);
|
|
2963
3014
|
if (alreadyFinished) {
|
|
2964
|
-
|
|
3015
|
+
stream.close("run_already_finished");
|
|
2965
3016
|
return;
|
|
2966
3017
|
}
|
|
2967
3018
|
let idleTimer = null;
|
|
@@ -2973,12 +3024,6 @@ async function registerWorkflowRoutes(server, config) {
|
|
|
2973
3024
|
cleanup();
|
|
2974
3025
|
}, IDLE_TIMEOUT_MS);
|
|
2975
3026
|
};
|
|
2976
|
-
const keepAliveTimer = setInterval(() => {
|
|
2977
|
-
if (raw.writableEnded) {
|
|
2978
|
-
return;
|
|
2979
|
-
}
|
|
2980
|
-
raw.write(": keep-alive\n\n");
|
|
2981
|
-
}, KEEP_ALIVE_MS);
|
|
2982
3027
|
const unsubscribe = platform.eventBus.subscribe(WORKFLOW_REDIS_CHANNEL, async (rawEvent) => {
|
|
2983
3028
|
const event = rawEvent;
|
|
2984
3029
|
if (event.runId !== runId) {
|
|
@@ -2995,17 +3040,17 @@ async function registerWorkflowRoutes(server, config) {
|
|
|
2995
3040
|
if (idleTimer) {
|
|
2996
3041
|
clearTimeout(idleTimer);
|
|
2997
3042
|
}
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
raw.write(`event: workflow.done
|
|
3001
|
-
data: {}
|
|
3002
|
-
|
|
3003
|
-
`);
|
|
3004
|
-
raw.end();
|
|
3005
|
-
}
|
|
3043
|
+
stream.send("workflow.done", {});
|
|
3044
|
+
stream.close("workflow_complete");
|
|
3006
3045
|
};
|
|
3007
3046
|
resetIdle();
|
|
3008
|
-
|
|
3047
|
+
stream.onCleanup(() => {
|
|
3048
|
+
if (idleTimer) {
|
|
3049
|
+
clearTimeout(idleTimer);
|
|
3050
|
+
}
|
|
3051
|
+
unsubscribe();
|
|
3052
|
+
});
|
|
3053
|
+
await stream.closed;
|
|
3009
3054
|
}
|
|
3010
3055
|
});
|
|
3011
3056
|
server.route({
|
|
@@ -3015,41 +3060,30 @@ data: {}
|
|
|
3015
3060
|
const runId = getRunId(request.params);
|
|
3016
3061
|
const query = request.query;
|
|
3017
3062
|
const idleTimeout = query.idleTimeoutMs ? parseInt(query.idleTimeoutMs, 10) : IDLE_TIMEOUT_MS;
|
|
3018
|
-
reply.hijack();
|
|
3019
|
-
const raw = reply.raw;
|
|
3020
3063
|
const origin = request.headers.origin;
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
raw.flushHeaders?.();
|
|
3029
|
-
raw.write(": connected\n\n");
|
|
3064
|
+
const stream = createSseStream(request, reply, {
|
|
3065
|
+
logger: request.kbLogger ?? platform.logger,
|
|
3066
|
+
serviceId: "rest",
|
|
3067
|
+
route: `${basePath}/workflows/runs/:runId/logs`,
|
|
3068
|
+
keepAliveMs: KEEP_ALIVE_MS,
|
|
3069
|
+
headers: typeof origin === "string" && (origin === "http://localhost:3000" || origin === "http://localhost:5173") ? { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true" } : void 0
|
|
3070
|
+
});
|
|
3030
3071
|
const sentLogIds = /* @__PURE__ */ new Set();
|
|
3031
3072
|
const sendLog = (event) => {
|
|
3032
|
-
if (raw.writableEnded) {
|
|
3033
|
-
return;
|
|
3034
|
-
}
|
|
3035
3073
|
const ts = event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3036
3074
|
const dedup = `${event.type}:${event.jobId ?? ""}:${event.stepId ?? ""}:${ts}`;
|
|
3037
3075
|
if (sentLogIds.has(dedup)) {
|
|
3038
3076
|
return;
|
|
3039
3077
|
}
|
|
3040
3078
|
sentLogIds.add(dedup);
|
|
3041
|
-
|
|
3042
|
-
`);
|
|
3043
|
-
raw.write(`data: ${JSON.stringify({
|
|
3079
|
+
stream.send("workflow.log", {
|
|
3044
3080
|
type: event.type,
|
|
3045
3081
|
runId: event.runId,
|
|
3046
3082
|
jobId: event.jobId,
|
|
3047
3083
|
stepId: event.stepId,
|
|
3048
3084
|
payload: event.payload,
|
|
3049
3085
|
timestamp: ts
|
|
3050
|
-
})
|
|
3051
|
-
|
|
3052
|
-
`);
|
|
3086
|
+
});
|
|
3053
3087
|
};
|
|
3054
3088
|
let logsAlreadyFinished = false;
|
|
3055
3089
|
try {
|
|
@@ -3074,11 +3108,8 @@ data: {}
|
|
|
3074
3108
|
} catch {
|
|
3075
3109
|
}
|
|
3076
3110
|
if (logsAlreadyFinished) {
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
`);
|
|
3081
|
-
raw.end();
|
|
3111
|
+
stream.send("workflow.done", {});
|
|
3112
|
+
stream.close("run_already_finished");
|
|
3082
3113
|
return;
|
|
3083
3114
|
}
|
|
3084
3115
|
let idleTimer = null;
|
|
@@ -3090,12 +3121,6 @@ data: {}
|
|
|
3090
3121
|
cleanup();
|
|
3091
3122
|
}, idleTimeout);
|
|
3092
3123
|
};
|
|
3093
|
-
const keepAliveTimer = setInterval(() => {
|
|
3094
|
-
if (raw.writableEnded) {
|
|
3095
|
-
return;
|
|
3096
|
-
}
|
|
3097
|
-
raw.write(": keep-alive\n\n");
|
|
3098
|
-
}, KEEP_ALIVE_MS);
|
|
3099
3124
|
const unsubscribe = platform.eventBus.subscribe(WORKFLOW_REDIS_CHANNEL, async (rawEvent) => {
|
|
3100
3125
|
const event = rawEvent;
|
|
3101
3126
|
if (event.runId !== runId) {
|
|
@@ -3112,17 +3137,17 @@ data: {}
|
|
|
3112
3137
|
if (idleTimer) {
|
|
3113
3138
|
clearTimeout(idleTimer);
|
|
3114
3139
|
}
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
raw.write(`event: workflow.done
|
|
3118
|
-
data: {}
|
|
3119
|
-
|
|
3120
|
-
`);
|
|
3121
|
-
raw.end();
|
|
3122
|
-
}
|
|
3140
|
+
stream.send("workflow.done", {});
|
|
3141
|
+
stream.close("workflow_complete");
|
|
3123
3142
|
};
|
|
3124
3143
|
resetIdle();
|
|
3125
|
-
|
|
3144
|
+
stream.onCleanup(() => {
|
|
3145
|
+
if (idleTimer) {
|
|
3146
|
+
clearTimeout(idleTimer);
|
|
3147
|
+
}
|
|
3148
|
+
unsubscribe();
|
|
3149
|
+
});
|
|
3150
|
+
await stream.closed;
|
|
3126
3151
|
}
|
|
3127
3152
|
});
|
|
3128
3153
|
}
|
|
@@ -3372,7 +3397,7 @@ async function registerCacheRoutes(server, config, registry) {
|
|
|
3372
3397
|
}
|
|
3373
3398
|
});
|
|
3374
3399
|
}
|
|
3375
|
-
async function registerObservabilityRoutes(fastify, config, repoRoot, historicalMetrics,
|
|
3400
|
+
async function registerObservabilityRoutes(fastify, config, repoRoot, historicalMetrics, platform17) {
|
|
3376
3401
|
const basePath = normalizeBasePath(config.basePath);
|
|
3377
3402
|
const stateBrokerPaths = resolvePaths(basePath, "/observability/state-broker");
|
|
3378
3403
|
const systemMetricsPaths = resolvePaths(basePath, "/observability/system-metrics");
|
|
@@ -3419,7 +3444,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3419
3444
|
}
|
|
3420
3445
|
};
|
|
3421
3446
|
} catch (error) {
|
|
3422
|
-
|
|
3447
|
+
platform17?.logger.error("Failed to fetch State Broker stats", error instanceof Error ? error : new Error(String(error)));
|
|
3423
3448
|
const isTimeout = error instanceof Error && error.name === "AbortError";
|
|
3424
3449
|
return reply.code(503).send({
|
|
3425
3450
|
ok: false,
|
|
@@ -3437,7 +3462,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3437
3462
|
for (const path2 of systemMetricsPaths) {
|
|
3438
3463
|
fastify.get(path2, { schema: { tags: ["Observability"], summary: "System resource metrics" } }, async (_request, reply) => {
|
|
3439
3464
|
try {
|
|
3440
|
-
if (!
|
|
3465
|
+
if (!platform17?.cache) {
|
|
3441
3466
|
return reply.code(503).send({
|
|
3442
3467
|
ok: false,
|
|
3443
3468
|
error: {
|
|
@@ -3449,18 +3474,18 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3449
3474
|
fastify.log.debug("Fetching system metrics from all instances");
|
|
3450
3475
|
const allMetrics = [];
|
|
3451
3476
|
try {
|
|
3452
|
-
const cacheWithScan =
|
|
3453
|
-
if ("scan" in
|
|
3477
|
+
const cacheWithScan = platform17.cache;
|
|
3478
|
+
if ("scan" in platform17.cache && typeof cacheWithScan.scan === "function") {
|
|
3454
3479
|
const keys = await cacheWithScan.scan("system-metrics:*");
|
|
3455
3480
|
for (const key of keys) {
|
|
3456
|
-
const metrics = await
|
|
3481
|
+
const metrics = await platform17.cache.get(key);
|
|
3457
3482
|
if (metrics) {
|
|
3458
3483
|
allMetrics.push(metrics);
|
|
3459
3484
|
}
|
|
3460
3485
|
}
|
|
3461
3486
|
} else {
|
|
3462
3487
|
const currentInstanceId = hostname();
|
|
3463
|
-
const metrics = await
|
|
3488
|
+
const metrics = await platform17.cache.get(`system-metrics:${currentInstanceId}`);
|
|
3464
3489
|
if (metrics) {
|
|
3465
3490
|
allMetrics.push(metrics);
|
|
3466
3491
|
}
|
|
@@ -3469,7 +3494,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3469
3494
|
} catch (scanError) {
|
|
3470
3495
|
fastify.log.warn({ err: scanError }, "Failed to scan platform.cache for system metrics");
|
|
3471
3496
|
const currentInstanceId = hostname();
|
|
3472
|
-
const metrics = await
|
|
3497
|
+
const metrics = await platform17.cache.get(`system-metrics:${currentInstanceId}`);
|
|
3473
3498
|
if (metrics) {
|
|
3474
3499
|
allMetrics.push(metrics);
|
|
3475
3500
|
}
|
|
@@ -3517,7 +3542,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3517
3542
|
}
|
|
3518
3543
|
};
|
|
3519
3544
|
} catch (error) {
|
|
3520
|
-
|
|
3545
|
+
platform17?.logger.error("Failed to fetch system metrics", error instanceof Error ? error : new Error(String(error)));
|
|
3521
3546
|
return reply.code(500).send({
|
|
3522
3547
|
ok: false,
|
|
3523
3548
|
error: {
|
|
@@ -3615,7 +3640,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3615
3640
|
}
|
|
3616
3641
|
};
|
|
3617
3642
|
} catch (error) {
|
|
3618
|
-
|
|
3643
|
+
platform17?.logger.error("Failed to query historical metrics", error instanceof Error ? error : new Error(String(error)), { query });
|
|
3619
3644
|
return reply.code(500).send({
|
|
3620
3645
|
ok: false,
|
|
3621
3646
|
error: {
|
|
@@ -3706,7 +3731,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3706
3731
|
}
|
|
3707
3732
|
};
|
|
3708
3733
|
} catch (error) {
|
|
3709
|
-
|
|
3734
|
+
platform17?.logger.error("Failed to query heatmap data", error instanceof Error ? error : new Error(String(error)), { query });
|
|
3710
3735
|
return reply.code(500).send({
|
|
3711
3736
|
ok: false,
|
|
3712
3737
|
error: {
|
|
@@ -3740,7 +3765,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3740
3765
|
}
|
|
3741
3766
|
}
|
|
3742
3767
|
}, async (request, reply) => {
|
|
3743
|
-
if (!
|
|
3768
|
+
if (!platform17?.llm) {
|
|
3744
3769
|
return reply.code(503).send({
|
|
3745
3770
|
ok: false,
|
|
3746
3771
|
error: {
|
|
@@ -3806,15 +3831,15 @@ Provide a clear, actionable response based on the data above. Include:
|
|
|
3806
3831
|
|
|
3807
3832
|
Be concise but thorough. Use markdown formatting.`;
|
|
3808
3833
|
fastify.log.debug({ question: body.question, contextLength: contextText.length }, "Calling LLM for insights");
|
|
3809
|
-
const result = await
|
|
3834
|
+
const result = await platform17.llm.complete(prompt, {
|
|
3810
3835
|
systemPrompt: "You are a DevOps and SRE expert assistant. Analyze system metrics and provide actionable insights. Be concise, technical, and helpful.",
|
|
3811
3836
|
temperature: 0.7,
|
|
3812
3837
|
maxTokens: 1e3
|
|
3813
3838
|
});
|
|
3814
3839
|
const totalTokens = result.usage.promptTokens + result.usage.completionTokens;
|
|
3815
3840
|
fastify.log.debug({ tokensUsed: totalTokens }, "LLM response received for insights");
|
|
3816
|
-
if (
|
|
3817
|
-
|
|
3841
|
+
if (platform17.analytics) {
|
|
3842
|
+
platform17.analytics.track("ai_insights.chat", {
|
|
3818
3843
|
questionLength: body.question.length,
|
|
3819
3844
|
contextIncluded: Object.keys(contextConfig).filter((k) => contextConfig[k]),
|
|
3820
3845
|
timeRange: contextConfig.timeRange,
|
|
@@ -3843,9 +3868,9 @@ Be concise but thorough. Use markdown formatting.`;
|
|
|
3843
3868
|
}
|
|
3844
3869
|
};
|
|
3845
3870
|
} catch (error) {
|
|
3846
|
-
|
|
3847
|
-
if (
|
|
3848
|
-
|
|
3871
|
+
platform17?.logger.error("Failed to generate insights", error instanceof Error ? error : new Error(String(error)));
|
|
3872
|
+
if (platform17?.analytics) {
|
|
3873
|
+
platform17.analytics.track("ai_insights.error", {
|
|
3849
3874
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
3850
3875
|
questionLength: request.body?.question?.length ?? 0
|
|
3851
3876
|
}).catch(() => {
|
|
@@ -5353,55 +5378,21 @@ async function registerLogRoutes(server, config, eventHub) {
|
|
|
5353
5378
|
message: "Logger adapter does not support streaming. Enable logRingBuffer in kb.config.json"
|
|
5354
5379
|
});
|
|
5355
5380
|
}
|
|
5356
|
-
reply.hijack();
|
|
5357
|
-
let streamClosed = false;
|
|
5358
5381
|
const origin = request.headers.origin;
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
reply.raw.setHeader("Connection", "keep-alive");
|
|
5368
|
-
try {
|
|
5369
|
-
reply.raw.flushHeaders?.();
|
|
5370
|
-
reply.raw.write(": connected\n\n");
|
|
5371
|
-
} catch (err) {
|
|
5372
|
-
streamClosed = true;
|
|
5373
|
-
return;
|
|
5374
|
-
}
|
|
5375
|
-
const unsubscribe = platform.logs.subscribe((log) => {
|
|
5376
|
-
if (!streamClosed && !reply.raw.writableEnded && !reply.raw.destroyed) {
|
|
5377
|
-
try {
|
|
5378
|
-
const frontendLog = toFrontendLogRecord(log);
|
|
5379
|
-
reply.raw.write(`event: log
|
|
5380
|
-
`);
|
|
5381
|
-
reply.raw.write(`data: ${JSON.stringify(frontendLog)}
|
|
5382
|
-
|
|
5383
|
-
`);
|
|
5384
|
-
} catch (err) {
|
|
5385
|
-
streamClosed = true;
|
|
5386
|
-
}
|
|
5387
|
-
}
|
|
5382
|
+
const stream = createSseStream(request, reply, {
|
|
5383
|
+
logger: request.kbLogger ?? platform.logger,
|
|
5384
|
+
serviceId: "rest",
|
|
5385
|
+
route: "/api/v1/logs/stream",
|
|
5386
|
+
// This stream carries logs, so event-level telemetry would feed it
|
|
5387
|
+
// back into itself. Lifecycle + close summary remain enabled.
|
|
5388
|
+
logEvents: false,
|
|
5389
|
+
headers: origin === "http://localhost:3000" || origin === "http://localhost:5173" ? { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true" } : { "Access-Control-Allow-Origin": "*" }
|
|
5388
5390
|
});
|
|
5389
|
-
const
|
|
5390
|
-
|
|
5391
|
-
streamClosed = true;
|
|
5392
|
-
unsubscribe();
|
|
5393
|
-
try {
|
|
5394
|
-
if (!reply.raw.writableEnded && !reply.raw.destroyed) {
|
|
5395
|
-
reply.raw.end();
|
|
5396
|
-
}
|
|
5397
|
-
} catch (err) {
|
|
5398
|
-
}
|
|
5399
|
-
}
|
|
5400
|
-
};
|
|
5401
|
-
request.raw.on("close", cleanup);
|
|
5402
|
-
request.raw.on("error", cleanup);
|
|
5403
|
-
await new Promise(() => {
|
|
5391
|
+
const unsubscribe = platform.logs.subscribe((log) => {
|
|
5392
|
+
stream.send("log", toFrontendLogRecord(log));
|
|
5404
5393
|
});
|
|
5394
|
+
stream.onCleanup(unsubscribe);
|
|
5395
|
+
await stream.closed;
|
|
5405
5396
|
}
|
|
5406
5397
|
);
|
|
5407
5398
|
server.get(
|
|
@@ -6565,7 +6556,7 @@ async function bootstrap(cwd = process.cwd()) {
|
|
|
6565
6556
|
});
|
|
6566
6557
|
}
|
|
6567
6558
|
async function startRestApi({
|
|
6568
|
-
platform:
|
|
6559
|
+
platform: platform17,
|
|
6569
6560
|
projectRoot: repoRoot,
|
|
6570
6561
|
platformRoot,
|
|
6571
6562
|
logger: serviceLogger
|
|
@@ -6604,7 +6595,7 @@ async function startRestApi({
|
|
|
6604
6595
|
platformRoot: platformRoot !== repoRoot ? platformRoot : void 0,
|
|
6605
6596
|
cache: {
|
|
6606
6597
|
ttlMs: snapshotTTL,
|
|
6607
|
-
adapter:
|
|
6598
|
+
adapter: platform17.cache
|
|
6608
6599
|
}
|
|
6609
6600
|
});
|
|
6610
6601
|
restDomainOperationMetrics.recordOperation(
|
|
@@ -6633,7 +6624,7 @@ async function startRestApi({
|
|
|
6633
6624
|
() => metricsCollector.getActiveRequests()
|
|
6634
6625
|
);
|
|
6635
6626
|
await metricsCollector2.start(1e4, 6e4);
|
|
6636
|
-
const restTransport =
|
|
6627
|
+
const restTransport = platform17.getAdapter("serviceTransport");
|
|
6637
6628
|
const restAddr = restTransport?.listenAddress?.("rest");
|
|
6638
6629
|
const netOffset = Number(process.env.KB_NET_OFFSET) || 0;
|
|
6639
6630
|
const listenPort = restAddr && "port" in restAddr ? restAddr.port : config.port + netOffset;
|