refactor: Phase 2 - add error handling modules and response helpers
This commit is contained in:
135
dashcaddy-api/error-logger.js
Normal file
135
dashcaddy-api/error-logger.js
Normal file
@@ -0,0 +1,135 @@
|
||||
// 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
|
||||
};
|
||||
Reference in New Issue
Block a user