mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
feat: implement structured logging system with UI controls
Implements core logging infrastructure for task #15 (Housekeeping.md). This is a foundational implementation that provides the backend and settings UI. Full log viewer UI remains as future work. ## Changes ### Main Process - Created `src/main/utils/logger.ts` with full logging system - Supports debug, info, warn, error log levels - Stores last 1000 log entries in memory - Filters based on configured minimum log level - Outputs to console with timestamps and context ### IPC Layer - Added logger API to preload.ts with 5 operations: - `logger:log` - Send log entry from renderer - `logger:getLogs` - Retrieve filtered logs - `logger:clearLogs` - Clear log storage - `logger:setLogLevel` - Update minimum log level - `logger:getLogLevel` - Get current log level - Added IPC handlers in main/index.ts - Log level persists to electron-store (maestro-settings.json) ### Renderer Process - Created `src/renderer/utils/logger.ts` for renderer logging - Integrated with useSettings hook for persistence - Added Log Level selector to Settings > General tab - Color-coded pills: Debug (indigo), Info (blue), Warn (amber), Error (red) - Setting persists across app restarts ## Remaining Work (Task #15) - Create LogViewer component with search functionality - Integrate LogViewer with Command-K menu - Replace 33 console.log/error calls with new logger - Add color-coded log display in viewer - Implement `/` search in log viewer ## Testing - Build verified successful - No TypeScript errors - Log level setting integrated with electron-store Related: tmp/Housekeeping.md #15
This commit is contained in:
@@ -5,6 +5,7 @@ import { ProcessManager } from './process-manager';
|
||||
import { WebServer } from './web-server';
|
||||
import { AgentDetector } from './agent-detector';
|
||||
import { execFileNoThrow } from './utils/execFile';
|
||||
import { logger } from './utils/logger';
|
||||
import Store from 'electron-store';
|
||||
|
||||
// Type definitions
|
||||
@@ -20,6 +21,7 @@ interface MaestroSettings {
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
customFonts: string[];
|
||||
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
||||
}
|
||||
|
||||
const store = new Store<MaestroSettings>({
|
||||
@@ -36,6 +38,7 @@ const store = new Store<MaestroSettings>({
|
||||
fontSize: 14,
|
||||
fontFamily: 'Roboto Mono, Menlo, "Courier New", monospace',
|
||||
customFonts: [],
|
||||
logLevel: 'info',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -106,6 +109,10 @@ app.whenReady().then(() => {
|
||||
webServer = new WebServer(8000);
|
||||
agentDetector = new AgentDetector();
|
||||
|
||||
// Load logger settings
|
||||
const logLevel = store.get('logLevel', 'info');
|
||||
logger.setLogLevel(logLevel);
|
||||
|
||||
// Set up IPC handlers
|
||||
setupIpcHandlers();
|
||||
|
||||
@@ -334,6 +341,48 @@ function setupIpcHandlers() {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Logger operations
|
||||
ipcMain.handle('logger:log', async (_event, level: string, message: string, context?: string, data?: unknown) => {
|
||||
const logLevel = level as 'debug' | 'info' | 'warn' | 'error';
|
||||
switch (logLevel) {
|
||||
case 'debug':
|
||||
logger.debug(message, context, data);
|
||||
break;
|
||||
case 'info':
|
||||
logger.info(message, context, data);
|
||||
break;
|
||||
case 'warn':
|
||||
logger.warn(message, context, data);
|
||||
break;
|
||||
case 'error':
|
||||
logger.error(message, context, data);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('logger:getLogs', async (_event, filter?: { level?: string; context?: string; limit?: number }) => {
|
||||
const typedFilter = filter ? {
|
||||
level: filter.level as 'debug' | 'info' | 'warn' | 'error' | undefined,
|
||||
context: filter.context,
|
||||
limit: filter.limit,
|
||||
} : undefined;
|
||||
return logger.getLogs(typedFilter);
|
||||
});
|
||||
|
||||
ipcMain.handle('logger:clearLogs', async () => {
|
||||
logger.clearLogs();
|
||||
});
|
||||
|
||||
ipcMain.handle('logger:setLogLevel', async (_event, level: string) => {
|
||||
const logLevel = level as 'debug' | 'info' | 'warn' | 'error';
|
||||
logger.setLogLevel(logLevel);
|
||||
store.set('logLevel', logLevel);
|
||||
});
|
||||
|
||||
ipcMain.handle('logger:getLogLevel', async () => {
|
||||
return logger.getLogLevel();
|
||||
});
|
||||
}
|
||||
|
||||
// Handle process output streaming (set up after initialization)
|
||||
|
||||
@@ -106,6 +106,17 @@ contextBridge.exposeInMainWorld('maestro', {
|
||||
close: () => ipcRenderer.invoke('devtools:close'),
|
||||
toggle: () => ipcRenderer.invoke('devtools:toggle'),
|
||||
},
|
||||
|
||||
// Logger API
|
||||
logger: {
|
||||
log: (level: string, message: string, context?: string, data?: unknown) =>
|
||||
ipcRenderer.invoke('logger:log', level, message, context, data),
|
||||
getLogs: (filter?: { level?: string; context?: string; limit?: number }) =>
|
||||
ipcRenderer.invoke('logger:getLogs', filter),
|
||||
clearLogs: () => ipcRenderer.invoke('logger:clearLogs'),
|
||||
setLogLevel: (level: string) => ipcRenderer.invoke('logger:setLogLevel', level),
|
||||
getLogLevel: () => ipcRenderer.invoke('logger:getLogLevel'),
|
||||
},
|
||||
});
|
||||
|
||||
// Type definitions for TypeScript
|
||||
@@ -161,6 +172,19 @@ export interface MaestroAPI {
|
||||
close: () => Promise<void>;
|
||||
toggle: () => Promise<void>;
|
||||
};
|
||||
logger: {
|
||||
log: (level: string, message: string, context?: string, data?: unknown) => Promise<void>;
|
||||
getLogs: (filter?: { level?: string; context?: string; limit?: number }) => Promise<Array<{
|
||||
timestamp: number;
|
||||
level: string;
|
||||
message: string;
|
||||
context?: string;
|
||||
data?: unknown;
|
||||
}>>;
|
||||
clearLogs: () => Promise<void>;
|
||||
setLogLevel: (level: string) => Promise<void>;
|
||||
getLogLevel: () => Promise<string>;
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
138
src/main/utils/logger.ts
Normal file
138
src/main/utils/logger.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Structured logging utility for the main process
|
||||
* Logs are stored in memory and can be retrieved via IPC
|
||||
*/
|
||||
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: number;
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
context?: string;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
class Logger {
|
||||
private logs: LogEntry[] = [];
|
||||
private maxLogs = 1000; // Keep last 1000 log entries
|
||||
private minLevel: LogLevel = 'info'; // Default log level
|
||||
|
||||
private levelPriority: Record<LogLevel, number> = {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3,
|
||||
};
|
||||
|
||||
setLogLevel(level: LogLevel): void {
|
||||
this.minLevel = level;
|
||||
}
|
||||
|
||||
getLogLevel(): LogLevel {
|
||||
return this.minLevel;
|
||||
}
|
||||
|
||||
private shouldLog(level: LogLevel): boolean {
|
||||
return this.levelPriority[level] >= this.levelPriority[this.minLevel];
|
||||
}
|
||||
|
||||
private addLog(entry: LogEntry): void {
|
||||
this.logs.push(entry);
|
||||
|
||||
// Keep only the last maxLogs entries
|
||||
if (this.logs.length > this.maxLogs) {
|
||||
this.logs = this.logs.slice(-this.maxLogs);
|
||||
}
|
||||
|
||||
// Also output to console for development
|
||||
const timestamp = new Date(entry.timestamp).toISOString();
|
||||
const prefix = `[${timestamp}] [${entry.level.toUpperCase()}]${entry.context ? ` [${entry.context}]` : ''}`;
|
||||
const message = `${prefix} ${entry.message}`;
|
||||
|
||||
switch (entry.level) {
|
||||
case 'error':
|
||||
console.error(message, entry.data || '');
|
||||
break;
|
||||
case 'warn':
|
||||
console.warn(message, entry.data || '');
|
||||
break;
|
||||
case 'info':
|
||||
console.info(message, entry.data || '');
|
||||
break;
|
||||
case 'debug':
|
||||
console.log(message, entry.data || '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
debug(message: string, context?: string, data?: unknown): void {
|
||||
if (!this.shouldLog('debug')) return;
|
||||
this.addLog({
|
||||
timestamp: Date.now(),
|
||||
level: 'debug',
|
||||
message,
|
||||
context,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
info(message: string, context?: string, data?: unknown): void {
|
||||
if (!this.shouldLog('info')) return;
|
||||
this.addLog({
|
||||
timestamp: Date.now(),
|
||||
level: 'info',
|
||||
message,
|
||||
context,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
warn(message: string, context?: string, data?: unknown): void {
|
||||
if (!this.shouldLog('warn')) return;
|
||||
this.addLog({
|
||||
timestamp: Date.now(),
|
||||
level: 'warn',
|
||||
message,
|
||||
context,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
error(message: string, context?: string, data?: unknown): void {
|
||||
if (!this.shouldLog('error')) return;
|
||||
this.addLog({
|
||||
timestamp: Date.now(),
|
||||
level: 'error',
|
||||
message,
|
||||
context,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
getLogs(filter?: { level?: LogLevel; context?: string; limit?: number }): LogEntry[] {
|
||||
let filtered = [...this.logs];
|
||||
|
||||
if (filter?.level) {
|
||||
const minPriority = this.levelPriority[filter.level];
|
||||
filtered = filtered.filter(log => this.levelPriority[log.level] >= minPriority);
|
||||
}
|
||||
|
||||
if (filter?.context) {
|
||||
filtered = filtered.filter(log => log.context === filter.context);
|
||||
}
|
||||
|
||||
if (filter?.limit) {
|
||||
filtered = filtered.slice(-filter.limit);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
clearLogs(): void {
|
||||
this.logs = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const logger = new Logger();
|
||||
@@ -27,6 +27,8 @@ interface SettingsModalProps {
|
||||
setFontFamily: (font: string) => void;
|
||||
fontSize: number;
|
||||
setFontSize: (size: number) => void;
|
||||
logLevel: string;
|
||||
setLogLevel: (level: string) => void;
|
||||
initialTab?: 'general' | 'llm' | 'shortcuts' | 'theme' | 'network';
|
||||
}
|
||||
|
||||
@@ -596,6 +598,64 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Log Level */}
|
||||
<div>
|
||||
<label className="block text-xs font-bold opacity-70 uppercase mb-2">System Log Level</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => props.setLogLevel('debug')}
|
||||
className={`flex-1 py-2 px-3 rounded border transition-all ${props.logLevel === 'debug' ? 'ring-2' : ''}`}
|
||||
style={{
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: props.logLevel === 'debug' ? '#6366f1' : 'transparent',
|
||||
ringColor: '#6366f1',
|
||||
color: props.logLevel === 'debug' ? 'white' : theme.colors.textMain
|
||||
}}
|
||||
>
|
||||
Debug
|
||||
</button>
|
||||
<button
|
||||
onClick={() => props.setLogLevel('info')}
|
||||
className={`flex-1 py-2 px-3 rounded border transition-all ${props.logLevel === 'info' ? 'ring-2' : ''}`}
|
||||
style={{
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: props.logLevel === 'info' ? '#3b82f6' : 'transparent',
|
||||
ringColor: '#3b82f6',
|
||||
color: props.logLevel === 'info' ? 'white' : theme.colors.textMain
|
||||
}}
|
||||
>
|
||||
Info
|
||||
</button>
|
||||
<button
|
||||
onClick={() => props.setLogLevel('warn')}
|
||||
className={`flex-1 py-2 px-3 rounded border transition-all ${props.logLevel === 'warn' ? 'ring-2' : ''}`}
|
||||
style={{
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: props.logLevel === 'warn' ? '#f59e0b' : 'transparent',
|
||||
ringColor: '#f59e0b',
|
||||
color: props.logLevel === 'warn' ? 'white' : theme.colors.textMain
|
||||
}}
|
||||
>
|
||||
Warn
|
||||
</button>
|
||||
<button
|
||||
onClick={() => props.setLogLevel('error')}
|
||||
className={`flex-1 py-2 px-3 rounded border transition-all ${props.logLevel === 'error' ? 'ring-2' : ''}`}
|
||||
style={{
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: props.logLevel === 'error' ? '#ef4444' : 'transparent',
|
||||
ringColor: '#ef4444',
|
||||
color: props.logLevel === 'error' ? 'white' : theme.colors.textMain
|
||||
}}
|
||||
>
|
||||
Error
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs opacity-50 mt-2">
|
||||
Higher levels show fewer logs. Debug shows all logs, Error shows only errors.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ export interface UseSettingsReturn {
|
||||
setRightPanelWidth: (value: number) => void;
|
||||
setMarkdownRawMode: (value: boolean) => void;
|
||||
|
||||
// Logging settings
|
||||
logLevel: string;
|
||||
setLogLevel: (value: string) => void;
|
||||
|
||||
// Shortcuts
|
||||
shortcuts: Record<string, Shortcut>;
|
||||
setShortcuts: (value: Record<string, Shortcut>) => void;
|
||||
@@ -71,6 +75,9 @@ export function useSettings(): UseSettingsReturn {
|
||||
const [rightPanelWidth, setRightPanelWidthState] = useState(384);
|
||||
const [markdownRawMode, setMarkdownRawModeState] = useState(false);
|
||||
|
||||
// Logging Config
|
||||
const [logLevel, setLogLevelState] = useState('info');
|
||||
|
||||
// Shortcuts
|
||||
const [shortcuts, setShortcutsState] = useState<Record<string, Shortcut>>(DEFAULT_SHORTCUTS);
|
||||
|
||||
@@ -150,6 +157,11 @@ export function useSettings(): UseSettingsReturn {
|
||||
window.maestro.settings.set('shortcuts', value);
|
||||
};
|
||||
|
||||
const setLogLevel = async (value: string) => {
|
||||
setLogLevelState(value);
|
||||
await window.maestro.logger.setLogLevel(value);
|
||||
};
|
||||
|
||||
// Load settings from electron-store on mount
|
||||
useEffect(() => {
|
||||
const loadSettings = async () => {
|
||||
@@ -168,6 +180,7 @@ export function useSettings(): UseSettingsReturn {
|
||||
const savedMarkdownRawMode = await window.maestro.settings.get('markdownRawMode');
|
||||
const savedShortcuts = await window.maestro.settings.get('shortcuts');
|
||||
const savedActiveThemeId = await window.maestro.settings.get('activeThemeId');
|
||||
const savedLogLevel = await window.maestro.logger.getLogLevel();
|
||||
|
||||
if (savedEnterToSend !== undefined) setEnterToSendState(savedEnterToSend);
|
||||
if (savedLlmProvider !== undefined) setLlmProviderState(savedLlmProvider);
|
||||
@@ -183,6 +196,7 @@ export function useSettings(): UseSettingsReturn {
|
||||
if (savedRightPanelWidth !== undefined) setRightPanelWidthState(savedRightPanelWidth);
|
||||
if (savedMarkdownRawMode !== undefined) setMarkdownRawModeState(savedMarkdownRawMode);
|
||||
if (savedActiveThemeId !== undefined) setActiveThemeIdState(savedActiveThemeId);
|
||||
if (savedLogLevel !== undefined) setLogLevelState(savedLogLevel);
|
||||
|
||||
// Merge saved shortcuts with defaults (in case new shortcuts were added)
|
||||
if (savedShortcuts !== undefined) {
|
||||
@@ -226,6 +240,8 @@ export function useSettings(): UseSettingsReturn {
|
||||
setLeftSidebarWidth,
|
||||
setRightPanelWidth,
|
||||
setMarkdownRawMode,
|
||||
logLevel,
|
||||
setLogLevel,
|
||||
shortcuts,
|
||||
setShortcuts,
|
||||
};
|
||||
|
||||
27
src/renderer/utils/logger.ts
Normal file
27
src/renderer/utils/logger.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Structured logging utility for the renderer process
|
||||
* Sends logs to the main process via IPC
|
||||
*/
|
||||
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
class RendererLogger {
|
||||
debug(message: string, context?: string, data?: unknown): void {
|
||||
window.maestro?.logger?.log('debug', message, context, data);
|
||||
}
|
||||
|
||||
info(message: string, context?: string, data?: unknown): void {
|
||||
window.maestro?.logger?.log('info', message, context, data);
|
||||
}
|
||||
|
||||
warn(message: string, context?: string, data?: unknown): void {
|
||||
window.maestro?.logger?.log('warn', message, context, data);
|
||||
}
|
||||
|
||||
error(message: string, context?: string, data?: unknown): void {
|
||||
window.maestro?.logger?.log('error', message, context, data);
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const logger = new RendererLogger();
|
||||
Reference in New Issue
Block a user