@pryv/boiler
Version:
Logging and config boilerplate library for Node.js apps and services at Pryv
308 lines (270 loc) • 7.91 kB
JavaScript
/**
* @license
* [BSD-3-Clause](https://github.com/pryv/pryv-boiler/blob/master/LICENSE)
*/
const util = require('util');
const winston = require('winston');
require('winston-daily-rotate-file');
const debugModule = require('debug');
let winstonInstance = null;
let rootLogger = null;
let customLoggerInstance = null;
// ------ winston formating
/**
*
* @param {Object} options
* @param {boolean} options.color - set to true to have colors
* @param {boolean} options.time - set to true to for timestamp
* @param {boolean} options.align - set to true to allign logs items
*/
function generateFormat (options) {
const formats = [];
if (options.color) {
formats.push(winston.format.colorize());
}
if (options.time) {
formats.push(winston.format.timestamp());
}
if (options.align) {
formats.push(winston.format.align());
}
function printf (info) {
const {
timestamp, level, message, ...args
} = info;
let items = info[Symbol.for('splat')] || {};
let itemStr = '';
if (items.length > 0) {
let skip = false;
if (items.length === 1) {
if (typeof items[0] === 'undefined') {
skip = true;
} else {
if (items[0] && items[0].context) {
items = items[0].context;
}
}
}
if (!skip) { itemStr = util.inspect(items, { depth: 10, colors: true }); }
}
const line = `[${level}]: ${message} ${itemStr}`;
if (options.time) {
const ts = timestamp.slice(0, 19).replace('T', ' ');
return ts + ' ' + line;
} else {
return line;
}
}
formats.push(winston.format.printf(printf));
return winston.format.combine(...formats);
}
/**
* Helper to pass log instructions to winston
*/
function globalLog (level, key, message, context) {
const text = `[${key}] ${message}`;
if (winstonInstance) {
winstonInstance[level](text, context);
} else {
console.log('Logger not initialized: ', ...arguments);
}
if (customLoggerInstance) {
customLoggerInstance.log(level, key, message, context);
}
}
/**
* Config initialize Logger right after beeing loaded
* This is done by config Only
*/
async function initLoggerWithConfig (config) {
if (winstonInstance) {
throw new Error('Logger was already initialized');
}
// console
winstonInstance = winston.createLogger({ });
const logConsole = config.get('logs:console');
let isSilent = !config.get('logs:console:active');
// LOGS env var can override settings
if (process.env.LOGS) {
logConsole.level = process.env.LOGS;
isSilent = false;
}
const consoleFormat = generateFormat(logConsole.format);
const myconsole = new winston.transports.Console({ format: consoleFormat, level: logConsole.level, silent: isSilent });
winstonInstance.add(myconsole);
rootLogger.debug((isSilent ? '** silent ** ' : '') + 'Console with level: ', logConsole.level);
// file
const logFile = config.get('logs:file');
if (config.get('logs:file:active')) {
const fileFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.json()
);
rootLogger.debug('File active: ' + logFile.path);
if (logFile.rotation.isActive) {
const rotatedFiles = new winston.transports.DailyRotateFile({
filename: logFile.path + '.%DATE%',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
level: logFile.level,
maxFiles: logFile.rotation.days ? logFile.rotation.days + 'd' : null,
format: fileFormat
});
winstonInstance.add(rotatedFiles);
} else {
const files = new winston.transports.File({
filename: logFile.path,
level: logFile.level,
maxSize: logFile.maxFileBytes || '10m',
maxFiles: logFile.maxNbFiles || '14d',
format: fileFormat
});
winstonInstance.add(files);
}
}
// custom
if (config.get('logs:custom:active')) {
customLoggerInstance = require(config.get('logs:custom:path'));
await customLoggerInstance.init(config.get('logs:custom:settings'));
}
// catch all errors.
if (!config.get('logs:skipUncaughtException')) {
process.on('uncaughtException', function (err) {
rootLogger.error('UncaughtException', { message: err.message, name: err.name, stack: err.stack });
throw err;
});
}
rootLogger.debug('Logger Initialized');
}
// --------------- debug utils
/**
* Dump objects with file and line
*/
function inspect () {
let line = '';
try {
throw new Error();
} catch (e) {
line = e.stack.split(' at ')[2].trim();
}
let res = '\n * dump at: ' + line;
for (let i = 0; i < arguments.length; i++) {
res += '\n' + i + ' ' + util.inspect(arguments[i], true, 10, true) + '\n';
}
return res;
}
function setGlobalName (name) {
// create root logger
rootLogger = new Logger(name, null);
rootLogger.debug('setGlobalName: ' + name);
}
class Logger {
parent; // eventual parent
debugInstance; // debug instance
constructor (name, parent) {
this.name = name;
this.parent = parent;
this.debugInstance = debugModule('pryv:' + this._name());
}
/**
* Private
*/
_name () {
if (this.parent) return this.parent._name() + ':' + this.name;
return this.name;
}
log () {
const level = arguments[0];
const message = hideSensitiveValues(arguments[1]);
const context = [];
let meta;
// Security measure: We do not want any sensitive value to appear in logs
for (let i = 2; i < arguments.length; i++) {
context.push(inspectAndHide(arguments[i]));
}
if (context.length === 1) {
meta = { context: context[0] };
} else if (context.length > 1) {
meta = { context };
}
globalLog(level, this._name(), message, meta);
}
info () { this.log('info', ...arguments); }
warn () { this.log('warn', ...arguments); }
error () { this.log('error', ...arguments); }
debug () {
if (winstonInstance) {
this.log('debug', ...arguments);
}
this.debugInstance(...arguments);
}
/**
* get a "sub" Logger
* @param {Logger} name
* @returns {Logger}
*/
getLogger (name) {
return new Logger(name, this);
}
inspect () { inspect(...arguments); }
}
/**
* Get a new logger, or root loggger if no name is provided
* @param {string} [name]
* @returns {Logger}
*/
function getLogger (name) {
if (!rootLogger) {
throw new Error('Initalize boiler before using logger');
}
if (!name) {
return rootLogger;
}
return rootLogger.getLogger(name);
}
module.exports = {
getLogger,
setGlobalName,
initLoggerWithConfig
};
// ----------------- Hide sensite data -------------------- //
function inspectAndHide (o) {
if (typeof o === 'undefined') return o;
if (o instanceof Error) return o;
return _inspectAndHide(JSON.parse(JSON.stringify(o))); // clone and remove circular
}
function _inspectAndHide (o) {
if (typeof o === 'string') {
return hideSensitiveValues(o);
}
if (o !== null && typeof o === 'object') {
if (Array.isArray(o)) {
const res = [];
for (const item of o) {
res.push(inspectAndHide(item));
}
return res;
}
const res = {};
for (const key of Object.keys(o)) {
if (['password', 'passwordHash', 'newPassword'].includes(key)) {
res[key] = '(hidden password)';
} else {
res[key] = inspectAndHide(o[key]);
}
}
return res;
}
return o;
}
// Hides sensitive values (auth tokens and passwords) in log messages
function hideSensitiveValues (msg) {
if (typeof msg !== 'string') return msg;
const tokenRegexp = /auth\=c([a-z0-9-]*)/g;
const passwordRegexp = /"(password|passwordHash|newPassword)"[:=]"([^"]*)"/g;
const mask = '(hidden)';
const res = msg
.replace(tokenRegexp, 'auth=' + mask)
.replace(passwordRegexp, '$1=' + mask);
return res;
}