UNPKG

@botpress/adk-cli

Version:

Command-line interface for the Botpress Agent Development Kit (ADK)

332 lines (325 loc) 9.39 kB
// @bun import { createCliLogger } from "./chunk-gzwt1qdr.js"; // src/utils/sqlite-log-store.ts import { Database } from "bun:sqlite"; import { mkdirSync } from "fs"; import { join as join2 } from "path"; // src/logs/log-reader.ts import { existsSync, readdirSync, statSync } from "fs"; import { join } from "path"; function normalizeLevel(type) { const lower = type.toLowerCase(); if (lower === "error") return "error"; if (lower === "warn" || lower === "warning") return "warning"; if (lower === "info" || lower === "stdout" || lower === "stderr" || lower === "log") return "info"; return lower; } function findLatestLogFile(agentRoot) { const logsDir = join(agentRoot, ".adk", "logs"); if (!existsSync(logsDir)) return null; try { const files = readdirSync(logsDir).filter((f) => f.startsWith("adk-dev-") && f.endsWith(".log")).map((f) => ({ name: f, path: join(logsDir, f), mtime: statSync(join(logsDir, f)).mtime.getTime() })).sort((a, b) => b.mtime - a.mtime); return files.length > 0 ? files[0].path : null; } catch { return null; } } function parseLine(line) { if (!line.trim()) return null; try { const parsed = JSON.parse(line); if (parsed.type === "worker_stats") return null; const msg = parsed.message || (Array.isArray(parsed.args) ? parsed.args.join(" ") : "") || ""; if (msg.includes("ExperimentalWarning") || msg.includes("punycode")) return null; return { timestamp: parsed.timestamp || new Date().toISOString(), type: parsed.type || "info", level: normalizeLevel(parsed.type || parsed.level || "info"), message: msg, raw: line, spanId: parsed.spanId, traceId: parsed.traceId }; } catch { return { timestamp: new Date().toISOString(), type: "raw", level: "raw", message: line, raw: line }; } } function matchesLevel(entry, level) { if (!level) return true; const l = entry.level; if (level === "error") return l === "error"; if (level === "warning") return l === "error" || l === "warning"; if (level === "info") return l === "error" || l === "warning" || l === "info"; return true; } function matchesSince(entry, since) { if (!since) return true; const ts = new Date(entry.timestamp).getTime(); return !isNaN(ts) && ts >= since; } function filterLogEntries(entries, options) { const limit = options.limit ?? 50; return entries.filter((entry) => matchesLevel(entry, options.level) && matchesSince(entry, options.since)).slice(-limit); } // src/utils/sqlite-log-store.ts var logger = createCliLogger({ tag: "sqlite-log-store" }); var FLUSH_DELAY_MS = 16; var FLUSH_MAX_BATCH = 250; var CHECKPOINT_INTERVAL_MS = 5000; var SCHEMA = ` CREATE TABLE IF NOT EXISTS logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts REAL NOT NULL, timestamp TEXT NOT NULL, type TEXT NOT NULL, level TEXT NOT NULL, message TEXT NOT NULL, span_id TEXT, trace_id TEXT, raw TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs(ts); CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level); CREATE INDEX IF NOT EXISTS idx_logs_trace ON logs(trace_id) WHERE trace_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_logs_span ON logs(span_id) WHERE span_id IS NOT NULL; `; function levelsFor(level) { if (level === "error") return ["error"]; if (level === "warning") return ["error", "warning"]; return ["error", "warning", "info"]; } function rowToEntry(row) { return { id: row.id, timestamp: row.timestamp, type: row.type, level: row.level, message: row.message, raw: row.raw, spanId: row.span_id ?? undefined, traceId: row.trace_id ?? undefined }; } class SqliteLogStore { db = null; dbPath; logsDir; insertStmt = null; writeQueue = []; flushTimer = null; checkpointTimer = null; constructor(agentPath) { this.logsDir = agentPath ? join2(agentPath, ".adk", "bot", "logs") : process.env.LOG_DIR ?? "logs"; this.dbPath = join2(this.logsDir, "logs.db"); } getDb() { if (this.db) return this.db; mkdirSync(this.logsDir, { recursive: true }); try { this.db = new Database(this.dbPath); this.db.run("PRAGMA journal_mode=WAL"); this.db.run("PRAGMA synchronous=NORMAL"); this.db.run("PRAGMA busy_timeout=5000"); this.db.run("PRAGMA wal_autocheckpoint=0"); this.db.run(SCHEMA); this.insertStmt = this.db.prepare(` INSERT INTO logs (ts, timestamp, type, level, message, span_id, trace_id, raw) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); this.startCheckpointTimer(); return this.db; } catch (err) { logger.warn(`Failed to initialize database: ${err instanceof Error ? err.message : String(err)}`); return null; } } clear() { this.writeQueue = []; if (this.flushTimer) { clearTimeout(this.flushTimer); this.flushTimer = null; } const db = this.getDb(); if (!db) return; db.run("DELETE FROM logs"); } insertLine(line) { const entry = parseLine(line); if (!entry) return; this.insertEntry(entry); } insertEntry(entry) { const db = this.getDb(); if (!db || !this.insertStmt) return; const parsedTs = Date.parse(entry.timestamp); const ts = Number.isNaN(parsedTs) ? Date.now() : parsedTs; this.insertStmt.run(ts, entry.timestamp, entry.type, entry.level, entry.message, entry.spanId ?? null, entry.traceId ?? null, entry.raw); } enqueueLine(line) { const entry = parseLine(line); if (!entry) return; this.writeQueue.push(entry); this.scheduleFlush(); } scheduleFlush() { if (this.writeQueue.length >= FLUSH_MAX_BATCH) { this.flush(); return; } if (this.flushTimer) return; this.flushTimer = setTimeout(() => { this.flushTimer = null; this.flush(); }, FLUSH_DELAY_MS); this.flushTimer.unref?.(); } flush() { if (this.writeQueue.length === 0) return; if (this.flushTimer) { clearTimeout(this.flushTimer); this.flushTimer = null; } const batch = this.writeQueue; this.writeQueue = []; const db = this.getDb(); if (!db) return; try { db.transaction((entries) => { for (const entry of entries) { try { this.insertEntry(entry); } catch (err) { logger.warn(`Failed to write log entry: ${err instanceof Error ? err.message : String(err)}`); } } })(batch); } catch (err) { logger.warn(`Log batch flush failed: ${err instanceof Error ? err.message : String(err)}`); } } startCheckpointTimer() { if (this.checkpointTimer) return; this.checkpointTimer = setInterval(() => { try { this.db?.run("PRAGMA wal_checkpoint(PASSIVE)"); } catch {} }, CHECKPOINT_INTERVAL_MS); this.checkpointTimer.unref?.(); } getRecentLogs(limit = 50) { return this.queryLogs({ limit }); } queryLogs(query) { this.flush(); const db = this.getDb(); if (!db) return []; const limit = query.limit ?? 50; if (limit <= 0) return []; const where = []; const params = []; if (query.level) { const levels = levelsFor(query.level); where.push(`level IN (${levels.map(() => "?").join(",")})`); params.push(...levels); } if (query.since != null) { where.push("ts >= ?"); params.push(query.since); } const whereSql = where.length > 0 ? `WHERE ${where.join(" AND ")}` : ""; const rows = db.prepare(`SELECT * FROM (SELECT * FROM logs ${whereSql} ORDER BY id DESC LIMIT ?) ORDER BY id ASC`).all(...params, limit); return rows.map(rowToEntry); } getMaxId() { this.flush(); const db = this.getDb(); if (!db) return 0; const row = db.prepare("SELECT MAX(id) as maxId FROM logs").get(); return row?.maxId ?? 0; } getLogsAfter(afterId, query = {}) { this.flush(); const db = this.getDb(); if (!db) return []; const where = ["id > ?"]; const params = [afterId]; if (query.level) { const levels = levelsFor(query.level); where.push(`level IN (${levels.map(() => "?").join(",")})`); params.push(...levels); } if (query.since != null) { where.push("ts >= ?"); params.push(query.since); } const rows = db.prepare(`SELECT * FROM logs WHERE ${where.join(" AND ")} ORDER BY id ASC`).all(...params); return rows.map(rowToEntry); } getLogsByTrace(traceId) { this.flush(); const db = this.getDb(); if (!db) return []; const rows = db.prepare("SELECT * FROM logs WHERE trace_id = ? ORDER BY id ASC").all(traceId); return rows.map(rowToEntry); } close() { if (this.flushTimer) { clearTimeout(this.flushTimer); this.flushTimer = null; } if (this.checkpointTimer) { clearInterval(this.checkpointTimer); this.checkpointTimer = null; } try { this.flush(); this.db?.run("PRAGMA wal_checkpoint(TRUNCATE)"); } catch {} try { this.db?.close(); this.db = null; this.insertStmt = null; } catch {} } } export { findLatestLogFile, filterLogEntries, SqliteLogStore };