Source: utils/requestLogger.js


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');
    }
};