@hasna/logs 0.3.25 → 0.3.27

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.
Files changed (132) hide show
  1. package/README.md +33 -10
  2. package/dashboard/dist/assets/index-C0wZYq1m.js +53 -0
  3. package/dashboard/dist/assets/index-DGNrK5qb.css +1 -0
  4. package/dashboard/dist/index.html +14 -0
  5. package/dist/cli/index.js +8511 -177
  6. package/dist/count-bmj4r2zb.js +10 -0
  7. package/dist/{diagnose-e0w5rwbc.js → diagnose-3q5cy9ra.js} +2 -2
  8. package/dist/{export-c3eqjste.js → export-cngdb9fh.js} +1 -1
  9. package/dist/{http-zm3ph78w.js → http-r0xc3d2s.js} +79 -8
  10. package/dist/index-931pbyn5.js +141 -0
  11. package/dist/index-b5c72f1p.js +7 -0
  12. package/dist/{index-p1vgwwsz.js → index-bnr19y0h.js} +596 -37
  13. package/dist/{index-7w7v7hnr.js → index-by1pdzbr.js} +14 -5
  14. package/dist/{index-3dr7d80h.js → index-e1930v9b.js} +12 -8
  15. package/dist/{index-eh9bkbpa.js → index-e72k53yq.js} +10 -2
  16. package/dist/{index-edn08m6f.js → index-gcd14q2f.js} +9 -6
  17. package/dist/index-hq6kzaah.js +26 -0
  18. package/dist/index-j34f36wy.js +5672 -0
  19. package/dist/index-p4dbdzx4.js +1849 -0
  20. package/dist/{index-5qznfyah.js → index-q27bgpr1.js} +1086 -1646
  21. package/dist/index-t3x838zw.js +2583 -0
  22. package/dist/{index-ww5ggfv3.js → index-zkb3z95a.js} +12 -9
  23. package/dist/index.js +2982 -22
  24. package/dist/{jobs-ypmmc2ma.js → jobs-hsgyhfvm.js} +2 -1
  25. package/dist/mcp/index.js +1473 -4286
  26. package/dist/{query-7jwj05er.js → query-c5a43zx3.js} +3 -2
  27. package/dist/server/index.js +2944 -417
  28. package/dist/storage.js +50 -0
  29. package/package.json +27 -8
  30. package/biome.json +0 -13
  31. package/bun.lock +0 -376
  32. package/dashboard/README.md +0 -73
  33. package/dashboard/bun.lock +0 -526
  34. package/dashboard/eslint.config.js +0 -23
  35. package/dashboard/index.html +0 -13
  36. package/dashboard/package.json +0 -32
  37. package/dashboard/src/App.css +0 -184
  38. package/dashboard/src/App.tsx +0 -49
  39. package/dashboard/src/api.ts +0 -33
  40. package/dashboard/src/assets/hero.png +0 -0
  41. package/dashboard/src/assets/react.svg +0 -1
  42. package/dashboard/src/assets/vite.svg +0 -1
  43. package/dashboard/src/index.css +0 -111
  44. package/dashboard/src/main.tsx +0 -10
  45. package/dashboard/src/pages/Alerts.tsx +0 -69
  46. package/dashboard/src/pages/Issues.tsx +0 -50
  47. package/dashboard/src/pages/Perf.tsx +0 -75
  48. package/dashboard/src/pages/Projects.tsx +0 -67
  49. package/dashboard/src/pages/Summary.tsx +0 -67
  50. package/dashboard/src/pages/Tail.tsx +0 -65
  51. package/dashboard/tsconfig.app.json +0 -28
  52. package/dashboard/tsconfig.json +0 -7
  53. package/dashboard/tsconfig.node.json +0 -26
  54. package/dashboard/vite.config.ts +0 -14
  55. package/dist/count-x3n7qg3c.js +0 -9
  56. package/dist/index-5cj74qka.js +0 -10803
  57. package/dist/index-997bkzr2.js +0 -15
  58. package/dist/index-kezb178p.js +0 -1241
  59. package/dist/index-pen6t0yc.js +0 -10794
  60. package/sdk/package.json +0 -27
  61. package/sdk/src/index.ts +0 -143
  62. package/sdk/src/types.ts +0 -56
  63. package/src/cli/entrypoints.test.ts +0 -63
  64. package/src/cli/index.ts +0 -471
  65. package/src/db/index.test.ts +0 -33
  66. package/src/db/index.ts +0 -189
  67. package/src/db/migrations/001_alert_rules.ts +0 -21
  68. package/src/db/migrations/002_issues.ts +0 -21
  69. package/src/db/migrations/003_retention.ts +0 -15
  70. package/src/db/migrations/004_page_auth.ts +0 -13
  71. package/src/db/pg-migrations.ts +0 -167
  72. package/src/index.ts +0 -1
  73. package/src/lib/alerts.test.ts +0 -67
  74. package/src/lib/alerts.ts +0 -117
  75. package/src/lib/browser-script.test.ts +0 -35
  76. package/src/lib/browser-script.ts +0 -31
  77. package/src/lib/compare.test.ts +0 -52
  78. package/src/lib/compare.ts +0 -85
  79. package/src/lib/count.test.ts +0 -44
  80. package/src/lib/count.ts +0 -55
  81. package/src/lib/diagnose.test.ts +0 -55
  82. package/src/lib/diagnose.ts +0 -91
  83. package/src/lib/export.test.ts +0 -66
  84. package/src/lib/export.ts +0 -65
  85. package/src/lib/github.ts +0 -38
  86. package/src/lib/health.test.ts +0 -48
  87. package/src/lib/health.ts +0 -51
  88. package/src/lib/ingest.test.ts +0 -57
  89. package/src/lib/ingest.ts +0 -78
  90. package/src/lib/issues.test.ts +0 -79
  91. package/src/lib/issues.ts +0 -70
  92. package/src/lib/jobs.test.ts +0 -69
  93. package/src/lib/jobs.ts +0 -63
  94. package/src/lib/lighthouse.ts +0 -65
  95. package/src/lib/package-meta.test.ts +0 -43
  96. package/src/lib/package-meta.ts +0 -80
  97. package/src/lib/page-auth.test.ts +0 -54
  98. package/src/lib/page-auth.ts +0 -48
  99. package/src/lib/parse-time.test.ts +0 -37
  100. package/src/lib/parse-time.ts +0 -14
  101. package/src/lib/perf.test.ts +0 -45
  102. package/src/lib/perf.ts +0 -46
  103. package/src/lib/projects.test.ts +0 -73
  104. package/src/lib/projects.ts +0 -69
  105. package/src/lib/query.test.ts +0 -104
  106. package/src/lib/query.ts +0 -84
  107. package/src/lib/retention.test.ts +0 -42
  108. package/src/lib/retention.ts +0 -62
  109. package/src/lib/rotate.test.ts +0 -37
  110. package/src/lib/rotate.ts +0 -27
  111. package/src/lib/scanner.ts +0 -131
  112. package/src/lib/scheduler.ts +0 -63
  113. package/src/lib/session-context.ts +0 -28
  114. package/src/lib/summarize.test.ts +0 -38
  115. package/src/lib/summarize.ts +0 -23
  116. package/src/mcp/http.test.ts +0 -92
  117. package/src/mcp/http.ts +0 -135
  118. package/src/mcp/index.test.ts +0 -27
  119. package/src/mcp/index.ts +0 -444
  120. package/src/server/index.ts +0 -61
  121. package/src/server/routes/alerts.ts +0 -32
  122. package/src/server/routes/issues.ts +0 -43
  123. package/src/server/routes/jobs.ts +0 -32
  124. package/src/server/routes/logs.ts +0 -113
  125. package/src/server/routes/perf.ts +0 -23
  126. package/src/server/routes/projects.ts +0 -67
  127. package/src/server/routes/stream.ts +0 -43
  128. package/src/server/server.test.ts +0 -194
  129. package/src/types/index.ts +0 -119
  130. package/tsconfig.json +0 -22
  131. /package/dashboard/{public → dist}/favicon.svg +0 -0
  132. /package/dashboard/{public → dist}/icons.svg +0 -0
package/src/cli/index.ts DELETED
@@ -1,471 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { registerEventsCommands } from "@hasna/events/commander";
3
- import { Command } from "commander"
4
- import { getDb } from "../db/index.ts"
5
- import { ingestLog } from "../lib/ingest.ts"
6
- import { PACKAGE_VERSION } from "../lib/package-meta.ts"
7
- import { searchLogs, tailLogs } from "../lib/query.ts"
8
- import { summarizeLogs } from "../lib/summarize.ts"
9
- import { createJob, listJobs } from "../lib/jobs.ts"
10
- import { createPage, createProject, listPages, listProjects, resolveProjectId } from "../lib/projects.ts"
11
- import { runJob } from "../lib/scheduler.ts"
12
- import type { LogLevel } from "../types/index.ts"
13
-
14
- // ── Color helpers ──────────────────────────────────────────
15
- const C = {
16
- reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
17
- red: "\x1b[31m", yellow: "\x1b[33m", cyan: "\x1b[36m", gray: "\x1b[90m",
18
- bgRed: "\x1b[41m\x1b[97m", magenta: "\x1b[35m",
19
- };
20
- const LEVEL_COLOR: Record<string, string> = {
21
- fatal: C.bgRed, error: C.red, warn: C.yellow, info: "", debug: C.gray,
22
- };
23
- function colorRow(ts: string, level: string, svc: string, msg: string): string {
24
- const lc = LEVEL_COLOR[level.toLowerCase()] ?? "";
25
- const isTTY = process.stdout.isTTY;
26
- if (!isTTY) return `${ts} ${pad(level.toUpperCase(), 5)} ${pad(svc, 12)} ${msg}`;
27
- return `${C.dim}${ts}${C.reset} ${lc}${C.bold}${pad(level.toUpperCase(), 5)}${C.reset} ${C.cyan}${pad(svc, 12)}${C.reset} ${msg}`;
28
- }
29
- function colorLevel(level: string): string {
30
- if (!process.stdout.isTTY) return pad(level.toUpperCase(), 5);
31
- const lc = LEVEL_COLOR[level.toLowerCase()] ?? "";
32
- return `${lc}${C.bold}${pad(level.toUpperCase(), 5)}${C.reset}`;
33
- }
34
-
35
- /** Resolve a project name or ID from CLI --project flag */
36
- function resolveProject(nameOrId: string | undefined): string | undefined {
37
- if (!nameOrId) return undefined;
38
- return resolveProjectId(getDb(), nameOrId) ?? nameOrId;
39
- }
40
-
41
- const program = new Command()
42
- .name("logs")
43
- .description("@hasna/logs — log aggregation and monitoring")
44
- .version(PACKAGE_VERSION)
45
-
46
- // ── logs list ──────────────────────────────────────────────
47
- program.command("list")
48
- .description("Search and list logs")
49
- .option("--project <name|id>", "Filter by project name or ID")
50
- .option("--page <id>", "Filter by page ID")
51
- .option("--level <levels>", "Comma-separated levels (error,warn,info,debug,fatal)")
52
- .option("--service <name>", "Filter by service")
53
- .option("--since <iso>", "Since timestamp or relative (1h, 24h, 7d)")
54
- .option("--until <iso>", "Until timestamp or relative (e.g. logs list --since 2h --until 1h)")
55
- .option("--text <query>", "Full-text search")
56
- .option("--limit <n>", "Max results", "100")
57
- .option("--format <fmt>", "Output format: table|json|compact", "table")
58
- .action((opts) => {
59
- const db = getDb()
60
- const since = parseRelativeTime(opts.since)
61
- const until = parseRelativeTime(opts.until)
62
- const rows = searchLogs(db, {
63
- project_id: resolveProject(opts.project),
64
- page_id: opts.page,
65
- level: opts.level ? (opts.level.split(",") as LogLevel[]) : undefined,
66
- service: opts.service,
67
- since,
68
- until,
69
- text: opts.text,
70
- limit: Number(opts.limit),
71
- })
72
- if (opts.format === "json") { console.log(JSON.stringify(rows, null, 2)); return }
73
- if (opts.format === "compact") {
74
- for (const r of rows) console.log(`${r.timestamp} [${r.level.toUpperCase()}] ${r.service ?? "-"} ${r.message}`)
75
- return
76
- }
77
- for (const r of rows) {
78
- const meta = r.metadata ? ` ${r.metadata}` : ""
79
- console.log(`${colorRow(r.timestamp, r.level, r.service ?? "-", r.message)}${meta}`)
80
- }
81
- console.log(`\n${rows.length} log(s)`)
82
- })
83
-
84
- // ── logs tail ──────────────────────────────────────────────
85
- program.command("tail")
86
- .description("Show most recent logs")
87
- .option("--project <name|id>", "Project name or ID")
88
- .option("--n <count>", "Number of logs", "50")
89
- .action((opts) => {
90
- const rows = tailLogs(getDb(), resolveProject(opts.project), Number(opts.n))
91
- for (const r of rows) console.log(colorRow(r.timestamp, r.level, r.service ?? "-", r.message))
92
- })
93
-
94
- // ── logs summary ──────────────────────────────────────────
95
- program.command("summary")
96
- .description("Error/warn summary by service")
97
- .option("--project <name|id>", "Project name or ID")
98
- .option("--since <time>", "Relative time (1h, 24h, 7d)", "24h")
99
- .option("--until <time>", "Upper bound time")
100
- .action((opts) => {
101
- const summary = summarizeLogs(getDb(), resolveProject(opts.project), parseRelativeTime(opts.since), parseRelativeTime(opts.until))
102
- if (!summary.length) { console.log("No errors/warnings in this window."); return }
103
- for (const s of summary) console.log(`${colorLevel(s.level)} ${C.cyan}${pad(s.service ?? "-", 15)}${C.reset} count=${s.count} latest=${s.latest}`)
104
- })
105
-
106
- // ── logs push ─────────────────────────────────────────────
107
- program.command("push <message>")
108
- .description("Push a log entry")
109
- .option("--level <level>", "Log level", "info")
110
- .option("--service <name>")
111
- .option("--project <name|id>", "Project name or ID")
112
- .option("--trace <id>", "Trace ID")
113
- .action((message, opts) => {
114
- const row = ingestLog(getDb(), { level: opts.level as LogLevel, message, service: opts.service, project_id: resolveProject(opts.project), trace_id: opts.trace })
115
- console.log(`Logged: ${row.id}`)
116
- })
117
-
118
- // ── logs project ──────────────────────────────────────────
119
- const projectCmd = program.command("project").description("Manage projects")
120
-
121
- projectCmd.command("create")
122
- .option("--name <name>", "Project name")
123
- .option("--repo <url>", "GitHub repo")
124
- .option("--url <url>", "Base URL")
125
- .action((opts) => {
126
- if (!opts.name) { console.error("--name is required"); process.exit(1) }
127
- const p = createProject(getDb(), { name: opts.name, github_repo: opts.repo, base_url: opts.url })
128
- console.log(`Created project: ${p.id} — ${p.name}`)
129
- })
130
-
131
- projectCmd.command("list").action(() => {
132
- const projects = listProjects(getDb())
133
- for (const p of projects) console.log(`${p.id} ${p.name} ${p.base_url ?? ""} ${p.github_repo ?? ""}`)
134
- })
135
-
136
- // ── logs page ─────────────────────────────────────────────
137
- const pageCmd = program.command("page").description("Manage pages")
138
-
139
- pageCmd.command("add")
140
- .option("--project <name|id>", "Project name or ID")
141
- .option("--url <url>")
142
- .option("--name <name>")
143
- .action((opts) => {
144
- if (!opts.project || !opts.url) { console.error("--project and --url required"); process.exit(1) }
145
- const p = createPage(getDb(), { project_id: resolveProject(opts.project), url: opts.url, name: opts.name })
146
- console.log(`Page registered: ${p.id} — ${p.url}`)
147
- })
148
-
149
- pageCmd.command("list").option("--project <name|id>", "Project name or ID").action((opts) => {
150
- if (!opts.project) { console.error("--project required"); process.exit(1) }
151
- const pages = listPages(getDb(), resolveProject(opts.project))
152
- for (const p of pages) console.log(`${p.id} ${p.url} last=${p.last_scanned_at ?? "never"}`)
153
- })
154
-
155
- // ── logs job ──────────────────────────────────────────────
156
- const jobCmd = program.command("job").description("Manage scan jobs")
157
-
158
- jobCmd.command("create")
159
- .option("--project <name|id>", "Project name or ID")
160
- .option("--schedule <cron>", "Cron expression", "*/30 * * * *")
161
- .action((opts) => {
162
- if (!opts.project) { console.error("--project required"); process.exit(1) }
163
- const j = createJob(getDb(), { project_id: resolveProject(opts.project), schedule: opts.schedule })
164
- console.log(`Job created: ${j.id} — ${j.schedule}`)
165
- })
166
-
167
- jobCmd.command("list").option("--project <name|id>", "Project name or ID").action((opts) => {
168
- const jobs = listJobs(getDb(), resolveProject(opts.project))
169
- for (const j of jobs) console.log(`${j.id} ${j.schedule} enabled=${j.enabled} last=${j.last_run_at ?? "never"}`)
170
- })
171
-
172
- // ── logs scan ─────────────────────────────────────────────
173
- program.command("scan")
174
- .description("Run an immediate scan for a job")
175
- .option("--job <id>")
176
- .option("--project <name|id>", "Project name or ID")
177
- .action(async (opts) => {
178
- if (!opts.job) { console.error("--job required"); process.exit(1) }
179
- const db = getDb()
180
- const job = (await import("../lib/jobs.ts")).getJob(db, opts.job)
181
- if (!job) { console.error("Job not found"); process.exit(1) }
182
- console.log("Running scan...")
183
- await runJob(db, job.id, job.project_id, job.page_id ?? undefined)
184
- console.log("Scan complete.")
185
- })
186
-
187
- // ── logs diagnose ─────────────────────────────────────────
188
- program.command("diagnose")
189
- .description("Health diagnosis: score, top errors, trends, failing pages")
190
- .option("--project <name|id>", "Project name or ID")
191
- .option("--since <time>", "Time window (1h, 24h, 7d)", "24h")
192
- .option("--include <items>", "Comma-separated: top_errors,error_rate,failing_pages,perf")
193
- .action(async (opts) => {
194
- const { diagnose } = await import("../lib/diagnose.ts")
195
- const projectId = resolveProject(opts.project)
196
- if (!projectId) { console.error("--project required"); process.exit(1) }
197
- const include = opts.include ? opts.include.split(",") : undefined
198
- const result = diagnose(getDb(), projectId, opts.since, include)
199
- const scoreColor = result.health_score >= 80 ? "\x1b[32m" : result.health_score >= 50 ? "\x1b[33m" : "\x1b[31m"
200
- console.log(`\n${C.bold}Health Score:${C.reset} ${scoreColor}${result.health_score}/100${C.reset}`)
201
- if (result.top_errors?.length) {
202
- console.log(`\n${C.bold}Top Errors:${C.reset}`)
203
- for (const e of result.top_errors) {
204
- console.log(` ${C.red}${pad(String(e.count), 5)}x${C.reset} ${C.cyan}${pad(e.service ?? "-", 12)}${C.reset} ${e.message}`)
205
- }
206
- }
207
- if (result.error_rate !== undefined) {
208
- console.log(`\n${C.bold}Error Rate:${C.reset} ${result.error_rate.toFixed(2)}%`)
209
- }
210
- if (result.failing_pages?.length) {
211
- console.log(`\n${C.bold}Failing Pages:${C.reset}`)
212
- for (const p of result.failing_pages) console.log(` ${C.red}✗${C.reset} ${p.url} (${p.error_count} errors)`)
213
- }
214
- if (result.perf_regressions?.length) {
215
- console.log(`\n${C.bold}Perf Regressions:${C.reset}`)
216
- for (const r of result.perf_regressions) console.log(` ${C.yellow}⚠${C.reset} ${r.page_url} p95=${r.p95_ms}ms`)
217
- }
218
- console.log("")
219
- })
220
-
221
- // ── logs watch ────────────────────────────────────────────
222
- program.command("watch")
223
- .description("Stream new logs in real time with color coding")
224
- .option("--project <name|id>", "Filter by project name or ID")
225
- .option("--level <levels>", "Comma-separated levels (debug,info,warn,error,fatal)")
226
- .option("--service <name>", "Filter by service name")
227
- .option("--interval <ms>", "Poll interval in milliseconds (default: 500)", "500")
228
- .option("--since <time>", "Start from this time (default: now)")
229
- .action(async (opts) => {
230
- const db = getDb()
231
- const { searchLogs } = await import("../lib/query.ts")
232
-
233
- // Resolve project name → ID if needed
234
- let projectId = opts.project
235
- if (projectId) {
236
- const proj = db.query("SELECT id FROM projects WHERE id = ? OR name = ?").get(projectId, projectId) as { id: string } | null
237
- if (proj) projectId = proj.id
238
- }
239
-
240
- const COLORS: Record<string, string> = {
241
- debug: "\x1b[90m", info: "\x1b[36m", warn: "\x1b[33m", error: "\x1b[31m", fatal: "\x1b[35m",
242
- }
243
- const RESET = "\x1b[0m"
244
- const BOLD = "\x1b[1m"
245
-
246
- let lastTimestamp = opts.since ? new Date(opts.since).toISOString() : new Date().toISOString()
247
- let errorCount = 0
248
- let warnCount = 0
249
- const pollIntervalMs = Math.max(100, Number(opts.interval) || 500)
250
-
251
- process.stdout.write(`\x1b[2J\x1b[H`) // clear screen
252
- console.log(`${BOLD}@hasna/logs watch${RESET} — Ctrl+C to exit${projectId ? ` [project: ${opts.project}]` : ''}\n`)
253
-
254
- const poll = () => {
255
- const rows = searchLogs(db, {
256
- project_id: projectId,
257
- level: opts.level ? (opts.level.split(",") as LogLevel[]) : undefined,
258
- service: opts.service,
259
- since: lastTimestamp,
260
- limit: 100,
261
- }).reverse()
262
-
263
- for (const row of rows) {
264
- if (row.timestamp <= lastTimestamp) continue
265
- lastTimestamp = row.timestamp
266
- if (row.level === "error" || row.level === "fatal") errorCount++
267
- if (row.level === "warn") warnCount++
268
- const color = COLORS[row.level] ?? ""
269
- const ts = row.timestamp.slice(11, 19)
270
- const svc = (row.service ?? "-").padEnd(12)
271
- const lvl = row.level.toUpperCase().padEnd(5)
272
- console.log(`${color}${ts} ${BOLD}${lvl}${RESET}${color} ${svc} ${row.message}${RESET}`)
273
- }
274
-
275
- // Update terminal title with counts
276
- process.stdout.write(`\x1b]2;logs: ${errorCount}E ${warnCount}W\x07`)
277
- }
278
-
279
- const interval = setInterval(poll, pollIntervalMs)
280
- process.on("SIGINT", () => { clearInterval(interval); console.log(`\n\nErrors: ${errorCount} Warnings: ${warnCount}`); process.exit(0) })
281
- })
282
-
283
- // ── logs count ────────────────────────────────────────────
284
- program.command("count")
285
- .description("Count logs with optional breakdown by level or service")
286
- .option("--project <name|id>", "Project name or ID")
287
- .option("--service <name>", "Filter by service")
288
- .option("--level <level>", "Filter by level")
289
- .option("--since <time>", "Since (1h, 24h, 7d)")
290
- .option("--until <time>", "Until")
291
- .option("--group-by <field>", "Breakdown: level | service")
292
- .action(async (opts) => {
293
- const { countLogs } = await import("../lib/count.ts")
294
- const result = countLogs(getDb(), {
295
- project_id: resolveProject(opts.project),
296
- service: opts.service,
297
- level: opts.level,
298
- since: opts.since,
299
- until: opts.until,
300
- group_by: opts.groupBy as "level" | "service" | undefined,
301
- })
302
- console.log(`Total: ${result.total} ${C.red}Errors: ${result.errors}${C.reset} ${C.yellow}Warns: ${result.warns}${C.reset} Fatals: ${result.fatals}`)
303
- if (result.by_service) {
304
- console.log(`\nBy Service:`)
305
- for (const [svc, cnt] of Object.entries(result.by_service)) {
306
- console.log(` ${C.cyan}${pad(svc, 20)}${C.reset} ${cnt}`)
307
- }
308
- } else if (opts.groupBy === "level") {
309
- console.log(`\nBy Level:`)
310
- for (const [lvl, cnt] of Object.entries(result.by_level)) {
311
- console.log(` ${colorLevel(lvl)} ${cnt}`)
312
- }
313
- }
314
- })
315
-
316
- // ── logs export ───────────────────────────────────────────
317
- program.command("export")
318
- .description("Export logs to JSON or CSV")
319
- .option("--project <name|id>", "Project name or ID")
320
- .option("--since <time>", "Relative time or ISO")
321
- .option("--level <level>")
322
- .option("--service <name>")
323
- .option("--format <fmt>", "json or csv", "json")
324
- .option("--output <file>", "Output file (default: stdout)")
325
- .option("--limit <n>", "Max rows", "100000")
326
- .action(async (opts) => {
327
- const { exportToCsv, exportToJson } = await import("../lib/export.ts")
328
- const { createWriteStream } = await import("node:fs")
329
- const db = getDb()
330
- const options = {
331
- project_id: resolveProject(opts.project),
332
- since: parseRelativeTime(opts.since),
333
- level: opts.level,
334
- service: opts.service,
335
- limit: Number(opts.limit),
336
- }
337
- let count = 0
338
- if (opts.output) {
339
- const stream = createWriteStream(opts.output)
340
- const write = (s: string) => stream.write(s)
341
- count = opts.format === "csv" ? exportToCsv(db, options, write) : exportToJson(db, options, write)
342
- stream.end()
343
- console.error(`Exported ${count} log(s) to ${opts.output}`)
344
- } else {
345
- const write = (s: string) => process.stdout.write(s)
346
- count = opts.format === "csv" ? exportToCsv(db, options, write) : exportToJson(db, options, write)
347
- process.stderr.write(`\nExported ${count} log(s)\n`)
348
- }
349
- })
350
-
351
- // ── logs stats ────────────────────────────────────────────
352
- program.command("stats")
353
- .description("Volume overview: count, DB size, timeline, top services, error rate")
354
- .option("--project <name|id>", "Scope to a project")
355
- .action((opts) => {
356
- const db = getDb()
357
- const projectId = resolveProject(opts.project)
358
- const pFilter = projectId ? `WHERE project_id = '${projectId.replace(/'/g, "''")}'` : ""
359
- const pAnd = projectId ? `AND project_id = '${projectId.replace(/'/g, "''")}'` : ""
360
-
361
- const total = (db.query(`SELECT COUNT(*) as c FROM logs ${pFilter}`).get() as { c: number }).c
362
- const oldest = (db.query(`SELECT MIN(timestamp) as t FROM logs ${pFilter}`).get() as { t: string | null }).t
363
- const newest = (db.query(`SELECT MAX(timestamp) as t FROM logs ${pFilter}`).get() as { t: string | null }).t
364
-
365
- const byLevel = db.query(`SELECT level, COUNT(*) as c FROM logs ${pFilter} GROUP BY level ORDER BY c DESC`)
366
- .all() as { level: string; c: number }[]
367
-
368
- const topServices = db.query(
369
- `SELECT COALESCE(service, '-') as service, COUNT(*) as c FROM logs ${pFilter} GROUP BY service ORDER BY c DESC LIMIT 5`
370
- ).all() as { service: string; c: number }[]
371
-
372
- // Last 7 days histogram
373
- const days = db.query(
374
- `SELECT strftime('%Y-%m-%d', timestamp) as day, COUNT(*) as c FROM logs WHERE timestamp >= datetime('now', '-7 days') ${pAnd} GROUP BY day ORDER BY day`
375
- ).all() as { day: string; c: number }[]
376
-
377
- const errors = byLevel.find(r => r.level === "error")?.c ?? 0
378
- const fatals = byLevel.find(r => r.level === "fatal")?.c ?? 0
379
- const errorRate = total > 0 ? (((errors + fatals) / total) * 100).toFixed(2) : "0.00"
380
-
381
- console.log(`\n${C.bold}Log Volume Stats${C.reset}${projectId ? ` [${opts.project}]` : ""}`)
382
- console.log(` Total: ${total.toLocaleString()}`)
383
- console.log(` Oldest: ${oldest?.slice(0, 19) ?? "-"}`)
384
- console.log(` Newest: ${newest?.slice(0, 19) ?? "-"}`)
385
- console.log(` Error rate: ${errorRate}% (${errors} errors, ${fatals} fatals)`)
386
-
387
- if (byLevel.length) {
388
- console.log(`\n${C.bold}By Level:${C.reset}`)
389
- for (const r of byLevel) console.log(` ${colorLevel(r.level)} ${r.c.toLocaleString()}`)
390
- }
391
-
392
- if (topServices.length) {
393
- console.log(`\n${C.bold}Top Services:${C.reset}`)
394
- for (const r of topServices) console.log(` ${C.cyan}${pad(r.service, 20)}${C.reset} ${r.c.toLocaleString()}`)
395
- }
396
-
397
- if (days.length) {
398
- const maxC = Math.max(...days.map(d => d.c))
399
- console.log(`\n${C.bold}Last 7 Days:${C.reset}`)
400
- for (const d of days) {
401
- const bar = "█".repeat(Math.max(1, Math.round((d.c / maxC) * 20)))
402
- console.log(` ${d.day} ${C.cyan}${bar}${C.reset} ${d.c.toLocaleString()}`)
403
- }
404
- }
405
- console.log("")
406
- })
407
-
408
- // ── logs health ───────────────────────────────────────────
409
- program.command("health")
410
- .description("Show server health and DB stats")
411
- .action(async () => {
412
- const { getHealth } = await import("../lib/health.ts")
413
- const h = getHealth(getDb())
414
- console.log(JSON.stringify(h, null, 2))
415
- })
416
-
417
- // ── logs mcp / logs serve ─────────────────────────────────
418
- program.command("mcp")
419
- .description("Start the MCP server")
420
- .option("--claude", "Install into Claude Code")
421
- .option("--codex", "Install into Codex")
422
- .option("--gemini", "Install into Gemini")
423
- .action(async (opts) => {
424
- if (opts.claude || opts.codex || opts.gemini) {
425
- const { execSync } = await import("node:child_process")
426
- // Resolve the MCP binary path — works from both source and dist
427
- const selfPath = process.argv[1] ?? new URL(import.meta.url).pathname
428
- const mcpBin = selfPath.replace(/cli\/index\.(ts|js)$/, "mcp/index.$1")
429
- const runtime = process.execPath // bun or node
430
-
431
- if (opts.claude) {
432
- const cmd = `claude mcp add --transport stdio --scope user logs -- ${runtime} ${mcpBin}`
433
- console.log(`Running: ${cmd}`)
434
- execSync(cmd, { stdio: "inherit" })
435
- console.log("✓ Installed logs-mcp into Claude Code")
436
- }
437
- if (opts.codex) {
438
- const config = `[mcp_servers.logs]\ncommand = "${runtime}"\nargs = ["${mcpBin}"]`
439
- console.log("Add to ~/.codex/config.toml:\n\n" + config)
440
- }
441
- if (opts.gemini) {
442
- const config = JSON.stringify({ mcpServers: { logs: { command: runtime, args: [mcpBin] } } }, null, 2)
443
- console.log("Add to ~/.gemini/settings.json mcpServers:\n\n" + config)
444
- }
445
- return
446
- }
447
- await import("../mcp/index.ts")
448
- })
449
-
450
- program.command("serve")
451
- .description("Start the REST API server")
452
- .option("--port <n>", "Port", "3460")
453
- .action(async (opts) => {
454
- process.env.LOGS_PORT = opts.port
455
- await import("../server/index.ts")
456
- })
457
-
458
- // ── helpers ───────────────────────────────────────────────
459
- function pad(s: string, n: number) { return s.padEnd(n) }
460
-
461
- function parseRelativeTime(val?: string): string | undefined {
462
- if (!val) return undefined
463
- const m = val.match(/^(\d+)(h|d|m)$/)
464
- if (!m) return val
465
- const [, n, unit] = m
466
- const ms = Number(n) * (unit === "h" ? 3600 : unit === "d" ? 86400 : 60) * 1000
467
- return new Date(Date.now() - ms).toISOString()
468
- }
469
- registerEventsCommands(program, { source: "logs" });
470
-
471
- program.parse()
@@ -1,33 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { createTestDb } from "./index.ts"
3
-
4
- describe("db migrations", () => {
5
- it("creates all tables", () => {
6
- const db = createTestDb()
7
- const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[]
8
- const names = tables.map(t => t.name)
9
- expect(names).toContain("projects")
10
- expect(names).toContain("pages")
11
- expect(names).toContain("logs")
12
- expect(names).toContain("scan_jobs")
13
- expect(names).toContain("scan_runs")
14
- expect(names).toContain("performance_snapshots")
15
- expect(names).toContain("logs_fts")
16
- })
17
-
18
- it("creates indexes", () => {
19
- const db = createTestDb()
20
- const indexes = db.prepare("SELECT name FROM sqlite_master WHERE type='index'").all() as { name: string }[]
21
- const names = indexes.map(i => i.name)
22
- expect(names).toContain("idx_logs_project_level_ts")
23
- expect(names).toContain("idx_logs_trace")
24
- expect(names).toContain("idx_logs_service")
25
- })
26
-
27
- it("is idempotent (migrate twice)", () => {
28
- const db = createTestDb()
29
- expect(() => {
30
- db.run("CREATE TABLE IF NOT EXISTS projects (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, github_repo TEXT, base_url TEXT, description TEXT, github_description TEXT, github_branch TEXT, github_sha TEXT, last_synced_at TEXT, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')))")
31
- }).not.toThrow()
32
- })
33
- })