UNPKG

bowling-analysis-system

Version:

A comprehensive system for analyzing bowling techniques using video processing and metrics calculation

288 lines (250 loc) 7.82 kB
/** * @module utils/logger * @description Unified logging implementation with structured logging support */ const fs = require('fs'); const path = require('path'); const { promisify } = require('util'); const writeFile = promisify(fs.writeFile); const appendFile = promisify(fs.appendFile); const readFile = promisify(fs.readFile); // Log levels with numeric values for comparison const LOG_LEVELS = { error: 0, warn: 1, info: 2, debug: 3, verbose: 4 }; // ANSI color codes for different log levels const COLORS = { error: '\x1b[31m', // Red warn: '\x1b[33m', // Yellow info: '\x1b[36m', // Cyan debug: '\x1b[32m', // Green verbose: '\x1b[35m', // Magenta reset: '\x1b[0m' // Reset }; class Logger { /** * Create a new Logger instance * @param {Object} options - Logger options * @param {string} [options.context=''] - Context name for the logger * @param {string} [options.level='info'] - Log level * @param {boolean} [options.timestamp=true] - Include timestamps * @param {boolean} [options.colors=true] - Use colors in output * @param {boolean} [options.logToFile=true] - Enable file logging * @param {string} [options.logDir='logs'] - Log directory * @param {number} [options.maxFileSize=10*1024*1024] - Max file size in bytes * @param {number} [options.maxFiles=5] - Max number of rotated files */ constructor(options = {}) { this.options = { context: '', level: 'info', timestamp: true, colors: true, logToFile: true, logDir: 'logs', maxFileSize: 10 * 1024 * 1024, // 10MB maxFiles: 5, ...options }; // Create log directory if needed if (this.options.logToFile) { const logDir = path.join(process.cwd(), this.options.logDir); if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); } } // Initialize log file path this.logFile = this.options.logToFile ? path.join(process.cwd(), this.options.logDir, 'app.log') : null; } /** * Format a log message * @param {string} level - Log level * @param {string} message - Log message * @param {Object} [meta] - Additional metadata * @returns {string} Formatted message * @private */ _formatMessage(level, message, meta = {}) { const parts = []; // Add timestamp if enabled if (this.options.timestamp) { parts.push(new Date().toISOString()); } // Add context if present if (this.options.context) { parts.push(`[${this.options.context}]`); } // Add level with color if enabled const levelStr = `[${level.toUpperCase()}]`; if (this.options.colors) { parts.push(`${COLORS[level]}${levelStr}${COLORS.reset}`); } else { parts.push(levelStr); } // Add message parts.push(message); // Add metadata if present if (Object.keys(meta).length > 0) { parts.push(JSON.stringify(meta)); } return parts.join(' '); } /** * Write a message to the log file * @param {string} message - Message to write * @param {string} level - Log level * @private */ async _writeToFile(message, level) { if (!this.options.logToFile || !this.logFile) return; try { // Check if we need to rotate logs if (fs.existsSync(this.logFile)) { const stats = await fs.promises.stat(this.logFile); if (stats.size >= this.options.maxFileSize) { await this._rotateLogs(); } } // Write the message await appendFile(this.logFile, message + '\n', 'utf8'); } catch (error) { console.error('Failed to write to log file:', error); } } /** * Rotate log files * @private */ async _rotateLogs() { try { // Rotate existing files for (let i = this.options.maxFiles - 1; i > 0; i--) { const oldFile = `${this.logFile}.${i}`; const newFile = `${this.logFile}.${i + 1}`; if (fs.existsSync(oldFile)) { await this._rotateFile(oldFile, newFile); } } // Rotate current log file await this._rotateFile(this.logFile, `${this.logFile}.1`); } catch (error) { console.error('Failed to rotate log files:', error); } } /** * Rotate a single log file * @param {string} oldPath - Path to old file * @param {string} newPath - Path to new file * @private */ async _rotateFile(oldPath, newPath) { try { // Read the old file const content = await readFile(oldPath, 'utf8'); // Write to new file await writeFile(newPath, content, 'utf8'); // Clear old file await writeFile(oldPath, '', 'utf8'); } catch (error) { console.error(`Failed to rotate file ${oldPath}:`, error); } } /** * Create a child logger with additional context * @param {string} childContext - Context for child logger * @returns {Logger} Child logger instance */ child(childContext) { return new Logger({ ...this.options, context: this.options.context ? `${this.options.context}:${childContext}` : childContext }); } /** * Log an error message * @param {string} message - Error message * @param {Error} [error] - Error object */ error(message, error) { if (!this.isLevelEnabled('error')) return; const meta = error ? { error: error.message, stack: error.stack } : {}; const formattedMessage = this._formatMessage('error', message, meta); console.error(formattedMessage); this._writeToFile(formattedMessage, 'error'); } /** * Log a warning message * @param {string} message - Warning message * @param {Object} [meta] - Additional metadata */ warn(message, meta) { if (!this.isLevelEnabled('warn')) return; const formattedMessage = this._formatMessage('warn', message, meta); console.warn(formattedMessage); this._writeToFile(formattedMessage, 'warn'); } /** * Log an info message * @param {string} message - Info message * @param {Object} [meta] - Additional metadata */ info(message, meta) { if (!this.isLevelEnabled('info')) return; const formattedMessage = this._formatMessage('info', message, meta); console.log(formattedMessage); this._writeToFile(formattedMessage, 'info'); } /** * Log a debug message * @param {string} message - Debug message * @param {Object} [meta] - Additional metadata */ debug(message, meta) { if (!this.isLevelEnabled('debug')) return; const formattedMessage = this._formatMessage('debug', message, meta); console.log(formattedMessage); this._writeToFile(formattedMessage, 'debug'); } /** * Log a verbose message * @param {string} message - Verbose message * @param {Object} [meta] - Additional metadata */ verbose(message, meta) { if (!this.isLevelEnabled('verbose')) return; const formattedMessage = this._formatMessage('verbose', message, meta); console.log(formattedMessage); this._writeToFile(formattedMessage, 'verbose'); } /** * Set the log level * @param {string} level - New log level */ setLevel(level) { if (LOG_LEVELS[level] !== undefined) { this.options.level = level; } } /** * Check if a log level is enabled * @param {string} level - Log level to check * @returns {boolean} Whether the level is enabled */ isLevelEnabled(level) { return LOG_LEVELS[level] <= LOG_LEVELS[this.options.level]; } } // Create and export default logger instance const defaultLogger = new Logger({ context: 'app', level: process.env.NODE_ENV === 'development' ? 'debug' : 'info' }); module.exports = { Logger, defaultLogger };