import yaml from 'yaml'
import {createStream} from 'rotating-file-stream'
import nodemailer from 'nodemailer'
/** @type {import('rotating-file-stream').RotatingFileStream | undefined} */
let logStreamUpdateDb
/** @type {import('rotating-file-stream').RotatingFileStream | undefined} */
let logStreamLogin
/** @type {import('rotating-file-stream').RotatingFileStream | undefined} */
let logStreamRead
/** @type {import('rotating-file-stream').RotatingFileStream | undefined} */
let logStreamDb
/** @type {import('rotating-file-stream').RotatingFileStream | undefined} */
let logStreamError
// Logger-Konfiguration
/**
* @typedef {Object} LoggerOptions
* @property {string} [maxSize] - Maximum size per log file (e.g., '20M')
* @property {string} [interval] - Time interval for rotation (e.g., '1d', '1h')
* @property {string} [path] - Directory path for logs
* @property {number} [maxFiles] - Number of files to retain
*/
/**
* Validates log size format
* @param {string} size - Size string to validate (e.g., '20M', '1G')
* @returns {boolean}
*/
const isValidLogSize = (size) => /^\d+[KMGB]$/.test(size)
/**
* Validates log interval format
* @param {string} interval - Interval string to validate (e.g., '1d', '1h')
* @returns {boolean}
*/
const isValidLogInterval = (interval) => /^\d+[dhms]$/.test(interval)
/**
* Validates logger configuration
* @throws {Error} If configuration is invalid
*/
const validateLoggerConfig = () => {
if (process.env.logSize && !isValidLogSize(process.env.logSize)) {
throw new Error(
`Invalid logSize format: "${process.env.logSize}". ` +
`Expected format: <number><unit> where unit is K, M, G, or B (e.g., "20M", "1G")`
)
}
if (process.env.logInterval && !isValidLogInterval(process.env.logInterval)) {
throw new Error(
`Invalid logInterval format: "${process.env.logInterval}". ` +
`Expected format: <number><unit> where unit is d (day), h (hour), m (minute), or s (second) (e.g., "1d", "30m")`
)
}
if (process.env.maxLogFiles) {
const maxFiles = parseInt(process.env.maxLogFiles)
if (isNaN(maxFiles) || maxFiles < 1) {
throw new Error(
`Invalid maxLogFiles: "${process.env.maxLogFiles}". ` +
`Expected a positive integer (e.g., "5", "10")`
)
}
}
}
/**
* Initialize all rotating file streams using env configuration.
*/
export const configLoggers=()=>
{
validateLoggerConfig()
const logSize = /** @type {`${number}M` | `${number}G` | `${number}K` | `${number}B`} */ (process.env.logSize ?? '20M')
const logInterval = /** @type {`${number}d` | `${number}h` | `${number}m` | `${number}s`} */ (process.env.logInterval ?? '1d')
const maxFiles = process.env.maxLogFiles ? parseInt(process.env.maxLogFiles) : 5
if(process.env.requestUpdateLogger)
logStreamUpdateDb = createStream('write.log', {
size: logSize,
interval: logInterval,
path: process.cwd()+'/logs/db',
maxFiles: maxFiles
})
if(process.env.loginLogger)
logStreamLogin = createStream('login.log', {
size: logSize,
interval: logInterval,
path: process.cwd()+'/logs/login',
maxFiles: maxFiles
})
if(process.env.readLogger)
logStreamRead = createStream('get.log', {
size: logSize,
interval: logInterval,
path: process.cwd()+'/logs/db',
maxFiles: maxFiles
})
if(process.env.dbLogger)
logStreamDb = createStream('db.log', {
size: logSize,
interval: logInterval,
path: process.cwd()+'/logs/db',
maxFiles: maxFiles
})
logStreamError = createStream('error.log', {
size: logSize,
interval: logInterval,
path: process.cwd()+'/logs/db',
maxFiles: maxFiles
})
}
// Middlewares for logging Requests
/**
* Logs write/update requests.
* @param {import('express').Request & { session?: { user?: { fullName?: string, isBotAuth?: boolean } } }} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
export const requestUpdateLogger = (req, res, next) => {
const sessionUser = req.session?.user
if(process.env.requestUpdateLogger && !sessionUser?.isBotAuth){
const logEntry = {
date: new Date().toISOString(),
method: req.method, // Hinzufügen des Request-Typs (HTTP-Methode)
ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress ,
clientDomain: req.headers.referer || req.headers.origin || 'unknown',
user: sessionUser?.fullName || null,
query: req.query,
path: req.baseUrl+req.path,
body: req.body, //JSON.stringify(req.body),
}
const yamlLogEntry = yaml.stringify(logEntry)
logStreamUpdateDb.write(yamlLogEntry + '\n')
}
next()
}
/**
* Logs login events.
* @param {import('express').Request & { session?: { user?: { fullName?: string, isBotAuth?: boolean } } }} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
export const loginLogger = (req, res, next) => {
const sessionUser = req.session?.user
if(process.env.loginLogger && sessionUser?.fullName!="@botUser")
{
const logEntry = {
date: new Date().toISOString(),
ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress ,
clientDomain: req.headers.referer || req.headers.origin || 'unknown',
user: sessionUser?.fullName || null,
app: req.params.app,
orga: req.params.UIDroot,
}
const yamlLogEntry = yaml.stringify(logEntry)
logStreamLogin.write(yamlLogEntry + '\n')
}
next()
}
/**
* Logs read/GET requests.
* @param {import('express').Request & { session?: { user?: { fullName?: string, isBotAuth?: boolean } } }} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
export const readLogger = (req, res, next) => {
const sessionUser = req.session?.user
if(process.env.readLogger && sessionUser?.fullName!="@botUser")
{
const logEntry = {
date: new Date().toISOString(),
method: req.method, // Hinzufügen des Request-Typs (HTTP-Methode)
ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress ,
clientDomain: req.headers.referer || req.headers.origin || 'unknown',
user: sessionUser?.fullName || null,
query: req.query,
path: req.baseUrl+req.path,
}
const yamlLogEntry = yaml.stringify(logEntry)
logStreamRead.write(yamlLogEntry + '\n')
}
next()
}
// Error logger in error log
// separate errorLoggerRead, errorLoggerUpdate are here only for legacy resons
/**
* Logs generic application errors.
* @param {unknown} error
*/
export const errorLogger = (error) => {
// Handle undefined or null errors
if (!error) {
const undefinedError = new Error('Fatal error [ undefined ] - errorLogger called with undefined/null error');
console.error('=== UNDEFINED ERROR DETECTED ===');
console.error(undefinedError.stack);
// In development mode, throw to get full stack trace
if (process.env.NODE_ENV === 'development' || process.env.THROW_ON_UNDEFINED_ERROR === 'true') {
throw undefinedError;
}
const logEntry = {
date: new Date().toISOString(),
error: undefinedError.stack,
originalError: 'undefined or null'
};
const yamlLogEntry = yaml.stringify(logEntry);
logStreamError?.write(yamlLogEntry + '\n');
return;
}
// Handle non-Error objects
const err = error instanceof Error ? error : new Error(`Non-Error object: ${JSON.stringify(error)}`)
console.error(err.stack);
const logEntry = {
date: new Date().toISOString(),
error: err.stack,
};
const yamlLogEntry = yaml.stringify(logEntry);
logStreamError?.write(yamlLogEntry + '\n');
};
/**
* Logs read-specific errors (legacy entry point).
* @param {unknown} error
*/
export const errorLoggerRead =(error)=>
{
// Handle undefined or null errors
if (!error) {
const undefinedError = new Error('Fatal error [ undefined ] - errorLoggerRead called with undefined/null error');
console.error('=== UNDEFINED ERROR DETECTED IN READ ===');
console.error(undefinedError.stack);
// In development mode, throw to get full stack trace
if (process.env.NODE_ENV === 'development' || process.env.THROW_ON_UNDEFINED_ERROR === 'true') {
throw undefinedError;
}
const logEntry = {
date: new Date().toISOString(),
error: undefinedError.stack,
originalError: 'undefined or null'
};
const yamlLogEntry = yaml.stringify(logEntry);
logStreamError?.write(yamlLogEntry + '\n');
return;
}
// Handle non-Error objects
const err = error instanceof Error ? error : new Error(`Non-Error object: ${JSON.stringify(error)}`)
console.error(err.stack);
const logEntry = {
date: new Date().toISOString(),
error: err.stack,
};
const yamlLogEntry = yaml.stringify(logEntry);
logStreamError?.write(yamlLogEntry + '\n');
};
/**
* Logs update-specific errors (legacy entry point).
* @param {unknown} error
*/
export const errorLoggerUpdate =(error)=>
{
// Handle undefined or null errors
if (!error) {
const undefinedError = new Error('Fatal error [ undefined ] - errorLoggerUpdate called with undefined/null error');
console.error('=== UNDEFINED ERROR DETECTED IN UPDATE ===');
console.error(undefinedError.stack);
// In development mode, throw to get full stack trace
if (process.env.NODE_ENV === 'development' || process.env.THROW_ON_UNDEFINED_ERROR === 'true') {
throw undefinedError;
}
const logEntry = {
date: new Date().toISOString(),
error: undefinedError.stack,
originalError: 'undefined or null'
};
const yamlLogEntry = yaml.stringify(logEntry);
if (logStreamError) {
logStreamError?.write(yamlLogEntry + '\n');
}
return;
}
// Handle non-Error objects
const err = error instanceof Error ? error : new Error(`Non-Error object: ${JSON.stringify(error)}`)
console.error(err.stack);
const logEntry = {
date: new Date().toISOString(),
error: err.stack,
};
const yamlLogEntry = yaml.stringify(logEntry);
logStreamError?.write(yamlLogEntry + '\n');
};
/**
* Custom database logger
* @param {string} logMessage - The message to log
* @param {string} [source] - Optional source/context identifier (e.g., 'query', 'transaction', 'migration')
*/
export const dbLogger = (logMessage, source) => {
if(process.env.dbLogger)
{
// Remove backslash line continuations from the message
const cleanedMessage = logMessage.replace(/\\\s+/g, ' ').trim();
const logEntry = {
date: new Date().toISOString(),
message: cleanedMessage,
...(source && { source })
};
// Format with literal block scalar style (|) for multi-line strings
let yamlLogEntry = yaml.stringify(logEntry, { lineWidth: -1 });
// Convert quoted message to literal block scalar style
yamlLogEntry = yamlLogEntry.replace(/^message: .*$/m, (match) => {
return `message: |\n${cleanedMessage.split('\n').map(line => ' ' + line).join('\n')}`;
});
logStreamDb?.write(yamlLogEntry + '\n');
}
};
/**
* Logs database errors using the DB logger stream.
* @param {Error} error
*/
export const dbErrorLogger = (error) => {
if(process.env.dbLogger)
{
const logEntry = {
date: new Date().toISOString(),
error: error.stack,
};
const yamlLogEntry = yaml.stringify(logEntry);
logStreamDb?.write(yamlLogEntry + '\n');
}
};