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
JavaScript
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