136 lines
3.6 KiB
JavaScript
136 lines
3.6 KiB
JavaScript
// Error Logger Utility
|
|
// Centralized error logging with rotation and request context tracking
|
|
|
|
const fsp = require('fs').promises;
|
|
const path = require('path');
|
|
const { LIMITS } = require('./constants');
|
|
|
|
const ERROR_LOG_FILE = path.join(__dirname, 'error.log');
|
|
const MAX_ERROR_LOG_SIZE = LIMITS.ERROR_LOG_SIZE;
|
|
|
|
/**
|
|
* Check if file exists
|
|
*/
|
|
async function exists(filepath) {
|
|
try {
|
|
await fsp.access(filepath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log error with context and rotation
|
|
* @param {string} context - Where the error occurred
|
|
* @param {Error|string} error - The error to log
|
|
* @param {Object} additionalInfo - Additional context (req, etc.)
|
|
*/
|
|
async function logError(context, error, additionalInfo = {}) {
|
|
const timestamp = new Date().toISOString();
|
|
|
|
// Extract request context if a request object is provided
|
|
const requestContext = extractRequestContext(additionalInfo.req);
|
|
if (additionalInfo.req) {
|
|
delete additionalInfo.req; // Remove req to avoid circular refs
|
|
}
|
|
|
|
const logEntry = {
|
|
timestamp,
|
|
context,
|
|
...requestContext,
|
|
error: {
|
|
message: error.message || error,
|
|
stack: error.stack,
|
|
code: error.code
|
|
},
|
|
...additionalInfo
|
|
};
|
|
|
|
// Format log line with request context
|
|
const contextInfo = Object.keys(requestContext).length > 0
|
|
? `\nRequest Context: ${JSON.stringify(requestContext, null, 2)}`
|
|
: '';
|
|
const logLine = `[${timestamp}] ${context}: ${error.message || error}\n${error.stack || ''}${contextInfo}\nAdditional Info: ${JSON.stringify(additionalInfo, null, 2)}\n${'='.repeat(80)}\n`;
|
|
|
|
try {
|
|
// Rotate log if it exceeds max size
|
|
await rotateLogIfNeeded();
|
|
await fsp.appendFile(ERROR_LOG_FILE, logLine);
|
|
} catch (e) {
|
|
console.error('Failed to write to error log', e.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract request context from Express request object
|
|
*/
|
|
function extractRequestContext(req) {
|
|
if (!req) return {};
|
|
|
|
const clientIP = req.ip || req.socket?.remoteAddress || '';
|
|
|
|
return {
|
|
requestId: req.id,
|
|
ip: clientIP,
|
|
userAgent: req.get('user-agent'),
|
|
method: req.method,
|
|
path: req.path
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Rotate log file if it exceeds max size
|
|
*/
|
|
async function rotateLogIfNeeded() {
|
|
try {
|
|
const stats = await fsp.stat(ERROR_LOG_FILE);
|
|
if (stats.size > MAX_ERROR_LOG_SIZE) {
|
|
const rotated = ERROR_LOG_FILE + '.1';
|
|
if (await exists(rotated)) {
|
|
await fsp.unlink(rotated);
|
|
}
|
|
await fsp.rename(ERROR_LOG_FILE, rotated);
|
|
}
|
|
} catch (_) {
|
|
// File may not exist yet, that's fine
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return a safe error message to the client without leaking internals
|
|
*/
|
|
function safeErrorMessage(error) {
|
|
const msg = error.message || String(error);
|
|
|
|
// Detect port conflict errors from Docker
|
|
const portMatch = msg.match(/exposing port TCP [^:]*:(\d+)/);
|
|
if (portMatch || msg.includes('port is already allocated') || msg.includes('ports are not available')) {
|
|
const port = portMatch ? portMatch[1] : 'requested';
|
|
return `Port ${port} is already in use. Please choose a different port or stop the conflicting service.`;
|
|
}
|
|
|
|
// Detect container not found errors
|
|
if (msg.includes('No such container')) {
|
|
return 'Container not found';
|
|
}
|
|
|
|
// Detect network errors
|
|
if (msg.includes('ECONNREFUSED') || msg.includes('ETIMEDOUT')) {
|
|
return 'Service unavailable';
|
|
}
|
|
|
|
// Generic safe message for unknown errors
|
|
if (process.env.NODE_ENV === 'production') {
|
|
return 'An error occurred. Please try again or contact support.';
|
|
}
|
|
|
|
// In development, show the actual error
|
|
return msg;
|
|
}
|
|
|
|
module.exports = {
|
|
logError,
|
|
safeErrorMessage
|
|
};
|