newrelic
Version:
New Relic agent
316 lines (271 loc) • 11.2 kB
JavaScript
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
const logger = require('../logger').child({ component: 'errors_lib' })
const DESTINATIONS = require('../config/attribute-filter').DESTINATIONS
const props = require('../util/properties')
const urltils = require('../util/urltils')
const errorHelper = require('../errors/helper')
const {
maybeAddQueueAttributes,
maybeAddExternalAttributes,
maybeAddDatabaseAttributes
} = require('../util/attributes')
const synthetics = require('../synthetics')
const ERROR_EXPECTED_PATH = 'error.expected'
/**
* Wraps `Error` instances for delivery to the back end. The wrapper includes
* the original error along with metadata, such as custom attributes, to be
* delivered.
*
* @class
* @param {object} options Configuration options for the exception
* @param {Error} options.error The error object to be wrapped
* @param {number} [options.timestamp=0] Unix timestamp (in milliseconds) when the error occurred
* @param {object} [options.customAttributes={}] Custom user-defined attributes associated with the error
* @param {object} [options.agentAttributes={}] Agent-generated attributes associated with the error
* @param {boolean} [options.expected] Whether this error is expected (not a true failure)
*/
class Exception {
constructor({ error, timestamp, customAttributes, agentAttributes, expected }) {
this.error = error
this.timestamp = timestamp || 0
this.customAttributes = customAttributes || {}
this.agentAttributes = agentAttributes || {}
this._expected = expected
this.errorGroupCallback = null
}
getErrorDetails(config) {
const errorDetails = errorHelper.extractErrorInformation(null, this.error, config)
if (typeof this._expected === 'undefined') {
this._expected = errorHelper.isExpected(
errorDetails.type,
errorDetails.message,
null,
config,
urltils
)
}
errorDetails.expected = this._expected
return errorDetails
}
}
/**
* Given the `.cause` value from an `Error` instance, serialize the chain
* into an object than can be attached to the agent attributes object.
*
* @param {Error|string} input The error cause to serialize.
* @returns {object[]} Serialized causes object array.
*/
function serializeCauses(input) {
if (!input) {
return []
}
if (typeof input === 'string') {
return [{ message: input }]
}
if (
Object.hasOwn(input, 'message') === false ||
Object.hasOwn(input, 'stack') === false
) {
return [{ message: input.message ?? 'cause does not look like an Error instance' }]
}
return [{
message: input.message,
stack: input.stack,
cause: input.cause ? serializeCauses(input.cause) : []
}]
}
/**
* Given either or both of a transaction and an exception, generate an error
* trace in the JSON format expected by the collector. Since this will be
* used by both the HTTP instrumentation, which uses HTTP status codes to
* determine whether a transaction is in error, and the domain-based error
* handler, which traps actual instances of Error, try to set sensible
* defaults for everything.
*
* NOTE: this function returns an array, but also conditionally mutates the array
* to add a "transaction" property with the transaction id to the array, which works
* because everything's an object in JS. I'm not entirely sure why we do this, but
* weird enough to make note of
*
* @param {Transaction} transaction The agent transaction, coming from the instrumentation
* @param {Exception} exception An custom Exception object with the error and other information
* @param {object} config The configuration to use when creating the object
* @returns {Array} an Array of Error information, [0] -> placeholder,
* [1] -> name extracted from error info,
* [2] -> extracted error message,
* [3] -> extracted error type,
* [4] -> attributes,
* [5] -> transaction id
*/
function createError(transaction, exception, config) {
const error = exception.error
const { name, message, type } = errorHelper.extractErrorInformation(
transaction,
error,
config,
urltils
)
const params = {
userAttributes: Object.create(null),
agentAttributes: Object.create(null),
intrinsics: Object.create(null)
}
if (transaction) {
// Copy all of the parameters off of the transaction.
params.intrinsics = transaction.getIntrinsicAttributes()
const transactionAgentAttributes =
transaction.trace.attributes.get(DESTINATIONS.ERROR_EVENT) || {}
// Merge the agent attributes specific to this error event with the transaction attributes
params.agentAttributes = Object.assign(exception.agentAttributes, transactionAgentAttributes)
// There should be no attributes to copy in HSM, but check status anyway
if (!config.high_security) {
urltils.overwriteParameters(
transaction.trace.custom.get(DESTINATIONS.ERROR_EVENT),
params.userAttributes
)
}
}
maybeAddUserAttributes(params.userAttributes, exception, config)
params.stack_trace = maybeAddStackTrace(exception, config)
params.intrinsics[ERROR_EXPECTED_PATH] =
exception._expected || errorHelper.isExpected(type, message, transaction, config, urltils)
maybeAddAgentAttributes(params, exception)
// We need to detect error causes and do something with them. We don't have
// a spec, or ui expectation, for them. So we are opting to embed them as
// agent attributes. This does not account for `AggregateError` instances.
if (Object.hasOwn(error ?? {}, 'cause') === true) {
// We have to stringify the data in order for it to show up correctly
// in the errors inbox's list of attributes. If we rely on our aggregator
// to stringify it, then the backend will decode it to an array of objects
// and the attribute will not be presented in the UI. By doubly encoding
// it, the back end will decode it to a JSON _string_ and show the
// attribute in the UI as desired.
params.agentAttributes['error.cause'] = JSON.stringify(
serializeCauses(error.cause)
)
}
return [0, name, message, type, params, transaction?.id]
}
function isValidErrorGroupOutput(output) {
return (typeof output === 'string' || output instanceof String) && output !== ''
}
function maybeAddAgentAttributes(attributes, exception) {
if (typeof exception.errorGroupCallback !== 'function') {
return
}
const callbackInput = {
error: exception.error,
customAttributes: Object.assign({}, attributes.userAttributes),
'request.uri': attributes.agentAttributes['request.uri'],
'http.statusCode': attributes.agentAttributes['http.statusCode'],
'http.method': attributes.agentAttributes['request.method'],
'error.expected': attributes.intrinsics[ERROR_EXPECTED_PATH]
}
try {
const callbackOutput = exception.errorGroupCallback(callbackInput)
if (!isValidErrorGroupOutput(callbackOutput)) {
logger.warn('Function provided via setErrorGroupCallback return value malformed')
return
}
attributes.agentAttributes['error.group.name'] = callbackOutput
} catch (err) {
logger.warn(
err,
'Function provided via setErrorGroupCallback failed, not generating `error.group.name`'
)
}
}
function maybeAddUserAttributes(userAttributes, exception, config) {
const customAttributes = exception.customAttributes
if (!config.high_security && config.api.custom_attributes_enabled && customAttributes) {
for (const key in customAttributes) {
if (props.hasOwn(customAttributes, key)) {
const dest = config.attributeFilter.filterTransaction(DESTINATIONS.ERROR_EVENT, key)
// eslint-disable-next-line sonarjs/bitwise-operators
if (dest & DESTINATIONS.ERROR_EVENT) {
userAttributes[key] = customAttributes[key]
}
}
}
}
}
function maybeAddStackTrace(exception, config) {
const stack = exception.error?.stack
let parsedStack
if (stack) {
parsedStack = ('' + stack).split(/[\n\r]/g)
if (config.high_security || config.strip_exception_messages.enabled) {
parsedStack[0] = exception.error.name + ': <redacted>'
}
}
return parsedStack
}
/**
* Creates a structure for error event that is sent to the collector.
* The error parameter is an output of the createError() function for a given exception.
*
* @param {Transaction} transaction the current transaction
* @param {Array} error createError() output
* @param {string} timestamp the timestamp of the error event
* @param {object} config agent configuration object
* @returns {Array} an Array of different types of attributes [0] -> intrinsic, [1] -> user/custom, [2] -> agent
*/
function createEvent(transaction, error, timestamp, config) {
const message = error[2]
const errorClass = error[3]
const errorParams = error[4]
const intrinsicAttributes = _getErrorEventIntrinsicAttrs(
transaction,
errorClass,
message,
errorParams.intrinsics[ERROR_EXPECTED_PATH],
timestamp,
config
)
// the error structure created by createError() already performs filtering of custom
// and agent attributes, so it is ok to just copy them
const userAttributes = Object.assign(Object.create(null), errorParams.userAttributes)
const agentAttributes = Object.assign(Object.create(null), errorParams.agentAttributes)
return [intrinsicAttributes, userAttributes, agentAttributes]
}
function _getErrorEventIntrinsicAttrs(transaction, errorClass, message, expected, timestamp, conf) {
// the server expects seconds instead of milliseconds
if (timestamp) {
timestamp = timestamp / 1000
}
const attributes = {
type: 'TransactionError',
'error.class': errorClass,
'error.message': conf.high_security ? '' : message,
timestamp,
'error.expected': expected
}
if (transaction) {
attributes.transactionName = transaction.getFullName()
attributes.duration = transaction.timer.getDurationInMillis() / 1000
maybeAddQueueAttributes(transaction, attributes)
maybeAddExternalAttributes(transaction, attributes)
maybeAddDatabaseAttributes(transaction, attributes)
synthetics.assignTransactionAttrs(transaction, attributes)
if (transaction.agent.config.distributed_tracing.enabled) {
transaction.addDistributedTraceIntrinsics(attributes)
} else {
attributes['nr.referringTransactionGuid'] = transaction.referringTransactionGuid
}
attributes['nr.transactionGuid'] = transaction.id
attributes.guid = transaction.id
if (transaction.port) {
attributes.port = transaction.port
}
} else {
attributes.transactionName = 'Unknown'
}
return attributes
}
module.exports.createError = createError
module.exports.createEvent = createEvent
module.exports.Exception = Exception