logger.ts•6.32 kB
/**
 * Log level type
 */
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
/**
 * Log metadata interface
 */
export interface LogMetadata {
  [key: string]: any;
}
/**
 * Log entry interface
 */
export interface LogEntry {
  level: LogLevel;
  message: string;
  timestamp: string;
  metadata?: LogMetadata;
}
/**
 * Enhanced Logging System
 *
 * Features:
 * - Structured logging
 * - Multiple log levels
 * - Metadata support
 * - Performance optimization
 */
export class Logger {
  private logLevel: LogLevel;
  private enableConsoleOutput: boolean;
  private logHistory: LogEntry[] = [];
  private maxHistorySize: number = 1000;
  constructor(logLevel: LogLevel = 'info', enableConsoleOutput: boolean = true) {
    this.logLevel = logLevel;
    this.enableConsoleOutput = enableConsoleOutput;
  }
  /**
   * Map log level priority
   */
  private getLevelPriority(level: LogLevel): number {
    const priorities = { debug: 0, info: 1, warn: 2, error: 3 };
    return priorities[level];
  }
  /**
   * Check if current log level should be output
   */
  private shouldLog(level: LogLevel): boolean {
    return this.getLevelPriority(level) >= this.getLevelPriority(this.logLevel);
  }
  /**
   * Format log message
   */
  private formatMessage(level: LogLevel, message: string, metadata?: LogMetadata): string {
    const timestamp = new Date().toISOString();
    const levelStr = level.toUpperCase().padEnd(5);
    let formattedMessage = `[${timestamp}] [${levelStr}] ${message}`;
    if (metadata) {
      try {
        const metadataStr = JSON.stringify(metadata, null, 2);
        formattedMessage += ` ${metadataStr}`;
      } catch (error) {
        // Handle circular references and other JSON stringify errors
        formattedMessage += ` [metadata formatting error: ${
          error instanceof Error ? error.message : String(error)
        }]`;
      }
    }
    return formattedMessage;
  }
  /**
   * Save log entry
   */
  private saveLogEntry(level: LogLevel, message: string, metadata?: LogMetadata): void {
    const entry: LogEntry = {
      level,
      message,
      timestamp: new Date().toISOString(),
      metadata,
    };
    this.logHistory.push(entry);
    // Limit history size
    if (this.logHistory.length > this.maxHistorySize) {
      this.logHistory = this.logHistory.slice(-this.maxHistorySize);
    }
  }
  /**
   * Base logging method
   */
  private log(level: LogLevel, message: string, metadata?: LogMetadata): void {
    if (!this.shouldLog(level)) return;
    // Save log entry
    this.saveLogEntry(level, message, metadata);
    if (this.enableConsoleOutput) {
      const formattedMessage = this.formatMessage(level, message, metadata);
      // Console output (use stderr to avoid affecting MCP communication)
      switch (level) {
        case 'debug':
          process.stderr.write(`[DEBUG] ${formattedMessage}\n`);
          break;
        case 'info':
          process.stderr.write(`[INFO] ${formattedMessage}\n`);
          break;
        case 'warn':
          process.stderr.write(`[WARN] ${formattedMessage}\n`);
          break;
        case 'error':
          process.stderr.write(`[ERROR] ${formattedMessage}\n`);
          break;
      }
    }
  }
  /**
   * Debug level logging
   */
  debug(message: string, metadata?: LogMetadata): void {
    this.log('debug', message, metadata);
  }
  /**
   * Info level logging
   */
  info(message: string, metadata?: LogMetadata): void {
    this.log('info', message, metadata);
  }
  /**
   * Warning level logging
   */
  warn(message: string, metadata?: LogMetadata): void {
    this.log('warn', message, metadata);
  }
  /**
   * Error level logging
   */
  error(message: string, metadata?: LogMetadata): void {
    this.log('error', message, metadata);
  }
  /**
   * Start performance measurement
   */
  startTimer(label: string): () => void {
    const startTime = process.hrtime.bigint();
    return () => {
      const endTime = process.hrtime.bigint();
      const duration = Number(endTime - startTime) / 1_000_000; // Convert to milliseconds
      this.debug(`Performance measurement: ${label}`, {
        executionTime: `${duration.toFixed(2)}ms`,
        label,
      });
    };
  }
  /**
   * Log query execution
   */
  logQuery(query: string, executionTime: number, success: boolean, metadata?: LogMetadata): void {
    const queryPreview = query.length > 100 ? `${query.substring(0, 100)}...` : query;
    if (success) {
      this.info('Query execution successful', {
        query: queryPreview,
        executionTime: `${executionTime}ms`,
        ...metadata,
      });
    } else {
      this.error('Query execution failed', {
        query: queryPreview,
        executionTime: `${executionTime}ms`,
        ...metadata,
      });
    }
  }
  /**
   * Log connection status
   */
  logConnection(action: string, success: boolean, metadata?: LogMetadata): void {
    if (success) {
      this.info(`Database ${action} successful`, metadata);
    } else {
      this.error(`Database ${action} failed`, metadata);
    }
  }
  /**
   * Log security events
   */
  logSecurityEvent(
    event: string,
    severity: 'low' | 'medium' | 'high',
    metadata?: LogMetadata
  ): void {
    const level = severity === 'high' ? 'error' : severity === 'medium' ? 'warn' : 'info';
    this.log(level, `Security event: ${event}`, { severity, ...metadata });
  }
  /**
   * Change log level
   */
  setLogLevel(level: LogLevel): void {
    this.logLevel = level;
    this.info(`Log level changed to ${level}.`);
  }
  /**
   * Get log history
   */
  getLogHistory(level?: LogLevel, limit?: number): LogEntry[] {
    let filtered = this.logHistory;
    if (level) {
      filtered = filtered.filter(entry => entry.level === level);
    }
    if (limit) {
      filtered = filtered.slice(-limit);
    }
    return filtered;
  }
  /**
   * Clear log history
   */
  clearHistory(): void {
    this.logHistory = [];
    // Note: Do not log this action to avoid immediately adding to cleared history
  }
  /**
   * Get log statistics
   */
  getStatistics(): { [level in LogLevel]: number } {
    const stats = { debug: 0, info: 0, warn: 0, error: 0 };
    this.logHistory.forEach(entry => {
      stats[entry.level]++;
    });
    return stats;
  }
}