simple-logmate
Version:
A flexible and powerful logging utility for Node.js applications.
240 lines (217 loc) • 8.99 kB
JavaScript
const fs = require('fs');
const path = require('path');
const { format } = require('./utils');
// ANSI color codes for different log levels
const colors = {
debug: '\x1b[36m', // Cyan
info: '\x1b[32m', // Green
warn: '\x1b[33m', // Yellow
error: '\x1b[31m', // Red
reset: '\x1b[0m' // Reset color
};
/**
* Logger class providing flexible logging functionality with multiple transports
* and log rotation capabilities.
*/
class Logger {
/**
* Creates a new Logger instance
* @param {Object} options - Configuration options
* @param {string} [options.level='info'] - Minimum log level
* @param {string} [options.format='[{timestamp}] {level}: {message}'] - Log message format
* @param {string} [options.filePath=null] - Path to log file
* @param {number} [options.maxFileSize=1048576] - Max file size in bytes before rotation
* @param {string[]} [options.transports=['console']] - Enabled transport types
* @param {boolean} [options.httpRequest=false] - Enable automatic HTTP request logging
*/
constructor(options = {}) {
// Initialize configuration with defaults
this.level = options.level || 'info';
this.format = options.format || '[{timestamp}] {level}: {message}';
this.filePath = options.filePath || null;
this.maxFileSize = options.maxFileSize || 1024 * 1024; // 1MB default
this.transports = options.transports || ['console'];
this.httpRequest = options.httpRequest || false;
// Queue for managing asynchronous file writes
this.logQueue = [];
this.isWriting = false;
// Set up file logging if enabled
if (this.filePath && this.transports.includes('file')) {
const dir = path.dirname(this.filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
// Set up automatic HTTP request logging if enabled
if (this.httpRequest) {
this.setupAutoRequestLogging();
}
}
/**
* Sets up automatic HTTP request logging middleware
* @private
*/
setupAutoRequestLogging() {
if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'test') {
try {
const http = require('http');
const originalCreateServer = http.createServer;
http.createServer = (...args) => {
const server = originalCreateServer.apply(http, args);
server.on('request', (req, res) => {
const start = Date.now();
res.on('finish', () => {
const responseTime = Date.now() - start;
this.logRequest(req, res, responseTime);
});
});
return server;
};
} catch (error) {
this.warn('Failed to setup automatic HTTP request logging:', error);
}
}
}
/**
* Generates a formatted timestamp string in local time
* @returns {string} Formatted timestamp (YYYY-MM-DD HH:mm:ss.SSS)
*/
getLocalTimestamp() {
const date = new Date();
return date.toLocaleString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3,
hour12: false
}).replace(',', '');
}
/**
* Core logging function that handles message formatting and transport routing
* @param {string} level - Log level (debug, info, warn, error)
* @param {string|Error} message - Log message content or Error object
*/
log(level, ...args) {
if (this.shouldLog(level)) {
const timestamp = this.getLocalTimestamp();
let formattedMessage;
// Process all arguments to create a complete message
const processedMessage = args.map(arg => {
if (arg instanceof Error) {
// Return error message and stack trace together
return `${arg.name}: ${arg.message}\n${arg.stack}`;
}
return String(arg);
}).join(' ');
formattedMessage = format(this.format, {
timestamp,
level,
message: processedMessage
});
// Route message to each enabled transport
this.transports.forEach(transport => {
if (transport === 'console') {
// Use appropriate console method based on level with colors
const consoleMethod = level === 'error' ? 'error' :
level === 'warn' ? 'warn' : 'log';
console[consoleMethod](colors[level] + formattedMessage + colors.reset);
} else if (transport === 'file' && this.filePath) {
this.logToFile(formattedMessage);
}
});
}
}
/**
* Queues a message for writing to the log file
* @param {string} message - Formatted log message
*/
logToFile(message) {
this.logQueue.push(message);
if (!this.isWriting) {
this.isWriting = true;
this.writeLogs();
}
}
/**
* Processes queued log messages and writes them to file
* Handles file rotation when size limit is reached
*/
async writeLogs() {
try {
while (this.logQueue.length > 0) {
const message = this.logQueue.shift();
// Add error handling for file access
if (!this.filePath) {
throw new Error('File path not configured for file transport');
}
await fs.promises.appendFile(this.filePath, message + '\n');
await this.checkFileSize();
}
} catch (error) {
console.error('Error writing to log file:', error);
// Re-add failed message back to queue
if (message) this.logQueue.unshift(message);
} finally {
this.isWriting = false;
}
}
/**
* Checks log file size and rotates if necessary
* @throws {Error} If file operations fail
*/
async checkFileSize() {
try {
const stats = await fs.promises.stat(this.filePath);
if (stats.size > this.maxFileSize) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const rotatedFilePath = this.filePath.replace(/(\.\w+)?$/, `-${timestamp}$1`);
await fs.promises.rename(this.filePath, rotatedFilePath);
}
} catch (error) {
console.error('Error during log rotation:', error);
}
}
/**
* Determines if a message should be logged based on level hierarchy
* @param {string} level - Log level to check
* @returns {boolean} Whether the message should be logged
*/
shouldLog(level) {
const levels = ['debug', 'info', 'warn', 'error'];
return levels.indexOf(level) >= levels.indexOf(this.level);
}
/**
* Logs HTTP request details
* @param {Object} req - Request object
* @param {Object} res - Response object
* @param {number} responseTime - Response time in milliseconds
*/
logRequest(req, res, responseTime) {
// Add validation for required properties
if (!req || !res) {
this.warn('Invalid request or response objects provided to logRequest');
return;
}
const requestLog = {
method: req.method || 'UNKNOWN',
url: req.url || 'UNKNOWN',
status: res.statusCode || 0,
responseTime: `${responseTime}ms`,
userAgent: req.headers?.['user-agent'] || 'UNKNOWN',
ip: req.ip || req.connection?.remoteAddress || 'UNKNOWN'
};
// Log the main request info using the same format as other logs
this.info(`${requestLog.method} ${requestLog.url} ${requestLog.status} ${requestLog.responseTime}`);
// Log detailed request info as debug
this.debug('Request Details:\n' + JSON.stringify(requestLog, null, 2));
}
// Convenience methods for different log levels
info(...args) { this.log('info', ...args); }
warn(...args) { this.log('warn', ...args); }
error(...args) { this.log('error', ...args); }
debug(...args) { this.log('debug', ...args); }
}
module.exports = Logger;