UNPKG

newrelic

Version:
256 lines (217 loc) 8.91 kB
/* * Copyright 2020 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' 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' 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 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 instrumentatation * @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) return [0, name, message, type, params, transaction?.id] } function isValidErrorGroupOutput(output) { return (typeof output === 'string' || output instanceof String) && output !== '' } function maybeAddAgentAttributes(attributes, exception) { if (exception.errorGroupCallback) { 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