@kb-labs/rest-api-app 2.94.0 → 2.98.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 CHANGED
@@ -3,7 +3,7 @@ import Fastify from 'fastify';
3
3
  import { loadEnvFromRoot, createServiceBootstrap, platform, getPlatformRoot } from '@kb-labs/core-runtime';
4
4
  import { EventEmitter } from 'events';
5
5
  import { performance, monitorEventLoopDelay } from 'perf_hooks';
6
- import { OperationMetricsTracker, registerOpenAPI, createCorrelatedLogger, createServiceObservabilityDescribe, createServiceObservabilityHealth } from '@kb-labs/shared-http';
6
+ import { OperationMetricsTracker, getListenOptions, registerOpenAPI, createCorrelatedLogger, 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';
@@ -13,14 +13,15 @@ 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
15
  import { mountWebSocketChannels } from '@kb-labs/plugin-execution';
16
+ import { InProcessBackend } from '@kb-labs/plugin-execution-factory';
16
17
  import { logDiagnosticEvent } from '@kb-labs/core-platform';
17
- import * as path2 from 'path';
18
+ import * as path from 'path';
18
19
  import * as fs from 'fs/promises';
19
20
  import { resolveWorkspaceRoot } from '@kb-labs/core-workspace';
20
21
  import { WORKFLOW_REDIS_CHANNEL } from '@kb-labs/workflow-constants';
21
22
  import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
22
23
  import { z } from 'zod';
23
- import { errorEnvelopeSchema, ErrorCode, ErrorResponseSchema, JobsListResponseSchema, ListJobsQuerySchema, JobStatsResponseSchema, JobResponseSchema, JobActionResponseSchema } from '@kb-labs/rest-api-contracts';
24
+ import { errorEnvelopeSchema, ErrorResponseSchema, JobsListResponseSchema, ListJobsQuerySchema, JobStatsResponseSchema, JobResponseSchema, JobActionResponseSchema } from '@kb-labs/rest-api-contracts';
24
25
  import { readKbConfig } from '@kb-labs/core-config';
25
26
  import { AdapterNameSchema } from '@kb-labs/gateway-contracts';
26
27
  import fastifyCors from '@fastify/cors';
@@ -30,6 +31,7 @@ import { randomUUID, createHash } from 'crypto';
30
31
  import { TenantRateLimiter, getDefaultTenantTier } from '@kb-labs/core-tenant';
31
32
  import { promises, existsSync, readFileSync } from 'fs';
32
33
  import { findRepoRoot } from '@kb-labs/core-sys';
34
+ import { makeAssemblyHook } from '@kb-labs/plugin-runtime';
33
35
 
34
36
  // src/bootstrap.ts
35
37
  var EventHub = class {
@@ -769,7 +771,7 @@ function updateLatencyHistogram(histogram, bucket, durationMs, statusCode) {
769
771
  stats.byStatus[statusKey] = (stats.byStatus[statusKey] || 0) + 1;
770
772
  }
771
773
 
772
- // src/utils/sse-auth.ts
774
+ // src/routes/sse-auth.ts
773
775
  function extractTokenFromHeader(headerValue, headerName) {
774
776
  if (typeof headerValue === "string") {
775
777
  if (headerName === "authorization") {
@@ -915,7 +917,7 @@ async function registerEventRoutes(server, basePath, registry, readiness, eventH
915
917
  });
916
918
  }
917
919
 
918
- // src/utils/path-helpers.ts
920
+ // src/routes/path-helpers.ts
919
921
  function normalizeBasePath(basePath) {
920
922
  if (!basePath || basePath === "/") {
921
923
  return "";
@@ -1073,6 +1075,323 @@ function getLatestSystemMetrics() {
1073
1075
  return latestSystemMetricsSnapshot;
1074
1076
  }
1075
1077
 
1078
+ // src/daemon/metrics/historical-metrics.ts
1079
+ var DEFAULT_CONFIG = {
1080
+ intervalMs: 5e3,
1081
+ maxPoints: {
1082
+ "1m": 12,
1083
+ "5m": 60,
1084
+ "10m": 120,
1085
+ "30m": 360,
1086
+ "1h": 720
1087
+ },
1088
+ debug: false
1089
+ };
1090
+ var HistoricalMetricsCollector = class {
1091
+ cache;
1092
+ config;
1093
+ intervalHandle = null;
1094
+ logger;
1095
+ startTimeMs = Date.now();
1096
+ constructor(cache, config = {}, logger = console) {
1097
+ this.cache = cache;
1098
+ this.config = { ...DEFAULT_CONFIG, ...config, maxPoints: { ...DEFAULT_CONFIG.maxPoints, ...config.maxPoints } };
1099
+ this.logger = logger;
1100
+ }
1101
+ /**
1102
+ * Start background collection
1103
+ */
1104
+ start() {
1105
+ if (this.intervalHandle) {
1106
+ this.log("warn", "Historical metrics collector already started");
1107
+ return;
1108
+ }
1109
+ this.log("info", "Starting historical metrics collector", {
1110
+ intervalMs: this.config.intervalMs
1111
+ });
1112
+ this.collect().catch((err) => {
1113
+ this.log("error", "Failed to collect initial metrics", { err });
1114
+ });
1115
+ this.intervalHandle = setInterval(() => {
1116
+ this.collect().catch((err) => {
1117
+ this.log("error", "Failed to collect metrics", { err });
1118
+ });
1119
+ }, this.config.intervalMs);
1120
+ }
1121
+ /**
1122
+ * Stop background collection
1123
+ */
1124
+ stop() {
1125
+ if (this.intervalHandle) {
1126
+ clearInterval(this.intervalHandle);
1127
+ this.intervalHandle = null;
1128
+ this.log("info", "Historical metrics collector stopped");
1129
+ }
1130
+ }
1131
+ /**
1132
+ * Collect current metrics snapshot and store in cache
1133
+ */
1134
+ async collect() {
1135
+ const now = Date.now();
1136
+ const metrics = metricsCollector.getMetrics();
1137
+ const snapshot = {
1138
+ timestamp: now,
1139
+ requests: {
1140
+ total: metrics.requests.total,
1141
+ success: metrics.requests.success ?? 0,
1142
+ clientErrors: metrics.requests.clientErrors ?? 0,
1143
+ serverErrors: metrics.requests.serverErrors ?? 0
1144
+ },
1145
+ latency: {
1146
+ average: metrics.latency.average,
1147
+ min: metrics.latency.min === Infinity ? 0 : metrics.latency.min,
1148
+ max: metrics.latency.max
1149
+ },
1150
+ uptime: (now - metrics.timestamps.startTime) / 1e3,
1151
+ perPlugin: metrics.perPlugin.map((p) => ({
1152
+ pluginId: p.pluginId,
1153
+ requests: p.total,
1154
+ errors: Object.values(p.statuses).filter((_, idx) => Object.keys(p.statuses)[idx]?.startsWith("4") || Object.keys(p.statuses)[idx]?.startsWith("5")).reduce((sum, count) => sum + count, 0),
1155
+ avgLatency: p.total > 0 ? p.totalDuration / p.total : 0
1156
+ }))
1157
+ };
1158
+ await Promise.all([
1159
+ this.appendToTimeSeries("1m", snapshot, 2 * 60 * 1e3),
1160
+ // TTL: 2 minutes
1161
+ this.appendToTimeSeries("5m", snapshot, 10 * 60 * 1e3),
1162
+ // TTL: 10 minutes
1163
+ this.appendToTimeSeries("10m", snapshot, 20 * 60 * 1e3),
1164
+ // TTL: 20 minutes
1165
+ this.appendToTimeSeries("30m", snapshot, 60 * 60 * 1e3),
1166
+ // TTL: 1 hour
1167
+ this.appendToTimeSeries("1h", snapshot, 2 * 60 * 60 * 1e3)
1168
+ // TTL: 2 hours
1169
+ ]);
1170
+ if (now % 6e4 < this.config.intervalMs) {
1171
+ await this.updateHeatmapAggregation(snapshot).catch((err) => {
1172
+ this.log("error", "Failed to update heatmap", { err });
1173
+ });
1174
+ }
1175
+ this.log("debug", "Metrics snapshot collected", {
1176
+ timestamp: new Date(now).toISOString(),
1177
+ requests: snapshot.requests.total,
1178
+ latency: snapshot.latency.average.toFixed(2)
1179
+ });
1180
+ }
1181
+ /**
1182
+ * Append snapshot to time series bucket
1183
+ */
1184
+ async appendToTimeSeries(range, snapshot, ttlMs) {
1185
+ const key = `metrics:history:${range}`;
1186
+ const maxPoints = this.config.maxPoints[range] ?? 120;
1187
+ let timeSeries = await this.cache.get(key);
1188
+ if (!timeSeries || !Array.isArray(timeSeries)) {
1189
+ timeSeries = [];
1190
+ }
1191
+ timeSeries.push(snapshot);
1192
+ if (timeSeries.length > maxPoints) {
1193
+ timeSeries = timeSeries.slice(timeSeries.length - maxPoints);
1194
+ }
1195
+ await this.cache.set(key, timeSeries, ttlMs);
1196
+ }
1197
+ /**
1198
+ * Update heatmap aggregation for weekly patterns
1199
+ */
1200
+ async updateHeatmapAggregation(snapshot) {
1201
+ const key = "metrics:heatmap:7d";
1202
+ const ttlMs = 24 * 60 * 60 * 1e3;
1203
+ let heatmapData = await this.cache.get(key);
1204
+ if (!heatmapData || typeof heatmapData !== "object") {
1205
+ heatmapData = { latency: [], errors: [], requests: [] };
1206
+ }
1207
+ const date = new Date(snapshot.timestamp);
1208
+ const day = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][date.getDay()];
1209
+ const hour = date.getHours();
1210
+ for (const metricType of ["latency", "errors", "requests"]) {
1211
+ let cells = heatmapData[metricType] || [];
1212
+ const cellIndex = cells.findIndex((c) => c.day === day && c.hour === hour);
1213
+ let value = 0;
1214
+ if (metricType === "latency") {
1215
+ value = snapshot.latency.average;
1216
+ } else if (metricType === "errors") {
1217
+ value = snapshot.requests.clientErrors + snapshot.requests.serverErrors;
1218
+ } else {
1219
+ value = snapshot.requests.total;
1220
+ }
1221
+ if (cellIndex >= 0 && cells[cellIndex]) {
1222
+ cells[cellIndex].value = cells[cellIndex].value * 0.9 + value * 0.1;
1223
+ } else {
1224
+ cells.push({ day, hour, value });
1225
+ }
1226
+ if (cells.length > 168) {
1227
+ cells = cells.slice(cells.length - 168);
1228
+ }
1229
+ heatmapData[metricType] = cells;
1230
+ }
1231
+ await this.cache.set(key, heatmapData, ttlMs);
1232
+ }
1233
+ /**
1234
+ * Query historical time-series data
1235
+ */
1236
+ async queryHistory(params) {
1237
+ const key = `metrics:history:${params.range}`;
1238
+ const timeSeries = await this.cache.get(key);
1239
+ if (!timeSeries || !Array.isArray(timeSeries)) {
1240
+ return [];
1241
+ }
1242
+ const dataPoints = timeSeries.map((snapshot) => {
1243
+ let value = 0;
1244
+ switch (params.metric) {
1245
+ case "requests":
1246
+ value = snapshot.requests.total;
1247
+ break;
1248
+ case "errors":
1249
+ value = snapshot.requests.clientErrors + snapshot.requests.serverErrors;
1250
+ break;
1251
+ case "latency":
1252
+ value = snapshot.latency.average;
1253
+ break;
1254
+ case "uptime":
1255
+ value = snapshot.uptime;
1256
+ break;
1257
+ }
1258
+ return {
1259
+ timestamp: snapshot.timestamp,
1260
+ value
1261
+ };
1262
+ });
1263
+ if (params.interval && params.interval !== "5s") {
1264
+ return this.aggregateByInterval(dataPoints, params.interval);
1265
+ }
1266
+ return dataPoints;
1267
+ }
1268
+ /**
1269
+ * Query heatmap data
1270
+ */
1271
+ async queryHeatmap(params) {
1272
+ const key = "metrics:heatmap:7d";
1273
+ const heatmapData = await this.cache.get(key);
1274
+ if (!heatmapData || typeof heatmapData !== "object") {
1275
+ return this.generateEmptyHeatmap();
1276
+ }
1277
+ const cells = heatmapData[params.metric] || [];
1278
+ if (cells.length === 0) {
1279
+ return this.generateEmptyHeatmap();
1280
+ }
1281
+ return this.fillHeatmapGaps(cells);
1282
+ }
1283
+ /**
1284
+ * Generate empty heatmap structure (7 days × 24 hours)
1285
+ */
1286
+ generateEmptyHeatmap() {
1287
+ const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
1288
+ const cells = [];
1289
+ for (const day of days) {
1290
+ for (let hour = 0; hour < 24; hour++) {
1291
+ cells.push({ day, hour, value: 0 });
1292
+ }
1293
+ }
1294
+ return cells;
1295
+ }
1296
+ /**
1297
+ * Fill gaps in heatmap data (missing day/hour combinations)
1298
+ */
1299
+ fillHeatmapGaps(cells) {
1300
+ const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
1301
+ const cellMap = /* @__PURE__ */ new Map();
1302
+ for (const cell of cells) {
1303
+ const key = `${cell.day}:${cell.hour}`;
1304
+ cellMap.set(key, cell.value);
1305
+ }
1306
+ const complete = [];
1307
+ for (const day of days) {
1308
+ for (let hour = 0; hour < 24; hour++) {
1309
+ const key = `${day}:${hour}`;
1310
+ complete.push({
1311
+ day,
1312
+ hour,
1313
+ value: cellMap.get(key) ?? 0
1314
+ });
1315
+ }
1316
+ }
1317
+ return complete;
1318
+ }
1319
+ /**
1320
+ * Aggregate data points by interval
1321
+ */
1322
+ aggregateByInterval(dataPoints, interval) {
1323
+ if (dataPoints.length === 0) {
1324
+ return [];
1325
+ }
1326
+ const intervalMs = interval === "1m" ? 60 * 1e3 : 5 * 60 * 1e3;
1327
+ const aggregated = [];
1328
+ let bucket = [];
1329
+ let bucketStart = Math.floor(dataPoints[0].timestamp / intervalMs) * intervalMs;
1330
+ for (const point of dataPoints) {
1331
+ const pointBucket = Math.floor(point.timestamp / intervalMs) * intervalMs;
1332
+ if (pointBucket === bucketStart) {
1333
+ bucket.push(point);
1334
+ } else {
1335
+ if (bucket.length > 0) {
1336
+ const avgValue = bucket.reduce((sum, p) => sum + p.value, 0) / bucket.length;
1337
+ aggregated.push({
1338
+ timestamp: bucketStart + intervalMs / 2,
1339
+ // midpoint
1340
+ value: avgValue
1341
+ });
1342
+ }
1343
+ bucket = [point];
1344
+ bucketStart = pointBucket;
1345
+ }
1346
+ }
1347
+ if (bucket.length > 0) {
1348
+ const avgValue = bucket.reduce((sum, p) => sum + p.value, 0) / bucket.length;
1349
+ aggregated.push({
1350
+ timestamp: bucketStart + intervalMs / 2,
1351
+ value: avgValue
1352
+ });
1353
+ }
1354
+ return aggregated;
1355
+ }
1356
+ /**
1357
+ * Get collector statistics
1358
+ */
1359
+ async getStats() {
1360
+ const stats = {
1361
+ running: this.intervalHandle !== null,
1362
+ uptimeSeconds: (Date.now() - this.startTimeMs) / 1e3,
1363
+ timeSeries: {},
1364
+ heatmap: { cells: 0, metrics: [] }
1365
+ };
1366
+ for (const range of ["1m", "5m", "10m", "30m", "1h"]) {
1367
+ const key = `metrics:history:${range}`;
1368
+ const timeSeries = await this.cache.get(key);
1369
+ if (timeSeries && Array.isArray(timeSeries)) {
1370
+ stats.timeSeries[range] = {
1371
+ points: timeSeries.length,
1372
+ oldestTimestamp: timeSeries[0]?.timestamp ?? null,
1373
+ newestTimestamp: timeSeries[timeSeries.length - 1]?.timestamp ?? null
1374
+ };
1375
+ } else {
1376
+ stats.timeSeries[range] = { points: 0, oldestTimestamp: null, newestTimestamp: null };
1377
+ }
1378
+ }
1379
+ const heatmapData = await this.cache.get("metrics:heatmap:7d");
1380
+ if (heatmapData && typeof heatmapData === "object") {
1381
+ const metrics = Object.keys(heatmapData);
1382
+ const totalCells = metrics.reduce((sum, m) => sum + (heatmapData[m]?.length ?? 0), 0);
1383
+ stats.heatmap = { cells: totalCells, metrics };
1384
+ }
1385
+ return stats;
1386
+ }
1387
+ log(level, message, meta) {
1388
+ if (level === "debug" && !this.config.debug) {
1389
+ return;
1390
+ }
1391
+ this.logger[level](`[HistoricalMetrics] ${message}`, meta);
1392
+ }
1393
+ };
1394
+
1076
1395
  // src/observability/service-contract.ts
1077
1396
  var REST_VERSION = "1.6.0" ;
1078
1397
  function resolveInstanceId() {
@@ -1727,6 +2046,7 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
1727
2046
  }
1728
2047
  emitRegistrySnapshotDiagnostics(snapshot);
1729
2048
  const backend = platform.executionBackend;
2049
+ const wsBackend = new InProcessBackend({ platform: platform });
1730
2050
  const mountableManifests = manifests.filter(
1731
2051
  (entry) => entry.manifest.rest?.routes && entry.manifest.rest.routes.length > 0 || entry.manifest.ws?.channels && entry.manifest.ws.channels.length > 0
1732
2052
  );
@@ -1741,8 +2061,8 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
1741
2061
  for (const route of entry.manifest.rest.routes) {
1742
2062
  const handlerFile = route.handler.split("#")[0];
1743
2063
  if (handlerFile) {
1744
- const pluginDistRoot = path2.join(entry.pluginRoot, "dist");
1745
- const handlerPath = path2.resolve(pluginDistRoot, handlerFile);
2064
+ const pluginDistRoot = path.join(entry.pluginRoot, "dist");
2065
+ const handlerPath = path.resolve(pluginDistRoot, handlerFile);
1746
2066
  handlerChecks.push({
1747
2067
  key: `${entry.manifest.id}::${route.method} ${route.path}`,
1748
2068
  filePath: handlerPath
@@ -1778,7 +2098,7 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
1778
2098
  const routeFailures = [];
1779
2099
  for (const entry of mountableManifests) {
1780
2100
  const { manifest, pluginRoot } = entry;
1781
- const pluginDistRoot = path2.join(pluginRoot, "dist");
2101
+ const pluginDistRoot = path.join(pluginRoot, "dist");
1782
2102
  const restValidationErrors = [];
1783
2103
  if (manifest.rest?.routes) {
1784
2104
  for (const route of manifest.rest.routes) {
@@ -1931,7 +2251,7 @@ async function registerPluginRoutes(server, config, repoRoot, registry, readines
1931
2251
  channels: manifest.ws.channels.length
1932
2252
  });
1933
2253
  const wsResult = await mountWebSocketChannels(server, manifest, {
1934
- backend,
2254
+ backend: wsBackend,
1935
2255
  pluginRoot: pluginDistRoot,
1936
2256
  workspaceRoot,
1937
2257
  basePath: wsBasePath,
@@ -2113,7 +2433,7 @@ async function registerPluginSnapshotRoutes(server, config, registry) {
2113
2433
  snapshot.manifests.map(async (entry) => {
2114
2434
  let buildTimestamp;
2115
2435
  try {
2116
- const distPath = path2.join(entry.pluginRoot, "dist");
2436
+ const distPath = path.join(entry.pluginRoot, "dist");
2117
2437
  const stats = await fs.stat(distPath);
2118
2438
  buildTimestamp = stats.mtime.toISOString();
2119
2439
  } catch {
@@ -2162,6 +2482,62 @@ async function registerPluginSnapshotRoutes(server, config, registry) {
2162
2482
  });
2163
2483
  }
2164
2484
  });
2485
+ server.get(`${basePath}/plugins/@:scope/:name/readme`, {
2486
+ schema: { hide: true }
2487
+ }, async (request, reply) => {
2488
+ const { scope, name } = request.params;
2489
+ if (!scope || !name) {
2490
+ return reply.code(400).send({ error: "Invalid plugin identifier" });
2491
+ }
2492
+ const pluginId = `@${scope}/${name}`;
2493
+ const snapshot = registry.snapshot();
2494
+ const manifests = extractSnapshotManifests(snapshot);
2495
+ const entry = manifests.find((e) => e.pluginId === pluginId);
2496
+ if (!entry) {
2497
+ return reply.code(404).send({ error: `Plugin not found: ${pluginId}` });
2498
+ }
2499
+ const readmePath = path.join(entry.pluginRoot, "README.md");
2500
+ if (!readmePath.startsWith(entry.pluginRoot)) {
2501
+ return reply.code(400).send({ error: "Invalid path" });
2502
+ }
2503
+ try {
2504
+ const content = await fs.readFile(readmePath, "utf-8");
2505
+ return reply.code(200).header("Content-Type", "text/plain; charset=utf-8").send(content);
2506
+ } catch (error) {
2507
+ if (error.code === "ENOENT") {
2508
+ return reply.code(404).send({ error: "README.md not found" });
2509
+ }
2510
+ return reply.code(500).send({ error: "Failed to read README.md" });
2511
+ }
2512
+ });
2513
+ server.get(`${basePath}/plugins/@:scope/:name/changelog`, {
2514
+ schema: { hide: true }
2515
+ }, async (request, reply) => {
2516
+ const { scope, name } = request.params;
2517
+ if (!scope || !name) {
2518
+ return reply.code(400).send({ error: "Invalid plugin identifier" });
2519
+ }
2520
+ const pluginId = `@${scope}/${name}`;
2521
+ const snapshot = registry.snapshot();
2522
+ const manifests = extractSnapshotManifests(snapshot);
2523
+ const entry = manifests.find((e) => e.pluginId === pluginId);
2524
+ if (!entry) {
2525
+ return reply.code(404).send({ error: `Plugin not found: ${pluginId}` });
2526
+ }
2527
+ const changelogPath = path.join(entry.pluginRoot, "CHANGELOG.md");
2528
+ if (!changelogPath.startsWith(entry.pluginRoot)) {
2529
+ return reply.code(400).send({ error: "Invalid path" });
2530
+ }
2531
+ try {
2532
+ const content = await fs.readFile(changelogPath, "utf-8");
2533
+ return reply.code(200).header("Content-Type", "text/plain; charset=utf-8").send(content);
2534
+ } catch (error) {
2535
+ if (error.code === "ENOENT") {
2536
+ return reply.code(404).send({ error: "CHANGELOG.md not found" });
2537
+ }
2538
+ return reply.code(500).send({ error: "Failed to read CHANGELOG.md" });
2539
+ }
2540
+ });
2165
2541
  server.get(`${basePath}/studio/registry`, {
2166
2542
  schema: { tags: ["Studio"], summary: "Get Studio Registry V2 (plugin pages for Module Federation)" }
2167
2543
  }, async (_request, reply) => {
@@ -2217,7 +2593,7 @@ async function registerPluginSnapshotRoutes(server, config, registry) {
2217
2593
  };
2218
2594
  const ext = extname(filePath);
2219
2595
  const isEntry = filePath === "remoteEntry.js";
2220
- 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" : "public, max-age=31536000, immutable").send(createReadStream(fullPath));
2596
+ 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));
2221
2597
  });
2222
2598
  server.get(`${basePath}/plugins/health`, async (_request, reply) => {
2223
2599
  try {
@@ -4268,1400 +4644,1078 @@ async function registerAdaptersRoutes(fastify, config) {
4268
4644
  }
4269
4645
  });
4270
4646
  }
4271
- platform.logger.error("Failed to fetch VectorStore usage stats", error instanceof Error ? error : new Error(String(error)));
4272
- return reply.code(500).send({
4273
- ok: false,
4274
- error: {
4275
- code: "VECTORSTORE_USAGE_STATS_ERROR",
4276
- message: error instanceof Error ? error.message : "Failed to fetch vectorstore statistics"
4277
- }
4278
- });
4279
- }
4280
- });
4281
- }
4282
- const cacheDailyStatsPaths = resolvePaths(basePath, "/adapters/cache/daily-stats");
4283
- for (const path3 of cacheDailyStatsPaths) {
4284
- fastify.get(path3, async (request, reply) => {
4285
- const analytics = platform.analytics;
4286
- if (!analytics.getDailyStats) {
4287
- return reply.code(501).send({
4288
- ok: false,
4289
- error: {
4290
- code: "DAILY_STATS_NOT_IMPLEMENTED",
4291
- message: "Analytics adapter does not support daily statistics"
4292
- }
4293
- });
4294
- }
4295
- try {
4296
- const dateRange = extractDateRange(request.query);
4297
- const statsOptions = extractStatsOptions(request.query);
4298
- const dailyStats = await analytics.getDailyStats({
4299
- type: ["cache.get.hit", "cache.get.miss", "cache.set.completed"],
4300
- from: dateRange.from,
4301
- to: dateRange.to,
4302
- limit: 5e4,
4303
- ...statsOptions
4304
- });
4305
- return reply.send({ ok: true, data: dailyStats });
4306
- } catch (error) {
4307
- if (error instanceof Error && error.message.includes("Invalid")) {
4308
- return reply.code(400).send({
4309
- ok: false,
4310
- error: { code: "INVALID_DATE_RANGE", message: error.message }
4311
- });
4312
- }
4313
- return reply.code(500).send({
4314
- ok: false,
4315
- error: {
4316
- code: "DAILY_STATS_ERROR",
4317
- message: error instanceof Error ? error.message : "Failed to fetch daily statistics"
4318
- }
4319
- });
4320
- }
4321
- });
4322
- }
4323
- const cacheUsagePaths = resolvePaths(basePath, "/adapters/cache/usage");
4324
- for (const path3 of cacheUsagePaths) {
4325
- fastify.get(path3, async (request, reply) => {
4326
- const analytics = platform.analytics;
4327
- if (!analytics.getEvents) {
4328
- return reply.code(501).send({
4329
- ok: false,
4330
- error: {
4331
- code: "ANALYTICS_NOT_IMPLEMENTED",
4332
- message: "Analytics adapter does not support reading events"
4333
- }
4334
- });
4335
- }
4336
- try {
4337
- const dateRange = extractDateRange(request.query);
4338
- const hitEvents = await fetchAllEventsBatched(
4339
- analytics,
4340
- {
4341
- type: "cache.get.hit",
4342
- from: dateRange.from,
4343
- to: dateRange.to
4344
- },
4345
- fastify
4346
- );
4347
- const missEvents = await fetchAllEventsBatched(
4348
- analytics,
4349
- {
4350
- type: "cache.get.miss",
4351
- from: dateRange.from,
4352
- to: dateRange.to
4353
- },
4354
- fastify
4355
- );
4356
- const setEvents = await fetchAllEventsBatched(
4357
- analytics,
4358
- {
4359
- type: "cache.set.completed",
4360
- from: dateRange.from,
4361
- to: dateRange.to
4362
- },
4363
- fastify
4364
- );
4365
- const totalGets = hitEvents.length + missEvents.length;
4366
- const hitRate = totalGets > 0 ? hitEvents.length / totalGets * 100 : 0;
4367
- const stats = {
4368
- totalGets,
4369
- hits: hitEvents.length,
4370
- misses: missEvents.length,
4371
- hitRate,
4372
- sets: setEvents.length,
4373
- avgGetDuration: 0,
4374
- avgSetDuration: 0
4375
- };
4376
- let totalGetDuration = 0;
4377
- for (const event of [...hitEvents, ...missEvents]) {
4378
- const props = getEventData(event);
4379
- totalGetDuration += Number(props.durationMs || 0);
4380
- }
4381
- stats.avgGetDuration = totalGets > 0 ? totalGetDuration / totalGets : 0;
4382
- let totalSetDuration = 0;
4383
- for (const event of setEvents) {
4384
- const props = getEventData(event);
4385
- totalSetDuration += Number(props.durationMs || 0);
4386
- }
4387
- stats.avgSetDuration = stats.sets > 0 ? totalSetDuration / stats.sets : 0;
4388
- return { ok: true, data: stats, meta: { source: "analytics-adapter" } };
4389
- } catch (error) {
4390
- if (error instanceof Error && error.message.includes("Invalid")) {
4391
- return reply.code(400).send({
4392
- ok: false,
4393
- error: {
4394
- code: "INVALID_DATE_RANGE",
4395
- message: error.message
4396
- }
4397
- });
4398
- }
4399
- platform.logger.error("Failed to fetch Cache usage stats", error instanceof Error ? error : new Error(String(error)));
4400
- return reply.code(500).send({
4401
- ok: false,
4402
- error: {
4403
- code: "CACHE_USAGE_STATS_ERROR",
4404
- message: error instanceof Error ? error.message : "Failed to fetch cache statistics"
4405
- }
4406
- });
4407
- }
4408
- });
4409
- }
4410
- const storageDailyStatsPaths = resolvePaths(basePath, "/adapters/storage/daily-stats");
4411
- for (const path3 of storageDailyStatsPaths) {
4412
- fastify.get(path3, async (request, reply) => {
4413
- const analytics = platform.analytics;
4414
- if (!analytics.getDailyStats) {
4415
- return reply.code(501).send({
4416
- ok: false,
4417
- error: {
4418
- code: "DAILY_STATS_NOT_IMPLEMENTED",
4419
- message: "Analytics adapter does not support daily statistics"
4420
- }
4421
- });
4422
- }
4423
- try {
4424
- const dateRange = extractDateRange(request.query);
4425
- const statsOptions = extractStatsOptions(request.query);
4426
- const dailyStats = await analytics.getDailyStats({
4427
- type: ["storage.read.completed", "storage.write.completed", "storage.delete.completed"],
4428
- from: dateRange.from,
4429
- to: dateRange.to,
4430
- limit: 5e4,
4431
- ...statsOptions
4432
- });
4433
- return reply.send({ ok: true, data: dailyStats });
4434
- } catch (error) {
4435
- if (error instanceof Error && error.message.includes("Invalid")) {
4436
- return reply.code(400).send({
4437
- ok: false,
4438
- error: { code: "INVALID_DATE_RANGE", message: error.message }
4439
- });
4440
- }
4441
- return reply.code(500).send({
4442
- ok: false,
4443
- error: {
4444
- code: "DAILY_STATS_ERROR",
4445
- message: error instanceof Error ? error.message : "Failed to fetch daily statistics"
4446
- }
4447
- });
4448
- }
4449
- });
4450
- }
4451
- const storageUsagePaths = resolvePaths(basePath, "/adapters/storage/usage");
4452
- for (const path3 of storageUsagePaths) {
4453
- fastify.get(path3, async (request, reply) => {
4454
- const analytics = platform.analytics;
4455
- if (!analytics.getEvents) {
4456
- return reply.code(501).send({
4457
- ok: false,
4458
- error: {
4459
- code: "ANALYTICS_NOT_IMPLEMENTED",
4460
- message: "Analytics adapter does not support reading events"
4461
- }
4462
- });
4463
- }
4464
- try {
4465
- const dateRange = extractDateRange(request.query);
4466
- const readEvents = await fetchAllEventsBatched(
4467
- analytics,
4468
- {
4469
- type: "storage.read.completed",
4470
- from: dateRange.from,
4471
- to: dateRange.to
4472
- },
4473
- fastify
4474
- );
4475
- const writeEvents = await fetchAllEventsBatched(
4476
- analytics,
4477
- {
4478
- type: "storage.write.completed",
4479
- from: dateRange.from,
4480
- to: dateRange.to
4481
- },
4482
- fastify
4483
- );
4484
- const deleteEvents = await fetchAllEventsBatched(
4485
- analytics,
4486
- {
4487
- type: "storage.delete.completed",
4488
- from: dateRange.from,
4489
- to: dateRange.to
4490
- },
4491
- fastify
4492
- );
4493
- const stats = {
4494
- readOperations: readEvents.length,
4495
- writeOperations: writeEvents.length,
4496
- deleteOperations: deleteEvents.length,
4497
- totalBytesRead: 0,
4498
- totalBytesWritten: 0,
4499
- avgReadDuration: 0,
4500
- avgWriteDuration: 0
4501
- };
4502
- let totalReadDuration = 0;
4503
- for (const event of readEvents) {
4504
- const props = getEventData(event);
4505
- stats.totalBytesRead += Number(props.bytesRead || 0);
4506
- totalReadDuration += Number(props.durationMs || 0);
4507
- }
4508
- stats.avgReadDuration = stats.readOperations > 0 ? totalReadDuration / stats.readOperations : 0;
4509
- let totalWriteDuration = 0;
4510
- for (const event of writeEvents) {
4511
- const props = getEventData(event);
4512
- stats.totalBytesWritten += Number(props.bytesWritten || 0);
4513
- totalWriteDuration += Number(props.durationMs || 0);
4514
- }
4515
- stats.avgWriteDuration = stats.writeOperations > 0 ? totalWriteDuration / stats.writeOperations : 0;
4516
- return { ok: true, data: stats, meta: { source: "analytics-adapter" } };
4517
- } catch (error) {
4518
- if (error instanceof Error && error.message.includes("Invalid")) {
4519
- return reply.code(400).send({
4520
- ok: false,
4521
- error: {
4522
- code: "INVALID_DATE_RANGE",
4523
- message: error.message
4524
- }
4525
- });
4526
- }
4527
- platform.logger.error("Failed to fetch Storage usage stats", error instanceof Error ? error : new Error(String(error)));
4647
+ platform.logger.error("Failed to fetch VectorStore usage stats", error instanceof Error ? error : new Error(String(error)));
4528
4648
  return reply.code(500).send({
4529
4649
  ok: false,
4530
4650
  error: {
4531
- code: "STORAGE_USAGE_STATS_ERROR",
4532
- message: error instanceof Error ? error.message : "Failed to fetch storage statistics"
4651
+ code: "VECTORSTORE_USAGE_STATS_ERROR",
4652
+ message: error instanceof Error ? error.message : "Failed to fetch vectorstore statistics"
4533
4653
  }
4534
4654
  });
4535
4655
  }
4536
4656
  });
4537
4657
  }
4538
- fastify.log.info("Platform adapter routes registered");
4539
- }
4540
- function mapPinoLevelToString(level) {
4541
- if (typeof level === "string") {
4542
- return level;
4543
- }
4544
- if (level <= 10) {
4545
- return "trace";
4546
- }
4547
- if (level <= 20) {
4548
- return "debug";
4549
- }
4550
- if (level <= 30) {
4551
- return "info";
4552
- }
4553
- if (level <= 40) {
4554
- return "warn";
4555
- }
4556
- if (level <= 50) {
4557
- return "error";
4558
- }
4559
- return "fatal";
4560
- }
4561
- function toFrontendLogRecord(record) {
4562
- const pinoLevel = record.fields.level;
4563
- const levelStr = typeof pinoLevel === "number" ? mapPinoLevelToString(pinoLevel) : record.level;
4564
- const messageStr = typeof record.message === "string" ? record.message : JSON.stringify(record.message);
4565
- const { level: _level, time: _time, ...restFields } = record.fields;
4566
- return {
4567
- id: record.id,
4568
- // Include ID for navigation to detail page
4569
- time: new Date(record.timestamp).toISOString(),
4570
- level: levelStr,
4571
- msg: messageStr,
4572
- plugin: record.source,
4573
- ...restFields
4574
- };
4575
- }
4576
- function buildLogSummaryPrompt(question, logs, stats, includeContext) {
4577
- let prompt = `You are analyzing application logs. User question: "${question}"
4578
-
4579
- `;
4580
- prompt += `Statistics:
4581
- `;
4582
- prompt += `Total logs: ${stats.total}
4583
- `;
4584
- prompt += `Errors: ${stats.byLevel.error || 0}
4585
- `;
4586
- prompt += `Warnings: ${stats.byLevel.warn || 0}
4587
- `;
4588
- prompt += `Info: ${stats.byLevel.info || 0}
4589
- `;
4590
- if (stats.timeRange.from && stats.timeRange.to) {
4591
- prompt += `Time range: ${stats.timeRange.from} to ${stats.timeRange.to}
4592
- `;
4593
- }
4594
- if (stats.topErrors.length > 0) {
4595
- prompt += `
4596
- Top Errors:
4597
- `;
4598
- stats.topErrors.slice(0, 5).forEach((err, idx) => {
4599
- prompt += `${idx + 1}. "${err.message}" (${err.count} occurrences)
4600
- `;
4601
- });
4602
- }
4603
- const relevantLogs = logs.filter((log) => {
4604
- if (!includeContext.errors && log.level === "error") {
4605
- return false;
4606
- }
4607
- if (!includeContext.warnings && log.level === "warn") {
4608
- return false;
4609
- }
4610
- if (!includeContext.info && log.level === "info") {
4611
- return false;
4612
- }
4613
- return true;
4614
- });
4615
- prompt += `
4616
- Log Entries (${Math.min(relevantLogs.length, 100)} most recent):
4617
- `;
4618
- relevantLogs.slice(-100).forEach((log) => {
4619
- prompt += `[${log.time}] ${log.level.toUpperCase()}`;
4620
- if (includeContext.metadata && log.plugin) {
4621
- prompt += ` [${log.plugin}]`;
4622
- }
4623
- prompt += `: ${log.msg || "(no message)"}
4624
- `;
4625
- if (includeContext.metadata && (log.traceId || log.executionId)) {
4626
- if (log.traceId) {
4627
- prompt += ` traceId: ${log.traceId}
4628
- `;
4629
- }
4630
- if (log.executionId) {
4631
- prompt += ` executionId: ${log.executionId}
4632
- `;
4633
- }
4634
- }
4635
- const logErr = log.err;
4636
- if (includeContext.stackTraces && logErr?.stack) {
4637
- prompt += ` stack: ${logErr.stack.split("\n").slice(0, 5).join("\n ")}
4638
- `;
4639
- }
4640
- });
4641
- prompt += `
4642
- Instructions:
4643
- `;
4644
- prompt += `Provide a clear, concise summary answering the user's question as plain text. Focus on:
4645
- `;
4646
- prompt += `1. What happened (timeline of events)
4647
- `;
4648
- prompt += `2. Root causes if errors are present
4649
- `;
4650
- prompt += `3. Patterns or trends you notice
4651
- `;
4652
- prompt += `4. Actionable recommendations if applicable
4653
- `;
4654
- prompt += `
4655
- Keep the summary under 300 words. Use simple paragraphs separated by double newlines. Do not use markdown formatting (no **, ##, -, or other markdown syntax). Write in clear, professional language.
4656
- `;
4657
- return prompt;
4658
- }
4659
- function extractCorrelationKeys(log) {
4660
- return {
4661
- requestId: log.fields.requestId ?? log.fields.reqId,
4662
- traceId: log.fields.traceId,
4663
- executionId: log.fields.executionId,
4664
- sessionId: log.fields.sessionId
4665
- };
4666
- }
4667
- async function findRelatedLogs(targetLog) {
4668
- const correlationKeys = extractCorrelationKeys(targetLog);
4669
- const timeWindow = 6e4;
4670
- if (correlationKeys.requestId || correlationKeys.traceId || correlationKeys.executionId) {
4671
- const relatedLogs = [];
4672
- const result2 = await platform.logs.query({
4673
- from: targetLog.timestamp - timeWindow,
4674
- to: targetLog.timestamp + timeWindow
4675
- }, {
4676
- limit: 1e3
4677
- });
4678
- for (const log of result2.logs) {
4679
- if (log.id === targetLog.id) {
4680
- continue;
4681
- }
4682
- const logKeys = extractCorrelationKeys(log);
4683
- if (correlationKeys.requestId && logKeys.requestId === correlationKeys.requestId || correlationKeys.traceId && logKeys.traceId === correlationKeys.traceId || correlationKeys.executionId && logKeys.executionId === correlationKeys.executionId || correlationKeys.sessionId && logKeys.sessionId === correlationKeys.sessionId) {
4684
- relatedLogs.push(log);
4658
+ const cacheDailyStatsPaths = resolvePaths(basePath, "/adapters/cache/daily-stats");
4659
+ for (const path3 of cacheDailyStatsPaths) {
4660
+ fastify.get(path3, async (request, reply) => {
4661
+ const analytics = platform.analytics;
4662
+ if (!analytics.getDailyStats) {
4663
+ return reply.code(501).send({
4664
+ ok: false,
4665
+ error: {
4666
+ code: "DAILY_STATS_NOT_IMPLEMENTED",
4667
+ message: "Analytics adapter does not support daily statistics"
4668
+ }
4669
+ });
4685
4670
  }
4686
- }
4687
- if (relatedLogs.length > 0) {
4688
- return relatedLogs.sort((a, b) => a.timestamp - b.timestamp).map(toFrontendLogRecord);
4689
- }
4690
- }
4691
- const result = await platform.logs.query({
4692
- source: targetLog.source,
4693
- from: targetLog.timestamp - timeWindow,
4694
- to: targetLog.timestamp + timeWindow
4695
- }, {
4696
- limit: 50
4697
- });
4698
- return result.logs.filter((log) => log.id !== targetLog.id).sort((a, b) => a.timestamp - b.timestamp).map(toFrontendLogRecord);
4699
- }
4700
- function generateFallbackSummary(stats, logs) {
4701
- let summary = `Log Summary
4702
-
4703
- `;
4704
- summary += `Total Logs: ${stats.total}
4705
-
4706
- `;
4707
- summary += `By Level:
4708
- `;
4709
- Object.entries(stats.byLevel).forEach(([level, count]) => {
4710
- summary += `${level}: ${count}
4711
- `;
4712
- });
4713
- summary += `
4714
- `;
4715
- if (Object.keys(stats.byPlugin).length > 0) {
4716
- summary += `By Plugin:
4717
- `;
4718
- Object.entries(stats.byPlugin).sort((a, b) => b[1] - a[1]).slice(0, 5).forEach(([plugin, count]) => {
4719
- summary += `${plugin}: ${count}
4720
- `;
4721
- });
4722
- summary += `
4723
- `;
4724
- }
4725
- if (stats.topErrors.length > 0) {
4726
- summary += `Top Errors:
4727
- `;
4728
- stats.topErrors.slice(0, 5).forEach((err, idx) => {
4729
- summary += `${idx + 1}. "${err.message}" (${err.count} times)
4730
- `;
4731
- });
4732
- summary += `
4733
- `;
4734
- }
4735
- if (stats.timeRange.from && stats.timeRange.to) {
4736
- summary += `Time Range: ${stats.timeRange.from} to ${stats.timeRange.to}
4737
-
4738
- `;
4739
- }
4740
- summary += `Note: LLM summarization is not available. This is a basic statistical summary.`;
4741
- return summary;
4742
- }
4743
- async function registerLogRoutes(server, config, eventHub) {
4744
- server.get("/api/v1/logs", { schema: { tags: ["Logs"], summary: "Query logs with filters" } }, async (request, reply) => {
4745
- try {
4746
- const limit = request.query.limit ? parseInt(request.query.limit, 10) : 100;
4747
- const offset = request.query.offset ? parseInt(request.query.offset, 10) : 0;
4748
- const query = {
4749
- level: request.query.level,
4750
- source: request.query.plugin,
4751
- from: request.query.from ? new Date(request.query.from).getTime() : void 0,
4752
- to: request.query.to ? new Date(request.query.to).getTime() : void 0
4753
- };
4754
- let result;
4755
- if (request.query.search) {
4756
- result = await platform.logs.search(request.query.search, {
4757
- limit,
4758
- offset
4671
+ try {
4672
+ const dateRange = extractDateRange(request.query);
4673
+ const statsOptions = extractStatsOptions(request.query);
4674
+ const dailyStats = await analytics.getDailyStats({
4675
+ type: ["cache.get.hit", "cache.get.miss", "cache.set.completed"],
4676
+ from: dateRange.from,
4677
+ to: dateRange.to,
4678
+ limit: 5e4,
4679
+ ...statsOptions
4759
4680
  });
4760
- } else {
4761
- result = await platform.logs.query(query, {
4762
- limit,
4763
- offset
4681
+ return reply.send({ ok: true, data: dailyStats });
4682
+ } catch (error) {
4683
+ if (error instanceof Error && error.message.includes("Invalid")) {
4684
+ return reply.code(400).send({
4685
+ ok: false,
4686
+ error: { code: "INVALID_DATE_RANGE", message: error.message }
4687
+ });
4688
+ }
4689
+ return reply.code(500).send({
4690
+ ok: false,
4691
+ error: {
4692
+ code: "DAILY_STATS_ERROR",
4693
+ message: error instanceof Error ? error.message : "Failed to fetch daily statistics"
4694
+ }
4764
4695
  });
4765
4696
  }
4766
- const frontendLogs = result.logs.map(toFrontendLogRecord);
4767
- const stats = await platform.logs.getStats();
4768
- return {
4769
- ok: true,
4770
- data: {
4771
- logs: frontendLogs,
4772
- total: result.total,
4773
- hasMore: result.hasMore,
4774
- filters: request.query,
4775
- source: "source" in result ? result.source : void 0,
4776
- stats: {
4777
- buffer: stats.buffer ? {
4778
- size: stats.buffer.size,
4779
- maxSize: stats.buffer.maxSize,
4780
- oldest: stats.buffer.oldestTimestamp ? new Date(stats.buffer.oldestTimestamp).toISOString() : void 0,
4781
- newest: stats.buffer.newestTimestamp ? new Date(stats.buffer.newestTimestamp).toISOString() : void 0
4782
- } : void 0,
4783
- persistence: stats.persistence ? {
4784
- totalLogs: stats.persistence.totalLogs,
4785
- oldestTimestamp: stats.persistence.oldestTimestamp ? new Date(stats.persistence.oldestTimestamp).toISOString() : void 0,
4786
- newestTimestamp: stats.persistence.newestTimestamp ? new Date(stats.persistence.newestTimestamp).toISOString() : void 0,
4787
- sizeBytes: stats.persistence.sizeBytes
4788
- } : void 0
4697
+ });
4698
+ }
4699
+ const cacheUsagePaths = resolvePaths(basePath, "/adapters/cache/usage");
4700
+ for (const path3 of cacheUsagePaths) {
4701
+ fastify.get(path3, async (request, reply) => {
4702
+ const analytics = platform.analytics;
4703
+ if (!analytics.getEvents) {
4704
+ return reply.code(501).send({
4705
+ ok: false,
4706
+ error: {
4707
+ code: "ANALYTICS_NOT_IMPLEMENTED",
4708
+ message: "Analytics adapter does not support reading events"
4789
4709
  }
4790
- }
4791
- };
4792
- } catch (error) {
4793
- return reply.code(503).send({
4794
- ok: false,
4795
- error: "Log query failed",
4796
- message: error instanceof Error ? error.message : "Unknown error"
4797
- });
4798
- }
4799
- });
4800
- server.get(
4801
- "/api/v1/logs/:id",
4802
- { schema: { tags: ["Logs"], summary: "Get log entry by ID" } },
4803
- async (request, reply) => {
4710
+ });
4711
+ }
4804
4712
  try {
4805
- const log = await platform.logs.getById(request.params.id);
4806
- if (!log) {
4807
- return reply.code(404).send({
4713
+ const dateRange = extractDateRange(request.query);
4714
+ const hitEvents = await fetchAllEventsBatched(
4715
+ analytics,
4716
+ {
4717
+ type: "cache.get.hit",
4718
+ from: dateRange.from,
4719
+ to: dateRange.to
4720
+ },
4721
+ fastify
4722
+ );
4723
+ const missEvents = await fetchAllEventsBatched(
4724
+ analytics,
4725
+ {
4726
+ type: "cache.get.miss",
4727
+ from: dateRange.from,
4728
+ to: dateRange.to
4729
+ },
4730
+ fastify
4731
+ );
4732
+ const setEvents = await fetchAllEventsBatched(
4733
+ analytics,
4734
+ {
4735
+ type: "cache.set.completed",
4736
+ from: dateRange.from,
4737
+ to: dateRange.to
4738
+ },
4739
+ fastify
4740
+ );
4741
+ const totalGets = hitEvents.length + missEvents.length;
4742
+ const hitRate = totalGets > 0 ? hitEvents.length / totalGets * 100 : 0;
4743
+ const stats = {
4744
+ totalGets,
4745
+ hits: hitEvents.length,
4746
+ misses: missEvents.length,
4747
+ hitRate,
4748
+ sets: setEvents.length,
4749
+ avgGetDuration: 0,
4750
+ avgSetDuration: 0
4751
+ };
4752
+ let totalGetDuration = 0;
4753
+ for (const event of [...hitEvents, ...missEvents]) {
4754
+ const props = getEventData(event);
4755
+ totalGetDuration += Number(props.durationMs || 0);
4756
+ }
4757
+ stats.avgGetDuration = totalGets > 0 ? totalGetDuration / totalGets : 0;
4758
+ let totalSetDuration = 0;
4759
+ for (const event of setEvents) {
4760
+ const props = getEventData(event);
4761
+ totalSetDuration += Number(props.durationMs || 0);
4762
+ }
4763
+ stats.avgSetDuration = stats.sets > 0 ? totalSetDuration / stats.sets : 0;
4764
+ return { ok: true, data: stats, meta: { source: "analytics-adapter" } };
4765
+ } catch (error) {
4766
+ if (error instanceof Error && error.message.includes("Invalid")) {
4767
+ return reply.code(400).send({
4808
4768
  ok: false,
4809
- error: "Log not found",
4810
- message: `Log with ID '${request.params.id}' does not exist`
4769
+ error: {
4770
+ code: "INVALID_DATE_RANGE",
4771
+ message: error.message
4772
+ }
4811
4773
  });
4812
4774
  }
4813
- const frontendLog = toFrontendLogRecord(log);
4814
- let relatedLogs = [];
4815
- if (request.query.includeRelated === "true") {
4816
- relatedLogs = await findRelatedLogs(log);
4817
- }
4818
- return {
4819
- ok: true,
4820
- data: {
4821
- log: frontendLog,
4822
- related: relatedLogs.length > 0 ? relatedLogs : void 0
4775
+ platform.logger.error("Failed to fetch Cache usage stats", error instanceof Error ? error : new Error(String(error)));
4776
+ return reply.code(500).send({
4777
+ ok: false,
4778
+ error: {
4779
+ code: "CACHE_USAGE_STATS_ERROR",
4780
+ message: error instanceof Error ? error.message : "Failed to fetch cache statistics"
4823
4781
  }
4824
- };
4782
+ });
4783
+ }
4784
+ });
4785
+ }
4786
+ const storageDailyStatsPaths = resolvePaths(basePath, "/adapters/storage/daily-stats");
4787
+ for (const path3 of storageDailyStatsPaths) {
4788
+ fastify.get(path3, async (request, reply) => {
4789
+ const analytics = platform.analytics;
4790
+ if (!analytics.getDailyStats) {
4791
+ return reply.code(501).send({
4792
+ ok: false,
4793
+ error: {
4794
+ code: "DAILY_STATS_NOT_IMPLEMENTED",
4795
+ message: "Analytics adapter does not support daily statistics"
4796
+ }
4797
+ });
4798
+ }
4799
+ try {
4800
+ const dateRange = extractDateRange(request.query);
4801
+ const statsOptions = extractStatsOptions(request.query);
4802
+ const dailyStats = await analytics.getDailyStats({
4803
+ type: ["storage.read.completed", "storage.write.completed", "storage.delete.completed"],
4804
+ from: dateRange.from,
4805
+ to: dateRange.to,
4806
+ limit: 5e4,
4807
+ ...statsOptions
4808
+ });
4809
+ return reply.send({ ok: true, data: dailyStats });
4825
4810
  } catch (error) {
4811
+ if (error instanceof Error && error.message.includes("Invalid")) {
4812
+ return reply.code(400).send({
4813
+ ok: false,
4814
+ error: { code: "INVALID_DATE_RANGE", message: error.message }
4815
+ });
4816
+ }
4826
4817
  return reply.code(500).send({
4827
4818
  ok: false,
4828
- error: "Failed to fetch log",
4829
- message: error instanceof Error ? error.message : "Unknown error"
4819
+ error: {
4820
+ code: "DAILY_STATS_ERROR",
4821
+ message: error instanceof Error ? error.message : "Failed to fetch daily statistics"
4822
+ }
4823
+ });
4824
+ }
4825
+ });
4826
+ }
4827
+ const storageUsagePaths = resolvePaths(basePath, "/adapters/storage/usage");
4828
+ for (const path3 of storageUsagePaths) {
4829
+ fastify.get(path3, async (request, reply) => {
4830
+ const analytics = platform.analytics;
4831
+ if (!analytics.getEvents) {
4832
+ return reply.code(501).send({
4833
+ ok: false,
4834
+ error: {
4835
+ code: "ANALYTICS_NOT_IMPLEMENTED",
4836
+ message: "Analytics adapter does not support reading events"
4837
+ }
4830
4838
  });
4831
4839
  }
4832
- }
4833
- );
4834
- server.get(
4835
- "/api/v1/logs/:id/related",
4836
- { schema: { tags: ["Logs"], summary: "Get logs related to a specific entry" } },
4837
- async (request, reply) => {
4838
4840
  try {
4839
- const log = await platform.logs.getById(request.params.id);
4840
- if (!log) {
4841
- return reply.code(404).send({
4841
+ const dateRange = extractDateRange(request.query);
4842
+ const readEvents = await fetchAllEventsBatched(
4843
+ analytics,
4844
+ {
4845
+ type: "storage.read.completed",
4846
+ from: dateRange.from,
4847
+ to: dateRange.to
4848
+ },
4849
+ fastify
4850
+ );
4851
+ const writeEvents = await fetchAllEventsBatched(
4852
+ analytics,
4853
+ {
4854
+ type: "storage.write.completed",
4855
+ from: dateRange.from,
4856
+ to: dateRange.to
4857
+ },
4858
+ fastify
4859
+ );
4860
+ const deleteEvents = await fetchAllEventsBatched(
4861
+ analytics,
4862
+ {
4863
+ type: "storage.delete.completed",
4864
+ from: dateRange.from,
4865
+ to: dateRange.to
4866
+ },
4867
+ fastify
4868
+ );
4869
+ const stats = {
4870
+ readOperations: readEvents.length,
4871
+ writeOperations: writeEvents.length,
4872
+ deleteOperations: deleteEvents.length,
4873
+ totalBytesRead: 0,
4874
+ totalBytesWritten: 0,
4875
+ avgReadDuration: 0,
4876
+ avgWriteDuration: 0
4877
+ };
4878
+ let totalReadDuration = 0;
4879
+ for (const event of readEvents) {
4880
+ const props = getEventData(event);
4881
+ stats.totalBytesRead += Number(props.bytesRead || 0);
4882
+ totalReadDuration += Number(props.durationMs || 0);
4883
+ }
4884
+ stats.avgReadDuration = stats.readOperations > 0 ? totalReadDuration / stats.readOperations : 0;
4885
+ let totalWriteDuration = 0;
4886
+ for (const event of writeEvents) {
4887
+ const props = getEventData(event);
4888
+ stats.totalBytesWritten += Number(props.bytesWritten || 0);
4889
+ totalWriteDuration += Number(props.durationMs || 0);
4890
+ }
4891
+ stats.avgWriteDuration = stats.writeOperations > 0 ? totalWriteDuration / stats.writeOperations : 0;
4892
+ return { ok: true, data: stats, meta: { source: "analytics-adapter" } };
4893
+ } catch (error) {
4894
+ if (error instanceof Error && error.message.includes("Invalid")) {
4895
+ return reply.code(400).send({
4842
4896
  ok: false,
4843
- error: "Log not found",
4844
- message: `Log with ID '${request.params.id}' does not exist`
4897
+ error: {
4898
+ code: "INVALID_DATE_RANGE",
4899
+ message: error.message
4900
+ }
4845
4901
  });
4846
4902
  }
4847
- const relatedLogs = await findRelatedLogs(log);
4848
- return {
4849
- ok: true,
4850
- data: {
4851
- total: relatedLogs.length,
4852
- logs: relatedLogs,
4853
- correlationKeys: extractCorrelationKeys(log)
4854
- }
4855
- };
4856
- } catch (error) {
4903
+ platform.logger.error("Failed to fetch Storage usage stats", error instanceof Error ? error : new Error(String(error)));
4857
4904
  return reply.code(500).send({
4858
4905
  ok: false,
4859
- error: "Failed to fetch related logs",
4860
- message: error instanceof Error ? error.message : "Unknown error"
4906
+ error: {
4907
+ code: "STORAGE_USAGE_STATS_ERROR",
4908
+ message: error instanceof Error ? error.message : "Failed to fetch storage statistics"
4909
+ }
4861
4910
  });
4862
4911
  }
4912
+ });
4913
+ }
4914
+ fastify.log.info("Platform adapter routes registered");
4915
+ }
4916
+ function mapPinoLevelToString(level) {
4917
+ if (typeof level === "string") {
4918
+ return level;
4919
+ }
4920
+ if (level <= 10) {
4921
+ return "trace";
4922
+ }
4923
+ if (level <= 20) {
4924
+ return "debug";
4925
+ }
4926
+ if (level <= 30) {
4927
+ return "info";
4928
+ }
4929
+ if (level <= 40) {
4930
+ return "warn";
4931
+ }
4932
+ if (level <= 50) {
4933
+ return "error";
4934
+ }
4935
+ return "fatal";
4936
+ }
4937
+ function toFrontendLogRecord(record) {
4938
+ const pinoLevel = record.fields.level;
4939
+ const levelStr = typeof pinoLevel === "number" ? mapPinoLevelToString(pinoLevel) : record.level;
4940
+ const messageStr = typeof record.message === "string" ? record.message : JSON.stringify(record.message);
4941
+ const { level: _level, time: _time, ...restFields } = record.fields;
4942
+ return {
4943
+ id: record.id,
4944
+ // Include ID for navigation to detail page
4945
+ time: new Date(record.timestamp).toISOString(),
4946
+ level: levelStr,
4947
+ msg: messageStr,
4948
+ ...restFields
4949
+ };
4950
+ }
4951
+ function buildLogSummaryPrompt(question, logs, stats, includeContext) {
4952
+ let prompt = `You are analyzing application logs. User question: "${question}"
4953
+
4954
+ `;
4955
+ prompt += `Statistics:
4956
+ `;
4957
+ prompt += `Total logs: ${stats.total}
4958
+ `;
4959
+ prompt += `Errors: ${stats.byLevel.error || 0}
4960
+ `;
4961
+ prompt += `Warnings: ${stats.byLevel.warn || 0}
4962
+ `;
4963
+ prompt += `Info: ${stats.byLevel.info || 0}
4964
+ `;
4965
+ if (stats.timeRange.from && stats.timeRange.to) {
4966
+ prompt += `Time range: ${stats.timeRange.from} to ${stats.timeRange.to}
4967
+ `;
4968
+ }
4969
+ if (stats.topErrors.length > 0) {
4970
+ prompt += `
4971
+ Top Errors:
4972
+ `;
4973
+ stats.topErrors.slice(0, 5).forEach((err, idx) => {
4974
+ prompt += `${idx + 1}. "${err.message}" (${err.count} occurrences)
4975
+ `;
4976
+ });
4977
+ }
4978
+ const relevantLogs = logs.filter((log) => {
4979
+ if (!includeContext.errors && log.level === "error") {
4980
+ return false;
4863
4981
  }
4864
- );
4865
- server.get("/api/v1/logs/stream", { schema: { hide: true } }, async (request, reply) => {
4866
- const caps = platform.logs.getCapabilities();
4867
- if (!caps.hasStreaming) {
4868
- return reply.code(503).send({
4869
- error: "Log streaming not enabled",
4870
- message: "Logger adapter does not support streaming. Enable logRingBuffer in kb.config.json"
4871
- });
4982
+ if (!includeContext.warnings && log.level === "warn") {
4983
+ return false;
4872
4984
  }
4873
- reply.hijack();
4874
- let streamClosed = false;
4875
- const origin = request.headers.origin;
4876
- if (origin === "http://localhost:3000" || origin === "http://localhost:5173") {
4877
- reply.raw.setHeader("Access-Control-Allow-Origin", origin);
4878
- reply.raw.setHeader("Access-Control-Allow-Credentials", "true");
4879
- } else {
4880
- reply.raw.setHeader("Access-Control-Allow-Origin", "*");
4985
+ if (!includeContext.info && log.level === "info") {
4986
+ return false;
4881
4987
  }
4882
- reply.raw.setHeader("Content-Type", "text/event-stream");
4883
- reply.raw.setHeader("Cache-Control", "no-cache, no-transform");
4884
- reply.raw.setHeader("Connection", "keep-alive");
4885
- try {
4886
- reply.raw.flushHeaders?.();
4887
- reply.raw.write(": connected\n\n");
4888
- } catch (err) {
4889
- streamClosed = true;
4890
- return;
4988
+ return true;
4989
+ });
4990
+ prompt += `
4991
+ Log Entries (${Math.min(relevantLogs.length, 100)} most recent):
4992
+ `;
4993
+ relevantLogs.slice(-100).forEach((log) => {
4994
+ prompt += `[${log.time}] ${log.level.toUpperCase()}`;
4995
+ if (includeContext.metadata && log.plugin) {
4996
+ prompt += ` [${log.plugin}]`;
4891
4997
  }
4892
- const unsubscribe = platform.logs.subscribe((log) => {
4893
- if (!streamClosed && !reply.raw.writableEnded && !reply.raw.destroyed) {
4894
- try {
4895
- const frontendLog = toFrontendLogRecord(log);
4896
- reply.raw.write(`event: log
4897
- `);
4898
- reply.raw.write(`data: ${JSON.stringify(frontendLog)}
4899
-
4900
- `);
4901
- } catch (err) {
4902
- streamClosed = true;
4903
- }
4998
+ prompt += `: ${log.msg || "(no message)"}
4999
+ `;
5000
+ if (includeContext.metadata && (log.traceId || log.executionId)) {
5001
+ if (log.traceId) {
5002
+ prompt += ` traceId: ${log.traceId}
5003
+ `;
4904
5004
  }
4905
- });
4906
- const cleanup = () => {
4907
- if (!streamClosed) {
4908
- streamClosed = true;
4909
- unsubscribe();
4910
- try {
4911
- if (!reply.raw.writableEnded && !reply.raw.destroyed) {
4912
- reply.raw.end();
4913
- }
4914
- } catch (err) {
4915
- }
5005
+ if (log.executionId) {
5006
+ prompt += ` executionId: ${log.executionId}
5007
+ `;
4916
5008
  }
4917
- };
4918
- request.raw.on("close", cleanup);
4919
- request.raw.on("error", cleanup);
4920
- await new Promise(() => {
4921
- });
5009
+ }
5010
+ const logErr = log.err;
5011
+ if (includeContext.stackTraces && logErr?.stack) {
5012
+ prompt += ` stack: ${logErr.stack.split("\n").slice(0, 5).join("\n ")}
5013
+ `;
5014
+ }
4922
5015
  });
4923
- server.get("/api/v1/logs/stats", { schema: { tags: ["Logs"], summary: "Get log storage statistics" } }, async (request, reply) => {
4924
- try {
4925
- const stats = await platform.logs.getStats();
4926
- const caps = platform.logs.getCapabilities();
4927
- return {
4928
- ok: true,
4929
- data: {
4930
- capabilities: caps,
4931
- buffer: stats.buffer ? {
4932
- size: stats.buffer.size,
4933
- maxSize: stats.buffer.maxSize,
4934
- oldest: stats.buffer.oldestTimestamp ? new Date(stats.buffer.oldestTimestamp).toISOString() : void 0,
4935
- newest: stats.buffer.newestTimestamp ? new Date(stats.buffer.newestTimestamp).toISOString() : void 0
4936
- } : void 0,
4937
- persistence: stats.persistence ? {
4938
- totalLogs: stats.persistence.totalLogs,
4939
- oldest: stats.persistence.oldestTimestamp ? new Date(stats.persistence.oldestTimestamp).toISOString() : void 0,
4940
- newest: stats.persistence.newestTimestamp ? new Date(stats.persistence.newestTimestamp).toISOString() : void 0,
4941
- sizeBytes: stats.persistence.sizeBytes
4942
- } : void 0
4943
- }
4944
- };
4945
- } catch (error) {
4946
- return reply.code(500).send({
4947
- ok: false,
4948
- error: "Failed to fetch stats",
4949
- message: error instanceof Error ? error.message : "Unknown error"
4950
- });
5016
+ prompt += `
5017
+ Instructions:
5018
+ `;
5019
+ prompt += `Provide a clear, concise summary answering the user's question as plain text. Focus on:
5020
+ `;
5021
+ prompt += `1. What happened (timeline of events)
5022
+ `;
5023
+ prompt += `2. Root causes if errors are present
5024
+ `;
5025
+ prompt += `3. Patterns or trends you notice
5026
+ `;
5027
+ prompt += `4. Actionable recommendations if applicable
5028
+ `;
5029
+ prompt += `
5030
+ Keep the summary under 300 words. Use simple paragraphs separated by double newlines. Do not use markdown formatting (no **, ##, -, or other markdown syntax). Write in clear, professional language.
5031
+ `;
5032
+ return prompt;
5033
+ }
5034
+ function extractCorrelationKeys(log) {
5035
+ return {
5036
+ requestId: log.fields.requestId ?? log.fields.reqId,
5037
+ traceId: log.fields.traceId,
5038
+ executionId: log.fields.executionId,
5039
+ sessionId: log.fields.sessionId
5040
+ };
5041
+ }
5042
+ async function findRelatedLogs(targetLog) {
5043
+ const correlationKeys = extractCorrelationKeys(targetLog);
5044
+ const timeWindow = 6e4;
5045
+ if (correlationKeys.requestId || correlationKeys.traceId || correlationKeys.executionId) {
5046
+ const relatedLogs = [];
5047
+ const result2 = await platform.logs.query({
5048
+ from: targetLog.timestamp - timeWindow,
5049
+ to: targetLog.timestamp + timeWindow
5050
+ }, {
5051
+ limit: 1e3
5052
+ });
5053
+ for (const log of result2.logs) {
5054
+ if (log.id === targetLog.id) {
5055
+ continue;
5056
+ }
5057
+ const logKeys = extractCorrelationKeys(log);
5058
+ if (correlationKeys.requestId && logKeys.requestId === correlationKeys.requestId || correlationKeys.traceId && logKeys.traceId === correlationKeys.traceId || correlationKeys.executionId && logKeys.executionId === correlationKeys.executionId || correlationKeys.sessionId && logKeys.sessionId === correlationKeys.sessionId) {
5059
+ relatedLogs.push(log);
5060
+ }
5061
+ }
5062
+ if (relatedLogs.length > 0) {
5063
+ return relatedLogs.sort((a, b) => a.timestamp - b.timestamp).map(toFrontendLogRecord);
4951
5064
  }
5065
+ }
5066
+ const result = await platform.logs.query({
5067
+ source: targetLog.source,
5068
+ from: targetLog.timestamp - timeWindow,
5069
+ to: targetLog.timestamp + timeWindow
5070
+ }, {
5071
+ limit: 50
4952
5072
  });
4953
- server.post("/api/v1/logs/summarize", { schema: { tags: ["Logs"], summary: "AI-powered log summarization" } }, async (request, reply) => {
4954
- const { timeRange, filters, groupBy, question, includeContext } = request.body;
5073
+ return result.logs.filter((log) => log.id !== targetLog.id).sort((a, b) => a.timestamp - b.timestamp).map(toFrontendLogRecord);
5074
+ }
5075
+ function generateFallbackSummary(stats, logs) {
5076
+ let summary = `Log Summary
5077
+
5078
+ `;
5079
+ summary += `Total Logs: ${stats.total}
5080
+
5081
+ `;
5082
+ summary += `By Level:
5083
+ `;
5084
+ Object.entries(stats.byLevel).forEach(([level, count]) => {
5085
+ summary += `${level}: ${count}
5086
+ `;
5087
+ });
5088
+ summary += `
5089
+ `;
5090
+ if (Object.keys(stats.byPlugin).length > 0) {
5091
+ summary += `By Plugin:
5092
+ `;
5093
+ Object.entries(stats.byPlugin).sort((a, b) => b[1] - a[1]).slice(0, 5).forEach(([plugin, count]) => {
5094
+ summary += `${plugin}: ${count}
5095
+ `;
5096
+ });
5097
+ summary += `
5098
+ `;
5099
+ }
5100
+ if (stats.topErrors.length > 0) {
5101
+ summary += `Top Errors:
5102
+ `;
5103
+ stats.topErrors.slice(0, 5).forEach((err, idx) => {
5104
+ summary += `${idx + 1}. "${err.message}" (${err.count} times)
5105
+ `;
5106
+ });
5107
+ summary += `
5108
+ `;
5109
+ }
5110
+ if (stats.timeRange.from && stats.timeRange.to) {
5111
+ summary += `Time Range: ${stats.timeRange.from} to ${stats.timeRange.to}
5112
+
5113
+ `;
5114
+ }
5115
+ summary += `Note: LLM summarization is not available. This is a basic statistical summary.`;
5116
+ return summary;
5117
+ }
5118
+ async function registerLogRoutes(server, config, eventHub) {
5119
+ server.get("/api/v1/logs", { schema: { tags: ["Logs"], summary: "Query logs with filters" } }, async (request, reply) => {
4955
5120
  try {
5121
+ const limit = request.query.limit ? parseInt(request.query.limit, 10) : 100;
5122
+ const offset = request.query.offset ? parseInt(request.query.offset, 10) : 0;
4956
5123
  const query = {
4957
- level: filters?.level,
4958
- source: filters?.plugin,
4959
- from: timeRange?.from ? new Date(timeRange.from).getTime() : void 0,
4960
- to: timeRange?.to ? new Date(timeRange.to).getTime() : void 0
4961
- };
4962
- const result = await platform.logs.query(query, { limit: 1e3 });
4963
- const frontendLogs = result.logs.map(toFrontendLogRecord);
4964
- let filteredLogs = frontendLogs;
4965
- if (filters?.traceId) {
4966
- filteredLogs = filteredLogs.filter((log) => log.traceId === filters.traceId);
4967
- }
4968
- if (filters?.executionId) {
4969
- filteredLogs = filteredLogs.filter((log) => log.executionId === filters.executionId);
4970
- }
4971
- const stats = {
4972
- total: filteredLogs.length,
4973
- byLevel: {},
4974
- byPlugin: {},
4975
- topErrors: [],
4976
- timeRange: {
4977
- from: filteredLogs.length > 0 ? filteredLogs[0].time : null,
4978
- to: filteredLogs.length > 0 ? filteredLogs[filteredLogs.length - 1].time : null
4979
- }
5124
+ level: request.query.level,
5125
+ source: request.query.plugin,
5126
+ from: request.query.from ? new Date(request.query.from).getTime() : void 0,
5127
+ to: request.query.to ? new Date(request.query.to).getTime() : void 0
4980
5128
  };
4981
- const errorMessages = /* @__PURE__ */ new Map();
4982
- for (const log of filteredLogs) {
4983
- stats.byLevel[log.level] = (stats.byLevel[log.level] || 0) + 1;
4984
- if (log.plugin) {
4985
- stats.byPlugin[log.plugin] = (stats.byPlugin[log.plugin] || 0) + 1;
4986
- }
4987
- if (log.level === "error" && log.msg) {
4988
- errorMessages.set(log.msg, (errorMessages.get(log.msg) || 0) + 1);
4989
- }
4990
- }
4991
- stats.topErrors = Array.from(errorMessages.entries()).map(([message2, count]) => ({ message: message2, count })).sort((a, b) => b.count - a.count).slice(0, 10);
4992
- let groups = null;
4993
- if (groupBy) {
4994
- const fieldMap = {
4995
- trace: "traceId",
4996
- execution: "executionId",
4997
- plugin: "plugin"
4998
- };
4999
- const groupMap = /* @__PURE__ */ new Map();
5000
- for (const log of filteredLogs) {
5001
- const fieldName = fieldMap[groupBy] || groupBy;
5002
- const key = String(log[fieldName] || "unknown");
5003
- if (!groupMap.has(key)) {
5004
- groupMap.set(key, []);
5005
- }
5006
- groupMap.get(key).push(log);
5007
- }
5008
- groups = Object.fromEntries(groupMap.entries());
5009
- }
5010
- let aiSummary = null;
5011
- let message = null;
5012
- if (platform.llm && question) {
5013
- try {
5014
- const context = includeContext || {
5015
- errors: true,
5016
- warnings: true,
5017
- info: false,
5018
- metadata: true,
5019
- stackTraces: true
5020
- };
5021
- const prompt = buildLogSummaryPrompt(question, filteredLogs, stats, context);
5022
- const response = await platform.llm.complete(prompt, {
5023
- temperature: 0.7,
5024
- maxTokens: 1e3,
5025
- systemPrompt: "You are a technical log analysis assistant. Provide clear, actionable insights based on application logs."
5026
- });
5027
- aiSummary = response.content.trim();
5028
- } catch (error) {
5029
- platform.logger.warn("LLM summarization failed, using fallback", {
5030
- error: error instanceof Error ? error.message : String(error)
5031
- });
5032
- aiSummary = generateFallbackSummary(stats, filteredLogs);
5033
- message = "AI summarization unavailable, showing statistical summary";
5034
- }
5129
+ let result;
5130
+ if (request.query.search) {
5131
+ result = await platform.logs.search(request.query.search, {
5132
+ limit,
5133
+ offset
5134
+ });
5035
5135
  } else {
5036
- aiSummary = generateFallbackSummary(stats, filteredLogs);
5037
- message = platform.llm ? "No question provided" : "LLM not configured";
5136
+ result = await platform.logs.query(query, {
5137
+ limit,
5138
+ offset
5139
+ });
5038
5140
  }
5141
+ const frontendLogs = result.logs.map(toFrontendLogRecord);
5142
+ const stats = await platform.logs.getStats();
5039
5143
  return {
5040
5144
  ok: true,
5041
5145
  data: {
5042
- summary: {
5043
- question: question || "General log summary",
5044
- timeRange: stats.timeRange,
5045
- total: stats.total,
5046
- stats,
5047
- groups
5048
- },
5049
- aiSummary,
5050
- message
5146
+ logs: frontendLogs,
5147
+ total: result.total,
5148
+ hasMore: result.hasMore,
5149
+ filters: request.query,
5150
+ source: "source" in result ? result.source : void 0,
5151
+ stats: {
5152
+ buffer: stats.buffer ? {
5153
+ size: stats.buffer.size,
5154
+ maxSize: stats.buffer.maxSize,
5155
+ oldest: stats.buffer.oldestTimestamp ? new Date(stats.buffer.oldestTimestamp).toISOString() : void 0,
5156
+ newest: stats.buffer.newestTimestamp ? new Date(stats.buffer.newestTimestamp).toISOString() : void 0
5157
+ } : void 0,
5158
+ persistence: stats.persistence ? {
5159
+ totalLogs: stats.persistence.totalLogs,
5160
+ oldestTimestamp: stats.persistence.oldestTimestamp ? new Date(stats.persistence.oldestTimestamp).toISOString() : void 0,
5161
+ newestTimestamp: stats.persistence.newestTimestamp ? new Date(stats.persistence.newestTimestamp).toISOString() : void 0,
5162
+ sizeBytes: stats.persistence.sizeBytes
5163
+ } : void 0
5164
+ }
5051
5165
  }
5052
5166
  };
5053
5167
  } catch (error) {
5054
- return reply.code(500).send({
5168
+ return reply.code(503).send({
5055
5169
  ok: false,
5056
- error: "Log summarization failed",
5170
+ error: "Log query failed",
5057
5171
  message: error instanceof Error ? error.message : "Unknown error"
5058
5172
  });
5059
5173
  }
5060
5174
  });
5061
- }
5062
- var SENSITIVE_KEYS = [
5063
- "apiKey",
5064
- "secret",
5065
- "password",
5066
- "token",
5067
- "key",
5068
- "credentials",
5069
- "auth"
5070
- ];
5071
- function redactSensitiveData(obj, redacted = [], path3 = "") {
5072
- const result = {};
5073
- for (const [key, value] of Object.entries(obj)) {
5074
- const currentPath = path3 ? `${path3}.${key}` : key;
5075
- const lowerKey = key.toLowerCase();
5076
- const isSensitive = SENSITIVE_KEYS.some((pattern) => lowerKey.includes(pattern.toLowerCase()));
5077
- if (isSensitive && typeof value === "string") {
5078
- result[key] = "***REDACTED***";
5079
- redacted.push(currentPath);
5080
- } else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
5081
- result[key] = redactSensitiveData(value, redacted, currentPath);
5082
- } else {
5083
- result[key] = value;
5084
- }
5085
- }
5086
- return result;
5087
- }
5088
- async function loadPlatformConfig(repoRoot) {
5089
- try {
5090
- const configResult = await readKbConfig(repoRoot);
5091
- if (!configResult || !configResult.data) {
5092
- return {
5093
- adapters: {},
5094
- adapterOptions: {},
5095
- execution: { mode: "in-process" }
5096
- };
5097
- }
5098
- const config = configResult.data;
5099
- const configPlatform = typeof config.platform === "object" && config.platform !== null ? config.platform : {};
5100
- return {
5101
- adapters: configPlatform.adapters ?? {},
5102
- adapterOptions: configPlatform.adapterOptions ?? {},
5103
- execution: configPlatform.execution ?? { mode: "in-process" }
5104
- };
5105
- } catch (error) {
5106
- return {
5107
- adapters: {},
5108
- adapterOptions: {},
5109
- execution: { mode: "in-process" }
5110
- };
5111
- }
5112
- }
5113
- async function registerPlatformRoutes(fastify, config, repoRoot) {
5114
- const basePath = normalizeBasePath(config.basePath);
5115
- const configPaths = resolvePaths(basePath, "/platform/config");
5116
- for (const path3 of configPaths) {
5117
- fastify.get(path3, async (_request, reply) => {
5118
- try {
5119
- const { adapters, adapterOptions, execution } = await restDomainOperationMetrics.observeOperation(
5120
- "platform.config.get",
5121
- () => loadPlatformConfig(repoRoot)
5122
- );
5123
- const redacted = [];
5124
- const sanitizedOptions = redactSensitiveData(adapterOptions, redacted);
5125
- const payload = {
5126
- schema: "kb.platform.config/1",
5127
- ts: (/* @__PURE__ */ new Date()).toISOString(),
5128
- adapters,
5129
- adapterOptions: sanitizedOptions,
5130
- execution,
5131
- redacted
5132
- };
5133
- const response = {
5175
+ server.get(
5176
+ "/api/v1/logs/:id",
5177
+ { schema: { tags: ["Logs"], summary: "Get log entry by ID" } },
5178
+ async (request, reply) => {
5179
+ try {
5180
+ const log = await platform.logs.getById(request.params.id);
5181
+ if (!log) {
5182
+ return reply.code(404).send({
5183
+ ok: false,
5184
+ error: "Log not found",
5185
+ message: `Log with ID '${request.params.id}' does not exist`
5186
+ });
5187
+ }
5188
+ const frontendLog = toFrontendLogRecord(log);
5189
+ let relatedLogs = [];
5190
+ if (request.query.includeRelated === "true") {
5191
+ relatedLogs = await findRelatedLogs(log);
5192
+ }
5193
+ return {
5134
5194
  ok: true,
5135
- data: payload
5195
+ data: {
5196
+ log: frontendLog,
5197
+ related: relatedLogs.length > 0 ? relatedLogs : void 0
5198
+ }
5136
5199
  };
5137
- return reply.send(response);
5138
5200
  } catch (error) {
5139
- platform.logger.error("Failed to load platform config", error instanceof Error ? error : new Error(String(error)));
5140
5201
  return reply.code(500).send({
5141
5202
  ok: false,
5142
- error: {
5143
- code: "INTERNAL_SERVER_ERROR",
5144
- message: "Failed to load platform configuration"
5145
- }
5203
+ error: "Failed to fetch log",
5204
+ message: error instanceof Error ? error.message : "Unknown error"
5146
5205
  });
5147
5206
  }
5148
- });
5149
- }
5150
- }
5151
- var AdapterRegistry = class {
5152
- methods = /* @__PURE__ */ new Map();
5153
- register(adapter, method, entry) {
5154
- this.methods.set(`${adapter}.${method}`, entry);
5155
- }
5156
- get(adapter, method) {
5157
- return this.methods.get(`${adapter}.${method}`);
5158
- }
5159
- has(adapter, method) {
5160
- return this.methods.has(`${adapter}.${method}`);
5161
- }
5162
- };
5163
- var AdapterCallRequestSchema = z.object({
5164
- requestId: z.string(),
5165
- adapter: AdapterNameSchema,
5166
- method: z.string(),
5167
- args: z.array(z.unknown()),
5168
- context: z.object({
5169
- namespaceId: z.string(),
5170
- hostId: z.string(),
5171
- workspaceId: z.string().optional(),
5172
- environmentId: z.string().optional(),
5173
- executionRequestId: z.string().optional(),
5174
- userId: z.string().optional(),
5175
- sessionId: z.string().optional()
5176
- })
5177
- });
5178
- function createAdapterRegistry() {
5179
- const registry = new AdapterRegistry();
5180
- registry.register("llm", "complete", {
5181
- inputSchema: z.array(z.unknown()),
5182
- outputSchema: z.unknown(),
5183
- execute: async (args) => {
5184
- const llm = platform.getAdapter("llm");
5185
- if (!llm) {
5186
- throw new Error("LLM adapter not available");
5187
- }
5188
- return llm.complete(...args);
5189
5207
  }
5190
- });
5191
- registry.register("cache", "get", {
5192
- inputSchema: z.array(z.unknown()),
5193
- outputSchema: z.unknown().nullable(),
5194
- execute: async (args) => platform.cache.get(args[0])
5195
- });
5196
- registry.register("cache", "set", {
5197
- inputSchema: z.array(z.unknown()),
5198
- outputSchema: z.void(),
5199
- execute: async (args) => platform.cache.set(args[0], args[1], args[2])
5200
- });
5201
- registry.register("cache", "delete", {
5202
- inputSchema: z.array(z.unknown()),
5203
- outputSchema: z.void(),
5204
- execute: async (args) => platform.cache.delete(args[0])
5205
- });
5206
- registry.register("vectorStore", "search", {
5207
- inputSchema: z.array(z.unknown()),
5208
- outputSchema: z.unknown(),
5209
- execute: async (args) => {
5210
- const vs = platform.getAdapter("vectorStore");
5211
- if (!vs) {
5212
- throw new Error("VectorStore adapter not available");
5208
+ );
5209
+ server.get(
5210
+ "/api/v1/logs/:id/related",
5211
+ { schema: { tags: ["Logs"], summary: "Get logs related to a specific entry" } },
5212
+ async (request, reply) => {
5213
+ try {
5214
+ const log = await platform.logs.getById(request.params.id);
5215
+ if (!log) {
5216
+ return reply.code(404).send({
5217
+ ok: false,
5218
+ error: "Log not found",
5219
+ message: `Log with ID '${request.params.id}' does not exist`
5220
+ });
5221
+ }
5222
+ const relatedLogs = await findRelatedLogs(log);
5223
+ return {
5224
+ ok: true,
5225
+ data: {
5226
+ total: relatedLogs.length,
5227
+ logs: relatedLogs,
5228
+ correlationKeys: extractCorrelationKeys(log)
5229
+ }
5230
+ };
5231
+ } catch (error) {
5232
+ return reply.code(500).send({
5233
+ ok: false,
5234
+ error: "Failed to fetch related logs",
5235
+ message: error instanceof Error ? error.message : "Unknown error"
5236
+ });
5213
5237
  }
5214
- return vs.search(...args);
5215
5238
  }
5216
- });
5217
- registry.register("embeddings", "embed", {
5218
- inputSchema: z.array(z.unknown()),
5219
- outputSchema: z.unknown(),
5220
- execute: async (args) => {
5221
- const emb = platform.getAdapter("embeddings");
5222
- if (!emb) {
5223
- throw new Error("Embeddings adapter not available");
5224
- }
5225
- return emb.embed(...args);
5239
+ );
5240
+ server.get("/api/v1/logs/stream", { schema: { hide: true } }, async (request, reply) => {
5241
+ const caps = platform.logs.getCapabilities();
5242
+ if (!caps.hasStreaming) {
5243
+ return reply.code(503).send({
5244
+ error: "Log streaming not enabled",
5245
+ message: "Logger adapter does not support streaming. Enable logRingBuffer in kb.config.json"
5246
+ });
5226
5247
  }
5227
- });
5228
- registry.register("storage", "read", {
5229
- inputSchema: z.array(z.unknown()),
5230
- outputSchema: z.unknown(),
5231
- execute: async (args) => {
5232
- const storage = platform.getAdapter("storage");
5233
- if (!storage) {
5234
- throw new Error("Storage adapter not available");
5235
- }
5236
- return storage.read(...args);
5248
+ reply.hijack();
5249
+ let streamClosed = false;
5250
+ const origin = request.headers.origin;
5251
+ if (origin === "http://localhost:3000" || origin === "http://localhost:5173") {
5252
+ reply.raw.setHeader("Access-Control-Allow-Origin", origin);
5253
+ reply.raw.setHeader("Access-Control-Allow-Credentials", "true");
5254
+ } else {
5255
+ reply.raw.setHeader("Access-Control-Allow-Origin", "*");
5237
5256
  }
5238
- });
5239
- registry.register("storage", "write", {
5240
- inputSchema: z.array(z.unknown()),
5241
- outputSchema: z.void(),
5242
- execute: async (args) => {
5243
- const storage = platform.getAdapter("storage");
5244
- if (!storage) {
5245
- throw new Error("Storage adapter not available");
5246
- }
5247
- return storage.write(...args);
5257
+ reply.raw.setHeader("Content-Type", "text/event-stream");
5258
+ reply.raw.setHeader("Cache-Control", "no-cache, no-transform");
5259
+ reply.raw.setHeader("Connection", "keep-alive");
5260
+ try {
5261
+ reply.raw.flushHeaders?.();
5262
+ reply.raw.write(": connected\n\n");
5263
+ } catch (err) {
5264
+ streamClosed = true;
5265
+ return;
5248
5266
  }
5267
+ const unsubscribe = platform.logs.subscribe((log) => {
5268
+ if (!streamClosed && !reply.raw.writableEnded && !reply.raw.destroyed) {
5269
+ try {
5270
+ const frontendLog = toFrontendLogRecord(log);
5271
+ reply.raw.write(`event: log
5272
+ `);
5273
+ reply.raw.write(`data: ${JSON.stringify(frontendLog)}
5274
+
5275
+ `);
5276
+ } catch (err) {
5277
+ streamClosed = true;
5278
+ }
5279
+ }
5280
+ });
5281
+ const cleanup = () => {
5282
+ if (!streamClosed) {
5283
+ streamClosed = true;
5284
+ unsubscribe();
5285
+ try {
5286
+ if (!reply.raw.writableEnded && !reply.raw.destroyed) {
5287
+ reply.raw.end();
5288
+ }
5289
+ } catch (err) {
5290
+ }
5291
+ }
5292
+ };
5293
+ request.raw.on("close", cleanup);
5294
+ request.raw.on("error", cleanup);
5295
+ await new Promise(() => {
5296
+ });
5249
5297
  });
5250
- return registry;
5251
- }
5252
- async function registerAdapterCallRoutes(server) {
5253
- const registry = createAdapterRegistry();
5254
- const internalSecret = process.env.GATEWAY_INTERNAL_SECRET;
5255
- const logger = platform.logger.child({ layer: "rest", route: "adapter-call" });
5256
- server.post("/api/v1/internal/adapter-call", async (request, reply) => {
5257
- const provided = request.headers["x-internal-secret"];
5258
- if (!internalSecret || provided !== internalSecret) {
5259
- return reply.code(403).send({ ok: false, error: { code: "FORBIDDEN", message: "Invalid internal secret", retryable: false } });
5260
- }
5261
- const parsed = AdapterCallRequestSchema.safeParse(request.body);
5262
- if (!parsed.success) {
5263
- return reply.code(400).send({ ok: false, error: { code: "VALIDATION_ERROR", message: parsed.error.message, retryable: false } });
5264
- }
5265
- const { requestId, adapter, method, args, context } = parsed.data;
5266
- const startMs = Date.now();
5267
- const entry = registry.get(adapter, method);
5268
- if (!entry) {
5269
- logger.warn("Adapter call rejected", { requestId, adapter, method, hostId: context.hostId });
5270
- return reply.code(403).send({ ok: false, error: { code: "ADAPTER_CALL_REJECTED", message: `Method not allowed: ${adapter}.${method}`, retryable: false } });
5271
- }
5298
+ server.get("/api/v1/logs/stats", { schema: { tags: ["Logs"], summary: "Get log storage statistics" } }, async (request, reply) => {
5272
5299
  try {
5273
- const operation = `adapter.call.${adapter}.${method}`;
5274
- const result = await restDomainOperationMetrics.observeOperation(
5275
- operation,
5276
- async () => entry.execute(args, context)
5277
- );
5278
- const latencyMs = Date.now() - startMs;
5279
- logger.info("Adapter call success", { requestId, adapter, method, hostId: context.hostId, latencyMs });
5280
- return { ok: true, result };
5281
- } catch (err) {
5282
- const latencyMs = Date.now() - startMs;
5283
- const message = err instanceof Error ? err.message : String(err);
5284
- logger.error("Adapter call failed", err instanceof Error ? err : void 0, { requestId, adapter, method, hostId: context.hostId, latencyMs });
5300
+ const stats = await platform.logs.getStats();
5301
+ const caps = platform.logs.getCapabilities();
5302
+ return {
5303
+ ok: true,
5304
+ data: {
5305
+ capabilities: caps,
5306
+ buffer: stats.buffer ? {
5307
+ size: stats.buffer.size,
5308
+ maxSize: stats.buffer.maxSize,
5309
+ oldest: stats.buffer.oldestTimestamp ? new Date(stats.buffer.oldestTimestamp).toISOString() : void 0,
5310
+ newest: stats.buffer.newestTimestamp ? new Date(stats.buffer.newestTimestamp).toISOString() : void 0
5311
+ } : void 0,
5312
+ persistence: stats.persistence ? {
5313
+ totalLogs: stats.persistence.totalLogs,
5314
+ oldest: stats.persistence.oldestTimestamp ? new Date(stats.persistence.oldestTimestamp).toISOString() : void 0,
5315
+ newest: stats.persistence.newestTimestamp ? new Date(stats.persistence.newestTimestamp).toISOString() : void 0,
5316
+ sizeBytes: stats.persistence.sizeBytes
5317
+ } : void 0
5318
+ }
5319
+ };
5320
+ } catch (error) {
5285
5321
  return reply.code(500).send({
5286
5322
  ok: false,
5287
- error: { code: "ADAPTER_ERROR", message, retryable: false }
5323
+ error: "Failed to fetch stats",
5324
+ message: error instanceof Error ? error.message : "Unknown error"
5288
5325
  });
5289
5326
  }
5290
5327
  });
5291
- }
5292
-
5293
- // src/routes/debug-routes.ts
5294
- var collectedRoutes = [];
5295
- var seenRoutes = /* @__PURE__ */ new Set();
5296
- function collectRoute(routeOptions) {
5297
- const methods = Array.isArray(routeOptions.method) ? routeOptions.method : [routeOptions.method];
5298
- for (const method of methods) {
5299
- if (method === "HEAD") {
5300
- continue;
5301
- }
5302
- const url = routeOptions.url;
5303
- const key = `${method}:${url}`;
5304
- if (!seenRoutes.has(key)) {
5305
- seenRoutes.add(key);
5306
- collectedRoutes.push({ method, url });
5307
- }
5308
- }
5309
- }
5310
- function registerRouteCollector(fastify) {
5311
- fastify.addHook("onRoute", collectRoute);
5312
- }
5313
- async function registerDebugRoutes(fastify, config) {
5314
- const basePath = normalizeBasePath(config.basePath);
5315
- const routesPaths = resolvePaths(basePath, "/routes");
5316
- for (const path3 of routesPaths) {
5317
- fastify.get(path3, async (_request, reply) => {
5318
- let routes = collectedRoutes.filter((r) => r.url.startsWith("/api/"));
5319
- if (routes.length === 0) {
5320
- routes = [...collectedRoutes];
5328
+ server.post("/api/v1/logs/summarize", { schema: { tags: ["Logs"], summary: "AI-powered log summarization" } }, async (request, reply) => {
5329
+ const { timeRange, filters, groupBy, question, includeContext } = request.body;
5330
+ try {
5331
+ const query = {
5332
+ level: filters?.level,
5333
+ source: filters?.plugin,
5334
+ from: timeRange?.from ? new Date(timeRange.from).getTime() : void 0,
5335
+ to: timeRange?.to ? new Date(timeRange.to).getTime() : void 0
5336
+ };
5337
+ const result = await platform.logs.query(query, { limit: 1e3 });
5338
+ const frontendLogs = result.logs.map(toFrontendLogRecord);
5339
+ let filteredLogs = frontendLogs;
5340
+ if (filters?.traceId) {
5341
+ filteredLogs = filteredLogs.filter((log) => log.traceId === filters.traceId);
5321
5342
  }
5322
- routes.sort((a, b) => {
5323
- const urlCompare = a.url.localeCompare(b.url);
5324
- if (urlCompare !== 0) {
5325
- return urlCompare;
5343
+ if (filters?.executionId) {
5344
+ filteredLogs = filteredLogs.filter((log) => log.executionId === filters.executionId);
5345
+ }
5346
+ const stats = {
5347
+ total: filteredLogs.length,
5348
+ byLevel: {},
5349
+ byPlugin: {},
5350
+ topErrors: [],
5351
+ timeRange: {
5352
+ from: filteredLogs.length > 0 ? filteredLogs[0].time : null,
5353
+ to: filteredLogs.length > 0 ? filteredLogs[filteredLogs.length - 1].time : null
5354
+ }
5355
+ };
5356
+ const errorMessages = /* @__PURE__ */ new Map();
5357
+ for (const log of filteredLogs) {
5358
+ stats.byLevel[log.level] = (stats.byLevel[log.level] || 0) + 1;
5359
+ if (log.plugin) {
5360
+ stats.byPlugin[log.plugin] = (stats.byPlugin[log.plugin] || 0) + 1;
5361
+ }
5362
+ if (log.level === "error" && log.msg) {
5363
+ errorMessages.set(log.msg, (errorMessages.get(log.msg) || 0) + 1);
5326
5364
  }
5327
- return a.method.localeCompare(b.method);
5328
- });
5329
- let raw = null;
5330
- try {
5331
- raw = fastify.printRoutes({ commonPrefix: false });
5332
- } catch {
5333
5365
  }
5334
- return reply.send({
5335
- schema: "kb.routes/1",
5336
- ts: (/* @__PURE__ */ new Date()).toISOString(),
5337
- count: routes.length,
5338
- routes,
5339
- raw
5340
- });
5341
- });
5342
- }
5343
- }
5344
-
5345
- // src/services/historical-metrics.ts
5346
- var DEFAULT_CONFIG = {
5347
- intervalMs: 5e3,
5348
- maxPoints: {
5349
- "1m": 12,
5350
- "5m": 60,
5351
- "10m": 120,
5352
- "30m": 360,
5353
- "1h": 720
5354
- },
5355
- debug: false
5356
- };
5357
- var HistoricalMetricsCollector = class {
5358
- cache;
5359
- config;
5360
- intervalHandle = null;
5361
- logger;
5362
- startTimeMs = Date.now();
5363
- constructor(cache, config = {}, logger = console) {
5364
- this.cache = cache;
5365
- this.config = { ...DEFAULT_CONFIG, ...config, maxPoints: { ...DEFAULT_CONFIG.maxPoints, ...config.maxPoints } };
5366
- this.logger = logger;
5367
- }
5368
- /**
5369
- * Start background collection
5370
- */
5371
- start() {
5372
- if (this.intervalHandle) {
5373
- this.log("warn", "Historical metrics collector already started");
5374
- return;
5375
- }
5376
- this.log("info", "Starting historical metrics collector", {
5377
- intervalMs: this.config.intervalMs
5378
- });
5379
- this.collect().catch((err) => {
5380
- this.log("error", "Failed to collect initial metrics", { err });
5381
- });
5382
- this.intervalHandle = setInterval(() => {
5383
- this.collect().catch((err) => {
5384
- this.log("error", "Failed to collect metrics", { err });
5385
- });
5386
- }, this.config.intervalMs);
5387
- }
5388
- /**
5389
- * Stop background collection
5390
- */
5391
- stop() {
5392
- if (this.intervalHandle) {
5393
- clearInterval(this.intervalHandle);
5394
- this.intervalHandle = null;
5395
- this.log("info", "Historical metrics collector stopped");
5396
- }
5397
- }
5398
- /**
5399
- * Collect current metrics snapshot and store in cache
5400
- */
5401
- async collect() {
5402
- const now = Date.now();
5403
- const metrics = metricsCollector.getMetrics();
5404
- const snapshot = {
5405
- timestamp: now,
5406
- requests: {
5407
- total: metrics.requests.total,
5408
- success: metrics.requests.success ?? 0,
5409
- clientErrors: metrics.requests.clientErrors ?? 0,
5410
- serverErrors: metrics.requests.serverErrors ?? 0
5411
- },
5412
- latency: {
5413
- average: metrics.latency.average,
5414
- min: metrics.latency.min === Infinity ? 0 : metrics.latency.min,
5415
- max: metrics.latency.max
5416
- },
5417
- uptime: (now - metrics.timestamps.startTime) / 1e3,
5418
- perPlugin: metrics.perPlugin.map((p) => ({
5419
- pluginId: p.pluginId,
5420
- requests: p.total,
5421
- errors: Object.values(p.statuses).filter((_, idx) => Object.keys(p.statuses)[idx]?.startsWith("4") || Object.keys(p.statuses)[idx]?.startsWith("5")).reduce((sum, count) => sum + count, 0),
5422
- avgLatency: p.total > 0 ? p.totalDuration / p.total : 0
5423
- }))
5424
- };
5425
- await Promise.all([
5426
- this.appendToTimeSeries("1m", snapshot, 2 * 60 * 1e3),
5427
- // TTL: 2 minutes
5428
- this.appendToTimeSeries("5m", snapshot, 10 * 60 * 1e3),
5429
- // TTL: 10 minutes
5430
- this.appendToTimeSeries("10m", snapshot, 20 * 60 * 1e3),
5431
- // TTL: 20 minutes
5432
- this.appendToTimeSeries("30m", snapshot, 60 * 60 * 1e3),
5433
- // TTL: 1 hour
5434
- this.appendToTimeSeries("1h", snapshot, 2 * 60 * 60 * 1e3)
5435
- // TTL: 2 hours
5436
- ]);
5437
- if (now % 6e4 < this.config.intervalMs) {
5438
- await this.updateHeatmapAggregation(snapshot).catch((err) => {
5439
- this.log("error", "Failed to update heatmap", { err });
5440
- });
5441
- }
5442
- this.log("debug", "Metrics snapshot collected", {
5443
- timestamp: new Date(now).toISOString(),
5444
- requests: snapshot.requests.total,
5445
- latency: snapshot.latency.average.toFixed(2)
5446
- });
5447
- }
5448
- /**
5449
- * Append snapshot to time series bucket
5450
- */
5451
- async appendToTimeSeries(range, snapshot, ttlMs) {
5452
- const key = `metrics:history:${range}`;
5453
- const maxPoints = this.config.maxPoints[range] ?? 120;
5454
- let timeSeries = await this.cache.get(key);
5455
- if (!timeSeries || !Array.isArray(timeSeries)) {
5456
- timeSeries = [];
5457
- }
5458
- timeSeries.push(snapshot);
5459
- if (timeSeries.length > maxPoints) {
5460
- timeSeries = timeSeries.slice(timeSeries.length - maxPoints);
5461
- }
5462
- await this.cache.set(key, timeSeries, ttlMs);
5463
- }
5464
- /**
5465
- * Update heatmap aggregation for weekly patterns
5466
- */
5467
- async updateHeatmapAggregation(snapshot) {
5468
- const key = "metrics:heatmap:7d";
5469
- const ttlMs = 24 * 60 * 60 * 1e3;
5470
- let heatmapData = await this.cache.get(key);
5471
- if (!heatmapData || typeof heatmapData !== "object") {
5472
- heatmapData = { latency: [], errors: [], requests: [] };
5473
- }
5474
- const date = new Date(snapshot.timestamp);
5475
- const day = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][date.getDay()];
5476
- const hour = date.getHours();
5477
- for (const metricType of ["latency", "errors", "requests"]) {
5478
- let cells = heatmapData[metricType] || [];
5479
- const cellIndex = cells.findIndex((c) => c.day === day && c.hour === hour);
5480
- let value = 0;
5481
- if (metricType === "latency") {
5482
- value = snapshot.latency.average;
5483
- } else if (metricType === "errors") {
5484
- value = snapshot.requests.clientErrors + snapshot.requests.serverErrors;
5485
- } else {
5486
- value = snapshot.requests.total;
5366
+ stats.topErrors = Array.from(errorMessages.entries()).map(([message2, count]) => ({ message: message2, count })).sort((a, b) => b.count - a.count).slice(0, 10);
5367
+ let groups = null;
5368
+ if (groupBy) {
5369
+ const fieldMap = {
5370
+ trace: "traceId",
5371
+ execution: "executionId",
5372
+ plugin: "plugin"
5373
+ };
5374
+ const groupMap = /* @__PURE__ */ new Map();
5375
+ for (const log of filteredLogs) {
5376
+ const fieldName = fieldMap[groupBy] || groupBy;
5377
+ const key = String(log[fieldName] || "unknown");
5378
+ if (!groupMap.has(key)) {
5379
+ groupMap.set(key, []);
5380
+ }
5381
+ groupMap.get(key).push(log);
5382
+ }
5383
+ groups = Object.fromEntries(groupMap.entries());
5487
5384
  }
5488
- if (cellIndex >= 0 && cells[cellIndex]) {
5489
- cells[cellIndex].value = cells[cellIndex].value * 0.9 + value * 0.1;
5385
+ let aiSummary = null;
5386
+ let message = null;
5387
+ if (platform.llm && question) {
5388
+ try {
5389
+ const context = includeContext || {
5390
+ errors: true,
5391
+ warnings: true,
5392
+ info: false,
5393
+ metadata: true,
5394
+ stackTraces: true
5395
+ };
5396
+ const prompt = buildLogSummaryPrompt(question, filteredLogs, stats, context);
5397
+ const response = await platform.llm.complete(prompt, {
5398
+ temperature: 0.7,
5399
+ maxTokens: 1e3,
5400
+ systemPrompt: "You are a technical log analysis assistant. Provide clear, actionable insights based on application logs."
5401
+ });
5402
+ aiSummary = response.content.trim();
5403
+ } catch (error) {
5404
+ platform.logger.warn("LLM summarization failed, using fallback", {
5405
+ error: error instanceof Error ? error.message : String(error)
5406
+ });
5407
+ aiSummary = generateFallbackSummary(stats, filteredLogs);
5408
+ message = "AI summarization unavailable, showing statistical summary";
5409
+ }
5490
5410
  } else {
5491
- cells.push({ day, hour, value });
5492
- }
5493
- if (cells.length > 168) {
5494
- cells = cells.slice(cells.length - 168);
5411
+ aiSummary = generateFallbackSummary(stats, filteredLogs);
5412
+ message = platform.llm ? "No question provided" : "LLM not configured";
5495
5413
  }
5496
- heatmapData[metricType] = cells;
5414
+ return {
5415
+ ok: true,
5416
+ data: {
5417
+ summary: {
5418
+ question: question || "General log summary",
5419
+ timeRange: stats.timeRange,
5420
+ total: stats.total,
5421
+ stats,
5422
+ groups
5423
+ },
5424
+ aiSummary,
5425
+ message
5426
+ }
5427
+ };
5428
+ } catch (error) {
5429
+ return reply.code(500).send({
5430
+ ok: false,
5431
+ error: "Log summarization failed",
5432
+ message: error instanceof Error ? error.message : "Unknown error"
5433
+ });
5497
5434
  }
5498
- await this.cache.set(key, heatmapData, ttlMs);
5499
- }
5500
- /**
5501
- * Query historical time-series data
5502
- */
5503
- async queryHistory(params) {
5504
- const key = `metrics:history:${params.range}`;
5505
- const timeSeries = await this.cache.get(key);
5506
- if (!timeSeries || !Array.isArray(timeSeries)) {
5507
- return [];
5435
+ });
5436
+ }
5437
+ var SENSITIVE_KEYS = [
5438
+ "apiKey",
5439
+ "secret",
5440
+ "password",
5441
+ "token",
5442
+ "key",
5443
+ "credentials",
5444
+ "auth"
5445
+ ];
5446
+ function redactSensitiveData(obj, redacted = [], path3 = "") {
5447
+ const result = {};
5448
+ for (const [key, value] of Object.entries(obj)) {
5449
+ const currentPath = path3 ? `${path3}.${key}` : key;
5450
+ const lowerKey = key.toLowerCase();
5451
+ const isSensitive = SENSITIVE_KEYS.some((pattern) => lowerKey.includes(pattern.toLowerCase()));
5452
+ if (isSensitive && typeof value === "string") {
5453
+ result[key] = "***REDACTED***";
5454
+ redacted.push(currentPath);
5455
+ } else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
5456
+ result[key] = redactSensitiveData(value, redacted, currentPath);
5457
+ } else {
5458
+ result[key] = value;
5508
5459
  }
5509
- const dataPoints = timeSeries.map((snapshot) => {
5510
- let value = 0;
5511
- switch (params.metric) {
5512
- case "requests":
5513
- value = snapshot.requests.total;
5514
- break;
5515
- case "errors":
5516
- value = snapshot.requests.clientErrors + snapshot.requests.serverErrors;
5517
- break;
5518
- case "latency":
5519
- value = snapshot.latency.average;
5520
- break;
5521
- case "uptime":
5522
- value = snapshot.uptime;
5523
- break;
5524
- }
5460
+ }
5461
+ return result;
5462
+ }
5463
+ async function loadPlatformConfig(repoRoot) {
5464
+ try {
5465
+ const configResult = await readKbConfig(repoRoot);
5466
+ if (!configResult || !configResult.data) {
5525
5467
  return {
5526
- timestamp: snapshot.timestamp,
5527
- value
5468
+ adapters: {},
5469
+ adapterOptions: {},
5470
+ execution: { mode: "in-process" }
5528
5471
  };
5529
- });
5530
- if (params.interval && params.interval !== "5s") {
5531
- return this.aggregateByInterval(dataPoints, params.interval);
5532
5472
  }
5533
- return dataPoints;
5473
+ const config = configResult.data;
5474
+ const configPlatform = typeof config.platform === "object" && config.platform !== null ? config.platform : {};
5475
+ return {
5476
+ adapters: configPlatform.adapters ?? {},
5477
+ adapterOptions: configPlatform.adapterOptions ?? {},
5478
+ execution: configPlatform.execution ?? { mode: "in-process" }
5479
+ };
5480
+ } catch (error) {
5481
+ return {
5482
+ adapters: {},
5483
+ adapterOptions: {},
5484
+ execution: { mode: "in-process" }
5485
+ };
5534
5486
  }
5535
- /**
5536
- * Query heatmap data
5537
- */
5538
- async queryHeatmap(params) {
5539
- const key = "metrics:heatmap:7d";
5540
- const heatmapData = await this.cache.get(key);
5541
- if (!heatmapData || typeof heatmapData !== "object") {
5542
- return this.generateEmptyHeatmap();
5543
- }
5544
- const cells = heatmapData[params.metric] || [];
5545
- if (cells.length === 0) {
5546
- return this.generateEmptyHeatmap();
5547
- }
5548
- return this.fillHeatmapGaps(cells);
5487
+ }
5488
+ async function registerPlatformRoutes(fastify, config, repoRoot) {
5489
+ const basePath = normalizeBasePath(config.basePath);
5490
+ const configPaths = resolvePaths(basePath, "/platform/config");
5491
+ for (const path3 of configPaths) {
5492
+ fastify.get(path3, async (_request, reply) => {
5493
+ try {
5494
+ const { adapters, adapterOptions, execution } = await restDomainOperationMetrics.observeOperation(
5495
+ "platform.config.get",
5496
+ () => loadPlatformConfig(repoRoot)
5497
+ );
5498
+ const redacted = [];
5499
+ const sanitizedOptions = redactSensitiveData(adapterOptions, redacted);
5500
+ const payload = {
5501
+ schema: "kb.platform.config/1",
5502
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5503
+ adapters,
5504
+ adapterOptions: sanitizedOptions,
5505
+ execution,
5506
+ redacted
5507
+ };
5508
+ const response = {
5509
+ ok: true,
5510
+ data: payload
5511
+ };
5512
+ return reply.send(response);
5513
+ } catch (error) {
5514
+ platform.logger.error("Failed to load platform config", error instanceof Error ? error : new Error(String(error)));
5515
+ return reply.code(500).send({
5516
+ ok: false,
5517
+ error: {
5518
+ code: "INTERNAL_SERVER_ERROR",
5519
+ message: "Failed to load platform configuration"
5520
+ }
5521
+ });
5522
+ }
5523
+ });
5549
5524
  }
5550
- /**
5551
- * Generate empty heatmap structure (7 days × 24 hours)
5552
- */
5553
- generateEmptyHeatmap() {
5554
- const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
5555
- const cells = [];
5556
- for (const day of days) {
5557
- for (let hour = 0; hour < 24; hour++) {
5558
- cells.push({ day, hour, value: 0 });
5525
+ }
5526
+ var AdapterRegistry = class {
5527
+ methods = /* @__PURE__ */ new Map();
5528
+ register(adapter, method, entry) {
5529
+ this.methods.set(`${adapter}.${method}`, entry);
5530
+ }
5531
+ get(adapter, method) {
5532
+ return this.methods.get(`${adapter}.${method}`);
5533
+ }
5534
+ has(adapter, method) {
5535
+ return this.methods.has(`${adapter}.${method}`);
5536
+ }
5537
+ };
5538
+ var AdapterCallRequestSchema = z.object({
5539
+ requestId: z.string(),
5540
+ adapter: AdapterNameSchema,
5541
+ method: z.string(),
5542
+ args: z.array(z.unknown()),
5543
+ context: z.object({
5544
+ namespaceId: z.string(),
5545
+ hostId: z.string(),
5546
+ workspaceId: z.string().optional(),
5547
+ environmentId: z.string().optional(),
5548
+ executionRequestId: z.string().optional(),
5549
+ userId: z.string().optional(),
5550
+ sessionId: z.string().optional()
5551
+ })
5552
+ });
5553
+ function createAdapterRegistry() {
5554
+ const registry = new AdapterRegistry();
5555
+ registry.register("llm", "complete", {
5556
+ inputSchema: z.array(z.unknown()),
5557
+ outputSchema: z.unknown(),
5558
+ execute: async (args) => {
5559
+ const llm = platform.getAdapter("llm");
5560
+ if (!llm) {
5561
+ throw new Error("LLM adapter not available");
5562
+ }
5563
+ return llm.complete(...args);
5564
+ }
5565
+ });
5566
+ registry.register("cache", "get", {
5567
+ inputSchema: z.array(z.unknown()),
5568
+ outputSchema: z.unknown().nullable(),
5569
+ execute: async (args) => platform.cache.get(args[0])
5570
+ });
5571
+ registry.register("cache", "set", {
5572
+ inputSchema: z.array(z.unknown()),
5573
+ outputSchema: z.void(),
5574
+ execute: async (args) => platform.cache.set(args[0], args[1], args[2])
5575
+ });
5576
+ registry.register("cache", "delete", {
5577
+ inputSchema: z.array(z.unknown()),
5578
+ outputSchema: z.void(),
5579
+ execute: async (args) => platform.cache.delete(args[0])
5580
+ });
5581
+ registry.register("vectorStore", "search", {
5582
+ inputSchema: z.array(z.unknown()),
5583
+ outputSchema: z.unknown(),
5584
+ execute: async (args) => {
5585
+ const vs = platform.getAdapter("vectorStore");
5586
+ if (!vs) {
5587
+ throw new Error("VectorStore adapter not available");
5559
5588
  }
5589
+ return vs.search(...args);
5560
5590
  }
5561
- return cells;
5562
- }
5563
- /**
5564
- * Fill gaps in heatmap data (missing day/hour combinations)
5565
- */
5566
- fillHeatmapGaps(cells) {
5567
- const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
5568
- const cellMap = /* @__PURE__ */ new Map();
5569
- for (const cell of cells) {
5570
- const key = `${cell.day}:${cell.hour}`;
5571
- cellMap.set(key, cell.value);
5572
- }
5573
- const complete = [];
5574
- for (const day of days) {
5575
- for (let hour = 0; hour < 24; hour++) {
5576
- const key = `${day}:${hour}`;
5577
- complete.push({
5578
- day,
5579
- hour,
5580
- value: cellMap.get(key) ?? 0
5581
- });
5591
+ });
5592
+ registry.register("embeddings", "embed", {
5593
+ inputSchema: z.array(z.unknown()),
5594
+ outputSchema: z.unknown(),
5595
+ execute: async (args) => {
5596
+ const emb = platform.getAdapter("embeddings");
5597
+ if (!emb) {
5598
+ throw new Error("Embeddings adapter not available");
5582
5599
  }
5600
+ return emb.embed(...args);
5583
5601
  }
5584
- return complete;
5585
- }
5586
- /**
5587
- * Aggregate data points by interval
5588
- */
5589
- aggregateByInterval(dataPoints, interval) {
5590
- if (dataPoints.length === 0) {
5591
- return [];
5602
+ });
5603
+ registry.register("storage", "read", {
5604
+ inputSchema: z.array(z.unknown()),
5605
+ outputSchema: z.unknown(),
5606
+ execute: async (args) => {
5607
+ const storage = platform.getAdapter("storage");
5608
+ if (!storage) {
5609
+ throw new Error("Storage adapter not available");
5610
+ }
5611
+ return storage.read(...args);
5592
5612
  }
5593
- const intervalMs = interval === "1m" ? 60 * 1e3 : 5 * 60 * 1e3;
5594
- const aggregated = [];
5595
- let bucket = [];
5596
- let bucketStart = Math.floor(dataPoints[0].timestamp / intervalMs) * intervalMs;
5597
- for (const point of dataPoints) {
5598
- const pointBucket = Math.floor(point.timestamp / intervalMs) * intervalMs;
5599
- if (pointBucket === bucketStart) {
5600
- bucket.push(point);
5601
- } else {
5602
- if (bucket.length > 0) {
5603
- const avgValue = bucket.reduce((sum, p) => sum + p.value, 0) / bucket.length;
5604
- aggregated.push({
5605
- timestamp: bucketStart + intervalMs / 2,
5606
- // midpoint
5607
- value: avgValue
5608
- });
5609
- }
5610
- bucket = [point];
5611
- bucketStart = pointBucket;
5613
+ });
5614
+ registry.register("storage", "write", {
5615
+ inputSchema: z.array(z.unknown()),
5616
+ outputSchema: z.void(),
5617
+ execute: async (args) => {
5618
+ const storage = platform.getAdapter("storage");
5619
+ if (!storage) {
5620
+ throw new Error("Storage adapter not available");
5612
5621
  }
5622
+ return storage.write(...args);
5613
5623
  }
5614
- if (bucket.length > 0) {
5615
- const avgValue = bucket.reduce((sum, p) => sum + p.value, 0) / bucket.length;
5616
- aggregated.push({
5617
- timestamp: bucketStart + intervalMs / 2,
5618
- value: avgValue
5619
- });
5624
+ });
5625
+ return registry;
5626
+ }
5627
+ async function registerAdapterCallRoutes(server) {
5628
+ const registry = createAdapterRegistry();
5629
+ const internalSecret = process.env.GATEWAY_INTERNAL_SECRET;
5630
+ const logger = platform.logger.child({ layer: "rest", route: "adapter-call" });
5631
+ server.post("/api/v1/internal/adapter-call", async (request, reply) => {
5632
+ const provided = request.headers["x-internal-secret"];
5633
+ if (!internalSecret || provided !== internalSecret) {
5634
+ return reply.code(403).send({ ok: false, error: { code: "FORBIDDEN", message: "Invalid internal secret", retryable: false } });
5620
5635
  }
5621
- return aggregated;
5622
- }
5623
- /**
5624
- * Get collector statistics
5625
- */
5626
- async getStats() {
5627
- const stats = {
5628
- running: this.intervalHandle !== null,
5629
- uptimeSeconds: (Date.now() - this.startTimeMs) / 1e3,
5630
- timeSeries: {},
5631
- heatmap: { cells: 0, metrics: [] }
5632
- };
5633
- for (const range of ["1m", "5m", "10m", "30m", "1h"]) {
5634
- const key = `metrics:history:${range}`;
5635
- const timeSeries = await this.cache.get(key);
5636
- if (timeSeries && Array.isArray(timeSeries)) {
5637
- stats.timeSeries[range] = {
5638
- points: timeSeries.length,
5639
- oldestTimestamp: timeSeries[0]?.timestamp ?? null,
5640
- newestTimestamp: timeSeries[timeSeries.length - 1]?.timestamp ?? null
5641
- };
5642
- } else {
5643
- stats.timeSeries[range] = { points: 0, oldestTimestamp: null, newestTimestamp: null };
5644
- }
5636
+ const parsed = AdapterCallRequestSchema.safeParse(request.body);
5637
+ if (!parsed.success) {
5638
+ return reply.code(400).send({ ok: false, error: { code: "VALIDATION_ERROR", message: parsed.error.message, retryable: false } });
5645
5639
  }
5646
- const heatmapData = await this.cache.get("metrics:heatmap:7d");
5647
- if (heatmapData && typeof heatmapData === "object") {
5648
- const metrics = Object.keys(heatmapData);
5649
- const totalCells = metrics.reduce((sum, m) => sum + (heatmapData[m]?.length ?? 0), 0);
5650
- stats.heatmap = { cells: totalCells, metrics };
5640
+ const { requestId, adapter, method, args, context } = parsed.data;
5641
+ const startMs = Date.now();
5642
+ const entry = registry.get(adapter, method);
5643
+ if (!entry) {
5644
+ logger.warn("Adapter call rejected", { requestId, adapter, method, hostId: context.hostId });
5645
+ return reply.code(403).send({ ok: false, error: { code: "ADAPTER_CALL_REJECTED", message: `Method not allowed: ${adapter}.${method}`, retryable: false } });
5651
5646
  }
5652
- return stats;
5653
- }
5654
- log(level, message, meta) {
5655
- if (level === "debug" && !this.config.debug) {
5656
- return;
5647
+ try {
5648
+ const operation = `adapter.call.${adapter}.${method}`;
5649
+ const result = await restDomainOperationMetrics.observeOperation(
5650
+ operation,
5651
+ async () => entry.execute(args, context)
5652
+ );
5653
+ const latencyMs = Date.now() - startMs;
5654
+ logger.info("Adapter call success", { requestId, adapter, method, hostId: context.hostId, latencyMs });
5655
+ return { ok: true, result };
5656
+ } catch (err) {
5657
+ const latencyMs = Date.now() - startMs;
5658
+ const message = err instanceof Error ? err.message : String(err);
5659
+ logger.error("Adapter call failed", err instanceof Error ? err : void 0, { requestId, adapter, method, hostId: context.hostId, latencyMs });
5660
+ return reply.code(500).send({
5661
+ ok: false,
5662
+ error: { code: "ADAPTER_ERROR", message, retryable: false }
5663
+ });
5657
5664
  }
5658
- if (this.logger[level]) {
5659
- this.logger[level](`[HistoricalMetrics] ${message}`, meta);
5660
- } else {
5661
- console.log(`[HistoricalMetrics] [${level}] ${message}`, meta);
5665
+ });
5666
+ }
5667
+
5668
+ // src/routes/debug.ts
5669
+ var collectedRoutes = [];
5670
+ var seenRoutes = /* @__PURE__ */ new Set();
5671
+ function collectRoute(routeOptions) {
5672
+ const methods = Array.isArray(routeOptions.method) ? routeOptions.method : [routeOptions.method];
5673
+ for (const method of methods) {
5674
+ if (method === "HEAD") {
5675
+ continue;
5676
+ }
5677
+ const url = routeOptions.url;
5678
+ const key = `${method}:${url}`;
5679
+ if (!seenRoutes.has(key)) {
5680
+ seenRoutes.add(key);
5681
+ collectedRoutes.push({ method, url });
5662
5682
  }
5663
5683
  }
5664
- };
5684
+ }
5685
+ function registerRouteCollector(fastify) {
5686
+ fastify.addHook("onRoute", collectRoute);
5687
+ }
5688
+ async function registerDebugRoutes(fastify, config) {
5689
+ const basePath = normalizeBasePath(config.basePath);
5690
+ const routesPaths = resolvePaths(basePath, "/routes");
5691
+ for (const path3 of routesPaths) {
5692
+ fastify.get(path3, async (_request, reply) => {
5693
+ let routes = collectedRoutes.filter((r) => r.url.startsWith("/api/"));
5694
+ if (routes.length === 0) {
5695
+ routes = [...collectedRoutes];
5696
+ }
5697
+ routes.sort((a, b) => {
5698
+ const urlCompare = a.url.localeCompare(b.url);
5699
+ if (urlCompare !== 0) {
5700
+ return urlCompare;
5701
+ }
5702
+ return a.method.localeCompare(b.method);
5703
+ });
5704
+ let raw = null;
5705
+ try {
5706
+ raw = fastify.printRoutes({ commonPrefix: false });
5707
+ } catch {
5708
+ }
5709
+ return reply.send({
5710
+ schema: "kb.routes/1",
5711
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5712
+ count: routes.length,
5713
+ routes,
5714
+ raw
5715
+ });
5716
+ });
5717
+ }
5718
+ }
5665
5719
 
5666
5720
  // src/routes/index.ts
5667
5721
  function normalizeBasePath2(basePath) {
@@ -5908,6 +5962,10 @@ async function registerPlugins(server, config) {
5908
5962
  const rateLimitOpts = {
5909
5963
  max: config.rateLimit.max,
5910
5964
  timeWindow: config.rateLimit.timeWindow,
5965
+ allowList: (req) => {
5966
+ const url = (req.url ?? "").split("?")[0] ?? "";
5967
+ return url === "/health" || url === "/api/v1/health" || url === "/api/v1/ready" || url === "/api/v1/observability/health" || url === "/api/v1/studio/registry" || url === "/api/v1/logs/stream" || url.startsWith("/plugins/");
5968
+ },
5911
5969
  addHeaders: {
5912
5970
  "x-ratelimit-limit": true,
5913
5971
  "x-ratelimit-remaining": true,
@@ -5925,6 +5983,9 @@ async function registerPlugins(server, config) {
5925
5983
  ui: corsProfile !== "prod"
5926
5984
  });
5927
5985
  }
5986
+ function isPluginErrorEnvelope(err) {
5987
+ return err !== null && typeof err === "object" && "status" in err && err.status === "error" && "meta" in err && typeof err.meta === "object" && "pluginId" in err.meta;
5988
+ }
5928
5989
  function registerEnvelopeMiddleware(server, config) {
5929
5990
  server.addHook("onSend", async (request, reply, payload) => {
5930
5991
  try {
@@ -5937,7 +5998,14 @@ function registerEnvelopeMiddleware(server, config) {
5937
5998
  return payload;
5938
5999
  }
5939
6000
  if (typeof payload !== "string") {
5940
- return payload;
6001
+ if (payload === null || payload === void 0 || Buffer.isBuffer(payload) || typeof payload.pipe === "function" || typeof payload.getReader === "function") {
6002
+ return payload;
6003
+ }
6004
+ try {
6005
+ return JSON.stringify(payload);
6006
+ } catch {
6007
+ return payload;
6008
+ }
5941
6009
  }
5942
6010
  let parsedPayload;
5943
6011
  try {
@@ -5975,36 +6043,51 @@ function registerEnvelopeMiddleware(server, config) {
5975
6043
  return JSON.stringify(envelope);
5976
6044
  });
5977
6045
  server.setErrorHandler(async (error, request, reply) => {
5978
- const statusCode = error.statusCode || 500;
5979
- const errorCode = error.code || "E_INTERNAL";
5980
- const message = error.message || "Internal server error";
5981
- const details = error.details || {};
5982
- const cause = error.cause;
5983
- const traceId = error.traceId;
6046
+ let statusCode;
6047
+ let errorCode;
6048
+ let message;
6049
+ let details;
6050
+ let cause;
6051
+ let traceId;
6052
+ if (isPluginErrorEnvelope(error)) {
6053
+ statusCode = error.http || 500;
6054
+ errorCode = error.code || "E_PLUGIN";
6055
+ message = error.message || "Plugin error";
6056
+ details = {
6057
+ ...error.details ?? {},
6058
+ pluginId: error.meta.pluginId,
6059
+ pluginVersion: error.meta.pluginVersion,
6060
+ routeOrCommand: error.meta.routeOrCommand,
6061
+ ...error.trace ? { trace: error.trace } : {}
6062
+ };
6063
+ cause = void 0;
6064
+ traceId = void 0;
6065
+ } else {
6066
+ const err = error;
6067
+ statusCode = err.statusCode || 500;
6068
+ errorCode = err.code || "E_INTERNAL";
6069
+ message = err.message || "Internal server error";
6070
+ details = err.details || {};
6071
+ cause = err.cause;
6072
+ traceId = err.traceId;
6073
+ }
5984
6074
  reply.errorCode = errorCode;
5985
6075
  if (request.kbLogger) {
5986
- request.kbLogger.error("Request error", error, {
6076
+ request.kbLogger.error("Request error", error instanceof Error ? error : new Error(String(error)), {
5987
6077
  errorCode,
5988
6078
  statusCode
5989
6079
  });
5990
6080
  }
5991
6081
  const errorEnvelope = errorEnvelopeSchema.parse({
5992
6082
  ok: false,
5993
- error: {
5994
- code: errorCode,
5995
- message,
5996
- details,
5997
- cause,
5998
- traceId
5999
- },
6083
+ error: { code: errorCode, message, details, cause, traceId },
6000
6084
  meta: {
6001
6085
  requestId: request.id,
6002
6086
  durationMs: reply.elapsedTime || 0,
6003
6087
  apiVersion: config.apiVersion
6004
6088
  }
6005
6089
  });
6006
- reply.status(statusCode);
6007
- return errorEnvelope;
6090
+ reply.status(statusCode).send(errorEnvelope);
6008
6091
  });
6009
6092
  }
6010
6093
  function registerRequestIdMiddleware(server) {
@@ -6096,46 +6179,18 @@ function registerCacheMiddleware(server) {
6096
6179
  return payload;
6097
6180
  });
6098
6181
  }
6099
- function registerErrorGuard(server) {
6100
- server.setErrorHandler((error, request, reply) => {
6101
- if (error && typeof error === "object" && "status" in error && error.status === "error" && "meta" in error && error.meta && typeof error.meta === "object" && "pluginId" in error.meta) {
6102
- const envelope2 = error;
6103
- reply.status(envelope2.http).send(envelope2);
6104
- return;
6105
- }
6106
- const requestId = request.id || "unknown";
6107
- const err = error instanceof Error ? error : new Error(String(error));
6108
- const errWithStatus = error;
6109
- const envelope = {
6110
- status: "error",
6111
- http: errWithStatus.statusCode || 500,
6112
- code: ErrorCode.INTERNAL,
6113
- message: err.message || "Internal server error",
6114
- details: {
6115
- error: err.message || String(error)
6116
- },
6117
- trace: err.stack,
6118
- meta: {
6119
- requestId,
6120
- pluginId: "system",
6121
- pluginVersion: "unknown",
6122
- routeOrCommand: request.url || "unknown",
6123
- timeMs: 0
6124
- }
6125
- };
6126
- reply.status(envelope.http).send(envelope);
6127
- });
6182
+ function registerProcessErrorGuard(server) {
6128
6183
  process.on("uncaughtException", (error) => {
6129
- platform.logger.fatal("Uncaught exception", error, { source: "error-guard" });
6184
+ platform.logger.fatal("Uncaught exception", error, { source: "process-error-guard" });
6130
6185
  });
6131
6186
  process.on("unhandledRejection", (reason) => {
6132
6187
  const error = reason instanceof Error ? reason : new Error(String(reason));
6133
- platform.logger.error("Unhandled rejection", error, { source: "error-guard" });
6188
+ platform.logger.error("Unhandled rejection", error, { source: "process-error-guard" });
6134
6189
  });
6135
6190
  }
6136
6191
 
6137
- // src/middleware/startup-guard.ts
6138
- function registerStartupGuard(server, config) {
6192
+ // src/middleware/startup-throttle.ts
6193
+ function registerStartupThrottle(server, config) {
6139
6194
  const startupConfig = config.startup;
6140
6195
  if (!startupConfig) {
6141
6196
  return;
@@ -6242,8 +6297,6 @@ function registerRequestTimeoutGuard(server, config) {
6242
6297
  done();
6243
6298
  });
6244
6299
  }
6245
-
6246
- // src/middleware/rate-limit.ts
6247
6300
  function extractTenantId(request) {
6248
6301
  return request.headers["x-tenant-id"] || process.env.KB_TENANT_ID || "default";
6249
6302
  }
@@ -6268,8 +6321,6 @@ function createRateLimitMiddleware(rateLimiter) {
6268
6321
  request.tenantId = tenantId;
6269
6322
  };
6270
6323
  }
6271
-
6272
- // src/middleware/tenant-rate-limit.ts
6273
6324
  function registerTenantRateLimitMiddleware(server, cache) {
6274
6325
  const limiter = new TenantRateLimiter(cache);
6275
6326
  limiter.setTier("default", getDefaultTenantTier());
@@ -6279,16 +6330,16 @@ function registerTenantRateLimitMiddleware(server, cache) {
6279
6330
 
6280
6331
  // src/middleware/index.ts
6281
6332
  function registerMiddleware(server, config) {
6282
- registerStartupGuard(server, config);
6333
+ registerStartupThrottle(server, config);
6283
6334
  registerSecurityHeadersMiddleware(server);
6284
6335
  registerRequestIdMiddleware(server);
6285
6336
  registerTenantRateLimitMiddleware(server, platform.cache);
6286
6337
  registerMockModeMiddleware(server, config);
6287
- registerCacheMiddleware(server);
6288
6338
  registerRequestTimeoutGuard(server, config);
6289
6339
  registerMetricsMiddleware(server);
6290
6340
  registerEnvelopeMiddleware(server, config);
6291
- registerErrorGuard(server);
6341
+ registerCacheMiddleware(server);
6342
+ registerProcessErrorGuard();
6292
6343
  }
6293
6344
  var kDisableRequestLogging = /* @__PURE__ */ Symbol.for("fastify.disableRequestLogging");
6294
6345
  async function createServer(config, repoRoot, registry) {
@@ -6375,72 +6426,25 @@ async function createServer(config, repoRoot, registry) {
6375
6426
  var registryInstance = null;
6376
6427
  var metricsCollector2 = null;
6377
6428
  async function findMonorepoRoot(startDir) {
6378
- let dir = path2.resolve(startDir);
6379
- let monorepoRoot = null;
6380
- while (true) {
6381
- try {
6382
- const workspacePath = path2.join(dir, "pnpm-workspace.yaml");
6383
- await promises.access(workspacePath);
6384
- const content = await promises.readFile(workspacePath, "utf-8");
6385
- if (content.includes("kb-*")) {
6386
- monorepoRoot = dir;
6387
- break;
6388
- }
6389
- } catch {
6390
- }
6391
- const parent = path2.dirname(dir);
6392
- if (parent === dir) {
6393
- break;
6394
- }
6395
- dir = parent;
6396
- }
6397
- if (monorepoRoot) {
6398
- return monorepoRoot;
6399
- }
6400
- dir = path2.resolve(startDir);
6401
- let foundRoot = null;
6402
- while (true) {
6403
- try {
6404
- const hasGit = await promises.access(path2.join(dir, ".git")).then(() => true).catch(() => false);
6405
- const hasWorkspace = await promises.access(path2.join(dir, "pnpm-workspace.yaml")).then(() => true).catch(() => false);
6406
- if (hasGit && hasWorkspace) {
6407
- foundRoot = dir;
6408
- }
6409
- } catch {
6410
- }
6411
- const parent = path2.dirname(dir);
6412
- if (parent === dir) {
6413
- break;
6414
- }
6415
- dir = parent;
6416
- }
6417
- if (foundRoot) {
6418
- return foundRoot;
6419
- }
6420
- dir = path2.resolve(startDir);
6421
- let topmostWorkspace = null;
6429
+ let dir = path.resolve(startDir);
6422
6430
  while (true) {
6423
- try {
6424
- await promises.access(path2.join(dir, "pnpm-workspace.yaml"));
6425
- topmostWorkspace = dir;
6426
- } catch {
6431
+ const hasWorkspace = await promises.access(path.join(dir, "pnpm-workspace.yaml")).then(() => true).catch(() => false);
6432
+ if (hasWorkspace) {
6433
+ return dir;
6427
6434
  }
6428
- const parent = path2.dirname(dir);
6435
+ const parent = path.dirname(dir);
6429
6436
  if (parent === dir) {
6430
6437
  break;
6431
6438
  }
6432
6439
  dir = parent;
6433
6440
  }
6434
- if (topmostWorkspace) {
6435
- return topmostWorkspace;
6436
- }
6437
6441
  return findRepoRoot(startDir);
6438
6442
  }
6439
6443
  async function bootstrap(cwd = process.cwd()) {
6440
6444
  const repoRoot = await findMonorepoRoot(cwd);
6441
6445
  loadEnvFromRoot(repoRoot);
6442
6446
  const { config, diagnostics } = await loadRestApiConfig(cwd);
6443
- await createServiceBootstrap({ appId: "rest-api", repoRoot });
6447
+ await createServiceBootstrap({ appId: "rest-api", repoRoot, assemblyHook: makeAssemblyHook() });
6444
6448
  const startupRequestId = `rest-startup-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
6445
6449
  const startupTraceId = randomUUID();
6446
6450
  const startupSpanId = randomUUID();
@@ -6468,10 +6472,11 @@ async function bootstrap(cwd = process.cwd()) {
6468
6472
  bootstrapLogger.info("Initializing entity registry");
6469
6473
  const isDevelopment = process.env.NODE_ENV !== "production";
6470
6474
  const snapshotTTL = isDevelopment ? 10 * 60 * 1e3 : 60 * 60 * 1e3;
6471
- const registryRoot = getPlatformRoot() ?? repoRoot;
6475
+ const platformRoot = getPlatformRoot();
6472
6476
  const registryInitStart = performance.now();
6473
6477
  const registry = await createRegistry({
6474
- root: registryRoot,
6478
+ root: repoRoot,
6479
+ platformRoot: platformRoot !== repoRoot ? platformRoot : void 0,
6475
6480
  cache: {
6476
6481
  ttlMs: snapshotTTL,
6477
6482
  adapter: platform.cache
@@ -6496,10 +6501,12 @@ async function bootstrap(cwd = process.cwd()) {
6496
6501
  bootstrapLogger.info("Starting system metrics collector");
6497
6502
  metricsCollector2 = new SystemMetricsCollector("rest", () => metricsCollector.getActiveRequests());
6498
6503
  await metricsCollector2.start(1e4, 6e4);
6499
- const address = await server.listen({
6500
- port: config.port,
6501
- host: process.env.REST_API_HOST ?? "0.0.0.0"
6502
- });
6504
+ const restTransport = platform.getAdapter("serviceTransport");
6505
+ const restAddr = restTransport?.listenAddress?.("rest");
6506
+ const listenPort = restAddr && "port" in restAddr ? restAddr.port : config.port;
6507
+ const restAddrHost = restAddr && "host" in restAddr ? restAddr.host : void 0;
6508
+ const restHost = process.env.REST_API_HOST ?? restAddrHost ?? "0.0.0.0";
6509
+ const address = await server.listen(getListenOptions(listenPort, restHost));
6503
6510
  bootstrapLogger.info("REST API server listening", { address });
6504
6511
  const shutdown = async (signal) => {
6505
6512
  bootstrapLogger.warn("Received shutdown signal", { signal });