UNPKG

domotz-remote-pawn

Version:

Domotz Agent

376 lines (344 loc) 13.5 kB
/** This file is part of Domotz Agent. * Copyright (C) 2016 Domotz Ltd * * Domotz Agent is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Domotz Agent is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Domotz Agent. If not, see <http://www.gnu.org/licenses/>. * * Created by Tommaso Latini <tommaso@domotz.com> on 06/05/15. * * LOG LEVEL DOCS: * * silent: Completely silent. Zero logging output. * error: When something bad happens. * An error should cause the SHUTDOWN of the application. * Examples: * --> Unable to get network information. * --> Zabbix processes not running. * alert: When something unexpected happens. * Examples: * --> AMQPS connection goes down. * --> Unreachable Engine. * --> Unable to communicate with proxy. * warn: When something odd is happening. * Examples: * --> Locked Mutexes * --> Cache Misses * --> Timeouts * info: Helpful information so you can track what is happening. * Examples: * --> Event Handlers * --> Device Discovery * debug: Debug information. * Examples: * --> Body of HTTP request/response * --> Body of AMQPS message * verbose: Even more. Perhaps just a wee bit obnoxious, even. * Examples: * --> Unchanged status * silly: Completely crazy, man. Dump everything. * Whole objects, you name it, whatever. * Examples: * --> Log inside loop cycles. */ const DOMOTZ_COLORS = { error: 'red', alert: 'magenta', warn: 'yellow', info: 'green', debug: 'white', verbose: 'cyan', silly: 'gray', }; const STACK_POSITION = 11; const MAX_LEN_FOR_FILE_NAME = 30; const BLOCK_SIZE = 16384; const STACK_REGEXP = /at\s+(.*)\s+\((.*):(\d*):(\d*)\)/gi; const STACK_REGEXP_2 = /at\s+()(.*):(\d*):(\d*)/gi; const LOG_BASE_FILE = 'domotz_listener.log'; const domotzLogging = require('../utils/logging'); module.exports.factory = function (resourceLocator) { var lodash = resourceLocator.lodash; var winston = resourceLocator.winston; var util = resourceLocator.util; var path = resourceLocator.path; var configuration = resourceLocator.configuration; var env = process.env; var logDir = env.DOMOTZ_LOG_DIR; var logName = env.DNAME; var logTransports = []; var logOptions, logTransport; var registry = require('../utils/logging/registry').registry(); var myLogger = registry.getLogger('setup.logger'); function dummy() { return null; } function defaultTimestamp() { return new Date().toISOString() + ' '; } function defaultFormatter(options) { return ( (options.timestamp() || '') + '(' + (options.pid || process.pid) + ') ' + '[' + options.level[0].toUpperCase() + '] ' + (options.message || 'No log message') ); } function cleanFolder() { var proc = resourceLocator.utils.proc; var file = resourceLocator.utils.file; file.listDir(logDir).forEach(function (f) { if (f.indexOf(LOG_BASE_FILE) === -1) { var fileName = path.join(logDir, f); proc.spawn('wc', ['-l', fileName]).then(function (output) { var rows = parseInt(output.split(/\s/)[0]); myLogger.info('LOG - Check file %s in log directory [%s]. ' + 'Length: %s rows', f, logDir, rows); if (rows > 100) { return proc.spawn('sed', ['-i', '-e', '1,' + (rows - 100) + 'd', fileName]); } }); } }); } function getCurrentFile(index) { return path.join(logDir, LOG_BASE_FILE + '.' + index); } function getNewestFileIndex() { var file = resourceLocator.utils.file; var fileRotation = 0, lastModified = new Date(0); myLogger.debug('LOG - Check LOG directory: %s', logDir); file.listDir(logDir).forEach(function (f) { if (f.indexOf(LOG_BASE_FILE) === 0) { var index = f.slice(-1); // N.B. We assume no more than 10 files. var fileName = path.join(logDir, f); var stats = file.getFileStats(fileName); var modAt = new Date(stats.mtime); if (stats && modAt >= lastModified) { fileRotation = parseInt(index); lastModified = modAt; } myLogger.debug('LOG - Analyze file: %s. Last modified: %s. Got index %s', f, modAt, index); } }); return fileRotation; } function addTransport(name, type, opts) { myLogger.info('LOG - Initialize log %s. Options: %s', name, JSON.stringify(opts)); logTransport = new winston.transports[type](opts); logTransport.name = name; logTransport.options = opts; logTransports.push(logTransport); } function createDomotzTransport() { var domotzLogger = (winston.transports.Domotz = function (options) { this.level = options.level || 'debug'; this.maxsize = options.maxsize || 1048576; // 1 MB this.maxFiles = options.maxFiles || 3; // 3 files this.timestamp = options.timestamp || null; this.formatter = options.formatter || defaultFormatter; this.writeOutput = []; this.writeOutputSize = 0; this.fileRotation = getNewestFileIndex(); this.lastFlush = null; }); util.inherits(domotzLogger, winston.Transport); domotzLogger.prototype.log = function (level, msg, meta, callback) { var output = this.formatter({ level: level, message: msg, timestamp: this.timestamp, pid: meta.pid, }); this.writeOutput.push(output); this.writeOutputSize += output.length; this.emit('logged'); callback(null, true); }; domotzLogger.prototype.flush = function () { var file = resourceLocator.utils.file; var logFile = getCurrentFile(this.fileRotation); var curSize = file.getSize(logFile); var maxBytes = this.maxsize - curSize; var stream = '', bytes = 0; myLogger.debug('LOG - Flush: File %s. Remaining Bytes %s', logFile, maxBytes); for (var i = 0; i < this.writeOutput.length; i++) { var str = this.writeOutput[i] + '\n'; var strlen = Buffer.byteLength(str, 'utf8'); stream += str; bytes += strlen; if (bytes > BLOCK_SIZE) { file.appendFile(logFile, stream.slice(0, BLOCK_SIZE)); stream = stream.slice(BLOCK_SIZE); bytes -= BLOCK_SIZE; } maxBytes -= strlen; if (maxBytes <= 0) { this.fileRotation = (this.fileRotation + 1) % this.maxFiles; logFile = getCurrentFile(this.fileRotation); maxBytes = this.maxsize; file.writeFile(logFile, ''); } } file.appendFile(logFile, stream); this.writeOutput = []; this.writeOutputSize = 0; this.lastFlush = new Date(); }; domotzLogger.prototype.status = function () { return { buffer: { length: this.writeOutput.length, bytes: this.writeOutputSize, }, storage: { directory: logDir, curFile: LOG_BASE_FILE + '.' + this.fileRotation, lastFlush: this.lastFlush, }, }; }; domotzLogger.prototype.getRawLogs = function () { return this.writeOutput; }; } /** * DEVELOPMENT LOGGER * Transport --> Console with stderr for error level, Stdout for all the others * Timestamp --> Locale Time String * Formatter --> [file:row] timestamp [level] message */ if (logName !== 'domotz') { logOptions = { level: env.DLEVEL || 'silly', colorize: 'all', debugStdout: true, formatter: defaultFormatter, timestamp: function () { Error.stackTraceLimit = STACK_POSITION; var stack = new Error().stack.split('\n'); var line = stack[STACK_POSITION]; var padding = Array(MAX_LEN_FOR_FILE_NAME).join(' '); var sp = STACK_REGEXP.exec(line) || STACK_REGEXP_2.exec(line); var file = sp ? '[' + sp[2].split('/').pop() + ':' + sp[3] + ']' : ' '; return (file + padding).substring(0, MAX_LEN_FOR_FILE_NAME); }, }; addTransport('DEVELOPMENT', 'Console', logOptions); } if (logName !== 'stdout') { /** * PRODUCTION LOGGER: * * ==> OPENWRT * Transport --> Console with stderr for error level, Stdout for all the others * Timestamp --> Already present in system log * Formatter --> [level] message * * N.B. OpenWRT procd provides to redirect stderr on syslog and stdout on /dev/null * No need for timestamp because it's already in system log * * ==> STANDARD * Transport --> Custom with a memory buffer flushed periodically * Timestamp --> ISO String * Formatter --> timestamp [level] message. * * N.B. The transport just offer te way to flush the log. * The periodical trigger is external (see cron.js) */ logOptions = configuration ? lodash.clone(configuration.log || {}) : {}; logOptions.timestamp = defaultTimestamp; logOptions.formatter = defaultFormatter; logOptions.level = 'silly'; // actual filtering is done by HierarchyLoggers if (env.DPLATFORM === 'openwrt' || env.DPLATFORM === 'luxul') { logOptions.debugStdout = true; addTransport('LUXUL', 'Console', logOptions); } else { createDomotzTransport(); addTransport('DOMOTZ', 'Domotz', logOptions); } } var logger = new winston.Logger({ transports: logTransports, colors: DOMOTZ_COLORS, levels: domotzLogging.DOMOTZ_LEVELS, }); registry.replaceConsoleWithWriter(logger); if (configuration && configuration.log) { registry.setLevel(null, configuration.log.level); } /** * Enrich the Error standard object prototype, so that exceptions can be more easily logged * * https://stackoverflow.com/questions/18391212/is-it-not-possible-to-stringify-an-error-using-json-stringify */ if (!('toJSON' in Error.prototype)) { Object.defineProperty(Error.prototype, 'toJSON', { value: function () { var alt = {}; Object.getOwnPropertyNames(this).forEach(function (key) { alt[key] = this[key]; }, this); return alt; }, configurable: true, writable: true, }); } function flush() { logTransports.forEach(function (logTransport) { myLogger.info('LOG - Flush on transport %s. Method %s available.', logTransport.name, logTransport.flush ? 'is' : "isn't"); if (logTransport.flush) { logTransport.flush(); } }); } function getRawLogs() { if (logTransport.getRawLogs) { return logTransport.getRawLogs(); } } function getInfo() { var statuses = {}; for (var i = 0; i < logTransports.length; i++) { var logTransport = logTransports[i]; var status = logTransport.status ? logTransport.status() : 'Unavailable'; statuses[logTransport.name] = { status: status, options: logTransport.options, }; } return { transports: statuses, }; } return { flush: flush, getRawLogs: getRawLogs, getInfo: getInfo, decorateLogs: domotzLogging.decorateLogs, cleanFolder: cleanFolder, // Testing _dummy: dummy, _createDomotzTransport: createDomotzTransport, _addTransport: addTransport, _getNewestFileIndex: getNewestFileIndex, _getCurrentFile: getCurrentFile, _defaultFormatter: defaultFormatter, _defaultTimestamp: defaultTimestamp, }; };