@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.
@@ -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
- countLogs
5
- } from "../index-6y8pmes4.js";
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-86j0hn03.js";
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-rbrsvsyh.js";
37
+ } from "../index-exeq2gs6.js";
36
38
  import {
37
- getHealth
38
- } from "../index-xjn8gam3.js";
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
+ };
@@ -6,10 +6,10 @@ import {
6
6
  setPageAuth,
7
7
  setRetentionPolicy,
8
8
  startScheduler
9
- } from "../index-4hj4sakk.js";
9
+ } from "../index-7qhh666n.js";
10
10
  import {
11
- countLogs
12
- } from "../index-6y8pmes4.js";
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-86j0hn03.js";
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-rbrsvsyh.js";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/logs",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Log aggregation + browser script + headless page scanner + performance monitoring for AI agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
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
  }))