domotz-remote-pawn
Version:
Domotz Agent
376 lines (344 loc) • 13.5 kB
JavaScript
/** 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,
};
};