@kb-labs/rest-api-app 2.112.0 → 2.114.0
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 +642 -519
- package/dist/index.js.map +1 -1
- package/package.json +21 -20
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { loadRestApiConfig, combineManifestsToRegistry } from '@kb-labs/rest-api-core';
|
|
2
2
|
import Fastify from 'fastify';
|
|
3
|
-
import {
|
|
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,
|
|
6
|
+
import { OperationMetricsTracker, getListenOptions, registerOpenAPI, createHttpLogger, 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';
|
|
@@ -29,9 +29,9 @@ import fastifyRateLimit from '@fastify/rate-limit';
|
|
|
29
29
|
import { ulid } from 'ulid';
|
|
30
30
|
import { randomUUID, createHash } from 'crypto';
|
|
31
31
|
import { TenantRateLimiter, getDefaultTenantTier } from '@kb-labs/core-tenant';
|
|
32
|
-
import {
|
|
33
|
-
import { findRepoRoot } from '@kb-labs/core-sys';
|
|
32
|
+
import { existsSync, readFileSync } from 'fs';
|
|
34
33
|
import { makeAssemblyHook } from '@kb-labs/plugin-runtime';
|
|
34
|
+
import { runService } from '@kb-labs/shared-daemon';
|
|
35
35
|
|
|
36
36
|
// src/bootstrap.ts
|
|
37
37
|
var EventHub = class {
|
|
@@ -899,17 +899,17 @@ async function registerEventRoutes(server, basePath, registry, readiness, eventH
|
|
|
899
899
|
request.kbLogger.warn("Failed to fetch system health for SSE client", { err: error });
|
|
900
900
|
}
|
|
901
901
|
});
|
|
902
|
-
await new Promise((
|
|
902
|
+
await new Promise((resolve2) => {
|
|
903
903
|
request.raw.on("close", () => {
|
|
904
904
|
unsubscribe();
|
|
905
905
|
reply.raw.end();
|
|
906
|
-
|
|
906
|
+
resolve2();
|
|
907
907
|
});
|
|
908
908
|
if (request.raw.socket && !request.raw.socket.writable) {
|
|
909
909
|
void healthPromise.then(() => {
|
|
910
910
|
unsubscribe();
|
|
911
911
|
reply.raw.end();
|
|
912
|
-
|
|
912
|
+
resolve2();
|
|
913
913
|
});
|
|
914
914
|
}
|
|
915
915
|
});
|
|
@@ -1529,8 +1529,8 @@ async function registerHealthRoutes(fastify, config, _repoRoot, registry, readin
|
|
|
1529
1529
|
const readyPaths = resolvePaths(basePath, "/ready");
|
|
1530
1530
|
const observabilityDescribePaths = resolvePaths(basePath, "/observability/describe");
|
|
1531
1531
|
const observabilityHealthPaths = resolvePaths(basePath, "/observability/health");
|
|
1532
|
-
for (const
|
|
1533
|
-
fastify.get(
|
|
1532
|
+
for (const path2 of healthPaths) {
|
|
1533
|
+
fastify.get(path2, async (_request, reply) => {
|
|
1534
1534
|
const eventHub = fastify.kbEventHub;
|
|
1535
1535
|
try {
|
|
1536
1536
|
const baseSnapshot = await getBaseSnapshot(registry);
|
|
@@ -1575,8 +1575,8 @@ async function registerHealthRoutes(fastify, config, _repoRoot, registry, readin
|
|
|
1575
1575
|
}
|
|
1576
1576
|
});
|
|
1577
1577
|
}
|
|
1578
|
-
for (const
|
|
1579
|
-
fastify.get(
|
|
1578
|
+
for (const path2 of readyPaths) {
|
|
1579
|
+
fastify.get(path2, async (_request, reply) => {
|
|
1580
1580
|
const eventHub = fastify.kbEventHub;
|
|
1581
1581
|
const { registryLoaded, registryPartial, registryStale } = evaluateRegistryState(registry);
|
|
1582
1582
|
readiness.registryLoaded = registryLoaded;
|
|
@@ -1604,11 +1604,11 @@ async function registerHealthRoutes(fastify, config, _repoRoot, registry, readin
|
|
|
1604
1604
|
return reply.code(statusCode).send(response);
|
|
1605
1605
|
});
|
|
1606
1606
|
}
|
|
1607
|
-
for (const
|
|
1608
|
-
fastify.get(
|
|
1607
|
+
for (const path2 of observabilityDescribePaths) {
|
|
1608
|
+
fastify.get(path2, async () => buildRestObservabilityDescribe(basePath));
|
|
1609
1609
|
}
|
|
1610
|
-
for (const
|
|
1611
|
-
fastify.get(
|
|
1610
|
+
for (const path2 of observabilityHealthPaths) {
|
|
1611
|
+
fastify.get(path2, async () => {
|
|
1612
1612
|
try {
|
|
1613
1613
|
const baseSnapshot = await getBaseSnapshot(registry);
|
|
1614
1614
|
const pluginsMetrics = metricsCollector.getLastPluginMountSnapshot();
|
|
@@ -1930,8 +1930,8 @@ function registerMetricsRoutes(server, config) {
|
|
|
1930
1930
|
reply.header("Content-Type", "text/plain; version=0.0.4; charset=utf-8");
|
|
1931
1931
|
return promMetrics + "\n" + customLines.join("\n");
|
|
1932
1932
|
};
|
|
1933
|
-
for (const
|
|
1934
|
-
server.get(
|
|
1933
|
+
for (const path2 of metricsPaths) {
|
|
1934
|
+
server.get(path2, handler);
|
|
1935
1935
|
}
|
|
1936
1936
|
server.get(`${basePath}/metrics/headers/debug`, {
|
|
1937
1937
|
schema: {
|
|
@@ -2087,7 +2087,10 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2087
2087
|
);
|
|
2088
2088
|
const handlerExistsMap = /* @__PURE__ */ new Map();
|
|
2089
2089
|
for (const result of handlerAccessResults) {
|
|
2090
|
-
handlerExistsMap.set(result.key, {
|
|
2090
|
+
handlerExistsMap.set(result.key, {
|
|
2091
|
+
exists: result.exists,
|
|
2092
|
+
filePath: result.filePath
|
|
2093
|
+
});
|
|
2091
2094
|
}
|
|
2092
2095
|
platform.logger.debug("Batch handler file validation completed", {
|
|
2093
2096
|
totalHandlers: handlerChecks.length,
|
|
@@ -2126,7 +2129,10 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2126
2129
|
const reasonCode = inferRouteValidationReasonCode(restValidationErrors);
|
|
2127
2130
|
logDiagnosticEvent(platform.logger, {
|
|
2128
2131
|
event: "plugin.routes.validation",
|
|
2129
|
-
level: resolveRouteValidationLogLevel(
|
|
2132
|
+
level: resolveRouteValidationLogLevel(
|
|
2133
|
+
restValidationErrors,
|
|
2134
|
+
manifest.rest?.routes?.length ?? 0
|
|
2135
|
+
),
|
|
2130
2136
|
reasonCode,
|
|
2131
2137
|
message: "Plugin route validation failed",
|
|
2132
2138
|
outcome: "failed",
|
|
@@ -2140,22 +2146,31 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2140
2146
|
totalRoutes: manifest.rest?.routes?.length ?? 0
|
|
2141
2147
|
}
|
|
2142
2148
|
});
|
|
2143
|
-
platform.logger.warn(
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2149
|
+
platform.logger.warn(
|
|
2150
|
+
"REST validation errors found, will skip problematic routes but continue mounting others",
|
|
2151
|
+
{
|
|
2152
|
+
plugin: `${manifest.id}@${manifest.version}`,
|
|
2153
|
+
pluginRoot,
|
|
2154
|
+
remediation: "Verify REST handler file and export exist",
|
|
2155
|
+
errors: restValidationErrors
|
|
2156
|
+
}
|
|
2157
|
+
);
|
|
2158
|
+
const errorPaths = new Set(
|
|
2159
|
+
restValidationErrors.map((error) => {
|
|
2160
|
+
const match = error.match(/Route\s+(\w+)\s+([^\s:]+)/);
|
|
2161
|
+
return match ? `${match[1]} ${match[2]}` : null;
|
|
2162
|
+
}).filter(Boolean)
|
|
2163
|
+
);
|
|
2153
2164
|
const validRoutes = manifest.rest.routes.filter((route) => {
|
|
2154
2165
|
const routeKey = `${route.method} ${route.path}`;
|
|
2155
2166
|
return !errorPaths.has(routeKey);
|
|
2156
2167
|
});
|
|
2157
2168
|
if (validRoutes.length === 0) {
|
|
2158
|
-
restDomainOperationMetrics.recordOperation(
|
|
2169
|
+
restDomainOperationMetrics.recordOperation(
|
|
2170
|
+
"plugin.routes.mount",
|
|
2171
|
+
0,
|
|
2172
|
+
"error"
|
|
2173
|
+
);
|
|
2159
2174
|
logDiagnosticEvent(platform.logger, {
|
|
2160
2175
|
event: "plugin.routes.validation",
|
|
2161
2176
|
level: "error",
|
|
@@ -2193,7 +2208,10 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2193
2208
|
if (manifest.rest.basePath.startsWith("/api/")) {
|
|
2194
2209
|
pluginBasePath = manifest.rest.basePath;
|
|
2195
2210
|
} else if (manifest.rest.basePath.startsWith("/v1/")) {
|
|
2196
|
-
pluginBasePath = manifest.rest.basePath.replace(
|
|
2211
|
+
pluginBasePath = manifest.rest.basePath.replace(
|
|
2212
|
+
/^\/v1/,
|
|
2213
|
+
config.basePath
|
|
2214
|
+
);
|
|
2197
2215
|
} else {
|
|
2198
2216
|
pluginBasePath = `${config.basePath}${manifest.rest.basePath}`;
|
|
2199
2217
|
}
|
|
@@ -2226,13 +2244,17 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2226
2244
|
);
|
|
2227
2245
|
}
|
|
2228
2246
|
mountMetrics.recordSuccess(manifest.id, routesCount, duration);
|
|
2229
|
-
restDomainOperationMetrics.recordOperation(
|
|
2247
|
+
restDomainOperationMetrics.recordOperation(
|
|
2248
|
+
"plugin.routes.mount",
|
|
2249
|
+
duration,
|
|
2250
|
+
"ok"
|
|
2251
|
+
);
|
|
2230
2252
|
for (const route of manifest.rest?.routes ?? []) {
|
|
2231
|
-
platform.logger.
|
|
2232
|
-
|
|
2233
|
-
method: route.method,
|
|
2234
|
-
|
|
2235
|
-
handler: route.handler
|
|
2253
|
+
platform.logger.debug("Mounted route", {
|
|
2254
|
+
pluginId: manifest.id,
|
|
2255
|
+
"http.method": route.method,
|
|
2256
|
+
"http.route": `${pluginBasePath}${route.path}`,
|
|
2257
|
+
"plugin.handler": route.handler
|
|
2236
2258
|
});
|
|
2237
2259
|
}
|
|
2238
2260
|
platform.logger.info("Successfully mounted plugin routes", {
|
|
@@ -2301,7 +2323,11 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2301
2323
|
succeeded.push(manifest.id);
|
|
2302
2324
|
} catch (error) {
|
|
2303
2325
|
mountMetrics.recordFailure(manifest.id, shortErrorMessage(error));
|
|
2304
|
-
restDomainOperationMetrics.recordOperation(
|
|
2326
|
+
restDomainOperationMetrics.recordOperation(
|
|
2327
|
+
"plugin.routes.mount",
|
|
2328
|
+
0,
|
|
2329
|
+
"error"
|
|
2330
|
+
);
|
|
2305
2331
|
logDiagnosticEvent(platform.logger, {
|
|
2306
2332
|
event: "plugin.routes.mount",
|
|
2307
2333
|
level: "error",
|
|
@@ -2332,14 +2358,17 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2332
2358
|
if (readiness) {
|
|
2333
2359
|
readiness.pluginRouteFailures = routeFailures;
|
|
2334
2360
|
}
|
|
2335
|
-
platform.logger.info(
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2361
|
+
platform.logger.info(
|
|
2362
|
+
"Sequential plugin route and WebSocket channel mounting completed",
|
|
2363
|
+
{
|
|
2364
|
+
total: mountableManifests.length,
|
|
2365
|
+
succeeded: succeeded.length,
|
|
2366
|
+
failed: failed.length,
|
|
2367
|
+
mountedRoutes,
|
|
2368
|
+
mountedChannels,
|
|
2369
|
+
errors
|
|
2370
|
+
}
|
|
2371
|
+
);
|
|
2343
2372
|
} catch (error) {
|
|
2344
2373
|
logDiagnosticEvent(platform.logger, {
|
|
2345
2374
|
domain: "registry",
|
|
@@ -2367,7 +2396,9 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2367
2396
|
error: `rest_discovery_failed ${shortErrorMessage(error)}`
|
|
2368
2397
|
});
|
|
2369
2398
|
}
|
|
2370
|
-
metricsCollector.completePluginMount(
|
|
2399
|
+
metricsCollector.completePluginMount(
|
|
2400
|
+
platform.logger
|
|
2401
|
+
);
|
|
2371
2402
|
return;
|
|
2372
2403
|
}
|
|
2373
2404
|
if (readiness) {
|
|
@@ -2380,15 +2411,20 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
|
|
|
2380
2411
|
(performance.now() - mountStart).toFixed(2)
|
|
2381
2412
|
);
|
|
2382
2413
|
}
|
|
2383
|
-
metricsCollector.completePluginMount(
|
|
2414
|
+
metricsCollector.completePluginMount(
|
|
2415
|
+
platform.logger
|
|
2416
|
+
);
|
|
2384
2417
|
}
|
|
2385
2418
|
async function registerPluginSnapshotRoutes(server, config, registry) {
|
|
2386
2419
|
const basePath = config.basePath;
|
|
2387
2420
|
server.post(`${basePath}/plugins/refresh`, async (_request, reply) => {
|
|
2388
2421
|
try {
|
|
2389
|
-
await restDomainOperationMetrics.observeOperation(
|
|
2390
|
-
|
|
2391
|
-
|
|
2422
|
+
await restDomainOperationMetrics.observeOperation(
|
|
2423
|
+
"plugin.registry.refresh",
|
|
2424
|
+
async () => {
|
|
2425
|
+
await registry.refresh();
|
|
2426
|
+
}
|
|
2427
|
+
);
|
|
2392
2428
|
const snapshot = await restDomainOperationMetrics.observeOperation(
|
|
2393
2429
|
"plugin.registry.snapshot",
|
|
2394
2430
|
async () => registry.snapshot()
|
|
@@ -2486,119 +2522,151 @@ async function registerPluginSnapshotRoutes(server, config, registry) {
|
|
|
2486
2522
|
});
|
|
2487
2523
|
}
|
|
2488
2524
|
});
|
|
2489
|
-
server.get(
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
return reply.code(
|
|
2525
|
+
server.get(
|
|
2526
|
+
`${basePath}/plugins/@:scope/:name/readme`,
|
|
2527
|
+
{
|
|
2528
|
+
schema: { hide: true }
|
|
2529
|
+
},
|
|
2530
|
+
async (request, reply) => {
|
|
2531
|
+
const { scope, name } = request.params;
|
|
2532
|
+
if (!scope || !name) {
|
|
2533
|
+
return reply.code(400).send({ error: "Invalid plugin identifier" });
|
|
2534
|
+
}
|
|
2535
|
+
const pluginId = `@${scope}/${name}`;
|
|
2536
|
+
const snapshot = registry.snapshot();
|
|
2537
|
+
const manifests = extractSnapshotManifests(snapshot);
|
|
2538
|
+
const entry = manifests.find((e) => e.pluginId === pluginId);
|
|
2539
|
+
if (!entry) {
|
|
2540
|
+
return reply.code(404).send({ error: `Plugin not found: ${pluginId}` });
|
|
2541
|
+
}
|
|
2542
|
+
const readmePath = path.join(entry.pluginRoot, "README.md");
|
|
2543
|
+
if (!readmePath.startsWith(entry.pluginRoot)) {
|
|
2544
|
+
return reply.code(400).send({ error: "Invalid path" });
|
|
2545
|
+
}
|
|
2546
|
+
try {
|
|
2547
|
+
const content = await fs.readFile(readmePath, "utf-8");
|
|
2548
|
+
return reply.code(200).header("Content-Type", "text/plain; charset=utf-8").send(content);
|
|
2549
|
+
} catch (error) {
|
|
2550
|
+
if (error.code === "ENOENT") {
|
|
2551
|
+
return reply.code(404).send({ error: "README.md not found" });
|
|
2552
|
+
}
|
|
2553
|
+
return reply.code(500).send({ error: "Failed to read README.md" });
|
|
2513
2554
|
}
|
|
2514
|
-
return reply.code(500).send({ error: "Failed to read README.md" });
|
|
2515
|
-
}
|
|
2516
|
-
});
|
|
2517
|
-
server.get(`${basePath}/plugins/@:scope/:name/changelog`, {
|
|
2518
|
-
schema: { hide: true }
|
|
2519
|
-
}, async (request, reply) => {
|
|
2520
|
-
const { scope, name } = request.params;
|
|
2521
|
-
if (!scope || !name) {
|
|
2522
|
-
return reply.code(400).send({ error: "Invalid plugin identifier" });
|
|
2523
|
-
}
|
|
2524
|
-
const pluginId = `@${scope}/${name}`;
|
|
2525
|
-
const snapshot = registry.snapshot();
|
|
2526
|
-
const manifests = extractSnapshotManifests(snapshot);
|
|
2527
|
-
const entry = manifests.find((e) => e.pluginId === pluginId);
|
|
2528
|
-
if (!entry) {
|
|
2529
|
-
return reply.code(404).send({ error: `Plugin not found: ${pluginId}` });
|
|
2530
2555
|
}
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2556
|
+
);
|
|
2557
|
+
server.get(
|
|
2558
|
+
`${basePath}/plugins/@:scope/:name/changelog`,
|
|
2559
|
+
{
|
|
2560
|
+
schema: { hide: true }
|
|
2561
|
+
},
|
|
2562
|
+
async (request, reply) => {
|
|
2563
|
+
const { scope, name } = request.params;
|
|
2564
|
+
if (!scope || !name) {
|
|
2565
|
+
return reply.code(400).send({ error: "Invalid plugin identifier" });
|
|
2566
|
+
}
|
|
2567
|
+
const pluginId = `@${scope}/${name}`;
|
|
2568
|
+
const snapshot = registry.snapshot();
|
|
2569
|
+
const manifests = extractSnapshotManifests(snapshot);
|
|
2570
|
+
const entry = manifests.find((e) => e.pluginId === pluginId);
|
|
2571
|
+
if (!entry) {
|
|
2572
|
+
return reply.code(404).send({ error: `Plugin not found: ${pluginId}` });
|
|
2573
|
+
}
|
|
2574
|
+
const changelogPath = path.join(entry.pluginRoot, "CHANGELOG.md");
|
|
2575
|
+
if (!changelogPath.startsWith(entry.pluginRoot)) {
|
|
2576
|
+
return reply.code(400).send({ error: "Invalid path" });
|
|
2577
|
+
}
|
|
2578
|
+
try {
|
|
2579
|
+
const content = await fs.readFile(changelogPath, "utf-8");
|
|
2580
|
+
return reply.code(200).header("Content-Type", "text/plain; charset=utf-8").send(content);
|
|
2581
|
+
} catch (error) {
|
|
2582
|
+
if (error.code === "ENOENT") {
|
|
2583
|
+
return reply.code(404).send({ error: "CHANGELOG.md not found" });
|
|
2584
|
+
}
|
|
2585
|
+
return reply.code(500).send({ error: "Failed to read CHANGELOG.md" });
|
|
2586
|
+
}
|
|
2534
2587
|
}
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2588
|
+
);
|
|
2589
|
+
server.get(
|
|
2590
|
+
`${basePath}/studio/registry`,
|
|
2591
|
+
{
|
|
2592
|
+
schema: {
|
|
2593
|
+
tags: ["Studio"],
|
|
2594
|
+
summary: "Get Studio Registry V2 (plugin pages for Module Federation)"
|
|
2595
|
+
}
|
|
2596
|
+
},
|
|
2597
|
+
async (_request, reply) => {
|
|
2598
|
+
try {
|
|
2599
|
+
const snapshot = await restDomainOperationMetrics.observeOperation(
|
|
2600
|
+
"plugin.studio.registry",
|
|
2601
|
+
async () => registry.snapshot()
|
|
2602
|
+
);
|
|
2603
|
+
const manifests = extractSnapshotManifests(snapshot);
|
|
2604
|
+
const studioManifests = manifests.filter((entry) => entry.manifest.studio?.version === 2).map((entry) => ({
|
|
2605
|
+
manifest: entry.manifest,
|
|
2606
|
+
pluginRoot: entry.pluginRoot
|
|
2607
|
+
}));
|
|
2608
|
+
const studioRegistry = combineManifestsToRegistry(studioManifests);
|
|
2609
|
+
reply.type("application/json");
|
|
2610
|
+
return studioRegistry;
|
|
2611
|
+
} catch (error) {
|
|
2612
|
+
platform.logger.error(
|
|
2613
|
+
"Failed to generate studio registry",
|
|
2614
|
+
error instanceof Error ? error : new Error(String(error))
|
|
2615
|
+
);
|
|
2616
|
+
reply.code(500).send({
|
|
2617
|
+
error: "Failed to generate studio registry",
|
|
2618
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2619
|
+
});
|
|
2541
2620
|
}
|
|
2542
|
-
return reply.code(500).send({ error: "Failed to read CHANGELOG.md" });
|
|
2543
2621
|
}
|
|
2544
|
-
|
|
2545
|
-
server.get(
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2622
|
+
);
|
|
2623
|
+
server.get(
|
|
2624
|
+
"/plugins/@:scope/:name/widgets/*",
|
|
2625
|
+
{
|
|
2626
|
+
schema: { hide: true }
|
|
2627
|
+
},
|
|
2628
|
+
async (request, reply) => {
|
|
2629
|
+
const { scope, name } = request.params;
|
|
2630
|
+
const filePath = request.params["*"];
|
|
2631
|
+
if (!scope || !name || !filePath || filePath.includes("..")) {
|
|
2632
|
+
return reply.code(400).send({ error: "Invalid path" });
|
|
2633
|
+
}
|
|
2634
|
+
const pluginId = `@${scope}/${name}`;
|
|
2635
|
+
const snapshot = registry.snapshot();
|
|
2553
2636
|
const manifests = extractSnapshotManifests(snapshot);
|
|
2554
|
-
const
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2637
|
+
const entry = manifests.find(
|
|
2638
|
+
(e) => e.manifest.id === pluginId && e.manifest.studio?.version === 2
|
|
2639
|
+
);
|
|
2640
|
+
if (!entry) {
|
|
2641
|
+
return reply.code(404).send({ error: `No widget bundle for ${pluginId}` });
|
|
2642
|
+
}
|
|
2643
|
+
const { join: join2, extname } = await import('path');
|
|
2644
|
+
const { createReadStream, existsSync: existsSync2, statSync } = await import('fs');
|
|
2645
|
+
const bundleDir = join2(entry.pluginRoot, "dist", "widgets");
|
|
2646
|
+
const fullPath = join2(bundleDir, filePath);
|
|
2647
|
+
if (!fullPath.startsWith(bundleDir)) {
|
|
2648
|
+
return reply.code(400).send({ error: "Invalid path" });
|
|
2649
|
+
}
|
|
2650
|
+
if (!existsSync2(fullPath) || !statSync(fullPath).isFile()) {
|
|
2651
|
+
return reply.code(404).send({
|
|
2652
|
+
error: `Widget file not found: ${pluginId}/widgets/${filePath}`
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
const MIME = {
|
|
2656
|
+
".js": "application/javascript",
|
|
2657
|
+
".mjs": "application/javascript",
|
|
2658
|
+
".css": "text/css",
|
|
2659
|
+
".json": "application/json",
|
|
2660
|
+
".map": "application/json"
|
|
2661
|
+
};
|
|
2662
|
+
const ext = extname(filePath);
|
|
2663
|
+
const isEntry = filePath === "remoteEntry.js";
|
|
2664
|
+
return reply.code(200).header("Content-Type", MIME[ext] ?? "application/octet-stream").header("Content-Length", statSync(fullPath).size).header(
|
|
2665
|
+
"Cache-Control",
|
|
2666
|
+
isEntry ? "public, max-age=10, must-revalidate" : process.env.NODE_ENV === "production" ? "public, max-age=31536000, immutable" : "no-cache"
|
|
2667
|
+
).send(createReadStream(fullPath));
|
|
2564
2668
|
}
|
|
2565
|
-
|
|
2566
|
-
server.get("/plugins/@:scope/:name/widgets/*", {
|
|
2567
|
-
schema: { hide: true }
|
|
2568
|
-
}, async (request, reply) => {
|
|
2569
|
-
const { scope, name } = request.params;
|
|
2570
|
-
const filePath = request.params["*"];
|
|
2571
|
-
if (!scope || !name || !filePath || filePath.includes("..")) {
|
|
2572
|
-
return reply.code(400).send({ error: "Invalid path" });
|
|
2573
|
-
}
|
|
2574
|
-
const pluginId = `@${scope}/${name}`;
|
|
2575
|
-
const snapshot = registry.snapshot();
|
|
2576
|
-
const manifests = extractSnapshotManifests(snapshot);
|
|
2577
|
-
const entry = manifests.find((e) => e.manifest.id === pluginId && e.manifest.studio?.version === 2);
|
|
2578
|
-
if (!entry) {
|
|
2579
|
-
return reply.code(404).send({ error: `No widget bundle for ${pluginId}` });
|
|
2580
|
-
}
|
|
2581
|
-
const { join: join3, extname } = await import('path');
|
|
2582
|
-
const { createReadStream, existsSync: existsSync2, statSync } = await import('fs');
|
|
2583
|
-
const bundleDir = join3(entry.pluginRoot, "dist", "widgets");
|
|
2584
|
-
const fullPath = join3(bundleDir, filePath);
|
|
2585
|
-
if (!fullPath.startsWith(bundleDir)) {
|
|
2586
|
-
return reply.code(400).send({ error: "Invalid path" });
|
|
2587
|
-
}
|
|
2588
|
-
if (!existsSync2(fullPath) || !statSync(fullPath).isFile()) {
|
|
2589
|
-
return reply.code(404).send({ error: `Widget file not found: ${pluginId}/widgets/${filePath}` });
|
|
2590
|
-
}
|
|
2591
|
-
const MIME = {
|
|
2592
|
-
".js": "application/javascript",
|
|
2593
|
-
".mjs": "application/javascript",
|
|
2594
|
-
".css": "text/css",
|
|
2595
|
-
".json": "application/json",
|
|
2596
|
-
".map": "application/json"
|
|
2597
|
-
};
|
|
2598
|
-
const ext = extname(filePath);
|
|
2599
|
-
const isEntry = filePath === "remoteEntry.js";
|
|
2600
|
-
return reply.code(200).header("Content-Type", MIME[ext] ?? "application/octet-stream").header("Content-Length", statSync(fullPath).size).header("Cache-Control", isEntry ? "public, max-age=10, must-revalidate" : process.env.NODE_ENV === "production" ? "public, max-age=31536000, immutable" : "no-cache").send(createReadStream(fullPath));
|
|
2601
|
-
});
|
|
2669
|
+
);
|
|
2602
2670
|
server.get(`${basePath}/plugins/health`, async (_request, reply) => {
|
|
2603
2671
|
try {
|
|
2604
2672
|
const snapshot = await restDomainOperationMetrics.observeOperation(
|
|
@@ -2617,7 +2685,9 @@ async function registerPluginSnapshotRoutes(server, config, registry) {
|
|
|
2617
2685
|
}
|
|
2618
2686
|
const registryErrors = snapshot.errors ?? [];
|
|
2619
2687
|
const discoveryDiagnostics = snapshot.diagnostics ?? [];
|
|
2620
|
-
const discoveryErrorCount = discoveryDiagnostics.filter(
|
|
2688
|
+
const discoveryErrorCount = discoveryDiagnostics.filter(
|
|
2689
|
+
(diagnostic) => diagnostic.severity === "error"
|
|
2690
|
+
).length;
|
|
2621
2691
|
const partialReasons = [];
|
|
2622
2692
|
if (snapshot.corrupted) {
|
|
2623
2693
|
partialReasons.push("snapshot_corrupted");
|
|
@@ -2658,7 +2728,9 @@ async function registerPluginSnapshotRoutes(server, config, registry) {
|
|
|
2658
2728
|
diagnostics: {
|
|
2659
2729
|
total: discoveryDiagnostics.length,
|
|
2660
2730
|
errors: discoveryErrorCount,
|
|
2661
|
-
warnings: discoveryDiagnostics.filter(
|
|
2731
|
+
warnings: discoveryDiagnostics.filter(
|
|
2732
|
+
(diagnostic) => diagnostic.severity === "warning"
|
|
2733
|
+
).length,
|
|
2662
2734
|
items: discoveryDiagnostics.map((diagnostic) => ({
|
|
2663
2735
|
severity: diagnostic.severity,
|
|
2664
2736
|
code: diagnostic.code,
|
|
@@ -2677,7 +2749,10 @@ async function registerPluginSnapshotRoutes(server, config, registry) {
|
|
|
2677
2749
|
message: registryErrors.length > 0 ? `Registry is partial - ${registryErrors.length} plugin(s) failed to load. Check registryErrors.items for details.` : discoveryErrorCount > 0 ? `Registry has discovery diagnostics - ${discoveryErrorCount} blocking issue(s). Check diagnostics.items for details.` : snapshot.partial && partialReasons.includes("initialization_incomplete") ? "Registry is partial - initialization incomplete. Try refreshing or wait for the snapshot rebuild to complete." : snapshot.partial ? `Registry is partial - reasons: ${partialReasons.join(", ")}` : snapshot.stale ? "Registry is stale - plugin snapshot may be outdated." : validationIssues.length > 0 ? `${validationIssues.length} plugin(s) have validation errors.` : "All plugins are healthy."
|
|
2678
2750
|
};
|
|
2679
2751
|
} catch (error) {
|
|
2680
|
-
platform.logger.error(
|
|
2752
|
+
platform.logger.error(
|
|
2753
|
+
"Failed to get plugin health",
|
|
2754
|
+
error instanceof Error ? error : new Error(String(error))
|
|
2755
|
+
);
|
|
2681
2756
|
reply.code(500).send({
|
|
2682
2757
|
error: "Failed to get plugin health",
|
|
2683
2758
|
message: error instanceof Error ? error.message : String(error)
|
|
@@ -2699,7 +2774,9 @@ async function registerPluginSnapshotRoutes(server, config, registry) {
|
|
|
2699
2774
|
"plugin.registry.snapshot",
|
|
2700
2775
|
async () => registry.snapshot()
|
|
2701
2776
|
);
|
|
2702
|
-
const pluginEntry = snapshot.manifests.find(
|
|
2777
|
+
const pluginEntry = snapshot.manifests.find(
|
|
2778
|
+
(entry) => entry.pluginId === pluginId
|
|
2779
|
+
);
|
|
2703
2780
|
if (!pluginEntry) {
|
|
2704
2781
|
reply.code(404).send({
|
|
2705
2782
|
error: "Plugin not found",
|
|
@@ -2730,7 +2807,10 @@ Please answer the question based on the plugin manifest above.`;
|
|
|
2730
2807
|
usage: response.usage
|
|
2731
2808
|
};
|
|
2732
2809
|
} catch (error) {
|
|
2733
|
-
platform.logger.error(
|
|
2810
|
+
platform.logger.error(
|
|
2811
|
+
"Failed to get AI answer about plugin",
|
|
2812
|
+
error instanceof Error ? error : new Error(String(error))
|
|
2813
|
+
);
|
|
2734
2814
|
reply.code(500).send({
|
|
2735
2815
|
error: "Failed to get AI answer",
|
|
2736
2816
|
message: error instanceof Error ? error.message : String(error)
|
|
@@ -3292,15 +3372,15 @@ async function registerCacheRoutes(server, config, registry) {
|
|
|
3292
3372
|
}
|
|
3293
3373
|
});
|
|
3294
3374
|
}
|
|
3295
|
-
async function registerObservabilityRoutes(fastify, config, repoRoot, historicalMetrics,
|
|
3375
|
+
async function registerObservabilityRoutes(fastify, config, repoRoot, historicalMetrics, platform16) {
|
|
3296
3376
|
const basePath = normalizeBasePath(config.basePath);
|
|
3297
3377
|
const stateBrokerPaths = resolvePaths(basePath, "/observability/state-broker");
|
|
3298
3378
|
const systemMetricsPaths = resolvePaths(basePath, "/observability/system-metrics");
|
|
3299
3379
|
const metricsHistoryPaths = resolvePaths(basePath, "/observability/metrics/history");
|
|
3300
3380
|
const metricsHeatmapPaths = resolvePaths(basePath, "/observability/metrics/heatmap");
|
|
3301
3381
|
const insightsChatPaths = resolvePaths(basePath, "/observability/insights/chat");
|
|
3302
|
-
for (const
|
|
3303
|
-
fastify.get(
|
|
3382
|
+
for (const path2 of stateBrokerPaths) {
|
|
3383
|
+
fastify.get(path2, { schema: { tags: ["Observability"], summary: "State Broker statistics" } }, async (_request, reply) => {
|
|
3304
3384
|
try {
|
|
3305
3385
|
const stateBrokerUrl = process.env.KB_STATE_DAEMON_URL || "http://localhost:7777";
|
|
3306
3386
|
fastify.log.debug({ url: stateBrokerUrl }, "Fetching State Broker stats");
|
|
@@ -3339,7 +3419,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3339
3419
|
}
|
|
3340
3420
|
};
|
|
3341
3421
|
} catch (error) {
|
|
3342
|
-
|
|
3422
|
+
platform16?.logger.error("Failed to fetch State Broker stats", error instanceof Error ? error : new Error(String(error)));
|
|
3343
3423
|
const isTimeout = error instanceof Error && error.name === "AbortError";
|
|
3344
3424
|
return reply.code(503).send({
|
|
3345
3425
|
ok: false,
|
|
@@ -3354,10 +3434,10 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3354
3434
|
}
|
|
3355
3435
|
});
|
|
3356
3436
|
}
|
|
3357
|
-
for (const
|
|
3358
|
-
fastify.get(
|
|
3437
|
+
for (const path2 of systemMetricsPaths) {
|
|
3438
|
+
fastify.get(path2, { schema: { tags: ["Observability"], summary: "System resource metrics" } }, async (_request, reply) => {
|
|
3359
3439
|
try {
|
|
3360
|
-
if (!
|
|
3440
|
+
if (!platform16?.cache) {
|
|
3361
3441
|
return reply.code(503).send({
|
|
3362
3442
|
ok: false,
|
|
3363
3443
|
error: {
|
|
@@ -3369,18 +3449,18 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3369
3449
|
fastify.log.debug("Fetching system metrics from all instances");
|
|
3370
3450
|
const allMetrics = [];
|
|
3371
3451
|
try {
|
|
3372
|
-
const cacheWithScan =
|
|
3373
|
-
if ("scan" in
|
|
3452
|
+
const cacheWithScan = platform16.cache;
|
|
3453
|
+
if ("scan" in platform16.cache && typeof cacheWithScan.scan === "function") {
|
|
3374
3454
|
const keys = await cacheWithScan.scan("system-metrics:*");
|
|
3375
3455
|
for (const key of keys) {
|
|
3376
|
-
const metrics = await
|
|
3456
|
+
const metrics = await platform16.cache.get(key);
|
|
3377
3457
|
if (metrics) {
|
|
3378
3458
|
allMetrics.push(metrics);
|
|
3379
3459
|
}
|
|
3380
3460
|
}
|
|
3381
3461
|
} else {
|
|
3382
3462
|
const currentInstanceId = hostname();
|
|
3383
|
-
const metrics = await
|
|
3463
|
+
const metrics = await platform16.cache.get(`system-metrics:${currentInstanceId}`);
|
|
3384
3464
|
if (metrics) {
|
|
3385
3465
|
allMetrics.push(metrics);
|
|
3386
3466
|
}
|
|
@@ -3389,7 +3469,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3389
3469
|
} catch (scanError) {
|
|
3390
3470
|
fastify.log.warn({ err: scanError }, "Failed to scan platform.cache for system metrics");
|
|
3391
3471
|
const currentInstanceId = hostname();
|
|
3392
|
-
const metrics = await
|
|
3472
|
+
const metrics = await platform16.cache.get(`system-metrics:${currentInstanceId}`);
|
|
3393
3473
|
if (metrics) {
|
|
3394
3474
|
allMetrics.push(metrics);
|
|
3395
3475
|
}
|
|
@@ -3437,7 +3517,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3437
3517
|
}
|
|
3438
3518
|
};
|
|
3439
3519
|
} catch (error) {
|
|
3440
|
-
|
|
3520
|
+
platform16?.logger.error("Failed to fetch system metrics", error instanceof Error ? error : new Error(String(error)));
|
|
3441
3521
|
return reply.code(500).send({
|
|
3442
3522
|
ok: false,
|
|
3443
3523
|
error: {
|
|
@@ -3448,8 +3528,8 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3448
3528
|
}
|
|
3449
3529
|
});
|
|
3450
3530
|
}
|
|
3451
|
-
for (const
|
|
3452
|
-
fastify.get(
|
|
3531
|
+
for (const path2 of metricsHistoryPaths) {
|
|
3532
|
+
fastify.get(path2, {
|
|
3453
3533
|
schema: {
|
|
3454
3534
|
tags: ["Observability"],
|
|
3455
3535
|
summary: "Historical time-series metrics",
|
|
@@ -3535,7 +3615,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3535
3615
|
}
|
|
3536
3616
|
};
|
|
3537
3617
|
} catch (error) {
|
|
3538
|
-
|
|
3618
|
+
platform16?.logger.error("Failed to query historical metrics", error instanceof Error ? error : new Error(String(error)), { query });
|
|
3539
3619
|
return reply.code(500).send({
|
|
3540
3620
|
ok: false,
|
|
3541
3621
|
error: {
|
|
@@ -3546,8 +3626,8 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3546
3626
|
}
|
|
3547
3627
|
});
|
|
3548
3628
|
}
|
|
3549
|
-
for (const
|
|
3550
|
-
fastify.get(
|
|
3629
|
+
for (const path2 of metricsHeatmapPaths) {
|
|
3630
|
+
fastify.get(path2, {
|
|
3551
3631
|
schema: {
|
|
3552
3632
|
tags: ["Observability"],
|
|
3553
3633
|
summary: "Metrics heatmap data",
|
|
@@ -3626,7 +3706,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3626
3706
|
}
|
|
3627
3707
|
};
|
|
3628
3708
|
} catch (error) {
|
|
3629
|
-
|
|
3709
|
+
platform16?.logger.error("Failed to query heatmap data", error instanceof Error ? error : new Error(String(error)), { query });
|
|
3630
3710
|
return reply.code(500).send({
|
|
3631
3711
|
ok: false,
|
|
3632
3712
|
error: {
|
|
@@ -3637,8 +3717,8 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3637
3717
|
}
|
|
3638
3718
|
});
|
|
3639
3719
|
}
|
|
3640
|
-
for (const
|
|
3641
|
-
fastify.post(
|
|
3720
|
+
for (const path2 of insightsChatPaths) {
|
|
3721
|
+
fastify.post(path2, {
|
|
3642
3722
|
schema: {
|
|
3643
3723
|
tags: ["Observability"],
|
|
3644
3724
|
summary: "AI-powered observability insights chat",
|
|
@@ -3660,7 +3740,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
|
|
|
3660
3740
|
}
|
|
3661
3741
|
}
|
|
3662
3742
|
}, async (request, reply) => {
|
|
3663
|
-
if (!
|
|
3743
|
+
if (!platform16?.llm) {
|
|
3664
3744
|
return reply.code(503).send({
|
|
3665
3745
|
ok: false,
|
|
3666
3746
|
error: {
|
|
@@ -3726,15 +3806,15 @@ Provide a clear, actionable response based on the data above. Include:
|
|
|
3726
3806
|
|
|
3727
3807
|
Be concise but thorough. Use markdown formatting.`;
|
|
3728
3808
|
fastify.log.debug({ question: body.question, contextLength: contextText.length }, "Calling LLM for insights");
|
|
3729
|
-
const result = await
|
|
3809
|
+
const result = await platform16.llm.complete(prompt, {
|
|
3730
3810
|
systemPrompt: "You are a DevOps and SRE expert assistant. Analyze system metrics and provide actionable insights. Be concise, technical, and helpful.",
|
|
3731
3811
|
temperature: 0.7,
|
|
3732
3812
|
maxTokens: 1e3
|
|
3733
3813
|
});
|
|
3734
3814
|
const totalTokens = result.usage.promptTokens + result.usage.completionTokens;
|
|
3735
3815
|
fastify.log.debug({ tokensUsed: totalTokens }, "LLM response received for insights");
|
|
3736
|
-
if (
|
|
3737
|
-
|
|
3816
|
+
if (platform16.analytics) {
|
|
3817
|
+
platform16.analytics.track("ai_insights.chat", {
|
|
3738
3818
|
questionLength: body.question.length,
|
|
3739
3819
|
contextIncluded: Object.keys(contextConfig).filter((k) => contextConfig[k]),
|
|
3740
3820
|
timeRange: contextConfig.timeRange,
|
|
@@ -3763,9 +3843,9 @@ Be concise but thorough. Use markdown formatting.`;
|
|
|
3763
3843
|
}
|
|
3764
3844
|
};
|
|
3765
3845
|
} catch (error) {
|
|
3766
|
-
|
|
3767
|
-
if (
|
|
3768
|
-
|
|
3846
|
+
platform16?.logger.error("Failed to generate insights", error instanceof Error ? error : new Error(String(error)));
|
|
3847
|
+
if (platform16?.analytics) {
|
|
3848
|
+
platform16.analytics.track("ai_insights.error", {
|
|
3769
3849
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
3770
3850
|
questionLength: request.body?.question?.length ?? 0
|
|
3771
3851
|
}).catch(() => {
|
|
@@ -3786,8 +3866,8 @@ Be concise but thorough. Use markdown formatting.`;
|
|
|
3786
3866
|
async function registerAnalyticsRoutes(fastify, config) {
|
|
3787
3867
|
const basePath = normalizeBasePath(config.basePath);
|
|
3788
3868
|
const eventsPaths = resolvePaths(basePath, "/analytics/events");
|
|
3789
|
-
for (const
|
|
3790
|
-
fastify.get(
|
|
3869
|
+
for (const path2 of eventsPaths) {
|
|
3870
|
+
fastify.get(path2, async (request, reply) => {
|
|
3791
3871
|
try {
|
|
3792
3872
|
const analytics = platform.analytics;
|
|
3793
3873
|
const getEvents = analytics.getEvents;
|
|
@@ -3856,8 +3936,8 @@ async function registerAnalyticsRoutes(fastify, config) {
|
|
|
3856
3936
|
});
|
|
3857
3937
|
}
|
|
3858
3938
|
const statsPaths = resolvePaths(basePath, "/analytics/stats");
|
|
3859
|
-
for (const
|
|
3860
|
-
fastify.get(
|
|
3939
|
+
for (const path2 of statsPaths) {
|
|
3940
|
+
fastify.get(path2, async (_request, reply) => {
|
|
3861
3941
|
try {
|
|
3862
3942
|
const analytics = platform.analytics;
|
|
3863
3943
|
const getStats = analytics.getStats;
|
|
@@ -3908,8 +3988,8 @@ async function registerAnalyticsRoutes(fastify, config) {
|
|
|
3908
3988
|
});
|
|
3909
3989
|
}
|
|
3910
3990
|
const bufferPaths = resolvePaths(basePath, "/analytics/buffer/status");
|
|
3911
|
-
for (const
|
|
3912
|
-
fastify.get(
|
|
3991
|
+
for (const path2 of bufferPaths) {
|
|
3992
|
+
fastify.get(path2, async (_request, reply) => {
|
|
3913
3993
|
try {
|
|
3914
3994
|
const analytics = platform.analytics;
|
|
3915
3995
|
const getBufferStatus = analytics.getBufferStatus;
|
|
@@ -3965,8 +4045,8 @@ async function registerAnalyticsRoutes(fastify, config) {
|
|
|
3965
4045
|
});
|
|
3966
4046
|
}
|
|
3967
4047
|
const dlqPaths = resolvePaths(basePath, "/analytics/dlq/status");
|
|
3968
|
-
for (const
|
|
3969
|
-
fastify.get(
|
|
4048
|
+
for (const path2 of dlqPaths) {
|
|
4049
|
+
fastify.get(path2, async (_request, reply) => {
|
|
3970
4050
|
try {
|
|
3971
4051
|
const analytics = platform.analytics;
|
|
3972
4052
|
const getDlqStatus = analytics.getDlqStatus;
|
|
@@ -4107,8 +4187,8 @@ function isValidISODate(dateString) {
|
|
|
4107
4187
|
async function registerAdaptersRoutes(fastify, config) {
|
|
4108
4188
|
const basePath = normalizeBasePath(config.basePath);
|
|
4109
4189
|
const llmUsagePaths = resolvePaths(basePath, "/adapters/llm/usage");
|
|
4110
|
-
for (const
|
|
4111
|
-
fastify.get(
|
|
4190
|
+
for (const path2 of llmUsagePaths) {
|
|
4191
|
+
fastify.get(path2, async (request, reply) => {
|
|
4112
4192
|
const analytics = platform.analytics;
|
|
4113
4193
|
if (!analytics.getEvents) {
|
|
4114
4194
|
return reply.code(501).send({
|
|
@@ -4291,8 +4371,8 @@ async function registerAdaptersRoutes(fastify, config) {
|
|
|
4291
4371
|
});
|
|
4292
4372
|
}
|
|
4293
4373
|
const llmDailyStatsPaths = resolvePaths(basePath, "/adapters/llm/daily-stats");
|
|
4294
|
-
for (const
|
|
4295
|
-
fastify.get(
|
|
4374
|
+
for (const path2 of llmDailyStatsPaths) {
|
|
4375
|
+
fastify.get(path2, async (request, reply) => {
|
|
4296
4376
|
const analytics = platform.analytics;
|
|
4297
4377
|
if (!analytics.getDailyStats) {
|
|
4298
4378
|
return reply.code(501).send({
|
|
@@ -4337,8 +4417,8 @@ async function registerAdaptersRoutes(fastify, config) {
|
|
|
4337
4417
|
});
|
|
4338
4418
|
}
|
|
4339
4419
|
const embeddingsDailyStatsPaths = resolvePaths(basePath, "/adapters/embeddings/daily-stats");
|
|
4340
|
-
for (const
|
|
4341
|
-
fastify.get(
|
|
4420
|
+
for (const path2 of embeddingsDailyStatsPaths) {
|
|
4421
|
+
fastify.get(path2, async (request, reply) => {
|
|
4342
4422
|
const analytics = platform.analytics;
|
|
4343
4423
|
if (!analytics.getDailyStats) {
|
|
4344
4424
|
return reply.code(501).send({
|
|
@@ -4378,8 +4458,8 @@ async function registerAdaptersRoutes(fastify, config) {
|
|
|
4378
4458
|
});
|
|
4379
4459
|
}
|
|
4380
4460
|
const embeddingsUsagePaths = resolvePaths(basePath, "/adapters/embeddings/usage");
|
|
4381
|
-
for (const
|
|
4382
|
-
fastify.get(
|
|
4461
|
+
for (const path2 of embeddingsUsagePaths) {
|
|
4462
|
+
fastify.get(path2, async (request, reply) => {
|
|
4383
4463
|
const analytics = platform.analytics;
|
|
4384
4464
|
if (!analytics.getEvents) {
|
|
4385
4465
|
return reply.code(501).send({
|
|
@@ -4507,8 +4587,8 @@ async function registerAdaptersRoutes(fastify, config) {
|
|
|
4507
4587
|
});
|
|
4508
4588
|
}
|
|
4509
4589
|
const vectorstoreDailyStatsPaths = resolvePaths(basePath, "/adapters/vectorstore/daily-stats");
|
|
4510
|
-
for (const
|
|
4511
|
-
fastify.get(
|
|
4590
|
+
for (const path2 of vectorstoreDailyStatsPaths) {
|
|
4591
|
+
fastify.get(path2, async (request, reply) => {
|
|
4512
4592
|
const analytics = platform.analytics;
|
|
4513
4593
|
if (!analytics.getDailyStats) {
|
|
4514
4594
|
return reply.code(501).send({
|
|
@@ -4548,8 +4628,8 @@ async function registerAdaptersRoutes(fastify, config) {
|
|
|
4548
4628
|
});
|
|
4549
4629
|
}
|
|
4550
4630
|
const vectorstoreUsagePaths = resolvePaths(basePath, "/adapters/vectorstore/usage");
|
|
4551
|
-
for (const
|
|
4552
|
-
fastify.get(
|
|
4631
|
+
for (const path2 of vectorstoreUsagePaths) {
|
|
4632
|
+
fastify.get(path2, async (request, reply) => {
|
|
4553
4633
|
const analytics = platform.analytics;
|
|
4554
4634
|
if (!analytics.getEvents) {
|
|
4555
4635
|
return reply.code(501).send({
|
|
@@ -4660,8 +4740,8 @@ async function registerAdaptersRoutes(fastify, config) {
|
|
|
4660
4740
|
});
|
|
4661
4741
|
}
|
|
4662
4742
|
const cacheDailyStatsPaths = resolvePaths(basePath, "/adapters/cache/daily-stats");
|
|
4663
|
-
for (const
|
|
4664
|
-
fastify.get(
|
|
4743
|
+
for (const path2 of cacheDailyStatsPaths) {
|
|
4744
|
+
fastify.get(path2, async (request, reply) => {
|
|
4665
4745
|
const analytics = platform.analytics;
|
|
4666
4746
|
if (!analytics.getDailyStats) {
|
|
4667
4747
|
return reply.code(501).send({
|
|
@@ -4701,8 +4781,8 @@ async function registerAdaptersRoutes(fastify, config) {
|
|
|
4701
4781
|
});
|
|
4702
4782
|
}
|
|
4703
4783
|
const cacheUsagePaths = resolvePaths(basePath, "/adapters/cache/usage");
|
|
4704
|
-
for (const
|
|
4705
|
-
fastify.get(
|
|
4784
|
+
for (const path2 of cacheUsagePaths) {
|
|
4785
|
+
fastify.get(path2, async (request, reply) => {
|
|
4706
4786
|
const analytics = platform.analytics;
|
|
4707
4787
|
if (!analytics.getEvents) {
|
|
4708
4788
|
return reply.code(501).send({
|
|
@@ -4788,8 +4868,8 @@ async function registerAdaptersRoutes(fastify, config) {
|
|
|
4788
4868
|
});
|
|
4789
4869
|
}
|
|
4790
4870
|
const storageDailyStatsPaths = resolvePaths(basePath, "/adapters/storage/daily-stats");
|
|
4791
|
-
for (const
|
|
4792
|
-
fastify.get(
|
|
4871
|
+
for (const path2 of storageDailyStatsPaths) {
|
|
4872
|
+
fastify.get(path2, async (request, reply) => {
|
|
4793
4873
|
const analytics = platform.analytics;
|
|
4794
4874
|
if (!analytics.getDailyStats) {
|
|
4795
4875
|
return reply.code(501).send({
|
|
@@ -4829,8 +4909,8 @@ async function registerAdaptersRoutes(fastify, config) {
|
|
|
4829
4909
|
});
|
|
4830
4910
|
}
|
|
4831
4911
|
const storageUsagePaths = resolvePaths(basePath, "/adapters/storage/usage");
|
|
4832
|
-
for (const
|
|
4833
|
-
fastify.get(
|
|
4912
|
+
for (const path2 of storageUsagePaths) {
|
|
4913
|
+
fastify.get(path2, async (request, reply) => {
|
|
4834
4914
|
const analytics = platform.analytics;
|
|
4835
4915
|
if (!analytics.getEvents) {
|
|
4836
4916
|
return reply.code(501).send({
|
|
@@ -5037,7 +5117,7 @@ Keep the summary under 300 words. Use simple paragraphs separated by double newl
|
|
|
5037
5117
|
}
|
|
5038
5118
|
function extractCorrelationKeys(log) {
|
|
5039
5119
|
return {
|
|
5040
|
-
requestId: log.fields.requestId
|
|
5120
|
+
requestId: log.fields.requestId,
|
|
5041
5121
|
traceId: log.fields.traceId,
|
|
5042
5122
|
executionId: log.fields.executionId,
|
|
5043
5123
|
sessionId: log.fields.sessionId
|
|
@@ -5048,12 +5128,15 @@ async function findRelatedLogs(targetLog) {
|
|
|
5048
5128
|
const timeWindow = 6e4;
|
|
5049
5129
|
if (correlationKeys.requestId || correlationKeys.traceId || correlationKeys.executionId) {
|
|
5050
5130
|
const relatedLogs = [];
|
|
5051
|
-
const result2 = await platform.logs.query(
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5131
|
+
const result2 = await platform.logs.query(
|
|
5132
|
+
{
|
|
5133
|
+
from: targetLog.timestamp - timeWindow,
|
|
5134
|
+
to: targetLog.timestamp + timeWindow
|
|
5135
|
+
},
|
|
5136
|
+
{
|
|
5137
|
+
limit: 1e3
|
|
5138
|
+
}
|
|
5139
|
+
);
|
|
5057
5140
|
for (const log of result2.logs) {
|
|
5058
5141
|
if (log.id === targetLog.id) {
|
|
5059
5142
|
continue;
|
|
@@ -5067,13 +5150,16 @@ async function findRelatedLogs(targetLog) {
|
|
|
5067
5150
|
return relatedLogs.sort((a, b) => a.timestamp - b.timestamp).map(toFrontendLogRecord);
|
|
5068
5151
|
}
|
|
5069
5152
|
}
|
|
5070
|
-
const result = await platform.logs.query(
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5153
|
+
const result = await platform.logs.query(
|
|
5154
|
+
{
|
|
5155
|
+
source: targetLog.source,
|
|
5156
|
+
from: targetLog.timestamp - timeWindow,
|
|
5157
|
+
to: targetLog.timestamp + timeWindow
|
|
5158
|
+
},
|
|
5159
|
+
{
|
|
5160
|
+
limit: 50
|
|
5161
|
+
}
|
|
5162
|
+
);
|
|
5077
5163
|
return result.logs.filter((log) => log.id !== targetLog.id).sort((a, b) => a.timestamp - b.timestamp).map(toFrontendLogRecord);
|
|
5078
5164
|
}
|
|
5079
5165
|
function generateFallbackSummary(stats, logs) {
|
|
@@ -5094,7 +5180,9 @@ function generateFallbackSummary(stats, logs) {
|
|
|
5094
5180
|
if (Object.keys(stats.byPlugin).length > 0) {
|
|
5095
5181
|
summary += `By Plugin:
|
|
5096
5182
|
`;
|
|
5097
|
-
Object.entries(stats.byPlugin).sort(
|
|
5183
|
+
Object.entries(stats.byPlugin).sort(
|
|
5184
|
+
(a, b) => b[1] - a[1]
|
|
5185
|
+
).slice(0, 5).forEach(([plugin, count]) => {
|
|
5098
5186
|
summary += `${plugin}: ${count}
|
|
5099
5187
|
`;
|
|
5100
5188
|
});
|
|
@@ -5120,62 +5208,70 @@ function generateFallbackSummary(stats, logs) {
|
|
|
5120
5208
|
return summary;
|
|
5121
5209
|
}
|
|
5122
5210
|
async function registerLogRoutes(server, config, eventHub) {
|
|
5123
|
-
server.get(
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5211
|
+
server.get(
|
|
5212
|
+
"/api/v1/logs",
|
|
5213
|
+
{ schema: { tags: ["Logs"], summary: "Query logs with filters" } },
|
|
5214
|
+
async (request, reply) => {
|
|
5215
|
+
try {
|
|
5216
|
+
const limit = request.query.limit ? parseInt(request.query.limit, 10) : 100;
|
|
5217
|
+
const offset = request.query.offset ? parseInt(request.query.offset, 10) : 0;
|
|
5218
|
+
const query = {
|
|
5219
|
+
level: request.query.level,
|
|
5220
|
+
source: request.query.plugin,
|
|
5221
|
+
from: request.query.from ? new Date(request.query.from).getTime() : void 0,
|
|
5222
|
+
to: request.query.to ? new Date(request.query.to).getTime() : void 0
|
|
5223
|
+
};
|
|
5224
|
+
let result;
|
|
5225
|
+
if (request.query.search) {
|
|
5226
|
+
result = await platform.logs.search(request.query.search, {
|
|
5227
|
+
limit,
|
|
5228
|
+
offset
|
|
5229
|
+
});
|
|
5230
|
+
} else {
|
|
5231
|
+
result = await platform.logs.query(query, {
|
|
5232
|
+
limit,
|
|
5233
|
+
offset
|
|
5234
|
+
});
|
|
5235
|
+
}
|
|
5236
|
+
const frontendLogs = result.logs.map(toFrontendLogRecord);
|
|
5237
|
+
const stats = await platform.logs.getStats();
|
|
5238
|
+
return {
|
|
5239
|
+
ok: true,
|
|
5240
|
+
data: {
|
|
5241
|
+
logs: frontendLogs,
|
|
5242
|
+
total: result.total,
|
|
5243
|
+
hasMore: result.hasMore,
|
|
5244
|
+
filters: request.query,
|
|
5245
|
+
source: "source" in result ? result.source : void 0,
|
|
5246
|
+
stats: {
|
|
5247
|
+
buffer: stats.buffer ? {
|
|
5248
|
+
size: stats.buffer.size,
|
|
5249
|
+
maxSize: stats.buffer.maxSize,
|
|
5250
|
+
oldest: stats.buffer.oldestTimestamp ? new Date(stats.buffer.oldestTimestamp).toISOString() : void 0,
|
|
5251
|
+
newest: stats.buffer.newestTimestamp ? new Date(stats.buffer.newestTimestamp).toISOString() : void 0
|
|
5252
|
+
} : void 0,
|
|
5253
|
+
persistence: stats.persistence ? {
|
|
5254
|
+
totalLogs: stats.persistence.totalLogs,
|
|
5255
|
+
oldestTimestamp: stats.persistence.oldestTimestamp ? new Date(
|
|
5256
|
+
stats.persistence.oldestTimestamp
|
|
5257
|
+
).toISOString() : void 0,
|
|
5258
|
+
newestTimestamp: stats.persistence.newestTimestamp ? new Date(
|
|
5259
|
+
stats.persistence.newestTimestamp
|
|
5260
|
+
).toISOString() : void 0,
|
|
5261
|
+
sizeBytes: stats.persistence.sizeBytes
|
|
5262
|
+
} : void 0
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
};
|
|
5266
|
+
} catch (error) {
|
|
5267
|
+
return reply.code(503).send({
|
|
5268
|
+
ok: false,
|
|
5269
|
+
error: "Log query failed",
|
|
5270
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
5143
5271
|
});
|
|
5144
5272
|
}
|
|
5145
|
-
const frontendLogs = result.logs.map(toFrontendLogRecord);
|
|
5146
|
-
const stats = await platform.logs.getStats();
|
|
5147
|
-
return {
|
|
5148
|
-
ok: true,
|
|
5149
|
-
data: {
|
|
5150
|
-
logs: frontendLogs,
|
|
5151
|
-
total: result.total,
|
|
5152
|
-
hasMore: result.hasMore,
|
|
5153
|
-
filters: request.query,
|
|
5154
|
-
source: "source" in result ? result.source : void 0,
|
|
5155
|
-
stats: {
|
|
5156
|
-
buffer: stats.buffer ? {
|
|
5157
|
-
size: stats.buffer.size,
|
|
5158
|
-
maxSize: stats.buffer.maxSize,
|
|
5159
|
-
oldest: stats.buffer.oldestTimestamp ? new Date(stats.buffer.oldestTimestamp).toISOString() : void 0,
|
|
5160
|
-
newest: stats.buffer.newestTimestamp ? new Date(stats.buffer.newestTimestamp).toISOString() : void 0
|
|
5161
|
-
} : void 0,
|
|
5162
|
-
persistence: stats.persistence ? {
|
|
5163
|
-
totalLogs: stats.persistence.totalLogs,
|
|
5164
|
-
oldestTimestamp: stats.persistence.oldestTimestamp ? new Date(stats.persistence.oldestTimestamp).toISOString() : void 0,
|
|
5165
|
-
newestTimestamp: stats.persistence.newestTimestamp ? new Date(stats.persistence.newestTimestamp).toISOString() : void 0,
|
|
5166
|
-
sizeBytes: stats.persistence.sizeBytes
|
|
5167
|
-
} : void 0
|
|
5168
|
-
}
|
|
5169
|
-
}
|
|
5170
|
-
};
|
|
5171
|
-
} catch (error) {
|
|
5172
|
-
return reply.code(503).send({
|
|
5173
|
-
ok: false,
|
|
5174
|
-
error: "Log query failed",
|
|
5175
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
5176
|
-
});
|
|
5177
5273
|
}
|
|
5178
|
-
|
|
5274
|
+
);
|
|
5179
5275
|
server.get(
|
|
5180
5276
|
"/api/v1/logs/:id",
|
|
5181
5277
|
{ schema: { tags: ["Logs"], summary: "Get log entry by ID" } },
|
|
@@ -5212,7 +5308,12 @@ async function registerLogRoutes(server, config, eventHub) {
|
|
|
5212
5308
|
);
|
|
5213
5309
|
server.get(
|
|
5214
5310
|
"/api/v1/logs/:id/related",
|
|
5215
|
-
{
|
|
5311
|
+
{
|
|
5312
|
+
schema: {
|
|
5313
|
+
tags: ["Logs"],
|
|
5314
|
+
summary: "Get logs related to a specific entry"
|
|
5315
|
+
}
|
|
5316
|
+
},
|
|
5216
5317
|
async (request, reply) => {
|
|
5217
5318
|
try {
|
|
5218
5319
|
const log = await platform.logs.getById(request.params.id);
|
|
@@ -5241,202 +5342,223 @@ async function registerLogRoutes(server, config, eventHub) {
|
|
|
5241
5342
|
}
|
|
5242
5343
|
}
|
|
5243
5344
|
);
|
|
5244
|
-
server.get(
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
reply.raw.
|
|
5266
|
-
reply.raw.
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5345
|
+
server.get(
|
|
5346
|
+
"/api/v1/logs/stream",
|
|
5347
|
+
{ schema: { hide: true } },
|
|
5348
|
+
async (request, reply) => {
|
|
5349
|
+
const caps = platform.logs.getCapabilities();
|
|
5350
|
+
if (!caps.hasStreaming) {
|
|
5351
|
+
return reply.code(503).send({
|
|
5352
|
+
error: "Log streaming not enabled",
|
|
5353
|
+
message: "Logger adapter does not support streaming. Enable logRingBuffer in kb.config.json"
|
|
5354
|
+
});
|
|
5355
|
+
}
|
|
5356
|
+
reply.hijack();
|
|
5357
|
+
let streamClosed = false;
|
|
5358
|
+
const origin = request.headers.origin;
|
|
5359
|
+
if (origin === "http://localhost:3000" || origin === "http://localhost:5173") {
|
|
5360
|
+
reply.raw.setHeader("Access-Control-Allow-Origin", origin);
|
|
5361
|
+
reply.raw.setHeader("Access-Control-Allow-Credentials", "true");
|
|
5362
|
+
} else {
|
|
5363
|
+
reply.raw.setHeader("Access-Control-Allow-Origin", "*");
|
|
5364
|
+
}
|
|
5365
|
+
reply.raw.setHeader("Content-Type", "text/event-stream");
|
|
5366
|
+
reply.raw.setHeader("Cache-Control", "no-cache, no-transform");
|
|
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
|
|
5276
5380
|
`);
|
|
5277
|
-
|
|
5381
|
+
reply.raw.write(`data: ${JSON.stringify(frontendLog)}
|
|
5278
5382
|
|
|
5279
5383
|
`);
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
}
|
|
5283
|
-
}
|
|
5284
|
-
});
|
|
5285
|
-
const cleanup = () => {
|
|
5286
|
-
if (!streamClosed) {
|
|
5287
|
-
streamClosed = true;
|
|
5288
|
-
unsubscribe();
|
|
5289
|
-
try {
|
|
5290
|
-
if (!reply.raw.writableEnded && !reply.raw.destroyed) {
|
|
5291
|
-
reply.raw.end();
|
|
5384
|
+
} catch (err) {
|
|
5385
|
+
streamClosed = true;
|
|
5292
5386
|
}
|
|
5293
|
-
} catch (err) {
|
|
5294
5387
|
}
|
|
5295
|
-
}
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
return {
|
|
5307
|
-
ok: true,
|
|
5308
|
-
data: {
|
|
5309
|
-
capabilities: caps,
|
|
5310
|
-
buffer: stats.buffer ? {
|
|
5311
|
-
size: stats.buffer.size,
|
|
5312
|
-
maxSize: stats.buffer.maxSize,
|
|
5313
|
-
oldest: stats.buffer.oldestTimestamp ? new Date(stats.buffer.oldestTimestamp).toISOString() : void 0,
|
|
5314
|
-
newest: stats.buffer.newestTimestamp ? new Date(stats.buffer.newestTimestamp).toISOString() : void 0
|
|
5315
|
-
} : void 0,
|
|
5316
|
-
persistence: stats.persistence ? {
|
|
5317
|
-
totalLogs: stats.persistence.totalLogs,
|
|
5318
|
-
oldest: stats.persistence.oldestTimestamp ? new Date(stats.persistence.oldestTimestamp).toISOString() : void 0,
|
|
5319
|
-
newest: stats.persistence.newestTimestamp ? new Date(stats.persistence.newestTimestamp).toISOString() : void 0,
|
|
5320
|
-
sizeBytes: stats.persistence.sizeBytes
|
|
5321
|
-
} : void 0
|
|
5388
|
+
});
|
|
5389
|
+
const cleanup = () => {
|
|
5390
|
+
if (!streamClosed) {
|
|
5391
|
+
streamClosed = true;
|
|
5392
|
+
unsubscribe();
|
|
5393
|
+
try {
|
|
5394
|
+
if (!reply.raw.writableEnded && !reply.raw.destroyed) {
|
|
5395
|
+
reply.raw.end();
|
|
5396
|
+
}
|
|
5397
|
+
} catch (err) {
|
|
5398
|
+
}
|
|
5322
5399
|
}
|
|
5323
5400
|
};
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
error: "Failed to fetch stats",
|
|
5328
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
5401
|
+
request.raw.on("close", cleanup);
|
|
5402
|
+
request.raw.on("error", cleanup);
|
|
5403
|
+
await new Promise(() => {
|
|
5329
5404
|
});
|
|
5330
5405
|
}
|
|
5331
|
-
|
|
5332
|
-
server.
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5406
|
+
);
|
|
5407
|
+
server.get(
|
|
5408
|
+
"/api/v1/logs/stats",
|
|
5409
|
+
{ schema: { tags: ["Logs"], summary: "Get log storage statistics" } },
|
|
5410
|
+
async (request, reply) => {
|
|
5411
|
+
try {
|
|
5412
|
+
const stats = await platform.logs.getStats();
|
|
5413
|
+
const caps = platform.logs.getCapabilities();
|
|
5414
|
+
return {
|
|
5415
|
+
ok: true,
|
|
5416
|
+
data: {
|
|
5417
|
+
capabilities: caps,
|
|
5418
|
+
buffer: stats.buffer ? {
|
|
5419
|
+
size: stats.buffer.size,
|
|
5420
|
+
maxSize: stats.buffer.maxSize,
|
|
5421
|
+
oldest: stats.buffer.oldestTimestamp ? new Date(stats.buffer.oldestTimestamp).toISOString() : void 0,
|
|
5422
|
+
newest: stats.buffer.newestTimestamp ? new Date(stats.buffer.newestTimestamp).toISOString() : void 0
|
|
5423
|
+
} : void 0,
|
|
5424
|
+
persistence: stats.persistence ? {
|
|
5425
|
+
totalLogs: stats.persistence.totalLogs,
|
|
5426
|
+
oldest: stats.persistence.oldestTimestamp ? new Date(stats.persistence.oldestTimestamp).toISOString() : void 0,
|
|
5427
|
+
newest: stats.persistence.newestTimestamp ? new Date(stats.persistence.newestTimestamp).toISOString() : void 0,
|
|
5428
|
+
sizeBytes: stats.persistence.sizeBytes
|
|
5429
|
+
} : void 0
|
|
5430
|
+
}
|
|
5431
|
+
};
|
|
5432
|
+
} catch (error) {
|
|
5433
|
+
return reply.code(500).send({
|
|
5434
|
+
ok: false,
|
|
5435
|
+
error: "Failed to fetch stats",
|
|
5436
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
5437
|
+
});
|
|
5438
|
+
}
|
|
5439
|
+
}
|
|
5440
|
+
);
|
|
5441
|
+
server.post(
|
|
5442
|
+
"/api/v1/logs/summarize",
|
|
5443
|
+
{ schema: { tags: ["Logs"], summary: "AI-powered log summarization" } },
|
|
5444
|
+
async (request, reply) => {
|
|
5445
|
+
const { timeRange, filters, groupBy, question, includeContext } = request.body;
|
|
5446
|
+
try {
|
|
5447
|
+
const query = {
|
|
5448
|
+
level: filters?.level,
|
|
5449
|
+
source: filters?.plugin,
|
|
5450
|
+
from: timeRange?.from ? new Date(timeRange.from).getTime() : void 0,
|
|
5451
|
+
to: timeRange?.to ? new Date(timeRange.to).getTime() : void 0
|
|
5452
|
+
};
|
|
5453
|
+
const result = await platform.logs.query(query, { limit: 1e3 });
|
|
5454
|
+
const frontendLogs = result.logs.map(toFrontendLogRecord);
|
|
5455
|
+
let filteredLogs = frontendLogs;
|
|
5456
|
+
if (filters?.traceId) {
|
|
5457
|
+
filteredLogs = filteredLogs.filter(
|
|
5458
|
+
(log) => log.traceId === filters.traceId
|
|
5459
|
+
);
|
|
5358
5460
|
}
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
const fieldMap = {
|
|
5374
|
-
trace: "traceId",
|
|
5375
|
-
execution: "executionId",
|
|
5376
|
-
plugin: "plugin"
|
|
5461
|
+
if (filters?.executionId) {
|
|
5462
|
+
filteredLogs = filteredLogs.filter(
|
|
5463
|
+
(log) => log.executionId === filters.executionId
|
|
5464
|
+
);
|
|
5465
|
+
}
|
|
5466
|
+
const stats = {
|
|
5467
|
+
total: filteredLogs.length,
|
|
5468
|
+
byLevel: {},
|
|
5469
|
+
byPlugin: {},
|
|
5470
|
+
topErrors: [],
|
|
5471
|
+
timeRange: {
|
|
5472
|
+
from: filteredLogs.length > 0 ? filteredLogs[0].time : null,
|
|
5473
|
+
to: filteredLogs.length > 0 ? filteredLogs[filteredLogs.length - 1].time : null
|
|
5474
|
+
}
|
|
5377
5475
|
};
|
|
5378
|
-
const
|
|
5476
|
+
const errorMessages = /* @__PURE__ */ new Map();
|
|
5379
5477
|
for (const log of filteredLogs) {
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5478
|
+
stats.byLevel[log.level] = (stats.byLevel[log.level] || 0) + 1;
|
|
5479
|
+
if (log.plugin) {
|
|
5480
|
+
stats.byPlugin[log.plugin] = (stats.byPlugin[log.plugin] || 0) + 1;
|
|
5481
|
+
}
|
|
5482
|
+
if (log.level === "error" && log.msg) {
|
|
5483
|
+
errorMessages.set(log.msg, (errorMessages.get(log.msg) || 0) + 1);
|
|
5384
5484
|
}
|
|
5385
|
-
groupMap.get(key).push(log);
|
|
5386
5485
|
}
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
errors: true,
|
|
5395
|
-
warnings: true,
|
|
5396
|
-
info: false,
|
|
5397
|
-
metadata: true,
|
|
5398
|
-
stackTraces: true
|
|
5486
|
+
stats.topErrors = Array.from(errorMessages.entries()).map(([message2, count]) => ({ message: message2, count })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
5487
|
+
let groups = null;
|
|
5488
|
+
if (groupBy) {
|
|
5489
|
+
const fieldMap = {
|
|
5490
|
+
trace: "traceId",
|
|
5491
|
+
execution: "executionId",
|
|
5492
|
+
plugin: "plugin"
|
|
5399
5493
|
};
|
|
5400
|
-
const
|
|
5401
|
-
const
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5494
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
5495
|
+
for (const log of filteredLogs) {
|
|
5496
|
+
const fieldName = fieldMap[groupBy] || groupBy;
|
|
5497
|
+
const key = String(log[fieldName] || "unknown");
|
|
5498
|
+
if (!groupMap.has(key)) {
|
|
5499
|
+
groupMap.set(key, []);
|
|
5500
|
+
}
|
|
5501
|
+
groupMap.get(key).push(log);
|
|
5502
|
+
}
|
|
5503
|
+
groups = Object.fromEntries(groupMap.entries());
|
|
5504
|
+
}
|
|
5505
|
+
let aiSummary = null;
|
|
5506
|
+
let message = null;
|
|
5507
|
+
if (platform.llm && question) {
|
|
5508
|
+
try {
|
|
5509
|
+
const context = includeContext || {
|
|
5510
|
+
errors: true,
|
|
5511
|
+
warnings: true,
|
|
5512
|
+
info: false,
|
|
5513
|
+
metadata: true,
|
|
5514
|
+
stackTraces: true
|
|
5515
|
+
};
|
|
5516
|
+
const prompt = buildLogSummaryPrompt(
|
|
5517
|
+
question,
|
|
5518
|
+
filteredLogs,
|
|
5519
|
+
stats,
|
|
5520
|
+
context
|
|
5521
|
+
);
|
|
5522
|
+
const response = await platform.llm.complete(prompt, {
|
|
5523
|
+
temperature: 0.7,
|
|
5524
|
+
maxTokens: 1e3,
|
|
5525
|
+
systemPrompt: "You are a technical log analysis assistant. Provide clear, actionable insights based on application logs."
|
|
5526
|
+
});
|
|
5527
|
+
aiSummary = response.content.trim();
|
|
5528
|
+
} catch (error) {
|
|
5529
|
+
platform.logger.warn("LLM summarization failed, using fallback", {
|
|
5530
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5531
|
+
});
|
|
5532
|
+
aiSummary = generateFallbackSummary(stats, filteredLogs);
|
|
5533
|
+
message = "AI summarization unavailable, showing statistical summary";
|
|
5534
|
+
}
|
|
5535
|
+
} else {
|
|
5411
5536
|
aiSummary = generateFallbackSummary(stats, filteredLogs);
|
|
5412
|
-
message = "
|
|
5537
|
+
message = platform.llm ? "No question provided" : "LLM not configured";
|
|
5413
5538
|
}
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5539
|
+
return {
|
|
5540
|
+
ok: true,
|
|
5541
|
+
data: {
|
|
5542
|
+
summary: {
|
|
5543
|
+
question: question || "General log summary",
|
|
5544
|
+
timeRange: stats.timeRange,
|
|
5545
|
+
total: stats.total,
|
|
5546
|
+
stats,
|
|
5547
|
+
groups
|
|
5548
|
+
},
|
|
5549
|
+
aiSummary,
|
|
5550
|
+
message
|
|
5551
|
+
}
|
|
5552
|
+
};
|
|
5553
|
+
} catch (error) {
|
|
5554
|
+
return reply.code(500).send({
|
|
5555
|
+
ok: false,
|
|
5556
|
+
error: "Log summarization failed",
|
|
5557
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
5558
|
+
});
|
|
5417
5559
|
}
|
|
5418
|
-
return {
|
|
5419
|
-
ok: true,
|
|
5420
|
-
data: {
|
|
5421
|
-
summary: {
|
|
5422
|
-
question: question || "General log summary",
|
|
5423
|
-
timeRange: stats.timeRange,
|
|
5424
|
-
total: stats.total,
|
|
5425
|
-
stats,
|
|
5426
|
-
groups
|
|
5427
|
-
},
|
|
5428
|
-
aiSummary,
|
|
5429
|
-
message
|
|
5430
|
-
}
|
|
5431
|
-
};
|
|
5432
|
-
} catch (error) {
|
|
5433
|
-
return reply.code(500).send({
|
|
5434
|
-
ok: false,
|
|
5435
|
-
error: "Log summarization failed",
|
|
5436
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
5437
|
-
});
|
|
5438
5560
|
}
|
|
5439
|
-
|
|
5561
|
+
);
|
|
5440
5562
|
}
|
|
5441
5563
|
var SENSITIVE_KEYS = [
|
|
5442
5564
|
"apiKey",
|
|
@@ -5447,10 +5569,10 @@ var SENSITIVE_KEYS = [
|
|
|
5447
5569
|
"credentials",
|
|
5448
5570
|
"auth"
|
|
5449
5571
|
];
|
|
5450
|
-
function redactSensitiveData(obj, redacted = [],
|
|
5572
|
+
function redactSensitiveData(obj, redacted = [], path2 = "") {
|
|
5451
5573
|
const result = {};
|
|
5452
5574
|
for (const [key, value] of Object.entries(obj)) {
|
|
5453
|
-
const currentPath =
|
|
5575
|
+
const currentPath = path2 ? `${path2}.${key}` : key;
|
|
5454
5576
|
const lowerKey = key.toLowerCase();
|
|
5455
5577
|
const isSensitive = SENSITIVE_KEYS.some((pattern) => lowerKey.includes(pattern.toLowerCase()));
|
|
5456
5578
|
if (isSensitive && typeof value === "string") {
|
|
@@ -5492,8 +5614,8 @@ async function loadPlatformConfig(repoRoot) {
|
|
|
5492
5614
|
async function registerPlatformRoutes(fastify, config, repoRoot) {
|
|
5493
5615
|
const basePath = normalizeBasePath(config.basePath);
|
|
5494
5616
|
const configPaths = resolvePaths(basePath, "/platform/config");
|
|
5495
|
-
for (const
|
|
5496
|
-
fastify.get(
|
|
5617
|
+
for (const path2 of configPaths) {
|
|
5618
|
+
fastify.get(path2, async (_request, reply) => {
|
|
5497
5619
|
try {
|
|
5498
5620
|
const { adapters, adapterOptions, execution } = await restDomainOperationMetrics.observeOperation(
|
|
5499
5621
|
"platform.config.get",
|
|
@@ -5692,8 +5814,8 @@ function registerRouteCollector(fastify) {
|
|
|
5692
5814
|
async function registerDebugRoutes(fastify, config) {
|
|
5693
5815
|
const basePath = normalizeBasePath(config.basePath);
|
|
5694
5816
|
const routesPaths = resolvePaths(basePath, "/routes");
|
|
5695
|
-
for (const
|
|
5696
|
-
fastify.get(
|
|
5817
|
+
for (const path2 of routesPaths) {
|
|
5818
|
+
fastify.get(path2, async (_request, reply) => {
|
|
5697
5819
|
let routes = collectedRoutes.filter((r) => r.url.startsWith("/api/"));
|
|
5698
5820
|
if (routes.length === 0) {
|
|
5699
5821
|
routes = [...collectedRoutes];
|
|
@@ -6101,20 +6223,18 @@ function registerRequestIdMiddleware(server) {
|
|
|
6101
6223
|
request.id = requestId;
|
|
6102
6224
|
reply.header("X-Request-Id", requestId);
|
|
6103
6225
|
reply.header("X-Trace-Id", traceId);
|
|
6104
|
-
|
|
6226
|
+
const logger = createHttpLogger(platform.logger, {
|
|
6105
6227
|
serviceId: "rest",
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
service: "request",
|
|
6228
|
+
layer: "service",
|
|
6229
|
+
component: "http-request",
|
|
6109
6230
|
requestId,
|
|
6110
6231
|
traceId,
|
|
6111
6232
|
method: request.method,
|
|
6112
6233
|
url: request.url,
|
|
6113
6234
|
operation: "http.request"
|
|
6114
6235
|
});
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
request.kbLogger.info(`\u2192 ${method} ${fullUrl}`);
|
|
6236
|
+
request.kbLogger = logger;
|
|
6237
|
+
logger.debug("HTTP request started");
|
|
6118
6238
|
});
|
|
6119
6239
|
}
|
|
6120
6240
|
|
|
@@ -6429,41 +6549,40 @@ async function createServer(config, repoRoot, registry) {
|
|
|
6429
6549
|
}
|
|
6430
6550
|
var registryInstance = null;
|
|
6431
6551
|
var metricsCollector2 = null;
|
|
6432
|
-
async function findMonorepoRoot(startDir) {
|
|
6433
|
-
let dir = path.resolve(startDir);
|
|
6434
|
-
while (true) {
|
|
6435
|
-
const hasWorkspace = await promises.access(path.join(dir, "pnpm-workspace.yaml")).then(() => true).catch(() => false);
|
|
6436
|
-
if (hasWorkspace) {
|
|
6437
|
-
return dir;
|
|
6438
|
-
}
|
|
6439
|
-
const parent = path.dirname(dir);
|
|
6440
|
-
if (parent === dir) {
|
|
6441
|
-
break;
|
|
6442
|
-
}
|
|
6443
|
-
dir = parent;
|
|
6444
|
-
}
|
|
6445
|
-
return findRepoRoot(startDir);
|
|
6446
|
-
}
|
|
6447
6552
|
async function bootstrap(cwd = process.cwd()) {
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6553
|
+
await runService({
|
|
6554
|
+
appId: "rest-api",
|
|
6555
|
+
serviceId: "rest",
|
|
6556
|
+
startDir: cwd,
|
|
6557
|
+
defaultPort: 5050,
|
|
6558
|
+
portEnvVar: "REST_API_PORT",
|
|
6559
|
+
defaultHost: "0.0.0.0",
|
|
6560
|
+
hostEnvVar: "REST_API_HOST",
|
|
6561
|
+
platform: {
|
|
6562
|
+
assemblyHook: makeAssemblyHook()
|
|
6563
|
+
},
|
|
6564
|
+
setup: startRestApi
|
|
6565
|
+
});
|
|
6566
|
+
}
|
|
6567
|
+
async function startRestApi({
|
|
6568
|
+
platform: platform16,
|
|
6569
|
+
projectRoot: repoRoot,
|
|
6570
|
+
platformRoot,
|
|
6571
|
+
logger: serviceLogger
|
|
6572
|
+
}) {
|
|
6573
|
+
const { config, diagnostics } = await loadRestApiConfig(repoRoot);
|
|
6452
6574
|
const startupRequestId = `rest-startup-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
6453
6575
|
const startupTraceId = randomUUID();
|
|
6454
6576
|
const startupSpanId = randomUUID();
|
|
6455
|
-
const bootstrapLogger =
|
|
6456
|
-
layer: "rest",
|
|
6457
|
-
service: "bootstrap",
|
|
6577
|
+
const bootstrapLogger = serviceLogger.forComponent("rest-bootstrap").forOperation("rest.bootstrap", {
|
|
6458
6578
|
requestId: startupRequestId,
|
|
6459
|
-
reqId: startupRequestId,
|
|
6460
6579
|
traceId: startupTraceId,
|
|
6461
|
-
spanId: startupSpanId
|
|
6462
|
-
|
|
6463
|
-
executionId: startupSpanId
|
|
6464
|
-
});
|
|
6580
|
+
spanId: startupSpanId
|
|
6581
|
+
}).with({ invocationId: startupSpanId, executionId: startupSpanId });
|
|
6465
6582
|
if (diagnostics.length > 0) {
|
|
6466
|
-
bootstrapLogger.warn("Configuration diagnostics", {
|
|
6583
|
+
bootstrapLogger.warn("Configuration diagnostics", {
|
|
6584
|
+
diagnosticsCount: diagnostics.length
|
|
6585
|
+
});
|
|
6467
6586
|
for (const diagnostic of diagnostics) {
|
|
6468
6587
|
bootstrapLogger.warn("Configuration diagnostic", {
|
|
6469
6588
|
level: diagnostic.level,
|
|
@@ -6471,22 +6590,28 @@ async function bootstrap(cwd = process.cwd()) {
|
|
|
6471
6590
|
});
|
|
6472
6591
|
}
|
|
6473
6592
|
}
|
|
6474
|
-
bootstrapLogger.info("Resolved
|
|
6593
|
+
bootstrapLogger.info("Resolved service roots", {
|
|
6594
|
+
projectRoot: repoRoot,
|
|
6595
|
+
platformRoot
|
|
6596
|
+
});
|
|
6475
6597
|
bootstrapLogger.info("Platform adapters initialized");
|
|
6476
6598
|
bootstrapLogger.info("Initializing entity registry");
|
|
6477
6599
|
const isDevelopment = process.env.NODE_ENV !== "production";
|
|
6478
6600
|
const snapshotTTL = isDevelopment ? 10 * 60 * 1e3 : 60 * 60 * 1e3;
|
|
6479
|
-
const platformRoot = getPlatformRoot();
|
|
6480
6601
|
const registryInitStart = performance.now();
|
|
6481
6602
|
const registry = await createRegistry({
|
|
6482
6603
|
root: repoRoot,
|
|
6483
6604
|
platformRoot: platformRoot !== repoRoot ? platformRoot : void 0,
|
|
6484
6605
|
cache: {
|
|
6485
6606
|
ttlMs: snapshotTTL,
|
|
6486
|
-
adapter:
|
|
6607
|
+
adapter: platform16.cache
|
|
6487
6608
|
}
|
|
6488
6609
|
});
|
|
6489
|
-
restDomainOperationMetrics.recordOperation(
|
|
6610
|
+
restDomainOperationMetrics.recordOperation(
|
|
6611
|
+
"registry.init",
|
|
6612
|
+
performance.now() - registryInitStart,
|
|
6613
|
+
"ok"
|
|
6614
|
+
);
|
|
6490
6615
|
const plugins = registry.listPlugins();
|
|
6491
6616
|
bootstrapLogger.info("Entity registry initialized", {
|
|
6492
6617
|
pluginsFound: plugins.length,
|
|
@@ -6503,9 +6628,12 @@ async function bootstrap(cwd = process.cwd()) {
|
|
|
6503
6628
|
});
|
|
6504
6629
|
const server = await createServer(config, repoRoot, registry);
|
|
6505
6630
|
bootstrapLogger.info("Starting system metrics collector");
|
|
6506
|
-
metricsCollector2 = new SystemMetricsCollector(
|
|
6631
|
+
metricsCollector2 = new SystemMetricsCollector(
|
|
6632
|
+
"rest",
|
|
6633
|
+
() => metricsCollector.getActiveRequests()
|
|
6634
|
+
);
|
|
6507
6635
|
await metricsCollector2.start(1e4, 6e4);
|
|
6508
|
-
const restTransport =
|
|
6636
|
+
const restTransport = platform16.getAdapter("serviceTransport");
|
|
6509
6637
|
const restAddr = restTransport?.listenAddress?.("rest");
|
|
6510
6638
|
const listenPort = restAddr && "port" in restAddr ? restAddr.port : config.port;
|
|
6511
6639
|
const restAddrHost = restAddr && "host" in restAddr ? restAddr.host : void 0;
|
|
@@ -6522,20 +6650,15 @@ async function bootstrap(cwd = process.cwd()) {
|
|
|
6522
6650
|
await registryInstance.dispose();
|
|
6523
6651
|
registryInstance = null;
|
|
6524
6652
|
}
|
|
6525
|
-
await platform.shutdown();
|
|
6526
|
-
bootstrapLogger.info("Platform shutdown complete");
|
|
6527
6653
|
await server.close();
|
|
6528
6654
|
bootstrapLogger.info("Server closed");
|
|
6529
|
-
process.exit(0);
|
|
6530
6655
|
};
|
|
6531
|
-
|
|
6532
|
-
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
6656
|
+
return () => shutdown("service-runner");
|
|
6533
6657
|
}
|
|
6534
6658
|
|
|
6535
6659
|
// src/index.ts
|
|
6536
|
-
bootstrap(process.cwd()).catch((
|
|
6537
|
-
|
|
6538
|
-
process.exit(1);
|
|
6660
|
+
bootstrap(process.cwd()).catch(() => {
|
|
6661
|
+
process.exitCode = 1;
|
|
6539
6662
|
});
|
|
6540
6663
|
//# sourceMappingURL=index.js.map
|
|
6541
6664
|
//# sourceMappingURL=index.js.map
|