newrelic
Version:
New Relic agent
306 lines (269 loc) • 9.41 kB
JavaScript
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
const cat = module.exports
const hashes = require('./hashes')
const logger = require('../logger').child({ component: 'cat' })
const NAMES = require('../metrics/names')
const HTTP_CAT_ID_HEADER = 'X-NewRelic-Id'
const MQ_CAT_ID_HEADER = 'NewRelicID'
const MATCH_CAT_ID_HEADER = new RegExp(
'^(?:' + HTTP_CAT_ID_HEADER + '|' + MQ_CAT_ID_HEADER + ')$',
'i'
)
const HTTP_CAT_TRANSACTION_HEADER = 'X-NewRelic-Transaction'
const MQ_CAT_TRANSACTION_HEADER = 'NewRelicTransaction'
const MATCH_CAT_TRANSACTION_HEADER = new RegExp(
'^(?:' + HTTP_CAT_TRANSACTION_HEADER + '|' + MQ_CAT_TRANSACTION_HEADER + ')$',
'i'
)
const HTTP_CAT_APP_DATA_HEADER = 'X-NewRelic-App-Data'
const MQ_CAT_APP_DATA_HEADER = 'NewRelicAppData'
const MATCH_CAT_APP_DATA_HEADER = new RegExp(
'^(?:' + HTTP_CAT_APP_DATA_HEADER + '|' + MQ_CAT_APP_DATA_HEADER + ')$',
'i'
)
/**
* Decodes the CAT id and transaction headers from incoming request
*
* @param {object} id CAT id
* @param {object} transactionId transaction header id
* @param {string} encKey config.encoding_key used to decode CAT headers
*
* @returns {object} { externalId, externalTransaction }
*/
cat.parseCatData = function parseCatData(id, transactionId, encKey) {
if (!encKey) {
logger.warn('Missing encoding key, not extract CAT headers!')
return {}
}
let externalId = null
if (id) {
externalId = hashes.deobfuscateNameUsingKey(id, encKey)
}
let externalTransaction = null
if (transactionId) {
try {
externalTransaction = JSON.parse(hashes.deobfuscateNameUsingKey(transactionId, encKey))
} catch {
logger.trace(`Got an unparsable CAT header ${HTTP_CAT_ID_HEADER} %s`, transactionId)
}
}
return { externalId, externalTransaction }
}
/**
* Adds the appropriate keys to transaction based on the incoming parsed CAT data
*
* @param {string} externalId decoded CAT id
* @param {Array} externalTransaction CAT transaction
* @param {Transaction} transaction active transaction
*/
cat.assignCatToTransaction = function assignCatToTransaction(
externalId,
externalTransaction,
transaction
) {
if (typeof externalId === 'string') {
transaction.incomingCatId = externalId
}
if (Array.isArray(externalTransaction)) {
const [referringGuid, , tripId, referringPathHash] = externalTransaction
transaction.referringTransactionGuid = referringGuid
if (typeof tripId === 'string') {
transaction.tripId = tripId
} else if (tripId) {
transaction.invalidIncomingExternalTransaction = true
}
if (typeof referringPathHash === 'string') {
transaction.referringPathHash = referringPathHash
} else if (referringPathHash) {
transaction.invalidIncomingExternalTransaction = true
}
}
if (transaction.incomingCatId) {
logger.trace(
'Got inbound CAT headers in transaction %s from %s',
transaction.id,
transaction.incomingCatId
)
}
}
/**
* Encodes the data to be set on the CAT app data header
* for incoming requests
*
* @param {object} config agent config
* @param {Transaction} transaction current transaction
* @param {string} contentLength length of content
* @param {boolean} useMqHeaders flag to return proper headers for MQ compliance
* @returns {object|undefined} { key, data } to add as header, or undefined if an error occurred
*/
cat.encodeAppData = function encodeAppData(config, transaction, contentLength, useMqHeaders) {
let appData = null
const transactionName = transaction.getFullName() || ''
try {
appData = JSON.stringify([
config.cross_process_id, // cross_process_id
transactionName, // transaction name
transaction.queueTime / 1000, // queue time (s)
transaction.catResponseTime / 1000, // response time (s)
contentLength, // content length (if content-length header is also being sent)
transaction.id, // TransactionGuid
false // force a transaction trace to be recorded
])
} catch (err) {
logger.trace(
err,
'Failed to serialize transaction: %s - not adding CAT response headers',
transactionName
)
return
}
const encKey = config.encoding_key
const obfAppData = hashes.obfuscateNameUsingKey(appData, encKey)
const key = useMqHeaders ? MQ_CAT_APP_DATA_HEADER : HTTP_CAT_APP_DATA_HEADER
return { key, data: obfAppData }
}
/**
* Adds CAT headers to outbound request.
*
* @param {object} config agent config
* @param {Transaction} transaction current transaction
* @param {object} headers object that contains headers the agent is adding to client request
* @param {boolean} useMqHeaders flag to return proper headers for MQ compliance
*/
cat.addCatHeaders = function addCatHeaders(config, transaction, headers, useMqHeaders) {
if (!config.encoding_key) {
logger.warn('Missing encoding key, not adding CAT headers!')
return
}
const idHeader = useMqHeaders ? MQ_CAT_ID_HEADER : HTTP_CAT_ID_HEADER
const transactionHeader = useMqHeaders ? MQ_CAT_TRANSACTION_HEADER : HTTP_CAT_TRANSACTION_HEADER
// Add in the application ID
if (config.obfuscatedId) {
headers[idHeader] = config.obfuscatedId
}
const transactionName = transaction.getFullName() || ''
const pathHash = hashes.calculatePathHash(
config.applications()[0],
transactionName,
transaction.referringPathHash
)
transaction.pushPathHash(pathHash)
try {
const transactionData = hashes.obfuscateNameUsingKey(
JSON.stringify([transaction.id, false, transaction.tripId || transaction.id, pathHash]),
config.encoding_key
)
headers[transactionHeader] = transactionData
logger.trace('Added outbound request CAT headers in transaction %s', transaction.id)
} catch (err) {
logger.trace(err, 'Failed to create CAT payload')
}
}
/**
* Find the CAT id, transaction, app data headers
* from the headers of either HTTP or MQ request.
*
* @param {object} headers HTTP/MQ headers
* @returns {object} { id, transactionId, appData }
*/
cat.extractCatHeaders = function extractCatHeaders(headers) {
// Hunt down the CAT headers.
let id = null
let transactionId = null
let appData = null
for (const key in headers) {
if (MATCH_CAT_ID_HEADER.test(key)) {
id = headers[key]
} else if (MATCH_CAT_TRANSACTION_HEADER.test(key)) {
transactionId = headers[key]
} else if (MATCH_CAT_APP_DATA_HEADER.test(key)) {
appData = headers[key]
}
if (id && transactionId && appData) {
break
}
}
return { id, transactionId, appData }
}
/**
* Extracts the account Id from CAT data and verifies if it is
* a trusted account id
*
* @param {object} data CAT data
* @param {Array} trustedAccounts from config
* @returns {boolean}
*/
cat.isTrustedAccountId = function isTrustedAccountId(data, trustedAccounts) {
const accountId = parseInt(data.split('#')[0], 10)
const trusted = trustedAccounts.includes(accountId)
if (!trusted) {
logger.trace('Response from untrusted CAT header account id: %s', accountId)
}
return trusted
}
/**
* Decodes the CAT App Data header and extracts the downstream
* CAT id, transaction id
*
* @param {object} config agent config
* @param {string} obfAppData encoded app data to parse and use
*
* @returns {Array|undefined} decoded app data header
*/
cat.parseAppData = function parseAppData(config, obfAppData) {
if (!config.encoding_key) {
logger.trace('config.encoding_key is not set - not parsing response CAT headers')
return
}
if (!config.trusted_account_ids) {
logger.trace('config.trusted_account_ids is not set - not parsing response CAT headers')
return
}
let appData = null
try {
appData = JSON.parse(hashes.deobfuscateNameUsingKey(obfAppData, config.encoding_key))
} catch {
logger.warn(`Got an unparsable CAT header ${HTTP_CAT_APP_DATA_HEADER}: %s`, obfAppData)
return
}
// Make sure it is a trusted account
if (!cat.isTrustedAccountId(appData && appData[0], config.trusted_account_ids)) {
return
}
return appData
}
/**
* Assigns the CAT id, transaction to segment and adds `transaction_guid` when it exists.
* It also renames the segment name based on the newly decoded app data when host is present
*
* @param {object} params to function
* @param {Array} params.appData decodes CAT app data
* @param {TraceSegment} params.segment segment to assign CAT data
* @param {string} [params.host] if host is present it will rename segment with app data and host
* @param {Transaction} params.transaction active transaction
*/
cat.assignCatToSegment = function assignCatToSegment({ appData, segment, host, transaction }) {
if (!Array.isArray(appData) || typeof appData[0] !== 'string') {
logger.trace(`Unknown format for CAT header ${HTTP_CAT_APP_DATA_HEADER}.`)
return
}
segment.catId = appData[0]
segment.catTransaction = appData[1]
if (host) {
segment.name = `${NAMES.EXTERNAL.TRANSACTION}${host}/${segment.catId}/${segment.catTransaction}`
}
let transactionGuid
if (appData.length >= 6) {
transactionGuid = appData[5]
segment.addAttribute('transaction_guid', transactionGuid)
}
logger.trace(
'Got inbound response CAT headers in transaction %s from %s',
transaction.id,
transactionGuid
)
}