logger.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. /**
  2. * logger.js: TODO: add file header description.
  3. *
  4. * (C) 2010 Charlie Robbins
  5. * MIT LICENCE
  6. */
  7. 'use strict';
  8. const { Stream, Transform } = require('readable-stream');
  9. const asyncForEach = require('async/forEach');
  10. const { LEVEL, SPLAT } = require('triple-beam');
  11. const isStream = require('is-stream');
  12. const ExceptionHandler = require('./exception-handler');
  13. const RejectionHandler = require('./rejection-handler');
  14. const LegacyTransportStream = require('winston-transport/legacy');
  15. const Profiler = require('./profiler');
  16. const { warn } = require('./common');
  17. const config = require('./config');
  18. /**
  19. * Captures the number of format (i.e. %s strings) in a given string.
  20. * Based on `util.format`, see Node.js source:
  21. * https://github.com/nodejs/node/blob/b1c8f15c5f169e021f7c46eb7b219de95fe97603/lib/util.js#L201-L230
  22. * @type {RegExp}
  23. */
  24. const formatRegExp = /%[scdjifoO%]/g;
  25. /**
  26. * TODO: add class description.
  27. * @type {Logger}
  28. * @extends {Transform}
  29. */
  30. class Logger extends Transform {
  31. /**
  32. * Constructor function for the Logger object responsible for persisting log
  33. * messages and metadata to one or more transports.
  34. * @param {!Object} options - foo
  35. */
  36. constructor(options) {
  37. super({ objectMode: true });
  38. this.configure(options);
  39. }
  40. child(defaultRequestMetadata) {
  41. const logger = this;
  42. return Object.create(logger, {
  43. write: {
  44. value: function (info) {
  45. const infoClone = Object.assign(
  46. {},
  47. defaultRequestMetadata,
  48. info
  49. );
  50. // Object.assign doesn't copy inherited Error
  51. // properties so we have to do that explicitly
  52. //
  53. // Remark (indexzero): we should remove this
  54. // since the errors format will handle this case.
  55. //
  56. if (info instanceof Error) {
  57. infoClone.stack = info.stack;
  58. infoClone.message = info.message;
  59. }
  60. logger.write(infoClone);
  61. }
  62. }
  63. });
  64. }
  65. /**
  66. * This will wholesale reconfigure this instance by:
  67. * 1. Resetting all transports. Older transports will be removed implicitly.
  68. * 2. Set all other options including levels, colors, rewriters, filters,
  69. * exceptionHandlers, etc.
  70. * @param {!Object} options - TODO: add param description.
  71. * @returns {undefined}
  72. */
  73. configure({
  74. silent,
  75. format,
  76. defaultMeta,
  77. levels,
  78. level = 'info',
  79. exitOnError = true,
  80. transports,
  81. colors,
  82. emitErrs,
  83. formatters,
  84. padLevels,
  85. rewriters,
  86. stripColors,
  87. exceptionHandlers,
  88. rejectionHandlers
  89. } = {}) {
  90. // Reset transports if we already have them
  91. if (this.transports.length) {
  92. this.clear();
  93. }
  94. this.silent = silent;
  95. this.format = format || this.format || require('logform/json')();
  96. this.defaultMeta = defaultMeta || null;
  97. // Hoist other options onto this instance.
  98. this.levels = levels || this.levels || config.npm.levels;
  99. this.level = level;
  100. if (this.exceptions) {
  101. this.exceptions.unhandle();
  102. }
  103. if (this.rejections) {
  104. this.rejections.unhandle();
  105. }
  106. this.exceptions = new ExceptionHandler(this);
  107. this.rejections = new RejectionHandler(this);
  108. this.profilers = {};
  109. this.exitOnError = exitOnError;
  110. // Add all transports we have been provided.
  111. if (transports) {
  112. transports = Array.isArray(transports) ? transports : [transports];
  113. transports.forEach(transport => this.add(transport));
  114. }
  115. if (
  116. colors ||
  117. emitErrs ||
  118. formatters ||
  119. padLevels ||
  120. rewriters ||
  121. stripColors
  122. ) {
  123. throw new Error(
  124. [
  125. '{ colors, emitErrs, formatters, padLevels, rewriters, stripColors } were removed in winston@3.0.0.',
  126. 'Use a custom winston.format(function) instead.',
  127. 'See: https://github.com/winstonjs/winston/tree/master/UPGRADE-3.0.md'
  128. ].join('\n')
  129. );
  130. }
  131. if (exceptionHandlers) {
  132. this.exceptions.handle(exceptionHandlers);
  133. }
  134. if (rejectionHandlers) {
  135. this.rejections.handle(rejectionHandlers);
  136. }
  137. }
  138. isLevelEnabled(level) {
  139. const givenLevelValue = getLevelValue(this.levels, level);
  140. if (givenLevelValue === null) {
  141. return false;
  142. }
  143. const configuredLevelValue = getLevelValue(this.levels, this.level);
  144. if (configuredLevelValue === null) {
  145. return false;
  146. }
  147. if (!this.transports || this.transports.length === 0) {
  148. return configuredLevelValue >= givenLevelValue;
  149. }
  150. const index = this.transports.findIndex(transport => {
  151. let transportLevelValue = getLevelValue(this.levels, transport.level);
  152. if (transportLevelValue === null) {
  153. transportLevelValue = configuredLevelValue;
  154. }
  155. return transportLevelValue >= givenLevelValue;
  156. });
  157. return index !== -1;
  158. }
  159. /* eslint-disable valid-jsdoc */
  160. /**
  161. * Ensure backwards compatibility with a `log` method
  162. * @param {mixed} level - Level the log message is written at.
  163. * @param {mixed} msg - TODO: add param description.
  164. * @param {mixed} meta - TODO: add param description.
  165. * @returns {Logger} - TODO: add return description.
  166. *
  167. * @example
  168. * // Supports the existing API:
  169. * logger.log('info', 'Hello world', { custom: true });
  170. * logger.log('info', new Error('Yo, it\'s on fire'));
  171. *
  172. * // Requires winston.format.splat()
  173. * logger.log('info', '%s %d%%', 'A string', 50, { thisIsMeta: true });
  174. *
  175. * // And the new API with a single JSON literal:
  176. * logger.log({ level: 'info', message: 'Hello world', custom: true });
  177. * logger.log({ level: 'info', message: new Error('Yo, it\'s on fire') });
  178. *
  179. * // Also requires winston.format.splat()
  180. * logger.log({
  181. * level: 'info',
  182. * message: '%s %d%%',
  183. * [SPLAT]: ['A string', 50],
  184. * meta: { thisIsMeta: true }
  185. * });
  186. *
  187. */
  188. /* eslint-enable valid-jsdoc */
  189. log(level, msg, ...splat) {
  190. // eslint-disable-line max-params
  191. // Optimize for the hotpath of logging JSON literals
  192. if (arguments.length === 1) {
  193. // Yo dawg, I heard you like levels ... seriously ...
  194. // In this context the LHS `level` here is actually the `info` so read
  195. // this as: info[LEVEL] = info.level;
  196. level[LEVEL] = level.level;
  197. this._addDefaultMeta(level);
  198. this.write(level);
  199. return this;
  200. }
  201. // Slightly less hotpath, but worth optimizing for.
  202. if (arguments.length === 2) {
  203. if (msg && typeof msg === 'object') {
  204. msg[LEVEL] = msg.level = level;
  205. this._addDefaultMeta(msg);
  206. this.write(msg);
  207. return this;
  208. }
  209. msg = { [LEVEL]: level, level, message: msg };
  210. this._addDefaultMeta(msg);
  211. this.write(msg);
  212. return this;
  213. }
  214. const [meta] = splat;
  215. if (typeof meta === 'object' && meta !== null) {
  216. // Extract tokens, if none available default to empty array to
  217. // ensure consistancy in expected results
  218. const tokens = msg && msg.match && msg.match(formatRegExp);
  219. if (!tokens) {
  220. const info = Object.assign({}, this.defaultMeta, meta, {
  221. [LEVEL]: level,
  222. [SPLAT]: splat,
  223. level,
  224. message: msg
  225. });
  226. if (meta.message) info.message = `${info.message} ${meta.message}`;
  227. if (meta.stack) info.stack = meta.stack;
  228. this.write(info);
  229. return this;
  230. }
  231. }
  232. this.write(Object.assign({}, this.defaultMeta, {
  233. [LEVEL]: level,
  234. [SPLAT]: splat,
  235. level,
  236. message: msg
  237. }));
  238. return this;
  239. }
  240. /**
  241. * Pushes data so that it can be picked up by all of our pipe targets.
  242. * @param {mixed} info - TODO: add param description.
  243. * @param {mixed} enc - TODO: add param description.
  244. * @param {mixed} callback - Continues stream processing.
  245. * @returns {undefined}
  246. * @private
  247. */
  248. _transform(info, enc, callback) {
  249. if (this.silent) {
  250. return callback();
  251. }
  252. // [LEVEL] is only soft guaranteed to be set here since we are a proper
  253. // stream. It is likely that `info` came in through `.log(info)` or
  254. // `.info(info)`. If it is not defined, however, define it.
  255. // This LEVEL symbol is provided by `triple-beam` and also used in:
  256. // - logform
  257. // - winston-transport
  258. // - abstract-winston-transport
  259. if (!info[LEVEL]) {
  260. info[LEVEL] = info.level;
  261. }
  262. // Remark: really not sure what to do here, but this has been reported as
  263. // very confusing by pre winston@2.0.0 users as quite confusing when using
  264. // custom levels.
  265. if (!this.levels[info[LEVEL]] && this.levels[info[LEVEL]] !== 0) {
  266. // eslint-disable-next-line no-console
  267. console.error('[winston] Unknown logger level: %s', info[LEVEL]);
  268. }
  269. // Remark: not sure if we should simply error here.
  270. if (!this._readableState.pipes) {
  271. // eslint-disable-next-line no-console
  272. console.error(
  273. '[winston] Attempt to write logs with no transports, which can increase memory usage: %j',
  274. info
  275. );
  276. }
  277. // Here we write to the `format` pipe-chain, which on `readable` above will
  278. // push the formatted `info` Object onto the buffer for this instance. We trap
  279. // (and re-throw) any errors generated by the user-provided format, but also
  280. // guarantee that the streams callback is invoked so that we can continue flowing.
  281. try {
  282. this.push(this.format.transform(info, this.format.options));
  283. } finally {
  284. this._writableState.sync = false;
  285. // eslint-disable-next-line callback-return
  286. callback();
  287. }
  288. }
  289. /**
  290. * Delays the 'finish' event until all transport pipe targets have
  291. * also emitted 'finish' or are already finished.
  292. * @param {mixed} callback - Continues stream processing.
  293. */
  294. _final(callback) {
  295. const transports = this.transports.slice();
  296. asyncForEach(
  297. transports,
  298. (transport, next) => {
  299. if (!transport || transport.finished) return setImmediate(next);
  300. transport.once('finish', next);
  301. transport.end();
  302. },
  303. callback
  304. );
  305. }
  306. /**
  307. * Adds the transport to this logger instance by piping to it.
  308. * @param {mixed} transport - TODO: add param description.
  309. * @returns {Logger} - TODO: add return description.
  310. */
  311. add(transport) {
  312. // Support backwards compatibility with all existing `winston < 3.x.x`
  313. // transports which meet one of two criteria:
  314. // 1. They inherit from winston.Transport in < 3.x.x which is NOT a stream.
  315. // 2. They expose a log method which has a length greater than 2 (i.e. more then
  316. // just `log(info, callback)`.
  317. const target =
  318. !isStream(transport) || transport.log.length > 2
  319. ? new LegacyTransportStream({ transport })
  320. : transport;
  321. if (!target._writableState || !target._writableState.objectMode) {
  322. throw new Error(
  323. 'Transports must WritableStreams in objectMode. Set { objectMode: true }.'
  324. );
  325. }
  326. // Listen for the `error` event and the `warn` event on the new Transport.
  327. this._onEvent('error', target);
  328. this._onEvent('warn', target);
  329. this.pipe(target);
  330. if (transport.handleExceptions) {
  331. this.exceptions.handle();
  332. }
  333. if (transport.handleRejections) {
  334. this.rejections.handle();
  335. }
  336. return this;
  337. }
  338. /**
  339. * Removes the transport from this logger instance by unpiping from it.
  340. * @param {mixed} transport - TODO: add param description.
  341. * @returns {Logger} - TODO: add return description.
  342. */
  343. remove(transport) {
  344. if (!transport) return this;
  345. let target = transport;
  346. if (!isStream(transport) || transport.log.length > 2) {
  347. target = this.transports.filter(
  348. match => match.transport === transport
  349. )[0];
  350. }
  351. if (target) {
  352. this.unpipe(target);
  353. }
  354. return this;
  355. }
  356. /**
  357. * Removes all transports from this logger instance.
  358. * @returns {Logger} - TODO: add return description.
  359. */
  360. clear() {
  361. this.unpipe();
  362. return this;
  363. }
  364. /**
  365. * Cleans up resources (streams, event listeners) for all transports
  366. * associated with this instance (if necessary).
  367. * @returns {Logger} - TODO: add return description.
  368. */
  369. close() {
  370. this.exceptions.unhandle();
  371. this.rejections.unhandle();
  372. this.clear();
  373. this.emit('close');
  374. return this;
  375. }
  376. /**
  377. * Sets the `target` levels specified on this instance.
  378. * @param {Object} Target levels to use on this instance.
  379. */
  380. setLevels() {
  381. warn.deprecated('setLevels');
  382. }
  383. /**
  384. * Queries the all transports for this instance with the specified `options`.
  385. * This will aggregate each transport's results into one object containing
  386. * a property per transport.
  387. * @param {Object} options - Query options for this instance.
  388. * @param {function} callback - Continuation to respond to when complete.
  389. */
  390. query(options, callback) {
  391. if (typeof options === 'function') {
  392. callback = options;
  393. options = {};
  394. }
  395. options = options || {};
  396. const results = {};
  397. const queryObject = Object.assign({}, options.query || {});
  398. // Helper function to query a single transport
  399. function queryTransport(transport, next) {
  400. if (options.query && typeof transport.formatQuery === 'function') {
  401. options.query = transport.formatQuery(queryObject);
  402. }
  403. transport.query(options, (err, res) => {
  404. if (err) {
  405. return next(err);
  406. }
  407. if (typeof transport.formatResults === 'function') {
  408. res = transport.formatResults(res, options.format);
  409. }
  410. next(null, res);
  411. });
  412. }
  413. // Helper function to accumulate the results from `queryTransport` into
  414. // the `results`.
  415. function addResults(transport, next) {
  416. queryTransport(transport, (err, result) => {
  417. // queryTransport could potentially invoke the callback multiple times
  418. // since Transport code can be unpredictable.
  419. if (next) {
  420. result = err || result;
  421. if (result) {
  422. results[transport.name] = result;
  423. }
  424. // eslint-disable-next-line callback-return
  425. next();
  426. }
  427. next = null;
  428. });
  429. }
  430. // Iterate over the transports in parallel setting the appropriate key in
  431. // the `results`.
  432. asyncForEach(
  433. this.transports.filter(transport => !!transport.query),
  434. addResults,
  435. () => callback(null, results)
  436. );
  437. }
  438. /**
  439. * Returns a log stream for all transports. Options object is optional.
  440. * @param{Object} options={} - Stream options for this instance.
  441. * @returns {Stream} - TODO: add return description.
  442. */
  443. stream(options = {}) {
  444. const out = new Stream();
  445. const streams = [];
  446. out._streams = streams;
  447. out.destroy = () => {
  448. let i = streams.length;
  449. while (i--) {
  450. streams[i].destroy();
  451. }
  452. };
  453. // Create a list of all transports for this instance.
  454. this.transports
  455. .filter(transport => !!transport.stream)
  456. .forEach(transport => {
  457. const str = transport.stream(options);
  458. if (!str) {
  459. return;
  460. }
  461. streams.push(str);
  462. str.on('log', log => {
  463. log.transport = log.transport || [];
  464. log.transport.push(transport.name);
  465. out.emit('log', log);
  466. });
  467. str.on('error', err => {
  468. err.transport = err.transport || [];
  469. err.transport.push(transport.name);
  470. out.emit('error', err);
  471. });
  472. });
  473. return out;
  474. }
  475. /**
  476. * Returns an object corresponding to a specific timing. When done is called
  477. * the timer will finish and log the duration. e.g.:
  478. * @returns {Profile} - TODO: add return description.
  479. * @example
  480. * const timer = winston.startTimer()
  481. * setTimeout(() => {
  482. * timer.done({
  483. * message: 'Logging message'
  484. * });
  485. * }, 1000);
  486. */
  487. startTimer() {
  488. return new Profiler(this);
  489. }
  490. /**
  491. * Tracks the time inbetween subsequent calls to this method with the same
  492. * `id` parameter. The second call to this method will log the difference in
  493. * milliseconds along with the message.
  494. * @param {string} id Unique id of the profiler
  495. * @returns {Logger} - TODO: add return description.
  496. */
  497. profile(id, ...args) {
  498. const time = Date.now();
  499. if (this.profilers[id]) {
  500. const timeEnd = this.profilers[id];
  501. delete this.profilers[id];
  502. // Attempt to be kind to users if they are still using older APIs.
  503. if (typeof args[args.length - 2] === 'function') {
  504. // eslint-disable-next-line no-console
  505. console.warn(
  506. 'Callback function no longer supported as of winston@3.0.0'
  507. );
  508. args.pop();
  509. }
  510. // Set the duration property of the metadata
  511. const info = typeof args[args.length - 1] === 'object' ? args.pop() : {};
  512. info.level = info.level || 'info';
  513. info.durationMs = time - timeEnd;
  514. info.message = info.message || id;
  515. return this.write(info);
  516. }
  517. this.profilers[id] = time;
  518. return this;
  519. }
  520. /**
  521. * Backwards compatibility to `exceptions.handle` in winston < 3.0.0.
  522. * @returns {undefined}
  523. * @deprecated
  524. */
  525. handleExceptions(...args) {
  526. // eslint-disable-next-line no-console
  527. console.warn(
  528. 'Deprecated: .handleExceptions() will be removed in winston@4. Use .exceptions.handle()'
  529. );
  530. this.exceptions.handle(...args);
  531. }
  532. /**
  533. * Backwards compatibility to `exceptions.handle` in winston < 3.0.0.
  534. * @returns {undefined}
  535. * @deprecated
  536. */
  537. unhandleExceptions(...args) {
  538. // eslint-disable-next-line no-console
  539. console.warn(
  540. 'Deprecated: .unhandleExceptions() will be removed in winston@4. Use .exceptions.unhandle()'
  541. );
  542. this.exceptions.unhandle(...args);
  543. }
  544. /**
  545. * Throw a more meaningful deprecation notice
  546. * @throws {Error} - TODO: add throws description.
  547. */
  548. cli() {
  549. throw new Error(
  550. [
  551. 'Logger.cli() was removed in winston@3.0.0',
  552. 'Use a custom winston.formats.cli() instead.',
  553. 'See: https://github.com/winstonjs/winston/tree/master/UPGRADE-3.0.md'
  554. ].join('\n')
  555. );
  556. }
  557. /**
  558. * Bubbles the `event` that occured on the specified `transport` up
  559. * from this instance.
  560. * @param {string} event - The event that occured
  561. * @param {Object} transport - Transport on which the event occured
  562. * @private
  563. */
  564. _onEvent(event, transport) {
  565. function transportEvent(err) {
  566. // https://github.com/winstonjs/winston/issues/1364
  567. if (event === 'error' && !this.transports.includes(transport)) {
  568. this.add(transport);
  569. }
  570. this.emit(event, err, transport);
  571. }
  572. if (!transport['__winston' + event]) {
  573. transport['__winston' + event] = transportEvent.bind(this);
  574. transport.on(event, transport['__winston' + event]);
  575. }
  576. }
  577. _addDefaultMeta(msg) {
  578. if (this.defaultMeta) {
  579. Object.assign(msg, this.defaultMeta);
  580. }
  581. }
  582. }
  583. function getLevelValue(levels, level) {
  584. const value = levels[level];
  585. if (!value && value !== 0) {
  586. return null;
  587. }
  588. return value;
  589. }
  590. /**
  591. * Represents the current readableState pipe targets for this Logger instance.
  592. * @type {Array|Object}
  593. */
  594. Object.defineProperty(Logger.prototype, 'transports', {
  595. configurable: false,
  596. enumerable: true,
  597. get() {
  598. const { pipes } = this._readableState;
  599. return !Array.isArray(pipes) ? [pipes].filter(Boolean) : pipes;
  600. }
  601. });
  602. module.exports = Logger;