generated from nhcarrigan/template
feat: add debug console for frontend and backend logs
Added a comprehensive debug console feature that captures and displays logs from both the frontend and backend in a unified interface. Frontend changes: - Created DebugConsole component with real-time log display - Added debugConsoleStore for state management and console capture - Integrated console into main layout - Added toggle button in StatusBar with console icon - Implemented Ctrl+` keyboard shortcut to open/close console - Features: log filtering by level, auto-scroll, timestamps, colour-coding Backend changes: - Added tracing and tracing-subscriber dependencies - Created custom TauriLogLayer to emit Rust logs to frontend - Integrated tracing subscriber in lib.rs setup - Logs are forwarded via Tauri events (debug:log) Key features: - Circular buffer (max 1000 logs) prevents memory issues - Frontend logs captured via console method overrides - Backend logs forwarded from Rust tracing layer - Log level filtering (debug, info, warn, error, all) - Source badges distinguish frontend vs backend logs - Colour-coded log levels for easy identification - Auto-scroll toggle for inspecting older logs - Clear logs button for resetting the console - Beautiful dark-themed UI matching app aesthetic Closes #126
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import { writable, derived } from "svelte/store";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
export interface LogEntry {
|
||||
id: string;
|
||||
timestamp: Date;
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
source: "frontend" | "backend";
|
||||
}
|
||||
|
||||
interface DebugConsoleState {
|
||||
logs: LogEntry[];
|
||||
isOpen: boolean;
|
||||
maxLogs: number;
|
||||
filterLevel: LogLevel | "all";
|
||||
autoScroll: boolean;
|
||||
}
|
||||
|
||||
const MAX_LOGS = 1000; // Circular buffer size
|
||||
|
||||
function createDebugConsoleStore() {
|
||||
const { subscribe, update } = writable<DebugConsoleState>({
|
||||
logs: [],
|
||||
isOpen: false,
|
||||
maxLogs: MAX_LOGS,
|
||||
filterLevel: "all",
|
||||
autoScroll: true,
|
||||
});
|
||||
|
||||
let logCounter = 0;
|
||||
|
||||
function addLog(level: LogLevel, message: string, source: "frontend" | "backend") {
|
||||
update((state) => {
|
||||
const newLog: LogEntry = {
|
||||
id: `log-${Date.now()}-${logCounter++}`,
|
||||
timestamp: new Date(),
|
||||
level,
|
||||
message,
|
||||
source,
|
||||
};
|
||||
|
||||
const updatedLogs = [...state.logs, newLog];
|
||||
|
||||
// Implement circular buffer - remove oldest if exceeding max
|
||||
if (updatedLogs.length > state.maxLogs) {
|
||||
updatedLogs.shift();
|
||||
}
|
||||
|
||||
return { ...state, logs: updatedLogs };
|
||||
});
|
||||
}
|
||||
|
||||
// Override console methods to capture frontend logs
|
||||
const originalConsole = {
|
||||
log: console.log,
|
||||
info: console.info,
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
debug: console.debug,
|
||||
};
|
||||
|
||||
function setupConsoleCapture() {
|
||||
console.log = (...args: unknown[]) => {
|
||||
originalConsole.log(...args);
|
||||
addLog("info", args.map((arg) => String(arg)).join(" "), "frontend");
|
||||
};
|
||||
|
||||
console.info = (...args: unknown[]) => {
|
||||
originalConsole.info(...args);
|
||||
addLog("info", args.map((arg) => String(arg)).join(" "), "frontend");
|
||||
};
|
||||
|
||||
console.warn = (...args: unknown[]) => {
|
||||
originalConsole.warn(...args);
|
||||
addLog("warn", args.map((arg) => String(arg)).join(" "), "frontend");
|
||||
};
|
||||
|
||||
console.error = (...args: unknown[]) => {
|
||||
originalConsole.error(...args);
|
||||
addLog("error", args.map((arg) => String(arg)).join(" "), "frontend");
|
||||
};
|
||||
|
||||
console.debug = (...args: unknown[]) => {
|
||||
originalConsole.debug(...args);
|
||||
addLog("debug", args.map((arg) => String(arg)).join(" "), "frontend");
|
||||
};
|
||||
}
|
||||
|
||||
function restoreConsole() {
|
||||
console.log = originalConsole.log;
|
||||
console.info = originalConsole.info;
|
||||
console.warn = originalConsole.warn;
|
||||
console.error = originalConsole.error;
|
||||
console.debug = originalConsole.debug;
|
||||
}
|
||||
|
||||
// Listen for backend logs
|
||||
async function setupBackendLogsListener() {
|
||||
await listen<{ level: LogLevel; message: string }>("debug:log", (event) => {
|
||||
addLog(event.payload.level, event.payload.message, "backend");
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
toggle: () => update((state) => ({ ...state, isOpen: !state.isOpen })),
|
||||
open: () => update((state) => ({ ...state, isOpen: true })),
|
||||
close: () => update((state) => ({ ...state, isOpen: false })),
|
||||
clear: () => update((state) => ({ ...state, logs: [] })),
|
||||
setFilterLevel: (level: LogLevel | "all") =>
|
||||
update((state) => ({ ...state, filterLevel: level })),
|
||||
setAutoScroll: (enabled: boolean) => update((state) => ({ ...state, autoScroll: enabled })),
|
||||
setupConsoleCapture,
|
||||
restoreConsole,
|
||||
setupBackendLogsListener,
|
||||
};
|
||||
}
|
||||
|
||||
export const debugConsoleStore = createDebugConsoleStore();
|
||||
|
||||
// Derived store for filtered logs
|
||||
export const filteredLogs = derived(debugConsoleStore, ($debugConsole) => {
|
||||
if ($debugConsole.filterLevel === "all") {
|
||||
return $debugConsole.logs;
|
||||
}
|
||||
|
||||
const levelPriority: Record<LogLevel, number> = {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3,
|
||||
};
|
||||
|
||||
const minPriority = levelPriority[$debugConsole.filterLevel];
|
||||
|
||||
return $debugConsole.logs.filter((log) => levelPriority[log.level] >= minPriority);
|
||||
});
|
||||
Reference in New Issue
Block a user