lib/errors.js (203 lines of code) (raw):

/* * Copyright Elasticsearch B.V. and other contributors where applicable. * Licensed under the BSD 2-Clause License; you may not use this file except in * compliance with the BSD 2-Clause License. */ 'use strict'; // Handle creating error event objects to be sent to APM server. // https://github.com/elastic/apm-server/blob/master/docs/spec/v2/error.json const crypto = require('crypto'); var path = require('path'); const util = require('util'); const { gatherStackTrace } = require('./stacktraces'); const MYSQL_ERROR_MSG_RE = /(ER_[A-Z_]+): /; // ---- internal support functions // Default `culprit` to the top of the stack or the highest non `library_frame` // frame if such exists function culpritFromStacktrace(frames) { if (frames.length === 0) return; var filename = frames[0].filename; var fnName = frames[0].function; for (var n = 0; n < frames.length; n++) { if (!frames[n].library_frame) { filename = frames[n].filename; fnName = frames[n].function; break; } } return filename ? fnName + ' (' + filename + ')' : fnName; } // Infer the node.js module name from the top frame filename, if possible. // Here `frames` is a data structure as returned by `parseStackTrace`. // // Examples: // node_modules/mymodule/index.js // ^^^^^^^^ // node_modules/@myorg/mymodule/index.js // ^^^^^^^^^^^^^^^ // or on Windows: // node_modules\@myorg\mymodule\lib\subpath\index.js // ^^^^^^^^^^^^^^^ function _moduleNameFromFrames(frames) { if (frames.length === 0) { return null; } var frame = frames[0]; if (!frame.library_frame) { return null; } var idx = frame.filename.lastIndexOf('node_modules' + path.sep); if (idx === -1) { return null; } var parts = frame.filename.slice(idx).split(path.sep); if (!parts[1]) { return null; } else if (parts[1].startsWith('@')) { if (!parts[2]) { // node_modules/@foo return null; } else { // Normalize the module name separator to '/', even on Windows. return parts[1] + '/' + parts[2]; } } else { return parts[1]; } } // Gather properties from `err` to be used for `error.exception.attributes`. // If there are no properties to include, then it returns undefined. function attributesFromErr(err) { let n = 0; const attrs = {}; const keys = Object.keys(err); for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (key === 'stack') { continue; // 'stack' seems to be enumerable in Node 0.11 } if (key === 'code') { continue; // 'code' is already used for `error.exception.code` } let val = err[key]; if (val === null) { continue; // null is typeof object and well break the switch below } switch (typeof val) { case 'function': continue; case 'object': // Ignore all objects except Dates. if ( typeof val.toISOString !== 'function' || typeof val.getTime !== 'function' ) { continue; } else if (Number.isNaN(val.getTime())) { val = 'Invalid Date'; // calling toISOString() on invalid dates throws } else { val = val.toISOString(); } } attrs[key] = val; n++; } return n ? attrs : undefined; } // ---- exports function generateErrorId() { return crypto.randomBytes(16).toString('hex'); } // Create an "error" APM event object to be sent to APM server. // // Required args: // - Exactly one of `args.exception` or `args.logMessage` must be set. // `args.exception` is an Error instance. `args.logMessage` is a log message // string, or an object of the form `{ message: 'template', params: [ ... ]}` // which will be formated with `util.format()`. // - `args.id` - An ID for the error. It should be created with // `errors.generateErrorId()`. // - `args.log` {Logger} // - `args.shouldCaptureAttributes` {Boolean} // - `args.timestampUs` {Integer} - Timestamp of the error in microseconds. // - `args.handled` {Boolean} // - `args.sourceLinesAppFrames` {Integer} - Number of lines of source context // to include in stack traces. // - `args.sourceLinesLibraryFrames` {Integer} - Number of lines of source // context to include in stack traces. This and the previous arg are typically // select from the agent `sourceLines{Error,Span}{App,Library}Frames` config // vars. // // Optional args: // - `args.callSiteLoc` - A `Error.captureStackTrace`d object with a stack to // include as `error.log.stacktrace`. // - `args.traceContext` - The current TraceContext, if any. // - `args.trans` - The current transaction, if any. // - `args.errorContext` - An object to be included as `error.context`. // - `args.message` - A message string that will be included as `error.log.message` // if `args.exception` is given. Ignored if `args.logMessage` is given. // - `args.exceptionType` - A string to use for `error.exception.type`. By // default `args.exception.name` is used. This argument is only relevant if // `args.exception` was provided. // // This always calls back with `cb(null, apmError)`, i.e. it doesn't fail. function createAPMError(args, cb) { let numAsyncStepsRemaining = 0; // finish() will call cb() only when this is 0. const error = { id: args.id, timestamp: args.timestampUs, }; if (args.traceContext) { error.parent_id = args.traceContext.traceparent.id; error.trace_id = args.traceContext.traceparent.traceId; } if (args.trans) { error.transaction_id = args.trans.id; error.transaction = { name: args.trans.name, type: args.trans.type, sampled: args.trans.sampled, }; } if (args.errorContext) { error.context = args.errorContext; } if (args.exception) { // Handle an exception, i.e. `captureError(<an Error instance>, ...)`. const err = args.exception; const errMsg = String(err.message); error.exception = { message: errMsg, type: args.exceptionType || String(err.name), handled: args.handled, }; if ('code' in err) { error.exception.code = String(err.code); } else { // To provide better grouping of mysql errors that happens after the async // boundery, we modify to exception type to include the custom mysql error // type (e.g. ER_PARSE_ERROR) var match = errMsg.match(MYSQL_ERROR_MSG_RE); if (match) { error.exception.code = match[1]; } } // Optional add an alternative error message as well as the exception message. if (args.message && typeof args.message === 'string') { error.log = { message: args.message }; } if (args.shouldCaptureAttributes) { const attrs = attributesFromErr(err); if (attrs) { error.exception.attributes = attrs; } } numAsyncStepsRemaining++; gatherStackTrace( args.log, args.exception, args.sourceLinesAppFrames, args.sourceLinesLibraryFrames, null, // filterCallSite function (_err, stacktrace) { // _err from gatherStackTrace is always null. const culprit = culpritFromStacktrace(stacktrace); if (culprit) { error.culprit = culprit; } const moduleName = _moduleNameFromFrames(stacktrace); if (moduleName) { // TODO: consider if we should include this as it's not originally what module was intended for error.exception.module = moduleName; } error.exception.stacktrace = stacktrace; finish(); }, ); } else { // Handle a logMessage, i.e. `captureError(<not an Error instance>, ...)`. error.log = {}; const msg = args.logMessage; if (typeof msg === 'string') { error.log.message = msg; } else if (typeof msg === 'object' && msg !== null) { if (msg.message) { error.log.message = util.format.apply( this, [msg.message].concat(msg.params), ); error.log.param_message = msg.message; } else { error.log.message = util.inspect(msg); } } else { error.log.message = String(msg); } } if (args.callSiteLoc) { numAsyncStepsRemaining++; gatherStackTrace( args.log, args.callSiteLoc, args.sourceLinesAppFrames, args.sourceLinesLibraryFrames, null, // filterCallSite function (_err, stacktrace) { // _err from gatherStackTrace is always null. if (stacktrace) { // In case there isn't any log object, we'll make a dummy message // as the APM Server requires a message to be present if a // stacktrace also present if (!error.log) { error.log = { message: error.exception.message }; } error.log.stacktrace = stacktrace; finish(); } }, ); } else { numAsyncStepsRemaining++; setImmediate(finish); } function finish() { numAsyncStepsRemaining--; if (numAsyncStepsRemaining === 0) { cb(null, error); } } } module.exports = { generateErrorId, createAPMError, // Exported for testing. attributesFromErr, _moduleNameFromFrames, };