@a-company/sentinel 0.2.0 → 3.6.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/adapters/express.d.ts +3 -1
- package/dist/adapters/fastify.d.ts +3 -1
- package/dist/adapters/hono.d.ts +3 -1
- package/dist/chunk-NTX74ZPM.js +2624 -0
- package/dist/chunk-VQ3SIN7S.js +422 -0
- package/dist/cli.js +6 -6
- package/dist/{commands-KIMGFR2I.js → commands-E7ACLOE7.js} +2367 -258
- package/dist/{dist-2F7NO4H4.js → dist-AG5JNIZU.js} +27 -2
- package/dist/{dist-BPWLYV4U.js → dist-TYG2XME3.js} +27 -2
- package/dist/index.d.ts +57 -5
- package/dist/index.js +288 -186
- package/dist/mcp.js +1538 -124
- package/dist/sdk-CzFDYYST.d.ts +180 -0
- package/dist/server/index.d.ts +20 -3
- package/dist/server/index.js +805 -9
- package/dist/storage-BKyt7aPJ.d.ts +319 -0
- package/dist/transport-DqamniUy.d.ts +185 -0
- package/dist/transport.d.ts +2 -0
- package/dist/transport.js +10 -0
- package/dist/{sdk-B27_vK1g.d.ts → types-BmVoO1iF.d.ts} +196 -259
- package/package.json +15 -1
- package/ui/dist/assets/index-BuK-X6af.js +62 -0
- package/ui/dist/assets/index-BuK-X6af.js.map +1 -0
- package/ui/dist/assets/{index-DPxatSdT.css → index-DCtwmE2_.css} +1 -1
- package/ui/dist/index.html +2 -2
- package/dist/chunk-KPMG4XED.js +0 -1249
- package/ui/dist/assets/index-BNgsn_C8.js +0 -62
- package/ui/dist/assets/index-BNgsn_C8.js.map +0 -1
|
@@ -8,7 +8,7 @@ import initSqlJs from "sql.js";
|
|
|
8
8
|
import { v4 as uuidv4 } from "uuid";
|
|
9
9
|
import * as path from "path";
|
|
10
10
|
import * as fs from "fs";
|
|
11
|
-
var SCHEMA_VERSION =
|
|
11
|
+
var SCHEMA_VERSION = 5;
|
|
12
12
|
var DEFAULT_CONFIDENCE = {
|
|
13
13
|
score: 50,
|
|
14
14
|
timesMatched: 0,
|
|
@@ -128,6 +128,72 @@ var SentinelStorage = class {
|
|
|
128
128
|
notes TEXT
|
|
129
129
|
);
|
|
130
130
|
|
|
131
|
+
-- Structured logs table
|
|
132
|
+
CREATE TABLE IF NOT EXISTS logs (
|
|
133
|
+
id TEXT PRIMARY KEY,
|
|
134
|
+
timestamp TEXT NOT NULL,
|
|
135
|
+
level TEXT NOT NULL CHECK (level IN ('debug','info','warn','error')),
|
|
136
|
+
symbol TEXT NOT NULL,
|
|
137
|
+
symbol_type TEXT NOT NULL DEFAULT 'raw',
|
|
138
|
+
message TEXT NOT NULL,
|
|
139
|
+
data_json TEXT,
|
|
140
|
+
service TEXT NOT NULL,
|
|
141
|
+
session_id TEXT,
|
|
142
|
+
correlation_id TEXT,
|
|
143
|
+
duration_ms REAL,
|
|
144
|
+
environment TEXT
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
-- Service registry
|
|
148
|
+
CREATE TABLE IF NOT EXISTS services (
|
|
149
|
+
name TEXT PRIMARY KEY,
|
|
150
|
+
version TEXT,
|
|
151
|
+
pid INTEGER,
|
|
152
|
+
started_at TEXT NOT NULL,
|
|
153
|
+
last_seen_at TEXT NOT NULL,
|
|
154
|
+
environment TEXT,
|
|
155
|
+
metadata_json TEXT
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
-- Live app state snapshots (latest-wins per service+session)
|
|
159
|
+
CREATE TABLE IF NOT EXISTS app_state (
|
|
160
|
+
service TEXT NOT NULL,
|
|
161
|
+
session_id TEXT NOT NULL,
|
|
162
|
+
timestamp TEXT NOT NULL,
|
|
163
|
+
state_json TEXT NOT NULL,
|
|
164
|
+
active_flows_json TEXT,
|
|
165
|
+
active_gates_json TEXT,
|
|
166
|
+
PRIMARY KEY (service, session_id)
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
-- Metrics table
|
|
170
|
+
CREATE TABLE IF NOT EXISTS metrics (
|
|
171
|
+
id TEXT PRIMARY KEY,
|
|
172
|
+
timestamp TEXT NOT NULL,
|
|
173
|
+
name TEXT NOT NULL,
|
|
174
|
+
type TEXT NOT NULL CHECK (type IN ('counter','gauge','histogram')),
|
|
175
|
+
value REAL NOT NULL,
|
|
176
|
+
tags_json TEXT DEFAULT '{}',
|
|
177
|
+
service TEXT NOT NULL,
|
|
178
|
+
environment TEXT
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
-- Traces table
|
|
182
|
+
CREATE TABLE IF NOT EXISTS traces (
|
|
183
|
+
trace_id TEXT NOT NULL,
|
|
184
|
+
span_id TEXT PRIMARY KEY,
|
|
185
|
+
parent_span_id TEXT,
|
|
186
|
+
service TEXT NOT NULL,
|
|
187
|
+
symbol TEXT NOT NULL,
|
|
188
|
+
operation TEXT NOT NULL,
|
|
189
|
+
start_time TEXT NOT NULL,
|
|
190
|
+
end_time TEXT,
|
|
191
|
+
duration_ms REAL,
|
|
192
|
+
status TEXT NOT NULL DEFAULT 'ok',
|
|
193
|
+
tags_json TEXT DEFAULT '{}',
|
|
194
|
+
log_ids_json TEXT DEFAULT '[]'
|
|
195
|
+
);
|
|
196
|
+
|
|
131
197
|
-- Indexes
|
|
132
198
|
CREATE INDEX IF NOT EXISTS idx_incidents_timestamp ON incidents(timestamp);
|
|
133
199
|
CREATE INDEX IF NOT EXISTS idx_incidents_status ON incidents(status);
|
|
@@ -137,6 +203,18 @@ var SentinelStorage = class {
|
|
|
137
203
|
CREATE INDEX IF NOT EXISTS idx_practice_events_habit_id ON practice_events(habit_id);
|
|
138
204
|
CREATE INDEX IF NOT EXISTS idx_practice_events_engineer ON practice_events(engineer);
|
|
139
205
|
CREATE INDEX IF NOT EXISTS idx_practice_events_session_id ON practice_events(session_id);
|
|
206
|
+
CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp);
|
|
207
|
+
CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
|
|
208
|
+
CREATE INDEX IF NOT EXISTS idx_logs_symbol ON logs(symbol);
|
|
209
|
+
CREATE INDEX IF NOT EXISTS idx_logs_service ON logs(service);
|
|
210
|
+
CREATE INDEX IF NOT EXISTS idx_logs_session_id ON logs(session_id);
|
|
211
|
+
CREATE INDEX IF NOT EXISTS idx_logs_correlation_id ON logs(correlation_id);
|
|
212
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp);
|
|
213
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_name ON metrics(name);
|
|
214
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_service ON metrics(service);
|
|
215
|
+
CREATE INDEX IF NOT EXISTS idx_traces_trace_id ON traces(trace_id);
|
|
216
|
+
CREATE INDEX IF NOT EXISTS idx_traces_service ON traces(service);
|
|
217
|
+
CREATE INDEX IF NOT EXISTS idx_traces_start_time ON traces(start_time);
|
|
140
218
|
`);
|
|
141
219
|
this.db.run(
|
|
142
220
|
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)",
|
|
@@ -266,6 +344,149 @@ var SentinelStorage = class {
|
|
|
266
344
|
this.db.run(
|
|
267
345
|
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '2')"
|
|
268
346
|
);
|
|
347
|
+
currentVersion = 2;
|
|
348
|
+
}
|
|
349
|
+
if (currentVersion < 3) {
|
|
350
|
+
try {
|
|
351
|
+
this.db.run(`
|
|
352
|
+
CREATE TABLE IF NOT EXISTS logs (
|
|
353
|
+
id TEXT PRIMARY KEY,
|
|
354
|
+
timestamp TEXT NOT NULL,
|
|
355
|
+
level TEXT NOT NULL CHECK (level IN ('debug','info','warn','error')),
|
|
356
|
+
symbol TEXT NOT NULL,
|
|
357
|
+
symbol_type TEXT NOT NULL DEFAULT 'raw',
|
|
358
|
+
message TEXT NOT NULL,
|
|
359
|
+
data_json TEXT,
|
|
360
|
+
service TEXT NOT NULL,
|
|
361
|
+
session_id TEXT,
|
|
362
|
+
correlation_id TEXT,
|
|
363
|
+
duration_ms REAL,
|
|
364
|
+
environment TEXT
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
CREATE TABLE IF NOT EXISTS services (
|
|
368
|
+
name TEXT PRIMARY KEY,
|
|
369
|
+
version TEXT,
|
|
370
|
+
pid INTEGER,
|
|
371
|
+
started_at TEXT NOT NULL,
|
|
372
|
+
last_seen_at TEXT NOT NULL,
|
|
373
|
+
environment TEXT,
|
|
374
|
+
metadata_json TEXT
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
CREATE TABLE IF NOT EXISTS app_state (
|
|
378
|
+
service TEXT NOT NULL,
|
|
379
|
+
session_id TEXT NOT NULL,
|
|
380
|
+
timestamp TEXT NOT NULL,
|
|
381
|
+
state_json TEXT NOT NULL,
|
|
382
|
+
active_flows_json TEXT,
|
|
383
|
+
active_gates_json TEXT,
|
|
384
|
+
PRIMARY KEY (service, session_id)
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp);
|
|
388
|
+
CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
|
|
389
|
+
CREATE INDEX IF NOT EXISTS idx_logs_symbol ON logs(symbol);
|
|
390
|
+
CREATE INDEX IF NOT EXISTS idx_logs_service ON logs(service);
|
|
391
|
+
CREATE INDEX IF NOT EXISTS idx_logs_session_id ON logs(session_id);
|
|
392
|
+
CREATE INDEX IF NOT EXISTS idx_logs_correlation_id ON logs(correlation_id);
|
|
393
|
+
`);
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
this.db.run(
|
|
397
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '3')"
|
|
398
|
+
);
|
|
399
|
+
currentVersion = 3;
|
|
400
|
+
}
|
|
401
|
+
if (currentVersion < 4) {
|
|
402
|
+
try {
|
|
403
|
+
this.db.run(`
|
|
404
|
+
CREATE TABLE IF NOT EXISTS metrics (
|
|
405
|
+
id TEXT PRIMARY KEY,
|
|
406
|
+
timestamp TEXT NOT NULL,
|
|
407
|
+
name TEXT NOT NULL,
|
|
408
|
+
type TEXT NOT NULL CHECK (type IN ('counter','gauge','histogram')),
|
|
409
|
+
value REAL NOT NULL,
|
|
410
|
+
tags_json TEXT DEFAULT '{}',
|
|
411
|
+
service TEXT NOT NULL,
|
|
412
|
+
environment TEXT
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
CREATE TABLE IF NOT EXISTS traces (
|
|
416
|
+
trace_id TEXT NOT NULL,
|
|
417
|
+
span_id TEXT PRIMARY KEY,
|
|
418
|
+
parent_span_id TEXT,
|
|
419
|
+
service TEXT NOT NULL,
|
|
420
|
+
symbol TEXT NOT NULL,
|
|
421
|
+
operation TEXT NOT NULL,
|
|
422
|
+
start_time TEXT NOT NULL,
|
|
423
|
+
end_time TEXT,
|
|
424
|
+
duration_ms REAL,
|
|
425
|
+
status TEXT NOT NULL DEFAULT 'ok',
|
|
426
|
+
tags_json TEXT DEFAULT '{}',
|
|
427
|
+
log_ids_json TEXT DEFAULT '[]'
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp);
|
|
431
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_name ON metrics(name);
|
|
432
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_service ON metrics(service);
|
|
433
|
+
CREATE INDEX IF NOT EXISTS idx_traces_trace_id ON traces(trace_id);
|
|
434
|
+
CREATE INDEX IF NOT EXISTS idx_traces_service ON traces(service);
|
|
435
|
+
CREATE INDEX IF NOT EXISTS idx_traces_start_time ON traces(start_time);
|
|
436
|
+
`);
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
this.db.run(
|
|
440
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '4')"
|
|
441
|
+
);
|
|
442
|
+
currentVersion = 4;
|
|
443
|
+
}
|
|
444
|
+
if (currentVersion < 5) {
|
|
445
|
+
try {
|
|
446
|
+
this.db.run(`
|
|
447
|
+
CREATE TABLE IF NOT EXISTS schemas (
|
|
448
|
+
id TEXT PRIMARY KEY,
|
|
449
|
+
version TEXT NOT NULL,
|
|
450
|
+
name TEXT NOT NULL,
|
|
451
|
+
description TEXT,
|
|
452
|
+
scope_json TEXT NOT NULL,
|
|
453
|
+
event_types_json TEXT NOT NULL,
|
|
454
|
+
causality_json TEXT,
|
|
455
|
+
visualization_json TEXT,
|
|
456
|
+
tags_json TEXT DEFAULT '[]',
|
|
457
|
+
registered_at TEXT NOT NULL,
|
|
458
|
+
updated_at TEXT NOT NULL
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
462
|
+
id TEXT PRIMARY KEY,
|
|
463
|
+
schema_id TEXT NOT NULL,
|
|
464
|
+
event_type TEXT NOT NULL,
|
|
465
|
+
category TEXT NOT NULL,
|
|
466
|
+
timestamp TEXT NOT NULL,
|
|
467
|
+
scope_value TEXT,
|
|
468
|
+
scope_ordinal INTEGER,
|
|
469
|
+
session_id TEXT,
|
|
470
|
+
service TEXT NOT NULL,
|
|
471
|
+
data_json TEXT,
|
|
472
|
+
severity TEXT DEFAULT 'info',
|
|
473
|
+
parent_event_id TEXT,
|
|
474
|
+
depth INTEGER DEFAULT 0
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
CREATE INDEX IF NOT EXISTS idx_events_schema ON events(schema_id);
|
|
478
|
+
CREATE INDEX IF NOT EXISTS idx_events_type ON events(event_type);
|
|
479
|
+
CREATE INDEX IF NOT EXISTS idx_events_scope ON events(schema_id, scope_value);
|
|
480
|
+
CREATE INDEX IF NOT EXISTS idx_events_scope_ord ON events(schema_id, scope_ordinal);
|
|
481
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
|
|
482
|
+
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
|
|
483
|
+
CREATE INDEX IF NOT EXISTS idx_events_service ON events(service);
|
|
484
|
+
`);
|
|
485
|
+
} catch {
|
|
486
|
+
}
|
|
487
|
+
this.db.run(
|
|
488
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '5')"
|
|
489
|
+
);
|
|
269
490
|
}
|
|
270
491
|
}
|
|
271
492
|
/**
|
|
@@ -1240,165 +1461,1114 @@ var SentinelStorage = class {
|
|
|
1240
1461
|
notes: obj.notes || void 0
|
|
1241
1462
|
};
|
|
1242
1463
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1464
|
+
// ─── Structured Logs ─────────────────────────────────────────────
|
|
1465
|
+
inferSymbolType(symbol) {
|
|
1466
|
+
if (symbol.startsWith("#")) return "component";
|
|
1467
|
+
if (symbol.startsWith("^")) return "gate";
|
|
1468
|
+
if (symbol.startsWith("!")) return "signal";
|
|
1469
|
+
if (symbol.startsWith("$")) return "flow";
|
|
1470
|
+
if (symbol.startsWith("~")) return "aspect";
|
|
1471
|
+
return "raw";
|
|
1249
1472
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1473
|
+
insertLog(input) {
|
|
1474
|
+
this.initializeSync();
|
|
1475
|
+
const id = input.id || uuidv4();
|
|
1476
|
+
const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
1477
|
+
const symbolType = input.symbolType || this.inferSymbolType(input.symbol);
|
|
1478
|
+
this.db.run(
|
|
1479
|
+
`INSERT INTO logs (
|
|
1480
|
+
id, timestamp, level, symbol, symbol_type, message, data_json,
|
|
1481
|
+
service, session_id, correlation_id, duration_ms, environment
|
|
1482
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1483
|
+
[
|
|
1484
|
+
id,
|
|
1485
|
+
timestamp,
|
|
1486
|
+
input.level,
|
|
1487
|
+
input.symbol,
|
|
1488
|
+
symbolType,
|
|
1489
|
+
input.message,
|
|
1490
|
+
input.data ? JSON.stringify(input.data) : null,
|
|
1491
|
+
input.service,
|
|
1492
|
+
input.sessionId || null,
|
|
1493
|
+
input.correlationId || null,
|
|
1494
|
+
input.durationMs ?? null,
|
|
1495
|
+
input.environment || null
|
|
1496
|
+
]
|
|
1497
|
+
);
|
|
1498
|
+
this.save();
|
|
1499
|
+
return id;
|
|
1261
1500
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
const
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1501
|
+
insertLogBatch(entries) {
|
|
1502
|
+
this.initializeSync();
|
|
1503
|
+
let accepted = 0;
|
|
1504
|
+
const errors = [];
|
|
1505
|
+
for (const input of entries) {
|
|
1506
|
+
try {
|
|
1507
|
+
const id = input.id || uuidv4();
|
|
1508
|
+
const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
1509
|
+
const symbolType = input.symbolType || this.inferSymbolType(input.symbol);
|
|
1510
|
+
this.db.run(
|
|
1511
|
+
`INSERT INTO logs (
|
|
1512
|
+
id, timestamp, level, symbol, symbol_type, message, data_json,
|
|
1513
|
+
service, session_id, correlation_id, duration_ms, environment
|
|
1514
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1515
|
+
[
|
|
1516
|
+
id,
|
|
1517
|
+
timestamp,
|
|
1518
|
+
input.level,
|
|
1519
|
+
input.symbol,
|
|
1520
|
+
symbolType,
|
|
1521
|
+
input.message,
|
|
1522
|
+
input.data ? JSON.stringify(input.data) : null,
|
|
1523
|
+
input.service,
|
|
1524
|
+
input.sessionId || null,
|
|
1525
|
+
input.correlationId || null,
|
|
1526
|
+
input.durationMs ?? null,
|
|
1527
|
+
input.environment || null
|
|
1528
|
+
]
|
|
1529
|
+
);
|
|
1530
|
+
accepted++;
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
1290
1533
|
}
|
|
1291
1534
|
}
|
|
1292
|
-
|
|
1535
|
+
this.save();
|
|
1536
|
+
return { accepted, errors };
|
|
1293
1537
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
if (!this.matchEnvironment(pattern, incident)) {
|
|
1303
|
-
continue;
|
|
1304
|
-
}
|
|
1305
|
-
const { score } = this.scoreMatch(pattern, incident);
|
|
1306
|
-
if (score >= 30) {
|
|
1307
|
-
wouldMatch.push(incident);
|
|
1308
|
-
totalScore += score;
|
|
1309
|
-
}
|
|
1538
|
+
queryLogs(options = {}) {
|
|
1539
|
+
this.initializeSync();
|
|
1540
|
+
const { limit = 100, offset = 0 } = options;
|
|
1541
|
+
const conditions = [];
|
|
1542
|
+
const params = [];
|
|
1543
|
+
if (options.level) {
|
|
1544
|
+
conditions.push("level = ?");
|
|
1545
|
+
params.push(options.level);
|
|
1310
1546
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
)
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1547
|
+
if (options.symbol) {
|
|
1548
|
+
conditions.push("symbol LIKE ?");
|
|
1549
|
+
params.push(`%${options.symbol}%`);
|
|
1550
|
+
}
|
|
1551
|
+
if (options.service) {
|
|
1552
|
+
conditions.push("service = ?");
|
|
1553
|
+
params.push(options.service);
|
|
1554
|
+
}
|
|
1555
|
+
if (options.sessionId) {
|
|
1556
|
+
conditions.push("session_id = ?");
|
|
1557
|
+
params.push(options.sessionId);
|
|
1558
|
+
}
|
|
1559
|
+
if (options.correlationId) {
|
|
1560
|
+
conditions.push("correlation_id = ?");
|
|
1561
|
+
params.push(options.correlationId);
|
|
1562
|
+
}
|
|
1563
|
+
if (options.search) {
|
|
1564
|
+
conditions.push("message LIKE ?");
|
|
1565
|
+
params.push(`%${options.search}%`);
|
|
1566
|
+
}
|
|
1567
|
+
if (options.since) {
|
|
1568
|
+
conditions.push("timestamp >= ?");
|
|
1569
|
+
params.push(options.since);
|
|
1570
|
+
}
|
|
1571
|
+
if (options.until) {
|
|
1572
|
+
conditions.push("timestamp <= ?");
|
|
1573
|
+
params.push(options.until);
|
|
1574
|
+
}
|
|
1575
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1576
|
+
const result = this.db.exec(
|
|
1577
|
+
`SELECT * FROM logs ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
|
|
1578
|
+
[...params, limit, offset]
|
|
1337
1579
|
);
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
incident,
|
|
1342
|
-
matchedCriteria.missingSignals
|
|
1580
|
+
if (result.length === 0) return [];
|
|
1581
|
+
return result[0].values.map(
|
|
1582
|
+
(row) => this.rowToLogEntry(result[0].columns, row)
|
|
1343
1583
|
);
|
|
1344
|
-
score += Math.min(signalScore, 25);
|
|
1345
|
-
score = Math.min(score, 100);
|
|
1346
|
-
return { score, matchedCriteria };
|
|
1347
1584
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
"component",
|
|
1356
|
-
"flow",
|
|
1357
|
-
"gate",
|
|
1358
|
-
"signal",
|
|
1359
|
-
"state",
|
|
1360
|
-
"integration"
|
|
1361
|
-
];
|
|
1362
|
-
for (const type of symbolTypes) {
|
|
1363
|
-
const patternValue = patternSymbols[type];
|
|
1364
|
-
const incidentValue = incidentSymbols[type];
|
|
1365
|
-
if (!patternValue || !incidentValue) {
|
|
1366
|
-
continue;
|
|
1367
|
-
}
|
|
1368
|
-
if (typeof patternValue === "string") {
|
|
1369
|
-
if (this.matchSingleSymbol(patternValue, incidentValue)) {
|
|
1370
|
-
score += patternValue.includes("*") ? 5 : 10;
|
|
1371
|
-
matched.push(type);
|
|
1372
|
-
}
|
|
1373
|
-
} else if (Array.isArray(patternValue)) {
|
|
1374
|
-
for (const pv of patternValue) {
|
|
1375
|
-
if (this.matchSingleSymbol(pv, incidentValue)) {
|
|
1376
|
-
score += 7;
|
|
1377
|
-
matched.push(type);
|
|
1378
|
-
break;
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1585
|
+
getLogCount(options = {}) {
|
|
1586
|
+
this.initializeSync();
|
|
1587
|
+
const conditions = [];
|
|
1588
|
+
const params = [];
|
|
1589
|
+
if (options.level) {
|
|
1590
|
+
conditions.push("level = ?");
|
|
1591
|
+
params.push(options.level);
|
|
1382
1592
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
* Match a single symbol value (supports wildcards)
|
|
1387
|
-
*/
|
|
1388
|
-
matchSingleSymbol(pattern, value) {
|
|
1389
|
-
if (pattern === "*") {
|
|
1390
|
-
return true;
|
|
1593
|
+
if (options.symbol) {
|
|
1594
|
+
conditions.push("symbol LIKE ?");
|
|
1595
|
+
params.push(`%${options.symbol}%`);
|
|
1391
1596
|
}
|
|
1392
|
-
if (
|
|
1393
|
-
|
|
1394
|
-
|
|
1597
|
+
if (options.service) {
|
|
1598
|
+
conditions.push("service = ?");
|
|
1599
|
+
params.push(options.service);
|
|
1395
1600
|
}
|
|
1396
|
-
if (
|
|
1397
|
-
|
|
1398
|
-
|
|
1601
|
+
if (options.since) {
|
|
1602
|
+
conditions.push("timestamp >= ?");
|
|
1603
|
+
params.push(options.since);
|
|
1399
1604
|
}
|
|
1400
|
-
if (
|
|
1401
|
-
|
|
1605
|
+
if (options.until) {
|
|
1606
|
+
conditions.push("timestamp <= ?");
|
|
1607
|
+
params.push(options.until);
|
|
1608
|
+
}
|
|
1609
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1610
|
+
const result = this.db.exec(
|
|
1611
|
+
`SELECT COUNT(*) as count FROM logs ${whereClause}`,
|
|
1612
|
+
params
|
|
1613
|
+
);
|
|
1614
|
+
if (result.length === 0 || result[0].values.length === 0) return 0;
|
|
1615
|
+
return result[0].values[0][0];
|
|
1616
|
+
}
|
|
1617
|
+
pruneLogs(maxCount) {
|
|
1618
|
+
this.initializeSync();
|
|
1619
|
+
if (maxCount <= 0) return 0;
|
|
1620
|
+
const currentCount = this.getLogCount();
|
|
1621
|
+
if (currentCount <= maxCount) return 0;
|
|
1622
|
+
const deleteCount = currentCount - maxCount;
|
|
1623
|
+
this.db.run(
|
|
1624
|
+
`DELETE FROM logs WHERE id IN (
|
|
1625
|
+
SELECT id FROM logs ORDER BY timestamp ASC LIMIT ?
|
|
1626
|
+
)`,
|
|
1627
|
+
[deleteCount]
|
|
1628
|
+
);
|
|
1629
|
+
this.save();
|
|
1630
|
+
return deleteCount;
|
|
1631
|
+
}
|
|
1632
|
+
rowToLogEntry(columns, row) {
|
|
1633
|
+
const obj = {};
|
|
1634
|
+
columns.forEach((col, i) => {
|
|
1635
|
+
obj[col] = row[i];
|
|
1636
|
+
});
|
|
1637
|
+
return {
|
|
1638
|
+
id: obj.id,
|
|
1639
|
+
timestamp: obj.timestamp,
|
|
1640
|
+
level: obj.level,
|
|
1641
|
+
symbol: obj.symbol,
|
|
1642
|
+
symbolType: obj.symbol_type || "raw",
|
|
1643
|
+
message: obj.message,
|
|
1644
|
+
data: obj.data_json ? JSON.parse(obj.data_json) : void 0,
|
|
1645
|
+
service: obj.service,
|
|
1646
|
+
sessionId: obj.session_id || void 0,
|
|
1647
|
+
correlationId: obj.correlation_id || void 0,
|
|
1648
|
+
durationMs: obj.duration_ms || void 0,
|
|
1649
|
+
environment: obj.environment || void 0
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
// ─── Service Registry ──────────────────────────────────────────
|
|
1653
|
+
registerService(reg) {
|
|
1654
|
+
this.initializeSync();
|
|
1655
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1656
|
+
this.db.run(
|
|
1657
|
+
`INSERT INTO services (name, version, pid, started_at, last_seen_at, environment, metadata_json)
|
|
1658
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1659
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
1660
|
+
version = excluded.version,
|
|
1661
|
+
pid = excluded.pid,
|
|
1662
|
+
last_seen_at = excluded.last_seen_at,
|
|
1663
|
+
environment = excluded.environment,
|
|
1664
|
+
metadata_json = excluded.metadata_json`,
|
|
1665
|
+
[
|
|
1666
|
+
reg.name,
|
|
1667
|
+
reg.version || null,
|
|
1668
|
+
reg.pid ?? null,
|
|
1669
|
+
now,
|
|
1670
|
+
now,
|
|
1671
|
+
reg.environment || null,
|
|
1672
|
+
reg.metadata ? JSON.stringify(reg.metadata) : null
|
|
1673
|
+
]
|
|
1674
|
+
);
|
|
1675
|
+
this.save();
|
|
1676
|
+
}
|
|
1677
|
+
updateServiceLastSeen(name) {
|
|
1678
|
+
this.initializeSync();
|
|
1679
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1680
|
+
this.db.run(
|
|
1681
|
+
"UPDATE services SET last_seen_at = ? WHERE name = ?",
|
|
1682
|
+
[now, name]
|
|
1683
|
+
);
|
|
1684
|
+
this.save();
|
|
1685
|
+
}
|
|
1686
|
+
getServices() {
|
|
1687
|
+
this.initializeSync();
|
|
1688
|
+
const result = this.db.exec(
|
|
1689
|
+
"SELECT * FROM services ORDER BY last_seen_at DESC"
|
|
1690
|
+
);
|
|
1691
|
+
if (result.length === 0) return [];
|
|
1692
|
+
return result[0].values.map((row) => {
|
|
1693
|
+
const obj = {};
|
|
1694
|
+
result[0].columns.forEach((col, i) => {
|
|
1695
|
+
obj[col] = row[i];
|
|
1696
|
+
});
|
|
1697
|
+
return {
|
|
1698
|
+
name: obj.name,
|
|
1699
|
+
version: obj.version || void 0,
|
|
1700
|
+
pid: obj.pid || void 0,
|
|
1701
|
+
startedAt: obj.started_at,
|
|
1702
|
+
lastSeenAt: obj.last_seen_at,
|
|
1703
|
+
environment: obj.environment || void 0,
|
|
1704
|
+
metadata: obj.metadata_json ? JSON.parse(obj.metadata_json) : void 0
|
|
1705
|
+
};
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
// ─── App State ──────────────────────────────────────────────────
|
|
1709
|
+
upsertAppState(state) {
|
|
1710
|
+
this.initializeSync();
|
|
1711
|
+
this.db.run(
|
|
1712
|
+
`INSERT INTO app_state (service, session_id, timestamp, state_json, active_flows_json, active_gates_json)
|
|
1713
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1714
|
+
ON CONFLICT(service, session_id) DO UPDATE SET
|
|
1715
|
+
timestamp = excluded.timestamp,
|
|
1716
|
+
state_json = excluded.state_json,
|
|
1717
|
+
active_flows_json = excluded.active_flows_json,
|
|
1718
|
+
active_gates_json = excluded.active_gates_json`,
|
|
1719
|
+
[
|
|
1720
|
+
state.service,
|
|
1721
|
+
state.sessionId,
|
|
1722
|
+
state.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1723
|
+
JSON.stringify(state.state),
|
|
1724
|
+
state.activeFlows ? JSON.stringify(state.activeFlows) : null,
|
|
1725
|
+
state.activeGates ? JSON.stringify(state.activeGates) : null
|
|
1726
|
+
]
|
|
1727
|
+
);
|
|
1728
|
+
this.save();
|
|
1729
|
+
}
|
|
1730
|
+
getAppState(service, sessionId) {
|
|
1731
|
+
this.initializeSync();
|
|
1732
|
+
let query = "SELECT * FROM app_state WHERE service = ?";
|
|
1733
|
+
const params = [service];
|
|
1734
|
+
if (sessionId) {
|
|
1735
|
+
query += " AND session_id = ?";
|
|
1736
|
+
params.push(sessionId);
|
|
1737
|
+
}
|
|
1738
|
+
query += " ORDER BY timestamp DESC";
|
|
1739
|
+
const result = this.db.exec(query, params);
|
|
1740
|
+
if (result.length === 0) return [];
|
|
1741
|
+
return result[0].values.map((row) => this.rowToAppState(result[0].columns, row));
|
|
1742
|
+
}
|
|
1743
|
+
getAllAppStates() {
|
|
1744
|
+
this.initializeSync();
|
|
1745
|
+
const result = this.db.exec(
|
|
1746
|
+
"SELECT * FROM app_state ORDER BY timestamp DESC"
|
|
1747
|
+
);
|
|
1748
|
+
if (result.length === 0) return [];
|
|
1749
|
+
return result[0].values.map((row) => this.rowToAppState(result[0].columns, row));
|
|
1750
|
+
}
|
|
1751
|
+
rowToAppState(columns, row) {
|
|
1752
|
+
const obj = {};
|
|
1753
|
+
columns.forEach((col, i) => {
|
|
1754
|
+
obj[col] = row[i];
|
|
1755
|
+
});
|
|
1756
|
+
return {
|
|
1757
|
+
service: obj.service,
|
|
1758
|
+
sessionId: obj.session_id,
|
|
1759
|
+
timestamp: obj.timestamp,
|
|
1760
|
+
state: JSON.parse(obj.state_json),
|
|
1761
|
+
activeFlows: obj.active_flows_json ? JSON.parse(obj.active_flows_json) : void 0,
|
|
1762
|
+
activeGates: obj.active_gates_json ? JSON.parse(obj.active_gates_json) : void 0
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
// ─── Metrics ───────────────────────────────────────────────────
|
|
1766
|
+
insertMetric(input) {
|
|
1767
|
+
this.initializeSync();
|
|
1768
|
+
const id = uuidv4();
|
|
1769
|
+
const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
1770
|
+
this.db.run(
|
|
1771
|
+
`INSERT INTO metrics (
|
|
1772
|
+
id, timestamp, name, type, value, tags_json, service, environment
|
|
1773
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1774
|
+
[
|
|
1775
|
+
id,
|
|
1776
|
+
timestamp,
|
|
1777
|
+
input.name,
|
|
1778
|
+
input.type,
|
|
1779
|
+
input.value,
|
|
1780
|
+
JSON.stringify(input.tags || {}),
|
|
1781
|
+
input.service,
|
|
1782
|
+
input.environment || null
|
|
1783
|
+
]
|
|
1784
|
+
);
|
|
1785
|
+
this.save();
|
|
1786
|
+
return id;
|
|
1787
|
+
}
|
|
1788
|
+
insertMetricBatch(entries) {
|
|
1789
|
+
this.initializeSync();
|
|
1790
|
+
let accepted = 0;
|
|
1791
|
+
const errors = [];
|
|
1792
|
+
for (const input of entries) {
|
|
1793
|
+
try {
|
|
1794
|
+
const id = uuidv4();
|
|
1795
|
+
const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
1796
|
+
this.db.run(
|
|
1797
|
+
`INSERT INTO metrics (
|
|
1798
|
+
id, timestamp, name, type, value, tags_json, service, environment
|
|
1799
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1800
|
+
[
|
|
1801
|
+
id,
|
|
1802
|
+
timestamp,
|
|
1803
|
+
input.name,
|
|
1804
|
+
input.type,
|
|
1805
|
+
input.value,
|
|
1806
|
+
JSON.stringify(input.tags || {}),
|
|
1807
|
+
input.service,
|
|
1808
|
+
input.environment || null
|
|
1809
|
+
]
|
|
1810
|
+
);
|
|
1811
|
+
accepted++;
|
|
1812
|
+
} catch (err) {
|
|
1813
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
this.save();
|
|
1817
|
+
return { accepted, errors };
|
|
1818
|
+
}
|
|
1819
|
+
queryMetrics(options = {}) {
|
|
1820
|
+
this.initializeSync();
|
|
1821
|
+
const { limit = 100, offset = 0 } = options;
|
|
1822
|
+
const conditions = [];
|
|
1823
|
+
const params = [];
|
|
1824
|
+
if (options.name) {
|
|
1825
|
+
conditions.push("name = ?");
|
|
1826
|
+
params.push(options.name);
|
|
1827
|
+
}
|
|
1828
|
+
if (options.type) {
|
|
1829
|
+
conditions.push("type = ?");
|
|
1830
|
+
params.push(options.type);
|
|
1831
|
+
}
|
|
1832
|
+
if (options.service) {
|
|
1833
|
+
conditions.push("service = ?");
|
|
1834
|
+
params.push(options.service);
|
|
1835
|
+
}
|
|
1836
|
+
if (options.tag) {
|
|
1837
|
+
const eqIdx = options.tag.indexOf("=");
|
|
1838
|
+
if (eqIdx > 0) {
|
|
1839
|
+
const tagKey = options.tag.substring(0, eqIdx);
|
|
1840
|
+
const tagValue = options.tag.substring(eqIdx + 1);
|
|
1841
|
+
conditions.push("tags_json LIKE ?");
|
|
1842
|
+
params.push(`%"${tagKey}":"${tagValue}"%`);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
if (options.since) {
|
|
1846
|
+
conditions.push("timestamp >= ?");
|
|
1847
|
+
params.push(options.since);
|
|
1848
|
+
}
|
|
1849
|
+
if (options.until) {
|
|
1850
|
+
conditions.push("timestamp <= ?");
|
|
1851
|
+
params.push(options.until);
|
|
1852
|
+
}
|
|
1853
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1854
|
+
const result = this.db.exec(
|
|
1855
|
+
`SELECT * FROM metrics ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
|
|
1856
|
+
[...params, limit, offset]
|
|
1857
|
+
);
|
|
1858
|
+
if (result.length === 0) return [];
|
|
1859
|
+
return result[0].values.map(
|
|
1860
|
+
(row) => this.rowToMetricEntry(result[0].columns, row)
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
getMetricCount(options = {}) {
|
|
1864
|
+
this.initializeSync();
|
|
1865
|
+
const conditions = [];
|
|
1866
|
+
const params = [];
|
|
1867
|
+
if (options.name) {
|
|
1868
|
+
conditions.push("name = ?");
|
|
1869
|
+
params.push(options.name);
|
|
1870
|
+
}
|
|
1871
|
+
if (options.type) {
|
|
1872
|
+
conditions.push("type = ?");
|
|
1873
|
+
params.push(options.type);
|
|
1874
|
+
}
|
|
1875
|
+
if (options.service) {
|
|
1876
|
+
conditions.push("service = ?");
|
|
1877
|
+
params.push(options.service);
|
|
1878
|
+
}
|
|
1879
|
+
if (options.tag) {
|
|
1880
|
+
const eqIdx = options.tag.indexOf("=");
|
|
1881
|
+
if (eqIdx > 0) {
|
|
1882
|
+
const tagKey = options.tag.substring(0, eqIdx);
|
|
1883
|
+
const tagValue = options.tag.substring(eqIdx + 1);
|
|
1884
|
+
conditions.push("tags_json LIKE ?");
|
|
1885
|
+
params.push(`%"${tagKey}":"${tagValue}"%`);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
if (options.since) {
|
|
1889
|
+
conditions.push("timestamp >= ?");
|
|
1890
|
+
params.push(options.since);
|
|
1891
|
+
}
|
|
1892
|
+
if (options.until) {
|
|
1893
|
+
conditions.push("timestamp <= ?");
|
|
1894
|
+
params.push(options.until);
|
|
1895
|
+
}
|
|
1896
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1897
|
+
const result = this.db.exec(
|
|
1898
|
+
`SELECT COUNT(*) as count FROM metrics ${whereClause}`,
|
|
1899
|
+
params
|
|
1900
|
+
);
|
|
1901
|
+
if (result.length === 0 || result[0].values.length === 0) return 0;
|
|
1902
|
+
return result[0].values[0][0];
|
|
1903
|
+
}
|
|
1904
|
+
aggregateMetric(name, options) {
|
|
1905
|
+
this.initializeSync();
|
|
1906
|
+
const conditions = ["name = ?"];
|
|
1907
|
+
const params = [name];
|
|
1908
|
+
if (options?.service) {
|
|
1909
|
+
conditions.push("service = ?");
|
|
1910
|
+
params.push(options.service);
|
|
1911
|
+
}
|
|
1912
|
+
if (options?.since) {
|
|
1913
|
+
conditions.push("timestamp >= ?");
|
|
1914
|
+
params.push(options.since);
|
|
1915
|
+
}
|
|
1916
|
+
if (options?.until) {
|
|
1917
|
+
conditions.push("timestamp <= ?");
|
|
1918
|
+
params.push(options.until);
|
|
1919
|
+
}
|
|
1920
|
+
const whereClause = `WHERE ${conditions.join(" AND ")}`;
|
|
1921
|
+
const result = this.db.exec(
|
|
1922
|
+
`SELECT COUNT(*) as count, SUM(value) as sum, MIN(value) as min, MAX(value) as max, AVG(value) as avg
|
|
1923
|
+
FROM metrics ${whereClause}`,
|
|
1924
|
+
params
|
|
1925
|
+
);
|
|
1926
|
+
if (result.length === 0 || result[0].values.length === 0) {
|
|
1927
|
+
return { name, count: 0, sum: 0, min: 0, max: 0, avg: 0 };
|
|
1928
|
+
}
|
|
1929
|
+
const row = result[0].values[0];
|
|
1930
|
+
return {
|
|
1931
|
+
name,
|
|
1932
|
+
count: row[0] || 0,
|
|
1933
|
+
sum: row[1] || 0,
|
|
1934
|
+
min: row[2] || 0,
|
|
1935
|
+
max: row[3] || 0,
|
|
1936
|
+
avg: row[4] || 0
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
pruneMetrics(maxCount) {
|
|
1940
|
+
this.initializeSync();
|
|
1941
|
+
if (maxCount <= 0) return 0;
|
|
1942
|
+
const currentCount = this.getMetricCount();
|
|
1943
|
+
if (currentCount <= maxCount) return 0;
|
|
1944
|
+
const deleteCount = currentCount - maxCount;
|
|
1945
|
+
this.db.run(
|
|
1946
|
+
`DELETE FROM metrics WHERE id IN (
|
|
1947
|
+
SELECT id FROM metrics ORDER BY timestamp ASC LIMIT ?
|
|
1948
|
+
)`,
|
|
1949
|
+
[deleteCount]
|
|
1950
|
+
);
|
|
1951
|
+
this.save();
|
|
1952
|
+
return deleteCount;
|
|
1953
|
+
}
|
|
1954
|
+
rowToMetricEntry(columns, row) {
|
|
1955
|
+
const obj = {};
|
|
1956
|
+
columns.forEach((col, i) => {
|
|
1957
|
+
obj[col] = row[i];
|
|
1958
|
+
});
|
|
1959
|
+
return {
|
|
1960
|
+
id: obj.id,
|
|
1961
|
+
timestamp: obj.timestamp,
|
|
1962
|
+
name: obj.name,
|
|
1963
|
+
type: obj.type,
|
|
1964
|
+
value: obj.value,
|
|
1965
|
+
tags: obj.tags_json ? JSON.parse(obj.tags_json) : {},
|
|
1966
|
+
service: obj.service,
|
|
1967
|
+
environment: obj.environment || void 0
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
// ─── Traces ───────────────────────────────────────────────────
|
|
1971
|
+
insertSpan(input) {
|
|
1972
|
+
this.initializeSync();
|
|
1973
|
+
const spanId = input.spanId || uuidv4();
|
|
1974
|
+
const startTime = input.startTime || (/* @__PURE__ */ new Date()).toISOString();
|
|
1975
|
+
this.db.run(
|
|
1976
|
+
`INSERT INTO traces (
|
|
1977
|
+
trace_id, span_id, parent_span_id, service, symbol, operation,
|
|
1978
|
+
start_time, end_time, duration_ms, status, tags_json, log_ids_json
|
|
1979
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1980
|
+
[
|
|
1981
|
+
input.traceId,
|
|
1982
|
+
spanId,
|
|
1983
|
+
input.parentSpanId || null,
|
|
1984
|
+
input.service,
|
|
1985
|
+
input.symbol,
|
|
1986
|
+
input.operation,
|
|
1987
|
+
startTime,
|
|
1988
|
+
input.endTime || null,
|
|
1989
|
+
input.durationMs ?? null,
|
|
1990
|
+
input.status || "ok",
|
|
1991
|
+
JSON.stringify(input.tags || {}),
|
|
1992
|
+
JSON.stringify(input.logIds || [])
|
|
1993
|
+
]
|
|
1994
|
+
);
|
|
1995
|
+
this.save();
|
|
1996
|
+
return spanId;
|
|
1997
|
+
}
|
|
1998
|
+
getTrace(traceId) {
|
|
1999
|
+
this.initializeSync();
|
|
2000
|
+
const result = this.db.exec(
|
|
2001
|
+
"SELECT * FROM traces WHERE trace_id = ? ORDER BY start_time ASC",
|
|
2002
|
+
[traceId]
|
|
2003
|
+
);
|
|
2004
|
+
if (result.length === 0 || result[0].values.length === 0) return null;
|
|
2005
|
+
const spans = result[0].values.map(
|
|
2006
|
+
(row) => this.rowToTraceSpan(result[0].columns, row)
|
|
2007
|
+
);
|
|
2008
|
+
const services = [...new Set(spans.map((s) => s.service))];
|
|
2009
|
+
const startTimes = spans.map((s) => s.startTime);
|
|
2010
|
+
const endTimes = spans.filter((s) => s.endTime).map((s) => s.endTime);
|
|
2011
|
+
const startTime = startTimes.sort()[0];
|
|
2012
|
+
const endTime = endTimes.length > 0 ? endTimes.sort().reverse()[0] : startTime;
|
|
2013
|
+
const startMs = new Date(startTime).getTime();
|
|
2014
|
+
const endMs = new Date(endTime).getTime();
|
|
2015
|
+
const totalDurationMs = endMs - startMs;
|
|
2016
|
+
return {
|
|
2017
|
+
traceId,
|
|
2018
|
+
spans,
|
|
2019
|
+
services,
|
|
2020
|
+
totalDurationMs: totalDurationMs > 0 ? totalDurationMs : 0,
|
|
2021
|
+
startTime,
|
|
2022
|
+
endTime
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
queryTraces(options = {}) {
|
|
2026
|
+
this.initializeSync();
|
|
2027
|
+
const conditions = [];
|
|
2028
|
+
const params = [];
|
|
2029
|
+
if (options.service) {
|
|
2030
|
+
conditions.push("service = ?");
|
|
2031
|
+
params.push(options.service);
|
|
2032
|
+
}
|
|
2033
|
+
if (options.symbol) {
|
|
2034
|
+
conditions.push("symbol = ?");
|
|
2035
|
+
params.push(options.symbol);
|
|
2036
|
+
}
|
|
2037
|
+
if (options.since) {
|
|
2038
|
+
conditions.push("start_time >= ?");
|
|
2039
|
+
params.push(options.since);
|
|
2040
|
+
}
|
|
2041
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2042
|
+
const traceLimit = Math.min(options.limit || 20, 20);
|
|
2043
|
+
const result = this.db.exec(
|
|
2044
|
+
`SELECT DISTINCT trace_id FROM traces ${whereClause} ORDER BY start_time DESC LIMIT ?`,
|
|
2045
|
+
[...params, traceLimit]
|
|
2046
|
+
);
|
|
2047
|
+
if (result.length === 0) return [];
|
|
2048
|
+
const traces = [];
|
|
2049
|
+
for (const row of result[0].values) {
|
|
2050
|
+
const traceId = row[0];
|
|
2051
|
+
const trace = this.getTrace(traceId);
|
|
2052
|
+
if (trace) {
|
|
2053
|
+
traces.push(trace);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return traces;
|
|
2057
|
+
}
|
|
2058
|
+
rowToTraceSpan(columns, row) {
|
|
2059
|
+
const obj = {};
|
|
2060
|
+
columns.forEach((col, i) => {
|
|
2061
|
+
obj[col] = row[i];
|
|
2062
|
+
});
|
|
2063
|
+
return {
|
|
2064
|
+
traceId: obj.trace_id,
|
|
2065
|
+
spanId: obj.span_id,
|
|
2066
|
+
parentSpanId: obj.parent_span_id || void 0,
|
|
2067
|
+
service: obj.service,
|
|
2068
|
+
symbol: obj.symbol,
|
|
2069
|
+
operation: obj.operation,
|
|
2070
|
+
startTime: obj.start_time,
|
|
2071
|
+
endTime: obj.end_time || void 0,
|
|
2072
|
+
durationMs: obj.duration_ms || void 0,
|
|
2073
|
+
status: obj.status || "ok",
|
|
2074
|
+
tags: obj.tags_json ? JSON.parse(obj.tags_json) : {},
|
|
2075
|
+
logs: obj.log_ids_json ? JSON.parse(obj.log_ids_json) : []
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
// ─── Schema Registry ─────────────────────────────────────────────
|
|
2079
|
+
registerSchema(schema) {
|
|
2080
|
+
this.initializeSync();
|
|
2081
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2082
|
+
this.db.run(
|
|
2083
|
+
`INSERT INTO schemas (
|
|
2084
|
+
id, version, name, description, scope_json, event_types_json,
|
|
2085
|
+
causality_json, visualization_json, tags_json, registered_at, updated_at
|
|
2086
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2087
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
2088
|
+
version = excluded.version,
|
|
2089
|
+
name = excluded.name,
|
|
2090
|
+
description = excluded.description,
|
|
2091
|
+
scope_json = excluded.scope_json,
|
|
2092
|
+
event_types_json = excluded.event_types_json,
|
|
2093
|
+
causality_json = excluded.causality_json,
|
|
2094
|
+
visualization_json = excluded.visualization_json,
|
|
2095
|
+
tags_json = excluded.tags_json,
|
|
2096
|
+
updated_at = excluded.updated_at`,
|
|
2097
|
+
[
|
|
2098
|
+
schema.id,
|
|
2099
|
+
schema.version,
|
|
2100
|
+
schema.name,
|
|
2101
|
+
schema.description || null,
|
|
2102
|
+
JSON.stringify(schema.scope),
|
|
2103
|
+
JSON.stringify(schema.eventTypes),
|
|
2104
|
+
schema.causality ? JSON.stringify(schema.causality) : null,
|
|
2105
|
+
schema.visualization ? JSON.stringify(schema.visualization) : null,
|
|
2106
|
+
JSON.stringify(schema.tags || []),
|
|
2107
|
+
now,
|
|
2108
|
+
now
|
|
2109
|
+
]
|
|
2110
|
+
);
|
|
2111
|
+
this.save();
|
|
2112
|
+
return {
|
|
2113
|
+
id: schema.id,
|
|
2114
|
+
version: schema.version,
|
|
2115
|
+
name: schema.name,
|
|
2116
|
+
description: schema.description,
|
|
2117
|
+
scope: schema.scope,
|
|
2118
|
+
eventTypes: schema.eventTypes,
|
|
2119
|
+
causality: schema.causality,
|
|
2120
|
+
visualization: schema.visualization,
|
|
2121
|
+
tags: schema.tags || [],
|
|
2122
|
+
registeredAt: now,
|
|
2123
|
+
updatedAt: now
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
getSchema(id) {
|
|
2127
|
+
this.initializeSync();
|
|
2128
|
+
const result = this.db.exec("SELECT * FROM schemas WHERE id = ?", [id]);
|
|
2129
|
+
if (result.length === 0 || result[0].values.length === 0) return null;
|
|
2130
|
+
return this.rowToSchema(result[0].columns, result[0].values[0]);
|
|
2131
|
+
}
|
|
2132
|
+
listSchemas() {
|
|
2133
|
+
this.initializeSync();
|
|
2134
|
+
const result = this.db.exec("SELECT * FROM schemas ORDER BY name ASC");
|
|
2135
|
+
if (result.length === 0) return [];
|
|
2136
|
+
return result[0].values.map(
|
|
2137
|
+
(row) => this.rowToSchema(result[0].columns, row)
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
rowToSchema(columns, row) {
|
|
2141
|
+
const obj = {};
|
|
2142
|
+
columns.forEach((col, i) => {
|
|
2143
|
+
obj[col] = row[i];
|
|
2144
|
+
});
|
|
2145
|
+
return {
|
|
2146
|
+
id: obj.id,
|
|
2147
|
+
version: obj.version,
|
|
2148
|
+
name: obj.name,
|
|
2149
|
+
description: obj.description || void 0,
|
|
2150
|
+
scope: JSON.parse(obj.scope_json),
|
|
2151
|
+
eventTypes: JSON.parse(obj.event_types_json),
|
|
2152
|
+
causality: obj.causality_json ? JSON.parse(obj.causality_json) : void 0,
|
|
2153
|
+
visualization: obj.visualization_json ? JSON.parse(obj.visualization_json) : void 0,
|
|
2154
|
+
tags: JSON.parse(obj.tags_json || "[]"),
|
|
2155
|
+
registeredAt: obj.registered_at,
|
|
2156
|
+
updatedAt: obj.updated_at
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
// ─── Generic Events ────────────────────────────────────────────
|
|
2160
|
+
insertEventBatch(schemaId, service, inputs) {
|
|
2161
|
+
this.initializeSync();
|
|
2162
|
+
const schema = this.getSchema(schemaId);
|
|
2163
|
+
const typeMap = /* @__PURE__ */ new Map();
|
|
2164
|
+
if (schema) {
|
|
2165
|
+
for (const et of schema.eventTypes) {
|
|
2166
|
+
typeMap.set(et.type, {
|
|
2167
|
+
category: et.category,
|
|
2168
|
+
severity: et.severity || "info"
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
let accepted = 0;
|
|
2173
|
+
const errors = [];
|
|
2174
|
+
for (const input of inputs) {
|
|
2175
|
+
try {
|
|
2176
|
+
const id = input.id || uuidv4();
|
|
2177
|
+
const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
2178
|
+
const resolved = typeMap.get(input.type);
|
|
2179
|
+
const category = resolved?.category || "unknown";
|
|
2180
|
+
const severity = input.severity || resolved?.severity || "info";
|
|
2181
|
+
const scopeValue = input.scopeValue != null ? String(input.scopeValue) : null;
|
|
2182
|
+
const scopeOrdinal = typeof input.scopeValue === "number" ? input.scopeValue : null;
|
|
2183
|
+
this.db.run(
|
|
2184
|
+
`INSERT INTO events (
|
|
2185
|
+
id, schema_id, event_type, category, timestamp, scope_value,
|
|
2186
|
+
scope_ordinal, session_id, service, data_json, severity,
|
|
2187
|
+
parent_event_id, depth
|
|
2188
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2189
|
+
[
|
|
2190
|
+
id,
|
|
2191
|
+
schemaId,
|
|
2192
|
+
input.type,
|
|
2193
|
+
category,
|
|
2194
|
+
timestamp,
|
|
2195
|
+
scopeValue,
|
|
2196
|
+
scopeOrdinal,
|
|
2197
|
+
input.sessionId || null,
|
|
2198
|
+
service,
|
|
2199
|
+
input.data ? JSON.stringify(input.data) : null,
|
|
2200
|
+
severity,
|
|
2201
|
+
input.parentEventId || null,
|
|
2202
|
+
input.depth ?? 0
|
|
2203
|
+
]
|
|
2204
|
+
);
|
|
2205
|
+
accepted++;
|
|
2206
|
+
} catch (err) {
|
|
2207
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
this.save();
|
|
2211
|
+
return { accepted, errors };
|
|
2212
|
+
}
|
|
2213
|
+
queryEvents(options = {}) {
|
|
2214
|
+
this.initializeSync();
|
|
2215
|
+
const { limit = 100, offset = 0 } = options;
|
|
2216
|
+
const conditions = [];
|
|
2217
|
+
const params = [];
|
|
2218
|
+
if (options.schemaId) {
|
|
2219
|
+
conditions.push("schema_id = ?");
|
|
2220
|
+
params.push(options.schemaId);
|
|
2221
|
+
}
|
|
2222
|
+
if (options.eventType) {
|
|
2223
|
+
conditions.push("event_type = ?");
|
|
2224
|
+
params.push(options.eventType);
|
|
2225
|
+
}
|
|
2226
|
+
if (options.category) {
|
|
2227
|
+
conditions.push("category = ?");
|
|
2228
|
+
params.push(options.category);
|
|
2229
|
+
}
|
|
2230
|
+
if (options.service) {
|
|
2231
|
+
conditions.push("service = ?");
|
|
2232
|
+
params.push(options.service);
|
|
2233
|
+
}
|
|
2234
|
+
if (options.sessionId) {
|
|
2235
|
+
conditions.push("session_id = ?");
|
|
2236
|
+
params.push(options.sessionId);
|
|
2237
|
+
}
|
|
2238
|
+
if (options.scopeValue) {
|
|
2239
|
+
conditions.push("scope_value = ?");
|
|
2240
|
+
params.push(options.scopeValue);
|
|
2241
|
+
}
|
|
2242
|
+
if (options.scopeFrom) {
|
|
2243
|
+
conditions.push("scope_value >= ?");
|
|
2244
|
+
params.push(options.scopeFrom);
|
|
2245
|
+
}
|
|
2246
|
+
if (options.scopeTo) {
|
|
2247
|
+
conditions.push("scope_value <= ?");
|
|
2248
|
+
params.push(options.scopeTo);
|
|
2249
|
+
}
|
|
2250
|
+
if (options.severity) {
|
|
2251
|
+
conditions.push("severity = ?");
|
|
2252
|
+
params.push(options.severity);
|
|
2253
|
+
}
|
|
2254
|
+
if (options.since) {
|
|
2255
|
+
conditions.push("timestamp >= ?");
|
|
2256
|
+
params.push(options.since);
|
|
2257
|
+
}
|
|
2258
|
+
if (options.until) {
|
|
2259
|
+
conditions.push("timestamp <= ?");
|
|
2260
|
+
params.push(options.until);
|
|
2261
|
+
}
|
|
2262
|
+
if (options.search) {
|
|
2263
|
+
conditions.push("data_json LIKE ?");
|
|
2264
|
+
params.push(`%${options.search}%`);
|
|
2265
|
+
}
|
|
2266
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2267
|
+
const result = this.db.exec(
|
|
2268
|
+
`SELECT * FROM events ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
|
|
2269
|
+
[...params, limit, offset]
|
|
2270
|
+
);
|
|
2271
|
+
if (result.length === 0) return [];
|
|
2272
|
+
return result[0].values.map(
|
|
2273
|
+
(row) => this.rowToGenericEvent(result[0].columns, row)
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
queryEventsByScope(schemaId, scopeValue) {
|
|
2277
|
+
this.initializeSync();
|
|
2278
|
+
const result = this.db.exec(
|
|
2279
|
+
`SELECT * FROM events
|
|
2280
|
+
WHERE schema_id = ? AND scope_value = ?
|
|
2281
|
+
ORDER BY timestamp ASC`,
|
|
2282
|
+
[schemaId, scopeValue]
|
|
2283
|
+
);
|
|
2284
|
+
if (result.length === 0) return [];
|
|
2285
|
+
return result[0].values.map(
|
|
2286
|
+
(row) => this.rowToGenericEvent(result[0].columns, row)
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
getEventScopes(schemaId, options = {}) {
|
|
2290
|
+
this.initializeSync();
|
|
2291
|
+
const { limit = 100, offset = 0 } = options;
|
|
2292
|
+
const conditions = ["schema_id = ?"];
|
|
2293
|
+
const params = [schemaId];
|
|
2294
|
+
if (options.sessionId) {
|
|
2295
|
+
conditions.push("session_id = ?");
|
|
2296
|
+
params.push(options.sessionId);
|
|
2297
|
+
}
|
|
2298
|
+
const whereClause = `WHERE ${conditions.join(" AND ")}`;
|
|
2299
|
+
const result = this.db.exec(
|
|
2300
|
+
`SELECT
|
|
2301
|
+
scope_value,
|
|
2302
|
+
MIN(scope_ordinal) as scope_ordinal,
|
|
2303
|
+
COUNT(*) as event_count,
|
|
2304
|
+
MIN(timestamp) as first_timestamp,
|
|
2305
|
+
MAX(timestamp) as last_timestamp
|
|
2306
|
+
FROM events
|
|
2307
|
+
${whereClause}
|
|
2308
|
+
AND scope_value IS NOT NULL
|
|
2309
|
+
GROUP BY scope_value
|
|
2310
|
+
ORDER BY MIN(COALESCE(scope_ordinal, 0)) DESC, MIN(timestamp) DESC
|
|
2311
|
+
LIMIT ? OFFSET ?`,
|
|
2312
|
+
[...params, limit, offset]
|
|
2313
|
+
);
|
|
2314
|
+
if (result.length === 0) return [];
|
|
2315
|
+
const scopes = [];
|
|
2316
|
+
for (const row of result[0].values) {
|
|
2317
|
+
const scopeValue = row[0];
|
|
2318
|
+
const scopeOrdinal = row[1] != null ? row[1] : void 0;
|
|
2319
|
+
const eventCount = row[2];
|
|
2320
|
+
const firstTimestamp = row[3];
|
|
2321
|
+
const lastTimestamp = row[4];
|
|
2322
|
+
const catResult = this.db.exec(
|
|
2323
|
+
`SELECT category, COUNT(*) as count FROM events
|
|
2324
|
+
WHERE schema_id = ? AND scope_value = ?
|
|
2325
|
+
GROUP BY category`,
|
|
2326
|
+
[schemaId, scopeValue]
|
|
2327
|
+
);
|
|
2328
|
+
const categories = {};
|
|
2329
|
+
if (catResult.length > 0) {
|
|
2330
|
+
for (const catRow of catResult[0].values) {
|
|
2331
|
+
categories[catRow[0]] = catRow[1];
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
scopes.push({
|
|
2335
|
+
scopeValue,
|
|
2336
|
+
scopeOrdinal,
|
|
2337
|
+
eventCount,
|
|
2338
|
+
categories,
|
|
2339
|
+
firstTimestamp,
|
|
2340
|
+
lastTimestamp
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
return scopes;
|
|
2344
|
+
}
|
|
2345
|
+
getEventCount(options = {}) {
|
|
2346
|
+
this.initializeSync();
|
|
2347
|
+
const conditions = [];
|
|
2348
|
+
const params = [];
|
|
2349
|
+
if (options.schemaId) {
|
|
2350
|
+
conditions.push("schema_id = ?");
|
|
2351
|
+
params.push(options.schemaId);
|
|
2352
|
+
}
|
|
2353
|
+
if (options.eventType) {
|
|
2354
|
+
conditions.push("event_type = ?");
|
|
2355
|
+
params.push(options.eventType);
|
|
2356
|
+
}
|
|
2357
|
+
if (options.service) {
|
|
2358
|
+
conditions.push("service = ?");
|
|
2359
|
+
params.push(options.service);
|
|
2360
|
+
}
|
|
2361
|
+
if (options.since) {
|
|
2362
|
+
conditions.push("timestamp >= ?");
|
|
2363
|
+
params.push(options.since);
|
|
2364
|
+
}
|
|
2365
|
+
if (options.until) {
|
|
2366
|
+
conditions.push("timestamp <= ?");
|
|
2367
|
+
params.push(options.until);
|
|
2368
|
+
}
|
|
2369
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2370
|
+
const result = this.db.exec(
|
|
2371
|
+
`SELECT COUNT(*) as count FROM events ${whereClause}`,
|
|
2372
|
+
params
|
|
2373
|
+
);
|
|
2374
|
+
if (result.length === 0 || result[0].values.length === 0) return 0;
|
|
2375
|
+
return result[0].values[0][0];
|
|
2376
|
+
}
|
|
2377
|
+
pruneEvents(maxCount) {
|
|
2378
|
+
this.initializeSync();
|
|
2379
|
+
if (maxCount <= 0) return 0;
|
|
2380
|
+
const currentCount = this.getEventCount();
|
|
2381
|
+
if (currentCount <= maxCount) return 0;
|
|
2382
|
+
const deleteCount = currentCount - maxCount;
|
|
2383
|
+
this.db.run(
|
|
2384
|
+
`DELETE FROM events WHERE id IN (
|
|
2385
|
+
SELECT id FROM events ORDER BY timestamp ASC LIMIT ?
|
|
2386
|
+
)`,
|
|
2387
|
+
[deleteCount]
|
|
2388
|
+
);
|
|
2389
|
+
this.save();
|
|
2390
|
+
return deleteCount;
|
|
2391
|
+
}
|
|
2392
|
+
rowToGenericEvent(columns, row) {
|
|
2393
|
+
const obj = {};
|
|
2394
|
+
columns.forEach((col, i) => {
|
|
2395
|
+
obj[col] = row[i];
|
|
2396
|
+
});
|
|
2397
|
+
return {
|
|
2398
|
+
id: obj.id,
|
|
2399
|
+
schemaId: obj.schema_id,
|
|
2400
|
+
eventType: obj.event_type,
|
|
2401
|
+
category: obj.category,
|
|
2402
|
+
timestamp: obj.timestamp,
|
|
2403
|
+
scopeValue: obj.scope_value || void 0,
|
|
2404
|
+
scopeOrdinal: obj.scope_ordinal != null ? obj.scope_ordinal : void 0,
|
|
2405
|
+
sessionId: obj.session_id || void 0,
|
|
2406
|
+
service: obj.service,
|
|
2407
|
+
data: obj.data_json ? JSON.parse(obj.data_json) : void 0,
|
|
2408
|
+
severity: obj.severity || "info",
|
|
2409
|
+
parentEventId: obj.parent_event_id || void 0,
|
|
2410
|
+
depth: obj.depth || 0
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
close() {
|
|
2414
|
+
if (this.db) {
|
|
2415
|
+
this.save();
|
|
2416
|
+
this.db.close();
|
|
2417
|
+
this.db = null;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
};
|
|
2421
|
+
|
|
2422
|
+
// src/matcher.ts
|
|
2423
|
+
var DEFAULT_CONFIG = {
|
|
2424
|
+
minScore: 30,
|
|
2425
|
+
maxResults: 5,
|
|
2426
|
+
boostConfidence: true
|
|
2427
|
+
};
|
|
2428
|
+
var PatternMatcher = class {
|
|
2429
|
+
constructor(storage) {
|
|
2430
|
+
this.storage = storage;
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Match an incident against all patterns and return ranked results
|
|
2434
|
+
*/
|
|
2435
|
+
match(incident, config = {}) {
|
|
2436
|
+
const { minScore, maxResults, boostConfidence } = {
|
|
2437
|
+
...DEFAULT_CONFIG,
|
|
2438
|
+
...config
|
|
2439
|
+
};
|
|
2440
|
+
const patterns = this.storage.getAllPatterns({ includePrivate: true });
|
|
2441
|
+
const matches = [];
|
|
2442
|
+
for (const pattern of patterns) {
|
|
2443
|
+
if (!this.matchEnvironment(pattern, incident)) {
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
const { score, matchedCriteria } = this.scoreMatch(pattern, incident);
|
|
2447
|
+
if (score >= minScore) {
|
|
2448
|
+
let confidence = score;
|
|
2449
|
+
if (boostConfidence) {
|
|
2450
|
+
const confidenceFactor = pattern.confidence.score / 100;
|
|
2451
|
+
confidence = score * (0.5 + 0.5 * confidenceFactor);
|
|
2452
|
+
}
|
|
2453
|
+
matches.push({
|
|
2454
|
+
pattern,
|
|
2455
|
+
score,
|
|
2456
|
+
matchedCriteria,
|
|
2457
|
+
confidence: Math.round(confidence)
|
|
2458
|
+
});
|
|
2459
|
+
this.storage.updatePatternConfidence(pattern.id, "matched");
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
return matches.sort((a, b) => b.confidence - a.confidence).slice(0, maxResults);
|
|
2463
|
+
}
|
|
2464
|
+
/**
|
|
2465
|
+
* Test a pattern against historical incidents
|
|
2466
|
+
*/
|
|
2467
|
+
testPattern(pattern, limit = 100) {
|
|
2468
|
+
const incidents = this.storage.getRecentIncidents({ limit });
|
|
2469
|
+
const wouldMatch = [];
|
|
2470
|
+
let totalScore = 0;
|
|
2471
|
+
for (const incident of incidents) {
|
|
2472
|
+
if (!this.matchEnvironment(pattern, incident)) {
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
const { score } = this.scoreMatch(pattern, incident);
|
|
2476
|
+
if (score >= 30) {
|
|
2477
|
+
wouldMatch.push(incident);
|
|
2478
|
+
totalScore += score;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
return {
|
|
2482
|
+
wouldMatch,
|
|
2483
|
+
matchCount: wouldMatch.length,
|
|
2484
|
+
avgScore: wouldMatch.length > 0 ? Math.round(totalScore / wouldMatch.length) : 0
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Score how well a pattern matches an incident
|
|
2489
|
+
*/
|
|
2490
|
+
scoreMatch(pattern, incident) {
|
|
2491
|
+
let score = 0;
|
|
2492
|
+
const matchedCriteria = {
|
|
2493
|
+
symbols: [],
|
|
2494
|
+
errorKeywords: [],
|
|
2495
|
+
missingSignals: []
|
|
2496
|
+
};
|
|
2497
|
+
const symbolScore = this.matchSymbols(
|
|
2498
|
+
pattern.pattern.symbols,
|
|
2499
|
+
incident.symbols,
|
|
2500
|
+
matchedCriteria.symbols
|
|
2501
|
+
);
|
|
2502
|
+
score += Math.min(symbolScore, 50);
|
|
2503
|
+
const errorScore = this.matchErrorText(
|
|
2504
|
+
pattern,
|
|
2505
|
+
incident,
|
|
2506
|
+
matchedCriteria.errorKeywords
|
|
2507
|
+
);
|
|
2508
|
+
score += Math.min(errorScore, 25);
|
|
2509
|
+
const signalScore = this.matchMissingSignals(
|
|
2510
|
+
pattern,
|
|
2511
|
+
incident,
|
|
2512
|
+
matchedCriteria.missingSignals
|
|
2513
|
+
);
|
|
2514
|
+
score += Math.min(signalScore, 25);
|
|
2515
|
+
score = Math.min(score, 100);
|
|
2516
|
+
return { score, matchedCriteria };
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Match symbols between pattern and incident
|
|
2520
|
+
*/
|
|
2521
|
+
matchSymbols(patternSymbols, incidentSymbols, matched) {
|
|
2522
|
+
let score = 0;
|
|
2523
|
+
const symbolTypes = [
|
|
2524
|
+
"feature",
|
|
2525
|
+
"component",
|
|
2526
|
+
"flow",
|
|
2527
|
+
"gate",
|
|
2528
|
+
"signal",
|
|
2529
|
+
"state",
|
|
2530
|
+
"integration"
|
|
2531
|
+
];
|
|
2532
|
+
for (const type of symbolTypes) {
|
|
2533
|
+
const patternValue = patternSymbols[type];
|
|
2534
|
+
const incidentValue = incidentSymbols[type];
|
|
2535
|
+
if (!patternValue || !incidentValue) {
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2538
|
+
if (typeof patternValue === "string") {
|
|
2539
|
+
if (this.matchSingleSymbol(patternValue, incidentValue)) {
|
|
2540
|
+
score += patternValue.includes("*") ? 5 : 10;
|
|
2541
|
+
matched.push(type);
|
|
2542
|
+
}
|
|
2543
|
+
} else if (Array.isArray(patternValue)) {
|
|
2544
|
+
for (const pv of patternValue) {
|
|
2545
|
+
if (this.matchSingleSymbol(pv, incidentValue)) {
|
|
2546
|
+
score += 7;
|
|
2547
|
+
matched.push(type);
|
|
2548
|
+
break;
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
return score;
|
|
2554
|
+
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Match a single symbol value (supports wildcards)
|
|
2557
|
+
*/
|
|
2558
|
+
matchSingleSymbol(pattern, value) {
|
|
2559
|
+
if (pattern === "*") {
|
|
2560
|
+
return true;
|
|
2561
|
+
}
|
|
2562
|
+
if (pattern.endsWith("*")) {
|
|
2563
|
+
const prefix = pattern.slice(0, -1);
|
|
2564
|
+
return value.startsWith(prefix);
|
|
2565
|
+
}
|
|
2566
|
+
if (pattern.startsWith("*")) {
|
|
2567
|
+
const suffix = pattern.slice(1);
|
|
2568
|
+
return value.endsWith(suffix);
|
|
2569
|
+
}
|
|
2570
|
+
if (pattern.includes("*")) {
|
|
2571
|
+
const regex = new RegExp(
|
|
1402
2572
|
"^" + pattern.replace(/\*/g, ".*") + "$"
|
|
1403
2573
|
);
|
|
1404
2574
|
return regex.test(value);
|
|
@@ -1902,10 +3072,12 @@ function loadAllSeedPatterns() {
|
|
|
1902
3072
|
|
|
1903
3073
|
// src/server/index.ts
|
|
1904
3074
|
import express from "express";
|
|
1905
|
-
import * as
|
|
1906
|
-
import * as
|
|
3075
|
+
import * as http from "http";
|
|
3076
|
+
import * as path6 from "path";
|
|
3077
|
+
import * as fs5 from "fs";
|
|
1907
3078
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1908
3079
|
import chalk3 from "chalk";
|
|
3080
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
1909
3081
|
|
|
1910
3082
|
// src/server/routes/symbols.ts
|
|
1911
3083
|
import { Router } from "express";
|
|
@@ -2002,7 +3174,7 @@ async function loadParadigmConfig(projectDir) {
|
|
|
2002
3174
|
}
|
|
2003
3175
|
async function loadWithPremiseCore(projectDir) {
|
|
2004
3176
|
try {
|
|
2005
|
-
const { aggregateFromDirectory } = await import("./dist-
|
|
3177
|
+
const { aggregateFromDirectory } = await import("./dist-TYG2XME3.js");
|
|
2006
3178
|
log.flow("load-symbols").info("Using premise-core aggregator", { path: projectDir });
|
|
2007
3179
|
const result = await aggregateFromDirectory(projectDir);
|
|
2008
3180
|
const counts = {};
|
|
@@ -2591,66 +3763,916 @@ function createIncidentsRouter(_projectDir) {
|
|
|
2591
3763
|
});
|
|
2592
3764
|
res.json({ success: true });
|
|
2593
3765
|
} catch (error) {
|
|
2594
|
-
console.error("Failed to resolve incident:", error);
|
|
2595
|
-
res.status(500).json({ error: "Failed to resolve incident" });
|
|
3766
|
+
console.error("Failed to resolve incident:", error);
|
|
3767
|
+
res.status(500).json({ error: "Failed to resolve incident" });
|
|
3768
|
+
}
|
|
3769
|
+
});
|
|
3770
|
+
return router;
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
// src/server/routes/patterns.ts
|
|
3774
|
+
import { Router as Router5 } from "express";
|
|
3775
|
+
function createPatternsRouter(_projectDir) {
|
|
3776
|
+
const router = Router5();
|
|
3777
|
+
const storage = new SentinelStorage();
|
|
3778
|
+
router.get("/", async (req, res) => {
|
|
3779
|
+
try {
|
|
3780
|
+
const source = req.query.source;
|
|
3781
|
+
const symbol = req.query.symbol;
|
|
3782
|
+
const minConfidence = parseInt(req.query.minConfidence) || void 0;
|
|
3783
|
+
const options = {};
|
|
3784
|
+
if (source && ["manual", "suggested", "imported", "community"].includes(source)) {
|
|
3785
|
+
options.source = source;
|
|
3786
|
+
}
|
|
3787
|
+
if (symbol) options.symbol = symbol;
|
|
3788
|
+
if (minConfidence) options.minConfidence = minConfidence;
|
|
3789
|
+
const patterns = storage.getAllPatterns(options);
|
|
3790
|
+
const summaries = patterns.map((pattern) => ({
|
|
3791
|
+
id: pattern.id,
|
|
3792
|
+
name: pattern.name,
|
|
3793
|
+
description: pattern.description,
|
|
3794
|
+
confidence: {
|
|
3795
|
+
score: pattern.confidence.score,
|
|
3796
|
+
timesMatched: pattern.confidence.timesMatched,
|
|
3797
|
+
timesResolved: pattern.confidence.timesResolved
|
|
3798
|
+
},
|
|
3799
|
+
tags: pattern.tags
|
|
3800
|
+
}));
|
|
3801
|
+
res.json({ patterns: summaries });
|
|
3802
|
+
} catch (error) {
|
|
3803
|
+
console.error("Failed to load patterns:", error);
|
|
3804
|
+
res.status(500).json({ error: "Failed to load patterns" });
|
|
3805
|
+
}
|
|
3806
|
+
});
|
|
3807
|
+
router.get("/:id", async (req, res) => {
|
|
3808
|
+
try {
|
|
3809
|
+
const pattern = storage.getPattern(req.params.id);
|
|
3810
|
+
if (!pattern) {
|
|
3811
|
+
res.status(404).json({ error: "Pattern not found" });
|
|
3812
|
+
return;
|
|
3813
|
+
}
|
|
3814
|
+
res.json({ pattern });
|
|
3815
|
+
} catch (error) {
|
|
3816
|
+
console.error("Failed to load pattern:", error);
|
|
3817
|
+
res.status(500).json({ error: "Failed to load pattern" });
|
|
3818
|
+
}
|
|
3819
|
+
});
|
|
3820
|
+
return router;
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
// src/server/routes/logs.ts
|
|
3824
|
+
import { Router as Router6 } from "express";
|
|
3825
|
+
import { v4 as uuidv42 } from "uuid";
|
|
3826
|
+
function inferSymbolType(symbol) {
|
|
3827
|
+
if (symbol.startsWith("#")) return "component";
|
|
3828
|
+
if (symbol.startsWith("^")) return "gate";
|
|
3829
|
+
if (symbol.startsWith("!")) return "signal";
|
|
3830
|
+
if (symbol.startsWith("$")) return "flow";
|
|
3831
|
+
if (symbol.startsWith("~")) return "aspect";
|
|
3832
|
+
return "raw";
|
|
3833
|
+
}
|
|
3834
|
+
function validateSymbol(symbol, index) {
|
|
3835
|
+
const entry = index.find((e) => e.symbol === symbol);
|
|
3836
|
+
if (entry) return { known: true };
|
|
3837
|
+
const symbolName = symbol.replace(/^[#^!$~]/, "");
|
|
3838
|
+
let bestMatch;
|
|
3839
|
+
let bestScore = 0;
|
|
3840
|
+
for (const e of index) {
|
|
3841
|
+
const eName = e.symbol.replace(/^[#^!$~]/, "");
|
|
3842
|
+
let shared = 0;
|
|
3843
|
+
for (let i = 0; i < Math.min(symbolName.length, eName.length); i++) {
|
|
3844
|
+
if (symbolName[i] === eName[i]) shared++;
|
|
3845
|
+
else break;
|
|
3846
|
+
}
|
|
3847
|
+
const score = shared / Math.max(symbolName.length, eName.length);
|
|
3848
|
+
if (score > bestScore && score > 0.5) {
|
|
3849
|
+
bestScore = score;
|
|
3850
|
+
bestMatch = e.symbol;
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
return { known: false, suggestion: bestMatch };
|
|
3854
|
+
}
|
|
3855
|
+
function autoPromoteToIncident(entry, storage) {
|
|
3856
|
+
try {
|
|
3857
|
+
const symbolType = inferSymbolType(entry.symbol);
|
|
3858
|
+
const symbols = {};
|
|
3859
|
+
if (symbolType === "component") symbols.component = entry.symbol;
|
|
3860
|
+
else if (symbolType === "gate") symbols.gate = entry.symbol;
|
|
3861
|
+
else if (symbolType === "signal") symbols.signal = entry.symbol;
|
|
3862
|
+
else if (symbolType === "flow") symbols.flow = entry.symbol;
|
|
3863
|
+
else symbols.component = entry.symbol;
|
|
3864
|
+
storage.recordIncident({
|
|
3865
|
+
error: {
|
|
3866
|
+
message: entry.message,
|
|
3867
|
+
type: "LogError"
|
|
3868
|
+
},
|
|
3869
|
+
symbols,
|
|
3870
|
+
environment: entry.environment || "unknown",
|
|
3871
|
+
service: entry.service
|
|
3872
|
+
});
|
|
3873
|
+
} catch {
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
var insertsSincePrune = 0;
|
|
3877
|
+
function createLogsRouter(options) {
|
|
3878
|
+
const router = Router6();
|
|
3879
|
+
const { storage, serverConfig, onLogReceived, symbolIndex } = options;
|
|
3880
|
+
router.post("/", async (req, res) => {
|
|
3881
|
+
try {
|
|
3882
|
+
const body = req.body;
|
|
3883
|
+
let entries;
|
|
3884
|
+
if (Array.isArray(body.entries)) {
|
|
3885
|
+
entries = body.entries;
|
|
3886
|
+
} else if (body.level && body.symbol && body.message && body.service) {
|
|
3887
|
+
entries = [body];
|
|
3888
|
+
} else {
|
|
3889
|
+
res.status(400).json({ error: "Expected {entries: [...]} or a single log entry with level, symbol, message, service" });
|
|
3890
|
+
return;
|
|
3891
|
+
}
|
|
3892
|
+
if (entries.length > serverConfig.maxBatchSize) {
|
|
3893
|
+
res.status(413).json({
|
|
3894
|
+
error: `Batch too large: ${entries.length} entries, max ${serverConfig.maxBatchSize}`
|
|
3895
|
+
});
|
|
3896
|
+
return;
|
|
3897
|
+
}
|
|
3898
|
+
for (let i = 0; i < entries.length; i++) {
|
|
3899
|
+
const e = entries[i];
|
|
3900
|
+
if (!e.level || !e.symbol || !e.message || !e.service) {
|
|
3901
|
+
res.status(400).json({
|
|
3902
|
+
error: `Entry ${i}: missing required fields (level, symbol, message, service)`
|
|
3903
|
+
});
|
|
3904
|
+
return;
|
|
3905
|
+
}
|
|
3906
|
+
if (!["debug", "info", "warn", "error"].includes(e.level)) {
|
|
3907
|
+
res.status(400).json({
|
|
3908
|
+
error: `Entry ${i}: invalid level "${e.level}", must be debug|info|warn|error`
|
|
3909
|
+
});
|
|
3910
|
+
return;
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
const result = storage.insertLogBatch(entries);
|
|
3914
|
+
for (const input of entries) {
|
|
3915
|
+
const entry = {
|
|
3916
|
+
id: input.id || uuidv42(),
|
|
3917
|
+
timestamp: input.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
3918
|
+
level: input.level,
|
|
3919
|
+
symbol: input.symbol,
|
|
3920
|
+
symbolType: input.symbolType || inferSymbolType(input.symbol),
|
|
3921
|
+
message: input.message,
|
|
3922
|
+
data: input.data,
|
|
3923
|
+
service: input.service,
|
|
3924
|
+
sessionId: input.sessionId,
|
|
3925
|
+
correlationId: input.correlationId,
|
|
3926
|
+
durationMs: input.durationMs,
|
|
3927
|
+
environment: input.environment
|
|
3928
|
+
};
|
|
3929
|
+
let validation;
|
|
3930
|
+
if (symbolIndex) {
|
|
3931
|
+
validation = validateSymbol(entry.symbol, symbolIndex);
|
|
3932
|
+
}
|
|
3933
|
+
if (entry.level === "error") {
|
|
3934
|
+
autoPromoteToIncident(entry, storage);
|
|
3935
|
+
}
|
|
3936
|
+
if (onLogReceived) {
|
|
3937
|
+
onLogReceived(entry, validation);
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
insertsSincePrune += result.accepted;
|
|
3941
|
+
if (serverConfig.maxLogs > 0 && insertsSincePrune >= serverConfig.pruneIntervalInserts) {
|
|
3942
|
+
insertsSincePrune = 0;
|
|
3943
|
+
storage.pruneLogs(serverConfig.maxLogs);
|
|
3944
|
+
}
|
|
3945
|
+
res.json({ accepted: result.accepted, errors: result.errors.length > 0 ? result.errors : void 0 });
|
|
3946
|
+
} catch (error) {
|
|
3947
|
+
res.status(500).json({ error: "Failed to insert logs" });
|
|
3948
|
+
}
|
|
3949
|
+
});
|
|
3950
|
+
router.get("/", async (req, res) => {
|
|
3951
|
+
try {
|
|
3952
|
+
const options2 = {
|
|
3953
|
+
level: req.query.level,
|
|
3954
|
+
symbol: req.query.symbol,
|
|
3955
|
+
service: req.query.service,
|
|
3956
|
+
sessionId: req.query.sessionId,
|
|
3957
|
+
correlationId: req.query.correlationId,
|
|
3958
|
+
search: req.query.search,
|
|
3959
|
+
since: req.query.since,
|
|
3960
|
+
until: req.query.until,
|
|
3961
|
+
limit: req.query.limit ? parseInt(req.query.limit) : 100,
|
|
3962
|
+
offset: req.query.offset ? parseInt(req.query.offset) : 0
|
|
3963
|
+
};
|
|
3964
|
+
const logs = storage.queryLogs(options2);
|
|
3965
|
+
const total = storage.getLogCount(options2);
|
|
3966
|
+
res.json({ count: logs.length, total, logs });
|
|
3967
|
+
} catch (error) {
|
|
3968
|
+
res.status(500).json({ error: "Failed to query logs" });
|
|
3969
|
+
}
|
|
3970
|
+
});
|
|
3971
|
+
return router;
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
// src/server/routes/services.ts
|
|
3975
|
+
import { Router as Router7 } from "express";
|
|
3976
|
+
function createServicesRouter(options) {
|
|
3977
|
+
const router = Router7();
|
|
3978
|
+
const { storage } = options;
|
|
3979
|
+
router.post("/", async (req, res) => {
|
|
3980
|
+
try {
|
|
3981
|
+
const { name, version, pid, environment, metadata } = req.body;
|
|
3982
|
+
if (!name) {
|
|
3983
|
+
res.status(400).json({ error: "Missing required field: name" });
|
|
3984
|
+
return;
|
|
3985
|
+
}
|
|
3986
|
+
storage.registerService({ name, version, pid, environment, metadata });
|
|
3987
|
+
res.json({ success: true, service: name });
|
|
3988
|
+
} catch (error) {
|
|
3989
|
+
res.status(500).json({ error: "Failed to register service" });
|
|
3990
|
+
}
|
|
3991
|
+
});
|
|
3992
|
+
router.get("/", async (_req, res) => {
|
|
3993
|
+
try {
|
|
3994
|
+
const services = storage.getServices();
|
|
3995
|
+
res.json({ count: services.length, services });
|
|
3996
|
+
} catch (error) {
|
|
3997
|
+
res.status(500).json({ error: "Failed to list services" });
|
|
3998
|
+
}
|
|
3999
|
+
});
|
|
4000
|
+
return router;
|
|
4001
|
+
}
|
|
4002
|
+
function createStateRouter(options) {
|
|
4003
|
+
const router = Router7();
|
|
4004
|
+
const { storage } = options;
|
|
4005
|
+
router.post("/", async (req, res) => {
|
|
4006
|
+
try {
|
|
4007
|
+
const { service, sessionId, state, activeFlows, activeGates } = req.body;
|
|
4008
|
+
if (!service || !sessionId || !state) {
|
|
4009
|
+
res.status(400).json({ error: "Missing required fields: service, sessionId, state" });
|
|
4010
|
+
return;
|
|
4011
|
+
}
|
|
4012
|
+
storage.upsertAppState({
|
|
4013
|
+
service,
|
|
4014
|
+
sessionId,
|
|
4015
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4016
|
+
state,
|
|
4017
|
+
activeFlows,
|
|
4018
|
+
activeGates
|
|
4019
|
+
});
|
|
4020
|
+
storage.updateServiceLastSeen(service);
|
|
4021
|
+
res.json({ success: true });
|
|
4022
|
+
} catch (error) {
|
|
4023
|
+
res.status(500).json({ error: "Failed to update state" });
|
|
4024
|
+
}
|
|
4025
|
+
});
|
|
4026
|
+
router.get("/", async (_req, res) => {
|
|
4027
|
+
try {
|
|
4028
|
+
const states = storage.getAllAppStates();
|
|
4029
|
+
res.json({ count: states.length, states });
|
|
4030
|
+
} catch (error) {
|
|
4031
|
+
res.status(500).json({ error: "Failed to get states" });
|
|
4032
|
+
}
|
|
4033
|
+
});
|
|
4034
|
+
router.get("/:service", async (req, res) => {
|
|
4035
|
+
try {
|
|
4036
|
+
const states = storage.getAppState(req.params.service);
|
|
4037
|
+
res.json({ count: states.length, states });
|
|
4038
|
+
} catch (error) {
|
|
4039
|
+
res.status(500).json({ error: "Failed to get service state" });
|
|
4040
|
+
}
|
|
4041
|
+
});
|
|
4042
|
+
return router;
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
// src/server/routes/metrics.ts
|
|
4046
|
+
import { Router as Router8 } from "express";
|
|
4047
|
+
var VALID_METRIC_TYPES = ["counter", "gauge", "histogram"];
|
|
4048
|
+
function createMetricsRouter(options) {
|
|
4049
|
+
const router = Router8();
|
|
4050
|
+
const { storage, serverConfig } = options;
|
|
4051
|
+
router.post("/", (req, res) => {
|
|
4052
|
+
try {
|
|
4053
|
+
const body = req.body;
|
|
4054
|
+
let entries;
|
|
4055
|
+
if (Array.isArray(body.entries)) {
|
|
4056
|
+
entries = body.entries;
|
|
4057
|
+
} else if (body.name && body.type && body.value !== void 0 && body.service) {
|
|
4058
|
+
entries = [body];
|
|
4059
|
+
} else {
|
|
4060
|
+
res.status(400).json({
|
|
4061
|
+
error: "Expected {entries: [...]} or a single metric with name, type, value, service"
|
|
4062
|
+
});
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
4065
|
+
if (entries.length > serverConfig.maxBatchSize) {
|
|
4066
|
+
res.status(413).json({
|
|
4067
|
+
error: `Batch too large: ${entries.length} entries, max ${serverConfig.maxBatchSize}`
|
|
4068
|
+
});
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
for (let i = 0; i < entries.length; i++) {
|
|
4072
|
+
const e = entries[i];
|
|
4073
|
+
if (!e.name || !e.type || e.value === void 0 || !e.service) {
|
|
4074
|
+
res.status(400).json({
|
|
4075
|
+
error: `Entry ${i}: missing required fields (name, type, value, service)`
|
|
4076
|
+
});
|
|
4077
|
+
return;
|
|
4078
|
+
}
|
|
4079
|
+
if (!VALID_METRIC_TYPES.includes(e.type)) {
|
|
4080
|
+
res.status(400).json({
|
|
4081
|
+
error: `Entry ${i}: invalid type "${e.type}", must be counter|gauge|histogram`
|
|
4082
|
+
});
|
|
4083
|
+
return;
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
const result = storage.insertMetricBatch(entries);
|
|
4087
|
+
res.json({
|
|
4088
|
+
accepted: result.accepted,
|
|
4089
|
+
errors: result.errors.length > 0 ? result.errors : void 0
|
|
4090
|
+
});
|
|
4091
|
+
} catch {
|
|
4092
|
+
res.status(500).json({ error: "Failed to insert metrics" });
|
|
4093
|
+
}
|
|
4094
|
+
});
|
|
4095
|
+
router.get("/", (req, res) => {
|
|
4096
|
+
try {
|
|
4097
|
+
const options2 = {
|
|
4098
|
+
name: req.query.name,
|
|
4099
|
+
type: req.query.type,
|
|
4100
|
+
service: req.query.service,
|
|
4101
|
+
tag: req.query.tag,
|
|
4102
|
+
since: req.query.since,
|
|
4103
|
+
until: req.query.until,
|
|
4104
|
+
limit: req.query.limit ? parseInt(req.query.limit) : 100,
|
|
4105
|
+
offset: req.query.offset ? parseInt(req.query.offset) : 0
|
|
4106
|
+
};
|
|
4107
|
+
const metrics = storage.queryMetrics(options2);
|
|
4108
|
+
const total = storage.getMetricCount(options2);
|
|
4109
|
+
res.json({ count: metrics.length, total, metrics });
|
|
4110
|
+
} catch {
|
|
4111
|
+
res.status(500).json({ error: "Failed to query metrics" });
|
|
4112
|
+
}
|
|
4113
|
+
});
|
|
4114
|
+
router.get("/aggregate/:name", (req, res) => {
|
|
4115
|
+
try {
|
|
4116
|
+
const aggregation = storage.aggregateMetric(req.params.name, {
|
|
4117
|
+
service: req.query.service,
|
|
4118
|
+
since: req.query.since,
|
|
4119
|
+
until: req.query.until
|
|
4120
|
+
});
|
|
4121
|
+
res.json(aggregation);
|
|
4122
|
+
} catch {
|
|
4123
|
+
res.status(500).json({ error: "Failed to aggregate metric" });
|
|
4124
|
+
}
|
|
4125
|
+
});
|
|
4126
|
+
return router;
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
// src/server/routes/traces.ts
|
|
4130
|
+
import { Router as Router9 } from "express";
|
|
4131
|
+
function createTracesRouter(options) {
|
|
4132
|
+
const router = Router9();
|
|
4133
|
+
const { storage } = options;
|
|
4134
|
+
router.post("/", (req, res) => {
|
|
4135
|
+
try {
|
|
4136
|
+
const body = req.body;
|
|
4137
|
+
if (!body.traceId || !body.service || !body.symbol || !body.operation) {
|
|
4138
|
+
res.status(400).json({
|
|
4139
|
+
error: "Missing required fields: traceId, service, symbol, operation"
|
|
4140
|
+
});
|
|
4141
|
+
return;
|
|
4142
|
+
}
|
|
4143
|
+
const spanId = storage.insertSpan(body);
|
|
4144
|
+
res.json({ spanId, traceId: body.traceId });
|
|
4145
|
+
} catch {
|
|
4146
|
+
res.status(500).json({ error: "Failed to insert trace span" });
|
|
4147
|
+
}
|
|
4148
|
+
});
|
|
4149
|
+
router.get("/", (req, res) => {
|
|
4150
|
+
try {
|
|
4151
|
+
const traces = storage.queryTraces({
|
|
4152
|
+
service: req.query.service,
|
|
4153
|
+
symbol: req.query.symbol,
|
|
4154
|
+
since: req.query.since,
|
|
4155
|
+
limit: req.query.limit ? parseInt(req.query.limit) : 20
|
|
4156
|
+
});
|
|
4157
|
+
res.json({ count: traces.length, traces });
|
|
4158
|
+
} catch {
|
|
4159
|
+
res.status(500).json({ error: "Failed to query traces" });
|
|
4160
|
+
}
|
|
4161
|
+
});
|
|
4162
|
+
router.get("/:traceId", (req, res) => {
|
|
4163
|
+
try {
|
|
4164
|
+
const trace = storage.getTrace(req.params.traceId);
|
|
4165
|
+
if (!trace) {
|
|
4166
|
+
res.status(404).json({ error: "Trace not found" });
|
|
4167
|
+
return;
|
|
4168
|
+
}
|
|
4169
|
+
res.json(trace);
|
|
4170
|
+
} catch {
|
|
4171
|
+
res.status(500).json({ error: "Failed to get trace" });
|
|
4172
|
+
}
|
|
4173
|
+
});
|
|
4174
|
+
return router;
|
|
4175
|
+
}
|
|
4176
|
+
|
|
4177
|
+
// src/server/routes/schemas.ts
|
|
4178
|
+
import { Router as Router10 } from "express";
|
|
4179
|
+
function createSchemasRouter(options) {
|
|
4180
|
+
const router = Router10();
|
|
4181
|
+
const { storage } = options;
|
|
4182
|
+
router.post("/", (req, res) => {
|
|
4183
|
+
try {
|
|
4184
|
+
const body = req.body;
|
|
4185
|
+
if (!body.id || !body.version || !body.name || !body.scope || !body.eventTypes) {
|
|
4186
|
+
res.status(400).json({
|
|
4187
|
+
error: "Missing required fields: id, version, name, scope, eventTypes"
|
|
4188
|
+
});
|
|
4189
|
+
return;
|
|
4190
|
+
}
|
|
4191
|
+
if (!body.scope.field || !body.scope.type || !body.scope.label || !body.scope.ordering) {
|
|
4192
|
+
res.status(400).json({
|
|
4193
|
+
error: "Invalid scope: requires field, type, label, ordering"
|
|
4194
|
+
});
|
|
4195
|
+
return;
|
|
4196
|
+
}
|
|
4197
|
+
if (!Array.isArray(body.eventTypes) || body.eventTypes.length === 0) {
|
|
4198
|
+
res.status(400).json({
|
|
4199
|
+
error: "eventTypes must be a non-empty array"
|
|
4200
|
+
});
|
|
4201
|
+
return;
|
|
4202
|
+
}
|
|
4203
|
+
for (let i = 0; i < body.eventTypes.length; i++) {
|
|
4204
|
+
const et = body.eventTypes[i];
|
|
4205
|
+
if (!et.type || !et.category) {
|
|
4206
|
+
res.status(400).json({
|
|
4207
|
+
error: `eventTypes[${i}]: missing required fields (type, category)`
|
|
4208
|
+
});
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
const schema = storage.registerSchema(body);
|
|
4213
|
+
res.status(201).json(schema);
|
|
4214
|
+
} catch (error) {
|
|
4215
|
+
res.status(500).json({ error: "Failed to register schema" });
|
|
4216
|
+
}
|
|
4217
|
+
});
|
|
4218
|
+
router.get("/", (_req, res) => {
|
|
4219
|
+
try {
|
|
4220
|
+
const schemas = storage.listSchemas();
|
|
4221
|
+
res.json({ count: schemas.length, schemas });
|
|
4222
|
+
} catch (error) {
|
|
4223
|
+
res.status(500).json({ error: "Failed to list schemas" });
|
|
4224
|
+
}
|
|
4225
|
+
});
|
|
4226
|
+
router.get("/:id", (req, res) => {
|
|
4227
|
+
try {
|
|
4228
|
+
const schema = storage.getSchema(req.params.id);
|
|
4229
|
+
if (!schema) {
|
|
4230
|
+
res.status(404).json({ error: "Schema not found" });
|
|
4231
|
+
return;
|
|
4232
|
+
}
|
|
4233
|
+
res.json(schema);
|
|
4234
|
+
} catch (error) {
|
|
4235
|
+
res.status(500).json({ error: "Failed to get schema" });
|
|
4236
|
+
}
|
|
4237
|
+
});
|
|
4238
|
+
return router;
|
|
4239
|
+
}
|
|
4240
|
+
|
|
4241
|
+
// src/server/routes/events.ts
|
|
4242
|
+
import { Router as Router11 } from "express";
|
|
4243
|
+
function createEventsRouter(options) {
|
|
4244
|
+
const router = Router11();
|
|
4245
|
+
const { storage, serverConfig, onEventReceived } = options;
|
|
4246
|
+
let insertsSincePrune2 = 0;
|
|
4247
|
+
router.post("/", (req, res) => {
|
|
4248
|
+
try {
|
|
4249
|
+
const body = req.body;
|
|
4250
|
+
if (!body.schemaId || !body.service || !Array.isArray(body.events)) {
|
|
4251
|
+
res.status(400).json({
|
|
4252
|
+
error: "Expected { schemaId, service, events: [...] }"
|
|
4253
|
+
});
|
|
4254
|
+
return;
|
|
4255
|
+
}
|
|
4256
|
+
const { schemaId, service, events } = body;
|
|
4257
|
+
const schema = storage.getSchema(schemaId);
|
|
4258
|
+
if (!schema) {
|
|
4259
|
+
res.status(404).json({
|
|
4260
|
+
error: `Schema "${schemaId}" not found. Register it first via POST /api/schemas.`
|
|
4261
|
+
});
|
|
4262
|
+
return;
|
|
4263
|
+
}
|
|
4264
|
+
if (events.length > serverConfig.maxBatchSize) {
|
|
4265
|
+
res.status(413).json({
|
|
4266
|
+
error: `Batch too large: ${events.length} events, max ${serverConfig.maxBatchSize}`
|
|
4267
|
+
});
|
|
4268
|
+
return;
|
|
4269
|
+
}
|
|
4270
|
+
for (let i = 0; i < events.length; i++) {
|
|
4271
|
+
const e = events[i];
|
|
4272
|
+
if (!e.type) {
|
|
4273
|
+
res.status(400).json({
|
|
4274
|
+
error: `events[${i}]: missing required field "type"`
|
|
4275
|
+
});
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
const result = storage.insertEventBatch(schemaId, service, events);
|
|
4280
|
+
if (onEventReceived) {
|
|
4281
|
+
const typeMap = /* @__PURE__ */ new Map();
|
|
4282
|
+
for (const et of schema.eventTypes) {
|
|
4283
|
+
typeMap.set(et.type, et.category);
|
|
4284
|
+
}
|
|
4285
|
+
for (const input of events) {
|
|
4286
|
+
const evt = {
|
|
4287
|
+
id: input.id || "",
|
|
4288
|
+
schemaId,
|
|
4289
|
+
eventType: input.type,
|
|
4290
|
+
category: typeMap.get(input.type) || "unknown",
|
|
4291
|
+
timestamp: input.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
4292
|
+
scopeValue: input.scopeValue != null ? String(input.scopeValue) : void 0,
|
|
4293
|
+
sessionId: input.sessionId,
|
|
4294
|
+
service,
|
|
4295
|
+
data: input.data,
|
|
4296
|
+
severity: input.severity || "info",
|
|
4297
|
+
parentEventId: input.parentEventId,
|
|
4298
|
+
depth: input.depth
|
|
4299
|
+
};
|
|
4300
|
+
onEventReceived(evt);
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
insertsSincePrune2 += result.accepted;
|
|
4304
|
+
if (serverConfig.maxLogs > 0 && insertsSincePrune2 >= serverConfig.pruneIntervalInserts) {
|
|
4305
|
+
insertsSincePrune2 = 0;
|
|
4306
|
+
storage.pruneEvents(serverConfig.maxLogs);
|
|
4307
|
+
}
|
|
4308
|
+
res.json({
|
|
4309
|
+
accepted: result.accepted,
|
|
4310
|
+
errors: result.errors.length > 0 ? result.errors : void 0
|
|
4311
|
+
});
|
|
4312
|
+
} catch (error) {
|
|
4313
|
+
res.status(500).json({ error: "Failed to ingest events" });
|
|
4314
|
+
}
|
|
4315
|
+
});
|
|
4316
|
+
router.get("/", (req, res) => {
|
|
4317
|
+
try {
|
|
4318
|
+
const query = {
|
|
4319
|
+
schemaId: req.query.schemaId,
|
|
4320
|
+
eventType: req.query.eventType,
|
|
4321
|
+
category: req.query.category,
|
|
4322
|
+
service: req.query.service,
|
|
4323
|
+
sessionId: req.query.sessionId,
|
|
4324
|
+
scopeValue: req.query.scopeValue,
|
|
4325
|
+
scopeFrom: req.query.scopeFrom,
|
|
4326
|
+
scopeTo: req.query.scopeTo,
|
|
4327
|
+
severity: req.query.severity,
|
|
4328
|
+
since: req.query.since,
|
|
4329
|
+
until: req.query.until,
|
|
4330
|
+
search: req.query.search,
|
|
4331
|
+
limit: req.query.limit ? parseInt(req.query.limit) : 100,
|
|
4332
|
+
offset: req.query.offset ? parseInt(req.query.offset) : 0
|
|
4333
|
+
};
|
|
4334
|
+
const events = storage.queryEvents(query);
|
|
4335
|
+
const total = storage.getEventCount(query);
|
|
4336
|
+
res.json({ count: events.length, total, events });
|
|
4337
|
+
} catch (error) {
|
|
4338
|
+
res.status(500).json({ error: "Failed to query events" });
|
|
2596
4339
|
}
|
|
2597
4340
|
});
|
|
2598
|
-
|
|
2599
|
-
}
|
|
2600
|
-
|
|
2601
|
-
// src/server/routes/patterns.ts
|
|
2602
|
-
import { Router as Router5 } from "express";
|
|
2603
|
-
function createPatternsRouter(_projectDir) {
|
|
2604
|
-
const router = Router5();
|
|
2605
|
-
const storage = new SentinelStorage();
|
|
2606
|
-
router.get("/", async (req, res) => {
|
|
4341
|
+
router.get("/scopes", (req, res) => {
|
|
2607
4342
|
try {
|
|
2608
|
-
const
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
if (source && ["manual", "suggested", "imported", "community"].includes(source)) {
|
|
2613
|
-
options.source = source;
|
|
4343
|
+
const schemaId = req.query.schemaId;
|
|
4344
|
+
if (!schemaId) {
|
|
4345
|
+
res.status(400).json({ error: "schemaId query parameter is required" });
|
|
4346
|
+
return;
|
|
2614
4347
|
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
description: pattern.description,
|
|
2622
|
-
confidence: {
|
|
2623
|
-
score: pattern.confidence.score,
|
|
2624
|
-
timesMatched: pattern.confidence.timesMatched,
|
|
2625
|
-
timesResolved: pattern.confidence.timesResolved
|
|
2626
|
-
},
|
|
2627
|
-
tags: pattern.tags
|
|
2628
|
-
}));
|
|
2629
|
-
res.json({ patterns: summaries });
|
|
4348
|
+
const scopes = storage.getEventScopes(schemaId, {
|
|
4349
|
+
limit: req.query.limit ? parseInt(req.query.limit) : 100,
|
|
4350
|
+
offset: req.query.offset ? parseInt(req.query.offset) : 0,
|
|
4351
|
+
sessionId: req.query.sessionId
|
|
4352
|
+
});
|
|
4353
|
+
res.json({ count: scopes.length, scopes });
|
|
2630
4354
|
} catch (error) {
|
|
2631
|
-
|
|
2632
|
-
res.status(500).json({ error: "Failed to load patterns" });
|
|
4355
|
+
res.status(500).json({ error: "Failed to query scopes" });
|
|
2633
4356
|
}
|
|
2634
4357
|
});
|
|
2635
|
-
router.get("/:
|
|
4358
|
+
router.get("/scope/:value", (req, res) => {
|
|
2636
4359
|
try {
|
|
2637
|
-
const
|
|
2638
|
-
if (!
|
|
2639
|
-
res.status(
|
|
4360
|
+
const schemaId = req.query.schemaId;
|
|
4361
|
+
if (!schemaId) {
|
|
4362
|
+
res.status(400).json({ error: "schemaId query parameter is required" });
|
|
2640
4363
|
return;
|
|
2641
4364
|
}
|
|
2642
|
-
|
|
4365
|
+
const events = storage.queryEventsByScope(schemaId, req.params.value);
|
|
4366
|
+
res.json({ count: events.length, events });
|
|
2643
4367
|
} catch (error) {
|
|
2644
|
-
|
|
2645
|
-
res.status(500).json({ error: "Failed to load pattern" });
|
|
4368
|
+
res.status(500).json({ error: "Failed to query scope events" });
|
|
2646
4369
|
}
|
|
2647
4370
|
});
|
|
2648
4371
|
return router;
|
|
2649
4372
|
}
|
|
2650
4373
|
|
|
4374
|
+
// src/server/middleware/auth.ts
|
|
4375
|
+
function createAuthMiddleware(config) {
|
|
4376
|
+
return function authMiddleware(requiredPermission) {
|
|
4377
|
+
return (req, res, next) => {
|
|
4378
|
+
if (!config.enabled) {
|
|
4379
|
+
next();
|
|
4380
|
+
return;
|
|
4381
|
+
}
|
|
4382
|
+
const authHeader = req.headers.authorization;
|
|
4383
|
+
if (!authHeader) {
|
|
4384
|
+
res.status(401).json({ error: "Authentication required. Provide Authorization: Bearer <token>" });
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4387
|
+
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
4388
|
+
if (!match) {
|
|
4389
|
+
res.status(401).json({ error: "Invalid authorization format. Use: Bearer <token>" });
|
|
4390
|
+
return;
|
|
4391
|
+
}
|
|
4392
|
+
const tokenValue = match[1];
|
|
4393
|
+
const tokenEntry = config.tokens.find((t) => t.token === tokenValue);
|
|
4394
|
+
if (!tokenEntry) {
|
|
4395
|
+
res.status(401).json({ error: "Invalid token" });
|
|
4396
|
+
return;
|
|
4397
|
+
}
|
|
4398
|
+
if (tokenEntry.expiresAt && new Date(tokenEntry.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
4399
|
+
res.status(401).json({ error: "Token expired" });
|
|
4400
|
+
return;
|
|
4401
|
+
}
|
|
4402
|
+
const permissionLevel = { read: 1, write: 2, admin: 3 };
|
|
4403
|
+
const hasPermission = tokenEntry.permissions.some(
|
|
4404
|
+
(p) => permissionLevel[p] >= permissionLevel[requiredPermission]
|
|
4405
|
+
);
|
|
4406
|
+
if (!hasPermission) {
|
|
4407
|
+
res.status(403).json({ error: `Insufficient permissions. Required: ${requiredPermission}` });
|
|
4408
|
+
return;
|
|
4409
|
+
}
|
|
4410
|
+
req.authToken = tokenEntry;
|
|
4411
|
+
next();
|
|
4412
|
+
};
|
|
4413
|
+
};
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4416
|
+
// src/server/middleware/rate-limit.ts
|
|
4417
|
+
var serviceWindows = /* @__PURE__ */ new Map();
|
|
4418
|
+
var globalWindow = { count: 0, windowStart: Date.now() };
|
|
4419
|
+
var WINDOW_MS = 6e4;
|
|
4420
|
+
function getOrResetWindow(entry) {
|
|
4421
|
+
const now = Date.now();
|
|
4422
|
+
if (now - entry.windowStart > WINDOW_MS) {
|
|
4423
|
+
entry.count = 0;
|
|
4424
|
+
entry.windowStart = now;
|
|
4425
|
+
}
|
|
4426
|
+
return entry;
|
|
4427
|
+
}
|
|
4428
|
+
function createRateLimiter(config) {
|
|
4429
|
+
return (req, res, next) => {
|
|
4430
|
+
if (!config.enabled) {
|
|
4431
|
+
next();
|
|
4432
|
+
return;
|
|
4433
|
+
}
|
|
4434
|
+
const service = req.body?.service || req.body?.entries?.[0]?.service || req.query.service || "_unknown";
|
|
4435
|
+
const rule = config.perService[service] || config.global;
|
|
4436
|
+
if (rule.samplingRate < 1 && Math.random() > rule.samplingRate) {
|
|
4437
|
+
res.status(200).json({ accepted: 0, sampled: true, message: "Request dropped by sampling" });
|
|
4438
|
+
return;
|
|
4439
|
+
}
|
|
4440
|
+
const gw = getOrResetWindow(globalWindow);
|
|
4441
|
+
if (gw.count >= config.global.maxRequestsPerMinute) {
|
|
4442
|
+
res.status(429).json({
|
|
4443
|
+
error: "Global rate limit exceeded",
|
|
4444
|
+
retryAfterMs: WINDOW_MS - (Date.now() - gw.windowStart)
|
|
4445
|
+
});
|
|
4446
|
+
return;
|
|
4447
|
+
}
|
|
4448
|
+
if (!serviceWindows.has(service)) {
|
|
4449
|
+
serviceWindows.set(service, { count: 0, windowStart: Date.now() });
|
|
4450
|
+
}
|
|
4451
|
+
const sw = getOrResetWindow(serviceWindows.get(service));
|
|
4452
|
+
if (sw.count >= rule.maxRequestsPerMinute) {
|
|
4453
|
+
res.status(429).json({
|
|
4454
|
+
error: `Rate limit exceeded for service: ${service}`,
|
|
4455
|
+
retryAfterMs: WINDOW_MS - (Date.now() - sw.windowStart)
|
|
4456
|
+
});
|
|
4457
|
+
return;
|
|
4458
|
+
}
|
|
4459
|
+
const batchSize = req.body?.entries?.length || 1;
|
|
4460
|
+
if (batchSize > rule.maxEntriesPerBatch) {
|
|
4461
|
+
res.status(413).json({
|
|
4462
|
+
error: `Batch too large: ${batchSize} entries, max ${rule.maxEntriesPerBatch} for service ${service}`
|
|
4463
|
+
});
|
|
4464
|
+
return;
|
|
4465
|
+
}
|
|
4466
|
+
gw.count++;
|
|
4467
|
+
sw.count++;
|
|
4468
|
+
next();
|
|
4469
|
+
};
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4472
|
+
// src/config.ts
|
|
4473
|
+
import * as fs4 from "fs";
|
|
4474
|
+
import * as path5 from "path";
|
|
4475
|
+
|
|
4476
|
+
// src/types.ts
|
|
4477
|
+
var DEFAULT_AUTH_CONFIG = {
|
|
4478
|
+
enabled: false,
|
|
4479
|
+
tokens: []
|
|
4480
|
+
};
|
|
4481
|
+
var DEFAULT_RATE_LIMIT_CONFIG = {
|
|
4482
|
+
enabled: false,
|
|
4483
|
+
global: {
|
|
4484
|
+
maxRequestsPerMinute: 600,
|
|
4485
|
+
maxEntriesPerBatch: 500,
|
|
4486
|
+
samplingRate: 1
|
|
4487
|
+
},
|
|
4488
|
+
perService: {}
|
|
4489
|
+
};
|
|
4490
|
+
var DEFAULT_SERVER_CONFIG = {
|
|
4491
|
+
port: 3838,
|
|
4492
|
+
maxLogs: 1e4,
|
|
4493
|
+
maxBatchSize: 500,
|
|
4494
|
+
wsMaxSubscribers: 256,
|
|
4495
|
+
pruneIntervalInserts: 100,
|
|
4496
|
+
logRetentionDays: 0,
|
|
4497
|
+
auth: DEFAULT_AUTH_CONFIG,
|
|
4498
|
+
rateLimit: DEFAULT_RATE_LIMIT_CONFIG
|
|
4499
|
+
};
|
|
4500
|
+
|
|
4501
|
+
// src/config.ts
|
|
4502
|
+
var CONFIG_FILES = [".sentinel.yaml", ".sentinel.yml"];
|
|
4503
|
+
function loadConfig(projectDir) {
|
|
4504
|
+
for (const filename of CONFIG_FILES) {
|
|
4505
|
+
const filePath = path5.join(projectDir, filename);
|
|
4506
|
+
if (fs4.existsSync(filePath)) {
|
|
4507
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
4508
|
+
return parseSimpleYaml(content);
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
return null;
|
|
4512
|
+
}
|
|
4513
|
+
function writeConfig(projectDir, config) {
|
|
4514
|
+
const filePath = path5.join(projectDir, ".sentinel.yaml");
|
|
4515
|
+
const content = serializeSimpleYaml(config);
|
|
4516
|
+
fs4.writeFileSync(filePath, content, "utf-8");
|
|
4517
|
+
}
|
|
4518
|
+
function parseSimpleYaml(content) {
|
|
4519
|
+
const config = { version: "1.0", project: "" };
|
|
4520
|
+
const lines = content.split("\n");
|
|
4521
|
+
let currentSection = null;
|
|
4522
|
+
let currentSubSection = null;
|
|
4523
|
+
for (const line of lines) {
|
|
4524
|
+
const trimmed = line.trimEnd();
|
|
4525
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
4526
|
+
const topMatch = trimmed.match(/^(\w+):\s*(.+)$/);
|
|
4527
|
+
if (topMatch) {
|
|
4528
|
+
const [, key, value] = topMatch;
|
|
4529
|
+
if (key === "version") config.version = value.replace(/['"]/g, "");
|
|
4530
|
+
else if (key === "project") config.project = value.replace(/['"]/g, "");
|
|
4531
|
+
else if (key === "environment") config.environment = value.replace(/['"]/g, "");
|
|
4532
|
+
currentSection = null;
|
|
4533
|
+
currentSubSection = null;
|
|
4534
|
+
continue;
|
|
4535
|
+
}
|
|
4536
|
+
const sectionMatch = trimmed.match(/^(\w+):$/);
|
|
4537
|
+
if (sectionMatch) {
|
|
4538
|
+
currentSection = sectionMatch[1];
|
|
4539
|
+
currentSubSection = null;
|
|
4540
|
+
if (currentSection === "symbols" && !config.symbols) {
|
|
4541
|
+
config.symbols = {};
|
|
4542
|
+
}
|
|
4543
|
+
if (currentSection === "routes" && !config.routes) {
|
|
4544
|
+
config.routes = {};
|
|
4545
|
+
}
|
|
4546
|
+
if (currentSection === "scrub" && !config.scrub) {
|
|
4547
|
+
config.scrub = {};
|
|
4548
|
+
}
|
|
4549
|
+
if (currentSection === "server" && !config.server) {
|
|
4550
|
+
config.server = {};
|
|
4551
|
+
}
|
|
4552
|
+
continue;
|
|
4553
|
+
}
|
|
4554
|
+
const subMatch = trimmed.match(/^\s{2}(\w+):$/);
|
|
4555
|
+
if (subMatch && currentSection) {
|
|
4556
|
+
currentSubSection = subMatch[1];
|
|
4557
|
+
if (currentSection === "symbols" && config.symbols) {
|
|
4558
|
+
config.symbols[currentSubSection] = [];
|
|
4559
|
+
}
|
|
4560
|
+
if (currentSection === "scrub" && config.scrub) {
|
|
4561
|
+
config.scrub[currentSubSection] = [];
|
|
4562
|
+
}
|
|
4563
|
+
continue;
|
|
4564
|
+
}
|
|
4565
|
+
const listMatch = trimmed.match(/^\s+-\s+(.+)$/);
|
|
4566
|
+
if (listMatch && currentSection && currentSubSection) {
|
|
4567
|
+
const value = listMatch[1].replace(/['"]/g, "");
|
|
4568
|
+
if (currentSection === "symbols" && config.symbols) {
|
|
4569
|
+
const arr = config.symbols[currentSubSection];
|
|
4570
|
+
if (Array.isArray(arr)) arr.push(value);
|
|
4571
|
+
}
|
|
4572
|
+
if (currentSection === "scrub" && config.scrub) {
|
|
4573
|
+
const arr = config.scrub[currentSubSection];
|
|
4574
|
+
if (Array.isArray(arr)) arr.push(value);
|
|
4575
|
+
}
|
|
4576
|
+
continue;
|
|
4577
|
+
}
|
|
4578
|
+
const routeMatch = trimmed.match(/^\s+(['"]?\/[^'"]+['"]?):\s+['"]?([^'"]+)['"]?$/);
|
|
4579
|
+
if (routeMatch && currentSection === "routes" && config.routes) {
|
|
4580
|
+
const route = routeMatch[1].replace(/['"]/g, "");
|
|
4581
|
+
config.routes[route] = routeMatch[2];
|
|
4582
|
+
continue;
|
|
4583
|
+
}
|
|
4584
|
+
const serverKvMatch = trimmed.match(/^\s+(\w+):\s+(\d+)$/);
|
|
4585
|
+
if (serverKvMatch && currentSection === "server" && config.server) {
|
|
4586
|
+
const key = serverKvMatch[1];
|
|
4587
|
+
const value = parseInt(serverKvMatch[2], 10);
|
|
4588
|
+
if (key in { port: 1, maxLogs: 1, maxBatchSize: 1, wsMaxSubscribers: 1, pruneIntervalInserts: 1, logRetentionDays: 1 }) {
|
|
4589
|
+
config.server[key] = value;
|
|
4590
|
+
}
|
|
4591
|
+
continue;
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
return config;
|
|
4595
|
+
}
|
|
4596
|
+
function serializeSimpleYaml(config) {
|
|
4597
|
+
const lines = [];
|
|
4598
|
+
lines.push(`# Sentinel Configuration`);
|
|
4599
|
+
lines.push(`# Auto-generated \u2014 edit freely`);
|
|
4600
|
+
lines.push("");
|
|
4601
|
+
lines.push(`version: "${config.version}"`);
|
|
4602
|
+
lines.push(`project: "${config.project}"`);
|
|
4603
|
+
if (config.environment) {
|
|
4604
|
+
lines.push(`environment: "${config.environment}"`);
|
|
4605
|
+
}
|
|
4606
|
+
if (config.symbols) {
|
|
4607
|
+
lines.push("");
|
|
4608
|
+
lines.push("symbols:");
|
|
4609
|
+
for (const [key, values] of Object.entries(config.symbols)) {
|
|
4610
|
+
if (values && values.length > 0) {
|
|
4611
|
+
lines.push(` ${key}:`);
|
|
4612
|
+
for (const v of values) {
|
|
4613
|
+
lines.push(` - ${v}`);
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
if (config.routes && Object.keys(config.routes).length > 0) {
|
|
4619
|
+
lines.push("");
|
|
4620
|
+
lines.push("routes:");
|
|
4621
|
+
for (const [route, symbol] of Object.entries(config.routes)) {
|
|
4622
|
+
lines.push(` "${route}": ${symbol}`);
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4625
|
+
if (config.scrub) {
|
|
4626
|
+
lines.push("");
|
|
4627
|
+
lines.push("scrub:");
|
|
4628
|
+
if (config.scrub.headers?.length) {
|
|
4629
|
+
lines.push(" headers:");
|
|
4630
|
+
for (const h of config.scrub.headers) {
|
|
4631
|
+
lines.push(` - ${h}`);
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
if (config.scrub.fields?.length) {
|
|
4635
|
+
lines.push(" fields:");
|
|
4636
|
+
for (const f of config.scrub.fields) {
|
|
4637
|
+
lines.push(` - ${f}`);
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
if (config.server && Object.keys(config.server).length > 0) {
|
|
4642
|
+
lines.push("");
|
|
4643
|
+
lines.push("server:");
|
|
4644
|
+
for (const [key, value] of Object.entries(config.server)) {
|
|
4645
|
+
if (value !== void 0) {
|
|
4646
|
+
lines.push(` ${key}: ${value}`);
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
lines.push("");
|
|
4651
|
+
return lines.join("\n");
|
|
4652
|
+
}
|
|
4653
|
+
function loadServerConfig(projectDir) {
|
|
4654
|
+
const config = { ...DEFAULT_SERVER_CONFIG };
|
|
4655
|
+
const yamlConfig = projectDir ? loadConfig(projectDir) : null;
|
|
4656
|
+
const globalDir = path5.join(process.env.HOME || "~", ".paradigm");
|
|
4657
|
+
const globalConfig = loadConfig(globalDir);
|
|
4658
|
+
for (const src of [globalConfig, yamlConfig]) {
|
|
4659
|
+
if (src?.server) {
|
|
4660
|
+
if (src.server.port !== void 0) config.port = src.server.port;
|
|
4661
|
+
if (src.server.maxLogs !== void 0) config.maxLogs = src.server.maxLogs;
|
|
4662
|
+
if (src.server.maxBatchSize !== void 0) config.maxBatchSize = src.server.maxBatchSize;
|
|
4663
|
+
if (src.server.wsMaxSubscribers !== void 0) config.wsMaxSubscribers = src.server.wsMaxSubscribers;
|
|
4664
|
+
if (src.server.pruneIntervalInserts !== void 0) config.pruneIntervalInserts = src.server.pruneIntervalInserts;
|
|
4665
|
+
if (src.server.logRetentionDays !== void 0) config.logRetentionDays = src.server.logRetentionDays;
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
if (process.env.SENTINEL_PORT) config.port = parseInt(process.env.SENTINEL_PORT, 10);
|
|
4669
|
+
if (process.env.SENTINEL_MAX_LOGS) config.maxLogs = parseInt(process.env.SENTINEL_MAX_LOGS, 10);
|
|
4670
|
+
return config;
|
|
4671
|
+
}
|
|
4672
|
+
|
|
2651
4673
|
// src/server/index.ts
|
|
2652
4674
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
2653
|
-
var __dirname2 =
|
|
4675
|
+
var __dirname2 = path6.dirname(__filename2);
|
|
2654
4676
|
var log3 = {
|
|
2655
4677
|
component(name) {
|
|
2656
4678
|
const symbol = chalk3.magenta(`#${name}`);
|
|
@@ -2676,11 +4698,20 @@ var log3 = {
|
|
|
2676
4698
|
};
|
|
2677
4699
|
function createApp(options) {
|
|
2678
4700
|
const app = express();
|
|
2679
|
-
app.use(express.json());
|
|
4701
|
+
app.use(express.json({ limit: "5mb" }));
|
|
2680
4702
|
app.use((_req, res, next) => {
|
|
2681
|
-
|
|
4703
|
+
const corsOrigin = options.serverConfig?.cors?.origin;
|
|
4704
|
+
const origin = Array.isArray(corsOrigin) ? corsOrigin.join(", ") : corsOrigin ?? "*";
|
|
4705
|
+
res.header("Access-Control-Allow-Origin", origin);
|
|
2682
4706
|
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
2683
|
-
res.header("Access-Control-Allow-Headers", "Content-Type");
|
|
4707
|
+
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
4708
|
+
if (options.serverConfig?.cors?.credentials) {
|
|
4709
|
+
res.header("Access-Control-Allow-Credentials", "true");
|
|
4710
|
+
}
|
|
4711
|
+
if (_req.method === "OPTIONS") {
|
|
4712
|
+
res.sendStatus(204);
|
|
4713
|
+
return;
|
|
4714
|
+
}
|
|
2684
4715
|
next();
|
|
2685
4716
|
});
|
|
2686
4717
|
app.use("/api/symbols", createSymbolsRouter(options.projectDir));
|
|
@@ -2688,27 +4719,162 @@ function createApp(options) {
|
|
|
2688
4719
|
app.use("/api/commits", createCommitsRouter(options.projectDir));
|
|
2689
4720
|
app.use("/api/incidents", createIncidentsRouter(options.projectDir));
|
|
2690
4721
|
app.use("/api/patterns", createPatternsRouter(options.projectDir));
|
|
4722
|
+
if (options.storage && options.serverConfig) {
|
|
4723
|
+
const config = options.serverConfig;
|
|
4724
|
+
const auth = createAuthMiddleware(config.auth);
|
|
4725
|
+
const rateLimiter = createRateLimiter(config.rateLimit);
|
|
4726
|
+
app.use("/api/logs", rateLimiter, auth("write"), createLogsRouter({
|
|
4727
|
+
storage: options.storage,
|
|
4728
|
+
serverConfig: config,
|
|
4729
|
+
onLogReceived: options.onLogReceived,
|
|
4730
|
+
symbolIndex: options.symbolIndex
|
|
4731
|
+
}));
|
|
4732
|
+
app.use("/api/services", rateLimiter, auth("write"), createServicesRouter({ storage: options.storage }));
|
|
4733
|
+
app.use("/api/state", rateLimiter, auth("write"), createStateRouter({ storage: options.storage }));
|
|
4734
|
+
app.use("/api/metrics", rateLimiter, auth("write"), createMetricsRouter({
|
|
4735
|
+
storage: options.storage,
|
|
4736
|
+
serverConfig: config
|
|
4737
|
+
}));
|
|
4738
|
+
app.use("/api/traces", rateLimiter, auth("write"), createTracesRouter({
|
|
4739
|
+
storage: options.storage
|
|
4740
|
+
}));
|
|
4741
|
+
app.use("/api/schemas", rateLimiter, auth("write"), createSchemasRouter({
|
|
4742
|
+
storage: options.storage
|
|
4743
|
+
}));
|
|
4744
|
+
app.use("/api/events", rateLimiter, auth("write"), createEventsRouter({
|
|
4745
|
+
storage: options.storage,
|
|
4746
|
+
serverConfig: config,
|
|
4747
|
+
onEventReceived: options.onEventReceived
|
|
4748
|
+
}));
|
|
4749
|
+
}
|
|
2691
4750
|
app.get("/api/health", (_req, res) => {
|
|
2692
4751
|
res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2693
4752
|
});
|
|
2694
|
-
const uiDistPath =
|
|
2695
|
-
if (
|
|
4753
|
+
const uiDistPath = path6.join(__dirname2, "..", "..", "ui", "dist");
|
|
4754
|
+
if (fs5.existsSync(uiDistPath)) {
|
|
2696
4755
|
app.use(express.static(uiDistPath));
|
|
2697
4756
|
app.get("{*path}", (req, res) => {
|
|
2698
4757
|
if (!req.path.startsWith("/api")) {
|
|
2699
|
-
res.sendFile(
|
|
4758
|
+
res.sendFile(path6.join(uiDistPath, "index.html"));
|
|
2700
4759
|
}
|
|
2701
4760
|
});
|
|
2702
4761
|
}
|
|
2703
4762
|
return app;
|
|
2704
4763
|
}
|
|
2705
4764
|
async function startServer(options) {
|
|
2706
|
-
const
|
|
4765
|
+
const serverConfig = loadServerConfig(options.projectDir);
|
|
4766
|
+
if (options.logPruneLimit !== void 0) {
|
|
4767
|
+
serverConfig.maxLogs = options.logPruneLimit;
|
|
4768
|
+
}
|
|
4769
|
+
const storage = new SentinelStorage(options.dbPath);
|
|
4770
|
+
await storage.ensureReady();
|
|
4771
|
+
let symbolIndex = [];
|
|
4772
|
+
try {
|
|
4773
|
+
symbolIndex = await loadSymbolIndex(options.projectDir);
|
|
4774
|
+
} catch {
|
|
4775
|
+
log3.component("sentinel-server").warn("Could not load symbol index for validation");
|
|
4776
|
+
}
|
|
4777
|
+
const wsClients = /* @__PURE__ */ new Set();
|
|
4778
|
+
function broadcast(message) {
|
|
4779
|
+
const data = JSON.stringify(message);
|
|
4780
|
+
for (const client of wsClients) {
|
|
4781
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
4782
|
+
client.send(data);
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
function onLogReceived(entry, validation) {
|
|
4787
|
+
const message = { type: "log", entry };
|
|
4788
|
+
if (validation && !validation.known) {
|
|
4789
|
+
message.validation = validation;
|
|
4790
|
+
}
|
|
4791
|
+
broadcast(message);
|
|
4792
|
+
if (entry.symbolType === "signal" || entry.symbolType === "gate" || entry.symbolType === "flow") {
|
|
4793
|
+
broadcast({
|
|
4794
|
+
type: "flow_event",
|
|
4795
|
+
flowId: entry.symbolType === "flow" ? entry.symbol : void 0,
|
|
4796
|
+
nodeSymbol: entry.symbol,
|
|
4797
|
+
event: entry.symbolType,
|
|
4798
|
+
timestamp: entry.timestamp,
|
|
4799
|
+
service: entry.service
|
|
4800
|
+
});
|
|
4801
|
+
}
|
|
4802
|
+
}
|
|
4803
|
+
function onEventReceived(event) {
|
|
4804
|
+
broadcast({ type: "event", event });
|
|
4805
|
+
}
|
|
4806
|
+
const app = createApp({
|
|
4807
|
+
...options,
|
|
4808
|
+
storage,
|
|
4809
|
+
serverConfig,
|
|
4810
|
+
symbolIndex,
|
|
4811
|
+
onLogReceived,
|
|
4812
|
+
onEventReceived
|
|
4813
|
+
});
|
|
2707
4814
|
log3.component("sentinel-server").info("Starting server", { port: options.port });
|
|
2708
4815
|
log3.component("sentinel-server").info("Project directory", { path: options.projectDir });
|
|
2709
4816
|
return new Promise((resolve, reject) => {
|
|
2710
|
-
const
|
|
4817
|
+
const httpServer = http.createServer(app);
|
|
4818
|
+
const wss = new WebSocketServer({ server: httpServer });
|
|
4819
|
+
wss.on("connection", (ws) => {
|
|
4820
|
+
if (wsClients.size >= serverConfig.wsMaxSubscribers) {
|
|
4821
|
+
ws.close(1013, "Max subscribers reached");
|
|
4822
|
+
return;
|
|
4823
|
+
}
|
|
4824
|
+
wsClients.add(ws);
|
|
4825
|
+
log3.component("sentinel-ws").info("Client connected", { total: wsClients.size });
|
|
4826
|
+
ws.on("message", (raw) => {
|
|
4827
|
+
try {
|
|
4828
|
+
const msg = JSON.parse(raw.toString());
|
|
4829
|
+
if (msg.method === "ping") {
|
|
4830
|
+
ws.send(JSON.stringify({
|
|
4831
|
+
jsonrpc: "2.0",
|
|
4832
|
+
result: { pong: true, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
4833
|
+
id: msg.id
|
|
4834
|
+
}));
|
|
4835
|
+
} else if (msg.method === "subscribe") {
|
|
4836
|
+
ws.send(JSON.stringify({
|
|
4837
|
+
jsonrpc: "2.0",
|
|
4838
|
+
result: { subscribed: true },
|
|
4839
|
+
id: msg.id
|
|
4840
|
+
}));
|
|
4841
|
+
} else if (msg.method === "query_logs") {
|
|
4842
|
+
const logs = storage.queryLogs(msg.params || {});
|
|
4843
|
+
ws.send(JSON.stringify({
|
|
4844
|
+
jsonrpc: "2.0",
|
|
4845
|
+
result: { logs },
|
|
4846
|
+
id: msg.id
|
|
4847
|
+
}));
|
|
4848
|
+
} else if (msg.method === "query_events") {
|
|
4849
|
+
const events = storage.queryEvents(msg.params || {});
|
|
4850
|
+
ws.send(JSON.stringify({
|
|
4851
|
+
jsonrpc: "2.0",
|
|
4852
|
+
result: { events },
|
|
4853
|
+
id: msg.id
|
|
4854
|
+
}));
|
|
4855
|
+
} else if (msg.method === "query_scopes") {
|
|
4856
|
+
const { schemaId, ...rest } = msg.params || {};
|
|
4857
|
+
const scopes = schemaId ? storage.getEventScopes(schemaId, rest) : [];
|
|
4858
|
+
ws.send(JSON.stringify({
|
|
4859
|
+
jsonrpc: "2.0",
|
|
4860
|
+
result: { scopes },
|
|
4861
|
+
id: msg.id
|
|
4862
|
+
}));
|
|
4863
|
+
}
|
|
4864
|
+
} catch {
|
|
4865
|
+
}
|
|
4866
|
+
});
|
|
4867
|
+
ws.on("close", () => {
|
|
4868
|
+
wsClients.delete(ws);
|
|
4869
|
+
log3.component("sentinel-ws").info("Client disconnected", { total: wsClients.size });
|
|
4870
|
+
});
|
|
4871
|
+
ws.on("error", () => {
|
|
4872
|
+
wsClients.delete(ws);
|
|
4873
|
+
});
|
|
4874
|
+
});
|
|
4875
|
+
httpServer.listen(options.port, () => {
|
|
2711
4876
|
log3.component("sentinel-server").success("Server running", { url: `http://localhost:${options.port}` });
|
|
4877
|
+
log3.component("sentinel-ws").success("WebSocket ready", { url: `ws://localhost:${options.port}` });
|
|
2712
4878
|
if (options.open) {
|
|
2713
4879
|
import("open").then((openModule) => {
|
|
2714
4880
|
openModule.default(`http://localhost:${options.port}`);
|
|
@@ -2719,7 +4885,7 @@ async function startServer(options) {
|
|
|
2719
4885
|
}
|
|
2720
4886
|
resolve();
|
|
2721
4887
|
});
|
|
2722
|
-
|
|
4888
|
+
httpServer.on("error", (err) => {
|
|
2723
4889
|
if (err.code === "EADDRINUSE") {
|
|
2724
4890
|
log3.component("sentinel-server").error("Port already in use", { port: options.port });
|
|
2725
4891
|
} else {
|
|
@@ -2730,63 +4896,6 @@ async function startServer(options) {
|
|
|
2730
4896
|
});
|
|
2731
4897
|
}
|
|
2732
4898
|
|
|
2733
|
-
// src/config.ts
|
|
2734
|
-
import * as fs5 from "fs";
|
|
2735
|
-
import * as path6 from "path";
|
|
2736
|
-
function writeConfig(projectDir, config) {
|
|
2737
|
-
const filePath = path6.join(projectDir, ".sentinel.yaml");
|
|
2738
|
-
const content = serializeSimpleYaml(config);
|
|
2739
|
-
fs5.writeFileSync(filePath, content, "utf-8");
|
|
2740
|
-
}
|
|
2741
|
-
function serializeSimpleYaml(config) {
|
|
2742
|
-
const lines = [];
|
|
2743
|
-
lines.push(`# Sentinel Configuration`);
|
|
2744
|
-
lines.push(`# Auto-generated \u2014 edit freely`);
|
|
2745
|
-
lines.push("");
|
|
2746
|
-
lines.push(`version: "${config.version}"`);
|
|
2747
|
-
lines.push(`project: "${config.project}"`);
|
|
2748
|
-
if (config.environment) {
|
|
2749
|
-
lines.push(`environment: "${config.environment}"`);
|
|
2750
|
-
}
|
|
2751
|
-
if (config.symbols) {
|
|
2752
|
-
lines.push("");
|
|
2753
|
-
lines.push("symbols:");
|
|
2754
|
-
for (const [key, values] of Object.entries(config.symbols)) {
|
|
2755
|
-
if (values && values.length > 0) {
|
|
2756
|
-
lines.push(` ${key}:`);
|
|
2757
|
-
for (const v of values) {
|
|
2758
|
-
lines.push(` - ${v}`);
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
if (config.routes && Object.keys(config.routes).length > 0) {
|
|
2764
|
-
lines.push("");
|
|
2765
|
-
lines.push("routes:");
|
|
2766
|
-
for (const [route, symbol] of Object.entries(config.routes)) {
|
|
2767
|
-
lines.push(` "${route}": ${symbol}`);
|
|
2768
|
-
}
|
|
2769
|
-
}
|
|
2770
|
-
if (config.scrub) {
|
|
2771
|
-
lines.push("");
|
|
2772
|
-
lines.push("scrub:");
|
|
2773
|
-
if (config.scrub.headers?.length) {
|
|
2774
|
-
lines.push(" headers:");
|
|
2775
|
-
for (const h of config.scrub.headers) {
|
|
2776
|
-
lines.push(` - ${h}`);
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
if (config.scrub.fields?.length) {
|
|
2780
|
-
lines.push(" fields:");
|
|
2781
|
-
for (const f of config.scrub.fields) {
|
|
2782
|
-
lines.push(` - ${f}`);
|
|
2783
|
-
}
|
|
2784
|
-
}
|
|
2785
|
-
}
|
|
2786
|
-
lines.push("");
|
|
2787
|
-
return lines.join("\n");
|
|
2788
|
-
}
|
|
2789
|
-
|
|
2790
4899
|
// src/detector.ts
|
|
2791
4900
|
import * as fs6 from "fs";
|
|
2792
4901
|
import * as path7 from "path";
|