/** * logger.js: TODO: add file header description. * * (C) 2010 Charlie Robbins * MIT LICENCE */ 'use strict'; const stream = require('readable-stream'); const asyncForEach = require('async/forEach'); const { LEVEL, SPLAT } = require('triple-beam'); const isStream = require('is-stream'); const ExceptionHandler = require('./exception-handler'); const LegacyTransportStream = require('winston-transport/legacy'); const Profiler = require('./profiler'); const { clone, warn } = require('./common'); const config = require('./config'); /** * TODO: add class description. * @type {Logger} * @extends {stream.Transform} */ class Logger extends stream.Transform { /** * Constructor function for the Logger object responsible for persisting log * messages and metadata to one or more transports. * @param {!Object} options - foo */ constructor(options) { super({ objectMode: true }); this.configure(options); } /** * This will wholesale reconfigure this instance by: * 1. Resetting all transports. Older transports will be removed implicitly. * 2. Set all other options including levels, colors, rewriters, filters, * exceptionHandlers, etc. * @param {!Object} options - TODO: add param description. * @returns {undefined} */ configure({ silent, format, levels, level = 'info', exitOnError = true, transports, colors, emitErrs, formatters, padLevels, rewriters, stripColors, exceptionHandlers } = {}) { // Reset transports if we already have them if (this.transports.length) { this.clear(); } this.silent = silent; this.format = format || this.format || require('logform/json')(); // Hoist other options onto this instance. this.levels = levels || this.levels || config.npm.levels; this.level = level; this.exceptions = new ExceptionHandler(this); this.profilers = {}; this.exitOnError = exitOnError; // Add all transports we have been provided. if (transports) { transports = Array.isArray(transports) ? transports : [transports]; transports.forEach(transport => this.add(transport)); } if ( colors || emitErrs || formatters || padLevels || rewriters || stripColors ) { throw new Error([ '{ colors, emitErrs, formatters, padLevels, rewriters, stripColors } were removed in winston@3.0.0.', 'Use a custom winston.format(function) instead.', 'See: https://github.com/winstonjs/winston/tree/master/UPGRADE-3.0.md' ].join('\n')); } if (exceptionHandlers) { this.exceptions.handle(exceptionHandlers); } } isLevelEnabled(level) { const givenLevelValue = getLevelValue(this.levels, level); if (givenLevelValue === null) { return false; } const configuredLevelValue = getLevelValue(this.levels, this.level); if (configuredLevelValue === null) { return false; } if (!this.transports || this.transports.length === 0) { return configuredLevelValue >= givenLevelValue; } const index = this.transports.findIndex(transport => { let transportLevelValue = getLevelValue(this.levels, transport.level); if (transportLevelValue === null) { transportLevelValue = configuredLevelValue; } return transportLevelValue >= givenLevelValue; }); return index !== -1; } /* eslint-disable valid-jsdoc */ /** * Ensure backwards compatibility with a `log` method * @param {mixed} level - Level the log message is written at. * @param {mixed} msg - TODO: add param description. * @param {mixed} meta - TODO: add param description. * @returns {Logger} - TODO: add return description. * * @example * // Supports the existing API: * logger.log('info', 'Hello world', { custom: true }); * logger.log('info', new Error('Yo, it\'s on fire')); * logger.log('info', '%s %d%%', 'A string', 50, { thisIsMeta: true }); * * // And the new API with a single JSON literal: * logger.log({ level: 'info', message: 'Hello world', custom: true }); * logger.log({ level: 'info', message: new Error('Yo, it\'s on fire') }); * logger.log({ * level: 'info', * message: '%s %d%%', * [SPLAT]: ['A string', 50], * meta: { thisIsMeta: true } * }); * */ /* eslint-enable valid-jsdoc */ log(level, msg, ...splat) { // eslint-disable-line max-params // Optimize for the hotpath of logging JSON literals if (arguments.length === 1) { // Yo dawg, I heard you like levels ... seriously ... // In this context the LHS `level` here is actually the `info` so read // this as: info[LEVEL] = info.level; level[LEVEL] = level.level; this.write(level); return this; } // Slightly less hotpath, but worth optimizing for. if (arguments.length === 2) { if (msg && typeof msg === 'object') { msg[LEVEL] = msg.level = level; this.write(msg); return this; } this.write({ [LEVEL]: level, level, message: msg }); return this; } const [meta] = splat; if (typeof meta === 'object' && meta !== null) { this.write(Object.assign({}, meta, { [LEVEL]: level, [SPLAT]: splat.slice(0), level, message: msg })); } else { this.write(Object.assign({}, { [LEVEL]: level, [SPLAT]: splat, level, message: msg })); } return this; } /** * Pushes data so that it can be picked up by all of our pipe targets. * @param {mixed} info - TODO: add param description. * @param {mixed} enc - TODO: add param description. * @param {mixed} callback - Continues stream processing. * @returns {undefined} * @private */ _transform(info, enc, callback) { if (this.silent) { return callback(); } // [LEVEL] is only soft guaranteed to be set here since we are a proper // stream. It is likely that `info` came in through `.log(info)` or // `.info(info)`. If it is not defined, however, define it. // This LEVEL symbol is provided by `triple-beam` and also used in: // - logform // - winston-transport // - abstract-winston-transport if (!info[LEVEL]) { info[LEVEL] = info.level; } // Remark: really not sure what to do here, but this has been reported as // very confusing by pre winston@2.0.0 users as quite confusing when using // custom levels. if (!this.levels[info[LEVEL]] && this.levels[info[LEVEL]] !== 0) { // eslint-disable-next-line no-console console.error('[winston] Unknown logger level: %s', info[LEVEL]); } // Remark: not sure if we should simply error here. if (!this._readableState.pipes) { // eslint-disable-next-line no-console console.error('[winston] Attempt to write logs with no transports %j', info); } // Here we write to the `format` pipe-chain, which on `readable` above will // push the formatted `info` Object onto the buffer for this instance. We trap // (and re-throw) any errors generated by the user-provided format, but also // guarantee that the streams callback is invoked so that we can continue flowing. try { this.push(this.format.transform(info, this.format.options)); } catch (ex) { throw ex; } finally { // eslint-disable-next-line callback-return callback(); } } /** * Delays the 'finish' event until all transport pipe targets have * also emitted 'finish' or are already finished. * @param {mixed} callback - Continues stream processing. */ _final(callback) { const transports = this.transports.slice(); asyncForEach(transports, (transport, next) => { if (!transport || transport.finished) return setImmediate(next); transport.once('finish', next); transport.end(); }, callback); } /** * Adds the transport to this logger instance by piping to it. * @param {mixed} transport - TODO: add param description. * @returns {Logger} - TODO: add return description. */ add(transport) { // Support backwards compatibility with all existing `winston < 3.x.x` // transports which meet one of two criteria: // 1. They inherit from winston.Transport in < 3.x.x which is NOT a stream. // 2. They expose a log method which has a length greater than 2 (i.e. more then // just `log(info, callback)`. const target = !isStream(transport) || transport.log.length > 2 ? new LegacyTransportStream({ transport }) : transport; if (!target._writableState || !target._writableState.objectMode) { throw new Error('Transports must WritableStreams in objectMode. Set { objectMode: true }.'); } // Listen for the `error` event on the new Transport. this._onError(target); this.pipe(target); if (transport.handleExceptions) { this.exceptions.handle(); } return this; } /** * Removes the transport from this logger instance by unpiping from it. * @param {mixed} transport - TODO: add param description. * @returns {Logger} - TODO: add return description. */ remove(transport) { let target = transport; if (!isStream(transport) || transport.log.length > 2) { target = this.transports .filter(match => match.transport === transport)[0]; } if (target) { this.unpipe(target); } return this; } /** * Removes all transports from this logger instance. * @returns {Logger} - TODO: add return description. */ clear() { this.unpipe(); return this; } /** * Cleans up resources (streams, event listeners) for all transports * associated with this instance (if necessary). * @returns {Logger} - TODO: add return description. */ close() { this.clear(); this.emit('close'); return this; } /** * Sets the `target` levels specified on this instance. * @param {Object} Target levels to use on this instance. */ setLevels() { warn.deprecated('setLevels'); } /** * Queries the all transports for this instance with the specified `options`. * This will aggregate each transport's results into one object containing * a property per transport. * @param {Object} options - Query options for this instance. * @param {function} callback - Continuation to respond to when complete. * @retruns {mixed} - TODO: add return description. */ query(options, callback) { if (typeof options === 'function') { callback = options; options = {}; } options = options || {}; const results = {}; const queryObject = clone(options.query) || {}; // Helper function to query a single transport function queryTransport(transport, next) { if (options.query) { options.query = transport.formatQuery(queryObject); } transport.query(options, (err, res) => { if (err) { return next(err); } next(null, transport.formatResults(res, options.format)); }); } // Helper function to accumulate the results from `queryTransport` into // the `results`. function addResults(transport, next) { queryTransport(transport, (err, result) => { // queryTransport could potentially invoke the callback multiple times // since Transport code can be unpredictable. if (next) { result = err || result; if (result) { results[transport.name] = result; } // eslint-disable-next-line callback-return next(); } next = null; }); } // Iterate over the transports in parallel setting the appropriate key in // the `results`. asyncForEach( this.transports.filter(transport => !!transport.query), addResults, () => callback(null, results) ); } /** * Returns a log stream for all transports. Options object is optional. * @param{Object} options={} - Stream options for this instance. * @returns {Stream} - TODO: add return description. */ stream(options = {}) { const out = new stream.Stream(); const streams = []; out._streams = streams; out.destroy = () => { let i = streams.length; while (i--) { streams[i].destroy(); } }; // Create a list of all transports for this instance. this.transports .filter(transport => !!transport.stream) .forEach(transport => { const str = transport.stream(options); if (!str) { return; } streams.push(str); str.on('log', log => { log.transport = log.transport || []; log.transport.push(transport.name); out.emit('log', log); }); str.on('error', err => { err.transport = err.transport || []; err.transport.push(transport.name); out.emit('error', err); }); }); return out; } /** * Returns an object corresponding to a specific timing. When done is called * the timer will finish and log the duration. e.g.: * @returns {Profile} - TODO: add return description. * @example * const timer = winston.startTimer() * setTimeout(() => { * timer.done({ * message: 'Logging message' * }); * }, 1000); */ startTimer() { return new Profiler(this); } /** * Tracks the time inbetween subsequent calls to this method with the same * `id` parameter. The second call to this method will log the difference in * milliseconds along with the message. * @param {string} id Unique id of the profiler * @returns {Logger} - TODO: add return description. */ profile(id, ...args) { const time = Date.now(); if (this.profilers[id]) { const timeEnd = this.profilers[id]; delete this.profilers[id]; // Attempt to be kind to users if they are still using older APIs. if (typeof args[args.length - 2] === 'function') { // eslint-disable-next-line no-console console.warn('Callback function no longer supported as of winston@3.0.0'); args.pop(); } // Set the duration property of the metadata const info = typeof args[args.length - 1] === 'object' ? args.pop() : {}; info.level = info.level || 'info'; info.durationMs = time - timeEnd; info.message = info.message || id; return this.write(info); } this.profilers[id] = time; return this; } /** * Backwards compatibility to `exceptions.handle` in winston < 3.0.0. * @returns {undefined} * @deprecated */ handleExceptions(...args) { // eslint-disable-next-line no-console console.warn('Deprecated: .handleExceptions() will be removed in winston@4. Use .exceptions.handle()'); this.exceptions.handle(...args); } /** * Backwards compatibility to `exceptions.handle` in winston < 3.0.0. * @returns {undefined} * @deprecated */ unhandleExceptions(...args) { // eslint-disable-next-line no-console console.warn('Deprecated: .unhandleExceptions() will be removed in winston@4. Use .exceptions.unhandle()'); this.exceptions.unhandle(...args); } /** * Throw a more meaningful deprecation notice * @throws {Error} - TODO: add throws description. */ cli() { throw new Error([ 'Logger.cli() was removed in winston@3.0.0', 'Use a custom winston.formats.cli() instead.', 'See: https://github.com/winstonjs/winston/tree/master/UPGRADE-3.0.md' ].join('\n')); } /** * Bubbles the error, `err`, that occured on the specified `transport` up * from this instance if `emitErrs` has been set. * @param {Object} transport - Transport on which the error occured * @throws {Error} - Error that occurred on the transport * @private */ _onError(transport) { function transportError(err) { this.emit('error', err, transport); } if (!transport.__winstonError) { transport.__winstonError = transportError.bind(this); transport.on('error', transport.__winstonError); } } } function getLevelValue(levels, level) { const value = levels[level]; if (!value && value !== 0) { return null; } return value; } /** * Represents the current readableState pipe targets for this Logger instance. * @type {Array|Object} */ Object.defineProperty(Logger.prototype, 'transports', { configurable: false, enumerable: true, get() { const { pipes } = this._readableState; return !Array.isArray(pipes) ? [pipes].filter(Boolean) : pipes; } }); module.exports = Logger;