logger.js 16 KB

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