UNPKG

@iobroker/js-controller-common-db

Version:

The Library contains the common utils for the ioBroker controller which can be used by db classes too, as they do not rely on the db (circular dependencies).

471 lines 23.1 kB
import winston from 'winston'; import DailyRotateFile from 'winston-daily-rotate-file'; import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import * as tools from '../../lib/common/tools.js'; import Transport from 'winston-transport'; import { LEVEL } from 'triple-beam'; import deepClone from 'deep-clone'; import * as url from 'node:url'; import { createRequire } from 'node:module'; // eslint-disable-next-line unicorn/prefer-module const thisDir = url.fileURLToPath(new URL('.', import.meta.url || `file://${__filename}`)); // eslint-disable-next-line unicorn/prefer-module const require = createRequire(import.meta.url || `file://${__filename}`); const hostname = tools.getHostName(); let SysLog; try { SysLog = require('winston-syslog').Syslog; } catch { //console.log('No syslog support'); } let Seq; try { Seq = require('@datalust/winston-seq').SeqTransport; } catch { //console.log('No seq support'); } // We must check if SysLog is defined before extending it const IoSysLog = SysLog && class extends SysLog { constructor(options) { super(options); } log(info, callback) { // we need to map the ioBroker loglevels to the Syslog ones const ioInfo = info; if (ioInfo[LEVEL] === 'warn') { ioInfo[LEVEL] = 'warning'; } if (ioInfo[LEVEL] === 'info') { ioInfo[LEVEL] = 'notice'; } if (ioInfo[LEVEL] === 'debug') { ioInfo[LEVEL] = 'info'; } if (ioInfo[LEVEL] === 'silly') { ioInfo[LEVEL] = 'debug'; } // No clue why log could ever be undefined, but hey... super.log?.(ioInfo, callback); } }; // We want to enhance some data for Seq const IoSeq = Seq && class extends Seq { log(info, callback) { const ioInfo = deepClone(info); ioInfo.props = ioInfo.props || {}; // map our log levels to Seq levels const level = (ioInfo.level || '').toLowerCase(); if (level.includes('error')) { ioInfo.level = 'Error'; } else if (level.includes('warn')) { ioInfo.level = 'Warning'; } else if (level.includes('info')) { ioInfo.level = 'Information'; } else if (level.includes('debug')) { ioInfo.level = 'Debug'; } else if (level.includes('silly')) { ioInfo.level = 'Verbose'; } else { ioInfo.level = 'Information'; } // we add own properties ioInfo.props.Hostname = tools.getHostName(); if (ioInfo.message) { // handle as single line with s flag, to if message ends with CR, etc const msgParts = ioInfo.message.match(/^([^.]+\.[0-9]+) \(([^)]+)\) (.*)$/s); if (msgParts) { ioInfo.props.Source = msgParts[1]; ioInfo.props.Pid = msgParts[2]; } else { ioInfo.props.Source = 'js-controller'; } } super.log(ioInfo, callback); } }; // Class used to inform adapter about new log entry class NotifierTransport extends Transport { name; constructor(opts) { super(opts); this.name = 'NT'; // NotifierTransport } log(info, callback) { const msg = { severity: info[LEVEL], ts: new Date(info.timestamp).getTime(), message: info.message, }; setImmediate(() => this.emit('logged', msg)); callback(); } } export function logger(level, files, noStdout, prefix) { const options = { transports: [], }; //var defaultMaxSize;// = 10 * 1024 * 1024; if (typeof files === 'string') { files = [files]; } const formatter = (info) => `${timestamp(info.timestamp)} - ${info.level}: ${info.message}`; files = files || []; // indicator which is used to determine the log dir for developing, where it should be inside the repository const isNpm = !thisDir .replace(/\\/g, '/') .toLowerCase() .includes(`${tools.appName.toLowerCase()}.js-controller/packages/`); if (tools.isObject(level)) { const userOptions = deepClone(level); level = userOptions.level; prefix = userOptions.prefix || ''; noStdout = userOptions.noStdout; const winstonFormats = []; /** @ts-expect-error formatter arg wrongly documented */ winstonFormats.push(winston.format.timestamp({ format: timestamp })); if (userOptions.json === undefined || userOptions.json) { winstonFormats.push(winston.format.json()); } if (prefix) { winstonFormats.push(winston.format.label({ label: prefix })); } if (userOptions.colorize === undefined || userOptions.colorize) { winstonFormats.push(winston.format.colorize()); } options.format = winston.format.combine.apply(null, winstonFormats); if (userOptions.prefix !== undefined) { delete userOptions.prefix; } if (userOptions.transport) { let fName = 0; const isWindows = os.platform().startsWith('win'); for (const transport of Object.values(userOptions.transport)) { transport._defaultConfigLoglevel = transport.level; // remember Loglevel if set transport.level = transport.level || level; if (transport.type === 'file' && transport.enabled !== false) { transport.filename = transport.filename || `log/${tools.appName}`; if (!transport.fileext && transport.filename.indexOf('.log') === -1) { transport.fileext = '.log'; } if (!fName) { transport.systemLog = true; } transport.handleExceptions = false; transport.name = fName ? `dailyRotateFile${fName}` : tools.appName; fName++; transport.filename = transport.filename.replace(/\\/g, '/'); if (transport.filename.match(/^\w:\/|^\//)) { transport.filename = path.normalize(transport.filename); } else { transport.filename = path.normalize(`${tools.getControllerDir()}${isNpm ? '/../../' : '/'}${transport.filename}`); } transport.auditFile = `${transport.filename}-audit.json`; transport.createSymlink = transport.createSymlink !== undefined ? transport.createSymlink : !isWindows; transport.symlinkName = transport.symlinkName !== undefined ? transport.symlinkName : path.basename(`${transport.filename}.current.log`); transport.filename += `.%DATE%${transport.fileext || ''}`; //transport.label = prefix || ''; //TODO format.label() // transport.json = (transport.json !== undefined) ? transport.json : false; // TODO format.json(), new Default!! transport.silent = transport.silent !== undefined ? transport.silent : false; // transport.colorize = (transport.colorize !== undefined) ? transport.colorize : ((userOptions.colorize === undefined) ? true : userOptions.colorize); //TODO format.colorize() transport.localTime = transport.localTime !== undefined ? transport.localTime : userOptions.localTime === undefined ? true : userOptions.localTime; transport.datePattern = 'YYYY-MM-DD'; transport.format = winston.format.combine(winston.format.printf(formatter)); /*transport.logException = function (message, info, next, err) { console.error(message); };*/ transport.zippedArchive = isWindows ? false : transport.zippedArchive !== undefined ? transport.zippedArchive : true; if (transport.maxFiles === null && userOptions.maxDays) { transport.maxFiles = `${userOptions.maxDays}d`; } try { const _log = new DailyRotateFile(transport); _log.on('error', err => { console.error(`Error on log file rotation: ${err.message}`); }); options.transports.push(_log); } catch (e) { if (e.code === 'EACCES') { // modify error code to make it unique for handling e.code = 'EACCES_LOG'; } throw e; } } else if (transport.type === 'syslog' && transport.enabled !== false) { if (!IoSysLog) { console.error('Syslog configured, but not installed! Ignore'); continue; } // host: The host running syslogd, defaults to localhost. // port: The port on the host that syslog is running on, defaults to syslogd's default port. // protocol: The network protocol to log over (e.g. tcp4, udp4, unix, unix-connect, etc). // path: The path to the syslog dgram socket (i.e. /dev/log or /var/run/syslog for OS X). // pid: PID of the process that log messages are coming from (Default process.pid). // facility: Syslog facility to use (Default: local0). // localhost: Host to indicate that log messages are coming from (Default: localhost). // sysLogType: The type of the syslog protocol to use (Default: BSD). // app_name: The name of the application (Default: process.title). // eol: The end of line character to be added to the end of the message (Default: Message without modifications). // replace the used by syslog attribute "type" with own "sysLogType" // If no name defined, use hostname as name transport.localhost = transport.localhost || hostname; transport.format = winston.format.combine(winston.format.printf(formatter)); if (transport.sysLogType) { transport.type = transport.sysLogType; delete transport.sysLogType; } else { delete transport.type; } try { options.transports.push(new IoSysLog(transport)); } catch (e) { console.error(`Cannot activate Syslog: ${e.message}`); } } else if (transport.type === 'http' && transport.enabled !== false) { // host: (Default: localhost) Remote host of the HTTP logging endpoint // port: (Default: 80 or 443) Remote port of the HTTP logging endpoint // path: (Default: /) Remote URI of the HTTP logging endpoint // auth: (Default: None) An object representing the username and password for HTTP Basic Auth // ssl: (Default: false) Value indicating if we should use HTTPS // If no name defined, use hostname as name transport.host = transport.host || 'localhost'; try { options.transports.push(new winston.transports.Http(transport)); } catch (e) { console.error(`Cannot activate HTTP: ${e.message}`); } } else if (transport.type === 'stream' && transport.enabled !== false) { // stream: any Node.js stream. If an objectMode stream is provided then the entire info object will be written. Otherwise info[MESSAGE] will be written. // level: Level of messages that this transport should log (default: level set on parent logger). // silent: Boolean flag indicating whether to suppress output (default false). // eol: Line-ending character to use. (default: os.EOL). // If no name defined, use hostname as name transport.host = transport.host || 'localhost'; try { if (typeof transport.stream === 'string') { transport.stream = fs.createWriteStream(transport.stream); transport.stream.on('error', (err) => { console.error(`Error in Stream: ${err.message}`); }); } options.transports.push(new winston.transports.Stream(transport)); } catch (e) { console.error(`Cannot activate Stream: ${e.message}`); } } else if (transport.type === 'seq' && transport.enabled !== false) { if (!IoSeq) { console.error('Seq configured, but not installed! Ignore'); continue; } // serverUrl?: string; // apiKey?: string; // maxBatchingTime?: number; // eventSizeLimit?: number; // batchSizeLimit?: number; // Add only if serverUrl is configured at least if (transport.serverUrl) { try { transport.onError = (e) => { console.log(`SEQ error: ${e.message}`); }; const seqLogger = new IoSeq(transport); options.transports.push(seqLogger); } catch (e) { console.error(`Cannot activate SEQ: ${e.message}`); } } else { console.error('Cannot activate SEQ: No serverUrl specified'); } } } } } else { for (let i = 0; i < files.length; i++) { const opt = { name: i ? `dailyRotateFile${i}` : tools.appName, filename: path.normalize(isNpm ? `${thisDir}/../../../log/${files[i]}` : `${thisDir}/../log/${files[i]}`), extension: '.log', datePattern: 'YYYY-MM-DD', //json: false, // If true, messages will be logged as JSON (default true). TODO format.json() level, silent: false, localTime: true, //colorize: (userOptions.colorize === undefined) ? true : userOptions.colorize, // TODO format.colorize() //timestamp: timestamp, // TODO: format.timestamp() //label: prefix || '', // TODO format.label() handleExceptions: false, //maxSize: defaultMaxSize }; options.transports.push(new DailyRotateFile(opt)); } } if (!noStdout) { options.transports.push(new winston.transports.Console({ level, silent: false, format: winston.format.combine(winston.format.printf(formatter)), //colorize: (userOptions.colorize === undefined) ? true : userOptions.colorize, // TODO format.colorize() //timestamp: timestamp, // TODO: format.timestamp() //label: prefix || '' // TODO format.label() })); } // Must be registered, for adapter.js notification about new logs options.transports.push(new NotifierTransport({ level, silent: false, })); const log = winston.createLogger(options); /** @ts-expect-error why do we override/add method to foreign instance? TODO */ log.getFileName = function () { /** @ts-expect-error we use undocumented stuff here TODO */ let transport = this.transports.find(t => (t.transport && t.transport.dirname) || t.dirname); if (transport) { /** @ts-expect-error we use undocumented stuff here TODO */ transport = transport.transport ? transport.transport : transport; /** @ts-expect-error we use undocumented stuff here TODO */ return `${transport.dirname}/${transport.filename.replace('%DATE%', getDate())}`; } return ''; }; log.on('error', error => { console.log(`Logger error: ${error.message}`); }); // This cannot be deleted, because file rotate works with the size of files and not with the time // TODO research and open new issue in winston-daily-rotate-file repo /** * @param isEnabled * @param daysCount */ // @ts-expect-error why do we override/add method to foreign instance? TODO log.activateDateChecker = function (isEnabled, daysCount) { /** @ts-expect-error we use undocumented stuff here TODO */ if (!isEnabled && this._fileChecker) { /** @ts-expect-error we use undocumented stuff here TODO */ clearInterval(this._fileChecker); /** @ts-expect-error we use undocumented stuff here TODO */ } else if (isEnabled && !this._fileChecker) { if (!daysCount) { daysCount = 3; } // Check every hour /** @ts-expect-error we use undocumented stuff here TODO */ this._fileChecker = setInterval(() => { this.transports.forEach(transport => { if ( /** @ts-expect-error we use undocumented stuff here TODO */ transport.name !== 'dailyRotateFile' || /** @ts-expect-error we use undocumented stuff here TODO */ !transport.options || /** @ts-expect-error we use undocumented stuff here TODO */ transport.options.name !== tools.appName) { return; } /** @ts-expect-error we use undocumented stuff here TODO */ if (transport && fs.existsSync(transport.dirname)) { let files; try { /** @ts-expect-error we use undocumented stuff here TODO */ files = fs.readdirSync(transport.dirname); } catch (e) { console.error(`host.${hostname} Cannot read log directory: ${e.message}`); return; } const forXdays = new Date(); forXdays.setDate(forXdays.getDate() - daysCount - 1); // normally winston now takes care, for old or on errors make sure fallback works a day later for (let i = 0; i < files.length; i++) { const match = files[i].match(/.+\.(\d+-\d+-\d+)/); if (match) { const date = new Date(match[1]); if (date < forXdays) { // delete file try { this.log({ level: 'info', message: `host.${hostname} Delete log file ${files[i]}`, }); console.log(`host.${hostname} Delete log file ${files[i]}`); /** @ts-expect-error we use undocumented stuff here TODO */ fs.unlinkSync(`${transport.dirname}/${files[i]}`); } catch (e) { // there is a bug under windows, that file stays opened and cannot be deleted this.log({ level: os.platform().startsWith('win') ? 'info' : 'error', message: `host.${hostname} Cannot delete file "${path.normalize( /** @ts-expect-error we use undocumented stuff here TODO */ `${transport.dirname}/${files[i]}`)}": ${e}`, }); console.log(`host.${hostname} Cannot delete file "${path.normalize( /** @ts-expect-error we use undocumented stuff here TODO */ `${transport.dirname}/${files[i]}`)}": ${e.message}`); } } } } } }); }, 3_600_000); // every hour } }; return log; } function getDate() { const ts = new Date(); return `${ts.getFullYear()}-${(ts.getMonth() + 1).toString().padStart(2, '0')}-${ts .getDate() .toString() .padStart(2, '0')}`; } function timestamp(date) { const ts = date ? new Date(date) : new Date(); return `${ts.getFullYear()}-${(ts.getMonth() + 1).toString().padStart(2, '0')}-${ts .getDate() .toString() .padStart(2, '0')} ${ts.getHours().toString().padStart(2, '0')}:${ts .getMinutes() .toString() .padStart(2, '0')}:${ts.getSeconds().toString().padStart(2, '0')}.${ts .getMilliseconds() .toString() .padStart(3, '0')} `; } //# sourceMappingURL=logger.js.map