@hasna/logs 0.0.1
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/biome.json +13 -0
- package/dist/cli/index.js +2272 -0
- package/dist/mcp/index.js +28507 -0
- package/dist/server/index.js +1862 -0
- package/package.json +43 -0
- package/sdk/package.json +21 -0
- package/sdk/src/index.ts +143 -0
- package/src/cli/index.ts +193 -0
- package/src/db/index.test.ts +33 -0
- package/src/db/index.ts +153 -0
- package/src/lib/browser-script.test.ts +35 -0
- package/src/lib/browser-script.ts +31 -0
- package/src/lib/github.ts +38 -0
- package/src/lib/ingest.test.ts +57 -0
- package/src/lib/ingest.ts +51 -0
- package/src/lib/jobs.test.ts +69 -0
- package/src/lib/jobs.ts +63 -0
- package/src/lib/lighthouse.ts +65 -0
- package/src/lib/perf.test.ts +45 -0
- package/src/lib/perf.ts +46 -0
- package/src/lib/projects.test.ts +73 -0
- package/src/lib/projects.ts +59 -0
- package/src/lib/query.test.ts +104 -0
- package/src/lib/query.ts +56 -0
- package/src/lib/rotate.test.ts +37 -0
- package/src/lib/rotate.ts +27 -0
- package/src/lib/scanner.ts +112 -0
- package/src/lib/scheduler.ts +57 -0
- package/src/lib/summarize.test.ts +38 -0
- package/src/lib/summarize.ts +21 -0
- package/src/mcp/index.ts +165 -0
- package/src/server/index.ts +42 -0
- package/src/server/routes/jobs.ts +32 -0
- package/src/server/routes/logs.ts +65 -0
- package/src/server/routes/perf.ts +23 -0
- package/src/server/routes/projects.ts +42 -0
- package/src/server/server.test.ts +194 -0
- package/src/types/index.ts +119 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Hono } from "hono"
|
|
2
|
+
import type { Database } from "bun:sqlite"
|
|
3
|
+
import { createPage, createProject, getProject, listPages, listProjects } from "../../lib/projects.ts"
|
|
4
|
+
import { syncGithubRepo } from "../../lib/github.ts"
|
|
5
|
+
|
|
6
|
+
export function projectsRoutes(db: Database) {
|
|
7
|
+
const app = new Hono()
|
|
8
|
+
|
|
9
|
+
app.post("/", async (c) => {
|
|
10
|
+
const body = await c.req.json()
|
|
11
|
+
if (!body.name) return c.json({ error: "name is required" }, 422)
|
|
12
|
+
const project = createProject(db, body)
|
|
13
|
+
return c.json(project, 201)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
app.get("/", (c) => c.json(listProjects(db)))
|
|
17
|
+
|
|
18
|
+
app.get("/:id", (c) => {
|
|
19
|
+
const project = getProject(db, c.req.param("id"))
|
|
20
|
+
if (!project) return c.json({ error: "not found" }, 404)
|
|
21
|
+
return c.json(project)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
app.post("/:id/pages", async (c) => {
|
|
25
|
+
const body = await c.req.json()
|
|
26
|
+
if (!body.url) return c.json({ error: "url is required" }, 422)
|
|
27
|
+
const page = createPage(db, { ...body, project_id: c.req.param("id") })
|
|
28
|
+
return c.json(page, 201)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
app.get("/:id/pages", (c) => c.json(listPages(db, c.req.param("id"))))
|
|
32
|
+
|
|
33
|
+
app.post("/:id/sync-repo", async (c) => {
|
|
34
|
+
const project = getProject(db, c.req.param("id"))
|
|
35
|
+
if (!project) return c.json({ error: "not found" }, 404)
|
|
36
|
+
if (!project.github_repo) return c.json({ error: "no github_repo set" }, 422)
|
|
37
|
+
const updated = await syncGithubRepo(db, project)
|
|
38
|
+
return c.json(updated)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return app
|
|
42
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach } from "bun:test"
|
|
2
|
+
import { Hono } from "hono"
|
|
3
|
+
import { cors } from "hono/cors"
|
|
4
|
+
import { createTestDb } from "../db/index.ts"
|
|
5
|
+
import { logsRoutes } from "./routes/logs.ts"
|
|
6
|
+
import { projectsRoutes } from "./routes/projects.ts"
|
|
7
|
+
import { jobsRoutes } from "./routes/jobs.ts"
|
|
8
|
+
import { perfRoutes } from "./routes/perf.ts"
|
|
9
|
+
|
|
10
|
+
function buildApp() {
|
|
11
|
+
const db = createTestDb()
|
|
12
|
+
const app = new Hono()
|
|
13
|
+
app.use("*", cors())
|
|
14
|
+
app.route("/api/logs", logsRoutes(db))
|
|
15
|
+
app.route("/api/projects", projectsRoutes(db))
|
|
16
|
+
app.route("/api/jobs", jobsRoutes(db))
|
|
17
|
+
app.route("/api/perf", perfRoutes(db))
|
|
18
|
+
return { app, db }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("POST /api/logs", () => {
|
|
22
|
+
it("ingests a single log", async () => {
|
|
23
|
+
const { app } = buildApp()
|
|
24
|
+
const res = await app.request("/api/logs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ level: "error", message: "boom" }) })
|
|
25
|
+
expect(res.status).toBe(201)
|
|
26
|
+
const body = await res.json() as { level: string; message: string }
|
|
27
|
+
expect(body.level).toBe("error")
|
|
28
|
+
expect(body.message).toBe("boom")
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("ingests a batch", async () => {
|
|
32
|
+
const { app } = buildApp()
|
|
33
|
+
const res = await app.request("/api/logs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify([{ level: "info", message: "a" }, { level: "warn", message: "b" }]) })
|
|
34
|
+
expect(res.status).toBe(201)
|
|
35
|
+
const body = await res.json() as { inserted: number }
|
|
36
|
+
expect(body.inserted).toBe(2)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe("GET /api/logs", () => {
|
|
41
|
+
it("lists logs", async () => {
|
|
42
|
+
const { app, db } = buildApp()
|
|
43
|
+
const { ingestBatch } = await import("../lib/ingest.ts")
|
|
44
|
+
ingestBatch(db, [{ level: "error", message: "e1" }, { level: "info", message: "i1" }])
|
|
45
|
+
const res = await app.request("/api/logs")
|
|
46
|
+
expect(res.status).toBe(200)
|
|
47
|
+
const body = await res.json() as unknown[]
|
|
48
|
+
expect(body.length).toBeGreaterThanOrEqual(2)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("filters by level", async () => {
|
|
52
|
+
const { app, db } = buildApp()
|
|
53
|
+
const { ingestBatch } = await import("../lib/ingest.ts")
|
|
54
|
+
ingestBatch(db, [{ level: "error", message: "e1" }, { level: "info", message: "i1" }])
|
|
55
|
+
const res = await app.request("/api/logs?level=error")
|
|
56
|
+
const body = await res.json() as { level: string }[]
|
|
57
|
+
expect(body.every(r => r.level === "error")).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("supports ?fields= projection", async () => {
|
|
61
|
+
const { app, db } = buildApp()
|
|
62
|
+
const { ingestLog } = await import("../lib/ingest.ts")
|
|
63
|
+
ingestLog(db, { level: "info", message: "hello" })
|
|
64
|
+
const res = await app.request("/api/logs?fields=level,message")
|
|
65
|
+
const body = await res.json() as Record<string, unknown>[]
|
|
66
|
+
expect(Object.keys(body[0]!).sort()).toEqual(["level", "message"].sort())
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe("GET /api/logs/tail", () => {
|
|
71
|
+
it("returns recent logs", async () => {
|
|
72
|
+
const { app, db } = buildApp()
|
|
73
|
+
const { ingestBatch } = await import("../lib/ingest.ts")
|
|
74
|
+
ingestBatch(db, Array.from({ length: 10 }, (_, i) => ({ level: "info" as const, message: `m${i}` })))
|
|
75
|
+
const res = await app.request("/api/logs/tail?n=5")
|
|
76
|
+
const body = await res.json() as unknown[]
|
|
77
|
+
expect(body).toHaveLength(5)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe("GET /api/logs/summary", () => {
|
|
82
|
+
it("returns summary of errors/warns", async () => {
|
|
83
|
+
const { app, db } = buildApp()
|
|
84
|
+
const { ingestBatch } = await import("../lib/ingest.ts")
|
|
85
|
+
ingestBatch(db, [{ level: "error", message: "x", service: "api" }, { level: "warn", message: "y", service: "db" }])
|
|
86
|
+
const res = await app.request("/api/logs/summary")
|
|
87
|
+
const body = await res.json() as unknown[]
|
|
88
|
+
expect(body.length).toBeGreaterThan(0)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe("GET /api/logs/:trace_id/context", () => {
|
|
93
|
+
it("returns logs for trace", async () => {
|
|
94
|
+
const { app, db } = buildApp()
|
|
95
|
+
const { ingestBatch } = await import("../lib/ingest.ts")
|
|
96
|
+
ingestBatch(db, [{ level: "info", message: "a", trace_id: "t99" }, { level: "error", message: "b", trace_id: "t99" }])
|
|
97
|
+
const res = await app.request("/api/logs/t99/context")
|
|
98
|
+
const body = await res.json() as unknown[]
|
|
99
|
+
expect(body).toHaveLength(2)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe("POST /api/projects", () => {
|
|
104
|
+
it("creates a project", async () => {
|
|
105
|
+
const { app } = buildApp()
|
|
106
|
+
const res = await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "myapp" }) })
|
|
107
|
+
expect(res.status).toBe(201)
|
|
108
|
+
const body = await res.json() as { name: string }
|
|
109
|
+
expect(body.name).toBe("myapp")
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("returns 422 without name", async () => {
|
|
113
|
+
const { app } = buildApp()
|
|
114
|
+
const res = await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) })
|
|
115
|
+
expect(res.status).toBe(422)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe("GET /api/projects", () => {
|
|
120
|
+
it("lists projects", async () => {
|
|
121
|
+
const { app } = buildApp()
|
|
122
|
+
await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "p1" }) })
|
|
123
|
+
const res = await app.request("/api/projects")
|
|
124
|
+
const body = await res.json() as unknown[]
|
|
125
|
+
expect(body.length).toBeGreaterThanOrEqual(1)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe("POST /api/projects/:id/pages", () => {
|
|
130
|
+
it("registers a page", async () => {
|
|
131
|
+
const { app } = buildApp()
|
|
132
|
+
const pRes = await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "app" }) })
|
|
133
|
+
const project = await pRes.json() as { id: string }
|
|
134
|
+
const res = await app.request(`/api/projects/${project.id}/pages`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://app.com/home" }) })
|
|
135
|
+
expect(res.status).toBe(201)
|
|
136
|
+
const page = await res.json() as { url: string }
|
|
137
|
+
expect(page.url).toBe("https://app.com/home")
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it("returns 422 without url", async () => {
|
|
141
|
+
const { app } = buildApp()
|
|
142
|
+
const pRes = await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "app2" }) })
|
|
143
|
+
const project = await pRes.json() as { id: string }
|
|
144
|
+
const res = await app.request(`/api/projects/${project.id}/pages`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) })
|
|
145
|
+
expect(res.status).toBe(422)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe("jobs routes", () => {
|
|
150
|
+
it("creates and lists jobs", async () => {
|
|
151
|
+
const { app } = buildApp()
|
|
152
|
+
const pRes = await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "appj" }) })
|
|
153
|
+
const { id } = await pRes.json() as { id: string }
|
|
154
|
+
const jRes = await app.request("/api/jobs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ project_id: id, schedule: "*/5 * * * *" }) })
|
|
155
|
+
expect(jRes.status).toBe(201)
|
|
156
|
+
const listRes = await app.request(`/api/jobs?project_id=${id}`)
|
|
157
|
+
const jobs = await listRes.json() as unknown[]
|
|
158
|
+
expect(jobs).toHaveLength(1)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it("returns 422 without required fields", async () => {
|
|
162
|
+
const { app } = buildApp()
|
|
163
|
+
const res = await app.request("/api/jobs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) })
|
|
164
|
+
expect(res.status).toBe(422)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it("deletes a job", async () => {
|
|
168
|
+
const { app } = buildApp()
|
|
169
|
+
const pRes = await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "appd" }) })
|
|
170
|
+
const { id } = await pRes.json() as { id: string }
|
|
171
|
+
const jRes = await app.request("/api/jobs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ project_id: id, schedule: "*/5 * * * *" }) })
|
|
172
|
+
const job = await jRes.json() as { id: string }
|
|
173
|
+
const del = await app.request(`/api/jobs/${job.id}`, { method: "DELETE" })
|
|
174
|
+
expect(del.status).toBe(200)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe("perf routes", () => {
|
|
179
|
+
it("returns 422 without project_id", async () => {
|
|
180
|
+
const { app } = buildApp()
|
|
181
|
+
const res = await app.request("/api/perf")
|
|
182
|
+
expect(res.status).toBe(422)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it("returns null when no snapshot exists", async () => {
|
|
186
|
+
const { app } = buildApp()
|
|
187
|
+
const pRes = await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "perf-app" }) })
|
|
188
|
+
const { id } = await pRes.json() as { id: string }
|
|
189
|
+
const res = await app.request(`/api/perf?project_id=${id}`)
|
|
190
|
+
expect(res.status).toBe(200)
|
|
191
|
+
const body = await res.json()
|
|
192
|
+
expect(body).toBeNull()
|
|
193
|
+
})
|
|
194
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error" | "fatal"
|
|
2
|
+
export type LogSource = "sdk" | "script" | "scanner"
|
|
3
|
+
|
|
4
|
+
export interface LogEntry {
|
|
5
|
+
id?: string
|
|
6
|
+
timestamp?: string
|
|
7
|
+
project_id?: string
|
|
8
|
+
page_id?: string
|
|
9
|
+
level: LogLevel
|
|
10
|
+
source?: LogSource
|
|
11
|
+
service?: string
|
|
12
|
+
message: string
|
|
13
|
+
trace_id?: string
|
|
14
|
+
session_id?: string
|
|
15
|
+
agent?: string
|
|
16
|
+
url?: string
|
|
17
|
+
stack_trace?: string
|
|
18
|
+
metadata?: Record<string, unknown>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LogRow {
|
|
22
|
+
id: string
|
|
23
|
+
timestamp: string
|
|
24
|
+
project_id: string | null
|
|
25
|
+
page_id: string | null
|
|
26
|
+
level: LogLevel
|
|
27
|
+
source: LogSource
|
|
28
|
+
service: string | null
|
|
29
|
+
message: string
|
|
30
|
+
trace_id: string | null
|
|
31
|
+
session_id: string | null
|
|
32
|
+
agent: string | null
|
|
33
|
+
url: string | null
|
|
34
|
+
stack_trace: string | null
|
|
35
|
+
metadata: string | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Project {
|
|
39
|
+
id: string
|
|
40
|
+
name: string
|
|
41
|
+
github_repo: string | null
|
|
42
|
+
base_url: string | null
|
|
43
|
+
description: string | null
|
|
44
|
+
github_description: string | null
|
|
45
|
+
github_branch: string | null
|
|
46
|
+
github_sha: string | null
|
|
47
|
+
last_synced_at: string | null
|
|
48
|
+
created_at: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface Page {
|
|
52
|
+
id: string
|
|
53
|
+
project_id: string
|
|
54
|
+
url: string
|
|
55
|
+
path: string
|
|
56
|
+
name: string | null
|
|
57
|
+
last_scanned_at: string | null
|
|
58
|
+
created_at: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ScanJob {
|
|
62
|
+
id: string
|
|
63
|
+
project_id: string
|
|
64
|
+
page_id: string | null
|
|
65
|
+
schedule: string
|
|
66
|
+
enabled: number
|
|
67
|
+
last_run_at: string | null
|
|
68
|
+
created_at: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ScanRun {
|
|
72
|
+
id: string
|
|
73
|
+
job_id: string
|
|
74
|
+
page_id: string | null
|
|
75
|
+
started_at: string
|
|
76
|
+
finished_at: string | null
|
|
77
|
+
status: "running" | "completed" | "failed"
|
|
78
|
+
logs_collected: number
|
|
79
|
+
errors_found: number
|
|
80
|
+
perf_score: number | null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface PerformanceSnapshot {
|
|
84
|
+
id: string
|
|
85
|
+
timestamp: string
|
|
86
|
+
project_id: string
|
|
87
|
+
page_id: string | null
|
|
88
|
+
url: string
|
|
89
|
+
lcp: number | null
|
|
90
|
+
fcp: number | null
|
|
91
|
+
cls: number | null
|
|
92
|
+
tti: number | null
|
|
93
|
+
ttfb: number | null
|
|
94
|
+
score: number | null
|
|
95
|
+
raw_audit: string | null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface LogQuery {
|
|
99
|
+
project_id?: string
|
|
100
|
+
page_id?: string
|
|
101
|
+
level?: LogLevel | LogLevel[]
|
|
102
|
+
service?: string
|
|
103
|
+
since?: string
|
|
104
|
+
until?: string
|
|
105
|
+
text?: string
|
|
106
|
+
trace_id?: string
|
|
107
|
+
limit?: number
|
|
108
|
+
offset?: number
|
|
109
|
+
fields?: string[]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface LogSummary {
|
|
113
|
+
project_id: string | null
|
|
114
|
+
service: string | null
|
|
115
|
+
page_id: string | null
|
|
116
|
+
level: LogLevel
|
|
117
|
+
count: number
|
|
118
|
+
latest: string
|
|
119
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "Preserve",
|
|
6
|
+
"moduleDetection": "force",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"noUncheckedIndexedAccess": true,
|
|
17
|
+
"noImplicitOverride": true,
|
|
18
|
+
"noUnusedLocals": false,
|
|
19
|
+
"noUnusedParameters": false,
|
|
20
|
+
"noPropertyAccessFromIndexSignature": false
|
|
21
|
+
}
|
|
22
|
+
}
|