@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
@@ -1,48 +0,0 @@
1
- import type { Database } from "bun:sqlite"
2
- import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto"
3
-
4
- const SECRET_KEY = Buffer.from((process.env.LOGS_SECRET_KEY ?? "open-logs-default-key-32bytesXXX").padEnd(32).slice(0, 32))
5
-
6
- export interface PageAuth {
7
- id: string
8
- page_id: string
9
- type: "cookie" | "bearer" | "basic"
10
- credentials: string
11
- created_at: string
12
- }
13
-
14
- function encrypt(text: string): string {
15
- const iv = randomBytes(16)
16
- const cipher = createCipheriv("aes-256-cbc", SECRET_KEY, iv)
17
- const encrypted = Buffer.concat([cipher.update(text, "utf8"), cipher.final()])
18
- return iv.toString("hex") + ":" + encrypted.toString("hex")
19
- }
20
-
21
- function decrypt(text: string): string {
22
- const [ivHex, encHex] = text.split(":")
23
- if (!ivHex || !encHex) return text
24
- const iv = Buffer.from(ivHex, "hex")
25
- const enc = Buffer.from(encHex, "hex")
26
- const decipher = createDecipheriv("aes-256-cbc", SECRET_KEY, iv)
27
- return Buffer.concat([decipher.update(enc), decipher.final()]).toString("utf8")
28
- }
29
-
30
- export function setPageAuth(db: Database, pageId: string, type: PageAuth["type"], credentials: string): PageAuth {
31
- const encrypted = encrypt(credentials)
32
- return db.prepare(`
33
- INSERT INTO page_auth (page_id, type, credentials)
34
- VALUES ($page_id, $type, $credentials)
35
- ON CONFLICT(page_id) DO UPDATE SET type = excluded.type, credentials = excluded.credentials
36
- RETURNING *
37
- `).get({ $page_id: pageId, $type: type, $credentials: encrypted }) as PageAuth
38
- }
39
-
40
- export function getPageAuth(db: Database, pageId: string): { type: PageAuth["type"]; credentials: string } | null {
41
- const row = db.prepare("SELECT * FROM page_auth WHERE page_id = $id").get({ $id: pageId }) as PageAuth | null
42
- if (!row) return null
43
- return { type: row.type, credentials: decrypt(row.credentials) }
44
- }
45
-
46
- export function deletePageAuth(db: Database, pageId: string): void {
47
- db.run("DELETE FROM page_auth WHERE page_id = $id", { $id: pageId })
48
- }
@@ -1,37 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { parseTime } from "./parse-time.ts"
3
-
4
- describe("parseTime", () => {
5
- it("returns undefined for undefined input", () => expect(parseTime(undefined)).toBeUndefined())
6
- it("returns ISO string unchanged", () => {
7
- const iso = "2026-01-01T00:00:00.000Z"
8
- expect(parseTime(iso)).toBe(iso)
9
- })
10
- it("parses 30m", () => {
11
- const result = parseTime("30m")!
12
- const diff = Date.now() - new Date(result).getTime()
13
- expect(diff).toBeGreaterThan(29 * 60 * 1000)
14
- expect(diff).toBeLessThan(31 * 60 * 1000)
15
- })
16
- it("parses 1h", () => {
17
- const result = parseTime("1h")!
18
- const diff = Date.now() - new Date(result).getTime()
19
- expect(diff).toBeGreaterThan(59 * 60 * 1000)
20
- expect(diff).toBeLessThan(61 * 60 * 1000)
21
- })
22
- it("parses 7d", () => {
23
- const result = parseTime("7d")!
24
- const diff = Date.now() - new Date(result).getTime()
25
- expect(diff).toBeGreaterThan(6.9 * 86400 * 1000)
26
- expect(diff).toBeLessThan(7.1 * 86400 * 1000)
27
- })
28
- it("parses 1w", () => {
29
- const result = parseTime("1w")!
30
- const diff = Date.now() - new Date(result).getTime()
31
- expect(diff).toBeGreaterThan(6.9 * 86400 * 1000)
32
- })
33
- it("returns unknown strings unchanged", () => {
34
- expect(parseTime("yesterday")).toBe("yesterday")
35
- expect(parseTime("now")).toBe("now")
36
- })
37
- })
@@ -1,14 +0,0 @@
1
- /**
2
- * Parses a relative time string or ISO timestamp into an ISO timestamp.
3
- * Accepts: "30m", "1h", "2h", "24h", "7d", "1w" or any ISO string.
4
- * Returns the input unchanged if it doesn't match a relative format.
5
- */
6
- export function parseTime(val: string | undefined): string | undefined {
7
- if (!val) return undefined
8
- const m = val.match(/^(\d+(?:\.\d+)?)(m|h|d|w)$/)
9
- if (!m) return val
10
- const n = parseFloat(m[1]!)
11
- const unit = m[2]!
12
- const ms = n * ({ m: 60, h: 3600, d: 86400, w: 604800 }[unit]!) * 1000
13
- return new Date(Date.now() - ms).toISOString()
14
- }
@@ -1,45 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { createTestDb } from "../db/index.ts"
3
- import { getLatestSnapshot, getPerfTrend, saveSnapshot, scoreLabel } from "./perf.ts"
4
-
5
- function seedProject(db: ReturnType<typeof createTestDb>) {
6
- return db.prepare("INSERT INTO projects (name) VALUES ('app') RETURNING id").get() as { id: string }
7
- }
8
-
9
- describe("perf", () => {
10
- it("saves and retrieves a snapshot", () => {
11
- const db = createTestDb()
12
- const p = seedProject(db)
13
- const snap = saveSnapshot(db, { project_id: p.id, url: "https://app.com", lcp: 1200, fcp: 800, cls: 0.05, tti: 2000, ttfb: 100, score: 91, raw_audit: null, page_id: null })
14
- expect(snap.id).toBeTruthy()
15
- expect(snap.score).toBe(91)
16
- const latest = getLatestSnapshot(db, p.id)
17
- expect(latest?.id).toBe(snap.id)
18
- })
19
-
20
- it("returns null when no snapshot exists", () => {
21
- const db = createTestDb()
22
- const p = seedProject(db)
23
- expect(getLatestSnapshot(db, p.id)).toBeNull()
24
- })
25
-
26
- it("returns trend in desc order", () => {
27
- const db = createTestDb()
28
- const p = seedProject(db)
29
- saveSnapshot(db, { project_id: p.id, url: "https://app.com", lcp: 1000, fcp: 700, cls: 0.03, tti: 1800, ttfb: 90, score: 95, raw_audit: null, page_id: null })
30
- saveSnapshot(db, { project_id: p.id, url: "https://app.com", lcp: 2000, fcp: 1200, cls: 0.1, tti: 3000, ttfb: 200, score: 70, raw_audit: null, page_id: null })
31
- const trend = getPerfTrend(db, p.id)
32
- expect(trend).toHaveLength(2)
33
- expect(trend[0]!.timestamp >= trend[1]!.timestamp).toBe(true)
34
- })
35
- })
36
-
37
- describe("scoreLabel", () => {
38
- it("returns green for >= 90", () => expect(scoreLabel(90)).toBe("green"))
39
- it("returns green for 100", () => expect(scoreLabel(100)).toBe("green"))
40
- it("returns yellow for 50-89", () => expect(scoreLabel(75)).toBe("yellow"))
41
- it("returns yellow for 50", () => expect(scoreLabel(50)).toBe("yellow"))
42
- it("returns red for < 50", () => expect(scoreLabel(49)).toBe("red"))
43
- it("returns red for 0", () => expect(scoreLabel(0)).toBe("red"))
44
- it("returns unknown for null", () => expect(scoreLabel(null)).toBe("unknown"))
45
- })
package/src/lib/perf.ts DELETED
@@ -1,46 +0,0 @@
1
- import type { Database } from "bun:sqlite"
2
- import type { PerformanceSnapshot } from "../types/index.ts"
3
-
4
- export function saveSnapshot(db: Database, data: Omit<PerformanceSnapshot, "id" | "timestamp">): PerformanceSnapshot {
5
- return db.prepare(`
6
- INSERT INTO performance_snapshots (project_id, page_id, url, lcp, fcp, cls, tti, ttfb, score, raw_audit)
7
- VALUES ($project_id, $page_id, $url, $lcp, $fcp, $cls, $tti, $ttfb, $score, $raw_audit)
8
- RETURNING *
9
- `).get({
10
- $project_id: data.project_id,
11
- $page_id: data.page_id ?? null,
12
- $url: data.url,
13
- $lcp: data.lcp ?? null,
14
- $fcp: data.fcp ?? null,
15
- $cls: data.cls ?? null,
16
- $tti: data.tti ?? null,
17
- $ttfb: data.ttfb ?? null,
18
- $score: data.score ?? null,
19
- $raw_audit: data.raw_audit ?? null,
20
- }) as PerformanceSnapshot
21
- }
22
-
23
- export function getLatestSnapshot(db: Database, projectId: string, pageId?: string): PerformanceSnapshot | null {
24
- if (pageId) {
25
- return db.prepare("SELECT * FROM performance_snapshots WHERE project_id = $p AND page_id = $pg ORDER BY timestamp DESC LIMIT 1")
26
- .get({ $p: projectId, $pg: pageId }) as PerformanceSnapshot | null
27
- }
28
- return db.prepare("SELECT * FROM performance_snapshots WHERE project_id = $p ORDER BY timestamp DESC LIMIT 1")
29
- .get({ $p: projectId }) as PerformanceSnapshot | null
30
- }
31
-
32
- export function getPerfTrend(db: Database, projectId: string, pageId?: string, since?: string, limit = 50): PerformanceSnapshot[] {
33
- const conditions = ["project_id = $p"]
34
- const params: Record<string, unknown> = { $p: projectId, $limit: limit }
35
- if (pageId) { conditions.push("page_id = $pg"); params.$pg = pageId }
36
- if (since) { conditions.push("timestamp >= $since"); params.$since = since }
37
- return db.prepare(`SELECT * FROM performance_snapshots WHERE ${conditions.join(" AND ")} ORDER BY timestamp DESC LIMIT $limit`)
38
- .all(params) as PerformanceSnapshot[]
39
- }
40
-
41
- export function scoreLabel(score: number | null): "green" | "yellow" | "red" | "unknown" {
42
- if (score === null) return "unknown"
43
- if (score >= 90) return "green"
44
- if (score >= 50) return "yellow"
45
- return "red"
46
- }
@@ -1,73 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { createTestDb } from "../db/index.ts"
3
- import { createPage, createProject, getPage, getProject, listPages, listProjects, touchPage, updateProject } from "./projects.ts"
4
-
5
- describe("projects", () => {
6
- it("creates a project", () => {
7
- const db = createTestDb()
8
- const p = createProject(db, { name: "my-app", github_repo: "https://github.com/foo/bar", base_url: "https://myapp.com" })
9
- expect(p.id).toBeTruthy()
10
- expect(p.name).toBe("my-app")
11
- expect(p.github_repo).toBe("https://github.com/foo/bar")
12
- })
13
-
14
- it("lists projects", () => {
15
- const db = createTestDb()
16
- createProject(db, { name: "app1" })
17
- createProject(db, { name: "app2" })
18
- expect(listProjects(db)).toHaveLength(2)
19
- })
20
-
21
- it("gets a project by id", () => {
22
- const db = createTestDb()
23
- const p = createProject(db, { name: "test" })
24
- expect(getProject(db, p.id)?.name).toBe("test")
25
- })
26
-
27
- it("returns null for unknown id", () => {
28
- expect(getProject(createTestDb(), "nope")).toBeNull()
29
- })
30
-
31
- it("updates project fields", () => {
32
- const db = createTestDb()
33
- const p = createProject(db, { name: "x" })
34
- const updated = updateProject(db, p.id, { github_sha: "abc123" })
35
- expect(updated?.github_sha).toBe("abc123")
36
- })
37
- })
38
-
39
- describe("pages", () => {
40
- it("creates a page", () => {
41
- const db = createTestDb()
42
- const p = createProject(db, { name: "app" })
43
- const page = createPage(db, { project_id: p.id, url: "https://app.com/dashboard", name: "Dashboard" })
44
- expect(page.id).toBeTruthy()
45
- expect(page.url).toBe("https://app.com/dashboard")
46
- expect(page.name).toBe("Dashboard")
47
- })
48
-
49
- it("upserts on duplicate url", () => {
50
- const db = createTestDb()
51
- const p = createProject(db, { name: "app" })
52
- createPage(db, { project_id: p.id, url: "https://app.com/", name: "Home" })
53
- createPage(db, { project_id: p.id, url: "https://app.com/", name: "Home v2" })
54
- expect(listPages(db, p.id)).toHaveLength(1)
55
- })
56
-
57
- it("lists pages for a project", () => {
58
- const db = createTestDb()
59
- const p = createProject(db, { name: "app" })
60
- createPage(db, { project_id: p.id, url: "https://app.com/a" })
61
- createPage(db, { project_id: p.id, url: "https://app.com/b" })
62
- expect(listPages(db, p.id)).toHaveLength(2)
63
- })
64
-
65
- it("touches last_scanned_at", () => {
66
- const db = createTestDb()
67
- const p = createProject(db, { name: "app" })
68
- const page = createPage(db, { project_id: p.id, url: "https://app.com/" })
69
- expect(page.last_scanned_at).toBeNull()
70
- touchPage(db, page.id)
71
- expect(getPage(db, page.id)?.last_scanned_at).toBeTruthy()
72
- })
73
- })
@@ -1,69 +0,0 @@
1
- import type { Database } from "bun:sqlite"
2
- import type { Page, Project } from "../types/index.ts"
3
-
4
- // Projects
5
- export function createProject(db: Database, data: { name: string; github_repo?: string; base_url?: string; description?: string }): Project {
6
- return db.prepare(`
7
- INSERT INTO projects (name, github_repo, base_url, description)
8
- VALUES ($name, $github_repo, $base_url, $description)
9
- RETURNING *
10
- `).get({
11
- $name: data.name,
12
- $github_repo: data.github_repo ?? null,
13
- $base_url: data.base_url ?? null,
14
- $description: data.description ?? null,
15
- }) as Project
16
- }
17
-
18
- export function listProjects(db: Database): Project[] {
19
- return db.prepare("SELECT * FROM projects ORDER BY created_at DESC").all() as Project[]
20
- }
21
-
22
- export function getProject(db: Database, id: string): Project | null {
23
- return db.prepare("SELECT * FROM projects WHERE id = $id").get({ $id: id }) as Project | null
24
- }
25
-
26
- export function updateProject(db: Database, id: string, data: Partial<Pick<Project, "name" | "github_repo" | "base_url" | "description" | "github_description" | "github_branch" | "github_sha" | "last_synced_at">>): Project | null {
27
- const fields = Object.keys(data).map(k => `${k} = $${k}`).join(", ")
28
- if (!fields) return getProject(db, id)
29
- const params = Object.fromEntries(Object.entries(data).map(([k, v]) => [`$${k}`, v]))
30
- params.$id = id
31
- return db.prepare(`UPDATE projects SET ${fields} WHERE id = $id RETURNING *`).get(params) as Project | null
32
- }
33
-
34
- // Pages
35
- export function createPage(db: Database, data: { project_id: string; url: string; path?: string; name?: string }): Page {
36
- return db.prepare(`
37
- INSERT INTO pages (project_id, url, path, name)
38
- VALUES ($project_id, $url, $path, $name)
39
- ON CONFLICT(project_id, url) DO UPDATE SET name = excluded.name
40
- RETURNING *
41
- `).get({
42
- $project_id: data.project_id,
43
- $url: data.url,
44
- $path: data.path ?? new URL(data.url).pathname,
45
- $name: data.name ?? null,
46
- }) as Page
47
- }
48
-
49
- export function listPages(db: Database, projectId: string): Page[] {
50
- return db.prepare("SELECT * FROM pages WHERE project_id = $p ORDER BY created_at ASC").all({ $p: projectId }) as Page[]
51
- }
52
-
53
- export function getPage(db: Database, id: string): Page | null {
54
- return db.prepare("SELECT * FROM pages WHERE id = $id").get({ $id: id }) as Page | null
55
- }
56
-
57
- /** Resolves a project ID or name to a project ID. Returns null if not found or input is empty. */
58
- export function resolveProjectId(db: Database, idOrName: string | undefined | null): string | null {
59
- if (!idOrName) return null
60
- // Looks like a hex ID (8+ hex chars)
61
- if (/^[0-9a-f]{8,}$/i.test(idOrName)) return idOrName
62
- // Try name lookup (case-insensitive)
63
- const p = db.prepare("SELECT id FROM projects WHERE LOWER(name) = LOWER($n)").get({ $n: idOrName }) as { id: string } | null
64
- return p?.id ?? null
65
- }
66
-
67
- export function touchPage(db: Database, id: string): void {
68
- db.run("UPDATE pages SET last_scanned_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = $id", { $id: id })
69
- }
@@ -1,104 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { createTestDb } from "../db/index.ts"
3
- import { ingestBatch } from "./ingest.ts"
4
- import { getLogContext, searchLogs, tailLogs } from "./query.ts"
5
-
6
- function seed(db: ReturnType<typeof createTestDb>) {
7
- ingestBatch(db, [
8
- { level: "error", message: "DB connection failed", service: "api", trace_id: "t1" },
9
- { level: "warn", message: "Slow query detected", service: "api", trace_id: "t1" },
10
- { level: "info", message: "User login", service: "auth" },
11
- { level: "debug", message: "Cache miss", service: "cache" },
12
- { level: "fatal", message: "Out of memory", service: "worker" },
13
- ])
14
- }
15
-
16
- describe("searchLogs", () => {
17
- it("returns all logs without filters", () => {
18
- const db = createTestDb()
19
- seed(db)
20
- const rows = searchLogs(db, {})
21
- expect(rows.length).toBe(5)
22
- })
23
-
24
- it("filters by level", () => {
25
- const db = createTestDb()
26
- seed(db)
27
- const rows = searchLogs(db, { level: "error" })
28
- expect(rows.every(r => r.level === "error")).toBe(true)
29
- })
30
-
31
- it("filters by multiple levels", () => {
32
- const db = createTestDb()
33
- seed(db)
34
- const rows = searchLogs(db, { level: ["error", "fatal"] })
35
- expect(rows).toHaveLength(2)
36
- })
37
-
38
- it("filters by service", () => {
39
- const db = createTestDb()
40
- seed(db)
41
- const rows = searchLogs(db, { service: "api" })
42
- expect(rows).toHaveLength(2)
43
- })
44
-
45
- it("full-text search on message", () => {
46
- const db = createTestDb()
47
- seed(db)
48
- const rows = searchLogs(db, { text: "connection" })
49
- expect(rows).toHaveLength(1)
50
- expect(rows[0]!.message).toContain("connection")
51
- })
52
-
53
- it("filters by trace_id", () => {
54
- const db = createTestDb()
55
- seed(db)
56
- const rows = searchLogs(db, { trace_id: "t1" })
57
- expect(rows).toHaveLength(2)
58
- })
59
-
60
- it("respects limit", () => {
61
- const db = createTestDb()
62
- seed(db)
63
- const rows = searchLogs(db, { limit: 2 })
64
- expect(rows).toHaveLength(2)
65
- })
66
-
67
- it("returns results ordered by timestamp desc", () => {
68
- const db = createTestDb()
69
- seed(db)
70
- const rows = searchLogs(db, {})
71
- expect(rows[0]!.timestamp >= rows[rows.length - 1]!.timestamp).toBe(true)
72
- })
73
- })
74
-
75
- describe("tailLogs", () => {
76
- it("returns n most recent logs", () => {
77
- const db = createTestDb()
78
- seed(db)
79
- const rows = tailLogs(db, undefined, 3)
80
- expect(rows).toHaveLength(3)
81
- })
82
-
83
- it("filters by project_id", () => {
84
- const db = createTestDb()
85
- const rows = tailLogs(db, "nonexistent")
86
- expect(rows).toHaveLength(0)
87
- })
88
- })
89
-
90
- describe("getLogContext", () => {
91
- it("returns all logs for a trace_id in asc order", () => {
92
- const db = createTestDb()
93
- seed(db)
94
- const rows = getLogContext(db, "t1")
95
- expect(rows).toHaveLength(2)
96
- expect(rows[0]!.timestamp <= rows[1]!.timestamp).toBe(true)
97
- })
98
-
99
- it("returns empty for unknown trace_id", () => {
100
- const db = createTestDb()
101
- const rows = getLogContext(db, "unknown")
102
- expect(rows).toHaveLength(0)
103
- })
104
- })
package/src/lib/query.ts DELETED
@@ -1,84 +0,0 @@
1
- import type { Database } from "bun:sqlite"
2
- import type { LogQuery, LogRow } from "../types/index.ts"
3
- import { parseTime } from "./parse-time.ts"
4
-
5
- export function searchLogs(db: Database, q: LogQuery): LogRow[] {
6
- const conditions: string[] = []
7
- const params: Record<string, unknown> = {}
8
-
9
- if (q.project_id) { conditions.push("l.project_id = $project_id"); params.$project_id = q.project_id }
10
- if (q.page_id) { conditions.push("l.page_id = $page_id"); params.$page_id = q.page_id }
11
- if (q.service) { conditions.push("l.service = $service"); params.$service = q.service }
12
- if (q.trace_id) { conditions.push("l.trace_id = $trace_id"); params.$trace_id = q.trace_id }
13
- if (q.since) { conditions.push("l.timestamp >= $since"); params.$since = parseTime(q.since) ?? q.since }
14
- if (q.until) { conditions.push("l.timestamp <= $until"); params.$until = parseTime(q.until) ?? q.until }
15
-
16
- if (q.level) {
17
- const levels = Array.isArray(q.level) ? q.level : [q.level]
18
- const placeholders = levels.map((_, i) => `$level${i}`).join(",")
19
- levels.forEach((lv, i) => { params[`$level${i}`] = lv })
20
- conditions.push(`l.level IN (${placeholders})`)
21
- }
22
-
23
- const limit = q.limit ?? 100
24
- const offset = q.offset ?? 0
25
- params.$limit = limit
26
- params.$offset = offset
27
-
28
- if (q.text) {
29
- // FTS search via subquery
30
- params.$text = q.text
31
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")} AND` : "WHERE"
32
- const sql = `
33
- SELECT l.* FROM logs l
34
- ${where} l.rowid IN (SELECT rowid FROM logs_fts WHERE logs_fts MATCH $text)
35
- ORDER BY l.timestamp DESC
36
- LIMIT $limit OFFSET $offset
37
- `
38
- return db.prepare(sql).all(params) as LogRow[]
39
- }
40
-
41
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""
42
- const sql = `SELECT * FROM logs l ${where} ORDER BY l.timestamp DESC LIMIT $limit OFFSET $offset`
43
- return db.prepare(sql).all(params) as LogRow[]
44
- }
45
-
46
- export function tailLogs(db: Database, projectId?: string, n = 50): LogRow[] {
47
- if (projectId) {
48
- return db.prepare("SELECT * FROM logs WHERE project_id = $p ORDER BY timestamp DESC LIMIT $n")
49
- .all({ $p: projectId, $n: n }) as LogRow[]
50
- }
51
- return db.prepare("SELECT * FROM logs ORDER BY timestamp DESC LIMIT $n").all({ $n: n }) as LogRow[]
52
- }
53
-
54
- export function getLogContext(db: Database, traceId: string): LogRow[] {
55
- return db.prepare("SELECT * FROM logs WHERE trace_id = $t ORDER BY timestamp ASC")
56
- .all({ $t: traceId }) as LogRow[]
57
- }
58
-
59
- export function getLogContextFromId(db: Database, logId: string, window = 0): LogRow[] {
60
- const log = db.prepare("SELECT * FROM logs WHERE id = $id").get({ $id: logId }) as LogRow | null
61
- if (!log) return []
62
-
63
- // Get trace context (or just the log itself if no trace)
64
- const trace: LogRow[] = log.trace_id ? getLogContext(db, log.trace_id) : [log]
65
-
66
- if (window <= 0) return trace
67
-
68
- // Fetch N rows before and after the target log's timestamp
69
- const before = db.prepare(
70
- `SELECT * FROM logs WHERE id != $id AND timestamp <= $ts ORDER BY timestamp DESC LIMIT $n`
71
- ).all({ $id: logId, $ts: log.timestamp, $n: window }) as LogRow[]
72
-
73
- const after = db.prepare(
74
- `SELECT * FROM logs WHERE id != $id AND timestamp > $ts ORDER BY timestamp ASC LIMIT $n`
75
- ).all({ $id: logId, $ts: log.timestamp, $n: window }) as LogRow[]
76
-
77
- // Merge: before (oldest first) + trace + after, deduplicate by id
78
- const seen = new Set<string>()
79
- const merged: LogRow[] = []
80
- for (const row of [...before.reverse(), ...trace, ...after]) {
81
- if (!seen.has(row.id)) { seen.add(row.id); merged.push(row) }
82
- }
83
- return merged.sort((a, b) => a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0)
84
- }
@@ -1,42 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { createTestDb } from "../db/index.ts"
3
- import { ingestBatch } from "./ingest.ts"
4
- import { runRetentionForProject, setRetentionPolicy } from "./retention.ts"
5
-
6
- function seedProject(db: ReturnType<typeof createTestDb>, name = "app") {
7
- return db.prepare("INSERT INTO projects (name) VALUES (?) RETURNING id").get(name) as { id: string }
8
- }
9
-
10
- describe("retention", () => {
11
- it("does nothing when under max_rows", () => {
12
- const db = createTestDb()
13
- const p = seedProject(db)
14
- ingestBatch(db, Array.from({ length: 5 }, () => ({ level: "info" as const, message: "x", project_id: p.id })))
15
- const result = runRetentionForProject(db, p.id)
16
- expect(result.deleted).toBe(0)
17
- })
18
-
19
- it("enforces max_rows", () => {
20
- const db = createTestDb()
21
- const p = seedProject(db)
22
- setRetentionPolicy(db, p.id, { max_rows: 3 })
23
- ingestBatch(db, Array.from({ length: 10 }, () => ({ level: "info" as const, message: "x", project_id: p.id })))
24
- runRetentionForProject(db, p.id)
25
- const count = (db.prepare("SELECT COUNT(*) as c FROM logs WHERE project_id = ?").get(p.id) as { c: number }).c
26
- expect(count).toBeLessThanOrEqual(3)
27
- })
28
-
29
- it("returns 0 for unknown project", () => {
30
- const db = createTestDb()
31
- expect(runRetentionForProject(db, "nope").deleted).toBe(0)
32
- })
33
-
34
- it("setRetentionPolicy updates project config", () => {
35
- const db = createTestDb()
36
- const p = seedProject(db)
37
- setRetentionPolicy(db, p.id, { max_rows: 500, debug_ttl_hours: 1 })
38
- const proj = db.prepare("SELECT max_rows, debug_ttl_hours FROM projects WHERE id = ?").get(p.id) as { max_rows: number; debug_ttl_hours: number }
39
- expect(proj.max_rows).toBe(500)
40
- expect(proj.debug_ttl_hours).toBe(1)
41
- })
42
- })
@@ -1,62 +0,0 @@
1
- import type { Database } from "bun:sqlite"
2
-
3
- interface RetentionConfig {
4
- max_rows: number
5
- debug_ttl_hours: number
6
- info_ttl_hours: number
7
- warn_ttl_hours: number
8
- error_ttl_hours: number
9
- }
10
-
11
- const TTL_BY_LEVEL: Record<string, keyof RetentionConfig> = {
12
- debug: "debug_ttl_hours",
13
- info: "info_ttl_hours",
14
- warn: "warn_ttl_hours",
15
- error: "error_ttl_hours",
16
- fatal: "error_ttl_hours",
17
- }
18
-
19
- export function runRetentionForProject(db: Database, projectId: string): { deleted: number } {
20
- const project = db.prepare("SELECT * FROM projects WHERE id = $id").get({ $id: projectId }) as (RetentionConfig & { id: string }) | null
21
- if (!project) return { deleted: 0 }
22
-
23
- let deleted = 0
24
-
25
- // TTL enforcement per level
26
- for (const [level, configKey] of Object.entries(TTL_BY_LEVEL)) {
27
- const ttlHours = project[configKey] as number
28
- const cutoff = new Date(Date.now() - ttlHours * 3600 * 1000).toISOString()
29
- const before = (db.prepare("SELECT COUNT(*) as c FROM logs WHERE project_id = $p AND level = $level AND timestamp < $cutoff").get({ $p: projectId, $level: level, $cutoff: cutoff }) as { c: number }).c
30
- if (before > 0) {
31
- db.prepare("DELETE FROM logs WHERE project_id = $p AND level = $level AND timestamp < $cutoff").run({ $p: projectId, $level: level, $cutoff: cutoff })
32
- deleted += before
33
- }
34
- }
35
-
36
- // max_rows enforcement
37
- const total = (db.prepare("SELECT COUNT(*) as c FROM logs WHERE project_id = $p").get({ $p: projectId }) as { c: number }).c
38
- if (total > project.max_rows) {
39
- const toDelete = total - project.max_rows
40
- db.prepare(`DELETE FROM logs WHERE id IN (SELECT id FROM logs WHERE project_id = $p ORDER BY timestamp ASC LIMIT ${toDelete})`).run({ $p: projectId })
41
- deleted += toDelete
42
- }
43
-
44
- return { deleted }
45
- }
46
-
47
- export function runRetentionAll(db: Database): { deleted: number; projects: number } {
48
- const projects = db.prepare("SELECT id FROM projects").all() as { id: string }[]
49
- let deleted = 0
50
- for (const p of projects) {
51
- deleted += runRetentionForProject(db, p.id).deleted
52
- }
53
- return { deleted, projects: projects.length }
54
- }
55
-
56
- export function setRetentionPolicy(db: Database, projectId: string, config: Partial<RetentionConfig>): void {
57
- const fields = Object.keys(config).map(k => `${k} = $${k}`).join(", ")
58
- if (!fields) return
59
- const params = Object.fromEntries(Object.entries(config).map(([k, v]) => [`$${k}`, v]))
60
- params.$id = projectId
61
- db.prepare(`UPDATE projects SET ${fields} WHERE id = $id`).run(params)
62
- }
@@ -1,37 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { createTestDb } from "../db/index.ts"
3
- import { ingestBatch } from "./ingest.ts"
4
- import { rotateLogs, rotateByProject } from "./rotate.ts"
5
-
6
- describe("rotateLogs", () => {
7
- it("does nothing when under maxRows", () => {
8
- const db = createTestDb()
9
- ingestBatch(db, [{ level: "info", message: "a" }, { level: "info", message: "b" }])
10
- const deleted = rotateLogs(db, 100)
11
- expect(deleted).toBe(0)
12
- })
13
-
14
- it("deletes oldest when over maxRows", () => {
15
- const db = createTestDb()
16
- ingestBatch(db, Array.from({ length: 10 }, (_, i) => ({ level: "info" as const, message: `msg ${i}` })))
17
- const deleted = rotateLogs(db, 5)
18
- expect(deleted).toBe(5)
19
- const remaining = (db.prepare("SELECT COUNT(*) as c FROM logs").get() as { c: number }).c
20
- expect(remaining).toBe(5)
21
- })
22
- })
23
-
24
- describe("rotateByProject", () => {
25
- it("only rotates logs for the specified project", () => {
26
- const db = createTestDb()
27
- const p1 = db.prepare("INSERT INTO projects (name) VALUES ('p1') RETURNING id").get() as { id: string }
28
- const p2 = db.prepare("INSERT INTO projects (name) VALUES ('p2') RETURNING id").get() as { id: string }
29
- ingestBatch(db, Array.from({ length: 8 }, () => ({ level: "info" as const, message: "x", project_id: p1.id })))
30
- ingestBatch(db, Array.from({ length: 5 }, () => ({ level: "info" as const, message: "y", project_id: p2.id })))
31
- rotateByProject(db, p1.id, 3)
32
- const p1count = (db.prepare("SELECT COUNT(*) as c FROM logs WHERE project_id = ?").get(p1.id) as { c: number }).c
33
- const p2count = (db.prepare("SELECT COUNT(*) as c FROM logs WHERE project_id = ?").get(p2.id) as { c: number }).c
34
- expect(p1count).toBe(3)
35
- expect(p2count).toBe(5) // untouched
36
- })
37
- })