bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
288 lines (250 loc) • 7.82 kB
JavaScript
/**
* @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
};