@hasna/logs 0.3.6 → 0.3.7

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,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
@@ -2,7 +2,7 @@
2
2
  // @bun
3
3
  import {
4
4
  countLogs
5
- } from "../index-6y8pmes4.js";
5
+ } from "../index-14dvwcf1.js";
6
6
  import {
7
7
  createAlertRule,
8
8
  createPage,
@@ -21,18 +21,23 @@ 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";
38
+ import {
39
+ parseTime
40
+ } from "../index-997bkzr2.js";
36
41
  import {
37
42
  getHealth
38
43
  } from "../index-xjn8gam3.js";
@@ -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(`
@@ -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
11
  countLogs
12
- } from "../index-6y8pmes4.js";
12
+ } from "../index-14dvwcf1.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,10 +40,12 @@ 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
+ parseTime
48
+ } from "../index-997bkzr2.js";
47
49
  import {
48
50
  exportToCsv,
49
51
  exportToJson
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/logs",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
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")