UNPKG

git-aiflow

Version:

🚀 An AI-powered workflow automation tool for effortless Git-based development, combining smart GitLab/GitHub merge & pull request creation with Conan package management.

578 lines • 20.8 kB
import winston from 'winston'; import path from 'path'; import fs from 'fs'; import os from 'os'; /** * Log levels */ export var LogLevel; (function (LogLevel) { LogLevel["ERROR"] = "error"; LogLevel["WARN"] = "warn"; LogLevel["INFO"] = "info"; LogLevel["HTTP"] = "http"; LogLevel["VERBOSE"] = "verbose"; LogLevel["DEBUG"] = "debug"; LogLevel["SILLY"] = "silly"; })(LogLevel || (LogLevel = {})); /** * Get global logs directory based on platform */ function getGlobalLogsDir() { const platform = os.platform(); if (platform === 'win32') { // Windows: %APPDATA%\aiflow\logs const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'); return path.join(appData, 'aiflow', 'logs'); } else { // Unix-like: ~/.config/aiflow/logs or ~/logs/aiflow const configDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'); return path.join(configDir, 'aiflow', 'logs'); } } /** * Ensure logs directory exists */ function ensureLogsDir(logDir) { try { if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); } } catch (error) { console.warn(`Failed to create logs directory ${logDir}:`, error); } } /** * Default logger configuration */ const defaultConfig = { level: LogLevel.DEBUG, consoleLevel: LogLevel.INFO, maxSize: '10m', // 10MB per file maxFiles: 5, // Keep 5 files logDir: getGlobalLogsDir(), enableConsole: true, bufferSize: 64 * 1024, // 64KB buffer for better performance flushInterval: 0, // Disable auto-flush to avoid blocking (was 5000) lazy: false, // Create files immediately (was true) highWaterMark: 16 * 1024 // 16KB high water mark }; const SPLAT_SYMBOL = Symbol.for('splat'); /** * Create Winston logger instance */ function createWinstonLogger(config = defaultConfig) { // Ensure log directory exists ensureLogsDir(config.logDir); const logFormat = winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), winston.format.errors({ stack: true }), winston.format.printf((info) => { const { level, message, timestamp, stack, ...meta } = info; let logMessage = `${timestamp} [${level.toUpperCase()}]: ${message}`; // Add stack trace for errors if (stack) { logMessage += `\n${stack}`; } // Combine metadata from both meta and winston's splat symbol let allMeta = { ...meta }; // Access winston's SPLAT symbol to get additional metadata const splatData = info[SPLAT_SYMBOL]; // If splat exists and contains metadata (additional parameters to logger), merge it if (splatData && Array.isArray(splatData) && splatData.length > 0) { for (const item of splatData) { if (typeof item === 'object' && item !== null) { allMeta = { ...allMeta, ...item }; } else if (typeof item === 'string') { logMessage += `\n${item}`; } } } // Add metadata if present if (allMeta && Object.keys(allMeta).length > 0) { logMessage += `\n${JSON.stringify(allMeta, null, 0)}`; } return logMessage; })); const transports = []; // File transport with rotation transports.push(new winston.transports.File({ filename: path.join(config.logDir, 'aiflow.log'), level: config.level, format: logFormat, maxsize: parseSize(config.maxSize), maxFiles: config.maxFiles, tailable: true, lazy: config.lazy })); // Error-only file transport transports.push(new winston.transports.File({ filename: path.join(config.logDir, 'error.log'), level: LogLevel.ERROR, format: logFormat, maxsize: parseSize(config.maxSize), maxFiles: config.maxFiles, tailable: true, lazy: config.lazy })); // Console transport (conditional) if (config.enableConsole) { transports.push(new winston.transports.Console({ level: config.consoleLevel, format: winston.format.combine(winston.format.colorize(), winston.format.timestamp({ format: 'HH:mm:ss.SSS' }), winston.format.printf((info) => { const { level, message, timestamp, ...meta } = info; let consoleMessage = `${timestamp} ${level}: ${message}`; // Combine metadata from both meta and winston's splat symbol let allMeta = { ...meta }; // Access winston's SPLAT symbol to get additional metadata const splatData = info[SPLAT_SYMBOL]; // If splat exists and contains metadata (additional parameters to logger), merge it if (splatData && Array.isArray(splatData) && splatData.length > 0) { for (const item of splatData) { if (typeof item === 'object' && item !== null) { allMeta = { ...allMeta, ...item }; } else if (typeof item === 'string') { consoleMessage += `\n${item}`; } } } // Add metadata if present (but keep it concise for console) if (allMeta && Object.keys(allMeta).length > 0) { const metaStr = JSON.stringify(allMeta, null, 0); if (metaStr.length < 100) { consoleMessage += ` ${metaStr}`; } else { consoleMessage += ` ${metaStr.substring(0, 100)}...`; } } return consoleMessage; })) })); } const logger = winston.createLogger({ level: config.level, transports, exitOnError: false, silent: false }); // Set up periodic flush for better write performance balance if (config.flushInterval > 0) { const flushTimer = setInterval(() => { try { logger.transports.forEach(transport => { if (transport instanceof winston.transports.File) { // Force flush file transports safely const stream = transport._stream; if (stream && stream.writable && typeof stream.flush === 'function') { // Use setImmediate to avoid blocking setImmediate(() => { try { stream.flush(); } catch (error) { // Ignore flush errors to prevent crashes } }); } } }); } catch (error) { // Ignore timer errors to prevent crashes } }, config.flushInterval); // Store timer for cleanup and make it not block process exit flushTimer.unref(); logger.flushTimer = flushTimer; } return logger; } /** * Parse size string to bytes */ function parseSize(sizeStr) { const size = parseFloat(sizeStr); const unit = sizeStr.toLowerCase().slice(-1); switch (unit) { case 'k': return size * 1024; case 'm': return size * 1024 * 1024; case 'g': return size * 1024 * 1024 * 1024; default: return size; } } /** * Cache for caller information to improve performance */ const callerCache = new Map(); /** * Get caller information from stack trace */ function getCallerInfo() { // Create a simple stack key for caching const stackKey = new Error().stack?.split('\n').slice(3, 6).join('|') || ''; // Check cache first if (callerCache.has(stackKey)) { return callerCache.get(stackKey); } const originalPrepareStackTrace = Error.prepareStackTrace; const originalStackTraceLimit = Error.stackTraceLimit; try { Error.prepareStackTrace = (_, stack) => stack; Error.stackTraceLimit = 20; const stack = new Error().stack; // Skip the first 3 frames: Error, getCallerInfo, and the logger method for (let i = 3; i < stack.length; i++) { const frame = stack[i]; const fileName = frame.getFileName(); // const functionName = frame.getFunctionName(); // const methodName = frame.getMethodName(); // const typeName = frame.getTypeName(); // Skip logger.ts, node_modules, and internal files if (fileName && !fileName.includes('/logger.ts') && !fileName.includes('/logger.js') && !fileName.includes('node_modules') && !fileName.includes('internal/') && !fileName.includes('/util.js') && !fileName.includes('/util.ts')) { // Extract filename without path and extension const baseName = path.basename(fileName, path.extname(fileName)); // Try to construct a meaningful context let context = baseName.toUpperCase().trim(); // If we have a type name (class name), use it // if (typeName && // typeName !== 'Object' && // typeName !== 'Function' && // typeName !== 'Module' && // typeName !== '') { // context = typeName; // // Add method name if available // if (methodName && methodName !== 'anonymous' && methodName !== '') { // context += `.${methodName}`; // } else if (functionName && functionName !== 'anonymous' && functionName !== '') { // context += `.${functionName}`; // } // } else if (functionName && functionName !== 'anonymous' && functionName !== '') { // // Use function name if no class name // context = `${baseName}.${functionName}`; // } else if (methodName && methodName !== 'anonymous' && methodName !== '') { // // Use method name if available // context = `${baseName}.${methodName}`; // } // Cache the result callerCache.set(stackKey, context); return context; } } const fallback = 'Unknown'; callerCache.set(stackKey, fallback); return fallback; } catch (error) { const fallback = 'Unknown'; callerCache.set(stackKey, fallback); return fallback; } finally { Error.prepareStackTrace = originalPrepareStackTrace; Error.stackTraceLimit = originalStackTraceLimit; } } /** * Logger class with convenient methods */ export class Logger { constructor(config) { this.isShuttingDown = false; this.winston = createWinstonLogger({ ...defaultConfig, ...config }); } /** * Get singleton instance */ static getInstance(config) { if (!Logger.instance) { Logger.instance = new Logger(config); // Store shutdown function as static method Logger.shutdownLogger = async function () { if (!Logger.hasInstance()) return; const loggerInstance = Logger.getInstance(); const winstonLogger = loggerInstance.getWinston(); // Mark as shutting down to prevent new logs loggerInstance.isShuttingDown = true; // Clear flush timer if exists const flushTimer = winstonLogger.flushTimer; if (flushTimer) { clearInterval(flushTimer); } // Final flush before closing (with timeout) const flushPromises = winstonLogger.transports.map(transport => { return new Promise(resolve => { if (transport instanceof winston.transports.File) { const stream = transport._stream; if (stream && stream.writable && typeof stream.flush === 'function') { try { stream.flush(); } catch (error) { // Ignore flush errors } } } resolve(); }); }); // Wait for flush with timeout await Promise.race([ Promise.all(flushPromises), new Promise(resolve => setTimeout(resolve, 1000)) // 1 second timeout ]); // Close all transports gracefully with timeout const closePromises = winstonLogger.transports.map(transport => { return new Promise(resolve => { const timeout = setTimeout(() => resolve(), 500); // 500ms timeout try { if (typeof transport.close === 'function') { transport.close(); } const stream = transport._stream; if (stream && typeof stream.end === 'function') { stream.end(() => { clearTimeout(timeout); resolve(); }); } else { clearTimeout(timeout); resolve(); } } catch (error) { clearTimeout(timeout); resolve(); } }); }); await Promise.all(closePromises); }; } else if (config) { // If config is provided and instance exists, recreate with new config Logger.instance.winston = createWinstonLogger({ ...defaultConfig, ...config }); } return Logger.instance; } /** * Create logger for specific context (deprecated, use getInstance instead) */ static create(_context, config) { console.warn('Logger.create() is deprecated, use Logger.getInstance() instead'); return Logger.getInstance(config); } /** * Configure global logger settings */ static configure(config) { Object.assign(defaultConfig, config); // Recreate logger with new config if (Logger.instance) { Logger.instance.winston = createWinstonLogger({ ...defaultConfig, ...config }); } } /** * Reset singleton instance (useful for testing) */ static reset() { Logger.instance = null; } /** * Check if singleton instance exists */ static hasInstance() { return Logger.instance !== null; } /** * Clear caller cache (useful for testing or memory management) */ static clearCache() { callerCache.clear(); } /** * Get cache size (for debugging) */ static getCacheSize() { return callerCache.size; } formatMessage(message) { const context = getCallerInfo(); return `[${context}] ${message}`; } error(message, error) { if (this.isShuttingDown) return; if (error instanceof Error) { this.winston.error(this.formatMessage(message), { error: error.message, stack: error.stack }); } else if (error) { this.winston.error(this.formatMessage(message), { error }); } else { this.winston.error(this.formatMessage(message)); } } warn(message, meta) { if (this.isShuttingDown) return; this.winston.warn(this.formatMessage(message), meta); } info(message, meta) { if (this.isShuttingDown) return; this.winston.info(this.formatMessage(message), meta); } http(message, meta) { if (this.isShuttingDown) return; this.winston.http(this.formatMessage(message), meta); } verbose(message, meta) { if (this.isShuttingDown) return; this.winston.verbose(this.formatMessage(message), meta); } debug(message, meta) { if (this.isShuttingDown) return; this.winston.debug(this.formatMessage(message), meta); } silly(message, meta) { if (this.isShuttingDown) return; this.winston.silly(this.formatMessage(message), meta); } /** * Log shell command execution */ shell(command, result, error) { if (this.isShuttingDown) return; if (error) { this.error(`Shell command failed: ${command}`, error); } else { this.debug(`Shell command: ${command}`, { result: result?.substring(0, 200) }); } } /** * Log HTTP request/response */ httpRequest(method, url, status, duration) { if (this.isShuttingDown) return; this.http(`${method} ${url} ${status ? `(${status})` : ''} ${duration ? `(${duration}ms)` : ''}`); } /** * Log service operations */ service(operation, service, meta) { if (this.isShuttingDown) return; if (meta) { // Use Winston's structured logging instead of stringifying in the message this.winston.info(this.formatMessage(`${service}: ${operation}`), meta); } else { this.winston.info(this.formatMessage(`${service}: ${operation}`)); } } /** * Get underlying Winston instance */ getWinston() { return this.winston; } /** * Force flush all file transports */ flush() { if (this.isShuttingDown) return; this.winston.transports.forEach(transport => { if (transport instanceof winston.transports.File) { const stream = transport._stream; if (stream && typeof stream.flush === 'function') { stream.flush(); } } }); } /** * Get current buffer stats (if available) */ getBufferStats() { return this.winston.transports.map(transport => { const stats = { transportType: transport.constructor.name }; if (transport instanceof winston.transports.File) { const stream = transport._stream; if (stream) { stats.buffered = stream._writableState?.bufferedRequestCount || 0; stats.highWaterMark = stream._writableState?.highWaterMark || 0; } } return stats; }); } } Logger.instance = null; Logger.shutdownLogger = null; /** * Default logger instance (singleton) */ export const logger = Logger.getInstance(); /** * Mark logger as shutting down (prevent new logs) */ export function markLoggerShuttingDown() { if (Logger.hasInstance()) { const instance = Logger.getInstance(); instance.isShuttingDown = true; } } /** * Gracefully shutdown logger (close file streams) */ export async function shutdownLogger() { if (Logger.shutdownLogger) { await Logger.shutdownLogger(); } } /** * Configure global logging */ export function configureLogging(config) { Logger.configure(config); } /** * Get the global logs directory path */ export function getLogsDir() { return getGlobalLogsDir(); } /** * Test function to verify stack trace parsing * This can be removed in production */ export function testLoggerContext() { logger.info('Testing logger context detection'); logger.debug('This should show the calling context'); logger.error('Error test with context'); } //# sourceMappingURL=logger.js.map