252 lines
6.8 KiB
JavaScript
252 lines
6.8 KiB
JavaScript
/**
|
|
* exception-handler.js: Object for handling uncaughtException events.
|
|
*
|
|
* (C) 2010 Charlie Robbins
|
|
* MIT LICENCE
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const os = require('os');
|
|
const asyncForEach = require('async/forEach');
|
|
const debug = require('@dabh/diagnostics')('winston:rejection');
|
|
const once = require('one-time');
|
|
const stackTrace = require('stack-trace');
|
|
const ExceptionStream = require('./exception-stream');
|
|
|
|
/**
|
|
* Object for handling unhandledRejection events.
|
|
* @type {RejectionHandler}
|
|
*/
|
|
module.exports = class RejectionHandler {
|
|
/**
|
|
* TODO: add contructor description
|
|
* @param {!Logger} logger - TODO: add param description
|
|
*/
|
|
constructor(logger) {
|
|
if (!logger) {
|
|
throw new Error('Logger is required to handle rejections');
|
|
}
|
|
|
|
this.logger = logger;
|
|
this.handlers = new Map();
|
|
}
|
|
|
|
/**
|
|
* Handles `unhandledRejection` events for the current process by adding any
|
|
* handlers passed in.
|
|
* @returns {undefined}
|
|
*/
|
|
handle(...args) {
|
|
args.forEach(arg => {
|
|
if (Array.isArray(arg)) {
|
|
return arg.forEach(handler => this._addHandler(handler));
|
|
}
|
|
|
|
this._addHandler(arg);
|
|
});
|
|
|
|
if (!this.catcher) {
|
|
this.catcher = this._unhandledRejection.bind(this);
|
|
process.on('unhandledRejection', this.catcher);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes any handlers to `unhandledRejection` events for the current
|
|
* process. This does not modify the state of the `this.handlers` set.
|
|
* @returns {undefined}
|
|
*/
|
|
unhandle() {
|
|
if (this.catcher) {
|
|
process.removeListener('unhandledRejection', this.catcher);
|
|
this.catcher = false;
|
|
|
|
Array.from(this.handlers.values()).forEach(wrapper =>
|
|
this.logger.unpipe(wrapper)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TODO: add method description
|
|
* @param {Error} err - Error to get information about.
|
|
* @returns {mixed} - TODO: add return description.
|
|
*/
|
|
getAllInfo(err) {
|
|
let { message } = err;
|
|
if (!message && typeof err === 'string') {
|
|
message = err;
|
|
}
|
|
|
|
return {
|
|
error: err,
|
|
// TODO (indexzero): how do we configure this?
|
|
level: 'error',
|
|
message: [
|
|
`unhandledRejection: ${message || '(no error message)'}`,
|
|
err.stack || ' No stack trace'
|
|
].join('\n'),
|
|
stack: err.stack,
|
|
exception: true,
|
|
date: new Date().toString(),
|
|
process: this.getProcessInfo(),
|
|
os: this.getOsInfo(),
|
|
trace: this.getTrace(err)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Gets all relevant process information for the currently running process.
|
|
* @returns {mixed} - TODO: add return description.
|
|
*/
|
|
getProcessInfo() {
|
|
return {
|
|
pid: process.pid,
|
|
uid: process.getuid ? process.getuid() : null,
|
|
gid: process.getgid ? process.getgid() : null,
|
|
cwd: process.cwd(),
|
|
execPath: process.execPath,
|
|
version: process.version,
|
|
argv: process.argv,
|
|
memoryUsage: process.memoryUsage()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Gets all relevant OS information for the currently running process.
|
|
* @returns {mixed} - TODO: add return description.
|
|
*/
|
|
getOsInfo() {
|
|
return {
|
|
loadavg: os.loadavg(),
|
|
uptime: os.uptime()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Gets a stack trace for the specified error.
|
|
* @param {mixed} err - TODO: add param description.
|
|
* @returns {mixed} - TODO: add return description.
|
|
*/
|
|
getTrace(err) {
|
|
const trace = err ? stackTrace.parse(err) : stackTrace.get();
|
|
return trace.map(site => {
|
|
return {
|
|
column: site.getColumnNumber(),
|
|
file: site.getFileName(),
|
|
function: site.getFunctionName(),
|
|
line: site.getLineNumber(),
|
|
method: site.getMethodName(),
|
|
native: site.isNative()
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper method to add a transport as an exception handler.
|
|
* @param {Transport} handler - The transport to add as an exception handler.
|
|
* @returns {void}
|
|
*/
|
|
_addHandler(handler) {
|
|
if (!this.handlers.has(handler)) {
|
|
handler.handleRejections = true;
|
|
const wrapper = new ExceptionStream(handler);
|
|
this.handlers.set(handler, wrapper);
|
|
this.logger.pipe(wrapper);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Logs all relevant information around the `err` and exits the current
|
|
* process.
|
|
* @param {Error} err - Error to handle
|
|
* @returns {mixed} - TODO: add return description.
|
|
* @private
|
|
*/
|
|
_unhandledRejection(err) {
|
|
const info = this.getAllInfo(err);
|
|
const handlers = this._getRejectionHandlers();
|
|
// Calculate if we should exit on this error
|
|
let doExit =
|
|
typeof this.logger.exitOnError === 'function'
|
|
? this.logger.exitOnError(err)
|
|
: this.logger.exitOnError;
|
|
let timeout;
|
|
|
|
if (!handlers.length && doExit) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn('winston: exitOnError cannot be true with no rejection handlers.');
|
|
// eslint-disable-next-line no-console
|
|
console.warn('winston: not exiting process.');
|
|
doExit = false;
|
|
}
|
|
|
|
function gracefulExit() {
|
|
debug('doExit', doExit);
|
|
debug('process._exiting', process._exiting);
|
|
|
|
if (doExit && !process._exiting) {
|
|
// Remark: Currently ignoring any rejections from transports when
|
|
// catching unhandled rejections.
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
}
|
|
// eslint-disable-next-line no-process-exit
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (!handlers || handlers.length === 0) {
|
|
return process.nextTick(gracefulExit);
|
|
}
|
|
|
|
// Log to all transports attempting to listen for when they are completed.
|
|
asyncForEach(
|
|
handlers,
|
|
(handler, next) => {
|
|
const done = once(next);
|
|
const transport = handler.transport || handler;
|
|
|
|
// Debug wrapping so that we can inspect what's going on under the covers.
|
|
function onDone(event) {
|
|
return () => {
|
|
debug(event);
|
|
done();
|
|
};
|
|
}
|
|
|
|
transport._ending = true;
|
|
transport.once('finish', onDone('finished'));
|
|
transport.once('error', onDone('error'));
|
|
},
|
|
() => doExit && gracefulExit()
|
|
);
|
|
|
|
this.logger.log(info);
|
|
|
|
// If exitOnError is true, then only allow the logging of exceptions to
|
|
// take up to `3000ms`.
|
|
if (doExit) {
|
|
timeout = setTimeout(gracefulExit, 3000);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the list of transports and exceptionHandlers for this instance.
|
|
* @returns {Array} - List of transports and exceptionHandlers for this
|
|
* instance.
|
|
* @private
|
|
*/
|
|
_getRejectionHandlers() {
|
|
// Remark (indexzero): since `logger.transports` returns all of the pipes
|
|
// from the _readableState of the stream we actually get the join of the
|
|
// explicit handlers and the implicit transports with
|
|
// `handleRejections: true`
|
|
return this.logger.transports.filter(wrap => {
|
|
const transport = wrap.transport || wrap;
|
|
return transport.handleRejections;
|
|
});
|
|
}
|
|
};
|