exception-handler.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. /**
  2. * exception-handler.js: Object for handling uncaughtException events.
  3. *
  4. * (C) 2010 Charlie Robbins
  5. * MIT LICENCE
  6. */
  7. 'use strict';
  8. const os = require('os');
  9. const asyncForEach = require('async/forEach');
  10. const debug = require('diagnostics')('winston:exception');
  11. const once = require('one-time');
  12. const stackTrace = require('stack-trace');
  13. const ExceptionStream = require('./exception-stream');
  14. /**
  15. * Object for handling uncaughtException events.
  16. * @type {ExceptionHandler}
  17. */
  18. module.exports = class ExceptionHandler {
  19. /**
  20. * TODO: add contructor description
  21. * @param {!Logger} logger - TODO: add param description
  22. */
  23. constructor(logger) {
  24. if (!logger) {
  25. throw new Error('Logger is required to handle exceptions');
  26. }
  27. this.logger = logger;
  28. this.handlers = new Map();
  29. }
  30. /**
  31. * Handles `uncaughtException` events for the current process by adding any
  32. * handlers passed in.
  33. * @returns {undefined}
  34. */
  35. handle(...args) {
  36. args.forEach(arg => {
  37. if (Array.isArray(arg)) {
  38. return arg.forEach(handler => this._addHandler(handler));
  39. }
  40. this._addHandler(arg);
  41. });
  42. if (!this.catcher) {
  43. this.catcher = this._uncaughtException.bind(this);
  44. process.on('uncaughtException', this.catcher);
  45. }
  46. }
  47. /**
  48. * Removes any handlers to `uncaughtException` events for the current
  49. * process. This does not modify the state of the `this.handlers` set.
  50. * @returns {undefined}
  51. */
  52. unhandle() {
  53. if (this.catcher) {
  54. process.removeListener('uncaughtException', this.catcher);
  55. this.catcher = false;
  56. Array.from(this.handlers.values())
  57. .forEach(wrapper => this.logger.unpipe(wrapper));
  58. }
  59. }
  60. /**
  61. * TODO: add method description
  62. * @param {Error} err - Error to get information about.
  63. * @returns {mixed} - TODO: add return description.
  64. */
  65. getAllInfo(err) {
  66. let { message } = err;
  67. if (!message && typeof err === 'string') {
  68. message = err;
  69. }
  70. return {
  71. error: err,
  72. // TODO (indexzero): how do we configure this?
  73. level: 'error',
  74. message: [
  75. `uncaughtException: ${(message || '(no error message)')}`,
  76. err.stack || ' No stack trace'
  77. ].join('\n'),
  78. stack: err.stack,
  79. exception: true,
  80. date: new Date().toString(),
  81. process: this.getProcessInfo(),
  82. os: this.getOsInfo(),
  83. trace: this.getTrace(err)
  84. };
  85. }
  86. /**
  87. * Gets all relevant process information for the currently running process.
  88. * @returns {mixed} - TODO: add return description.
  89. */
  90. getProcessInfo() {
  91. return {
  92. pid: process.pid,
  93. uid: process.getuid ? process.getuid() : null,
  94. gid: process.getgid ? process.getgid() : null,
  95. cwd: process.cwd(),
  96. execPath: process.execPath,
  97. version: process.version,
  98. argv: process.argv,
  99. memoryUsage: process.memoryUsage()
  100. };
  101. }
  102. /**
  103. * Gets all relevant OS information for the currently running process.
  104. * @returns {mixed} - TODO: add return description.
  105. */
  106. getOsInfo() {
  107. return {
  108. loadavg: os.loadavg(),
  109. uptime: os.uptime()
  110. };
  111. }
  112. /**
  113. * Gets a stack trace for the specified error.
  114. * @param {mixed} err - TODO: add param description.
  115. * @returns {mixed} - TODO: add return description.
  116. */
  117. getTrace(err) {
  118. const trace = err ? stackTrace.parse(err) : stackTrace.get();
  119. return trace.map(site => {
  120. return {
  121. column: site.getColumnNumber(),
  122. file: site.getFileName(),
  123. function: site.getFunctionName(),
  124. line: site.getLineNumber(),
  125. method: site.getMethodName(),
  126. native: site.isNative()
  127. };
  128. });
  129. }
  130. /**
  131. * Helper method to add a transport as an exception handler.
  132. * @param {Transport} handler - The transport to add as an exception handler.
  133. * @returns {void}
  134. */
  135. _addHandler(handler) {
  136. if (!this.handlers.has(handler)) {
  137. handler.handleExceptions = true;
  138. const wrapper = new ExceptionStream(handler);
  139. this.handlers.set(handler, wrapper);
  140. this.logger.pipe(wrapper);
  141. }
  142. }
  143. /**
  144. * Logs all relevant information around the `err` and exits the current
  145. * process.
  146. * @param {Error} err - Error to handle
  147. * @returns {mixed} - TODO: add return description.
  148. * @private
  149. */
  150. _uncaughtException(err) {
  151. const info = this.getAllInfo(err);
  152. const handlers = this._getExceptionHandlers();
  153. // Calculate if we should exit on this error
  154. let doExit = typeof this.logger.exitOnError === 'function'
  155. ? this.logger.exitOnError(err)
  156. : this.logger.exitOnError;
  157. let timeout;
  158. if (!handlers.length && doExit) {
  159. // eslint-disable-next-line no-console
  160. console.warn('winston: exitOnError cannot be false with no exception handlers.');
  161. // eslint-disable-next-line no-console
  162. console.warn('winston: exiting process.');
  163. doExit = false;
  164. }
  165. function gracefulExit() {
  166. debug('doExit', doExit);
  167. debug('process._exiting', process._exiting);
  168. if (doExit && !process._exiting) {
  169. // Remark: Currently ignoring any exceptions from transports when
  170. // catching uncaught exceptions.
  171. if (timeout) {
  172. clearTimeout(timeout);
  173. }
  174. // eslint-disable-next-line no-process-exit
  175. process.exit(1);
  176. }
  177. }
  178. if (!handlers || handlers.length === 0) {
  179. return process.nextTick(gracefulExit);
  180. }
  181. // Log to all transports attempting to listen for when they are completed.
  182. asyncForEach(handlers, (handler, next) => {
  183. // TODO: Change these to the correct WritableStream events so that we
  184. // wait until exit.
  185. const done = once(next);
  186. const transport = handler.transport || handler;
  187. // Debug wrapping so that we can inspect what's going on under the covers.
  188. function onDone(event) {
  189. return () => {
  190. debug(event);
  191. done();
  192. };
  193. }
  194. transport.once('logged', onDone('logged'));
  195. transport.once('error', onDone('error'));
  196. }, gracefulExit);
  197. this.logger.log(info);
  198. // If exitOnError is true, then only allow the logging of exceptions to
  199. // take up to `3000ms`.
  200. if (doExit) {
  201. timeout = setTimeout(gracefulExit, 3000);
  202. }
  203. }
  204. /**
  205. * Returns the list of transports and exceptionHandlers for this instance.
  206. * @returns {Array} - List of transports and exceptionHandlers for this
  207. * instance.
  208. * @private
  209. */
  210. _getExceptionHandlers() {
  211. // Remark (indexzero): since `logger.transports` returns all of the pipes
  212. // from the _readableState of the stream we actually get the join of the
  213. // explicit handlers and the implicit transports with
  214. // `handleExceptions: true`
  215. return this.logger.transports.filter(wrap => {
  216. const transport = wrap.transport || wrap;
  217. return transport.handleExceptions;
  218. });
  219. }
  220. };