Skip to main content
Glama
logger.ts15.5 kB
/** * Logger * * Comprehensive logging system for Git MCP server with multiple log levels, * structured logging, and configurable output formats. */ import { promises as fs } from 'fs'; import path from 'path'; export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal'; export interface LogEntry { timestamp: string; level: LogLevel; message: string; context?: string; metadata?: Record<string, any>; error?: { name: string; message: string; stack?: string; }; operation?: string; provider?: string; toolName?: string; projectPath?: string; executionTime?: number; } export interface LoggerConfig { level: LogLevel; enableConsole: boolean; enableFile: boolean; logFilePath?: string; maxFileSize?: number; // in bytes maxFiles?: number; format: 'json' | 'text'; includeTimestamp: boolean; includeMetadata: boolean; colorize: boolean; } export class Logger { private config: LoggerConfig; private logLevels: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }; private colors: Record<LogLevel, string> = { debug: '\x1b[36m', // Cyan info: '\x1b[32m', // Green warn: '\x1b[33m', // Yellow error: '\x1b[31m', // Red fatal: '\x1b[35m' // Magenta }; private resetColor = '\x1b[0m'; constructor(config: Partial<LoggerConfig> = {}) { this.config = { level: 'info', enableConsole: true, enableFile: false, format: 'text', includeTimestamp: true, includeMetadata: true, colorize: true, maxFileSize: 10 * 1024 * 1024, // 10MB maxFiles: 5, ...config }; // Set log file path if file logging is enabled but no path provided if (this.config.enableFile && !this.config.logFilePath) { this.config.logFilePath = path.join(process.cwd(), 'logs', 'git-mcp.log'); } } /** * Log debug message */ debug(message: string, context?: string, metadata?: Record<string, any>): void { this.log('debug', message, context, metadata); } /** * Log info message */ info(message: string, context?: string, metadata?: Record<string, any>): void { this.log('info', message, context, metadata); } /** * Log warning message */ warn(message: string, context?: string, metadata?: Record<string, any>): void { this.log('warn', message, context, metadata); } /** * Log error message */ error(message: string, error?: Error, context?: string, metadata?: Record<string, any>): void { const errorInfo = error ? { name: error.name, message: error.message, stack: error.stack } : undefined; this.log('error', message, context, metadata, errorInfo); } /** * Log fatal error message */ fatal(message: string, error?: Error, context?: string, metadata?: Record<string, any>): void { const errorInfo = error ? { name: error.name, message: error.message, stack: error.stack } : undefined; this.log('fatal', message, context, metadata, errorInfo); } /** * Log operation start */ operationStart( operation: string, toolName?: string, provider?: string, projectPath?: string, metadata?: Record<string, any> ): void { this.log('info', `Starting operation: ${operation}`, 'OPERATION', { ...metadata, operation, toolName, provider, projectPath, phase: 'start' }); } /** * Log operation completion */ operationComplete( operation: string, executionTime: number, success: boolean, toolName?: string, provider?: string, projectPath?: string, metadata?: Record<string, any> ): void { const level: LogLevel = success ? 'info' : 'error'; const status = success ? 'completed' : 'failed'; this.log(level, `Operation ${status}: ${operation} (${executionTime}ms)`, 'OPERATION', { ...metadata, operation, toolName, provider, projectPath, executionTime, success, phase: 'complete' }); } /** * Log Git command execution */ gitCommand( command: string, args: string[], projectPath?: string, success?: boolean, executionTime?: number, output?: string ): void { const fullCommand = `git ${command} ${args.join(' ')}`; const level: LogLevel = success === false ? 'error' : 'debug'; this.log(level, `Git command: ${fullCommand}`, 'GIT', { command, args, projectPath, success, executionTime, output: output?.substring(0, 500) // Truncate long output }); } /** * Log provider API call */ providerCall( provider: string, endpoint: string, method: string, success?: boolean, statusCode?: number, executionTime?: number, metadata?: Record<string, any> ): void { const level: LogLevel = success === false ? 'error' : 'debug'; this.log(level, `${provider} API: ${method} ${endpoint}`, 'PROVIDER', { ...metadata, provider, endpoint, method, success, statusCode, executionTime }); } /** * Log configuration events */ configuration( event: 'loaded' | 'validated' | 'error', details: Record<string, any> ): void { const level: LogLevel = event === 'error' ? 'error' : 'info'; this.log(level, `Configuration ${event}`, 'CONFIG', details); } /** * Log server events */ server( event: 'starting' | 'started' | 'stopping' | 'stopped' | 'error', details?: Record<string, any> ): void { const level: LogLevel = event === 'error' ? 'error' : 'info'; this.log(level, `Server ${event}`, 'SERVER', details); } /** * Log performance metrics */ performance( operation: string, metrics: { executionTime: number; memoryUsage?: number; cpuUsage?: number; [key: string]: any; } ): void { this.log('debug', `Performance: ${operation}`, 'PERFORMANCE', metrics); } /** * Core logging method */ private log( level: LogLevel, message: string, context?: string, metadata?: Record<string, any>, error?: { name: string; message: string; stack?: string } ): void { // Check if log level is enabled if (this.logLevels[level] < this.logLevels[this.config.level]) { return; } const logEntry: LogEntry = { timestamp: new Date().toISOString(), level, message, context, metadata: this.config.includeMetadata ? metadata : undefined, error, operation: metadata?.operation, provider: metadata?.provider, toolName: metadata?.toolName, projectPath: metadata?.projectPath, executionTime: metadata?.executionTime }; // Output to console if (this.config.enableConsole) { this.logToConsole(logEntry); } // Output to file if (this.config.enableFile && this.config.logFilePath) { this.logToFile(logEntry).catch(err => { console.error('Failed to write to log file:', err); }); } } /** * Log to console with formatting */ private logToConsole(entry: LogEntry): void { let output = ''; // Add color if enabled if (this.config.colorize) { output += this.colors[entry.level]; } // Add timestamp if (this.config.includeTimestamp) { const timestamp = new Date(entry.timestamp).toLocaleTimeString(); output += `[${timestamp}] `; } // Add level output += `${entry.level.toUpperCase().padEnd(5)} `; // Add context if (entry.context) { output += `[${entry.context}] `; } // Add message output += entry.message; // Reset color if (this.config.colorize) { output += this.resetColor; } // Format based on configuration if (this.config.format === 'json') { console.log(JSON.stringify(entry, null, 2)); } else { console.log(output); // Add metadata in text format if available if (entry.metadata && Object.keys(entry.metadata).length > 0) { console.log(' Metadata:', JSON.stringify(entry.metadata, null, 2)); } // Add error details if available if (entry.error) { console.log(' Error:', entry.error.message); if (entry.error.stack) { console.log(' Stack:', entry.error.stack); } } } } /** * Log to file with rotation */ private async logToFile(entry: LogEntry): Promise<void> { if (!this.config.logFilePath) return; try { // Ensure log directory exists const logDir = path.dirname(this.config.logFilePath); await fs.mkdir(logDir, { recursive: true }); // Check file size and rotate if needed await this.rotateLogFileIfNeeded(); // Format log entry const logLine = this.config.format === 'json' ? JSON.stringify(entry) + '\n' : this.formatTextLogEntry(entry) + '\n'; // Append to log file await fs.appendFile(this.config.logFilePath, logLine, 'utf8'); } catch (error) { // Don't throw errors from logging to avoid infinite loops console.error('Failed to write to log file:', error); } } /** * Format log entry for text output */ private formatTextLogEntry(entry: LogEntry): string { let line = ''; // Timestamp if (this.config.includeTimestamp) { line += `${entry.timestamp} `; } // Level line += `${entry.level.toUpperCase().padEnd(5)} `; // Context if (entry.context) { line += `[${entry.context}] `; } // Message line += entry.message; // Add metadata as key=value pairs if (entry.metadata && Object.keys(entry.metadata).length > 0) { const metadataStr = Object.entries(entry.metadata) .map(([key, value]) => `${key}=${JSON.stringify(value)}`) .join(' '); line += ` | ${metadataStr}`; } // Add error information if (entry.error) { line += ` | error="${entry.error.message}"`; if (entry.error.stack) { line += ` stack="${entry.error.stack.replace(/\n/g, '\\n')}"`; } } return line; } /** * Rotate log file if it exceeds maximum size */ private async rotateLogFileIfNeeded(): Promise<void> { if (!this.config.logFilePath || !this.config.maxFileSize) return; try { const stats = await fs.stat(this.config.logFilePath); if (stats.size >= this.config.maxFileSize) { await this.rotateLogFiles(); } } catch (error) { // File doesn't exist yet, no need to rotate if ((error as any).code !== 'ENOENT') { console.error('Error checking log file size:', error); } } } /** * Rotate log files */ private async rotateLogFiles(): Promise<void> { if (!this.config.logFilePath || !this.config.maxFiles) return; const logDir = path.dirname(this.config.logFilePath); const logName = path.basename(this.config.logFilePath, path.extname(this.config.logFilePath)); const logExt = path.extname(this.config.logFilePath); try { // Remove oldest log file if we've reached the limit const oldestLogPath = path.join(logDir, `${logName}.${this.config.maxFiles - 1}${logExt}`); try { await fs.unlink(oldestLogPath); } catch { // File doesn't exist, ignore } // Rotate existing log files for (let i = this.config.maxFiles - 2; i >= 1; i--) { const currentPath = path.join(logDir, `${logName}.${i}${logExt}`); const nextPath = path.join(logDir, `${logName}.${i + 1}${logExt}`); try { await fs.rename(currentPath, nextPath); } catch { // File doesn't exist, ignore } } // Move current log file to .1 const firstRotatedPath = path.join(logDir, `${logName}.1${logExt}`); await fs.rename(this.config.logFilePath, firstRotatedPath); } catch (error) { console.error('Error rotating log files:', error); } } /** * Update logger configuration */ updateConfig(newConfig: Partial<LoggerConfig>): void { this.config = { ...this.config, ...newConfig }; } /** * Get current configuration */ getConfig(): LoggerConfig { return { ...this.config }; } /** * Clear log files */ async clearLogs(): Promise<void> { if (!this.config.logFilePath) return; const logDir = path.dirname(this.config.logFilePath); const logName = path.basename(this.config.logFilePath, path.extname(this.config.logFilePath)); const logExt = path.extname(this.config.logFilePath); try { // Remove main log file try { await fs.unlink(this.config.logFilePath); } catch { // File doesn't exist, ignore } // Remove rotated log files if (this.config.maxFiles) { for (let i = 1; i < this.config.maxFiles; i++) { const rotatedPath = path.join(logDir, `${logName}.${i}${logExt}`); try { await fs.unlink(rotatedPath); } catch { // File doesn't exist, ignore } } } } catch (error) { console.error('Error clearing log files:', error); } } /** * Get log statistics */ async getLogStats(): Promise<{ totalSize: number; fileCount: number; oldestEntry?: string; newestEntry?: string; }> { if (!this.config.logFilePath) { return { totalSize: 0, fileCount: 0 }; } const logDir = path.dirname(this.config.logFilePath); const logName = path.basename(this.config.logFilePath, path.extname(this.config.logFilePath)); const logExt = path.extname(this.config.logFilePath); let totalSize = 0; let fileCount = 0; let oldestEntry: string | undefined; let newestEntry: string | undefined; try { // Check main log file try { const stats = await fs.stat(this.config.logFilePath); totalSize += stats.size; fileCount++; // Get first and last lines for timestamps const content = await fs.readFile(this.config.logFilePath, 'utf8'); const lines = content.trim().split('\n').filter(line => line.trim()); if (lines.length > 0) { oldestEntry = lines[0]; newestEntry = lines[lines.length - 1]; } } catch { // File doesn't exist } // Check rotated log files if (this.config.maxFiles) { for (let i = 1; i < this.config.maxFiles; i++) { const rotatedPath = path.join(logDir, `${logName}.${i}${logExt}`); try { const stats = await fs.stat(rotatedPath); totalSize += stats.size; fileCount++; } catch { // File doesn't exist } } } } catch (error) { console.error('Error getting log statistics:', error); } return { totalSize, fileCount, oldestEntry, newestEntry }; } } // Create default logger instance export const logger = new Logger({ level: (process.env.LOG_LEVEL as LogLevel) || 'info', enableConsole: process.env.LOG_CONSOLE !== 'false', enableFile: process.env.LOG_FILE === 'true', logFilePath: process.env.LOG_FILE_PATH, format: (process.env.LOG_FORMAT as 'json' | 'text') || 'text', colorize: process.env.LOG_COLORIZE !== 'false' });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Andre-Buzeli/git-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server