2021-10-31 17:55:28 -05:00
|
|
|
const Path = require('path')
|
2022-07-05 19:53:01 -05:00
|
|
|
const fs = require('../libs/fsExtra')
|
2021-10-31 17:55:28 -05:00
|
|
|
|
2024-02-15 16:46:19 -06:00
|
|
|
const Logger = require('../Logger')
|
2022-03-20 16:41:06 -05:00
|
|
|
const DailyLog = require('../objects/DailyLog')
|
2021-10-31 17:55:28 -05:00
|
|
|
|
2024-02-15 16:46:19 -06:00
|
|
|
const { LogLevel } = require('../utils/constants')
|
2021-10-31 17:55:28 -05:00
|
|
|
|
|
|
|
|
const TAG = '[LogManager]'
|
|
|
|
|
|
2024-02-15 16:46:19 -06:00
|
|
|
/**
|
|
|
|
|
* @typedef LogObject
|
|
|
|
|
* @property {string} timestamp
|
|
|
|
|
* @property {string} source
|
|
|
|
|
* @property {string} message
|
|
|
|
|
* @property {string} levelName
|
|
|
|
|
* @property {number} level
|
|
|
|
|
*/
|
|
|
|
|
|
2021-10-31 17:55:28 -05:00
|
|
|
class LogManager {
|
2023-07-04 18:14:44 -05:00
|
|
|
constructor() {
|
2022-12-15 17:30:45 -06:00
|
|
|
this.DailyLogPath = Path.posix.join(global.MetadataPath, 'logs', 'daily')
|
|
|
|
|
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
|
2021-10-31 17:55:28 -05:00
|
|
|
|
2024-02-15 16:46:19 -06:00
|
|
|
/** @type {DailyLog} */
|
2021-10-31 17:55:28 -05:00
|
|
|
this.currentDailyLog = null
|
2024-02-15 16:46:19 -06:00
|
|
|
|
|
|
|
|
/** @type {LogObject[]} */
|
2021-10-31 17:55:28 -05:00
|
|
|
this.dailyLogBuffer = []
|
2024-02-15 16:46:19 -06:00
|
|
|
|
|
|
|
|
/** @type {string[]} */
|
2021-10-31 17:55:28 -05:00
|
|
|
this.dailyLogFiles = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get loggerDailyLogsToKeep() {
|
2023-07-04 18:14:44 -05:00
|
|
|
return global.ServerSettings.loggerDailyLogsToKeep || 7
|
2021-10-31 17:55:28 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-16 14:45:54 -06:00
|
|
|
get loggerScannerLogsToKeep() {
|
|
|
|
|
return global.ServerSettings.loggerScannerLogsToKeep || 2
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 22:48:02 -06:00
|
|
|
async enforceDailyLogRetention() {
|
|
|
|
|
while (this.dailyLogFiles.length > this.loggerDailyLogsToKeep) {
|
|
|
|
|
await this.removeLogFile(this.dailyLogFiles[0])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-15 17:30:45 -06:00
|
|
|
async ensureLogDirs() {
|
2025-12-01 18:00:34 +02:00
|
|
|
try {
|
|
|
|
|
await fs.ensureDir(this.DailyLogPath)
|
|
|
|
|
await fs.ensureDir(this.ScanLogPath)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`[LogManager] Failed to create log directories at "${this.DailyLogPath}": ${error.message}`)
|
|
|
|
|
throw new Error(`[LogManager] Failed to create log directories at "${this.DailyLogPath}"`, { cause: error })
|
|
|
|
|
}
|
2022-12-15 17:30:45 -06:00
|
|
|
}
|
|
|
|
|
|
2024-02-15 16:46:19 -06:00
|
|
|
/**
|
|
|
|
|
* 1. Ensure log directories exist
|
|
|
|
|
* 2. Load daily log files
|
2026-01-27 22:48:02 -06:00
|
|
|
* 3. Create/set current daily log file
|
|
|
|
|
* 4. Remove old daily log files
|
|
|
|
|
* 5. Log any buffered daily logs
|
2024-02-15 16:46:19 -06:00
|
|
|
*/
|
2021-10-31 17:55:28 -05:00
|
|
|
async init() {
|
2022-12-15 17:30:45 -06:00
|
|
|
await this.ensureLogDirs()
|
|
|
|
|
|
2021-10-31 17:55:28 -05:00
|
|
|
// Load daily logs
|
|
|
|
|
await this.scanLogFiles()
|
|
|
|
|
|
2024-02-15 16:46:19 -06:00
|
|
|
// set current daily log file or create if does not exist
|
2022-12-15 17:30:45 -06:00
|
|
|
const currentDailyLogFilename = DailyLog.getCurrentDailyLogFilename()
|
2021-10-31 17:55:28 -05:00
|
|
|
Logger.info(TAG, `Init current daily log filename: ${currentDailyLogFilename}`)
|
|
|
|
|
|
2024-02-15 16:46:19 -06:00
|
|
|
this.currentDailyLog = new DailyLog(this.DailyLogPath)
|
2021-10-31 17:55:28 -05:00
|
|
|
|
|
|
|
|
if (this.dailyLogFiles.includes(currentDailyLogFilename)) {
|
|
|
|
|
Logger.debug(TAG, `Daily log file already exists - set in Logger`)
|
2021-10-31 19:10:45 -05:00
|
|
|
await this.currentDailyLog.loadLogs()
|
2021-10-31 17:55:28 -05:00
|
|
|
} else {
|
|
|
|
|
this.dailyLogFiles.push(this.currentDailyLog.filename)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 22:48:02 -06:00
|
|
|
// Check remove extra daily logs
|
|
|
|
|
await this.enforceDailyLogRetention()
|
|
|
|
|
|
2024-02-15 16:46:19 -06:00
|
|
|
// Log buffered daily logs
|
2021-10-31 17:55:28 -05:00
|
|
|
if (this.dailyLogBuffer.length) {
|
2021-10-31 19:10:45 -05:00
|
|
|
this.dailyLogBuffer.forEach((logObj) => {
|
|
|
|
|
this.currentDailyLog.appendLog(logObj)
|
|
|
|
|
})
|
2021-10-31 17:55:28 -05:00
|
|
|
this.dailyLogBuffer = []
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-15 16:46:19 -06:00
|
|
|
/**
|
|
|
|
|
* Load all daily log filenames in /metadata/logs/daily
|
|
|
|
|
*/
|
2021-10-31 17:55:28 -05:00
|
|
|
async scanLogFiles() {
|
2022-12-15 17:30:45 -06:00
|
|
|
const dailyFiles = await fs.readdir(this.DailyLogPath)
|
2024-02-15 16:46:19 -06:00
|
|
|
if (dailyFiles?.length) {
|
2021-10-31 17:55:28 -05:00
|
|
|
dailyFiles.forEach((logFile) => {
|
|
|
|
|
if (Path.extname(logFile) === '.txt') {
|
2021-10-31 19:10:45 -05:00
|
|
|
Logger.debug('Daily Log file found', logFile)
|
2021-10-31 17:55:28 -05:00
|
|
|
this.dailyLogFiles.push(logFile)
|
|
|
|
|
} else {
|
|
|
|
|
Logger.debug(TAG, 'Unknown File in Daily log files dir', logFile)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
this.dailyLogFiles.sort()
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-15 16:46:19 -06:00
|
|
|
/**
|
2025-12-01 18:00:34 +02:00
|
|
|
*
|
|
|
|
|
* @param {string} filename
|
2024-02-15 16:46:19 -06:00
|
|
|
*/
|
2021-10-31 17:55:28 -05:00
|
|
|
async removeLogFile(filename) {
|
2022-12-15 17:30:45 -06:00
|
|
|
const fullPath = Path.join(this.DailyLogPath, filename)
|
|
|
|
|
const exists = await fs.pathExists(fullPath)
|
2021-10-31 17:55:28 -05:00
|
|
|
if (!exists) {
|
|
|
|
|
Logger.error(TAG, 'Invalid log dne ' + fullPath)
|
2025-12-01 18:00:34 +02:00
|
|
|
this.dailyLogFiles = this.dailyLogFiles.filter((dlf) => dlf !== filename)
|
2021-10-31 17:55:28 -05:00
|
|
|
} else {
|
|
|
|
|
try {
|
|
|
|
|
await fs.unlink(fullPath)
|
|
|
|
|
Logger.info(TAG, 'Removed daily log: ' + filename)
|
2025-12-01 18:00:34 +02:00
|
|
|
this.dailyLogFiles = this.dailyLogFiles.filter((dlf) => dlf !== filename)
|
2021-10-31 17:55:28 -05:00
|
|
|
} catch (error) {
|
|
|
|
|
Logger.error(TAG, 'Failed to unlink log file ' + fullPath)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-15 16:46:19 -06:00
|
|
|
/**
|
2025-12-01 18:00:34 +02:00
|
|
|
*
|
|
|
|
|
* @param {LogObject} logObj
|
2024-02-15 16:46:19 -06:00
|
|
|
*/
|
|
|
|
|
async logToFile(logObj) {
|
|
|
|
|
// Fatal crashes get logged to a separate file
|
|
|
|
|
if (logObj.level === LogLevel.FATAL) {
|
|
|
|
|
await this.logCrashToFile(logObj)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Buffer when logging before daily logs have been initialized
|
2021-10-31 17:55:28 -05:00
|
|
|
if (!this.currentDailyLog) {
|
|
|
|
|
this.dailyLogBuffer.push(logObj)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check log rolls to next day
|
|
|
|
|
if (this.currentDailyLog.id !== DailyLog.getCurrentDateString()) {
|
2024-02-15 16:46:19 -06:00
|
|
|
this.currentDailyLog = new DailyLog(this.DailyLogPath)
|
2026-01-27 22:48:02 -06:00
|
|
|
|
|
|
|
|
// Track the new daily log file so retention works for long-running servers
|
|
|
|
|
if (!this.dailyLogFiles.includes(this.currentDailyLog.filename)) {
|
|
|
|
|
this.dailyLogFiles.push(this.currentDailyLog.filename)
|
2021-10-31 17:55:28 -05:00
|
|
|
}
|
2026-01-27 22:48:02 -06:00
|
|
|
|
|
|
|
|
await this.enforceDailyLogRetention()
|
2021-10-31 17:55:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Append log line to log file
|
2024-02-15 16:46:19 -06:00
|
|
|
return this.currentDailyLog.appendLog(logObj)
|
2021-10-31 17:55:28 -05:00
|
|
|
}
|
2021-10-31 19:10:45 -05:00
|
|
|
|
2024-02-15 16:46:19 -06:00
|
|
|
/**
|
2025-12-01 18:00:34 +02:00
|
|
|
*
|
|
|
|
|
* @param {LogObject} logObj
|
2024-02-15 16:46:19 -06:00
|
|
|
*/
|
|
|
|
|
async logCrashToFile(logObj) {
|
|
|
|
|
const line = JSON.stringify(logObj) + '\n'
|
|
|
|
|
|
|
|
|
|
const logsDir = Path.join(global.MetadataPath, 'logs')
|
|
|
|
|
await fs.ensureDir(logsDir)
|
|
|
|
|
const crashLogPath = Path.join(logsDir, 'crash_logs.txt')
|
2025-12-01 18:00:34 +02:00
|
|
|
return fs.writeFile(crashLogPath, line, { flag: 'a+' }).catch((error) => {
|
2024-02-15 16:46:19 -06:00
|
|
|
console.log('[LogManager] Appended crash log', error)
|
|
|
|
|
})
|
|
|
|
|
}
|
2021-10-31 19:10:45 -05:00
|
|
|
|
2024-02-15 16:46:19 -06:00
|
|
|
/**
|
|
|
|
|
* Most recent 5000 daily logs
|
2025-12-01 18:00:34 +02:00
|
|
|
*
|
2024-02-15 16:46:19 -06:00
|
|
|
* @returns {string}
|
|
|
|
|
*/
|
|
|
|
|
getMostRecentCurrentDailyLogs() {
|
|
|
|
|
return this.currentDailyLog?.logs.slice(-5000) || ''
|
2021-10-31 19:10:45 -05:00
|
|
|
}
|
2026-02-16 14:45:54 -06:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Keep the most recent N scan logs in metadata/logs/scans.
|
|
|
|
|
* Where N is the server setting `loggerScannerLogsToKeep`.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} [logDir]
|
|
|
|
|
*/
|
|
|
|
|
async purgeOldScanLogs(logDir = this.ScanLogPath) {
|
|
|
|
|
const scanLogsToKeep = this.loggerScannerLogsToKeep
|
|
|
|
|
|
|
|
|
|
let scanFiles
|
|
|
|
|
try {
|
|
|
|
|
scanFiles = await fs.readdir(logDir)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
Logger.warn(TAG, `Failed to read scan log dir "${logDir}": ${error.message}`)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const scanLogFiles = (scanFiles || []).filter((f) => Path.extname(f) === '.txt').sort()
|
|
|
|
|
if (scanLogFiles.length <= scanLogsToKeep) return
|
|
|
|
|
|
|
|
|
|
const filesToRemove = scanLogFiles.slice(0, scanLogFiles.length - scanLogsToKeep)
|
|
|
|
|
for (const file of filesToRemove) {
|
|
|
|
|
const fullPath = Path.join(logDir, file)
|
|
|
|
|
try {
|
|
|
|
|
await fs.unlink(fullPath)
|
|
|
|
|
Logger.info(TAG, `Removed scan log "${fullPath}"`)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
Logger.warn(TAG, `Failed to remove scan log "${fullPath}": ${error.message}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-10-31 17:55:28 -05:00
|
|
|
}
|
2025-12-01 18:00:34 +02:00
|
|
|
module.exports = LogManager
|