@dharshansr/gitgenius
Version:
AI-powered commit message generator with enhanced features
306 lines • 9.29 kB
JavaScript
/**
* Comprehensive structured logging system for GitGenius
* Supports multiple log levels, log rotation, and debug mode
*/
import chalk from 'chalk';
import { writeFileSync, existsSync, mkdirSync, readFileSync, appendFileSync, statSync, readdirSync, unlinkSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
export class Logger {
constructor() {
this.levels = {
trace: 0,
debug: 1,
info: 2,
warn: 3,
error: 4
};
const logDir = join(homedir(), '.gitgenius', 'logs');
this.config = {
level: this.getLogLevelFromEnv(),
enableConsole: true,
enableFile: true,
logDir,
maxFileSize: 10 * 1024 * 1024, // 10MB
maxFiles: 5,
prettyPrint: process.env.LOG_PRETTY !== 'false'
};
// Ensure log directory exists
if (!existsSync(this.config.logDir)) {
mkdirSync(this.config.logDir, { recursive: true });
}
this.logFilePath = join(this.config.logDir, 'gitgenius.log');
this.rotateLogsIfNeeded();
}
static getInstance() {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
/**
* Get log level from environment variables
*/
getLogLevelFromEnv() {
const debugEnv = process.env.DEBUG;
const logLevel = process.env.LOG_LEVEL;
if (logLevel && ['debug', 'info', 'warn', 'error', 'trace'].includes(logLevel)) {
return logLevel;
}
if (debugEnv && debugEnv.includes('gitgenius')) {
return 'debug';
}
return 'info';
}
/**
* Check if logs should be rotated
*/
rotateLogsIfNeeded() {
if (!existsSync(this.logFilePath)) {
return;
}
const stats = statSync(this.logFilePath);
if (stats.size >= this.config.maxFileSize) {
this.rotateLogs();
}
}
/**
* Rotate log files
*/
rotateLogs() {
try {
// Remove oldest log if we have too many
const logFiles = readdirSync(this.config.logDir)
.filter(f => f.startsWith('gitgenius.log'))
.sort()
.reverse();
if (logFiles.length >= this.config.maxFiles) {
const oldestLog = join(this.config.logDir, logFiles[logFiles.length - 1]);
unlinkSync(oldestLog);
}
// Rotate current log
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const rotatedPath = join(this.config.logDir, `gitgenius.log.${timestamp}`);
if (existsSync(this.logFilePath)) {
const content = readFileSync(this.logFilePath, 'utf-8');
writeFileSync(rotatedPath, content);
writeFileSync(this.logFilePath, '');
}
}
catch (error) {
// Silently fail - don't break the app if log rotation fails
console.error('Failed to rotate logs:', error);
}
}
/**
* Check if a log level should be logged
*/
shouldLog(level) {
return this.levels[level] >= this.levels[this.config.level];
}
/**
* Write log entry to file
*/
writeToFile(entry) {
if (!this.config.enableFile) {
return;
}
try {
this.rotateLogsIfNeeded();
const logLine = JSON.stringify(entry) + '\n';
appendFileSync(this.logFilePath, logLine);
}
catch (error) {
// Silently fail - don't break the app if file logging fails
}
}
/**
* Format log entry for console output
*/
formatForConsole(entry) {
if (!this.config.prettyPrint) {
return JSON.stringify(entry);
}
const timestamp = chalk.gray(entry.timestamp);
const category = chalk.cyan(`[${entry.category}]`);
let levelStr = '';
switch (entry.level) {
case 'trace':
levelStr = chalk.magenta('[TRACE]');
break;
case 'debug':
levelStr = chalk.blue('[DEBUG]');
break;
case 'info':
levelStr = chalk.green('[INFO]');
break;
case 'warn':
levelStr = chalk.yellow('[WARN]');
break;
case 'error':
levelStr = chalk.red('[ERROR]');
break;
}
let output = `${timestamp} ${levelStr} ${category} ${entry.message}`;
if (entry.metadata && Object.keys(entry.metadata).length > 0) {
output += '\n' + chalk.gray(JSON.stringify(entry.metadata, null, 2));
}
if (entry.stack) {
output += '\n' + chalk.gray(entry.stack);
}
return output;
}
/**
* Core log method
*/
log(level, category, message, metadata, error) {
if (!this.shouldLog(level)) {
return;
}
const entry = {
timestamp: new Date().toISOString(),
level,
category,
message,
metadata,
stack: error?.stack
};
// Write to file
this.writeToFile(entry);
// Write to console
if (this.config.enableConsole) {
const formatted = this.formatForConsole(entry);
switch (level) {
case 'error':
console.error(formatted);
break;
case 'warn':
console.warn(formatted);
break;
default:
console.log(formatted);
}
}
}
/**
* Public logging methods
*/
trace(category, message, metadata) {
this.log('trace', category, message, metadata);
}
debug(category, message, metadata) {
this.log('debug', category, message, metadata);
}
info(category, message, metadata) {
this.log('info', category, message, metadata);
}
warn(category, message, metadata) {
this.log('warn', category, message, metadata);
}
error(category, message, error, metadata) {
this.log('error', category, message, metadata, error);
}
/**
* Configuration methods
*/
setLogLevel(level) {
this.config.level = level;
}
getLogLevel() {
return this.config.level;
}
enableFileLogging(enable) {
this.config.enableFile = enable;
}
enableConsoleLogging(enable) {
this.config.enableConsole = enable;
}
getLogFilePath() {
return this.logFilePath;
}
getLogDir() {
return this.config.logDir;
}
/**
* Get recent logs from file
*/
getRecentLogs(lines = 100) {
try {
if (!existsSync(this.logFilePath)) {
return [];
}
const content = readFileSync(this.logFilePath, 'utf-8');
const logLines = content.trim().split('\n').filter(line => line.length > 0);
const recentLines = logLines.slice(-lines);
return recentLines.map(line => {
try {
return JSON.parse(line);
}
catch {
return null;
}
}).filter(entry => entry !== null);
}
catch (error) {
return [];
}
}
/**
* Clear all logs
*/
clearLogs() {
try {
if (existsSync(this.logFilePath)) {
writeFileSync(this.logFilePath, '');
}
// Clear rotated logs
const logFiles = readdirSync(this.config.logDir)
.filter(f => f.startsWith('gitgenius.log'));
logFiles.forEach(file => {
const filePath = join(this.config.logDir, file);
if (filePath !== this.logFilePath) {
unlinkSync(filePath);
}
});
}
catch (error) {
console.error('Failed to clear logs:', error);
}
}
/**
* Get log statistics
*/
getLogStats() {
try {
const logs = this.getRecentLogs(1000);
const byLevel = {
trace: 0,
debug: 0,
info: 0,
warn: 0,
error: 0
};
logs.forEach(log => {
byLevel[log.level] = (byLevel[log.level] || 0) + 1;
});
const fileSize = existsSync(this.logFilePath)
? statSync(this.logFilePath).size
: 0;
return {
totalLogs: logs.length,
byLevel,
fileSize
};
}
catch {
return {
totalLogs: 0,
byLevel: { trace: 0, debug: 0, info: 0, warn: 0, error: 0 },
fileSize: 0
};
}
}
}
// Export singleton instance
export const logger = Logger.getInstance();
//# sourceMappingURL=Logger.js.map