refactor(utils): Extract utilities from server.js
- Create src/utils/http.js - fetchT and HTTP helpers - Create src/utils/logging.js - Structured logging and error logging - Create src/utils/responses.js - Standard API responses - Create src/utils/async-handler.js - Async wrapper with error handling - Create src/utils/index.js - Consolidated exports Removes scattered helper functions from server.js
This commit is contained in:
119
dashcaddy-api/src/utils/logging.js
Normal file
119
dashcaddy-api/src/utils/logging.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Logging utilities - Structured logging and error handling
|
||||
*/
|
||||
const fsp = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
||||
|
||||
/**
|
||||
* Create a structured logger
|
||||
*/
|
||||
function createLogger(LOG_LEVEL) {
|
||||
function log(level, context, message, data = {}) {
|
||||
if (LOG_LEVELS[level] < LOG_LEVEL) return;
|
||||
|
||||
const entry = {
|
||||
t: new Date().toISOString(),
|
||||
level,
|
||||
ctx: context,
|
||||
msg: message,
|
||||
};
|
||||
|
||||
if (Object.keys(data).length) entry.data = data;
|
||||
|
||||
const fn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.info;
|
||||
fn(JSON.stringify(entry));
|
||||
}
|
||||
|
||||
log.info = (ctx, msg, data) => log('info', ctx, msg, data);
|
||||
log.warn = (ctx, msg, data) => log('warn', ctx, msg, data);
|
||||
log.error = (ctx, msg, data) => log('error', ctx, msg, data);
|
||||
log.debug = (ctx, msg, data) => log('debug', ctx, msg, data);
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced error logging with context tracking
|
||||
*/
|
||||
async function logError(ERROR_LOG_FILE, MAX_ERROR_LOG_SIZE, context, error, additionalInfo = {}, log) {
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Extract request context
|
||||
const requestContext = {};
|
||||
if (additionalInfo.req) {
|
||||
const req = additionalInfo.req;
|
||||
const clientIP = req.ip || req.socket?.remoteAddress || '';
|
||||
requestContext.requestId = req.id;
|
||||
requestContext.ip = clientIP;
|
||||
requestContext.userAgent = req.get('user-agent');
|
||||
requestContext.method = req.method;
|
||||
requestContext.path = req.path;
|
||||
delete additionalInfo.req;
|
||||
}
|
||||
|
||||
const logEntry = {
|
||||
timestamp,
|
||||
context,
|
||||
...requestContext,
|
||||
error: {
|
||||
message: error.message || error,
|
||||
stack: error.stack,
|
||||
code: error.code
|
||||
},
|
||||
...additionalInfo
|
||||
};
|
||||
|
||||
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
|
||||
try {
|
||||
const stats = await fsp.stat(ERROR_LOG_FILE);
|
||||
if (stats.size > MAX_ERROR_LOG_SIZE) {
|
||||
const rotated = ERROR_LOG_FILE + '.1';
|
||||
const exists = await fsp.access(rotated).then(() => true).catch(() => false);
|
||||
if (exists) await fsp.unlink(rotated);
|
||||
await fsp.rename(ERROR_LOG_FILE, rotated);
|
||||
}
|
||||
} catch (_) { /* file may not exist yet */ }
|
||||
|
||||
await fsp.appendFile(ERROR_LOG_FILE, logLine);
|
||||
} catch (e) {
|
||||
if (log && log.error) {
|
||||
log.error('errorlog', 'Failed to write to error log', { error: e.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a safe error message without leaking internals
|
||||
*/
|
||||
function safeErrorMessage(error) {
|
||||
const msg = error.message || String(error);
|
||||
|
||||
// Detect port conflict errors
|
||||
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 `[DC-200] Port ${port} is already in use. Try a different port or stop the service using that port first.`;
|
||||
}
|
||||
|
||||
// Only expose short, user-facing messages
|
||||
if (msg.length < 200 && !msg.includes('/') && !msg.includes('\\') && !msg.includes(' at ')) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
return 'An internal error occurred';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
LOG_LEVELS,
|
||||
createLogger,
|
||||
logError,
|
||||
safeErrorMessage,
|
||||
};
|
||||
Reference in New Issue
Block a user