@a-company/sentinel 0.2.0 → 3.5.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-KPMG4XED.js → chunk-FOF7CPJ6.js} +994 -2
- package/dist/chunk-VQ3SIN7S.js +422 -0
- package/dist/cli.js +6 -6
- package/dist/{commands-KIMGFR2I.js → commands-7PHRWGOB.js} +1791 -289
- 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 +47 -5
- package/dist/index.js +141 -186
- package/dist/mcp.js +1040 -9
- package/dist/sdk-BTblv--p.d.ts +180 -0
- package/dist/server/index.d.ts +19 -3
- package/dist/server/index.js +581 -9
- package/dist/storage-BqCJqZat.d.ts +129 -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-DPxatSdT.css → index-9iUtfyBP.css} +1 -1
- package/ui/dist/assets/index-BfINPxlF.js +62 -0
- package/ui/dist/assets/index-BfINPxlF.js.map +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-BNgsn_C8.js +0 -62
- package/ui/dist/assets/index-BNgsn_C8.js.map +0 -1
package/dist/mcp.js
CHANGED
|
@@ -13,7 +13,7 @@ import initSqlJs from "sql.js";
|
|
|
13
13
|
import { v4 as uuidv4 } from "uuid";
|
|
14
14
|
import * as path from "path";
|
|
15
15
|
import * as fs from "fs";
|
|
16
|
-
var SCHEMA_VERSION =
|
|
16
|
+
var SCHEMA_VERSION = 4;
|
|
17
17
|
var DEFAULT_CONFIDENCE = {
|
|
18
18
|
score: 50,
|
|
19
19
|
timesMatched: 0,
|
|
@@ -133,6 +133,72 @@ var SentinelStorage = class {
|
|
|
133
133
|
notes TEXT
|
|
134
134
|
);
|
|
135
135
|
|
|
136
|
+
-- Structured logs table
|
|
137
|
+
CREATE TABLE IF NOT EXISTS logs (
|
|
138
|
+
id TEXT PRIMARY KEY,
|
|
139
|
+
timestamp TEXT NOT NULL,
|
|
140
|
+
level TEXT NOT NULL CHECK (level IN ('debug','info','warn','error')),
|
|
141
|
+
symbol TEXT NOT NULL,
|
|
142
|
+
symbol_type TEXT NOT NULL DEFAULT 'raw',
|
|
143
|
+
message TEXT NOT NULL,
|
|
144
|
+
data_json TEXT,
|
|
145
|
+
service TEXT NOT NULL,
|
|
146
|
+
session_id TEXT,
|
|
147
|
+
correlation_id TEXT,
|
|
148
|
+
duration_ms REAL,
|
|
149
|
+
environment TEXT
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
-- Service registry
|
|
153
|
+
CREATE TABLE IF NOT EXISTS services (
|
|
154
|
+
name TEXT PRIMARY KEY,
|
|
155
|
+
version TEXT,
|
|
156
|
+
pid INTEGER,
|
|
157
|
+
started_at TEXT NOT NULL,
|
|
158
|
+
last_seen_at TEXT NOT NULL,
|
|
159
|
+
environment TEXT,
|
|
160
|
+
metadata_json TEXT
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
-- Live app state snapshots (latest-wins per service+session)
|
|
164
|
+
CREATE TABLE IF NOT EXISTS app_state (
|
|
165
|
+
service TEXT NOT NULL,
|
|
166
|
+
session_id TEXT NOT NULL,
|
|
167
|
+
timestamp TEXT NOT NULL,
|
|
168
|
+
state_json TEXT NOT NULL,
|
|
169
|
+
active_flows_json TEXT,
|
|
170
|
+
active_gates_json TEXT,
|
|
171
|
+
PRIMARY KEY (service, session_id)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
-- Metrics table
|
|
175
|
+
CREATE TABLE IF NOT EXISTS metrics (
|
|
176
|
+
id TEXT PRIMARY KEY,
|
|
177
|
+
timestamp TEXT NOT NULL,
|
|
178
|
+
name TEXT NOT NULL,
|
|
179
|
+
type TEXT NOT NULL CHECK (type IN ('counter','gauge','histogram')),
|
|
180
|
+
value REAL NOT NULL,
|
|
181
|
+
tags_json TEXT DEFAULT '{}',
|
|
182
|
+
service TEXT NOT NULL,
|
|
183
|
+
environment TEXT
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
-- Traces table
|
|
187
|
+
CREATE TABLE IF NOT EXISTS traces (
|
|
188
|
+
trace_id TEXT NOT NULL,
|
|
189
|
+
span_id TEXT PRIMARY KEY,
|
|
190
|
+
parent_span_id TEXT,
|
|
191
|
+
service TEXT NOT NULL,
|
|
192
|
+
symbol TEXT NOT NULL,
|
|
193
|
+
operation TEXT NOT NULL,
|
|
194
|
+
start_time TEXT NOT NULL,
|
|
195
|
+
end_time TEXT,
|
|
196
|
+
duration_ms REAL,
|
|
197
|
+
status TEXT NOT NULL DEFAULT 'ok',
|
|
198
|
+
tags_json TEXT DEFAULT '{}',
|
|
199
|
+
log_ids_json TEXT DEFAULT '[]'
|
|
200
|
+
);
|
|
201
|
+
|
|
136
202
|
-- Indexes
|
|
137
203
|
CREATE INDEX IF NOT EXISTS idx_incidents_timestamp ON incidents(timestamp);
|
|
138
204
|
CREATE INDEX IF NOT EXISTS idx_incidents_status ON incidents(status);
|
|
@@ -142,6 +208,18 @@ var SentinelStorage = class {
|
|
|
142
208
|
CREATE INDEX IF NOT EXISTS idx_practice_events_habit_id ON practice_events(habit_id);
|
|
143
209
|
CREATE INDEX IF NOT EXISTS idx_practice_events_engineer ON practice_events(engineer);
|
|
144
210
|
CREATE INDEX IF NOT EXISTS idx_practice_events_session_id ON practice_events(session_id);
|
|
211
|
+
CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp);
|
|
212
|
+
CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
|
|
213
|
+
CREATE INDEX IF NOT EXISTS idx_logs_symbol ON logs(symbol);
|
|
214
|
+
CREATE INDEX IF NOT EXISTS idx_logs_service ON logs(service);
|
|
215
|
+
CREATE INDEX IF NOT EXISTS idx_logs_session_id ON logs(session_id);
|
|
216
|
+
CREATE INDEX IF NOT EXISTS idx_logs_correlation_id ON logs(correlation_id);
|
|
217
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp);
|
|
218
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_name ON metrics(name);
|
|
219
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_service ON metrics(service);
|
|
220
|
+
CREATE INDEX IF NOT EXISTS idx_traces_trace_id ON traces(trace_id);
|
|
221
|
+
CREATE INDEX IF NOT EXISTS idx_traces_service ON traces(service);
|
|
222
|
+
CREATE INDEX IF NOT EXISTS idx_traces_start_time ON traces(start_time);
|
|
145
223
|
`);
|
|
146
224
|
this.db.run(
|
|
147
225
|
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)",
|
|
@@ -271,6 +349,101 @@ var SentinelStorage = class {
|
|
|
271
349
|
this.db.run(
|
|
272
350
|
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '2')"
|
|
273
351
|
);
|
|
352
|
+
currentVersion = 2;
|
|
353
|
+
}
|
|
354
|
+
if (currentVersion < 3) {
|
|
355
|
+
try {
|
|
356
|
+
this.db.run(`
|
|
357
|
+
CREATE TABLE IF NOT EXISTS logs (
|
|
358
|
+
id TEXT PRIMARY KEY,
|
|
359
|
+
timestamp TEXT NOT NULL,
|
|
360
|
+
level TEXT NOT NULL CHECK (level IN ('debug','info','warn','error')),
|
|
361
|
+
symbol TEXT NOT NULL,
|
|
362
|
+
symbol_type TEXT NOT NULL DEFAULT 'raw',
|
|
363
|
+
message TEXT NOT NULL,
|
|
364
|
+
data_json TEXT,
|
|
365
|
+
service TEXT NOT NULL,
|
|
366
|
+
session_id TEXT,
|
|
367
|
+
correlation_id TEXT,
|
|
368
|
+
duration_ms REAL,
|
|
369
|
+
environment TEXT
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
CREATE TABLE IF NOT EXISTS services (
|
|
373
|
+
name TEXT PRIMARY KEY,
|
|
374
|
+
version TEXT,
|
|
375
|
+
pid INTEGER,
|
|
376
|
+
started_at TEXT NOT NULL,
|
|
377
|
+
last_seen_at TEXT NOT NULL,
|
|
378
|
+
environment TEXT,
|
|
379
|
+
metadata_json TEXT
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
CREATE TABLE IF NOT EXISTS app_state (
|
|
383
|
+
service TEXT NOT NULL,
|
|
384
|
+
session_id TEXT NOT NULL,
|
|
385
|
+
timestamp TEXT NOT NULL,
|
|
386
|
+
state_json TEXT NOT NULL,
|
|
387
|
+
active_flows_json TEXT,
|
|
388
|
+
active_gates_json TEXT,
|
|
389
|
+
PRIMARY KEY (service, session_id)
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp);
|
|
393
|
+
CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
|
|
394
|
+
CREATE INDEX IF NOT EXISTS idx_logs_symbol ON logs(symbol);
|
|
395
|
+
CREATE INDEX IF NOT EXISTS idx_logs_service ON logs(service);
|
|
396
|
+
CREATE INDEX IF NOT EXISTS idx_logs_session_id ON logs(session_id);
|
|
397
|
+
CREATE INDEX IF NOT EXISTS idx_logs_correlation_id ON logs(correlation_id);
|
|
398
|
+
`);
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
this.db.run(
|
|
402
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '3')"
|
|
403
|
+
);
|
|
404
|
+
currentVersion = 3;
|
|
405
|
+
}
|
|
406
|
+
if (currentVersion < 4) {
|
|
407
|
+
try {
|
|
408
|
+
this.db.run(`
|
|
409
|
+
CREATE TABLE IF NOT EXISTS metrics (
|
|
410
|
+
id TEXT PRIMARY KEY,
|
|
411
|
+
timestamp TEXT NOT NULL,
|
|
412
|
+
name TEXT NOT NULL,
|
|
413
|
+
type TEXT NOT NULL CHECK (type IN ('counter','gauge','histogram')),
|
|
414
|
+
value REAL NOT NULL,
|
|
415
|
+
tags_json TEXT DEFAULT '{}',
|
|
416
|
+
service TEXT NOT NULL,
|
|
417
|
+
environment TEXT
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
CREATE TABLE IF NOT EXISTS traces (
|
|
421
|
+
trace_id TEXT NOT NULL,
|
|
422
|
+
span_id TEXT PRIMARY KEY,
|
|
423
|
+
parent_span_id TEXT,
|
|
424
|
+
service TEXT NOT NULL,
|
|
425
|
+
symbol TEXT NOT NULL,
|
|
426
|
+
operation TEXT NOT NULL,
|
|
427
|
+
start_time TEXT NOT NULL,
|
|
428
|
+
end_time TEXT,
|
|
429
|
+
duration_ms REAL,
|
|
430
|
+
status TEXT NOT NULL DEFAULT 'ok',
|
|
431
|
+
tags_json TEXT DEFAULT '{}',
|
|
432
|
+
log_ids_json TEXT DEFAULT '[]'
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp);
|
|
436
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_name ON metrics(name);
|
|
437
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_service ON metrics(service);
|
|
438
|
+
CREATE INDEX IF NOT EXISTS idx_traces_trace_id ON traces(trace_id);
|
|
439
|
+
CREATE INDEX IF NOT EXISTS idx_traces_service ON traces(service);
|
|
440
|
+
CREATE INDEX IF NOT EXISTS idx_traces_start_time ON traces(start_time);
|
|
441
|
+
`);
|
|
442
|
+
} catch {
|
|
443
|
+
}
|
|
444
|
+
this.db.run(
|
|
445
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '4')"
|
|
446
|
+
);
|
|
274
447
|
}
|
|
275
448
|
}
|
|
276
449
|
/**
|
|
@@ -1245,6 +1418,620 @@ var SentinelStorage = class {
|
|
|
1245
1418
|
notes: obj.notes || void 0
|
|
1246
1419
|
};
|
|
1247
1420
|
}
|
|
1421
|
+
// ─── Structured Logs ─────────────────────────────────────────────
|
|
1422
|
+
inferSymbolType(symbol) {
|
|
1423
|
+
if (symbol.startsWith("#")) return "component";
|
|
1424
|
+
if (symbol.startsWith("^")) return "gate";
|
|
1425
|
+
if (symbol.startsWith("!")) return "signal";
|
|
1426
|
+
if (symbol.startsWith("$")) return "flow";
|
|
1427
|
+
if (symbol.startsWith("~")) return "aspect";
|
|
1428
|
+
return "raw";
|
|
1429
|
+
}
|
|
1430
|
+
insertLog(input) {
|
|
1431
|
+
this.initializeSync();
|
|
1432
|
+
const id = input.id || uuidv4();
|
|
1433
|
+
const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
1434
|
+
const symbolType = input.symbolType || this.inferSymbolType(input.symbol);
|
|
1435
|
+
this.db.run(
|
|
1436
|
+
`INSERT INTO logs (
|
|
1437
|
+
id, timestamp, level, symbol, symbol_type, message, data_json,
|
|
1438
|
+
service, session_id, correlation_id, duration_ms, environment
|
|
1439
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1440
|
+
[
|
|
1441
|
+
id,
|
|
1442
|
+
timestamp,
|
|
1443
|
+
input.level,
|
|
1444
|
+
input.symbol,
|
|
1445
|
+
symbolType,
|
|
1446
|
+
input.message,
|
|
1447
|
+
input.data ? JSON.stringify(input.data) : null,
|
|
1448
|
+
input.service,
|
|
1449
|
+
input.sessionId || null,
|
|
1450
|
+
input.correlationId || null,
|
|
1451
|
+
input.durationMs ?? null,
|
|
1452
|
+
input.environment || null
|
|
1453
|
+
]
|
|
1454
|
+
);
|
|
1455
|
+
this.save();
|
|
1456
|
+
return id;
|
|
1457
|
+
}
|
|
1458
|
+
insertLogBatch(entries) {
|
|
1459
|
+
this.initializeSync();
|
|
1460
|
+
let accepted = 0;
|
|
1461
|
+
const errors = [];
|
|
1462
|
+
for (const input of entries) {
|
|
1463
|
+
try {
|
|
1464
|
+
const id = input.id || uuidv4();
|
|
1465
|
+
const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
1466
|
+
const symbolType = input.symbolType || this.inferSymbolType(input.symbol);
|
|
1467
|
+
this.db.run(
|
|
1468
|
+
`INSERT INTO logs (
|
|
1469
|
+
id, timestamp, level, symbol, symbol_type, message, data_json,
|
|
1470
|
+
service, session_id, correlation_id, duration_ms, environment
|
|
1471
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1472
|
+
[
|
|
1473
|
+
id,
|
|
1474
|
+
timestamp,
|
|
1475
|
+
input.level,
|
|
1476
|
+
input.symbol,
|
|
1477
|
+
symbolType,
|
|
1478
|
+
input.message,
|
|
1479
|
+
input.data ? JSON.stringify(input.data) : null,
|
|
1480
|
+
input.service,
|
|
1481
|
+
input.sessionId || null,
|
|
1482
|
+
input.correlationId || null,
|
|
1483
|
+
input.durationMs ?? null,
|
|
1484
|
+
input.environment || null
|
|
1485
|
+
]
|
|
1486
|
+
);
|
|
1487
|
+
accepted++;
|
|
1488
|
+
} catch (err) {
|
|
1489
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
this.save();
|
|
1493
|
+
return { accepted, errors };
|
|
1494
|
+
}
|
|
1495
|
+
queryLogs(options = {}) {
|
|
1496
|
+
this.initializeSync();
|
|
1497
|
+
const { limit = 100, offset = 0 } = options;
|
|
1498
|
+
const conditions = [];
|
|
1499
|
+
const params = [];
|
|
1500
|
+
if (options.level) {
|
|
1501
|
+
conditions.push("level = ?");
|
|
1502
|
+
params.push(options.level);
|
|
1503
|
+
}
|
|
1504
|
+
if (options.symbol) {
|
|
1505
|
+
conditions.push("symbol LIKE ?");
|
|
1506
|
+
params.push(`%${options.symbol}%`);
|
|
1507
|
+
}
|
|
1508
|
+
if (options.service) {
|
|
1509
|
+
conditions.push("service = ?");
|
|
1510
|
+
params.push(options.service);
|
|
1511
|
+
}
|
|
1512
|
+
if (options.sessionId) {
|
|
1513
|
+
conditions.push("session_id = ?");
|
|
1514
|
+
params.push(options.sessionId);
|
|
1515
|
+
}
|
|
1516
|
+
if (options.correlationId) {
|
|
1517
|
+
conditions.push("correlation_id = ?");
|
|
1518
|
+
params.push(options.correlationId);
|
|
1519
|
+
}
|
|
1520
|
+
if (options.search) {
|
|
1521
|
+
conditions.push("message LIKE ?");
|
|
1522
|
+
params.push(`%${options.search}%`);
|
|
1523
|
+
}
|
|
1524
|
+
if (options.since) {
|
|
1525
|
+
conditions.push("timestamp >= ?");
|
|
1526
|
+
params.push(options.since);
|
|
1527
|
+
}
|
|
1528
|
+
if (options.until) {
|
|
1529
|
+
conditions.push("timestamp <= ?");
|
|
1530
|
+
params.push(options.until);
|
|
1531
|
+
}
|
|
1532
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1533
|
+
const result = this.db.exec(
|
|
1534
|
+
`SELECT * FROM logs ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
|
|
1535
|
+
[...params, limit, offset]
|
|
1536
|
+
);
|
|
1537
|
+
if (result.length === 0) return [];
|
|
1538
|
+
return result[0].values.map(
|
|
1539
|
+
(row) => this.rowToLogEntry(result[0].columns, row)
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
getLogCount(options = {}) {
|
|
1543
|
+
this.initializeSync();
|
|
1544
|
+
const conditions = [];
|
|
1545
|
+
const params = [];
|
|
1546
|
+
if (options.level) {
|
|
1547
|
+
conditions.push("level = ?");
|
|
1548
|
+
params.push(options.level);
|
|
1549
|
+
}
|
|
1550
|
+
if (options.symbol) {
|
|
1551
|
+
conditions.push("symbol LIKE ?");
|
|
1552
|
+
params.push(`%${options.symbol}%`);
|
|
1553
|
+
}
|
|
1554
|
+
if (options.service) {
|
|
1555
|
+
conditions.push("service = ?");
|
|
1556
|
+
params.push(options.service);
|
|
1557
|
+
}
|
|
1558
|
+
if (options.since) {
|
|
1559
|
+
conditions.push("timestamp >= ?");
|
|
1560
|
+
params.push(options.since);
|
|
1561
|
+
}
|
|
1562
|
+
if (options.until) {
|
|
1563
|
+
conditions.push("timestamp <= ?");
|
|
1564
|
+
params.push(options.until);
|
|
1565
|
+
}
|
|
1566
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1567
|
+
const result = this.db.exec(
|
|
1568
|
+
`SELECT COUNT(*) as count FROM logs ${whereClause}`,
|
|
1569
|
+
params
|
|
1570
|
+
);
|
|
1571
|
+
if (result.length === 0 || result[0].values.length === 0) return 0;
|
|
1572
|
+
return result[0].values[0][0];
|
|
1573
|
+
}
|
|
1574
|
+
pruneLogs(maxCount) {
|
|
1575
|
+
this.initializeSync();
|
|
1576
|
+
if (maxCount <= 0) return 0;
|
|
1577
|
+
const currentCount = this.getLogCount();
|
|
1578
|
+
if (currentCount <= maxCount) return 0;
|
|
1579
|
+
const deleteCount = currentCount - maxCount;
|
|
1580
|
+
this.db.run(
|
|
1581
|
+
`DELETE FROM logs WHERE id IN (
|
|
1582
|
+
SELECT id FROM logs ORDER BY timestamp ASC LIMIT ?
|
|
1583
|
+
)`,
|
|
1584
|
+
[deleteCount]
|
|
1585
|
+
);
|
|
1586
|
+
this.save();
|
|
1587
|
+
return deleteCount;
|
|
1588
|
+
}
|
|
1589
|
+
rowToLogEntry(columns, row) {
|
|
1590
|
+
const obj = {};
|
|
1591
|
+
columns.forEach((col, i) => {
|
|
1592
|
+
obj[col] = row[i];
|
|
1593
|
+
});
|
|
1594
|
+
return {
|
|
1595
|
+
id: obj.id,
|
|
1596
|
+
timestamp: obj.timestamp,
|
|
1597
|
+
level: obj.level,
|
|
1598
|
+
symbol: obj.symbol,
|
|
1599
|
+
symbolType: obj.symbol_type || "raw",
|
|
1600
|
+
message: obj.message,
|
|
1601
|
+
data: obj.data_json ? JSON.parse(obj.data_json) : void 0,
|
|
1602
|
+
service: obj.service,
|
|
1603
|
+
sessionId: obj.session_id || void 0,
|
|
1604
|
+
correlationId: obj.correlation_id || void 0,
|
|
1605
|
+
durationMs: obj.duration_ms || void 0,
|
|
1606
|
+
environment: obj.environment || void 0
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
// ─── Service Registry ──────────────────────────────────────────
|
|
1610
|
+
registerService(reg) {
|
|
1611
|
+
this.initializeSync();
|
|
1612
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1613
|
+
this.db.run(
|
|
1614
|
+
`INSERT INTO services (name, version, pid, started_at, last_seen_at, environment, metadata_json)
|
|
1615
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1616
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
1617
|
+
version = excluded.version,
|
|
1618
|
+
pid = excluded.pid,
|
|
1619
|
+
last_seen_at = excluded.last_seen_at,
|
|
1620
|
+
environment = excluded.environment,
|
|
1621
|
+
metadata_json = excluded.metadata_json`,
|
|
1622
|
+
[
|
|
1623
|
+
reg.name,
|
|
1624
|
+
reg.version || null,
|
|
1625
|
+
reg.pid ?? null,
|
|
1626
|
+
now,
|
|
1627
|
+
now,
|
|
1628
|
+
reg.environment || null,
|
|
1629
|
+
reg.metadata ? JSON.stringify(reg.metadata) : null
|
|
1630
|
+
]
|
|
1631
|
+
);
|
|
1632
|
+
this.save();
|
|
1633
|
+
}
|
|
1634
|
+
updateServiceLastSeen(name) {
|
|
1635
|
+
this.initializeSync();
|
|
1636
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1637
|
+
this.db.run(
|
|
1638
|
+
"UPDATE services SET last_seen_at = ? WHERE name = ?",
|
|
1639
|
+
[now, name]
|
|
1640
|
+
);
|
|
1641
|
+
this.save();
|
|
1642
|
+
}
|
|
1643
|
+
getServices() {
|
|
1644
|
+
this.initializeSync();
|
|
1645
|
+
const result = this.db.exec(
|
|
1646
|
+
"SELECT * FROM services ORDER BY last_seen_at DESC"
|
|
1647
|
+
);
|
|
1648
|
+
if (result.length === 0) return [];
|
|
1649
|
+
return result[0].values.map((row) => {
|
|
1650
|
+
const obj = {};
|
|
1651
|
+
result[0].columns.forEach((col, i) => {
|
|
1652
|
+
obj[col] = row[i];
|
|
1653
|
+
});
|
|
1654
|
+
return {
|
|
1655
|
+
name: obj.name,
|
|
1656
|
+
version: obj.version || void 0,
|
|
1657
|
+
pid: obj.pid || void 0,
|
|
1658
|
+
startedAt: obj.started_at,
|
|
1659
|
+
lastSeenAt: obj.last_seen_at,
|
|
1660
|
+
environment: obj.environment || void 0,
|
|
1661
|
+
metadata: obj.metadata_json ? JSON.parse(obj.metadata_json) : void 0
|
|
1662
|
+
};
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
// ─── App State ──────────────────────────────────────────────────
|
|
1666
|
+
upsertAppState(state) {
|
|
1667
|
+
this.initializeSync();
|
|
1668
|
+
this.db.run(
|
|
1669
|
+
`INSERT INTO app_state (service, session_id, timestamp, state_json, active_flows_json, active_gates_json)
|
|
1670
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1671
|
+
ON CONFLICT(service, session_id) DO UPDATE SET
|
|
1672
|
+
timestamp = excluded.timestamp,
|
|
1673
|
+
state_json = excluded.state_json,
|
|
1674
|
+
active_flows_json = excluded.active_flows_json,
|
|
1675
|
+
active_gates_json = excluded.active_gates_json`,
|
|
1676
|
+
[
|
|
1677
|
+
state.service,
|
|
1678
|
+
state.sessionId,
|
|
1679
|
+
state.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1680
|
+
JSON.stringify(state.state),
|
|
1681
|
+
state.activeFlows ? JSON.stringify(state.activeFlows) : null,
|
|
1682
|
+
state.activeGates ? JSON.stringify(state.activeGates) : null
|
|
1683
|
+
]
|
|
1684
|
+
);
|
|
1685
|
+
this.save();
|
|
1686
|
+
}
|
|
1687
|
+
getAppState(service, sessionId) {
|
|
1688
|
+
this.initializeSync();
|
|
1689
|
+
let query = "SELECT * FROM app_state WHERE service = ?";
|
|
1690
|
+
const params = [service];
|
|
1691
|
+
if (sessionId) {
|
|
1692
|
+
query += " AND session_id = ?";
|
|
1693
|
+
params.push(sessionId);
|
|
1694
|
+
}
|
|
1695
|
+
query += " ORDER BY timestamp DESC";
|
|
1696
|
+
const result = this.db.exec(query, params);
|
|
1697
|
+
if (result.length === 0) return [];
|
|
1698
|
+
return result[0].values.map((row) => this.rowToAppState(result[0].columns, row));
|
|
1699
|
+
}
|
|
1700
|
+
getAllAppStates() {
|
|
1701
|
+
this.initializeSync();
|
|
1702
|
+
const result = this.db.exec(
|
|
1703
|
+
"SELECT * FROM app_state ORDER BY timestamp DESC"
|
|
1704
|
+
);
|
|
1705
|
+
if (result.length === 0) return [];
|
|
1706
|
+
return result[0].values.map((row) => this.rowToAppState(result[0].columns, row));
|
|
1707
|
+
}
|
|
1708
|
+
rowToAppState(columns, row) {
|
|
1709
|
+
const obj = {};
|
|
1710
|
+
columns.forEach((col, i) => {
|
|
1711
|
+
obj[col] = row[i];
|
|
1712
|
+
});
|
|
1713
|
+
return {
|
|
1714
|
+
service: obj.service,
|
|
1715
|
+
sessionId: obj.session_id,
|
|
1716
|
+
timestamp: obj.timestamp,
|
|
1717
|
+
state: JSON.parse(obj.state_json),
|
|
1718
|
+
activeFlows: obj.active_flows_json ? JSON.parse(obj.active_flows_json) : void 0,
|
|
1719
|
+
activeGates: obj.active_gates_json ? JSON.parse(obj.active_gates_json) : void 0
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
// ─── Metrics ───────────────────────────────────────────────────
|
|
1723
|
+
insertMetric(input) {
|
|
1724
|
+
this.initializeSync();
|
|
1725
|
+
const id = uuidv4();
|
|
1726
|
+
const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
1727
|
+
this.db.run(
|
|
1728
|
+
`INSERT INTO metrics (
|
|
1729
|
+
id, timestamp, name, type, value, tags_json, service, environment
|
|
1730
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1731
|
+
[
|
|
1732
|
+
id,
|
|
1733
|
+
timestamp,
|
|
1734
|
+
input.name,
|
|
1735
|
+
input.type,
|
|
1736
|
+
input.value,
|
|
1737
|
+
JSON.stringify(input.tags || {}),
|
|
1738
|
+
input.service,
|
|
1739
|
+
input.environment || null
|
|
1740
|
+
]
|
|
1741
|
+
);
|
|
1742
|
+
this.save();
|
|
1743
|
+
return id;
|
|
1744
|
+
}
|
|
1745
|
+
insertMetricBatch(entries) {
|
|
1746
|
+
this.initializeSync();
|
|
1747
|
+
let accepted = 0;
|
|
1748
|
+
const errors = [];
|
|
1749
|
+
for (const input of entries) {
|
|
1750
|
+
try {
|
|
1751
|
+
const id = uuidv4();
|
|
1752
|
+
const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
1753
|
+
this.db.run(
|
|
1754
|
+
`INSERT INTO metrics (
|
|
1755
|
+
id, timestamp, name, type, value, tags_json, service, environment
|
|
1756
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1757
|
+
[
|
|
1758
|
+
id,
|
|
1759
|
+
timestamp,
|
|
1760
|
+
input.name,
|
|
1761
|
+
input.type,
|
|
1762
|
+
input.value,
|
|
1763
|
+
JSON.stringify(input.tags || {}),
|
|
1764
|
+
input.service,
|
|
1765
|
+
input.environment || null
|
|
1766
|
+
]
|
|
1767
|
+
);
|
|
1768
|
+
accepted++;
|
|
1769
|
+
} catch (err) {
|
|
1770
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
this.save();
|
|
1774
|
+
return { accepted, errors };
|
|
1775
|
+
}
|
|
1776
|
+
queryMetrics(options = {}) {
|
|
1777
|
+
this.initializeSync();
|
|
1778
|
+
const { limit = 100, offset = 0 } = options;
|
|
1779
|
+
const conditions = [];
|
|
1780
|
+
const params = [];
|
|
1781
|
+
if (options.name) {
|
|
1782
|
+
conditions.push("name = ?");
|
|
1783
|
+
params.push(options.name);
|
|
1784
|
+
}
|
|
1785
|
+
if (options.type) {
|
|
1786
|
+
conditions.push("type = ?");
|
|
1787
|
+
params.push(options.type);
|
|
1788
|
+
}
|
|
1789
|
+
if (options.service) {
|
|
1790
|
+
conditions.push("service = ?");
|
|
1791
|
+
params.push(options.service);
|
|
1792
|
+
}
|
|
1793
|
+
if (options.tag) {
|
|
1794
|
+
const eqIdx = options.tag.indexOf("=");
|
|
1795
|
+
if (eqIdx > 0) {
|
|
1796
|
+
const tagKey = options.tag.substring(0, eqIdx);
|
|
1797
|
+
const tagValue = options.tag.substring(eqIdx + 1);
|
|
1798
|
+
conditions.push("tags_json LIKE ?");
|
|
1799
|
+
params.push(`%"${tagKey}":"${tagValue}"%`);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
if (options.since) {
|
|
1803
|
+
conditions.push("timestamp >= ?");
|
|
1804
|
+
params.push(options.since);
|
|
1805
|
+
}
|
|
1806
|
+
if (options.until) {
|
|
1807
|
+
conditions.push("timestamp <= ?");
|
|
1808
|
+
params.push(options.until);
|
|
1809
|
+
}
|
|
1810
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1811
|
+
const result = this.db.exec(
|
|
1812
|
+
`SELECT * FROM metrics ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
|
|
1813
|
+
[...params, limit, offset]
|
|
1814
|
+
);
|
|
1815
|
+
if (result.length === 0) return [];
|
|
1816
|
+
return result[0].values.map(
|
|
1817
|
+
(row) => this.rowToMetricEntry(result[0].columns, row)
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
getMetricCount(options = {}) {
|
|
1821
|
+
this.initializeSync();
|
|
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 COUNT(*) as count FROM metrics ${whereClause}`,
|
|
1856
|
+
params
|
|
1857
|
+
);
|
|
1858
|
+
if (result.length === 0 || result[0].values.length === 0) return 0;
|
|
1859
|
+
return result[0].values[0][0];
|
|
1860
|
+
}
|
|
1861
|
+
aggregateMetric(name, options) {
|
|
1862
|
+
this.initializeSync();
|
|
1863
|
+
const conditions = ["name = ?"];
|
|
1864
|
+
const params = [name];
|
|
1865
|
+
if (options?.service) {
|
|
1866
|
+
conditions.push("service = ?");
|
|
1867
|
+
params.push(options.service);
|
|
1868
|
+
}
|
|
1869
|
+
if (options?.since) {
|
|
1870
|
+
conditions.push("timestamp >= ?");
|
|
1871
|
+
params.push(options.since);
|
|
1872
|
+
}
|
|
1873
|
+
if (options?.until) {
|
|
1874
|
+
conditions.push("timestamp <= ?");
|
|
1875
|
+
params.push(options.until);
|
|
1876
|
+
}
|
|
1877
|
+
const whereClause = `WHERE ${conditions.join(" AND ")}`;
|
|
1878
|
+
const result = this.db.exec(
|
|
1879
|
+
`SELECT COUNT(*) as count, SUM(value) as sum, MIN(value) as min, MAX(value) as max, AVG(value) as avg
|
|
1880
|
+
FROM metrics ${whereClause}`,
|
|
1881
|
+
params
|
|
1882
|
+
);
|
|
1883
|
+
if (result.length === 0 || result[0].values.length === 0) {
|
|
1884
|
+
return { name, count: 0, sum: 0, min: 0, max: 0, avg: 0 };
|
|
1885
|
+
}
|
|
1886
|
+
const row = result[0].values[0];
|
|
1887
|
+
return {
|
|
1888
|
+
name,
|
|
1889
|
+
count: row[0] || 0,
|
|
1890
|
+
sum: row[1] || 0,
|
|
1891
|
+
min: row[2] || 0,
|
|
1892
|
+
max: row[3] || 0,
|
|
1893
|
+
avg: row[4] || 0
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
pruneMetrics(maxCount) {
|
|
1897
|
+
this.initializeSync();
|
|
1898
|
+
if (maxCount <= 0) return 0;
|
|
1899
|
+
const currentCount = this.getMetricCount();
|
|
1900
|
+
if (currentCount <= maxCount) return 0;
|
|
1901
|
+
const deleteCount = currentCount - maxCount;
|
|
1902
|
+
this.db.run(
|
|
1903
|
+
`DELETE FROM metrics WHERE id IN (
|
|
1904
|
+
SELECT id FROM metrics ORDER BY timestamp ASC LIMIT ?
|
|
1905
|
+
)`,
|
|
1906
|
+
[deleteCount]
|
|
1907
|
+
);
|
|
1908
|
+
this.save();
|
|
1909
|
+
return deleteCount;
|
|
1910
|
+
}
|
|
1911
|
+
rowToMetricEntry(columns, row) {
|
|
1912
|
+
const obj = {};
|
|
1913
|
+
columns.forEach((col, i) => {
|
|
1914
|
+
obj[col] = row[i];
|
|
1915
|
+
});
|
|
1916
|
+
return {
|
|
1917
|
+
id: obj.id,
|
|
1918
|
+
timestamp: obj.timestamp,
|
|
1919
|
+
name: obj.name,
|
|
1920
|
+
type: obj.type,
|
|
1921
|
+
value: obj.value,
|
|
1922
|
+
tags: obj.tags_json ? JSON.parse(obj.tags_json) : {},
|
|
1923
|
+
service: obj.service,
|
|
1924
|
+
environment: obj.environment || void 0
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
// ─── Traces ───────────────────────────────────────────────────
|
|
1928
|
+
insertSpan(input) {
|
|
1929
|
+
this.initializeSync();
|
|
1930
|
+
const spanId = input.spanId || uuidv4();
|
|
1931
|
+
const startTime = input.startTime || (/* @__PURE__ */ new Date()).toISOString();
|
|
1932
|
+
this.db.run(
|
|
1933
|
+
`INSERT INTO traces (
|
|
1934
|
+
trace_id, span_id, parent_span_id, service, symbol, operation,
|
|
1935
|
+
start_time, end_time, duration_ms, status, tags_json, log_ids_json
|
|
1936
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1937
|
+
[
|
|
1938
|
+
input.traceId,
|
|
1939
|
+
spanId,
|
|
1940
|
+
input.parentSpanId || null,
|
|
1941
|
+
input.service,
|
|
1942
|
+
input.symbol,
|
|
1943
|
+
input.operation,
|
|
1944
|
+
startTime,
|
|
1945
|
+
input.endTime || null,
|
|
1946
|
+
input.durationMs ?? null,
|
|
1947
|
+
input.status || "ok",
|
|
1948
|
+
JSON.stringify(input.tags || {}),
|
|
1949
|
+
JSON.stringify(input.logIds || [])
|
|
1950
|
+
]
|
|
1951
|
+
);
|
|
1952
|
+
this.save();
|
|
1953
|
+
return spanId;
|
|
1954
|
+
}
|
|
1955
|
+
getTrace(traceId) {
|
|
1956
|
+
this.initializeSync();
|
|
1957
|
+
const result = this.db.exec(
|
|
1958
|
+
"SELECT * FROM traces WHERE trace_id = ? ORDER BY start_time ASC",
|
|
1959
|
+
[traceId]
|
|
1960
|
+
);
|
|
1961
|
+
if (result.length === 0 || result[0].values.length === 0) return null;
|
|
1962
|
+
const spans = result[0].values.map(
|
|
1963
|
+
(row) => this.rowToTraceSpan(result[0].columns, row)
|
|
1964
|
+
);
|
|
1965
|
+
const services = [...new Set(spans.map((s) => s.service))];
|
|
1966
|
+
const startTimes = spans.map((s) => s.startTime);
|
|
1967
|
+
const endTimes = spans.filter((s) => s.endTime).map((s) => s.endTime);
|
|
1968
|
+
const startTime = startTimes.sort()[0];
|
|
1969
|
+
const endTime = endTimes.length > 0 ? endTimes.sort().reverse()[0] : startTime;
|
|
1970
|
+
const startMs = new Date(startTime).getTime();
|
|
1971
|
+
const endMs = new Date(endTime).getTime();
|
|
1972
|
+
const totalDurationMs = endMs - startMs;
|
|
1973
|
+
return {
|
|
1974
|
+
traceId,
|
|
1975
|
+
spans,
|
|
1976
|
+
services,
|
|
1977
|
+
totalDurationMs: totalDurationMs > 0 ? totalDurationMs : 0,
|
|
1978
|
+
startTime,
|
|
1979
|
+
endTime
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
queryTraces(options = {}) {
|
|
1983
|
+
this.initializeSync();
|
|
1984
|
+
const conditions = [];
|
|
1985
|
+
const params = [];
|
|
1986
|
+
if (options.service) {
|
|
1987
|
+
conditions.push("service = ?");
|
|
1988
|
+
params.push(options.service);
|
|
1989
|
+
}
|
|
1990
|
+
if (options.symbol) {
|
|
1991
|
+
conditions.push("symbol = ?");
|
|
1992
|
+
params.push(options.symbol);
|
|
1993
|
+
}
|
|
1994
|
+
if (options.since) {
|
|
1995
|
+
conditions.push("start_time >= ?");
|
|
1996
|
+
params.push(options.since);
|
|
1997
|
+
}
|
|
1998
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1999
|
+
const traceLimit = Math.min(options.limit || 20, 20);
|
|
2000
|
+
const result = this.db.exec(
|
|
2001
|
+
`SELECT DISTINCT trace_id FROM traces ${whereClause} ORDER BY start_time DESC LIMIT ?`,
|
|
2002
|
+
[...params, traceLimit]
|
|
2003
|
+
);
|
|
2004
|
+
if (result.length === 0) return [];
|
|
2005
|
+
const traces = [];
|
|
2006
|
+
for (const row of result[0].values) {
|
|
2007
|
+
const traceId = row[0];
|
|
2008
|
+
const trace = this.getTrace(traceId);
|
|
2009
|
+
if (trace) {
|
|
2010
|
+
traces.push(trace);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
return traces;
|
|
2014
|
+
}
|
|
2015
|
+
rowToTraceSpan(columns, row) {
|
|
2016
|
+
const obj = {};
|
|
2017
|
+
columns.forEach((col, i) => {
|
|
2018
|
+
obj[col] = row[i];
|
|
2019
|
+
});
|
|
2020
|
+
return {
|
|
2021
|
+
traceId: obj.trace_id,
|
|
2022
|
+
spanId: obj.span_id,
|
|
2023
|
+
parentSpanId: obj.parent_span_id || void 0,
|
|
2024
|
+
service: obj.service,
|
|
2025
|
+
symbol: obj.symbol,
|
|
2026
|
+
operation: obj.operation,
|
|
2027
|
+
startTime: obj.start_time,
|
|
2028
|
+
endTime: obj.end_time || void 0,
|
|
2029
|
+
durationMs: obj.duration_ms || void 0,
|
|
2030
|
+
status: obj.status || "ok",
|
|
2031
|
+
tags: obj.tags_json ? JSON.parse(obj.tags_json) : {},
|
|
2032
|
+
logs: obj.log_ids_json ? JSON.parse(obj.log_ids_json) : []
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
1248
2035
|
close() {
|
|
1249
2036
|
if (this.db) {
|
|
1250
2037
|
this.save();
|
|
@@ -1902,7 +2689,7 @@ var PatternSuggester = class {
|
|
|
1902
2689
|
},
|
|
1903
2690
|
resolution: {
|
|
1904
2691
|
description: incident.resolution?.notes || "Resolution approach TBD",
|
|
1905
|
-
strategy:
|
|
2692
|
+
strategy: this.inferStrategy([incident]),
|
|
1906
2693
|
priority: "medium"
|
|
1907
2694
|
},
|
|
1908
2695
|
source: "suggested",
|
|
@@ -1917,6 +2704,7 @@ var PatternSuggester = class {
|
|
|
1917
2704
|
suggestFromGroup(group) {
|
|
1918
2705
|
const baseId = `group-${group.id.toLowerCase().replace(/[^a-z0-9]/g, "-")}`;
|
|
1919
2706
|
const symbols = this.buildSymbolCriteria(group.commonSymbols);
|
|
2707
|
+
const groupIncidents = group.incidents.slice(0, 20).map((id) => this.storage.getIncident(id)).filter((i) => i != null);
|
|
1920
2708
|
const pattern = {
|
|
1921
2709
|
id: baseId,
|
|
1922
2710
|
name: group.name || `Pattern from group ${group.id}`,
|
|
@@ -1927,7 +2715,7 @@ var PatternSuggester = class {
|
|
|
1927
2715
|
},
|
|
1928
2716
|
resolution: {
|
|
1929
2717
|
description: "Resolution approach TBD based on grouped incidents",
|
|
1930
|
-
strategy: "fix-code",
|
|
2718
|
+
strategy: groupIncidents.length > 0 ? this.inferStrategy(groupIncidents) : "fix-code",
|
|
1931
2719
|
priority: this.getPriorityFromCount(group.count)
|
|
1932
2720
|
},
|
|
1933
2721
|
source: "suggested",
|
|
@@ -2226,21 +3014,38 @@ var PatternSuggester = class {
|
|
|
2226
3014
|
return false;
|
|
2227
3015
|
}
|
|
2228
3016
|
/**
|
|
2229
|
-
* Infer resolution strategy from
|
|
3017
|
+
* Infer resolution strategy from incident error patterns and context.
|
|
3018
|
+
* Uses keyword heuristics across all incident messages to pick the
|
|
3019
|
+
* most likely resolution approach.
|
|
2230
3020
|
*/
|
|
2231
3021
|
inferStrategy(incidents) {
|
|
2232
3022
|
const messages = incidents.map((i) => i.error.message.toLowerCase());
|
|
2233
|
-
|
|
3023
|
+
const hasKeyword = (keywords) => messages.some((m) => keywords.some((k) => m.includes(k)));
|
|
3024
|
+
if (hasKeyword(["revert", "rollback", "regression", "broke after deploy", "since deploy"])) {
|
|
3025
|
+
return "rollback";
|
|
3026
|
+
}
|
|
3027
|
+
if (hasKeyword(["config", "environment variable", "env var", "missing key", "secret", "credential"])) {
|
|
3028
|
+
return "config-change";
|
|
3029
|
+
}
|
|
3030
|
+
if (hasKeyword(["out of memory", "oom", "heap", "memory limit", "capacity", "too many connections", "pool exhausted"])) {
|
|
3031
|
+
return "scale-up";
|
|
3032
|
+
}
|
|
3033
|
+
if (hasKeyword(["timeout", "network", "econnrefused", "econnreset", "dns", "socket hang up"])) {
|
|
2234
3034
|
return "retry";
|
|
2235
3035
|
}
|
|
2236
|
-
if (
|
|
2237
|
-
|
|
2238
|
-
|
|
3036
|
+
if (hasKeyword(["unavailable", "service down", "circuit breaker", "fallback", "503", "502"])) {
|
|
3037
|
+
return "fallback";
|
|
3038
|
+
}
|
|
3039
|
+
if (hasKeyword(["validation", "invalid", "required", "constraint", "duplicate", "not found", "404"])) {
|
|
2239
3040
|
return "fix-data";
|
|
2240
3041
|
}
|
|
2241
|
-
if (
|
|
3042
|
+
if (hasKeyword(["permission", "forbidden", "403", "401", "unauthorized", "access denied"])) {
|
|
2242
3043
|
return "escalate";
|
|
2243
3044
|
}
|
|
3045
|
+
const uniqueTypes = new Set(incidents.map((i) => i.error.type).filter(Boolean));
|
|
3046
|
+
if (uniqueTypes.size > 2) {
|
|
3047
|
+
return "investigate";
|
|
3048
|
+
}
|
|
2244
3049
|
return "fix-code";
|
|
2245
3050
|
}
|
|
2246
3051
|
/**
|
|
@@ -2497,6 +3302,101 @@ function getToolsList() {
|
|
|
2497
3302
|
minOccurrences: { type: "number", description: "Min similar incidents for suggestion" }
|
|
2498
3303
|
}
|
|
2499
3304
|
}
|
|
3305
|
+
},
|
|
3306
|
+
// ─── Observability Tools ──────────────────────────────────────
|
|
3307
|
+
{
|
|
3308
|
+
name: "sentinel_logs",
|
|
3309
|
+
description: "Query structured logs from connected apps. Filters by level, symbol, service, search text, time range.",
|
|
3310
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
3311
|
+
inputSchema: {
|
|
3312
|
+
type: "object",
|
|
3313
|
+
properties: {
|
|
3314
|
+
level: { type: "string", enum: ["debug", "info", "warn", "error"], description: "Filter by log level" },
|
|
3315
|
+
symbol: { type: "string", description: "Filter by symbol (partial match)" },
|
|
3316
|
+
service: { type: "string", description: "Filter by service name" },
|
|
3317
|
+
search: { type: "string", description: "Search in log messages" },
|
|
3318
|
+
since: { type: "string", description: "ISO timestamp \u2014 logs after this time" },
|
|
3319
|
+
sessionId: { type: "string", description: "Filter by session ID" },
|
|
3320
|
+
correlationId: { type: "string", description: "Filter by correlation ID" },
|
|
3321
|
+
limit: { type: "number", description: "Max results (default: 50)" }
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
},
|
|
3325
|
+
{
|
|
3326
|
+
name: "sentinel_services",
|
|
3327
|
+
description: "List all registered services with version, environment, and last-seen time.",
|
|
3328
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
3329
|
+
inputSchema: {
|
|
3330
|
+
type: "object",
|
|
3331
|
+
properties: {}
|
|
3332
|
+
}
|
|
3333
|
+
},
|
|
3334
|
+
{
|
|
3335
|
+
name: "sentinel_app_state",
|
|
3336
|
+
description: "Get live app state snapshots. Shows current state, active flows, and held gates for connected services.",
|
|
3337
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
3338
|
+
inputSchema: {
|
|
3339
|
+
type: "object",
|
|
3340
|
+
properties: {
|
|
3341
|
+
service: { type: "string", description: "Filter by service name" }
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
},
|
|
3345
|
+
{
|
|
3346
|
+
name: "sentinel_validate_symbol",
|
|
3347
|
+
description: "Check if a symbol has been used in logs. Returns usage count and suggestions.",
|
|
3348
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
3349
|
+
inputSchema: {
|
|
3350
|
+
type: "object",
|
|
3351
|
+
properties: {
|
|
3352
|
+
symbol: { type: "string", description: "Symbol to validate (e.g., #checkout, ^auth)" }
|
|
3353
|
+
},
|
|
3354
|
+
required: ["symbol"]
|
|
3355
|
+
}
|
|
3356
|
+
},
|
|
3357
|
+
{
|
|
3358
|
+
name: "sentinel_flow_activity",
|
|
3359
|
+
description: "Get recent flow events \u2014 which flow nodes were hit, in what order, by which service.",
|
|
3360
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
3361
|
+
inputSchema: {
|
|
3362
|
+
type: "object",
|
|
3363
|
+
properties: {
|
|
3364
|
+
flowId: { type: "string", description: "Filter by flow ID (e.g., $checkout-flow)" },
|
|
3365
|
+
service: { type: "string", description: "Filter by service name" },
|
|
3366
|
+
since: { type: "string", description: "ISO timestamp \u2014 events after this time" }
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
},
|
|
3370
|
+
{
|
|
3371
|
+
name: "sentinel_metrics",
|
|
3372
|
+
description: "Query metrics (counters, gauges, histograms) from connected apps. Supports filtering and aggregation.",
|
|
3373
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
3374
|
+
inputSchema: {
|
|
3375
|
+
type: "object",
|
|
3376
|
+
properties: {
|
|
3377
|
+
name: { type: "string", description: "Metric name filter" },
|
|
3378
|
+
type: { type: "string", enum: ["counter", "gauge", "histogram"], description: "Metric type filter" },
|
|
3379
|
+
service: { type: "string", description: "Service name filter" },
|
|
3380
|
+
since: { type: "string", description: "ISO timestamp \u2014 metrics after this time" },
|
|
3381
|
+
aggregate: { type: "boolean", description: "If true and name is provided, return aggregation instead of raw data" },
|
|
3382
|
+
limit: { type: "number", description: "Max results (default: 50)" }
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
},
|
|
3386
|
+
{
|
|
3387
|
+
name: "sentinel_traces",
|
|
3388
|
+
description: "Query distributed traces across services. Shows span trees with timing, status, and service hops.",
|
|
3389
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
3390
|
+
inputSchema: {
|
|
3391
|
+
type: "object",
|
|
3392
|
+
properties: {
|
|
3393
|
+
traceId: { type: "string", description: "Get a specific trace by ID" },
|
|
3394
|
+
service: { type: "string", description: "Filter by service name" },
|
|
3395
|
+
symbol: { type: "string", description: "Filter by symbol" },
|
|
3396
|
+
since: { type: "string", description: "ISO timestamp \u2014 traces after this time" },
|
|
3397
|
+
limit: { type: "number", description: "Max traces (default: 10, max: 20)" }
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
2500
3400
|
}
|
|
2501
3401
|
];
|
|
2502
3402
|
}
|
|
@@ -2725,6 +3625,137 @@ async function handleTool(name, args) {
|
|
|
2725
3625
|
2
|
|
2726
3626
|
);
|
|
2727
3627
|
}
|
|
3628
|
+
// ─── Observability Tools ──────────────────────────────────────
|
|
3629
|
+
case "sentinel_logs": {
|
|
3630
|
+
const { level, symbol, service, search, since, sessionId, correlationId, limit = 50 } = args;
|
|
3631
|
+
const logs = store.queryLogs({
|
|
3632
|
+
level,
|
|
3633
|
+
symbol,
|
|
3634
|
+
service,
|
|
3635
|
+
search,
|
|
3636
|
+
since,
|
|
3637
|
+
sessionId,
|
|
3638
|
+
correlationId,
|
|
3639
|
+
limit
|
|
3640
|
+
});
|
|
3641
|
+
const total = store.getLogCount({ level, symbol, service, since });
|
|
3642
|
+
return JSON.stringify({
|
|
3643
|
+
count: logs.length,
|
|
3644
|
+
total,
|
|
3645
|
+
logs: logs.map((l) => ({
|
|
3646
|
+
timestamp: l.timestamp,
|
|
3647
|
+
level: l.level,
|
|
3648
|
+
symbol: l.symbol,
|
|
3649
|
+
service: l.service,
|
|
3650
|
+
message: l.message,
|
|
3651
|
+
data: l.data,
|
|
3652
|
+
sessionId: l.sessionId,
|
|
3653
|
+
correlationId: l.correlationId,
|
|
3654
|
+
durationMs: l.durationMs
|
|
3655
|
+
}))
|
|
3656
|
+
}, null, 2);
|
|
3657
|
+
}
|
|
3658
|
+
case "sentinel_services": {
|
|
3659
|
+
const services = store.getServices();
|
|
3660
|
+
return JSON.stringify({
|
|
3661
|
+
count: services.length,
|
|
3662
|
+
services: services.map((s) => ({
|
|
3663
|
+
name: s.name,
|
|
3664
|
+
version: s.version,
|
|
3665
|
+
environment: s.environment,
|
|
3666
|
+
lastSeen: s.lastSeenAt,
|
|
3667
|
+
startedAt: s.startedAt,
|
|
3668
|
+
pid: s.pid
|
|
3669
|
+
}))
|
|
3670
|
+
}, null, 2);
|
|
3671
|
+
}
|
|
3672
|
+
case "sentinel_app_state": {
|
|
3673
|
+
const { service: svc } = args;
|
|
3674
|
+
const states = svc ? store.getAppState(svc) : store.getAllAppStates();
|
|
3675
|
+
return JSON.stringify({
|
|
3676
|
+
states: states.map((s) => ({
|
|
3677
|
+
service: s.service,
|
|
3678
|
+
sessionId: s.sessionId,
|
|
3679
|
+
state: s.state,
|
|
3680
|
+
activeFlows: s.activeFlows,
|
|
3681
|
+
activeGates: s.activeGates,
|
|
3682
|
+
timestamp: s.timestamp
|
|
3683
|
+
}))
|
|
3684
|
+
}, null, 2);
|
|
3685
|
+
}
|
|
3686
|
+
case "sentinel_validate_symbol": {
|
|
3687
|
+
const { symbol: sym } = args;
|
|
3688
|
+
const logCount = store.getLogCount({ symbol: sym });
|
|
3689
|
+
return JSON.stringify({
|
|
3690
|
+
symbol: sym,
|
|
3691
|
+
usedInLogs: logCount > 0,
|
|
3692
|
+
logCount,
|
|
3693
|
+
tip: logCount === 0 ? "This symbol has not appeared in any logs. It may be a typo or unused." : `This symbol has been used in ${logCount} log entries.`
|
|
3694
|
+
}, null, 2);
|
|
3695
|
+
}
|
|
3696
|
+
case "sentinel_flow_activity": {
|
|
3697
|
+
const { flowId, service: flowSvc, since: flowSince } = args;
|
|
3698
|
+
const flowLogs = store.queryLogs({ symbol: flowId, service: flowSvc, since: flowSince, limit: 100 });
|
|
3699
|
+
const flowEvents = flowLogs.filter((l) => ["flow", "signal", "gate"].includes(l.symbolType)).map((l) => ({
|
|
3700
|
+
timestamp: l.timestamp,
|
|
3701
|
+
symbol: l.symbol,
|
|
3702
|
+
symbolType: l.symbolType,
|
|
3703
|
+
service: l.service,
|
|
3704
|
+
message: l.message,
|
|
3705
|
+
level: l.level
|
|
3706
|
+
}));
|
|
3707
|
+
return JSON.stringify({ count: flowEvents.length, events: flowEvents }, null, 2);
|
|
3708
|
+
}
|
|
3709
|
+
case "sentinel_metrics": {
|
|
3710
|
+
const { name: metricName, type: metricType, service: metricSvc, since: metricSince, aggregate, limit: metricLimit } = args;
|
|
3711
|
+
if (aggregate && metricName) {
|
|
3712
|
+
const agg = store.aggregateMetric(metricName, { service: metricSvc, since: metricSince });
|
|
3713
|
+
return JSON.stringify(agg, null, 2);
|
|
3714
|
+
}
|
|
3715
|
+
const metrics = store.queryMetrics({
|
|
3716
|
+
name: metricName,
|
|
3717
|
+
type: metricType,
|
|
3718
|
+
service: metricSvc,
|
|
3719
|
+
since: metricSince,
|
|
3720
|
+
limit: Math.min(metricLimit || 50, 100)
|
|
3721
|
+
});
|
|
3722
|
+
return JSON.stringify({
|
|
3723
|
+
count: metrics.length,
|
|
3724
|
+
metrics: metrics.map((m) => ({
|
|
3725
|
+
timestamp: m.timestamp,
|
|
3726
|
+
name: m.name,
|
|
3727
|
+
type: m.type,
|
|
3728
|
+
value: m.value,
|
|
3729
|
+
tags: m.tags,
|
|
3730
|
+
service: m.service
|
|
3731
|
+
}))
|
|
3732
|
+
}, null, 2);
|
|
3733
|
+
}
|
|
3734
|
+
case "sentinel_traces": {
|
|
3735
|
+
const { traceId: tid, service: traceSvc, symbol: traceSym, since: traceSince, limit: traceLimit } = args;
|
|
3736
|
+
if (tid) {
|
|
3737
|
+
const trace = store.getTrace(tid);
|
|
3738
|
+
if (!trace) return JSON.stringify({ error: "Trace not found" });
|
|
3739
|
+
return JSON.stringify(trace, null, 2);
|
|
3740
|
+
}
|
|
3741
|
+
const traces = store.queryTraces({
|
|
3742
|
+
service: traceSvc,
|
|
3743
|
+
symbol: traceSym,
|
|
3744
|
+
since: traceSince,
|
|
3745
|
+
limit: Math.min(traceLimit || 10, 20)
|
|
3746
|
+
});
|
|
3747
|
+
return JSON.stringify({
|
|
3748
|
+
count: traces.length,
|
|
3749
|
+
traces: traces.map((t) => ({
|
|
3750
|
+
traceId: t.traceId,
|
|
3751
|
+
services: t.services,
|
|
3752
|
+
spanCount: t.spans.length,
|
|
3753
|
+
totalDurationMs: t.totalDurationMs,
|
|
3754
|
+
startTime: t.startTime,
|
|
3755
|
+
endTime: t.endTime
|
|
3756
|
+
}))
|
|
3757
|
+
}, null, 2);
|
|
3758
|
+
}
|
|
2728
3759
|
default:
|
|
2729
3760
|
return JSON.stringify({ error: `Unknown tool: ${name}` });
|
|
2730
3761
|
}
|