@hasna/logs 0.1.0 → 0.2.0
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/dashboard/README.md +73 -0
- package/dashboard/bun.lock +526 -0
- package/dashboard/eslint.config.js +23 -0
- package/dashboard/index.html +13 -0
- package/dashboard/package.json +32 -0
- package/dashboard/public/favicon.svg +1 -0
- package/dashboard/public/icons.svg +24 -0
- package/dashboard/src/App.css +184 -0
- package/dashboard/src/App.tsx +49 -0
- package/dashboard/src/api.ts +33 -0
- package/dashboard/src/assets/hero.png +0 -0
- package/dashboard/src/assets/react.svg +1 -0
- package/dashboard/src/assets/vite.svg +1 -0
- package/dashboard/src/index.css +111 -0
- package/dashboard/src/main.tsx +10 -0
- package/dashboard/src/pages/Alerts.tsx +69 -0
- package/dashboard/src/pages/Issues.tsx +50 -0
- package/dashboard/src/pages/Perf.tsx +75 -0
- package/dashboard/src/pages/Projects.tsx +67 -0
- package/dashboard/src/pages/Summary.tsx +67 -0
- package/dashboard/src/pages/Tail.tsx +65 -0
- package/dashboard/tsconfig.app.json +28 -0
- package/dashboard/tsconfig.json +7 -0
- package/dashboard/tsconfig.node.json +26 -0
- package/dashboard/vite.config.ts +14 -0
- package/dist/cli/index.js +80 -9
- package/dist/mcp/index.js +63 -35
- package/dist/server/index.js +281 -6
- package/package.json +3 -1
- package/sdk/package.json +3 -2
- package/sdk/src/index.ts +1 -1
- package/sdk/src/types.ts +56 -0
- package/src/cli/index.ts +70 -4
- package/src/lib/session-context.ts +28 -0
- package/src/mcp/index.ts +10 -0
- package/src/server/index.ts +4 -1
package/sdk/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LogEntry, LogLevel, LogQuery, LogRow, LogSummary, Page, PerformanceSnapshot, Project, ScanJob } from "
|
|
1
|
+
import type { LogEntry, LogLevel, LogQuery, LogRow, LogSummary, Page, PerformanceSnapshot, Project, ScanJob } from "./types.ts"
|
|
2
2
|
|
|
3
3
|
export type { LogEntry, LogLevel, LogQuery, LogRow, LogSummary, Page, PerformanceSnapshot, Project, ScanJob }
|
|
4
4
|
|
package/sdk/src/types.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error" | "fatal"
|
|
2
|
+
export type LogSource = "sdk" | "script" | "scanner"
|
|
3
|
+
|
|
4
|
+
export interface LogEntry {
|
|
5
|
+
level: LogLevel
|
|
6
|
+
message: string
|
|
7
|
+
project_id?: string
|
|
8
|
+
page_id?: string
|
|
9
|
+
source?: LogSource
|
|
10
|
+
service?: string
|
|
11
|
+
trace_id?: string
|
|
12
|
+
session_id?: string
|
|
13
|
+
agent?: string
|
|
14
|
+
url?: string
|
|
15
|
+
stack_trace?: string
|
|
16
|
+
metadata?: Record<string, unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LogRow {
|
|
20
|
+
id: string; timestamp: string; project_id: string | null; page_id: string | null
|
|
21
|
+
level: LogLevel; source: LogSource; service: string | null; message: string
|
|
22
|
+
trace_id: string | null; session_id: string | null; agent: string | null
|
|
23
|
+
url: string | null; stack_trace: string | null; metadata: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Project {
|
|
27
|
+
id: string; name: string; github_repo: string | null; base_url: string | null
|
|
28
|
+
description: string | null; created_at: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Page {
|
|
32
|
+
id: string; project_id: string; url: string; path: string
|
|
33
|
+
name: string | null; last_scanned_at: string | null; created_at: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ScanJob {
|
|
37
|
+
id: string; project_id: string; page_id: string | null
|
|
38
|
+
schedule: string; enabled: number; last_run_at: string | null; created_at: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PerformanceSnapshot {
|
|
42
|
+
id: string; timestamp: string; project_id: string; page_id: string | null
|
|
43
|
+
url: string; lcp: number | null; fcp: number | null; cls: number | null
|
|
44
|
+
tti: number | null; ttfb: number | null; score: number | null; raw_audit: string | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LogQuery {
|
|
48
|
+
project_id?: string; page_id?: string; level?: LogLevel | LogLevel[]
|
|
49
|
+
service?: string; since?: string; until?: string; text?: string
|
|
50
|
+
trace_id?: string; limit?: number; offset?: number; fields?: string[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LogSummary {
|
|
54
|
+
project_id: string | null; service: string | null; page_id: string | null
|
|
55
|
+
level: LogLevel; count: number; latest: string
|
|
56
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -151,6 +151,58 @@ program.command("scan")
|
|
|
151
151
|
console.log("Scan complete.")
|
|
152
152
|
})
|
|
153
153
|
|
|
154
|
+
// ── logs watch ────────────────────────────────────────────
|
|
155
|
+
program.command("watch")
|
|
156
|
+
.description("Stream new logs in real time with color coding")
|
|
157
|
+
.option("--project <id>")
|
|
158
|
+
.option("--level <levels>", "Comma-separated levels")
|
|
159
|
+
.option("--service <name>")
|
|
160
|
+
.action(async (opts) => {
|
|
161
|
+
const db = getDb()
|
|
162
|
+
const { searchLogs } = await import("../lib/query.ts")
|
|
163
|
+
|
|
164
|
+
const COLORS: Record<string, string> = {
|
|
165
|
+
debug: "\x1b[90m", info: "\x1b[36m", warn: "\x1b[33m", error: "\x1b[31m", fatal: "\x1b[35m",
|
|
166
|
+
}
|
|
167
|
+
const RESET = "\x1b[0m"
|
|
168
|
+
const BOLD = "\x1b[1m"
|
|
169
|
+
|
|
170
|
+
let lastTimestamp = new Date().toISOString()
|
|
171
|
+
let errorCount = 0
|
|
172
|
+
let warnCount = 0
|
|
173
|
+
|
|
174
|
+
process.stdout.write(`\x1b[2J\x1b[H`) // clear screen
|
|
175
|
+
console.log(`${BOLD}@hasna/logs watch${RESET} — Ctrl+C to exit\n`)
|
|
176
|
+
|
|
177
|
+
const poll = () => {
|
|
178
|
+
const rows = searchLogs(db, {
|
|
179
|
+
project_id: opts.project,
|
|
180
|
+
level: opts.level ? (opts.level.split(",") as LogLevel[]) : undefined,
|
|
181
|
+
service: opts.service,
|
|
182
|
+
since: lastTimestamp,
|
|
183
|
+
limit: 100,
|
|
184
|
+
}).reverse()
|
|
185
|
+
|
|
186
|
+
for (const row of rows) {
|
|
187
|
+
if (row.timestamp <= lastTimestamp) continue
|
|
188
|
+
lastTimestamp = row.timestamp
|
|
189
|
+
if (row.level === "error" || row.level === "fatal") errorCount++
|
|
190
|
+
if (row.level === "warn") warnCount++
|
|
191
|
+
const color = COLORS[row.level] ?? ""
|
|
192
|
+
const ts = row.timestamp.slice(11, 19)
|
|
193
|
+
const svc = (row.service ?? "-").padEnd(12)
|
|
194
|
+
const lvl = row.level.toUpperCase().padEnd(5)
|
|
195
|
+
console.log(`${color}${ts} ${BOLD}${lvl}${RESET}${color} ${svc} ${row.message}${RESET}`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Update terminal title with counts
|
|
199
|
+
process.stdout.write(`\x1b]2;logs: ${errorCount}E ${warnCount}W\x07`)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const interval = setInterval(poll, 500)
|
|
203
|
+
process.on("SIGINT", () => { clearInterval(interval); console.log(`\n\nErrors: ${errorCount} Warnings: ${warnCount}`); process.exit(0) })
|
|
204
|
+
})
|
|
205
|
+
|
|
154
206
|
// ── logs export ───────────────────────────────────────────
|
|
155
207
|
program.command("export")
|
|
156
208
|
.description("Export logs to JSON or CSV")
|
|
@@ -203,11 +255,25 @@ program.command("mcp")
|
|
|
203
255
|
.option("--gemini", "Install into Gemini")
|
|
204
256
|
.action(async (opts) => {
|
|
205
257
|
if (opts.claude || opts.codex || opts.gemini) {
|
|
206
|
-
const
|
|
207
|
-
|
|
258
|
+
const { execSync } = await import("node:child_process")
|
|
259
|
+
// Resolve the MCP binary path — works from both source and dist
|
|
260
|
+
const selfPath = process.argv[1] ?? new URL(import.meta.url).pathname
|
|
261
|
+
const mcpBin = selfPath.replace(/cli\/index\.(ts|js)$/, "mcp/index.$1")
|
|
262
|
+
const runtime = process.execPath // bun or node
|
|
263
|
+
|
|
208
264
|
if (opts.claude) {
|
|
209
|
-
const
|
|
210
|
-
|
|
265
|
+
const cmd = `claude mcp add --transport stdio --scope user logs -- ${runtime} ${mcpBin}`
|
|
266
|
+
console.log(`Running: ${cmd}`)
|
|
267
|
+
execSync(cmd, { stdio: "inherit" })
|
|
268
|
+
console.log("✓ Installed logs-mcp into Claude Code")
|
|
269
|
+
}
|
|
270
|
+
if (opts.codex) {
|
|
271
|
+
const config = `[mcp_servers.logs]\ncommand = "${runtime}"\nargs = ["${mcpBin}"]`
|
|
272
|
+
console.log("Add to ~/.codex/config.toml:\n\n" + config)
|
|
273
|
+
}
|
|
274
|
+
if (opts.gemini) {
|
|
275
|
+
const config = JSON.stringify({ mcpServers: { logs: { command: runtime, args: [mcpBin] } } }, null, 2)
|
|
276
|
+
console.log("Add to ~/.gemini/settings.json mcpServers:\n\n" + config)
|
|
211
277
|
}
|
|
212
278
|
return
|
|
213
279
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import type { LogRow } from "../types/index.ts"
|
|
3
|
+
|
|
4
|
+
export interface SessionContext {
|
|
5
|
+
session_id: string
|
|
6
|
+
logs: LogRow[]
|
|
7
|
+
session?: Record<string, unknown>
|
|
8
|
+
error?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function getSessionContext(db: Database, sessionId: string): Promise<SessionContext> {
|
|
12
|
+
const logs = db.prepare("SELECT * FROM logs WHERE session_id = $s ORDER BY timestamp ASC")
|
|
13
|
+
.all({ $s: sessionId }) as LogRow[]
|
|
14
|
+
|
|
15
|
+
const sessionsUrl = process.env.SESSIONS_URL
|
|
16
|
+
if (!sessionsUrl) {
|
|
17
|
+
return { session_id: sessionId, logs }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(`${sessionsUrl.replace(/\/$/, "")}/api/sessions/${sessionId}`)
|
|
22
|
+
if (!res.ok) return { session_id: sessionId, logs }
|
|
23
|
+
const session = await res.json() as Record<string, unknown>
|
|
24
|
+
return { session_id: sessionId, logs, session }
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return { session_id: sessionId, logs, error: String(err) }
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/mcp/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { listIssues, updateIssueStatus } from "../lib/issues.ts"
|
|
|
14
14
|
import { diagnose } from "../lib/diagnose.ts"
|
|
15
15
|
import { compare } from "../lib/compare.ts"
|
|
16
16
|
import { getHealth } from "../lib/health.ts"
|
|
17
|
+
import { getSessionContext } from "../lib/session-context.ts"
|
|
17
18
|
import type { LogLevel, LogRow } from "../types/index.ts"
|
|
18
19
|
|
|
19
20
|
const db = getDb()
|
|
@@ -47,6 +48,7 @@ const TOOLS: Record<string, string> = {
|
|
|
47
48
|
create_alert_rule: "Create alert rule (project_id, name, level, threshold_count, window_seconds, webhook_url?)",
|
|
48
49
|
list_alert_rules: "List alert rules (project_id?)",
|
|
49
50
|
delete_alert_rule: "Delete alert rule (id)",
|
|
51
|
+
log_session_context: "Logs + session metadata for a session_id (requires SESSIONS_URL env)",
|
|
50
52
|
get_health: "Server health + DB stats",
|
|
51
53
|
search_tools: "Search tools by keyword (query)",
|
|
52
54
|
describe_tools: "List all tools",
|
|
@@ -191,6 +193,14 @@ server.tool("delete_alert_rule", { id: z.string() }, ({ id }) => {
|
|
|
191
193
|
return { content: [{ type: "text", text: "deleted" }] }
|
|
192
194
|
})
|
|
193
195
|
|
|
196
|
+
server.tool("log_session_context", {
|
|
197
|
+
session_id: z.string(),
|
|
198
|
+
brief: z.boolean().optional(),
|
|
199
|
+
}, async ({ session_id, brief }) => {
|
|
200
|
+
const ctx = await getSessionContext(db, session_id)
|
|
201
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...ctx, logs: applyBrief(ctx.logs, brief !== false) }) }] }
|
|
202
|
+
})
|
|
203
|
+
|
|
194
204
|
server.tool("get_health", {}, () => ({
|
|
195
205
|
content: [{ type: "text", text: JSON.stringify(getHealth(db)) }]
|
|
196
206
|
}))
|
package/src/server/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Hono } from "hono"
|
|
3
3
|
import { cors } from "hono/cors"
|
|
4
|
+
import { serveStatic } from "hono/bun"
|
|
4
5
|
import { getDb } from "../db/index.ts"
|
|
5
6
|
import { getBrowserScript } from "../lib/browser-script.ts"
|
|
6
7
|
import { getHealth } from "../lib/health.ts"
|
|
@@ -37,7 +38,9 @@ app.route("/api/issues", issuesRoutes(db))
|
|
|
37
38
|
app.route("/api/perf", perfRoutes(db))
|
|
38
39
|
|
|
39
40
|
app.get("/health", (c) => c.json(getHealth(db)))
|
|
40
|
-
app.get("/", (c) => c.
|
|
41
|
+
app.get("/dashboard", (c) => c.redirect("/dashboard/"))
|
|
42
|
+
app.use("/dashboard/*", serveStatic({ root: "./dashboard/dist", rewriteRequestPath: (p) => p.replace(/^\/dashboard/, "") }))
|
|
43
|
+
app.get("/", (c) => c.json({ service: "@hasna/logs", port: PORT, status: "ok", dashboard: `http://localhost:${PORT}/dashboard/` }))
|
|
41
44
|
|
|
42
45
|
// Start scheduler
|
|
43
46
|
startScheduler(db)
|