@hasna/logs 0.0.1 → 0.2.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/dashboard/README.md +73 -0
- package/dashboard/bun.lock +526 -0
- package/dashboard/eslint.config.js +23 -0
- package/dashboard/index.html +13 -0
- package/dashboard/package.json +32 -0
- package/dashboard/public/favicon.svg +1 -0
- package/dashboard/public/icons.svg +24 -0
- package/dashboard/src/App.css +184 -0
- package/dashboard/src/App.tsx +49 -0
- package/dashboard/src/api.ts +33 -0
- package/dashboard/src/assets/hero.png +0 -0
- package/dashboard/src/assets/react.svg +1 -0
- package/dashboard/src/assets/vite.svg +1 -0
- package/dashboard/src/index.css +111 -0
- package/dashboard/src/main.tsx +10 -0
- package/dashboard/src/pages/Alerts.tsx +69 -0
- package/dashboard/src/pages/Issues.tsx +50 -0
- package/dashboard/src/pages/Perf.tsx +75 -0
- package/dashboard/src/pages/Projects.tsx +67 -0
- package/dashboard/src/pages/Summary.tsx +67 -0
- package/dashboard/src/pages/Tail.tsx +65 -0
- package/dashboard/tsconfig.app.json +28 -0
- package/dashboard/tsconfig.json +7 -0
- package/dashboard/tsconfig.node.json +26 -0
- package/dashboard/vite.config.ts +14 -0
- package/dist/cli/index.js +116 -12
- package/dist/mcp/index.js +306 -100
- package/dist/server/index.js +592 -7
- package/package.json +12 -2
- package/sdk/package.json +3 -2
- package/sdk/src/index.ts +1 -1
- package/sdk/src/types.ts +56 -0
- package/src/cli/index.ts +114 -4
- package/src/db/index.ts +10 -0
- package/src/db/migrations/001_alert_rules.ts +21 -0
- package/src/db/migrations/002_issues.ts +21 -0
- package/src/db/migrations/003_retention.ts +15 -0
- package/src/db/migrations/004_page_auth.ts +13 -0
- package/src/lib/alerts.test.ts +67 -0
- package/src/lib/alerts.ts +117 -0
- package/src/lib/compare.test.ts +52 -0
- package/src/lib/compare.ts +85 -0
- package/src/lib/diagnose.test.ts +55 -0
- package/src/lib/diagnose.ts +76 -0
- package/src/lib/export.test.ts +66 -0
- package/src/lib/export.ts +65 -0
- package/src/lib/health.test.ts +48 -0
- package/src/lib/health.ts +51 -0
- package/src/lib/ingest.ts +25 -2
- package/src/lib/issues.test.ts +79 -0
- package/src/lib/issues.ts +70 -0
- package/src/lib/page-auth.test.ts +54 -0
- package/src/lib/page-auth.ts +48 -0
- package/src/lib/retention.test.ts +42 -0
- package/src/lib/retention.ts +62 -0
- package/src/lib/scanner.ts +21 -2
- package/src/lib/scheduler.ts +6 -0
- package/src/lib/session-context.ts +28 -0
- package/src/mcp/index.ts +133 -89
- package/src/server/index.ts +12 -1
- package/src/server/routes/alerts.ts +32 -0
- package/src/server/routes/issues.ts +43 -0
- package/src/server/routes/logs.ts +21 -0
- package/src/server/routes/projects.ts +25 -0
- package/src/server/routes/stream.ts +43 -0
package/dist/mcp/index.js
CHANGED
|
@@ -1,27 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
import {
|
|
4
|
+
createAlertRule,
|
|
4
5
|
createPage,
|
|
5
6
|
createProject,
|
|
7
|
+
deleteAlertRule,
|
|
6
8
|
getDb,
|
|
7
9
|
getLatestSnapshot,
|
|
8
|
-
getLogContext,
|
|
9
10
|
getPerfTrend,
|
|
10
11
|
ingestLog,
|
|
12
|
+
listAlertRules,
|
|
13
|
+
listIssues,
|
|
11
14
|
listPages,
|
|
12
15
|
listProjects,
|
|
13
16
|
scoreLabel,
|
|
14
|
-
searchLogs,
|
|
15
17
|
summarizeLogs,
|
|
18
|
+
updateIssueStatus
|
|
19
|
+
} from "../index-77ss2sf4.js";
|
|
20
|
+
import {
|
|
21
|
+
createJob,
|
|
22
|
+
listJobs
|
|
23
|
+
} from "../jobs-124e878j.js";
|
|
24
|
+
import {
|
|
25
|
+
getLogContext,
|
|
26
|
+
searchLogs,
|
|
16
27
|
tailLogs
|
|
17
|
-
} from "../
|
|
28
|
+
} from "../query-0qv7fvzt.js";
|
|
29
|
+
import {
|
|
30
|
+
getHealth
|
|
31
|
+
} from "../health-f2qrebqc.js";
|
|
18
32
|
import {
|
|
19
33
|
__commonJS,
|
|
20
34
|
__export,
|
|
21
|
-
__toESM
|
|
22
|
-
|
|
23
|
-
listJobs
|
|
24
|
-
} from "../index-4mnved04.js";
|
|
35
|
+
__toESM
|
|
36
|
+
} from "../index-g8dczzvv.js";
|
|
25
37
|
|
|
26
38
|
// node_modules/ajv/dist/compile/codegen/code.js
|
|
27
39
|
var require_code = __commonJS((exports) => {
|
|
@@ -6282,7 +6294,7 @@ var require_formats = __commonJS((exports) => {
|
|
|
6282
6294
|
}
|
|
6283
6295
|
var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
6284
6296
|
function getTime(strictTimeZone) {
|
|
6285
|
-
return function
|
|
6297
|
+
return function time(str) {
|
|
6286
6298
|
const matches = TIME.exec(str);
|
|
6287
6299
|
if (!matches)
|
|
6288
6300
|
return false;
|
|
@@ -10322,7 +10334,7 @@ __export(exports_core2, {
|
|
|
10322
10334
|
safeDecode: () => safeDecode,
|
|
10323
10335
|
registry: () => registry,
|
|
10324
10336
|
regexes: () => exports_regexes,
|
|
10325
|
-
process: () =>
|
|
10337
|
+
process: () => process2,
|
|
10326
10338
|
prettifyError: () => prettifyError,
|
|
10327
10339
|
parseAsync: () => parseAsync,
|
|
10328
10340
|
parse: () => parse,
|
|
@@ -20786,7 +20798,7 @@ function initializeContext(params) {
|
|
|
20786
20798
|
external: params?.external ?? undefined
|
|
20787
20799
|
};
|
|
20788
20800
|
}
|
|
20789
|
-
function
|
|
20801
|
+
function process2(schema, ctx, _params = { path: [], schemaPath: [] }) {
|
|
20790
20802
|
var _a2;
|
|
20791
20803
|
const def = schema._zod.def;
|
|
20792
20804
|
const seen = ctx.seen.get(schema);
|
|
@@ -20823,7 +20835,7 @@ function process(schema, ctx, _params = { path: [], schemaPath: [] }) {
|
|
|
20823
20835
|
if (parent) {
|
|
20824
20836
|
if (!result.ref)
|
|
20825
20837
|
result.ref = parent;
|
|
20826
|
-
|
|
20838
|
+
process2(parent, ctx, params);
|
|
20827
20839
|
ctx.seen.get(parent).isParent = true;
|
|
20828
20840
|
}
|
|
20829
20841
|
}
|
|
@@ -21099,14 +21111,14 @@ function isTransforming(_schema, _ctx) {
|
|
|
21099
21111
|
}
|
|
21100
21112
|
var createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
|
|
21101
21113
|
const ctx = initializeContext({ ...params, processors });
|
|
21102
|
-
|
|
21114
|
+
process2(schema, ctx);
|
|
21103
21115
|
extractDefs(ctx, schema);
|
|
21104
21116
|
return finalize(ctx, schema);
|
|
21105
21117
|
};
|
|
21106
21118
|
var createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) => {
|
|
21107
21119
|
const { libraryOptions, target } = params ?? {};
|
|
21108
21120
|
const ctx = initializeContext({ ...libraryOptions ?? {}, target, io, processors });
|
|
21109
|
-
|
|
21121
|
+
process2(schema, ctx);
|
|
21110
21122
|
extractDefs(ctx, schema);
|
|
21111
21123
|
return finalize(ctx, schema);
|
|
21112
21124
|
};
|
|
@@ -21357,7 +21369,7 @@ var arrayProcessor = (schema, ctx, _json, params) => {
|
|
|
21357
21369
|
if (typeof maximum === "number")
|
|
21358
21370
|
json.maxItems = maximum;
|
|
21359
21371
|
json.type = "array";
|
|
21360
|
-
json.items =
|
|
21372
|
+
json.items = process2(def.element, ctx, { ...params, path: [...params.path, "items"] });
|
|
21361
21373
|
};
|
|
21362
21374
|
var objectProcessor = (schema, ctx, _json, params) => {
|
|
21363
21375
|
const json = _json;
|
|
@@ -21366,7 +21378,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
|
|
|
21366
21378
|
json.properties = {};
|
|
21367
21379
|
const shape = def.shape;
|
|
21368
21380
|
for (const key in shape) {
|
|
21369
|
-
json.properties[key] =
|
|
21381
|
+
json.properties[key] = process2(shape[key], ctx, {
|
|
21370
21382
|
...params,
|
|
21371
21383
|
path: [...params.path, "properties", key]
|
|
21372
21384
|
});
|
|
@@ -21389,7 +21401,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
|
|
|
21389
21401
|
if (ctx.io === "output")
|
|
21390
21402
|
json.additionalProperties = false;
|
|
21391
21403
|
} else if (def.catchall) {
|
|
21392
|
-
json.additionalProperties =
|
|
21404
|
+
json.additionalProperties = process2(def.catchall, ctx, {
|
|
21393
21405
|
...params,
|
|
21394
21406
|
path: [...params.path, "additionalProperties"]
|
|
21395
21407
|
});
|
|
@@ -21398,7 +21410,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
|
|
|
21398
21410
|
var unionProcessor = (schema, ctx, json, params) => {
|
|
21399
21411
|
const def = schema._zod.def;
|
|
21400
21412
|
const isExclusive = def.inclusive === false;
|
|
21401
|
-
const options = def.options.map((x, i) =>
|
|
21413
|
+
const options = def.options.map((x, i) => process2(x, ctx, {
|
|
21402
21414
|
...params,
|
|
21403
21415
|
path: [...params.path, isExclusive ? "oneOf" : "anyOf", i]
|
|
21404
21416
|
}));
|
|
@@ -21410,11 +21422,11 @@ var unionProcessor = (schema, ctx, json, params) => {
|
|
|
21410
21422
|
};
|
|
21411
21423
|
var intersectionProcessor = (schema, ctx, json, params) => {
|
|
21412
21424
|
const def = schema._zod.def;
|
|
21413
|
-
const a =
|
|
21425
|
+
const a = process2(def.left, ctx, {
|
|
21414
21426
|
...params,
|
|
21415
21427
|
path: [...params.path, "allOf", 0]
|
|
21416
21428
|
});
|
|
21417
|
-
const b =
|
|
21429
|
+
const b = process2(def.right, ctx, {
|
|
21418
21430
|
...params,
|
|
21419
21431
|
path: [...params.path, "allOf", 1]
|
|
21420
21432
|
});
|
|
@@ -21431,11 +21443,11 @@ var tupleProcessor = (schema, ctx, _json, params) => {
|
|
|
21431
21443
|
json.type = "array";
|
|
21432
21444
|
const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items";
|
|
21433
21445
|
const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
|
|
21434
|
-
const prefixItems = def.items.map((x, i) =>
|
|
21446
|
+
const prefixItems = def.items.map((x, i) => process2(x, ctx, {
|
|
21435
21447
|
...params,
|
|
21436
21448
|
path: [...params.path, prefixPath, i]
|
|
21437
21449
|
}));
|
|
21438
|
-
const rest = def.rest ?
|
|
21450
|
+
const rest = def.rest ? process2(def.rest, ctx, {
|
|
21439
21451
|
...params,
|
|
21440
21452
|
path: [...params.path, restPath, ...ctx.target === "openapi-3.0" ? [def.items.length] : []]
|
|
21441
21453
|
}) : null;
|
|
@@ -21475,7 +21487,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
|
|
|
21475
21487
|
const keyBag = keyType._zod.bag;
|
|
21476
21488
|
const patterns = keyBag?.patterns;
|
|
21477
21489
|
if (def.mode === "loose" && patterns && patterns.size > 0) {
|
|
21478
|
-
const valueSchema =
|
|
21490
|
+
const valueSchema = process2(def.valueType, ctx, {
|
|
21479
21491
|
...params,
|
|
21480
21492
|
path: [...params.path, "patternProperties", "*"]
|
|
21481
21493
|
});
|
|
@@ -21485,12 +21497,12 @@ var recordProcessor = (schema, ctx, _json, params) => {
|
|
|
21485
21497
|
}
|
|
21486
21498
|
} else {
|
|
21487
21499
|
if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") {
|
|
21488
|
-
json.propertyNames =
|
|
21500
|
+
json.propertyNames = process2(def.keyType, ctx, {
|
|
21489
21501
|
...params,
|
|
21490
21502
|
path: [...params.path, "propertyNames"]
|
|
21491
21503
|
});
|
|
21492
21504
|
}
|
|
21493
|
-
json.additionalProperties =
|
|
21505
|
+
json.additionalProperties = process2(def.valueType, ctx, {
|
|
21494
21506
|
...params,
|
|
21495
21507
|
path: [...params.path, "additionalProperties"]
|
|
21496
21508
|
});
|
|
@@ -21505,7 +21517,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
|
|
|
21505
21517
|
};
|
|
21506
21518
|
var nullableProcessor = (schema, ctx, json, params) => {
|
|
21507
21519
|
const def = schema._zod.def;
|
|
21508
|
-
const inner =
|
|
21520
|
+
const inner = process2(def.innerType, ctx, params);
|
|
21509
21521
|
const seen = ctx.seen.get(schema);
|
|
21510
21522
|
if (ctx.target === "openapi-3.0") {
|
|
21511
21523
|
seen.ref = def.innerType;
|
|
@@ -21516,20 +21528,20 @@ var nullableProcessor = (schema, ctx, json, params) => {
|
|
|
21516
21528
|
};
|
|
21517
21529
|
var nonoptionalProcessor = (schema, ctx, _json, params) => {
|
|
21518
21530
|
const def = schema._zod.def;
|
|
21519
|
-
|
|
21531
|
+
process2(def.innerType, ctx, params);
|
|
21520
21532
|
const seen = ctx.seen.get(schema);
|
|
21521
21533
|
seen.ref = def.innerType;
|
|
21522
21534
|
};
|
|
21523
21535
|
var defaultProcessor = (schema, ctx, json, params) => {
|
|
21524
21536
|
const def = schema._zod.def;
|
|
21525
|
-
|
|
21537
|
+
process2(def.innerType, ctx, params);
|
|
21526
21538
|
const seen = ctx.seen.get(schema);
|
|
21527
21539
|
seen.ref = def.innerType;
|
|
21528
21540
|
json.default = JSON.parse(JSON.stringify(def.defaultValue));
|
|
21529
21541
|
};
|
|
21530
21542
|
var prefaultProcessor = (schema, ctx, json, params) => {
|
|
21531
21543
|
const def = schema._zod.def;
|
|
21532
|
-
|
|
21544
|
+
process2(def.innerType, ctx, params);
|
|
21533
21545
|
const seen = ctx.seen.get(schema);
|
|
21534
21546
|
seen.ref = def.innerType;
|
|
21535
21547
|
if (ctx.io === "input")
|
|
@@ -21537,7 +21549,7 @@ var prefaultProcessor = (schema, ctx, json, params) => {
|
|
|
21537
21549
|
};
|
|
21538
21550
|
var catchProcessor = (schema, ctx, json, params) => {
|
|
21539
21551
|
const def = schema._zod.def;
|
|
21540
|
-
|
|
21552
|
+
process2(def.innerType, ctx, params);
|
|
21541
21553
|
const seen = ctx.seen.get(schema);
|
|
21542
21554
|
seen.ref = def.innerType;
|
|
21543
21555
|
let catchValue;
|
|
@@ -21551,32 +21563,32 @@ var catchProcessor = (schema, ctx, json, params) => {
|
|
|
21551
21563
|
var pipeProcessor = (schema, ctx, _json, params) => {
|
|
21552
21564
|
const def = schema._zod.def;
|
|
21553
21565
|
const innerType = ctx.io === "input" ? def.in._zod.def.type === "transform" ? def.out : def.in : def.out;
|
|
21554
|
-
|
|
21566
|
+
process2(innerType, ctx, params);
|
|
21555
21567
|
const seen = ctx.seen.get(schema);
|
|
21556
21568
|
seen.ref = innerType;
|
|
21557
21569
|
};
|
|
21558
21570
|
var readonlyProcessor = (schema, ctx, json, params) => {
|
|
21559
21571
|
const def = schema._zod.def;
|
|
21560
|
-
|
|
21572
|
+
process2(def.innerType, ctx, params);
|
|
21561
21573
|
const seen = ctx.seen.get(schema);
|
|
21562
21574
|
seen.ref = def.innerType;
|
|
21563
21575
|
json.readOnly = true;
|
|
21564
21576
|
};
|
|
21565
21577
|
var promiseProcessor = (schema, ctx, _json, params) => {
|
|
21566
21578
|
const def = schema._zod.def;
|
|
21567
|
-
|
|
21579
|
+
process2(def.innerType, ctx, params);
|
|
21568
21580
|
const seen = ctx.seen.get(schema);
|
|
21569
21581
|
seen.ref = def.innerType;
|
|
21570
21582
|
};
|
|
21571
21583
|
var optionalProcessor = (schema, ctx, _json, params) => {
|
|
21572
21584
|
const def = schema._zod.def;
|
|
21573
|
-
|
|
21585
|
+
process2(def.innerType, ctx, params);
|
|
21574
21586
|
const seen = ctx.seen.get(schema);
|
|
21575
21587
|
seen.ref = def.innerType;
|
|
21576
21588
|
};
|
|
21577
21589
|
var lazyProcessor = (schema, ctx, _json, params) => {
|
|
21578
21590
|
const innerType = schema._zod.innerType;
|
|
21579
|
-
|
|
21591
|
+
process2(innerType, ctx, params);
|
|
21580
21592
|
const seen = ctx.seen.get(schema);
|
|
21581
21593
|
seen.ref = innerType;
|
|
21582
21594
|
};
|
|
@@ -21628,7 +21640,7 @@ function toJSONSchema(input, params) {
|
|
|
21628
21640
|
const defs = {};
|
|
21629
21641
|
for (const entry of registry2._idmap.entries()) {
|
|
21630
21642
|
const [_, schema] = entry;
|
|
21631
|
-
|
|
21643
|
+
process2(schema, ctx2);
|
|
21632
21644
|
}
|
|
21633
21645
|
const schemas = {};
|
|
21634
21646
|
const external = {
|
|
@@ -21651,7 +21663,7 @@ function toJSONSchema(input, params) {
|
|
|
21651
21663
|
return { schemas };
|
|
21652
21664
|
}
|
|
21653
21665
|
const ctx = initializeContext({ ...params, processors: allProcessors });
|
|
21654
|
-
|
|
21666
|
+
process2(input, ctx);
|
|
21655
21667
|
extractDefs(ctx, input);
|
|
21656
21668
|
return finalize(ctx, input);
|
|
21657
21669
|
}
|
|
@@ -21697,7 +21709,7 @@ class JSONSchemaGenerator {
|
|
|
21697
21709
|
});
|
|
21698
21710
|
}
|
|
21699
21711
|
process(schema, _params = { path: [], schemaPath: [] }) {
|
|
21700
|
-
return
|
|
21712
|
+
return process2(schema, this.ctx, _params);
|
|
21701
21713
|
}
|
|
21702
21714
|
emit(schema, _params) {
|
|
21703
21715
|
if (_params) {
|
|
@@ -28278,7 +28290,7 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
28278
28290
|
};
|
|
28279
28291
|
|
|
28280
28292
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
28281
|
-
import
|
|
28293
|
+
import process3 from "process";
|
|
28282
28294
|
|
|
28283
28295
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
28284
28296
|
class ReadBuffer {
|
|
@@ -28312,7 +28324,7 @@ function serializeMessage(message) {
|
|
|
28312
28324
|
|
|
28313
28325
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
28314
28326
|
class StdioServerTransport {
|
|
28315
|
-
constructor(_stdin =
|
|
28327
|
+
constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
|
|
28316
28328
|
this._stdin = _stdin;
|
|
28317
28329
|
this._stdout = _stdout;
|
|
28318
28330
|
this._readBuffer = new ReadBuffer;
|
|
@@ -28368,25 +28380,174 @@ class StdioServerTransport {
|
|
|
28368
28380
|
}
|
|
28369
28381
|
}
|
|
28370
28382
|
|
|
28383
|
+
// src/lib/diagnose.ts
|
|
28384
|
+
function diagnose(db, projectId, since) {
|
|
28385
|
+
const window = since ?? new Date(Date.now() - 24 * 3600 * 1000).toISOString();
|
|
28386
|
+
const top_errors = db.prepare(`
|
|
28387
|
+
SELECT message, COUNT(*) as count, service, MAX(timestamp) as last_seen
|
|
28388
|
+
FROM logs
|
|
28389
|
+
WHERE project_id = $p AND level IN ('error','fatal') AND timestamp >= $since
|
|
28390
|
+
GROUP BY message, service
|
|
28391
|
+
ORDER BY count DESC
|
|
28392
|
+
LIMIT 10
|
|
28393
|
+
`).all({ $p: projectId, $since: window });
|
|
28394
|
+
const error_rate_by_service = db.prepare(`
|
|
28395
|
+
SELECT service,
|
|
28396
|
+
SUM(CASE WHEN level IN ('error','fatal') THEN 1 ELSE 0 END) as errors,
|
|
28397
|
+
SUM(CASE WHEN level = 'warn' THEN 1 ELSE 0 END) as warns,
|
|
28398
|
+
COUNT(*) as total
|
|
28399
|
+
FROM logs
|
|
28400
|
+
WHERE project_id = $p AND timestamp >= $since
|
|
28401
|
+
GROUP BY service
|
|
28402
|
+
ORDER BY errors DESC
|
|
28403
|
+
`).all({ $p: projectId, $since: window });
|
|
28404
|
+
const failing_pages = db.prepare(`
|
|
28405
|
+
SELECT l.page_id, p.url, COUNT(*) as error_count
|
|
28406
|
+
FROM logs l
|
|
28407
|
+
JOIN pages p ON p.id = l.page_id
|
|
28408
|
+
WHERE l.project_id = $p AND l.level IN ('error','fatal') AND l.timestamp >= $since AND l.page_id IS NOT NULL
|
|
28409
|
+
GROUP BY l.page_id, p.url
|
|
28410
|
+
ORDER BY error_count DESC
|
|
28411
|
+
LIMIT 10
|
|
28412
|
+
`).all({ $p: projectId, $since: window });
|
|
28413
|
+
const perf_regressions = db.prepare(`
|
|
28414
|
+
SELECT * FROM (
|
|
28415
|
+
SELECT
|
|
28416
|
+
cur.page_id,
|
|
28417
|
+
p.url,
|
|
28418
|
+
cur.score as score_now,
|
|
28419
|
+
prev.score as score_prev,
|
|
28420
|
+
(cur.score - prev.score) as delta
|
|
28421
|
+
FROM performance_snapshots cur
|
|
28422
|
+
JOIN pages p ON p.id = cur.page_id
|
|
28423
|
+
LEFT JOIN performance_snapshots prev ON prev.page_id = cur.page_id AND prev.id != cur.id
|
|
28424
|
+
WHERE cur.project_id = $p
|
|
28425
|
+
AND cur.timestamp = (SELECT MAX(timestamp) FROM performance_snapshots WHERE page_id = cur.page_id)
|
|
28426
|
+
AND (prev.timestamp = (SELECT MAX(timestamp) FROM performance_snapshots WHERE page_id = cur.page_id AND id != cur.id) OR prev.id IS NULL)
|
|
28427
|
+
) WHERE delta < -5 OR delta IS NULL
|
|
28428
|
+
ORDER BY delta ASC
|
|
28429
|
+
LIMIT 10
|
|
28430
|
+
`).all({ $p: projectId });
|
|
28431
|
+
const totalErrors = top_errors.reduce((s, e) => s + e.count, 0);
|
|
28432
|
+
const topService = error_rate_by_service[0];
|
|
28433
|
+
const summary = totalErrors === 0 ? "No errors in this window. All looks good." : `${totalErrors} error(s) detected. Worst service: ${topService?.service ?? "unknown"} (${topService?.errors ?? 0} errors). ${failing_pages.length} page(s) with errors. ${perf_regressions.length} perf regression(s).`;
|
|
28434
|
+
return { project_id: projectId, window, top_errors, error_rate_by_service, failing_pages, perf_regressions, summary };
|
|
28435
|
+
}
|
|
28436
|
+
|
|
28437
|
+
// src/lib/compare.ts
|
|
28438
|
+
function getErrorsByMessage(db, projectId, since, until) {
|
|
28439
|
+
return db.prepare(`
|
|
28440
|
+
SELECT message, service, COUNT(*) as count
|
|
28441
|
+
FROM logs
|
|
28442
|
+
WHERE project_id = $p AND level IN ('error','fatal') AND timestamp >= $since AND timestamp <= $until
|
|
28443
|
+
GROUP BY message, service
|
|
28444
|
+
`).all({ $p: projectId, $since: since, $until: until });
|
|
28445
|
+
}
|
|
28446
|
+
function getErrorsByService(db, projectId, since, until) {
|
|
28447
|
+
return db.prepare(`
|
|
28448
|
+
SELECT service, COUNT(*) as errors
|
|
28449
|
+
FROM logs
|
|
28450
|
+
WHERE project_id = $p AND level IN ('error','fatal') AND timestamp >= $since AND timestamp <= $until
|
|
28451
|
+
GROUP BY service
|
|
28452
|
+
`).all({ $p: projectId, $since: since, $until: until });
|
|
28453
|
+
}
|
|
28454
|
+
function compare(db, projectId, aSince, aUntil, bSince, bUntil) {
|
|
28455
|
+
const errorsA = getErrorsByMessage(db, projectId, aSince, aUntil);
|
|
28456
|
+
const errorsB = getErrorsByMessage(db, projectId, bSince, bUntil);
|
|
28457
|
+
const keyA = new Set(errorsA.map((e) => `${e.service}|${e.message}`));
|
|
28458
|
+
const keyB = new Set(errorsB.map((e) => `${e.service}|${e.message}`));
|
|
28459
|
+
const new_errors = errorsB.filter((e) => !keyA.has(`${e.service}|${e.message}`));
|
|
28460
|
+
const resolved_errors = errorsA.filter((e) => !keyB.has(`${e.service}|${e.message}`));
|
|
28461
|
+
const svcA = getErrorsByService(db, projectId, aSince, aUntil);
|
|
28462
|
+
const svcB = getErrorsByService(db, projectId, bSince, bUntil);
|
|
28463
|
+
const svcMapA = new Map(svcA.map((s) => [s.service, s.errors]));
|
|
28464
|
+
const svcMapB = new Map(svcB.map((s) => [s.service, s.errors]));
|
|
28465
|
+
const allSvcs = new Set([...svcMapA.keys(), ...svcMapB.keys()]);
|
|
28466
|
+
const error_delta_by_service = [...allSvcs].map((svc) => ({
|
|
28467
|
+
service: svc,
|
|
28468
|
+
errors_a: svcMapA.get(svc) ?? 0,
|
|
28469
|
+
errors_b: svcMapB.get(svc) ?? 0,
|
|
28470
|
+
delta: (svcMapB.get(svc) ?? 0) - (svcMapA.get(svc) ?? 0)
|
|
28471
|
+
})).sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
|
|
28472
|
+
const perf_delta_by_page = db.prepare(`
|
|
28473
|
+
SELECT
|
|
28474
|
+
pa.page_id, pg.url,
|
|
28475
|
+
pa.score as score_a,
|
|
28476
|
+
pb.score as score_b,
|
|
28477
|
+
(pb.score - pa.score) as delta
|
|
28478
|
+
FROM
|
|
28479
|
+
(SELECT page_id, AVG(score) as score FROM performance_snapshots WHERE project_id = $p AND timestamp >= $as AND timestamp <= $au GROUP BY page_id) pa
|
|
28480
|
+
JOIN pages pg ON pg.id = pa.page_id
|
|
28481
|
+
LEFT JOIN (SELECT page_id, AVG(score) as score FROM performance_snapshots WHERE project_id = $p AND timestamp >= $bs AND timestamp <= $bu GROUP BY page_id) pb ON pb.page_id = pa.page_id
|
|
28482
|
+
ORDER BY delta ASC
|
|
28483
|
+
`).all({ $p: projectId, $as: aSince, $au: aUntil, $bs: bSince, $bu: bUntil });
|
|
28484
|
+
const summary = [
|
|
28485
|
+
`${new_errors.length} new error type(s), ${resolved_errors.length} resolved.`,
|
|
28486
|
+
error_delta_by_service.filter((s) => s.delta > 0).map((s) => `${s.service ?? "unknown"}: +${s.delta}`).join(", ") || "No error increases."
|
|
28487
|
+
].join(" ");
|
|
28488
|
+
return {
|
|
28489
|
+
project_id: projectId,
|
|
28490
|
+
window_a: { since: aSince, until: aUntil },
|
|
28491
|
+
window_b: { since: bSince, until: bUntil },
|
|
28492
|
+
new_errors,
|
|
28493
|
+
resolved_errors,
|
|
28494
|
+
error_delta_by_service,
|
|
28495
|
+
perf_delta_by_page,
|
|
28496
|
+
summary
|
|
28497
|
+
};
|
|
28498
|
+
}
|
|
28499
|
+
|
|
28500
|
+
// src/lib/session-context.ts
|
|
28501
|
+
async function getSessionContext(db, sessionId) {
|
|
28502
|
+
const logs = db.prepare("SELECT * FROM logs WHERE session_id = $s ORDER BY timestamp ASC").all({ $s: sessionId });
|
|
28503
|
+
const sessionsUrl = process.env.SESSIONS_URL;
|
|
28504
|
+
if (!sessionsUrl) {
|
|
28505
|
+
return { session_id: sessionId, logs };
|
|
28506
|
+
}
|
|
28507
|
+
try {
|
|
28508
|
+
const res = await fetch(`${sessionsUrl.replace(/\/$/, "")}/api/sessions/${sessionId}`);
|
|
28509
|
+
if (!res.ok)
|
|
28510
|
+
return { session_id: sessionId, logs };
|
|
28511
|
+
const session = await res.json();
|
|
28512
|
+
return { session_id: sessionId, logs, session };
|
|
28513
|
+
} catch (err) {
|
|
28514
|
+
return { session_id: sessionId, logs, error: String(err) };
|
|
28515
|
+
}
|
|
28516
|
+
}
|
|
28517
|
+
|
|
28371
28518
|
// src/mcp/index.ts
|
|
28372
28519
|
var db = getDb();
|
|
28373
|
-
var server = new McpServer({ name: "logs", version: "0.0
|
|
28520
|
+
var server = new McpServer({ name: "logs", version: "0.1.0" });
|
|
28521
|
+
function applyBrief(rows, brief = true) {
|
|
28522
|
+
if (!brief)
|
|
28523
|
+
return rows;
|
|
28524
|
+
return rows.map((r) => ({ id: r.id, timestamp: r.timestamp, level: r.level, message: r.message, service: r.service }));
|
|
28525
|
+
}
|
|
28374
28526
|
var TOOLS = {
|
|
28375
28527
|
register_project: "Register a project (name, github_repo?, base_url?, description?)",
|
|
28376
|
-
register_page: "Register a page URL
|
|
28377
|
-
create_scan_job: "Schedule
|
|
28528
|
+
register_page: "Register a page URL (project_id, url, path?, name?)",
|
|
28529
|
+
create_scan_job: "Schedule page scans (project_id, schedule, page_id?)",
|
|
28378
28530
|
log_push: "Push a log entry (level, message, project_id?, service?, trace_id?, metadata?)",
|
|
28379
|
-
log_search: "Search logs (project_id?,
|
|
28380
|
-
log_tail: "
|
|
28381
|
-
log_summary: "Error/warn counts by service
|
|
28382
|
-
log_context: "All logs for a trace_id",
|
|
28383
|
-
|
|
28384
|
-
|
|
28385
|
-
|
|
28386
|
-
|
|
28531
|
+
log_search: "Search logs (project_id?, level?, since?, text?, brief?=true, limit?)",
|
|
28532
|
+
log_tail: "Recent logs (project_id?, n?, brief?=true)",
|
|
28533
|
+
log_summary: "Error/warn counts by service (project_id?, since?)",
|
|
28534
|
+
log_context: "All logs for a trace_id (trace_id, brief?=true)",
|
|
28535
|
+
log_diagnose: "Full diagnosis: top errors, failing pages, perf regressions (project_id, since?)",
|
|
28536
|
+
log_compare: "Compare two time windows for new/resolved errors and perf delta",
|
|
28537
|
+
perf_snapshot: "Latest perf snapshot (project_id, page_id?)",
|
|
28538
|
+
perf_trend: "Perf over time (project_id, page_id?, since?, limit?)",
|
|
28539
|
+
scan_status: "Last scan jobs (project_id?)",
|
|
28540
|
+
list_projects: "List all projects",
|
|
28387
28541
|
list_pages: "List pages for a project (project_id)",
|
|
28388
|
-
|
|
28389
|
-
|
|
28542
|
+
list_issues: "List grouped error issues (project_id?, status?, limit?)",
|
|
28543
|
+
resolve_issue: "Update issue status (id, status: open|resolved|ignored)",
|
|
28544
|
+
create_alert_rule: "Create alert rule (project_id, name, level, threshold_count, window_seconds, webhook_url?)",
|
|
28545
|
+
list_alert_rules: "List alert rules (project_id?)",
|
|
28546
|
+
delete_alert_rule: "Delete alert rule (id)",
|
|
28547
|
+
log_session_context: "Logs + session metadata for a session_id (requires SESSIONS_URL env)",
|
|
28548
|
+
get_health: "Server health + DB stats",
|
|
28549
|
+
search_tools: "Search tools by keyword (query)",
|
|
28550
|
+
describe_tools: "List all tools"
|
|
28390
28551
|
};
|
|
28391
28552
|
server.tool("search_tools", { query: exports_external.string() }, ({ query }) => {
|
|
28392
28553
|
const q = query.toLowerCase();
|
|
@@ -28394,37 +28555,27 @@ server.tool("search_tools", { query: exports_external.string() }, ({ query }) =>
|
|
|
28394
28555
|
return { content: [{ type: "text", text: matches.map(([k, v]) => `${k}: ${v}`).join(`
|
|
28395
28556
|
`) || "No matches" }] };
|
|
28396
28557
|
});
|
|
28397
|
-
server.tool("describe_tools", {}, () => {
|
|
28398
|
-
|
|
28399
|
-
`)
|
|
28400
|
-
|
|
28401
|
-
});
|
|
28558
|
+
server.tool("describe_tools", {}, () => ({
|
|
28559
|
+
content: [{ type: "text", text: Object.entries(TOOLS).map(([k, v]) => `${k}: ${v}`).join(`
|
|
28560
|
+
`) }]
|
|
28561
|
+
}));
|
|
28402
28562
|
server.tool("register_project", {
|
|
28403
28563
|
name: exports_external.string(),
|
|
28404
28564
|
github_repo: exports_external.string().optional(),
|
|
28405
28565
|
base_url: exports_external.string().optional(),
|
|
28406
28566
|
description: exports_external.string().optional()
|
|
28407
|
-
}, (args) => {
|
|
28408
|
-
const project = createProject(db, args);
|
|
28409
|
-
return { content: [{ type: "text", text: JSON.stringify(project) }] };
|
|
28410
|
-
});
|
|
28567
|
+
}, (args) => ({ content: [{ type: "text", text: JSON.stringify(createProject(db, args)) }] }));
|
|
28411
28568
|
server.tool("register_page", {
|
|
28412
28569
|
project_id: exports_external.string(),
|
|
28413
28570
|
url: exports_external.string(),
|
|
28414
28571
|
path: exports_external.string().optional(),
|
|
28415
28572
|
name: exports_external.string().optional()
|
|
28416
|
-
}, (args) => {
|
|
28417
|
-
const page = createPage(db, args);
|
|
28418
|
-
return { content: [{ type: "text", text: JSON.stringify(page) }] };
|
|
28419
|
-
});
|
|
28573
|
+
}, (args) => ({ content: [{ type: "text", text: JSON.stringify(createPage(db, args)) }] }));
|
|
28420
28574
|
server.tool("create_scan_job", {
|
|
28421
28575
|
project_id: exports_external.string(),
|
|
28422
28576
|
schedule: exports_external.string(),
|
|
28423
28577
|
page_id: exports_external.string().optional()
|
|
28424
|
-
}, (args) => {
|
|
28425
|
-
const job = createJob(db, args);
|
|
28426
|
-
return { content: [{ type: "text", text: JSON.stringify(job) }] };
|
|
28427
|
-
});
|
|
28578
|
+
}, (args) => ({ content: [{ type: "text", text: JSON.stringify(createJob(db, args)) }] }));
|
|
28428
28579
|
server.tool("log_push", {
|
|
28429
28580
|
level: exports_external.enum(["debug", "info", "warn", "error", "fatal"]),
|
|
28430
28581
|
message: exports_external.string(),
|
|
@@ -28448,60 +28599,115 @@ server.tool("log_search", {
|
|
|
28448
28599
|
until: exports_external.string().optional(),
|
|
28449
28600
|
text: exports_external.string().optional(),
|
|
28450
28601
|
trace_id: exports_external.string().optional(),
|
|
28451
|
-
limit: exports_external.number().optional()
|
|
28602
|
+
limit: exports_external.number().optional(),
|
|
28603
|
+
brief: exports_external.boolean().optional()
|
|
28452
28604
|
}, (args) => {
|
|
28453
|
-
const rows = searchLogs(db, {
|
|
28454
|
-
|
|
28455
|
-
level: args.level ? args.level.split(",") : undefined
|
|
28456
|
-
});
|
|
28457
|
-
return { content: [{ type: "text", text: JSON.stringify(rows) }] };
|
|
28605
|
+
const rows = searchLogs(db, { ...args, level: args.level ? args.level.split(",") : undefined });
|
|
28606
|
+
return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, args.brief !== false)) }] };
|
|
28458
28607
|
});
|
|
28459
28608
|
server.tool("log_tail", {
|
|
28460
28609
|
project_id: exports_external.string().optional(),
|
|
28461
|
-
n: exports_external.number().optional()
|
|
28462
|
-
|
|
28610
|
+
n: exports_external.number().optional(),
|
|
28611
|
+
brief: exports_external.boolean().optional()
|
|
28612
|
+
}, ({ project_id, n, brief }) => {
|
|
28463
28613
|
const rows = tailLogs(db, project_id, n ?? 50);
|
|
28464
|
-
return { content: [{ type: "text", text: JSON.stringify(rows) }] };
|
|
28614
|
+
return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, brief !== false)) }] };
|
|
28465
28615
|
});
|
|
28466
28616
|
server.tool("log_summary", {
|
|
28467
28617
|
project_id: exports_external.string().optional(),
|
|
28468
28618
|
since: exports_external.string().optional()
|
|
28469
|
-
}, ({ project_id, since }) => {
|
|
28470
|
-
|
|
28471
|
-
|
|
28472
|
-
|
|
28473
|
-
|
|
28619
|
+
}, ({ project_id, since }) => ({
|
|
28620
|
+
content: [{ type: "text", text: JSON.stringify(summarizeLogs(db, project_id, since)) }]
|
|
28621
|
+
}));
|
|
28622
|
+
server.tool("log_context", {
|
|
28623
|
+
trace_id: exports_external.string(),
|
|
28624
|
+
brief: exports_external.boolean().optional()
|
|
28625
|
+
}, ({ trace_id, brief }) => {
|
|
28474
28626
|
const rows = getLogContext(db, trace_id);
|
|
28475
|
-
return { content: [{ type: "text", text: JSON.stringify(rows) }] };
|
|
28627
|
+
return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, brief !== false)) }] };
|
|
28476
28628
|
});
|
|
28629
|
+
server.tool("log_diagnose", {
|
|
28630
|
+
project_id: exports_external.string(),
|
|
28631
|
+
since: exports_external.string().optional()
|
|
28632
|
+
}, ({ project_id, since }) => ({
|
|
28633
|
+
content: [{ type: "text", text: JSON.stringify(diagnose(db, project_id, since)) }]
|
|
28634
|
+
}));
|
|
28635
|
+
server.tool("log_compare", {
|
|
28636
|
+
project_id: exports_external.string(),
|
|
28637
|
+
a_since: exports_external.string(),
|
|
28638
|
+
a_until: exports_external.string(),
|
|
28639
|
+
b_since: exports_external.string(),
|
|
28640
|
+
b_until: exports_external.string()
|
|
28641
|
+
}, ({ project_id, a_since, a_until, b_since, b_until }) => ({
|
|
28642
|
+
content: [{ type: "text", text: JSON.stringify(compare(db, project_id, a_since, a_until, b_since, b_until)) }]
|
|
28643
|
+
}));
|
|
28477
28644
|
server.tool("perf_snapshot", {
|
|
28478
28645
|
project_id: exports_external.string(),
|
|
28479
28646
|
page_id: exports_external.string().optional()
|
|
28480
28647
|
}, ({ project_id, page_id }) => {
|
|
28481
28648
|
const snap = getLatestSnapshot(db, project_id, page_id);
|
|
28482
|
-
|
|
28483
|
-
return { content: [{ type: "text", text: JSON.stringify({ ...snap, label }) }] };
|
|
28649
|
+
return { content: [{ type: "text", text: JSON.stringify(snap ? { ...snap, label: scoreLabel(snap.score) } : null) }] };
|
|
28484
28650
|
});
|
|
28485
28651
|
server.tool("perf_trend", {
|
|
28486
28652
|
project_id: exports_external.string(),
|
|
28487
28653
|
page_id: exports_external.string().optional(),
|
|
28488
28654
|
since: exports_external.string().optional(),
|
|
28489
28655
|
limit: exports_external.number().optional()
|
|
28490
|
-
}, ({ project_id, page_id, since, limit }) => {
|
|
28491
|
-
|
|
28492
|
-
|
|
28493
|
-
});
|
|
28656
|
+
}, ({ project_id, page_id, since, limit }) => ({
|
|
28657
|
+
content: [{ type: "text", text: JSON.stringify(getPerfTrend(db, project_id, page_id, since, limit ?? 50)) }]
|
|
28658
|
+
}));
|
|
28494
28659
|
server.tool("scan_status", {
|
|
28495
28660
|
project_id: exports_external.string().optional()
|
|
28496
|
-
}, ({ project_id }) => {
|
|
28497
|
-
|
|
28498
|
-
|
|
28499
|
-
})
|
|
28500
|
-
|
|
28501
|
-
|
|
28502
|
-
})
|
|
28503
|
-
|
|
28504
|
-
|
|
28505
|
-
|
|
28661
|
+
}, ({ project_id }) => ({
|
|
28662
|
+
content: [{ type: "text", text: JSON.stringify(listJobs(db, project_id)) }]
|
|
28663
|
+
}));
|
|
28664
|
+
server.tool("list_projects", {}, () => ({
|
|
28665
|
+
content: [{ type: "text", text: JSON.stringify(listProjects(db)) }]
|
|
28666
|
+
}));
|
|
28667
|
+
server.tool("list_pages", { project_id: exports_external.string() }, ({ project_id }) => ({
|
|
28668
|
+
content: [{ type: "text", text: JSON.stringify(listPages(db, project_id)) }]
|
|
28669
|
+
}));
|
|
28670
|
+
server.tool("list_issues", {
|
|
28671
|
+
project_id: exports_external.string().optional(),
|
|
28672
|
+
status: exports_external.string().optional(),
|
|
28673
|
+
limit: exports_external.number().optional()
|
|
28674
|
+
}, ({ project_id, status, limit }) => ({
|
|
28675
|
+
content: [{ type: "text", text: JSON.stringify(listIssues(db, project_id, status, limit ?? 50)) }]
|
|
28676
|
+
}));
|
|
28677
|
+
server.tool("resolve_issue", {
|
|
28678
|
+
id: exports_external.string(),
|
|
28679
|
+
status: exports_external.enum(["open", "resolved", "ignored"])
|
|
28680
|
+
}, ({ id, status }) => ({
|
|
28681
|
+
content: [{ type: "text", text: JSON.stringify(updateIssueStatus(db, id, status)) }]
|
|
28682
|
+
}));
|
|
28683
|
+
server.tool("create_alert_rule", {
|
|
28684
|
+
project_id: exports_external.string(),
|
|
28685
|
+
name: exports_external.string(),
|
|
28686
|
+
level: exports_external.string().optional(),
|
|
28687
|
+
service: exports_external.string().optional(),
|
|
28688
|
+
threshold_count: exports_external.number().optional(),
|
|
28689
|
+
window_seconds: exports_external.number().optional(),
|
|
28690
|
+
action: exports_external.enum(["webhook", "log"]).optional(),
|
|
28691
|
+
webhook_url: exports_external.string().optional()
|
|
28692
|
+
}, (args) => ({ content: [{ type: "text", text: JSON.stringify(createAlertRule(db, args)) }] }));
|
|
28693
|
+
server.tool("list_alert_rules", {
|
|
28694
|
+
project_id: exports_external.string().optional()
|
|
28695
|
+
}, ({ project_id }) => ({
|
|
28696
|
+
content: [{ type: "text", text: JSON.stringify(listAlertRules(db, project_id)) }]
|
|
28697
|
+
}));
|
|
28698
|
+
server.tool("delete_alert_rule", { id: exports_external.string() }, ({ id }) => {
|
|
28699
|
+
deleteAlertRule(db, id);
|
|
28700
|
+
return { content: [{ type: "text", text: "deleted" }] };
|
|
28701
|
+
});
|
|
28702
|
+
server.tool("log_session_context", {
|
|
28703
|
+
session_id: exports_external.string(),
|
|
28704
|
+
brief: exports_external.boolean().optional()
|
|
28705
|
+
}, async ({ session_id, brief }) => {
|
|
28706
|
+
const ctx = await getSessionContext(db, session_id);
|
|
28707
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...ctx, logs: applyBrief(ctx.logs, brief !== false) }) }] };
|
|
28708
|
+
});
|
|
28709
|
+
server.tool("get_health", {}, () => ({
|
|
28710
|
+
content: [{ type: "text", text: JSON.stringify(getHealth(db)) }]
|
|
28711
|
+
}));
|
|
28506
28712
|
var transport = new StdioServerTransport;
|
|
28507
28713
|
await server.connect(transport);
|