@hasna/logs 0.3.6 → 0.3.8
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/cli/index.js +67 -4
- package/dist/count-x3n7qg3c.js +9 -0
- package/dist/diagnose-e0w5rwbc.js +9 -0
- package/dist/index-14dvwcf1.js +45 -0
- package/dist/index-1f2ghyhm.js +540 -0
- package/dist/index-7qhh666n.js +1241 -0
- package/dist/index-7w7v7hnr.js +76 -0
- package/dist/index-997bkzr2.js +15 -0
- package/dist/index-edn08m6f.js +51 -0
- package/dist/index-exeq2gs6.js +79 -0
- package/dist/mcp/index.js +14 -78
- package/dist/query-6s5gqkck.js +15 -0
- package/dist/server/index.js +11 -9
- package/package.json +1 -1
- package/src/cli/index.ts +67 -0
- package/src/lib/count.ts +10 -0
- package/src/mcp/index.ts +1 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
parseTime
|
|
4
|
+
} from "./index-997bkzr2.js";
|
|
5
|
+
|
|
6
|
+
// src/lib/diagnose.ts
|
|
7
|
+
function diagnose(db, projectId, since, include) {
|
|
8
|
+
const window = parseTime(since) ?? since ?? new Date(Date.now() - 24 * 3600 * 1000).toISOString();
|
|
9
|
+
const all = !include || include.length === 0;
|
|
10
|
+
const want = (k) => all || include.includes(k);
|
|
11
|
+
const top_errors = want("top_errors") ? db.prepare(`
|
|
12
|
+
SELECT message, COUNT(*) as count, service, MAX(timestamp) as last_seen
|
|
13
|
+
FROM logs
|
|
14
|
+
WHERE project_id = $p AND level IN ('error','fatal') AND timestamp >= $since
|
|
15
|
+
GROUP BY message, service
|
|
16
|
+
ORDER BY count DESC
|
|
17
|
+
LIMIT 10
|
|
18
|
+
`).all({ $p: projectId, $since: window }) : [];
|
|
19
|
+
const error_rate_by_service = want("error_rate") ? db.prepare(`
|
|
20
|
+
SELECT service,
|
|
21
|
+
SUM(CASE WHEN level IN ('error','fatal') THEN 1 ELSE 0 END) as errors,
|
|
22
|
+
SUM(CASE WHEN level = 'warn' THEN 1 ELSE 0 END) as warns,
|
|
23
|
+
COUNT(*) as total
|
|
24
|
+
FROM logs
|
|
25
|
+
WHERE project_id = $p AND timestamp >= $since
|
|
26
|
+
GROUP BY service
|
|
27
|
+
ORDER BY errors DESC
|
|
28
|
+
`).all({ $p: projectId, $since: window }) : [];
|
|
29
|
+
const failing_pages = want("failing_pages") ? db.prepare(`
|
|
30
|
+
SELECT l.page_id, p.url, COUNT(*) as error_count
|
|
31
|
+
FROM logs l
|
|
32
|
+
JOIN pages p ON p.id = l.page_id
|
|
33
|
+
WHERE l.project_id = $p AND l.level IN ('error','fatal') AND l.timestamp >= $since AND l.page_id IS NOT NULL
|
|
34
|
+
GROUP BY l.page_id, p.url
|
|
35
|
+
ORDER BY error_count DESC
|
|
36
|
+
LIMIT 10
|
|
37
|
+
`).all({ $p: projectId, $since: window }) : [];
|
|
38
|
+
const perf_regressions = want("perf") ? db.prepare(`
|
|
39
|
+
SELECT * FROM (
|
|
40
|
+
SELECT
|
|
41
|
+
cur.page_id,
|
|
42
|
+
p.url,
|
|
43
|
+
cur.score as score_now,
|
|
44
|
+
prev.score as score_prev,
|
|
45
|
+
(cur.score - prev.score) as delta
|
|
46
|
+
FROM performance_snapshots cur
|
|
47
|
+
JOIN pages p ON p.id = cur.page_id
|
|
48
|
+
LEFT JOIN performance_snapshots prev ON prev.page_id = cur.page_id AND prev.id != cur.id
|
|
49
|
+
WHERE cur.project_id = $p
|
|
50
|
+
AND cur.timestamp = (SELECT MAX(timestamp) FROM performance_snapshots WHERE page_id = cur.page_id)
|
|
51
|
+
AND (prev.timestamp = (SELECT MAX(timestamp) FROM performance_snapshots WHERE page_id = cur.page_id AND id != cur.id) OR prev.id IS NULL)
|
|
52
|
+
) WHERE delta < -5 OR delta IS NULL
|
|
53
|
+
ORDER BY delta ASC
|
|
54
|
+
LIMIT 10
|
|
55
|
+
`).all({ $p: projectId }) : [];
|
|
56
|
+
const totalErrors = top_errors.reduce((s, e) => s + e.count, 0);
|
|
57
|
+
const totalWarns = error_rate_by_service.reduce((s, r) => s + r.warns, 0);
|
|
58
|
+
const topService = error_rate_by_service[0];
|
|
59
|
+
const score = totalErrors === 0 ? "green" : totalErrors <= 10 ? "yellow" : "red";
|
|
60
|
+
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).`;
|
|
61
|
+
return {
|
|
62
|
+
project_id: projectId,
|
|
63
|
+
window,
|
|
64
|
+
score,
|
|
65
|
+
error_count: totalErrors,
|
|
66
|
+
warn_count: totalWarns,
|
|
67
|
+
has_perf_regression: perf_regressions.length > 0,
|
|
68
|
+
top_errors,
|
|
69
|
+
error_rate_by_service,
|
|
70
|
+
failing_pages,
|
|
71
|
+
perf_regressions,
|
|
72
|
+
summary
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { diagnose };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/lib/parse-time.ts
|
|
3
|
+
function parseTime(val) {
|
|
4
|
+
if (!val)
|
|
5
|
+
return;
|
|
6
|
+
const m = val.match(/^(\d+(?:\.\d+)?)(m|h|d|w)$/);
|
|
7
|
+
if (!m)
|
|
8
|
+
return val;
|
|
9
|
+
const n = parseFloat(m[1]);
|
|
10
|
+
const unit = m[2];
|
|
11
|
+
const ms = n * { m: 60, h: 3600, d: 86400, w: 604800 }[unit] * 1000;
|
|
12
|
+
return new Date(Date.now() - ms).toISOString();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { parseTime };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
parseTime
|
|
4
|
+
} from "./index-997bkzr2.js";
|
|
5
|
+
|
|
6
|
+
// src/lib/count.ts
|
|
7
|
+
function countLogs(db, opts) {
|
|
8
|
+
const conditions = [];
|
|
9
|
+
const params = {};
|
|
10
|
+
if (opts.project_id) {
|
|
11
|
+
conditions.push("project_id = $p");
|
|
12
|
+
params.$p = opts.project_id;
|
|
13
|
+
}
|
|
14
|
+
if (opts.service) {
|
|
15
|
+
conditions.push("service = $service");
|
|
16
|
+
params.$service = opts.service;
|
|
17
|
+
}
|
|
18
|
+
if (opts.level) {
|
|
19
|
+
conditions.push("level = $level");
|
|
20
|
+
params.$level = opts.level;
|
|
21
|
+
}
|
|
22
|
+
const since = parseTime(opts.since);
|
|
23
|
+
const until = parseTime(opts.until);
|
|
24
|
+
if (since) {
|
|
25
|
+
conditions.push("timestamp >= $since");
|
|
26
|
+
params.$since = since;
|
|
27
|
+
}
|
|
28
|
+
if (until) {
|
|
29
|
+
conditions.push("timestamp <= $until");
|
|
30
|
+
params.$until = until;
|
|
31
|
+
}
|
|
32
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
33
|
+
const byLevel = db.prepare(`SELECT level, COUNT(*) as c FROM logs ${where} GROUP BY level`).all(params);
|
|
34
|
+
const by_level = Object.fromEntries(byLevel.map((r) => [r.level, r.c]));
|
|
35
|
+
const total = byLevel.reduce((s, r) => s + r.c, 0);
|
|
36
|
+
let by_service;
|
|
37
|
+
if (opts.group_by === "service") {
|
|
38
|
+
const bySvc = db.prepare(`SELECT COALESCE(service, '-') as service, COUNT(*) as c FROM logs ${where} GROUP BY service ORDER BY c DESC`).all(params);
|
|
39
|
+
by_service = Object.fromEntries(bySvc.map((r) => [r.service, r.c]));
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
total,
|
|
43
|
+
errors: by_level["error"] ?? 0,
|
|
44
|
+
warns: by_level["warn"] ?? 0,
|
|
45
|
+
fatals: by_level["fatal"] ?? 0,
|
|
46
|
+
by_level,
|
|
47
|
+
by_service
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { countLogs };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
parseTime
|
|
4
|
+
} from "./index-997bkzr2.js";
|
|
5
|
+
|
|
6
|
+
// src/lib/query.ts
|
|
7
|
+
function searchLogs(db, q) {
|
|
8
|
+
const conditions = [];
|
|
9
|
+
const params = {};
|
|
10
|
+
if (q.project_id) {
|
|
11
|
+
conditions.push("l.project_id = $project_id");
|
|
12
|
+
params.$project_id = q.project_id;
|
|
13
|
+
}
|
|
14
|
+
if (q.page_id) {
|
|
15
|
+
conditions.push("l.page_id = $page_id");
|
|
16
|
+
params.$page_id = q.page_id;
|
|
17
|
+
}
|
|
18
|
+
if (q.service) {
|
|
19
|
+
conditions.push("l.service = $service");
|
|
20
|
+
params.$service = q.service;
|
|
21
|
+
}
|
|
22
|
+
if (q.trace_id) {
|
|
23
|
+
conditions.push("l.trace_id = $trace_id");
|
|
24
|
+
params.$trace_id = q.trace_id;
|
|
25
|
+
}
|
|
26
|
+
if (q.since) {
|
|
27
|
+
conditions.push("l.timestamp >= $since");
|
|
28
|
+
params.$since = parseTime(q.since) ?? q.since;
|
|
29
|
+
}
|
|
30
|
+
if (q.until) {
|
|
31
|
+
conditions.push("l.timestamp <= $until");
|
|
32
|
+
params.$until = parseTime(q.until) ?? q.until;
|
|
33
|
+
}
|
|
34
|
+
if (q.level) {
|
|
35
|
+
const levels = Array.isArray(q.level) ? q.level : [q.level];
|
|
36
|
+
const placeholders = levels.map((_, i) => `$level${i}`).join(",");
|
|
37
|
+
levels.forEach((lv, i) => {
|
|
38
|
+
params[`$level${i}`] = lv;
|
|
39
|
+
});
|
|
40
|
+
conditions.push(`l.level IN (${placeholders})`);
|
|
41
|
+
}
|
|
42
|
+
const limit = q.limit ?? 100;
|
|
43
|
+
const offset = q.offset ?? 0;
|
|
44
|
+
params.$limit = limit;
|
|
45
|
+
params.$offset = offset;
|
|
46
|
+
if (q.text) {
|
|
47
|
+
params.$text = q.text;
|
|
48
|
+
const where2 = conditions.length ? `WHERE ${conditions.join(" AND ")} AND` : "WHERE";
|
|
49
|
+
const sql2 = `
|
|
50
|
+
SELECT l.* FROM logs l
|
|
51
|
+
${where2} l.rowid IN (SELECT rowid FROM logs_fts WHERE logs_fts MATCH $text)
|
|
52
|
+
ORDER BY l.timestamp DESC
|
|
53
|
+
LIMIT $limit OFFSET $offset
|
|
54
|
+
`;
|
|
55
|
+
return db.prepare(sql2).all(params);
|
|
56
|
+
}
|
|
57
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
58
|
+
const sql = `SELECT * FROM logs l ${where} ORDER BY l.timestamp DESC LIMIT $limit OFFSET $offset`;
|
|
59
|
+
return db.prepare(sql).all(params);
|
|
60
|
+
}
|
|
61
|
+
function tailLogs(db, projectId, n = 50) {
|
|
62
|
+
if (projectId) {
|
|
63
|
+
return db.prepare("SELECT * FROM logs WHERE project_id = $p ORDER BY timestamp DESC LIMIT $n").all({ $p: projectId, $n: n });
|
|
64
|
+
}
|
|
65
|
+
return db.prepare("SELECT * FROM logs ORDER BY timestamp DESC LIMIT $n").all({ $n: n });
|
|
66
|
+
}
|
|
67
|
+
function getLogContext(db, traceId) {
|
|
68
|
+
return db.prepare("SELECT * FROM logs WHERE trace_id = $t ORDER BY timestamp ASC").all({ $t: traceId });
|
|
69
|
+
}
|
|
70
|
+
function getLogContextFromId(db, logId) {
|
|
71
|
+
const log = db.prepare("SELECT * FROM logs WHERE id = $id").get({ $id: logId });
|
|
72
|
+
if (!log)
|
|
73
|
+
return [];
|
|
74
|
+
if (log.trace_id)
|
|
75
|
+
return getLogContext(db, log.trace_id);
|
|
76
|
+
return [log];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { searchLogs, tailLogs, getLogContext, getLogContextFromId };
|
package/dist/mcp/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
} from "../index-
|
|
4
|
+
getHealth
|
|
5
|
+
} from "../index-xjn8gam3.js";
|
|
6
6
|
import {
|
|
7
7
|
createAlertRule,
|
|
8
8
|
createPage,
|
|
@@ -21,21 +21,26 @@ import {
|
|
|
21
21
|
scoreLabel,
|
|
22
22
|
summarizeLogs,
|
|
23
23
|
updateIssueStatus
|
|
24
|
-
} from "../index-
|
|
24
|
+
} from "../index-1f2ghyhm.js";
|
|
25
25
|
import {
|
|
26
26
|
createJob,
|
|
27
27
|
listJobs
|
|
28
28
|
} from "../index-3dr7d80h.js";
|
|
29
|
+
import {
|
|
30
|
+
diagnose
|
|
31
|
+
} from "../index-7w7v7hnr.js";
|
|
29
32
|
import {
|
|
30
33
|
getLogContext,
|
|
31
34
|
getLogContextFromId,
|
|
32
|
-
parseTime,
|
|
33
35
|
searchLogs,
|
|
34
36
|
tailLogs
|
|
35
|
-
} from "../index-
|
|
37
|
+
} from "../index-exeq2gs6.js";
|
|
36
38
|
import {
|
|
37
|
-
|
|
38
|
-
} from "../index-
|
|
39
|
+
countLogs
|
|
40
|
+
} from "../index-edn08m6f.js";
|
|
41
|
+
import {
|
|
42
|
+
parseTime
|
|
43
|
+
} from "../index-997bkzr2.js";
|
|
39
44
|
import {
|
|
40
45
|
__commonJS,
|
|
41
46
|
__export,
|
|
@@ -28387,76 +28392,6 @@ class StdioServerTransport {
|
|
|
28387
28392
|
}
|
|
28388
28393
|
}
|
|
28389
28394
|
|
|
28390
|
-
// src/lib/diagnose.ts
|
|
28391
|
-
function diagnose(db, projectId, since, include) {
|
|
28392
|
-
const window = parseTime(since) ?? since ?? new Date(Date.now() - 24 * 3600 * 1000).toISOString();
|
|
28393
|
-
const all = !include || include.length === 0;
|
|
28394
|
-
const want = (k) => all || include.includes(k);
|
|
28395
|
-
const top_errors = want("top_errors") ? db.prepare(`
|
|
28396
|
-
SELECT message, COUNT(*) as count, service, MAX(timestamp) as last_seen
|
|
28397
|
-
FROM logs
|
|
28398
|
-
WHERE project_id = $p AND level IN ('error','fatal') AND timestamp >= $since
|
|
28399
|
-
GROUP BY message, service
|
|
28400
|
-
ORDER BY count DESC
|
|
28401
|
-
LIMIT 10
|
|
28402
|
-
`).all({ $p: projectId, $since: window }) : [];
|
|
28403
|
-
const error_rate_by_service = want("error_rate") ? db.prepare(`
|
|
28404
|
-
SELECT service,
|
|
28405
|
-
SUM(CASE WHEN level IN ('error','fatal') THEN 1 ELSE 0 END) as errors,
|
|
28406
|
-
SUM(CASE WHEN level = 'warn' THEN 1 ELSE 0 END) as warns,
|
|
28407
|
-
COUNT(*) as total
|
|
28408
|
-
FROM logs
|
|
28409
|
-
WHERE project_id = $p AND timestamp >= $since
|
|
28410
|
-
GROUP BY service
|
|
28411
|
-
ORDER BY errors DESC
|
|
28412
|
-
`).all({ $p: projectId, $since: window }) : [];
|
|
28413
|
-
const failing_pages = want("failing_pages") ? db.prepare(`
|
|
28414
|
-
SELECT l.page_id, p.url, COUNT(*) as error_count
|
|
28415
|
-
FROM logs l
|
|
28416
|
-
JOIN pages p ON p.id = l.page_id
|
|
28417
|
-
WHERE l.project_id = $p AND l.level IN ('error','fatal') AND l.timestamp >= $since AND l.page_id IS NOT NULL
|
|
28418
|
-
GROUP BY l.page_id, p.url
|
|
28419
|
-
ORDER BY error_count DESC
|
|
28420
|
-
LIMIT 10
|
|
28421
|
-
`).all({ $p: projectId, $since: window }) : [];
|
|
28422
|
-
const perf_regressions = want("perf") ? db.prepare(`
|
|
28423
|
-
SELECT * FROM (
|
|
28424
|
-
SELECT
|
|
28425
|
-
cur.page_id,
|
|
28426
|
-
p.url,
|
|
28427
|
-
cur.score as score_now,
|
|
28428
|
-
prev.score as score_prev,
|
|
28429
|
-
(cur.score - prev.score) as delta
|
|
28430
|
-
FROM performance_snapshots cur
|
|
28431
|
-
JOIN pages p ON p.id = cur.page_id
|
|
28432
|
-
LEFT JOIN performance_snapshots prev ON prev.page_id = cur.page_id AND prev.id != cur.id
|
|
28433
|
-
WHERE cur.project_id = $p
|
|
28434
|
-
AND cur.timestamp = (SELECT MAX(timestamp) FROM performance_snapshots WHERE page_id = cur.page_id)
|
|
28435
|
-
AND (prev.timestamp = (SELECT MAX(timestamp) FROM performance_snapshots WHERE page_id = cur.page_id AND id != cur.id) OR prev.id IS NULL)
|
|
28436
|
-
) WHERE delta < -5 OR delta IS NULL
|
|
28437
|
-
ORDER BY delta ASC
|
|
28438
|
-
LIMIT 10
|
|
28439
|
-
`).all({ $p: projectId }) : [];
|
|
28440
|
-
const totalErrors = top_errors.reduce((s, e) => s + e.count, 0);
|
|
28441
|
-
const totalWarns = error_rate_by_service.reduce((s, r) => s + r.warns, 0);
|
|
28442
|
-
const topService = error_rate_by_service[0];
|
|
28443
|
-
const score = totalErrors === 0 ? "green" : totalErrors <= 10 ? "yellow" : "red";
|
|
28444
|
-
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).`;
|
|
28445
|
-
return {
|
|
28446
|
-
project_id: projectId,
|
|
28447
|
-
window,
|
|
28448
|
-
score,
|
|
28449
|
-
error_count: totalErrors,
|
|
28450
|
-
warn_count: totalWarns,
|
|
28451
|
-
has_perf_regression: perf_regressions.length > 0,
|
|
28452
|
-
top_errors,
|
|
28453
|
-
error_rate_by_service,
|
|
28454
|
-
failing_pages,
|
|
28455
|
-
perf_regressions,
|
|
28456
|
-
summary
|
|
28457
|
-
};
|
|
28458
|
-
}
|
|
28459
|
-
|
|
28460
28395
|
// src/lib/compare.ts
|
|
28461
28396
|
function getErrorsByMessage(db, projectId, since, until) {
|
|
28462
28397
|
return db.prepare(`
|
|
@@ -28683,7 +28618,8 @@ server.tool("log_count", {
|
|
|
28683
28618
|
service: exports_external.string().optional(),
|
|
28684
28619
|
level: exports_external.string().optional(),
|
|
28685
28620
|
since: exports_external.string().optional(),
|
|
28686
|
-
until: exports_external.string().optional()
|
|
28621
|
+
until: exports_external.string().optional(),
|
|
28622
|
+
group_by: exports_external.enum(["level", "service"]).optional().describe("Return breakdown by 'level' or 'service' in addition to totals")
|
|
28687
28623
|
}, (args) => ({
|
|
28688
28624
|
content: [{ type: "text", text: JSON.stringify(countLogs(db, { ...args, project_id: rp(args.project_id) })) }]
|
|
28689
28625
|
}));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
getLogContext,
|
|
4
|
+
getLogContextFromId,
|
|
5
|
+
searchLogs,
|
|
6
|
+
tailLogs
|
|
7
|
+
} from "./index-exeq2gs6.js";
|
|
8
|
+
import"./index-997bkzr2.js";
|
|
9
|
+
import"./index-re3ntm60.js";
|
|
10
|
+
export {
|
|
11
|
+
tailLogs,
|
|
12
|
+
searchLogs,
|
|
13
|
+
getLogContextFromId,
|
|
14
|
+
getLogContext
|
|
15
|
+
};
|
package/dist/server/index.js
CHANGED
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
setPageAuth,
|
|
7
7
|
setRetentionPolicy,
|
|
8
8
|
startScheduler
|
|
9
|
-
} from "../index-
|
|
9
|
+
} from "../index-7qhh666n.js";
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
} from "../index-
|
|
11
|
+
getHealth
|
|
12
|
+
} from "../index-xjn8gam3.js";
|
|
13
13
|
import {
|
|
14
14
|
createAlertRule,
|
|
15
15
|
createPage,
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
updateAlertRule,
|
|
32
32
|
updateIssueStatus,
|
|
33
33
|
updateProject
|
|
34
|
-
} from "../index-
|
|
34
|
+
} from "../index-1f2ghyhm.js";
|
|
35
35
|
import {
|
|
36
36
|
createJob,
|
|
37
37
|
deleteJob,
|
|
@@ -40,17 +40,19 @@ import {
|
|
|
40
40
|
} from "../index-3dr7d80h.js";
|
|
41
41
|
import {
|
|
42
42
|
getLogContext,
|
|
43
|
-
parseTime,
|
|
44
43
|
searchLogs,
|
|
45
44
|
tailLogs
|
|
46
|
-
} from "../index-
|
|
45
|
+
} from "../index-exeq2gs6.js";
|
|
46
|
+
import {
|
|
47
|
+
countLogs
|
|
48
|
+
} from "../index-edn08m6f.js";
|
|
49
|
+
import {
|
|
50
|
+
parseTime
|
|
51
|
+
} from "../index-997bkzr2.js";
|
|
47
52
|
import {
|
|
48
53
|
exportToCsv,
|
|
49
54
|
exportToJson
|
|
50
55
|
} from "../index-eh9bkbpa.js";
|
|
51
|
-
import {
|
|
52
|
-
getHealth
|
|
53
|
-
} from "../index-xjn8gam3.js";
|
|
54
56
|
import"../index-re3ntm60.js";
|
|
55
57
|
|
|
56
58
|
// node_modules/hono/dist/compose.js
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -182,6 +182,40 @@ program.command("scan")
|
|
|
182
182
|
console.log("Scan complete.")
|
|
183
183
|
})
|
|
184
184
|
|
|
185
|
+
// ── logs diagnose ─────────────────────────────────────────
|
|
186
|
+
program.command("diagnose")
|
|
187
|
+
.description("Health diagnosis: score, top errors, trends, failing pages")
|
|
188
|
+
.option("--project <name|id>", "Project name or ID")
|
|
189
|
+
.option("--since <time>", "Time window (1h, 24h, 7d)", "24h")
|
|
190
|
+
.option("--include <items>", "Comma-separated: top_errors,error_rate,failing_pages,perf")
|
|
191
|
+
.action(async (opts) => {
|
|
192
|
+
const { diagnose } = await import("../lib/diagnose.ts")
|
|
193
|
+
const projectId = resolveProject(opts.project)
|
|
194
|
+
if (!projectId) { console.error("--project required"); process.exit(1) }
|
|
195
|
+
const include = opts.include ? opts.include.split(",") : undefined
|
|
196
|
+
const result = diagnose(getDb(), projectId, opts.since, include)
|
|
197
|
+
const scoreColor = result.health_score >= 80 ? "\x1b[32m" : result.health_score >= 50 ? "\x1b[33m" : "\x1b[31m"
|
|
198
|
+
console.log(`\n${C.bold}Health Score:${C.reset} ${scoreColor}${result.health_score}/100${C.reset}`)
|
|
199
|
+
if (result.top_errors?.length) {
|
|
200
|
+
console.log(`\n${C.bold}Top Errors:${C.reset}`)
|
|
201
|
+
for (const e of result.top_errors) {
|
|
202
|
+
console.log(` ${C.red}${pad(String(e.count), 5)}x${C.reset} ${C.cyan}${pad(e.service ?? "-", 12)}${C.reset} ${e.message}`)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (result.error_rate !== undefined) {
|
|
206
|
+
console.log(`\n${C.bold}Error Rate:${C.reset} ${result.error_rate.toFixed(2)}%`)
|
|
207
|
+
}
|
|
208
|
+
if (result.failing_pages?.length) {
|
|
209
|
+
console.log(`\n${C.bold}Failing Pages:${C.reset}`)
|
|
210
|
+
for (const p of result.failing_pages) console.log(` ${C.red}✗${C.reset} ${p.url} (${p.error_count} errors)`)
|
|
211
|
+
}
|
|
212
|
+
if (result.perf_regressions?.length) {
|
|
213
|
+
console.log(`\n${C.bold}Perf Regressions:${C.reset}`)
|
|
214
|
+
for (const r of result.perf_regressions) console.log(` ${C.yellow}⚠${C.reset} ${r.page_url} p95=${r.p95_ms}ms`)
|
|
215
|
+
}
|
|
216
|
+
console.log("")
|
|
217
|
+
})
|
|
218
|
+
|
|
185
219
|
// ── logs watch ────────────────────────────────────────────
|
|
186
220
|
program.command("watch")
|
|
187
221
|
.description("Stream new logs in real time with color coding")
|
|
@@ -244,6 +278,39 @@ program.command("watch")
|
|
|
244
278
|
process.on("SIGINT", () => { clearInterval(interval); console.log(`\n\nErrors: ${errorCount} Warnings: ${warnCount}`); process.exit(0) })
|
|
245
279
|
})
|
|
246
280
|
|
|
281
|
+
// ── logs count ────────────────────────────────────────────
|
|
282
|
+
program.command("count")
|
|
283
|
+
.description("Count logs with optional breakdown by level or service")
|
|
284
|
+
.option("--project <name|id>", "Project name or ID")
|
|
285
|
+
.option("--service <name>", "Filter by service")
|
|
286
|
+
.option("--level <level>", "Filter by level")
|
|
287
|
+
.option("--since <time>", "Since (1h, 24h, 7d)")
|
|
288
|
+
.option("--until <time>", "Until")
|
|
289
|
+
.option("--group-by <field>", "Breakdown: level | service")
|
|
290
|
+
.action(async (opts) => {
|
|
291
|
+
const { countLogs } = await import("../lib/count.ts")
|
|
292
|
+
const result = countLogs(getDb(), {
|
|
293
|
+
project_id: resolveProject(opts.project),
|
|
294
|
+
service: opts.service,
|
|
295
|
+
level: opts.level,
|
|
296
|
+
since: opts.since,
|
|
297
|
+
until: opts.until,
|
|
298
|
+
group_by: opts.groupBy as "level" | "service" | undefined,
|
|
299
|
+
})
|
|
300
|
+
console.log(`Total: ${result.total} ${C.red}Errors: ${result.errors}${C.reset} ${C.yellow}Warns: ${result.warns}${C.reset} Fatals: ${result.fatals}`)
|
|
301
|
+
if (result.by_service) {
|
|
302
|
+
console.log(`\nBy Service:`)
|
|
303
|
+
for (const [svc, cnt] of Object.entries(result.by_service)) {
|
|
304
|
+
console.log(` ${C.cyan}${pad(svc, 20)}${C.reset} ${cnt}`)
|
|
305
|
+
}
|
|
306
|
+
} else if (opts.groupBy === "level") {
|
|
307
|
+
console.log(`\nBy Level:`)
|
|
308
|
+
for (const [lvl, cnt] of Object.entries(result.by_level)) {
|
|
309
|
+
console.log(` ${colorLevel(lvl)} ${cnt}`)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
|
|
247
314
|
// ── logs export ───────────────────────────────────────────
|
|
248
315
|
program.command("export")
|
|
249
316
|
.description("Export logs to JSON or CSV")
|
package/src/lib/count.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface LogCount {
|
|
|
7
7
|
warns: number
|
|
8
8
|
fatals: number
|
|
9
9
|
by_level: Record<string, number>
|
|
10
|
+
by_service?: Record<string, number>
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function countLogs(db: Database, opts: {
|
|
@@ -15,6 +16,7 @@ export function countLogs(db: Database, opts: {
|
|
|
15
16
|
level?: string
|
|
16
17
|
since?: string
|
|
17
18
|
until?: string
|
|
19
|
+
group_by?: "level" | "service"
|
|
18
20
|
}): LogCount {
|
|
19
21
|
const conditions: string[] = []
|
|
20
22
|
const params: Record<string, unknown> = {}
|
|
@@ -35,11 +37,19 @@ export function countLogs(db: Database, opts: {
|
|
|
35
37
|
const by_level = Object.fromEntries(byLevel.map(r => [r.level, r.c]))
|
|
36
38
|
const total = byLevel.reduce((s, r) => s + r.c, 0)
|
|
37
39
|
|
|
40
|
+
let by_service: Record<string, number> | undefined
|
|
41
|
+
if (opts.group_by === "service") {
|
|
42
|
+
const bySvc = db.prepare(`SELECT COALESCE(service, '-') as service, COUNT(*) as c FROM logs ${where} GROUP BY service ORDER BY c DESC`)
|
|
43
|
+
.all(params) as { service: string; c: number }[]
|
|
44
|
+
by_service = Object.fromEntries(bySvc.map(r => [r.service, r.c]))
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
return {
|
|
39
48
|
total,
|
|
40
49
|
errors: by_level["error"] ?? 0,
|
|
41
50
|
warns: by_level["warn"] ?? 0,
|
|
42
51
|
fatals: by_level["fatal"] ?? 0,
|
|
43
52
|
by_level,
|
|
53
|
+
by_service,
|
|
44
54
|
}
|
|
45
55
|
}
|
package/src/mcp/index.ts
CHANGED
|
@@ -154,6 +154,7 @@ server.tool("log_tail", {
|
|
|
154
154
|
server.tool("log_count", {
|
|
155
155
|
project_id: z.string().optional(), service: z.string().optional(),
|
|
156
156
|
level: z.string().optional(), since: z.string().optional(), until: z.string().optional(),
|
|
157
|
+
group_by: z.enum(["level", "service"]).optional().describe("Return breakdown by 'level' or 'service' in addition to totals"),
|
|
157
158
|
}, (args) => ({
|
|
158
159
|
content: [{ type: "text", text: JSON.stringify(countLogs(db, { ...args, project_id: rp(args.project_id) })) }]
|
|
159
160
|
}))
|