@kb-labs/rest-api-app 2.94.0 → 2.96.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 +1425 -1418
- package/dist/index.js.map +1 -1
- package/dist/manifest.js +5 -0
- package/dist/manifest.js.map +1 -1
- package/dist/manifest.json +29 -0
- package/package.json +23 -23
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
|
|
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,
|
|
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/
|
|
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/
|
|
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 =
|
|
1745
|
-
const handlerPath =
|
|
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 =
|
|
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 =
|
|
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) => {
|
|
@@ -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: "
|
|
4532
|
-
message: error instanceof Error ? error.message : "Failed to fetch
|
|
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
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
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
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
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
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
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
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
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
|
|
4806
|
-
|
|
4807
|
-
|
|
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:
|
|
4810
|
-
|
|
4769
|
+
error: {
|
|
4770
|
+
code: "INVALID_DATE_RANGE",
|
|
4771
|
+
message: error.message
|
|
4772
|
+
}
|
|
4811
4773
|
});
|
|
4812
4774
|
}
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
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:
|
|
4829
|
-
|
|
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
|
|
4840
|
-
|
|
4841
|
-
|
|
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:
|
|
4844
|
-
|
|
4897
|
+
error: {
|
|
4898
|
+
code: "INVALID_DATE_RANGE",
|
|
4899
|
+
message: error.message
|
|
4900
|
+
}
|
|
4845
4901
|
});
|
|
4846
4902
|
}
|
|
4847
|
-
|
|
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:
|
|
4860
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4874
|
-
|
|
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
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
}
|
|
4889
|
-
|
|
4890
|
-
|
|
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
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
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
|
-
|
|
4907
|
-
|
|
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
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
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
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
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
|
-
|
|
4954
|
-
|
|
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:
|
|
4958
|
-
source:
|
|
4959
|
-
from:
|
|
4960
|
-
to:
|
|
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
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
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
|
-
|
|
5037
|
-
|
|
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
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
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(
|
|
5168
|
+
return reply.code(503).send({
|
|
5055
5169
|
ok: false,
|
|
5056
|
-
error: "Log
|
|
5170
|
+
error: "Log query failed",
|
|
5057
5171
|
message: error instanceof Error ? error.message : "Unknown error"
|
|
5058
5172
|
});
|
|
5059
5173
|
}
|
|
5060
5174
|
});
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
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
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
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
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
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
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
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
|
-
|
|
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
|
|
5274
|
-
const
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
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:
|
|
5323
|
+
error: "Failed to fetch stats",
|
|
5324
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
5288
5325
|
});
|
|
5289
5326
|
}
|
|
5290
5327
|
});
|
|
5291
|
-
}
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
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
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
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
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
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
|
-
|
|
5489
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
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
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
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
|
-
|
|
5527
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
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
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
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
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
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
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
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
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
const
|
|
5599
|
-
if (
|
|
5600
|
-
|
|
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
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
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
|
-
|
|
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
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
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
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
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
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
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
|
|
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-
|
|
6138
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
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 =
|
|
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
|
|
6475
|
+
const platformRoot = getPlatformRoot();
|
|
6472
6476
|
const registryInitStart = performance.now();
|
|
6473
6477
|
const registry = await createRegistry({
|
|
6474
|
-
root:
|
|
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
|
|
6500
|
-
|
|
6501
|
-
|
|
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 });
|