diff --git a/lib/instrumentation/core/http.js b/lib/instrumentation/core/http.js index e7d33c60fe..549f87714c 100644 --- a/lib/instrumentation/core/http.js +++ b/lib/instrumentation/core/http.js @@ -5,8 +5,6 @@ 'use strict' -/* eslint sonarjs/cognitive-complexity: ["error", 42] -- TODO: https://issues.newrelic.com/browse/NEWRELIC-5252 */ - const shimmer = require('../../shimmer') const logger = require('../../logger').child({ component: 'http' }) const recordWeb = require('../../metrics/recorders/http') @@ -35,8 +33,6 @@ const symbols = require('../../symbols') 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) { const transaction = tracer.getTransaction() if (!transaction) { @@ -91,15 +87,7 @@ function wrapEmitWithTransaction(agent, emit, isHTTPS) { segment.addSpanAttribute('request.uri', transaction.url) - // store the port on which this transaction runs - if (this.address instanceof Function) { - const address = this.address() - if (address) { - serverPort = address.port - } - } - transaction.port = serverPort - + transaction.port = parsePort(this) // need to set any config-driven names early for RUM logger.trace( { url: request.url, transaction: transaction.id }, @@ -114,68 +102,102 @@ function wrapEmitWithTransaction(agent, emit, isHTTPS) { synthetics.assignHeadersToTransaction(agent.config, transaction, request.headers) - if (agent.config.distributed_tracing.enabled) { - // Node http headers are automatically lowercase - transaction.acceptDistributedTraceHeaders(transport, request.headers) - } else if (agent.config.cross_application_tracer.enabled) { - const { id, transactionId } = cat.extractCatHeaders(request.headers) - const { externalId, externalTransaction } = cat.parseCatData( - id, - transactionId, - agent.config.encoding_key - ) - cat.assignCatToTransaction(externalId, externalTransaction, transaction) - } + maybeAddDtCatHeaders({ transaction, request, transport, agent }) - function instrumentedFinish() { - // Remove listeners so this doesn't get called twice. - response.removeListener('finish', instrumentedFinish) - response.removeListener('close', instrumentedFinish) + response.once('finish', instrumentedFinish.bind(response, segment, transaction)) + response.once('close', instrumentedFinish.bind(response, segment, transaction)) - // 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) + return tracer.bindFunction(emit, segment).apply(this, arguments) + }) +} - if (response) { - if (response.statusCode != null) { - const responseCode = String(response.statusCode) +/** + * Gets the port from the Server object + * + * @param {object} server http(s) server + * @returns {number|null} parsed port + */ +function parsePort(server) { + let serverPort = null + // store the port on which this transaction runs + if (server.address instanceof Function) { + const address = server.address() + if (address) { + serverPort = address.port + } + } + return serverPort +} - if (/^\d+$/.test(responseCode)) { - transaction.trace.attributes.addAttribute( - DESTS.TRANS_COMMON, - 'http.statusCode', - responseCode - ) +function maybeAddDtCatHeaders({ agent, request, transaction, transport }) { + if (agent.config.distributed_tracing.enabled) { + // Node http headers are automatically lowercase + transaction.acceptDistributedTraceHeaders(transport, request.headers) + } else if (agent.config.cross_application_tracer.enabled) { + const { id, transactionId } = cat.extractCatHeaders(request.headers) + const { externalId, externalTransaction } = cat.parseCatData( + id, + transactionId, + agent.config.encoding_key + ) + cat.assignCatToTransaction(externalId, externalTransaction, transaction) + } +} - segment.addSpanAttribute('http.statusCode', responseCode) - } - } +/** + * Adds instrumentation to response on finish/close. + * It will add `http.statusCode`, `http.statusText` + * to the transaction trace and span. + * It will also assign the response headers to the transaction + * + * @param {TraceSegment} segment active segment + * @param {Transaction} transaction active transaction + */ +function instrumentedFinish(segment, transaction) { + // Remove listeners so this doesn't get called twice. + this.removeListener('finish', instrumentedFinish) + this.removeListener('close', instrumentedFinish) + + // Naming must happen before the segment and transaction are ended, + // because metrics recording depends on naming's side effects. + transaction.finalizeNameFromUri(transaction.parsedUrl, this.statusCode) + + if (this) { + const { statusCode, statusMessage } = this + + if (statusCode != null) { + const responseCode = String(statusCode) + + if (/^\d+$/.test(responseCode)) { + transaction.trace.attributes.addAttribute( + DESTS.TRANS_COMMON, + 'http.statusCode', + responseCode + ) - if (response.statusMessage !== undefined) { - transaction.trace.attributes.addAttribute( - DESTS.TRANS_COMMON, - 'http.statusText', - response.statusMessage - ) + segment.addSpanAttribute('http.statusCode', responseCode) + } + } - segment.addSpanAttribute('http.statusText', response.statusMessage) - } + if (statusMessage !== undefined) { + transaction.trace.attributes.addAttribute( + DESTS.TRANS_COMMON, + 'http.statusText', + statusMessage + ) - const headers = response.getHeaders() - if (headers) { - headerAttributes.collectResponseHeaders(headers, transaction) - } - } + segment.addSpanAttribute('http.statusText', statusMessage) + } - // And we are done! End the segment and transaction. - segment.end() - transaction.end() + const headers = this.getHeaders() + if (headers) { + headerAttributes.collectResponseHeaders(headers, transaction) } - response.once('finish', instrumentedFinish) - response.once('close', instrumentedFinish) + } - return tracer.bindFunction(emit, segment).apply(this, arguments) - }) + // And we are done! End the segment and transaction. + segment.end() + transaction.end() } function storeTxInfo(transaction, request, response) { @@ -341,28 +363,42 @@ function urlToOptions(_url) { return options } -function wrapRequest(agent, request) { - return function wrappedRequest(input, options, cb) { - // If the first argument is a URL, merge it into the options object. - // This code is copied from Node internals. - if (typeof input === 'string') { - const urlStr = input - input = urlToOptions(new URL(urlStr)) - } else if (input.constructor && input.constructor.name === 'URL') { - input = urlToOptions(input) - } else { - cb = options - options = input - input = null - } +/** + * http.request and http.get signatures vary. This function + * will parse the options and callback + * + * @param {*} input first arg of http.request and http.get + * @param {*} options request opts of callback + * @param {Function} cb if present it is the callback + * @returns {Array} [options, cb] + */ +function parseRequest(input, options, cb) { + // If the first argument is a URL, merge it into the options object. + // This code is copied from Node internals. + if (typeof input === 'string') { + const urlStr = input + input = urlToOptions(new URL(urlStr)) + } else if (input.constructor && input.constructor.name === 'URL') { + input = urlToOptions(input) + } else { + cb = options + options = input + input = null + } - if (typeof options === 'function') { - cb = options - options = input || {} - } else { - options = Object.assign(input || {}, options) - } + if (typeof options === 'function') { + cb = options + options = input || {} + } else { + options = Object.assign(input || {}, options) + } + + return [options, cb] +} +function wrapRequest(agent, request) { + return function wrappedRequest(input, options, cb) { + ;[options, cb] = parseRequest(input, options, cb) const reqArgs = [options, cb] // Don't pollute metrics and calls with NR connections