Browse Source
- Add Winston-based structured logging system with: - Separate console and file output formats - Custom SQLite and migration loggers - Configurable log levels and verbosity - Log rotation and file management - Type-safe logger extensions - Improve IPC handler management: - Add handler registration tracking - Implement proper cleanup before re-registration - Fix handler registration conflicts - Add better error handling for IPC operations - Add migration logging controls: - Configurable via DEBUG_MIGRATIONS env var - Reduced console noise while maintaining file logs - Structured migration status reporting Security: - Add proper log file permissions (0o755) - Implement log rotation to prevent disk space issues - Add type safety for all logging operations - Prevent handler registration conflicts Dependencies: - Add winston for enhanced logging - Remove deprecated @types/winston This change improves debugging capabilities while reducing console noise and fixing IPC handler registration issues that could cause database operation failures.sql-absurd-sql-further
5 changed files with 571 additions and 280 deletions
@ -1,77 +1,140 @@ |
|||
/** |
|||
* Structured logging system for TimeSafari |
|||
* Enhanced logging system for TimeSafari Electron |
|||
* Provides structured logging with proper levels and formatting |
|||
* Supports both console and file output with different verbosity levels |
|||
* |
|||
* Provides consistent logging across the application with: |
|||
* - Timestamp tracking |
|||
* - Log levels (debug, info, warn, error) |
|||
* - Structured data support |
|||
* - Component tagging |
|||
* |
|||
* @author Matthew Raymer <matthew.raymer@anomalistdesign.com> |
|||
* @version 1.0.0 |
|||
* @since 2025-06-01 |
|||
* @author Matthew Raymer |
|||
*/ |
|||
|
|||
// Log levels
|
|||
export enum LogLevel { |
|||
DEBUG = 'DEBUG', |
|||
INFO = 'INFO', |
|||
WARN = 'WARN', |
|||
ERROR = 'ERROR' |
|||
import winston from 'winston'; |
|||
import path from 'path'; |
|||
import os from 'os'; |
|||
import fs from 'fs'; |
|||
|
|||
// Extend Winston Logger type with our custom loggers
|
|||
declare module 'winston' { |
|||
interface Logger { |
|||
sqlite: { |
|||
debug: (message: string, ...args: unknown[]) => void; |
|||
info: (message: string, ...args: unknown[]) => void; |
|||
warn: (message: string, ...args: unknown[]) => void; |
|||
error: (message: string, ...args: unknown[]) => void; |
|||
}; |
|||
migration: { |
|||
debug: (message: string, ...args: unknown[]) => void; |
|||
info: (message: string, ...args: unknown[]) => void; |
|||
warn: (message: string, ...args: unknown[]) => void; |
|||
error: (message: string, ...args: unknown[]) => void; |
|||
}; |
|||
} |
|||
} |
|||
|
|||
// Log entry interface
|
|||
interface LogEntry { |
|||
timestamp: string; |
|||
level: LogLevel; |
|||
component: string; |
|||
message: string; |
|||
data?: unknown; |
|||
// Ensure log directory exists
|
|||
const logDir = path.join(os.homedir(), 'Logs', 'TimeSafari'); |
|||
if (!fs.existsSync(logDir)) { |
|||
fs.mkdirSync(logDir, { recursive: true, mode: 0o755 }); |
|||
} |
|||
|
|||
// Format log entry
|
|||
const formatLogEntry = (entry: LogEntry): string => { |
|||
const { timestamp, level, component, message, data } = entry; |
|||
const dataStr = data ? ` ${JSON.stringify(data, null, 2)}` : ''; |
|||
return `[${timestamp}] [${level}] [${component}] ${message}${dataStr}`; |
|||
}; |
|||
// Custom format for console output
|
|||
const consoleFormat = winston.format.combine( |
|||
winston.format.timestamp(), |
|||
winston.format.colorize(), |
|||
winston.format.printf(({ level, message, timestamp, ...metadata }) => { |
|||
// Skip debug logs for migrations unless explicitly enabled
|
|||
if (level === 'debug' && |
|||
typeof message === 'string' && |
|||
message.includes('Migration') && |
|||
!process.env.DEBUG_MIGRATIONS) { |
|||
return ''; |
|||
} |
|||
|
|||
// Create logger for a specific component
|
|||
export const createLogger = (component: string) => { |
|||
const log = (level: LogLevel, message: string, data?: unknown) => { |
|||
const entry: LogEntry = { |
|||
timestamp: new Date().toISOString(), |
|||
level, |
|||
component, |
|||
message, |
|||
data |
|||
}; |
|||
let msg = `${timestamp} [${level}] ${message}`; |
|||
if (Object.keys(metadata).length > 0) { |
|||
msg += ` ${JSON.stringify(metadata, null, 2)}`; |
|||
} |
|||
return msg; |
|||
}) |
|||
); |
|||
|
|||
const formatted = formatLogEntry(entry); |
|||
// Custom format for file output
|
|||
const fileFormat = winston.format.combine( |
|||
winston.format.timestamp(), |
|||
winston.format.json() |
|||
); |
|||
|
|||
switch (level) { |
|||
case LogLevel.DEBUG: |
|||
console.debug(formatted); |
|||
break; |
|||
case LogLevel.INFO: |
|||
console.info(formatted); |
|||
break; |
|||
case LogLevel.WARN: |
|||
console.warn(formatted); |
|||
break; |
|||
case LogLevel.ERROR: |
|||
console.error(formatted); |
|||
break; |
|||
} |
|||
// Create logger instance
|
|||
export const logger = winston.createLogger({ |
|||
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info', |
|||
format: fileFormat, |
|||
defaultMeta: { service: 'timesafari-electron' }, |
|||
transports: [ |
|||
// Console transport with custom format
|
|||
new winston.transports.Console({ |
|||
format: consoleFormat, |
|||
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info' |
|||
}), |
|||
// File transport for all logs
|
|||
new winston.transports.File({ |
|||
filename: path.join(logDir, 'error.log'), |
|||
level: 'error', |
|||
maxsize: 5242880, // 5MB
|
|||
maxFiles: 5 |
|||
}), |
|||
// File transport for all logs including debug
|
|||
new winston.transports.File({ |
|||
filename: path.join(logDir, 'combined.log'), |
|||
maxsize: 5242880, // 5MB
|
|||
maxFiles: 5 |
|||
}) |
|||
] |
|||
}) as winston.Logger & { |
|||
sqlite: { |
|||
debug: (message: string, ...args: unknown[]) => void; |
|||
info: (message: string, ...args: unknown[]) => void; |
|||
warn: (message: string, ...args: unknown[]) => void; |
|||
error: (message: string, ...args: unknown[]) => void; |
|||
}; |
|||
|
|||
return { |
|||
debug: (message: string, data?: unknown) => log(LogLevel.DEBUG, message, data), |
|||
info: (message: string, data?: unknown) => log(LogLevel.INFO, message, data), |
|||
warn: (message: string, data?: unknown) => log(LogLevel.WARN, message, data), |
|||
error: (message: string, data?: unknown) => log(LogLevel.ERROR, message, data) |
|||
migration: { |
|||
debug: (message: string, ...args: unknown[]) => void; |
|||
info: (message: string, ...args: unknown[]) => void; |
|||
warn: (message: string, ...args: unknown[]) => void; |
|||
error: (message: string, ...args: unknown[]) => void; |
|||
}; |
|||
}; |
|||
|
|||
// Create default logger for SQLite operations
|
|||
export const logger = createLogger('SQLite'); |
|||
// Add SQLite specific logger
|
|||
logger.sqlite = { |
|||
debug: (message: string, ...args: unknown[]) => { |
|||
logger.debug(`[SQLite] ${message}`, ...args); |
|||
}, |
|||
info: (message: string, ...args: unknown[]) => { |
|||
logger.info(`[SQLite] ${message}`, ...args); |
|||
}, |
|||
warn: (message: string, ...args: unknown[]) => { |
|||
logger.warn(`[SQLite] ${message}`, ...args); |
|||
}, |
|||
error: (message: string, ...args: unknown[]) => { |
|||
logger.error(`[SQLite] ${message}`, ...args); |
|||
} |
|||
}; |
|||
|
|||
// Add migration specific logger
|
|||
logger.migration = { |
|||
debug: (message: string, ...args: unknown[]) => { |
|||
if (process.env.DEBUG_MIGRATIONS) { |
|||
logger.debug(`[Migration] ${message}`, ...args); |
|||
} |
|||
}, |
|||
info: (message: string, ...args: unknown[]) => { |
|||
logger.info(`[Migration] ${message}`, ...args); |
|||
}, |
|||
warn: (message: string, ...args: unknown[]) => { |
|||
logger.warn(`[Migration] ${message}`, ...args); |
|||
}, |
|||
error: (message: string, ...args: unknown[]) => { |
|||
logger.error(`[Migration] ${message}`, ...args); |
|||
} |
|||
}; |
|||
|
|||
// Export logger instance
|
|||
export default logger; |
Loading…
Reference in new issue