CentralBackend/middleware/logger.js
2025-09-07 12:44:01 +02:00

228 lines
6.3 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const { createLogger, format, transports } = require('winston');
const crypto = require('crypto');
const DailyRotateFile = require('winston-daily-rotate-file');
const LOG_DIR = process.env.LOG_DIR || path.join(__dirname, '..', 'logs');
const NODE_ENV = process.env.NODE_ENV || 'development';
const LOG_LEVEL = process.env.LOG_LEVEL || 'info';
const SERVICE_NAME = 'central-server';
// ensure log directory exists
try {
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true });
}
} catch (e) {
// fallback in case of permission issues
console.error('Failed to ensure log directory', e);
}
// use JSON (one object per line) to be FluentBit/OpenSearch-friendly
const jsonFormat = format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.splat(),
format.json()
);
// Custom timestamp formatter for console logs
const customTimestampFormat = format((info) => {
const date = new Date(info.timestamp || Date.now());
const pad = (n) => n < 10 ? '0' + n : n;
const day = pad(date.getDate());
const month = pad(date.getMonth() + 1);
const year = date.getFullYear();
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
const seconds = pad(date.getSeconds());
info.timestamp = `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`;
return info;
});
const loggerTransports = [
new transports.Console({
level: LOG_LEVEL,
format: format.combine(
format.timestamp(),
customTimestampFormat(),
format.colorize({ all: NODE_ENV === 'development' }),
// keep console human-friendly in dev, but still structured in prod
NODE_ENV === 'development'
? format.printf(({ timestamp, level, message, ...meta }) => {
const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : '';
return `${timestamp} ${level}: ${message} ${metaStr}`;
})
: jsonFormat
)
})
];
// add daily rotate file transports only in production
if (NODE_ENV === 'production') {
loggerTransports.push(
new DailyRotateFile({
filename: path.join(LOG_DIR, 'error-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
level: 'error',
format: jsonFormat,
maxSize: '5m',
maxFiles: '7d',
zippedArchive: true
}),
new DailyRotateFile({
filename: path.join(LOG_DIR, 'combined-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
level: LOG_LEVEL,
format: jsonFormat,
maxSize: '10m',
maxFiles: '7d',
zippedArchive: true
})
);
}
const logger = createLogger({
level: LOG_LEVEL,
defaultMeta: { service: SERVICE_NAME, env: NODE_ENV },
transports: loggerTransports,
exitOnError: false
});
// capture console.* and route to winston so third-party libs are logged
const _origConsole = {
debug: console.debug,
info: console.info,
warn: console.warn,
error: console.error,
log: console.log
};
['debug','info','warn','error','log'].forEach(level => {
console[level] = (...args) => {
try {
const msg = args.map(a => {
if (typeof a === 'string') return a;
if (a instanceof Error) return a.stack || a.message;
try { return JSON.stringify(a); } catch (e) { return String(a); }
}).join(' ');
// map console.log -> info
const winLevel = level === 'log' ? 'info' : level;
if (logger[winLevel]) logger[winLevel](msg);
else logger.info(msg);
} catch (e) {
_origConsole[level](...args);
}
};
});
// runtime setter for log level
function setLogLevel(level) {
try {
logger.level = level;
logger.transports.forEach((t) => {
if (typeof t.level !== 'undefined') t.level = level;
});
logger.info('logLevel:changed', { level });
return true;
} catch (e) {
_origConsole.error('Failed to set log level', e);
return false;
}
}
// Express middleware: attach requestId, log start and end with duration
function requestLogger(req, res, next) {
const requestId = req.headers['x-request-id'] || crypto.randomBytes(8).toString('hex');
req.id = requestId;
const start = process.hrtime();
logger.info('request:start', {
requestId,
method: req.method,
url: req.originalUrl || req.url,
ip: req.ip,
headers: {
referer: req.get('referer') || '',
origin: req.get('origin') || '',
authorization: req.get('authorization') ? 'present' : 'absent'
}
});
res.on('finish', () => {
const [s, ns] = process.hrtime(start);
const durationMs = Math.round((s * 1e3) + (ns / 1e6));
logger.info('request:finish', {
requestId,
method: req.method,
url: req.originalUrl || req.url,
status: res.statusCode,
durationMs,
ip: req.ip,
user: req.user ? { id: req.user.id, email: req.user.email } : undefined
});
});
res.on('close', () => {
const [s, ns] = process.hrtime(start);
const durationMs = Math.round((s * 1e3) + (ns / 1e6));
logger.warn('request:closed', {
requestId,
method: req.method,
url: req.originalUrl || req.url,
status: res.statusCode,
durationMs,
ip: req.ip
});
});
next();
}
// Add per-transport setter and a getter for current levels
function setTransportLevel(transportIdentifier, level) {
try {
let matched = false;
const id = (transportIdentifier || 'all').toString().toLowerCase();
logger.transports.forEach((t) => {
const ctorName = (t.constructor && t.constructor.name) ? t.constructor.name.toLowerCase() : '';
const tName = (t.name || '').toString().toLowerCase();
// match "console", "file", transport class name, or "all"
if (id === 'all' || ctorName.includes(id) || tName.includes(id) || (id === 'file' && ctorName.includes('file')) || (id === 'console' && ctorName.includes('console'))) {
if (typeof t.level !== 'undefined') {
t.level = level;
matched = true;
}
}
});
if (matched) {
logger.info('logLevel:transport_changed', { transport: transportIdentifier, level });
return true;
} else {
_origConsole.warn('setTransportLevel: no transport matched', transportIdentifier);
return false;
}
} catch (e) {
_origConsole.error('Failed to set transport level', e);
return false;
}
}
function getLoggerLevels() {
try {
return {
loggerLevel: logger.level,
transports: logger.transports.map(t => ({
name: (t.constructor && t.constructor.name) || t.name || 'unknown',
level: typeof t.level !== 'undefined' ? t.level : null
}))
};
} catch (e) {
_origConsole.error('Failed to read logger levels', e);
return null;
}
}
module.exports = { logger, requestLogger, setLogLevel, setTransportLevel, getLoggerLevels };