@interaktiv/mibuilder-core
Version:
Core libraries to interact with MiBuilder projects.
614 lines (507 loc) • 19 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Logger = exports.isValidLevel = exports.DEFAULT_LEVEL = exports.ROOT_NAME = exports.LEVEL_SILLY = exports.LEVEL_DEBUG = exports.LEVEL_VERBOSE = exports.LEVEL_HTTP = exports.LEVEL_INFO = exports.LEVEL_WARN = exports.LEVEL_ERROR = exports.getLevels = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _os = require("os");
var _path = _interopRequireDefault(require("path"));
var _stream = _interopRequireDefault(require("stream"));
var _dxl = require("@interaktiv/dxl");
var _json = require("@interaktiv/json");
var _types = require("@interaktiv/types");
var _debug = _interopRequireDefault(require("debug"));
var _winston = require("winston");
var _globalConfig = require("../config/global-config");
var _constants = require("../config/constants");
var _mibuilderError = require("../mibuilder-error");
var _winstonMemory = require("./winston-memory");
// Ok to log clientid
const FILTERED_KEYS = ['sid', 'Authorization', // Any json attribute that contains the words "access" and "token" will have the attribute/value hidden
{
name: 'access_token',
regex: 'access[^\'"]*token'
}, // Any json attribute that contains the words "refresh" and "token" will have the attribute/value hidden
{
name: 'refresh_token',
regex: 'refresh[^\'"]*token'
}, 'clientsecret', // Any json attribute that contains the words "auth", and "url" will have the attribute/value hidden
{
name: 'authurl',
regex: '[^\'"]*auth[^\'"]*url'
}];
/**
* Standard `Logger` levels.
*/
const {
levels
} = _winston.config.npm;
const getLevels = () => (0, _types.keysOf)(levels);
exports.getLevels = getLevels;
const LEVEL_ERROR = 'error';
exports.LEVEL_ERROR = LEVEL_ERROR;
const LEVEL_WARN = 'warn';
exports.LEVEL_WARN = LEVEL_WARN;
const LEVEL_INFO = 'info';
exports.LEVEL_INFO = LEVEL_INFO;
const LEVEL_HTTP = 'http';
exports.LEVEL_HTTP = LEVEL_HTTP;
const LEVEL_VERBOSE = 'verbose';
exports.LEVEL_VERBOSE = LEVEL_VERBOSE;
const LEVEL_DEBUG = 'debug';
exports.LEVEL_DEBUG = LEVEL_DEBUG;
const LEVEL_SILLY = 'silly';
/**
* The name of the root mibuilder `Logger`.
*/
exports.LEVEL_SILLY = LEVEL_SILLY;
const ROOT_NAME = 'mibuilder';
/**
* The default `LoggerLevel` when constructing new `Logger` instances.
*/
exports.ROOT_NAME = ROOT_NAME;
const DEFAULT_LEVEL = LEVEL_WARN;
exports.DEFAULT_LEVEL = DEFAULT_LEVEL;
const HIDDEN = 'HIDDEN';
const filterData = info => {
return Object.entries(info).map(([key, value]) => {
if (!value) return [key, value]; // Normalize all objects into a string. This include errors.
let arg;
if (value instanceof Buffer) {
arg = '<Buffer>';
} else if ((0, _types.isObject)(value)) {
arg = JSON.stringify(value);
} else if ((0, _types.isString)(value)) {
arg = value;
} else {
arg = '';
}
FILTERED_KEYS.forEach(filteredKey => {
let expElement = filteredKey;
let expName = filteredKey; // Filtered keys can be strings or objects containing regular expression components.
if ((0, _types.isPlainObject)(filteredKey)) {
expElement = filteredKey.regex;
expName = filteredKey.name;
}
const hiddenAttrMessage = `"<${expName} - ${HIDDEN}>"`; // Match all json attribute values case insensitive: ex. {" Access*^&(*()^* Token " : " 45143075913458901348905 \n\t" ...}
// eslint-disable-next-line security/detect-non-literal-regexp
const regexTokens = new RegExp(`(['"][^'"]*${expElement}[^'"]*['"]\\s*:\\s*)['"][^'"]*['"]`, 'gi');
arg = arg.replace(regexTokens, `$1${hiddenAttrMessage}`); // Match all key value attribute case insensitive: ex. {" key\t" : ' access_token ' , " value " : " dsafgasr431 " ....}
// eslint-disable-next-line security/detect-non-literal-regexp
const keyRegex = new RegExp(`(['"]\\s*key\\s*['"]\\s*:)\\s*['"]\\s*${expElement}\\s*['"]\\s*.\\s*['"]\\s*value\\s*['"]\\s*:\\s*['"]\\s*[^'"]*['"]`, 'gi');
arg = arg.replace(keyRegex, `$1${hiddenAttrMessage}`);
});
arg = arg.replace(/sid=(.*)/, `sid=<${HIDDEN}>`); // Return an object if an object was logged; otherwise return the filtered string.
if ((0, _types.isObject)(value)) return [key, JSON.parse(arg)];
return [key, arg];
}).reduce((acc, [key, value]) => (0, _extends2.default)({}, acc, {
[key]: value
}), {});
};
const isValidLevel = (level = '') => {
if (!(0, _types.isString)(level) || !(0, _types.isKeyOf)(levels, level)) return false;
return true;
};
exports.isValidLevel = isValidLevel;
let rootLogger;
/**
* A logging abstraction powered by {@link https://github.com/winstonjs/winston} that provides both a default
* logger configuration that will log to `mibuilder.log`, and a way to create custom loggers based on the same foundation.
*
* ```
* // Gets the root mibuilder logger
* const logger = await Logger.root();
*
* // Creates a child logger of the root mibuilder logger with custom fields applied
* const childLogger = await Logger.child('myRootChild', {tag: 'value'});
*
* // Creates a custom logger unaffiliated with the root logger
* const myCustomLogger = new Logger('myCustomLogger');
*
* // Creates a child of a custom logger unaffiliated with the root logger with custom fields applied
* const myCustomChildLogger = myCustomLogger.child('myCustomChild', {tag: 'value'});
* ```
* **See** https://github.com/winstonjs/winstonjs
*
* @param {String|Object} optionsOrName - The logger namespace or whole configuration as object
*/
class Logger {
/**
* Gets the root logger with the default level and file stream.
*
* @return {Logger} The root logger
*/
static async root() {
if (rootLogger) return rootLogger;
rootLogger = new Logger(ROOT_NAME);
rootLogger.setLevel(); // Disable log file writing, if applicable
if (_dxl.env.getBoolean('MIBUILDER_DISABLE_LOG_FILE') !== true && (0, _globalConfig.getEnvironmentMode)() !== _globalConfig.MODE_TEST) {
await rootLogger.addLogFileStream(_constants.LOG_FILE_PATH);
}
rootLogger.trace(`Created root '${rootLogger.getName()}' logger instance`); // The debug library does this for you, but no point setting up the stream if it isn't there
if (_dxl.env.getBoolean('DEBUG')) {
const debuggers = {};
debuggers.core = (0, _debug.default)(`${rootLogger.getName()}:core`);
rootLogger.addStream({
name: 'debug',
stream: new _stream.default.Writable({
write: (chunk, encoding, next) => {
const json = (0, _json.parseJsonMap)(chunk.toString());
let debuggerName = 'core';
if ((0, _types.isString)(json.label) || (0, _types.isString)(json.log)) {
debuggerName = json.label || json.log;
if (debuggers[debuggerName] == null) {
debuggers[debuggerName] = (0, _debug.default)(`${rootLogger.getName()}:${debuggerName}`);
}
}
const debug = debuggers[debuggerName];
if ((0, _types.isFunction)(debug) && json.level === LEVEL_DEBUG) debug(json.message);
next();
}
}),
// Consume all levels
level: LEVEL_SILLY
});
}
return rootLogger;
}
/**
* Create a child of the root logger, inheriting this instance's configuration such as `level`, `streams`, etc.
*
* @param {String} name The name of the child logger.
* @param {Object} [metaData] Additional fields included in all log lines.
*/
static async child(name, metaData) {
return (await this.root()).child(name, metaData);
}
/**
* Destroys the root `Logger`.
*/
static destroyRoot() {
if (!rootLogger) return;
rootLogger.close();
rootLogger = undefined;
}
constructor(optionsOrName) {
this._filters = [];
this._name = 'logger';
let options;
if ((0, _types.isString)(optionsOrName)) {
options = {
level: DEFAULT_LEVEL,
format: _winston.format.combine(...[this._applyFilters(), rootLogger ? _winston.format.label({
label: optionsOrName
}) : null, _winston.format.splat(), _winston.format.errors({
stack: true
}), _winston.format.timestamp(), _winston.format.json()].filter(Boolean)),
defaultMeta: {
name: optionsOrName,
pid: process.pid
},
exitOnError: false,
transports: []
};
this._name = optionsOrName;
} else {
options = optionsOrName;
this._name = options.name;
}
if (rootLogger && this._name === ROOT_NAME) {
throw new _mibuilderError.MiBuilderError('RedundantRootLogger');
}
this.winston = (0, _winston.createLogger)(options); // All loggers must filter sensitive data
this.addFilter((...args) => filterData(...args));
} // Custom format to run any log through filters and return final result
_applyFilters() {
return (0, _winston.format)((info, opts) => {
const filteredInfo = this._filters.reduce((acc, filter) => (0, _extends2.default)({}, acc, filter(acc, opts)), info);
return filteredInfo;
})();
}
/**
* Adds a stream.
*
* For options {@see transports.Stream}
*
* @param {Object} options The stream configuration to add
* @param {Object<Stream>} [options.name=stream] The name for the stream.
* @param {Object<Stream>} options.stream The stream configuration to add.
* @param {String} options.level The default level of the stream.
*/
addStream(options) {
if ((0, _types.isObject)(options) === false) return;
const newStreamTransport = new _winston.transports.Stream(options);
newStreamTransport.name = options.name || 'stream';
this.winston.add(newStreamTransport);
}
/**
* Adds a file stream to this logger. Resolved or rejected upon completion of
* the addition.
*
* @param {String} logFile The path to the log file. If it doesn't exist it will be created.
*/
async addLogFileStream(logFile) {
// Check if we have write access to the log file (i.e., we created it
// already)
try {
await (0, _dxl.access)(logFile, _dxl.constants.W_OK);
} catch (err1) {
await (0, _dxl.mkdirp)(_path.default.dirname(logFile));
await (0, _dxl.writeFile)(logFile, '');
} // Avoid multiple streams to same log file
const streamAlreadyAdded = this.winston.transports.filter(transport => (0, _types.isClassAssignableTo)(transport, _winston.transports.File)).find(transport => transport.filename === logFile) != null;
if (streamAlreadyAdded) return this; // TODO: rotating-file
// https://github.com/trentm/node-bunyan#stream-type-rotating-file
this.winston.add(new _winston.transports.File({
filename: logFile,
level: this.winston.level
}));
return this;
}
/**
* Sets the name for this logger.
*
* @param {String} newName The new name for this logger
* @return {String} name Returns the name of this logger
*/
setName(newName) {
if (!(0, _types.isString)(newName)) return this._name;
this._name = newName;
this.addMetaData('label', this._name);
return this._name;
}
/**
* Gets the name of this logger.
*
* @return {String} The name of the current logger
*/
getName() {
return this._name;
}
/**
* Gets the current level of this logger.
*
* @return {String} The log level of the current logger
*/
getLevel() {
return this.winston.level;
}
/**
* Set the logging level of all streams for this logger. If a specific `level` is not provided, this method will
* attempt to read it from the environment variable `<BIN>_LOG_LEVEL`, and if not found,
* {@link DEFAULT_LEVEL} will be used instead. For convenience `this` object is returned.
*
* ```
* // Sets the level from the environment or default value
* logger.setLevel()
*
* // Set the level from the INFO enum
* logger.setLevel(LEVEL_INFO)
*
* // Sets the level case-insensitively from a string value
* logger.setLevel(Logger.getLevelByName('info'))
* ```
*
* @param {LEVEL_<VALUE>} [level=DEFAULT_LEVEL] The logger level.
* @return {Logger} this
*/
setLevel(level = _dxl.env.getString('MIBUILDER_LOG_LEVEL', DEFAULT_LEVEL)) {
let newLevel = level;
if (isValidLevel(newLevel) === false) newLevel = DEFAULT_LEVEL;
if (this.winston.level === newLevel) return this;
this.winston.level = newLevel;
this.winston.transports.filter(transport => transport.name !== 'debug').forEach(transport => {
// eslint-disable-next-line no-param-reassign
transport.level = newLevel;
});
return this;
}
/**
* Compares the requested log level with the current log level. Returns true if
* the requested log level is greater than or equal to the current log level.
*
* @param {string} level The requested log level to compare against the currently set log level.
* @return {boolean} Should we log
*/
shouldLog(level) {
return this.winston.isLevelEnabled(level);
}
/**
* Gets the underlying winston logger.
*
* @return {Object<winston.logger>} The winston logger object
*/
getWinstonLogger() {
return this.winston;
}
/**
* Use in-memory logging for this logger instance instead of any parent
* streams. Useful for testing.
*
* **WARNING: This cannot be undone for this logger instance.**
*
* @return {Object<Logger>} Returns this for chaining
*/
useMemoryLogging() {
// Remove all previous transports
this.winston.clear();
this.winston.add(new _winstonMemory.Memory({
limit: 5000,
level: this.winston.level
}));
return this;
}
/**
* Gets an array of log line objects. Each element is an object that
* corresponds to a log line.
*
* @return {Object[]} The buffered log line objects
*/
getBufferedRecords() {
const memoryTransport = this.winston.transports.find(transport => (0, _types.isClassAssignableTo)(transport, _winstonMemory.Memory));
return (0, _types.getArray)(memoryTransport, 'records', []);
}
/**
* Reads a text blob of all the log lines contained in memory or the log file.
*
* @return {String} The log contents as text
*/
async readLogContentsAsText() {
const bufferedRecords = this.getBufferedRecords();
if ((0, _types.isEmpty)(bufferedRecords) === false) {
return bufferedRecords.reduce((content, line) => `${content}${JSON.stringify(line)}${_os.EOL}`, '');
}
try {
const results = await Promise.all(this.winston.transports.filter(transport => (0, _types.isClassAssignableTo)(transport, _winston.transports.File)).map(transport => (0, _dxl.readFile)(transport.filename, 'utf8')));
return results.join('');
} catch (_err) {
return '';
}
}
/**
* Adds a filter to be applied to all logged messages.
*
* @param {Function} filter A function with signature `(info: object, opts) => object` that transforms log message arguments.
*/
addFilter(filter) {
this._filters = (0, _types.getArray)(this, '_filters', []);
this._filters.push(filter);
}
setFilters(newFilters) {
if ((0, _types.isArray)(newFilters) === false) return this._filters;
this._filters = newFilters;
return this._filters;
}
/**
* Close the logger, including any streams, and remove all listeners.
*
* @param {Function} fn A function with signature `(stream: transports) => void` to call for each stream with the stream as an arg.
*/
close(fn) {
if ((0, _types.isFunction)(fn)) {
this.winston.transports.forEach(transport => fn(transport));
}
this.winston.close();
}
/**
* Create a child logger, typically to add a few log record fields. For convenience this object is returned.
*
* @param {String} childName The name of the child logger that is emitted w/ log line as `log:<name>`.
* @param {Object} [metaData={}] Additional fields included in all log lines for the child logger.
* @return {Logger} The created child logger
*/
child(childName, metaData = {}) {
if (!childName) throw new Error('A logger name is required');
const childLogger = new Logger(childName); // Only support including additional fields on log line (no config)
childLogger.winston = this.winston.child((0, _extends2.default)({}, metaData, {
label: childName,
pid: process.pid
}));
childLogger.setFilters(this._filters);
this.trace(`Setup child '${childName}' logger instance`);
return childLogger;
}
/**
* Add a field to all log lines for this logger. For convenience `this` object is returned.
*
* @param {String} key The name of the field to add.
* @param {*} value The value of the field to be logged.
* @return {Logger} this
*/
addMetaData(key, value) {
this.winston.defaultMeta[key] = value;
return this;
}
/**
* Logs at `silly` level with filtering applied. For convenience `this` object is returned.
*
* @param {*} args Any number of arguments to be logged.
* @return {Logger} this
*/
silly(...args) {
this.winston.silly(...args);
return this;
}
trace(...args) {
return this.silly(...args);
}
/**
* Logs at `debug` level with filtering applied. For convenience `this` object is returned.
*
* @param {*} args Any number of arguments to be logged.
* @return {Logger} this
*/
debug(...args) {
this.winston.debug(...args);
return this;
}
/**
* Logs at `info` level with filtering applied. For convenience `this` object is returned.
*
* @param {*} args Any number of arguments to be logged.
* @return {Logger} this
*/
info(...args) {
this.winston.info(...args);
return this;
}
log(...args) {
this.winston.info(...args);
return this;
}
/**
* Logs at `warn` level with filtering applied. For convenience `this` object is returned.
*
* @param {*} args Any number of arguments to be logged.
* @return {Logger} this
*/
warn(...args) {
this.winston.warn(...args);
return this;
}
/**
* Logs at `error` level with filtering applied. For convenience `this` object is returned.
*
* @param {*} args Any number of arguments to be logged.
* @return {Logger} this
*/
error(...args) {
this.winston.error(...args);
return this;
}
/**
* Logs at `fatal` level. For convenience `this` object is returned.
*
* @param {*} args Any number of arguments to be logged.
* @return {Logger} this
*/
fatal(...args) {
// Always show fatal to stderr
// eslint-disable-next-line no-console
console.error(...args);
this.winston.error(...args);
return this;
}
}
exports.Logger = Logger;