UNPKG

zwave-js-ui

Version:

Z-Wave Control Panel and MQTT Gateway

317 lines (316 loc) 11.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.stopCleanJob = exports.setupCleanJob = exports.setupAll = exports.module = exports.setupLogger = exports.customTransports = exports.logStream = exports.customFormat = exports.sanitizedConfig = exports.disableColors = exports.defaultLogFile = void 0; const winston_daily_rotate_file_1 = __importDefault(require("winston-daily-rotate-file")); const fs_extra_1 = require("fs-extra"); const winston_1 = __importDefault(require("winston")); const app_1 = require("../config/app"); const utils_1 = require("./utils"); const path = __importStar(require("path")); const promises_1 = require("fs/promises"); const escape_string_regexp_1 = __importDefault(require("@esm2cjs/escape-string-regexp")); const stream_1 = require("stream"); const { format, transports, addColors } = winston_1.default; const { combine, timestamp, printf, colorize, splat } = format; exports.defaultLogFile = 'z-ui_%DATE%.log'; exports.disableColors = process.env.NO_LOG_COLORS === 'true'; let transportsList = null; // ensure store and logs directories exist (0, fs_extra_1.ensureDirSync)(app_1.storeDir); (0, fs_extra_1.ensureDirSync)(app_1.logsDir); // custom colors for timestamp and module addColors({ time: 'grey', module: 'bold', }); const colorizer = colorize(); /** * Generate logger configuration starting from settings.gateway */ function sanitizedConfig(module, config) { config = config || {}; const filePath = (0, utils_1.joinPath)(app_1.logsDir, config.logFileName || exports.defaultLogFile); return { module: module || '-', enabled: config.logEnabled !== undefined ? config.logEnabled : true, level: config.logLevel || 'info', logToFile: config.logToFile !== undefined ? config.logToFile : false, filePath: filePath, }; } exports.sanitizedConfig = sanitizedConfig; /** * Return a custom logger format */ function customFormat(noColor = false) { noColor = noColor || exports.disableColors; const formats = [ splat(), // used for formats like: logger.log('info', Message %s', strinVal) timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), format((info) => { info.level = info.level.toUpperCase(); return info; })(), ]; if (!noColor) { formats.push(colorize({ level: true })); } // must be added at last formats.push(printf((info) => { if (!noColor) { info.timestamp = colorizer.colorize('time', info.timestamp); info.module = colorizer.colorize('module', info.module); } return `${info.timestamp} ${info.level} ${info.module}: ${info.message}${info.stack ? '\n' + info.stack : ''}`; })); return combine(...formats); } exports.customFormat = customFormat; exports.logStream = new stream_1.PassThrough(); /** * Create the base transports based on settings provided */ function customTransports(config) { // setup transports only once (see issue #2937) if (transportsList) { return transportsList; } transportsList = []; if (process.env.ZUI_NO_CONSOLE !== 'true') { transportsList.push(new transports.Console({ format: customFormat(), level: config.level, stderrLevels: ['error'], })); } const streamTransport = new transports.Stream({ format: customFormat(), level: config.level, stream: exports.logStream, }); transportsList.push(streamTransport); if (config.logToFile) { let fileTransport; if (process.env.DISABLE_LOG_ROTATION === 'true') { fileTransport = new transports.File({ format: customFormat(true), filename: config.filePath, level: config.level, }); } else { const options = { filename: config.filePath, auditFile: (0, utils_1.joinPath)(app_1.logsDir, 'zui-logs.audit.json'), datePattern: 'YYYY-MM-DD', createSymlink: true, symlinkName: path .basename(config.filePath) .replace(`_%DATE%`, '_current'), zippedArchive: true, maxFiles: process.env.ZUI_LOG_MAXFILES || '7d', maxSize: process.env.ZUI_LOG_MAXSIZE || '50m', level: config.level, format: customFormat(true), }; fileTransport = new winston_daily_rotate_file_1.default(options); setupCleanJob(options); } transportsList.push(fileTransport); } // giving that we re-use transports, each module will subscribe to events // increeasing the default limit of 100 prevents warnings transportsList.forEach((t) => { t.setMaxListeners(100); }); return transportsList; } exports.customTransports = customTransports; /** * Setup a logger */ function setupLogger(container, module, config) { const sanitized = sanitizedConfig(module, config); // Winston automatically reuses an existing module logger const logger = container.add(module); const moduleName = module.toUpperCase() || '-'; logger.configure({ format: combine(format((info) => { info.module = moduleName; return info; })(), format.errors({ stack: true }), format.json()), // to correctly parse errors silent: !sanitized.enabled, level: sanitized.level, transports: customTransports(sanitized), }); logger.module = module; logger.setup = (cfg) => setupLogger(container, module, cfg); return logger; } exports.setupLogger = setupLogger; const logContainer = new winston_1.default.Container(); /** * Create a new logger for a specific module */ function module(module) { return setupLogger(logContainer, module); } exports.module = module; /** * Setup all loggers starting from config */ function setupAll(config) { stopCleanJob(); transportsList.forEach((t) => { if (typeof t.close === 'function') { t.close(); } }); transportsList = null; logContainer.loggers.forEach((logger) => { logger.setup(config); }); } exports.setupAll = setupAll; let cleanJob; function setupCleanJob(settings) { if (cleanJob) { return; } let maxFilesMs; let maxFiles; let maxSizeBytes; const logger = module('LOGGER'); // convert maxFiles to milliseconds if (settings.maxFiles !== undefined) { const matches = settings.maxFiles.toString().match(/(\d+)([dhm])/); if (matches) { const value = parseInt(matches[1]); const unit = matches[2]; switch (unit) { case 'd': maxFilesMs = value * 24 * 60 * 60 * 1000; break; case 'h': maxFilesMs = value * 60 * 60 * 1000; break; case 'm': maxFilesMs = value * 60 * 1000; break; } } else { maxFiles = Number(settings.maxFiles); } } if (settings.maxSize !== undefined) { // convert maxSize to bytes const matches2 = settings.maxSize.toString().match(/(\d+)([kmg])/); if (matches2) { const value = parseInt(matches2[1]); const unit = matches2[2]; switch (unit) { case 'k': maxSizeBytes = value * 1024; break; case 'm': maxSizeBytes = value * 1024 * 1024; break; case 'g': maxSizeBytes = value * 1024 * 1024 * 1024; break; } } else { maxSizeBytes = Number(settings.maxSize); } } // clean up old log files based on maxFiles and maxSize const filePathRegExp = new RegExp((0, escape_string_regexp_1.default)(path.basename(settings.filename)).replace(/%DATE%/g, '(.*)')); const logsDir = path.dirname(settings.filename); const deleteFile = async (filePath) => { logger.info(`Deleting log file: ${filePath}`); return (0, promises_1.unlink)(filePath).catch((e) => { if (e.code !== 'ENOENT') { logger.error(`Error deleting log file: ${filePath}`, e); } }); }; const clean = async () => { try { logger.info('Cleaning up log files...'); const files = await (0, promises_1.readdir)(logsDir); const logFiles = files.filter((file) => file !== settings.symlinkName && filePathRegExp.test(file)); const fileStats = await Promise.allSettled(logFiles.map(async (file) => ({ file, stats: await (0, promises_1.stat)(path.join(logsDir, file)), }))); const logFilesStats = []; for (const res of fileStats) { if (res.status === 'fulfilled') { logFilesStats.push(res.value); } else { logger.error('Error getting file stats:', res.reason); } } logFilesStats.sort((a, b) => a.stats.mtimeMs - b.stats.mtimeMs); // sort by mtime let totalSize = 0; let deletedFiles = 0; for (const { file, stats } of logFilesStats) { const filePath = path.join(logsDir, file); totalSize += stats.size; // last modified time in milliseconds const fileMs = stats.mtimeMs; const shouldDelete = (maxSizeBytes && totalSize > maxSizeBytes) || (maxFiles && logFiles.length - deletedFiles > maxFiles) || (maxFilesMs && fileMs && Date.now() - fileMs > maxFilesMs); if (shouldDelete) { await deleteFile(filePath); deletedFiles++; } } } catch (e) { logger.error('Error cleaning up log files:', e); } }; cleanJob = setInterval(clean, 60 * 60 * 1000); clean().catch(() => { }); } exports.setupCleanJob = setupCleanJob; function stopCleanJob() { if (cleanJob) { clearInterval(cleanJob); cleanJob = undefined; } } exports.stopCleanJob = stopCleanJob; exports.default = logContainer.loggers;