@hasna/logs 0.3.26 → 0.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/dashboard/dist/assets/index-C0wZYq1m.js +53 -0
- package/dashboard/dist/assets/index-DGNrK5qb.css +1 -0
- package/dashboard/dist/index.html +14 -0
- package/dist/cli/index.js +8511 -177
- package/dist/count-bmj4r2zb.js +10 -0
- package/dist/{diagnose-e0w5rwbc.js → diagnose-3q5cy9ra.js} +2 -2
- package/dist/{export-c3eqjste.js → export-cngdb9fh.js} +1 -1
- package/dist/{http-zm3ph78w.js → http-r0xc3d2s.js} +79 -8
- package/dist/index-931pbyn5.js +141 -0
- package/dist/index-b5c72f1p.js +7 -0
- package/dist/{index-7w7v7hnr.js → index-by1pdzbr.js} +14 -5
- package/dist/{index-3dr7d80h.js → index-e1930v9b.js} +12 -8
- package/dist/{index-eh9bkbpa.js → index-e72k53yq.js} +10 -2
- package/dist/{index-edn08m6f.js → index-gcd14q2f.js} +9 -6
- package/dist/index-hq6kzaah.js +26 -0
- package/dist/index-j34f36wy.js +5672 -0
- package/dist/{index-5qznfyah.js → index-q27bgpr1.js} +1086 -1646
- package/dist/index-qk8dbvbc.js +1859 -0
- package/dist/index-t3x838zw.js +2583 -0
- package/dist/{index-gc0zvs88.js → index-y2y0mdtd.js} +596 -37
- package/dist/{index-ww5ggfv3.js → index-zkb3z95a.js} +12 -9
- package/dist/index.js +2990 -22
- package/dist/{jobs-ypmmc2ma.js → jobs-hsgyhfvm.js} +2 -1
- package/dist/mcp/index.js +1473 -4286
- package/dist/{query-7jwj05er.js → query-c5a43zx3.js} +3 -2
- package/dist/server/index.js +2944 -417
- package/dist/storage.js +50 -0
- package/package.json +27 -8
- package/biome.json +0 -13
- package/bun.lock +0 -376
- package/dashboard/README.md +0 -73
- package/dashboard/bun.lock +0 -526
- package/dashboard/eslint.config.js +0 -23
- package/dashboard/index.html +0 -13
- package/dashboard/package.json +0 -32
- package/dashboard/src/App.css +0 -184
- package/dashboard/src/App.tsx +0 -49
- package/dashboard/src/api.ts +0 -33
- package/dashboard/src/assets/hero.png +0 -0
- package/dashboard/src/assets/react.svg +0 -1
- package/dashboard/src/assets/vite.svg +0 -1
- package/dashboard/src/index.css +0 -111
- package/dashboard/src/main.tsx +0 -10
- package/dashboard/src/pages/Alerts.tsx +0 -69
- package/dashboard/src/pages/Issues.tsx +0 -50
- package/dashboard/src/pages/Perf.tsx +0 -75
- package/dashboard/src/pages/Projects.tsx +0 -67
- package/dashboard/src/pages/Summary.tsx +0 -67
- package/dashboard/src/pages/Tail.tsx +0 -65
- package/dashboard/tsconfig.app.json +0 -28
- package/dashboard/tsconfig.json +0 -7
- package/dashboard/tsconfig.node.json +0 -26
- package/dashboard/vite.config.ts +0 -14
- package/dist/count-x3n7qg3c.js +0 -9
- package/dist/index-997bkzr2.js +0 -15
- package/dist/index-pen6t0yc.js +0 -10794
- package/sdk/package.json +0 -27
- package/sdk/src/index.ts +0 -143
- package/sdk/src/types.ts +0 -56
- package/src/cli/entrypoints.test.ts +0 -63
- package/src/cli/index.ts +0 -471
- package/src/db/index.test.ts +0 -33
- package/src/db/index.ts +0 -189
- package/src/db/migrations/001_alert_rules.ts +0 -21
- package/src/db/migrations/002_issues.ts +0 -21
- package/src/db/migrations/003_retention.ts +0 -15
- package/src/db/migrations/004_page_auth.ts +0 -13
- package/src/db/pg-migrations.ts +0 -167
- package/src/index.ts +0 -1
- package/src/lib/alerts.test.ts +0 -67
- package/src/lib/alerts.ts +0 -117
- package/src/lib/browser-script.test.ts +0 -35
- package/src/lib/browser-script.ts +0 -31
- package/src/lib/compare.test.ts +0 -52
- package/src/lib/compare.ts +0 -85
- package/src/lib/count.test.ts +0 -44
- package/src/lib/count.ts +0 -55
- package/src/lib/diagnose.test.ts +0 -55
- package/src/lib/diagnose.ts +0 -91
- package/src/lib/export.test.ts +0 -66
- package/src/lib/export.ts +0 -65
- package/src/lib/github.ts +0 -38
- package/src/lib/health.test.ts +0 -48
- package/src/lib/health.ts +0 -51
- package/src/lib/ingest.test.ts +0 -57
- package/src/lib/ingest.ts +0 -78
- package/src/lib/issues.test.ts +0 -79
- package/src/lib/issues.ts +0 -70
- package/src/lib/jobs.test.ts +0 -69
- package/src/lib/jobs.ts +0 -63
- package/src/lib/lighthouse.ts +0 -65
- package/src/lib/package-meta.test.ts +0 -43
- package/src/lib/package-meta.ts +0 -80
- package/src/lib/page-auth.test.ts +0 -54
- package/src/lib/page-auth.ts +0 -48
- package/src/lib/parse-time.test.ts +0 -37
- package/src/lib/parse-time.ts +0 -14
- package/src/lib/perf.test.ts +0 -45
- package/src/lib/perf.ts +0 -46
- package/src/lib/projects.test.ts +0 -73
- package/src/lib/projects.ts +0 -69
- package/src/lib/query.test.ts +0 -104
- package/src/lib/query.ts +0 -84
- package/src/lib/retention.test.ts +0 -42
- package/src/lib/retention.ts +0 -62
- package/src/lib/rotate.test.ts +0 -37
- package/src/lib/rotate.ts +0 -27
- package/src/lib/scanner.ts +0 -131
- package/src/lib/scheduler.ts +0 -63
- package/src/lib/session-context.ts +0 -28
- package/src/lib/summarize.test.ts +0 -38
- package/src/lib/summarize.ts +0 -23
- package/src/mcp/http.test.ts +0 -92
- package/src/mcp/http.ts +0 -135
- package/src/mcp/index.test.ts +0 -27
- package/src/mcp/index.ts +0 -444
- package/src/server/index.ts +0 -61
- package/src/server/routes/alerts.ts +0 -32
- package/src/server/routes/issues.ts +0 -43
- package/src/server/routes/jobs.ts +0 -32
- package/src/server/routes/logs.ts +0 -113
- package/src/server/routes/perf.ts +0 -23
- package/src/server/routes/projects.ts +0 -67
- package/src/server/routes/stream.ts +0 -43
- package/src/server/server.test.ts +0 -194
- package/src/types/index.ts +0 -119
- package/tsconfig.json +0 -22
- /package/dashboard/{public → dist}/favicon.svg +0 -0
- /package/dashboard/{public → dist}/icons.svg +0 -0
package/src/lib/page-auth.ts
DELETED
|
@@ -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
|
-
})
|
package/src/lib/parse-time.ts
DELETED
|
@@ -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
|
-
}
|
package/src/lib/perf.test.ts
DELETED
|
@@ -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
|
-
}
|
package/src/lib/projects.test.ts
DELETED
|
@@ -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
|
-
})
|
package/src/lib/projects.ts
DELETED
|
@@ -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
|
-
}
|
package/src/lib/query.test.ts
DELETED
|
@@ -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
|
-
})
|
package/src/lib/retention.ts
DELETED
|
@@ -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
|
-
}
|
package/src/lib/rotate.test.ts
DELETED
|
@@ -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
|
-
})
|