newrelic
Version:
New Relic agent
728 lines (634 loc) • 23.6 kB
JavaScript
'use strict'
const shimmer = require('../../shimmer')
const logger = require('../../logger').child({component: 'http'})
const recordWeb = require('../../metrics/recorders/http')
const hashes = require('../../util/hashes')
const cat = require('../../util/cat')
const instrumentOutbound = require('./http-outbound')
const util = require('util')
const url = require('url')
const urltils = require('../../util/urltils')
const properties = require('../../util/properties')
const psemver = require('../../util/process-version')
const headerAttributes = require('../../header-attributes')
const NAMES = require('../../metrics/names')
const DESTS = require('../../config/attribute-filter').DESTINATIONS
/*
*
* CONSTANTS
*
*/
const SHOULD_WRAP_HTTPS = psemver.satisfies('>=9.0.0 || 8.9.0')
const NR_CONNECTION_PROP = '__NR__connection'
const REQUEST_HEADER = 'x-request-start'
const QUEUE_HEADER = 'x-queue-start'
const NEWRELIC_ID_HEADER = 'x-newrelic-id'
const NEWRELIC_APP_DATA_HEADER = 'x-newrelic-app-data'
const NEWRELIC_TRACE_HEADER = 'newrelic'
const NEWRELIC_TRANSACTION_HEADER = 'x-newrelic-transaction'
const NEWRELIC_SYNTHETICS_HEADER = 'x-newrelic-synthetics'
const CONTENT_LENGTH_REGEX = /^Content-Length$/i
const TRANSACTION_INFO_KEY = '__NR_transactionInfo'
// For incoming requests this instrumentation functions by wrapping
// `http.createServer` and `http.Server#addListener`. The former merely sets the
// agent dispatcher to 'http' and the latter wraps any event handlers bound to
// `request`.
//
// The `request` event listener wrapper creates a transaction proxy which will
// start a new transaction whenever a new request comes in. It also scans the
// headers of the incoming request looking for CAT and synthetics headers.
function wrapEmitWithTransaction(agent, emit, isHTTPS) {
const tracer = agent.tracer
const transport = isHTTPS ? 'HTTPS' : 'HTTP'
let serverPort = null
return tracer.transactionProxy(function wrappedHandler(evnt, request, response) {
var transaction = tracer.getTransaction()
if (!transaction) return emit.apply(this, arguments)
transaction.nameState.setPrefix(NAMES.NODEJS.PREFIX)
transaction.nameState.setDelimiter(NAMES.ACTION_DELIMITER)
// Store the transaction information on the request and response.
storeTxInfo(transaction, request, response)
// Hook for web framework instrumentations that don't have easy access to
// the request entry point.
if (properties.hasOwn(this, '__NR_onRequestStarted')) {
this.__NR_onRequestStarted(request, response)
}
if (request) {
initializeRequest(transaction, request)
}
// Create the transaction segment using the request URL for now. Once a
// better name can be determined this segment will be renamed to that.
var segment = tracer.createSegment(request.url, recordWeb)
segment.start()
if (agent.config.feature_flag.custom_instrumentation) {
transaction.type = 'web'
transaction.baseSegment = segment
}
/* Needed for Connect and Express middleware that monkeypatch request
* and response via listeners.
*/
tracer.bindEmitter(request, segment)
tracer.bindEmitter(response, segment)
// the error tracer needs a URL for tracing, even though naming overwrites
transaction.parsedUrl = url.parse(request.url, true)
transaction.url = transaction.parsedUrl.pathname
transaction.verb = request.method
// URL is sent as an agent attribute with transaction events
transaction.trace.addAttribute(
DESTS.TRANS_EVENT | DESTS.ERROR_EVENT,
'request.uri',
transaction.url
)
// store the port on which this transaction runs
if (this.address instanceof Function) {
var address = this.address()
if (address) {
serverPort = address.port
}
}
transaction.port = serverPort
// need to set any config-driven names early for RUM
logger.trace({url: request.url, transaction: transaction.id},
'Applying user naming rules for RUM.')
transaction.applyUserNamingRules(request.url)
/**
* Calculate Queue Time
*
* Queue time is provided by certain providers by stamping the request
* header with the time the request arrived at the router.
*
* Units for queue time are
*/
var qtime = request.headers[REQUEST_HEADER] || request.headers[QUEUE_HEADER]
if (qtime) {
var split = qtime.split('=')
if (split.length > 1) {
qtime = split[1]
}
var start = parseFloat(qtime)
if (isNaN(start)) {
logger.warn('Queue time header parsed as NaN (%s)', qtime)
} else {
// nano seconds
if (start > 1e18) start = start / 1e6
// micro seconds
else if (start > 1e15) start = start / 1e3
// seconds
else if (start < 1e12) start = start * 1e3
transaction.queueTime = Date.now() - start
}
}
if (agent.config.distributed_tracing.enabled) {
const payload = request.headers[NEWRELIC_TRACE_HEADER]
if (payload) {
logger.trace(
'Accepting distributed trace payload for transaction %s',
transaction.id
)
transaction.acceptDistributedTracePayload(payload, transport)
}
} else if (agent.config.cross_application_tracer.enabled) {
var encKey = agent.config.encoding_key
var incomingCatId = request.headers[NEWRELIC_ID_HEADER]
var obfTransaction = request.headers[NEWRELIC_TRANSACTION_HEADER]
var synthHeader = request.headers[NEWRELIC_SYNTHETICS_HEADER]
if (encKey) {
cat.handleCatHeaders(incomingCatId, obfTransaction, encKey, transaction)
if (transaction.incomingCatId) {
logger.trace('Got inbound request CAT headers in transaction %s',
transaction.id)
}
if (synthHeader && agent.config.trusted_account_ids) {
handleSyntheticsHeader(
synthHeader,
encKey,
agent.config.trusted_account_ids,
transaction
)
}
}
}
function instrumentedFinish() {
// Remove listeners so this doesn't get called twice.
response.removeListener('finish', instrumentedFinish)
request.removeListener('aborted', instrumentedFinish)
// Naming must happen before the segment and transaction are ended,
// because metrics recording depends on naming's side effects.
transaction.finalizeNameFromUri(transaction.parsedUrl, response.statusCode)
if (response) {
if (response.statusCode != null) {
var responseCode = String(response.statusCode)
transaction.trace.addAttribute(DESTS.COMMON, 'httpResponseCode', responseCode)
if (/^\d+$/.test(responseCode)) {
transaction.trace.addAttribute(DESTS.COMMON, 'response.status', responseCode)
}
}
if (response.statusMessage !== undefined) {
transaction.trace.addAttribute(
DESTS.COMMON,
'httpResponseMessage',
response.statusMessage
)
}
// TODO: Just use `getHeaders()` after deprecating Node <7.7.
var headers = (response.getHeaders && response.getHeaders()) || response._headers
if (headers) {
headerAttributes.collectResponseHeaders(headers, transaction)
}
}
// And we are done! End the segment and transaction.
segment.end()
transaction.end()
}
response.once('finish', instrumentedFinish)
request.once('aborted', instrumentedFinish)
return tracer.bindFunction(emit, segment).apply(this, arguments)
})
}
function storeTxInfo(transaction, request, response) {
if (!request || !response) {
logger.debug('Missing request or response object! Not storing transaction info.')
return
}
var hideInternal = transaction.agent.config.transaction_tracer.hide_internals
var txInfo = {
transaction: transaction,
segmentStack: [],
errorHandled: false,
error: null
}
if (hideInternal) {
properties.setInternal(request, TRANSACTION_INFO_KEY, txInfo)
properties.setInternal(response, TRANSACTION_INFO_KEY, txInfo)
} else {
request[TRANSACTION_INFO_KEY] = response[TRANSACTION_INFO_KEY] = txInfo
}
logger.trace(
'Stored transaction %s information on request and response',
transaction.id
)
}
function initializeRequest(transaction, request) {
headerAttributes.collectRequestHeaders(request.headers, transaction)
if (request.method != null) {
transaction.trace.addAttribute(DESTS.COMMON, 'request.method', request.method)
transaction.nameState.setVerb(request.method)
}
}
function wrapResponseEnd(agent, proto) {
var tracer = agent.tracer
// On end, we must freeze the current name state to maintain the route that
// responded and also end the current segment (otherwise it may become truncated).
shimmer.wrapMethod(proto, 'Response.prototype', 'end', function wrapResEnd(end) {
if (typeof end !== 'function') {
logger.debug('Response#end is not a function?')
return end
}
return function wrappedResEnd() {
var txInfo = this && this[TRANSACTION_INFO_KEY]
if (!txInfo) {
return end.apply(this, arguments)
}
// If an error happend, add it to the aggregator.
if (txInfo.error) {
if (!txInfo.errorHandled || urltils.isError(agent.config, this.statusCode)) {
agent.errors.add(txInfo.transaction, txInfo.error)
}
}
// End all the segments leading up to and including this one.
for (var i = txInfo.segmentStack.length - 1; i >= 0; --i) {
txInfo.segmentStack[i].end()
}
var segment = tracer.getSegment()
if (segment) {
segment.end()
}
// Freeze the name state to prevent further changes.
txInfo.transaction.nameState.freeze()
return end.apply(this, arguments)
}
})
}
// CAT this wont be used unless CAT is enabled, see below where we actually do
// the shimmer stuff if you'd like to verify.
function wrapWriteHead(agent, writeHead) {
return function wrappedWriteHead() {
var transaction = agent.tracer.getTransaction()
if (!transaction) {
logger.trace('No transaction - not adding response CAT headers')
return writeHead.apply(this, arguments)
}
// FLAG: synthetics
if (agent.config.feature_flag.synthetics && transaction.syntheticsHeader) {
this.setHeader(NEWRELIC_SYNTHETICS_HEADER, transaction.syntheticsHeader)
}
if (!transaction.incomingCatId) {
logger.trace('No incoming CAT ID - not adding response CAT headers')
return writeHead.apply(this, arguments)
}
if (!agent.config.trusted_account_ids) {
logger.trace(
'No account IDs in config.trusted_account_ids - not adding response CAT headers'
)
return writeHead.apply(this, arguments)
}
var accountId = transaction.incomingCatId.split('#')[0]
accountId = parseInt(accountId, 10)
if (agent.config.trusted_account_ids.indexOf(accountId) === -1) {
logger.trace(
'Request from untrusted account id: %s - not adding response CAT headers',
accountId
)
return writeHead.apply(this, arguments)
}
// Not sure this could ever happen, but should guard against it anyway
// otherwise exception we blow up the user's app.
if (!agent.config.cross_process_id || !agent.config.encoding_key) {
logger.trace(
'Managed to have %s but not cross_process_id (%s) or encoding_key (%s) - %s',
'agent.config.trusted_account_ids',
agent.config.cross_process_id,
agent.config.encoding_key,
'not adding response CAT headers'
)
return writeHead.apply(this, arguments)
}
// -1 means no content length header was sent. We should only send this
// value in the appData if the header is set.
var contentLength = -1
var new_headers = arguments[arguments.length - 1]
if (typeof new_headers === 'object') {
for (var header in new_headers) { // jshint ignore: line
if (CONTENT_LENGTH_REGEX.test(header)) {
contentLength = new_headers[header]
break
}
}
}
if (contentLength === -1 && this._headers) {
// JSHint complains about ownProperty stuff, but since we are looking
// for a specific name that doesn't matter so I'm disabling it.
// Outbound headers can be capitalized in any way, use regex instead
// of direct lookup.
for (var userHeader in this._headers) { // jshint ignore: line
if (CONTENT_LENGTH_REGEX.test(userHeader)) {
contentLength = this._headers[userHeader]
break
}
}
}
// Stored on the tx so we can push a metric with this time instead of
// actual duration.
transaction.catResponseTime = transaction.timer.getDurationInMillis()
var appData = null
var txName = transaction.getFullName() || ''
try {
appData = JSON.stringify([
agent.config.cross_process_id, // cross_process_id
txName, // 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',
txName
)
return writeHead.apply(this, arguments)
}
var encKey = agent.config.encoding_key
var obfAppData = hashes.obfuscateNameUsingKey(appData, encKey)
this.setHeader(NEWRELIC_APP_DATA_HEADER, obfAppData)
logger.trace('Added outbound response CAT headers in transaction %s', transaction.id)
return writeHead.apply(this, arguments)
}
}
function wrapRequest(agent, request) {
return function wrappedRequest(options) {
// Don't pollute metrics and calls with NR connections
const internalOnly = options && options[NR_CONNECTION_PROP]
if (internalOnly) {
delete options[NR_CONNECTION_PROP]
}
// If this is not a request we're recording, exit early.
const transaction = agent.tracer.getTransaction()
if (!transaction || internalOnly) {
if (!internalOnly && logger.traceEnabled()) {
const logOpts = typeof options === 'string' ? url.parse(options) : options
logger.trace(
'No transaction, not recording external to %s:%s',
logOpts.hostname || logOpts.host,
logOpts.port
)
}
return request.apply(this, arguments)
}
const args = agent.tracer.slice(arguments)
const context = this
// hostname & port logic pulled directly from node's 0.10 lib/http.js
return instrumentOutbound(agent, options, function makeRequest(opts) {
args[0] = opts
return request.apply(context, args)
})
}
}
function wrapLegacyRequest(agent, request) {
return function wrappedLegacyRequest(method, path, headers) {
var makeRequest = request.bind(this, method, path, headers)
if (agent.tracer.getTransaction()) {
return instrumentOutbound(agent, this, makeRequest)
}
logger.trace('No transaction, not recording external to %s:%s', this.host, this.port)
return makeRequest()
}
}
function wrapLegacyClient(agent, proto) {
shimmer.wrapMethod(
proto,
'http.Client.prototype',
'request',
wrapLegacyRequest.bind(null, agent)
)
}
module.exports = function initialize(agent, http, moduleName) {
if (!http) {
logger.debug('Did not get http module, not instrumenting!')
return false
}
const IS_HTTPS = moduleName === 'https'
// FIXME: will this ever not be called?
shimmer.wrapMethod(http, 'http', 'createServer', function cb_wrapMethod(createServer) {
return function setDispatcher(requestListener) { // eslint-disable-line no-unused-vars
agent.environment.setDispatcher('http')
return createServer.apply(this, arguments)
}
})
// It's not a great idea to monkeypatch EventEmitter methods given how hot
// they are, but this method is simple and works with all versions of node
// supported by the module.
shimmer.wrapMethod(
http.Server && http.Server.prototype,
'http.Server.prototype',
'emit',
function wrapEmit(emit) {
var txStarter = wrapEmitWithTransaction(agent, emit, IS_HTTPS)
return function wrappedEmit(evnt) {
if (evnt === 'request') {
return txStarter.apply(this, arguments)
}
return emit.apply(this, arguments)
}
}
)
wrapResponseEnd(agent, http.ServerResponse && http.ServerResponse.prototype)
// If CAT is enabled we'll wrap `writeHead` to inject our headers.
if (agent.config.cross_application_tracer.enabled) {
shimmer.wrapMethod(
http.ServerResponse && http.ServerResponse.prototype,
'http.ServerResponse.prototype',
'writeHead',
wrapWriteHead.bind(null, agent)
)
}
var agentProto = http && http.Agent && http.Agent.prototype
// As of node 0.8, http.request() is the right way to originate outbound
// requests. From 0.11 until 9, the `https` module simply called through to
// the `http` methods, so to prevent double-instrumenting we need to check
// what module we're instrumenting and what version of Node we're on. This
// change originally also appeared in 8.9.0 but was reverted in 8.9.1.
//
// TODO: Remove `SHOULD_WRAP_HTTPS` after deprecating Node <9.
if (SHOULD_WRAP_HTTPS || !IS_HTTPS) {
shimmer.wrapMethod(
http,
'http',
'request',
wrapRequest.bind(null, agent)
)
// Upon updating to https-proxy-agent v2, we are now using
// agent-base v4, which patches https.get to use https.request.
//
// https://github.com/TooTallNate/node-agent-base/commit/7de92df780e147d4618
//
// This means we need to skip instrumenting to avoid double
// instrumenting external requests.
if (!IS_HTTPS && psemver.satisfies('>=8')) {
shimmer.wrapMethod(
http,
'http',
'get',
wrapRequest.bind(null, agent)
)
}
}
shimmer.wrapMethod(
agentProto,
'http.Agent.prototype',
'createConnection',
function wrapCreateConnection(original) {
return function wrappedCreateConnection() {
if (!agent.getTransaction()) {
return original.apply(this, arguments)
}
var segment = agent.tracer.createSegment('http.Agent#createConnection')
var args = agent.tracer.slice(arguments)
if (typeof args[1] === 'function') {
args[1] = agent.tracer.bindFunction(args[1], segment, true)
}
var sock = agent.tracer.bindFunction(original, segment, true).apply(this, args)
return sock
}
}
)
// http.Client is deprecated, but still in use
// TODO: Remove this once Node <7 is deprecated.
var DeprecatedClient, deprecatedCreateClient
function clearGetters() {
if (DeprecatedClient) {
delete http.Client
http.Client = DeprecatedClient
}
if (deprecatedCreateClient) {
delete http.createClient
http.createClient = deprecatedCreateClient
}
}
// TODO: Remove this once Node <7 is deprecated.
DeprecatedClient = shimmer.wrapDeprecated(
http,
'http',
'Client',
{
get: function get() {
var example = new DeprecatedClient(80, 'localhost')
wrapLegacyClient(agent, example.constructor.prototype)
clearGetters()
return DeprecatedClient
},
set: function set(NewClient) {
DeprecatedClient = NewClient
}
}
)
// TODO: Remove this once Node <7 is deprecated.
deprecatedCreateClient = shimmer.wrapDeprecated(
http,
'http',
'createClient',
{
get: function get() {
var example = deprecatedCreateClient(80, 'localhost')
wrapLegacyClient(agent, example.constructor.prototype)
clearGetters()
return deprecatedCreateClient
},
set: function set(newCreateClient) {
deprecatedCreateClient = newCreateClient
}
}
)
}
/**
* 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 {Transaction} transaction - Where the synthetics data is attached to.
*/
function handleSyntheticsHeader(header, encKey, trustedIds, transaction) {
var synthData = parseSyntheticsHeader(header, encKey, trustedIds)
if (!synthData) {
return
}
transaction.syntheticsData = synthData
transaction.syntheticsHeader = header
}
/**
* 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.
* @return {Object|null} - On successful parse and verification an object of
* synthetics data is returned, otherwise null is
* returned.
*/
function parseSyntheticsHeader(header, encKey, trustedIds) {
// Eagerly declare this object because we know what it should look like and
// can use that for header verification.
var parsedData = {
version: null,
accountId: null,
resourceId: null,
jobId: null,
monitorId: null
}
var synthData = null
try {
synthData = JSON.parse(
hashes.deobfuscateNameUsingKey(header, encKey)
)
} catch (e) {
logger.trace(e, 'Got unparsable synthetics header: %s', header)
return
}
if (!util.isArray(synthData)) {
logger.trace(
'Synthetics data is not an array: %s (%s)',
synthData,
typeof synthData
)
return
}
if (synthData.length < Object.keys(parsedData).length) {
logger.trace(
'Synthetics header length is %s, expected at least %s',
synthData.length,
Object.keys(parsedData).length
)
}
parsedData.version = synthData[0]
if (parsedData.version !== 1) {
logger.trace(
'Synthetics header version is not 1, got: %s (%s)',
parsedData.version,
synthData
)
return
}
parsedData.accountId = synthData[1]
if (parsedData.accountId) {
if (trustedIds.indexOf(parsedData.accountId) === -1) {
logger.trace(
'Synthetics header account ID is not in trusted account IDs: %s (%s)',
parsedData.accountId,
trustedIds
)
return
}
} else {
logger.trace('Synthetics header account ID missing.')
return
}
parsedData.resourceId = synthData[2]
if (!parsedData.resourceId) {
logger.trace('Synthetics resource ID is missing.')
return
}
parsedData.jobId = synthData[3]
if (!parsedData.jobId) {
logger.trace('Synthetics job ID is missing.')
}
parsedData.monitorId = synthData[4]
if (!parsedData.monitorId) {
logger.trace('Synthetics monitor ID is missing.')
}
return parsedData
}