newrelic
Version:
New Relic agent
256 lines (226 loc) • 8.92 kB
JavaScript
/*
* Copyright 2023 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
'use strict'
const logger = require('./logger').child({ component: 'synthetics' })
const hashes = require('./util/hashes')
const { isNotEmpty } = require('./util/objects')
const toSnakeCase = require('./util/snake-case')
const toCamelCase = require('./util/camel-case')
const synthetics = module.exports
const NEWRELIC_SYNTHETICS_HEADER = 'x-newrelic-synthetics'
const NEWRELIC_SYNTHETICS_INFO_HEADER = 'x-newrelic-synthetics-info'
const KEYS = ['version', 'accountId', 'resourceId', 'jobId', 'monitorId']
/**
* Decode the X-NewRelic-Synthetics and X-NewRelic-Synthetics-Info headers
* and assign to transactions as properties both the raw and decoded values
*
* @param {object} config agent config
* @param {object} transaction The current transaction
* @param {object} headers raw http headers
*/
synthetics.assignHeadersToTransaction = function processHeaders(config, transaction, headers) {
const synthHeader = headers[NEWRELIC_SYNTHETICS_HEADER]
const synthInfoHeader = headers[NEWRELIC_SYNTHETICS_INFO_HEADER]
if (synthHeader && config.trusted_account_ids && config.encoding_key) {
assignSyntheticsHeader(
synthHeader,
config.encoding_key,
config.trusted_account_ids,
transaction
)
}
if (synthInfoHeader && config.encoding_key) {
assignSyntheticsInfoHeader(synthInfoHeader, config.encoding_key, transaction)
}
}
/**
* Take the X-NewRelic-Synthetics header and apply any appropriate data to the
* transaction for later use. This is the gate keeper for attributes being
* added onto the transaction object for synthetics.
*
* @param {string} header - The raw X-NewRelic-Synthetics header
* @param {string} encKey - Encoding key handed down from the server
* @param {Array.<number>} trustedIds - Array of accounts to trust the header from.
* @param {object} transaction - Where the synthetics data is attached to.
*/
function assignSyntheticsHeader(header, encKey, trustedIds, transaction) {
const synthData = parseSyntheticsHeader(header, encKey, trustedIds)
if (!synthData) {
return
}
transaction.syntheticsData = synthData
transaction.syntheticsHeader = header
}
/**
* Take the X-NewRelic-Synthetics-Info header and apply any appropriate data to the
* transaction for later use. This is the gate keeper for attributes being
* added onto the transaction object for synthetics info.
*
* @param {string} header - The raw X-NewRelic-Synthetics-Info header
* @param {string} encKey - Encoding key handed down from the server
* @param {object} transaction - Where the synthetics data is attached to.
*/
function assignSyntheticsInfoHeader(header, encKey, transaction) {
const synthInfoData = parseSyntheticsInfoHeader(header, encKey)
if (!synthInfoData) {
return
}
transaction.syntheticsInfoData = synthInfoData
transaction.syntheticsInfoHeader = header
}
/**
* Parse out and verify the the pieces of the X-NewRelic-Synthetics-Info header.
*
* @param {string} header - The raw X-NewRelic-Synthetics-Info header
* @param {string} encKey - Encoding key handed down from the server
* @returns {object | null} - On successful parse and verification an object of
* synthetics info data is returned, otherwise null is
* returned.
*/
function parseSyntheticsInfoHeader(header, encKey) {
let synthInfoData = null
try {
synthInfoData = JSON.parse(hashes.deobfuscateNameUsingKey(header, encKey))
logger.trace('Parsed synthetics info header: %s', synthInfoData)
} catch (e) {
logger.trace(e, 'Cannot parse synthetics info header: %s', header)
return
}
if (!isNotEmpty(synthInfoData)) {
logger.trace('Synthetics info data is not an object.')
return
}
const { version } = synthInfoData
if (version !== 1) {
logger.trace('Synthetics info header version is not 1, got: %s', version)
return
}
return synthInfoData
}
/**
* Parse out and verify the the pieces of the X-NewRelic-Synthetics header.
*
* @param {string} header - The raw X-NewRelic-Synthetics header
* @param {string} encKey - Encoding key handed down from the server
* @param {Array.<number>} trustedIds - Array of accounts to trust the header from.
* @returns {object | null} - On successful parse and verification an object of
* synthetics data is returned, otherwise null is
* returned.
*/
function parseSyntheticsHeader(header, encKey, trustedIds) {
let synthData = null
try {
synthData = JSON.parse(hashes.deobfuscateNameUsingKey(header, encKey))
logger.trace('Parsed synthetics header: %s', synthData)
} catch (e) {
logger.trace(e, 'Cannot parse synthetics header: %s', header)
return
}
if (!Array.isArray(synthData)) {
logger.trace('Synthetics data is not an array.')
return
}
if (synthData.length < KEYS.length) {
logger.trace(
'Synthetics header length is %s, expected at least %s',
synthData.length,
KEYS.length
)
}
const [version, accountId, resourceId, jobId, monitorId] = synthData
if (version !== 1) {
logger.trace('Synthetics header version is not 1, got: %s', version)
return
}
if (accountId && !trustedIds.includes(accountId)) {
logger.trace(
'Synthetics header account ID is not in trusted account IDs: %s (%s)',
accountId,
trustedIds.toString()
)
return
}
return {
version,
accountId,
resourceId,
jobId,
monitorId
}
}
/**
* Helper method for adding relevant synthetics intrinsics to transaction traces
*
* @param {object} transaction The current transaction
*/
synthetics.assignIntrinsicsToTransaction = function assignIntrinsicsToTransaction(transaction) {
if (transaction.syntheticsData) {
transaction._intrinsicAttributes.synthetics_resource_id = transaction.syntheticsData?.resourceId
transaction._intrinsicAttributes.synthetics_job_id = transaction.syntheticsData?.jobId
transaction._intrinsicAttributes.synthetics_monitor_id = transaction.syntheticsData?.monitorId
if (transaction.syntheticsInfoData) {
transaction._intrinsicAttributes.synthetics_type = transaction.syntheticsInfoData?.type
transaction._intrinsicAttributes.synthetics_initiator =
transaction.syntheticsInfoData?.initiator
for (const [key, value] of Object.entries(transaction.syntheticsInfoData.attributes)) {
transaction._intrinsicAttributes[`synthetics_${toSnakeCase(key)}`] = value
}
}
}
}
/**
* Helper method for modifying attributes by reference if transaction has Synthetics metrics
*
* @param {object} transaction The current transaction
* @param {object} attributes The attributes object to modify (by reference)
*/
synthetics.assignTransactionAttrs = function assignTransactionAttrs(transaction, attributes) {
if (transaction.syntheticsData) {
attributes['nr.syntheticsResourceId'] = transaction.syntheticsData?.resourceId
attributes['nr.syntheticsJobId'] = transaction.syntheticsData?.jobId
attributes['nr.syntheticsMonitorId'] = transaction.syntheticsData?.monitorId
if (transaction.syntheticsInfoData) {
attributes['nr.syntheticsType'] = transaction.syntheticsInfoData?.type
attributes['nr.syntheticsInitiator'] = transaction.syntheticsInfoData?.initiator
for (const [key, value] of Object.entries(transaction.syntheticsInfoData.attributes)) {
const attr = toCamelCase(`synthetics_${key}`)
attributes[`nr.${attr}`] = value
}
}
}
}
/**
* Helper method for assign the X-NewRelic-Synthetics and X-NewRelic-Synthetics-Info headers to outbound http requests
*
* @param {object} config agent config
* @param {object} transaction The current transaction
* @param {object} headers outgoing headers
*/
synthetics.assignHeadersToOutgoingRequest = function addHeadersToOutgoingRequest(
config,
transaction,
headers
) {
if (config.encoding_key && transaction.syntheticsHeader) {
headers[NEWRELIC_SYNTHETICS_HEADER] = transaction.syntheticsHeader
if (transaction.syntheticsInfoHeader) {
headers[NEWRELIC_SYNTHETICS_INFO_HEADER] = transaction.syntheticsInfoHeader
}
}
}
/**
* Helper method for assigning the X-NewRelic-Synthetics and X-NewRelic-Synthetics-Info headers to the response
*
* @param {object} response http response object
* @param {object} transaction The current transaction
*/
synthetics.assignHeadersToResponse = function assignHeadersToResponse(response, transaction) {
if (transaction.syntheticsHeader) {
response.setHeader(NEWRELIC_SYNTHETICS_HEADER, transaction.syntheticsHeader)
if (transaction.syntheticsInfoHeader) {
response.setHeader(NEWRELIC_SYNTHETICS_INFO_HEADER, transaction.syntheticsInfoHeader)
}
}
}