UNPKG

newrelic

Version:
1,457 lines (1,274 loc) 45.8 kB
/* * Copyright 2020 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' const errorHelper = require('../errors/helper') const hashes = require('../util/hashes') const logger = require('../logger').child({ component: 'transaction' }) const Metrics = require('../metrics') const NAMES = require('../metrics/names') const NameState = require('./name-state') const props = require('../util/properties') const Timer = require('../timer') const Trace = require('./trace') const synthetics = require('../synthetics') const urltils = require('../util/urltils') const TraceContext = require('./tracecontext').TraceContext const Logs = require('./logs') const DT_ACCEPT_PAYLOAD_EXCEPTION_METRIC = 'DistributedTrace/AcceptPayload/Exception' const DT_ACCEPT_PAYLOAD_PARSE_EXCEPTION_METRIC = 'DistributedTrace/AcceptPayload/ParseException' const REQUEST_PARAMS_PATH = 'request.parameters.' /* * * CONSTANTS * */ const DESTS = require('../config/attribute-filter').DESTINATIONS const FROM_MILLIS = 1e-3 const TYPES = { WEB: 'web', BG: 'bg', MESSAGE: 'message' } const TYPES_SET = _makeValueSet(TYPES) const TYPE_METRICS = { web: NAMES.WEB.RESPONSE_TIME, bg: NAMES.OTHER_TRANSACTION.RESPONSE_TIME, message: NAMES.OTHER_TRANSACTION.MESSAGE } const TRANSPORT_TYPES = { AMQP: 'AMQP', HTTP: 'HTTP', HTTPS: 'HTTPS', IRONMQ: 'IronMQ', JMS: 'JMS', KAFKA: 'Kafka', OTHER: 'Other', QUEUE: 'Queue', UNKNOWN: 'Unknown' } const TRANSPORT_TYPES_SET = _makeValueSet(TRANSPORT_TYPES) const REQUIRED_DT_KEYS = ['ty', 'ac', 'ap', 'tr', 'ti'] const DTPayload = require('./dt-payload') const DTPayloadStub = DTPayload.Stub const TRACE_CONTEXT_PARENT_HEADER = 'traceparent' const TRACE_CONTEXT_STATE_HEADER = 'tracestate' const NEWRELIC_TRACE_HEADER = 'newrelic' const MULTIPLE_INSERT_MESSAGE = 'insertDistributedTraceHeaders called on headers object that already contains ' + "distributed trace data. These may be overwritten. traceparent? '%s', newrelic? '%s'." /** * Bundle together the metrics and the trace segment for a single agent * transaction. * * @param {object} agent The agent. * @param {string} traceId if present, it will use this to assign traceId of transaction. only used in otel bridge mode to ensure trace id is same as otel spans * @fires Agent#transactionStarted */ function Transaction(agent, traceId) { if (!agent) { throw new Error('every transaction must be bound to the agent') } this.agent = agent this.metrics = new Metrics(agent.config.apdex_t, agent.mapper, agent.metricNameNormalizer) ++agent.activeTransactions this.numSegments = 0 this.id = hashes.makeId(16) this.trace = new Trace(this) this.exceptions = [] this.userErrors = [] this.timer = new Timer() this.timer.begin() this._recorders = [] this._intrinsicAttributes = Object.create(null) this._partialName = null // If handledExternally is set to true the transaction will not ended // automatically, instead it should be ended by user code. this.handledExternally = false // hidden class optimization this.catResponseTime = 0 this.error = null this.forceIgnore = null this.forceName = null this.ignore = false this.incomingCatId = null this.name = null this.nameState = new NameState(null, null, null, null) this.pathHashes = [] this.queueTime = 0 this.referringPathHash = null this.referringTransactionGuid = null this.invalidIncomingExternalTransaction = false this.statusCode = null this.syntheticsHeader = null this.syntheticsInfoHeader = null this.syntheticsData = null this.syntheticsInfoData = null this.url = null this.parsedUrl = null this.verb = null this.baseSegment = null this.type = TYPES.WEB // DT fields this.parentId = null this.parentType = null this.parentApp = null this.parentAcct = null this.parentTransportType = null this.parentTransportDuration = null this._traceId = traceId || null Object.defineProperty(this, 'traceId', { get() { if (this._traceId === null) { this._traceId = hashes.makeId(32) } return this._traceId }, set(traceId) { this._traceId = traceId } }) this.parentSpanId = null this.isDistributedTrace = null this.acceptedDistributedTrace = null // Lazy evaluate the priority and sampling in case we end up accepting a payload. this.priority = null this.sampled = null this.traceContext = new TraceContext(this) this.logs = new Logs(agent) this.ignoreApdex = false agent.emit('transactionStarted', this) } Transaction.DESTINATIONS = DESTS Transaction.TYPES = TYPES Transaction.TYPES_SET = TYPES_SET Transaction.TRANSPORT_TYPES = TRANSPORT_TYPES Transaction.TRANSPORT_TYPES_SET = TRANSPORT_TYPES_SET Transaction.TRACE_CONTEXT_PARENT_HEADER = TRACE_CONTEXT_PARENT_HEADER /** * Add a clear API method for determining whether a transaction is web or * background. * * @returns {boolean} Whether this transaction has a URL. */ Transaction.prototype.isWeb = function isWeb() { return this.type === TYPES.WEB } /** * @returns {boolean} Is this transaction still alive? */ Transaction.prototype.isActive = function isActive() { return this.timer.isActive() } /** * Close out the current transaction and its associated trace. Remove any * instances of this transaction annotated onto the call stack. * * @returns {(Transaction|undefined)} this transaction, or undefined * * @fires Agent#transactionFinished */ Transaction.prototype.end = function end() { if (!this.timer.isActive()) { return } if (!this.name) { this.finalizeName(null) // Use existing partial name. } if (this.baseSegment) { this.baseSegment.touch() } this.agent.recordSupportability('Nodejs/Transactions/Segments', this.numSegments) this._calculatePriority() this.trace.end() this.timer.end() // recorders must be run before the trace is collected if (!this.ignore) { this.record() // This method currently must be called after all recorders have been fired due // to some of the recorders (namely the db recorders) adding parameters to the // segments. this.trace.generateSpanEvents() this.logs.flush(this.priority) } this.agent.emit('transactionFinished', this) // Do after emit so all post-processing can complete this._cleanUneededReferences() return this } /** * Cleans up references that will not be used later for processing such as * transaction traces. * * Errors won't be needed for later processing but can contain extra details we * don't want to hold in memory. Particularly, axios errors can result in indirect * references to promises which will prevent them from being destroyed and result * in a memory leak. This is due to the TraceSegment not getting removed from the * async-hooks segmentMap because 'destroy' never fires. */ Transaction.prototype._cleanUneededReferences = function _cleanUneededReferences() { this.userErrors = null this.exceptions = null } /** * For web transactions, this represents the time from when the request was received * to when response was sent. For background transactions, it is equal to duration * of the transaction trace (until last segment ended). * * @returns {number} timer or trace duration in milliseconds */ Transaction.prototype.getResponseTimeInMillis = function getResponseTimeInMillis() { if (this.isWeb()) { return this.timer.getDurationInMillis() } return this.trace.getDurationInMillis() } /** * Executes the user and server provided naming rules to clean up the given url. * * @private * @param {string} requestUrl - The URL to normalize. * @returns {object} The normalization results after running user and server rules. */ Transaction.prototype._runUserNamingRules = function _runUserNamingRules(requestUrl) { // 1. user normalization rules (set in configuration) const normalized = this.agent.userNormalizer.normalize(requestUrl) if (normalized.matched) { // After applying user naming rule, apply server-side sent rules to // further squash possible MGIs const serverNormalized = this.agent.urlNormalizer.normalize(normalized.value) if (serverNormalized.ignore) { normalized.ignore = true } if (serverNormalized.matched) { // NAMES.NORMALIZED is prepended by the sever rule normalizer normalized.value = serverNormalized.value } else { normalized.value = NAMES.NORMALIZED + normalized.value } } return normalized } /** * Executes the user naming rules and applies the results to the transaction. * * @param {string} requestUrl - The URL to normalize and apply to this transaction. */ Transaction.prototype.applyUserNamingRules = function applyUserNamingRules(requestUrl) { const normalized = this._runUserNamingRules(requestUrl) if (normalized.ignore) { this.ignore = normalized.ignore } if (normalized.matched) { this._partialName = normalized.value } } /** * Set's the transaction partial name. * * The partial name is everything after the `WebTransaction/` part. * * @param {string} name - The new transaction partial name to use. */ Transaction.prototype.setPartialName = function setPartialName(name) { this._partialName = name } /** * Derive the transaction partial name from the given url and status code. * * @private * @param {string} requestUrl - The URL to derive the name from. * @param {number} status - The status code of the response. * @returns {object} An object with the derived partial name in `value` and a * boolean flag in `ignore`. */ Transaction.prototype._partialNameFromUri = _partialNameFromUri function _partialNameFromUri(requestUrl, status) { const scrubbedUrl = urltils.scrub(requestUrl) // 0. If there is a name in the name-state stack, use it. let partialName = this._partialName let ignore = false if (!this.nameState.isEmpty()) { partialName = this.nameState.getFullName() } // 1. name set by the api if (this.forceName !== null) { partialName = this.forceName } // 2. user normalization rules (set in configuration) can override transaction // naming from API const userNormalized = this._runUserNamingRules(scrubbedUrl) ignore = ignore || userNormalized.ignore if (userNormalized.matched) { partialName = userNormalized.value } // 3. URL normalization rules (sent by server). // Nothing has already set a name for this transaction, so normalize and // potentially apply the URL backstop now. Only do so if no user rules matched. if (!partialName) { // avoid polluting root path when 404 const statusName = this.nameState.getStatusName(status) if (statusName) { partialName = statusName } else { const normalized = this.agent.urlNormalizer.normalize(scrubbedUrl) ignore = ignore || normalized.ignore partialName = normalized.value } } return { ignore, value: partialName } } /** * Set the forceIgnore value on the transaction. This will cause the * transaction to clean up after itself without collecting any data. * * @param {boolean} ignore The value to assign to transaction.ignore */ Transaction.prototype.setForceIgnore = function setForceIgnore(ignore) { if (ignore != null) { this.forceIgnore = ignore } else { logger.debug('Transaction#setForceIgnore called with null value') } } /** * * Gets the current ignore state for the transaction. * */ Transaction.prototype.isIgnored = function getIgnore() { return this.ignore || this.forceIgnore } /** * Derives the transaction's name from the given URL and status code. * * The transaction's name will be set after this as well as its ignored status * based on the derived name. * * @param {string} requestURL - The URL to derive the request's name and status from. * @param {number} statusCode - The response status code. */ Transaction.prototype.finalizeNameFromUri = finalizeNameFromUri function finalizeNameFromUri(requestURL, statusCode) { if (logger.traceEnabled()) { logger.trace( { requestURL, statusCode, transactionId: this.id, transactionName: this.name }, 'Setting transaction name' ) } if (!this.agent.config.url_obfuscation.enabled) { this.url = urltils.scrub(requestURL) } this.statusCode = statusCode this.name = this.getFullName() // If a namestate stack exists, copy route parameters over to the trace. if (!this.nameState.isEmpty() && this.baseSegment) { this.nameState.forEachParams(forEachRouteParams, this) } // Allow the API to explicitly set the ignored status. if (this.forceIgnore !== null) { this.ignore = this.forceIgnore } this.baseSegment && this._markAsWeb(requestURL) this._copyNameToActiveSpan(this.name) if (logger.traceEnabled()) { logger.trace( { transactionId: this.id, transactionName: this.name, ignore: this.ignore }, 'Finished setting transaction name from Uri' ) } } function forEachRouteParams(params) { for (const key in params) { if (props.hasOwn(params, key)) { this.trace.attributes.addAttribute(DESTS.NONE, key, params[key]) const segment = this.agent.tracer.getSegment() if (!segment) { logger.trace( 'Active segment not available, not adding request.parameters.route attribute for %s', key ) } else { segment.attributes.addAttribute(DESTS.NONE, key, params[key]) } } } } Transaction.prototype._copyNameToActiveSpan = function _copyNameToActiveSpan(name) { const spanContext = this.agent.tracer.getSpanContext() if (!spanContext) { logger.trace('Span context not available, not adding transaction.name attribute for %s', name) return } spanContext.addIntrinsicAttribute('transaction.name', name) } /** * Copies final base segment parameters to trace attributes before reapplying * them to the segment. * * Handles adding query parameters to `request.parameter.*` attributes * * @param {string} rawURL The URL, as it came in, for parameter extraction. */ Transaction.prototype._markAsWeb = function _markAsWeb(rawURL) { // Because we are assured we have the URL here, lets grab query params. const params = urltils.parseParameters(rawURL) for (const key in params) { if (props.hasOwn(params, key)) { this.trace.attributes.addAttribute(DESTS.NONE, REQUEST_PARAMS_PATH + key, params[key]) const segment = this.agent.tracer.getSegment() if (!segment) { logger.trace( 'Active segment not available, not adding request.parameters span attribute for %s', key ) } else { segment.attributes.addAttribute(DESTS.NONE, REQUEST_PARAMS_PATH + key, params[key]) } } } this.baseSegment.markAsWeb(this) } /** * Sets the transaction's name and determines if it will be ignored. * * @param {string} [name] * Optional. The partial name to use for the finalized transaction. If omitted * the current partial name is used. * @returns {undefined} undefined, finalizing name as a side effect */ Transaction.prototype.finalizeName = function finalizeName(name) { // If no name is given, and this is a web transaction with a url, then // finalize the name using the stored url. if (name == null && this.isWeb() && this.url) { return this.finalizeNameFromUri(this.url, this.statusCode) } // this may seem out of place but certain API methods // set the _partialName directly so use that as a fallback this._partialName = name || this._partialName name = this.getFullName() if (!name) { logger.debug('No name for transaction %s, not finalizing.', this.id) return } this.name = name if (this.forceIgnore !== null) { this.ignore = this.forceIgnore } this.baseSegment && this.baseSegment.setNameFromTransaction(this) this._copyNameToActiveSpan(this.name) if (logger.traceEnabled()) { logger.trace( { transactionId: this.id, transactionName: this.name, ignore: this.ignore }, 'Finished setting transaction name from string' ) } } /** * Gets the transaction name safely. * * Gathering the transaction name for WebTransactions is risky complicated * business. OtherTransactions (aka background) are much simpler as they are * always fully specified by the user at creation time. * * This has the potential of causing the normalizers run extra times, which can * cause extra performance overhead. Once this is refactored we can make the * caching better and eliminate this extra overhead. Be mindful of if/when this * is called. * * @returns {string} finalized name value or partial name */ Transaction.prototype.getName = function getName() { if (this.isWeb() && this.url) { const finalName = this._partialNameFromUri(this.url, this.statusCode) if (finalName.ignore) { this.ignore = true } return finalName.value } return this._partialName } Transaction.prototype.getFullName = function getFullName() { let name = null // use value from `api.setTransaction` if (this.forceName) { name = this.forceName // use value from previously finalized named } else if (this.name) { return this.name // derive name from uri in web case // or just use whatever was this._partialName } else { name = this.getName() } if (!name) { return null } this._partialName = name let fullName = TYPE_METRICS[this.type] + '/' + name const normalized = this.agent.transactionNameNormalizer.normalize(fullName) if (normalized.ignore) { this.ignore = true } fullName = normalized.value // apply transaction segment term normalizer // only to web transactions if (this.isWeb() && this.url) { fullName = this.agent.txSegmentNormalizer.normalize(fullName).value } return fullName } /** * The instrumentation associates metrics with the different kinds of trace * segments. The metrics recorders are dependent on the transaction name to * collect their scoped metrics, and so must wait for the transaction's * name to be finalized before the recording process. Segments are only * responsible for their own life cycle, so responsibility for understanding * when the transaction name has been finalized is handed off to the trace, * which for now defers running these recorders until the trace is ended. * * @param {Function} recorder The callback which records metrics. Takes a * single parameter, which is the transaction's * name. */ Transaction.prototype.addRecorder = function addRecorder(recorder) { this._recorders.push(recorder) } /** * Run the metrics recorders for this trace. If the transaction's name / * scope hasn't been set yet, the recorder will be passed an undefined name, * and should be written to handle this. */ Transaction.prototype.record = function record() { const name = this.name for (let i = 0, l = this._recorders.length; i < l; ++i) { this._recorders[i](name, this) } } /** * Measure the duration of an operation named by a metric, optionally * belonging to a scope. * * @param {string} name The name of the metric to gather. * @param {string} scope (optional) Scope to which the metric is bound. * @param {number} duration The time taken by the operation, in milliseconds. * @param {number} exclusive The time exclusively taken by an operation, and * not its children. */ Transaction.prototype.measure = function measure(name, scope, duration, exclusive) { this.metrics.measureMilliseconds(name, scope, duration, exclusive) } /** * Based on the status code and the duration of a web transaction, either * mark the transaction as frustrating, or record its time for apdex purposes. * * @param {string} name Metric name. * @param {number} duration Duration of the transaction, in milliseconds. * @param {number} keyApdexInMillis Duration sent to the metrics getOrCreateApdexMetric method, to * derive apdex from timing in milliseconds */ Transaction.prototype._setApdex = function _setApdex(name, duration, keyApdexInMillis) { if (this.ignoreApdex) { logger.warn('Ignoring the collection of apdex stats for %s as ignoreApdex is true', this.name) return } const apdexStats = this.metrics.getOrCreateApdexMetric(name, null, keyApdexInMillis) // if we have an error-like status code, and all the errors are // expected, we know the status code was caused by an expected // error, so we will not report "frustrating." Otherwise, we // don't know which error triggered the error-like status code, // and will still increment "frustrating." If this is an issue, // users can either set a status code as expected, or ignore the // specific error to avoid incrementing to frustrating if ( urltils.isError(this.agent.config, this.statusCode) && !urltils.isExpectedError(this.agent.config, this.statusCode) && !this.hasOnlyExpectedErrors() ) { apdexStats.incrementFrustrating() } else { apdexStats.recordValueInMillis(duration, keyApdexInMillis) } } /** * Store first 10 unique path hashes calculated for a transaction. * * @param {string} pathHash Path hash * @returns {undefined} */ Transaction.prototype.pushPathHash = function pushPathHash(pathHash) { if (this.pathHashes.length >= 10 || this.pathHashes.indexOf(pathHash) !== -1) { return } this.pathHashes.unshift(pathHash) } /** * Return whether transaction spawned any outbound requests. * * @returns {boolean} if there are more than zero pathHashes */ Transaction.prototype.includesOutboundRequests = function includesOutboundRequests() { return this.pathHashes.length > 0 } /** * Get unique previous path hashes for a transaction. Does not include * current path hash. * * @returns {(string|null)} Returns sorted altHashes joined by commas, or null. */ Transaction.prototype.alternatePathHashes = function alternatePathHashes() { const curHash = hashes.calculatePathHash( this.agent.config.applications()[0], this.getFullName(), this.referringPathHash ) const altHashes = this.pathHashes.slice() const curIndex = altHashes.indexOf(curHash) if (curIndex !== -1) { altHashes.splice(curIndex, 1) } return altHashes.length === 0 ? null : altHashes.sort().join(',') } /** * Add the error information to the current segment and add the segment ID as * an attribute onto the exception. * * @param {Exception} exception The exception object to be collected. * @param {Segment} segment The segment to which the exception is linked. */ Transaction.prototype._linkExceptionToSegment = _linkExceptionToSegment function _linkExceptionToSegment(exception, segment) { segment = segment || this.agent.tracer.getSegment() if (!segment) { return } const spanContext = segment.getSpanContext() if (spanContext) { // Exception attributes will be added to span unless transaction // status code has been ignored. Last error wins. const config = this.agent.config const details = exception.getErrorDetails(config) spanContext.setError(details) } // Add the span/segment ID to the exception as agent attributes exception.agentAttributes.spanId = segment.id } /** * Associate an exception with the transaction. When the transaction ends, * the exception will be collected along with the transaction details. * * @param {Exception} exception The exception object to be collected. * @param {Segment} segment The segment to which the exception is linked. */ Transaction.prototype.addException = _addException function _addException(exception, segment) { if (!this.isActive()) { logger.trace('Transaction is not active. Not capturing error: ', exception) return } this._linkExceptionToSegment(exception, segment) this.exceptions.push(exception) } /** * Associate a user error (reported using the noticeError() API) with the transaction. * When the transaction ends, the exception will be collected along with the transaction * details. * * @param {Exception} exception The exception object to be collected. */ Transaction.prototype.addUserError = _addUserError function _addUserError(exception) { if (!this.isActive()) { logger.trace('Transaction is not active. Not capturing user error: ', exception) return } this._linkExceptionToSegment(exception) this.userErrors.push(exception) } /** * @returns {boolean} true if the transaction's current status code is errored * but considered ignored via the config. */ Transaction.prototype.hasIgnoredErrorStatusCode = function _hasIgnoredErrorStatusCode() { return urltils.isIgnoredError(this.agent.config, this.statusCode) } /** * @returns {boolean} true if an error happened during the transaction or if the transaction itself is * considered to be an error. */ Transaction.prototype.hasErrors = function _hasErrors() { const isErroredTransaction = urltils.isError(this.agent.config, this.statusCode) const transactionHasExceptions = this.exceptions.length > 0 const transactionHasuserErrors = this.userErrors.length > 0 return transactionHasExceptions || transactionHasuserErrors || isErroredTransaction } /** * @returns {boolean} true if all the errors/exceptions collected so far are expected errors */ Transaction.prototype.hasOnlyExpectedErrors = function hasOnlyExpectedErrors() { if (this.exceptions.length === 0) { return false } for (let i = 0; i < this.exceptions.length; i++) { const exception = this.exceptions[i] // this exception is neither expected nor ignored const isUnexpected = !( errorHelper.isExpectedException(this, exception, this.agent.config, urltils) || errorHelper.shouldIgnoreError(this, exception.error, this.agent.config) ) if (isUnexpected) { return false } } return true } /** * @returns {object} agent intrinsic attribute for this transaction. */ Transaction.prototype.getIntrinsicAttributes = function getIntrinsicAttributes() { if (!this._intrinsicAttributes.totalTime) { const config = this.agent.config this._intrinsicAttributes.totalTime = this.trace.getTotalTimeDurationInMillis() * FROM_MILLIS if (config.distributed_tracing.enabled) { this.addDistributedTraceIntrinsics(this._intrinsicAttributes) } else if (config.cross_application_tracer.enabled) { this._intrinsicAttributes.path_hash = hashes.calculatePathHash( config.applications()[0], this.name || this._partialName, this.referringPathHash ) this._intrinsicAttributes.trip_id = this.tripId || this.id if (this.referringTransactionGuid) { this._intrinsicAttributes.referring_transaction_guid = this.referringTransactionGuid } if (this.incomingCatId) { this._intrinsicAttributes.client_cross_process_id = this.incomingCatId } } synthetics.assignIntrinsicsToTransaction(this) } return Object.assign(Object.create(null), this._intrinsicAttributes) } /** * Parsing incoming headers for use in a distributed trace. * W3C TraceContext format is preferred over the NewRelic DT format. * NewRelic DT format will be used if no `traceparent` header is found. * * @param {string} [transport='Unknown'] - The transport type that delivered the trace. * @param {object} headers - Headers to search for supported trace formats. Keys must be lowercase. */ Transaction.prototype.acceptDistributedTraceHeaders = acceptDistributedTraceHeaders function acceptDistributedTraceHeaders(transportType, headers) { if (headers == null || typeof headers !== 'object') { logger.trace( 'Ignoring distributed trace headers for transaction %s. Headers not passed in as object.', this.id ) return } const transport = TRANSPORT_TYPES_SET[transportType] ? transportType : TRANSPORT_TYPES.UNKNOWN // assumes header keys already lowercase const traceparentHeader = headers[TRACE_CONTEXT_PARENT_HEADER]?.toString('utf8') const tracestateHeader = headers[TRACE_CONTEXT_STATE_HEADER]?.toString('utf8') if (traceparentHeader) { logger.trace('Accepting trace context DT payload for transaction %s', this.id) this.acceptTraceContextPayload(traceparentHeader, tracestateHeader, transport) } else if (NEWRELIC_TRACE_HEADER in headers) { logger.trace('Accepting newrelic DT payload for transaction %s', this.id) // assumes header keys already lowercase const payload = headers[NEWRELIC_TRACE_HEADER] this._acceptDistributedTracePayload(payload, transport) } } /** * Inserts distributed trace headers into the provided headers map. * * @param {object} headers * @param {object} setter - otel bridge setter to assign headers * @param {object} spanContext otel span context */ Transaction.prototype.insertDistributedTraceHeaders = insertDistributedTraceHeaders function insertDistributedTraceHeaders(headers, setter, spanContext) { if (!headers) { logger.trace('insertDistributedTraceHeaders called without headers.') return } checkForExistingNrTraceHeaders(headers) // Ensure we have priority before generating trace headers. this._calculatePriority() this.traceContext.addTraceContextHeaders(headers, setter, spanContext) this.isDistributedTrace = true logger.trace('Added outbound request w3c trace context headers in transaction %s', this.id) if (this.agent.config.distributed_tracing.exclude_newrelic_header) { logger.trace('Excluding newrelic header due to exclude_newrelic_header: true') return } try { const newrelicFormatData = this._createDistributedTracePayload().httpSafe() if (newrelicFormatData) { headers[NEWRELIC_TRACE_HEADER] = newrelicFormatData logger.trace('Added outbound request distributed tracing headers in transaction %s', this.id) } } catch (error) { logger.trace(error, 'Failed to create distributed trace payload') } } function checkForExistingNrTraceHeaders(headers) { const traceparentHeader = headers[TRACE_CONTEXT_PARENT_HEADER] const newrelicHeader = headers[NEWRELIC_TRACE_HEADER] const hasExisting = traceparentHeader || newrelicHeader if (hasExisting) { logger.trace(MULTIPLE_INSERT_MESSAGE, traceparentHeader, newrelicHeader) } } Transaction.prototype.acceptTraceContextPayload = acceptTraceContextPayload function acceptTraceContextPayload(traceparentHeader, tracestateHeader, transport) { if (this.isDistributedTrace) { logger.warn( 'Already accepted or created a distributed trace payload for transaction %s, ignoring call', this.id ) if (this.acceptedDistributedTrace) { this.agent.recordSupportability('TraceContext/Accept/Ignored/Multiple') } else { this.agent.recordSupportability('TraceContext/Accept/Ignored/CreateBeforeAccept') } return } const { traceparent, tracestate } = this.traceContext.acceptTraceContextPayload(traceparentHeader, tracestateHeader) if (traceparent === undefined) { // If the traceparent wasn't accepted, there isn't anything to do. Per spec, // the trace state should not be used if the traceparent is not provided // or is not valid. return } this.acceptedDistributedTrace = true this.isDistributedTrace = true this.traceId = traceparent.traceId this.parentSpanId = traceparent.parentId this.parentTransportType = transport if (tracestate !== undefined && tracestate.intrinsics?.isValid === true) { // Add properties that are only available if we have a New Relic tracestate // list member present. this.parentType = tracestate.parentType this.parentAcct = tracestate.parentAccountId this.parentApp = tracestate.parentAppId this.parentId = tracestate.transactionId if (tracestate.timestamp != null) { this.parentTransportDuration = Math.max(0, (Date.now() - tracestate.timestamp) / 1_000) } } decideSamplingFromW3cData({ transaction: this, traceparent, tracestate }) } /** * Updates the transaction with sampling configuration as determined by the * agent's distributed tracing sampler configuration. In short, updates the * transaction to always sample, never sample, or let the standard sampling * algorithm make the decision. * * @param {object} params Input parameters. * @param {Transaction} params.transaction The transaction to update. * @param {Traceparent} params.traceparent The object representation of a * traceparent header. * @param {Tracestate} params.tracestate The object representation of a * tracestate header. */ function decideSamplingFromW3cData({ transaction, traceparent, tracestate }) { const { remote_parent_sampled: parentSampled, remote_parent_not_sampled: parentNotSampled } = transaction.agent.config.distributed_tracing.sampler if (traceparent.isSampled === true) { switch (parentSampled) { case 'default': { applyOriginalSamplingDecision({ transaction, tracestate }) break } case 'always_on': { transaction.sampled = true transaction.priority = 2.0 break } case 'always_off': { transaction.sampled = false transaction.priority = 0 break } } } else if (traceparent.isSampled === false) { switch (parentNotSampled) { case 'default': { applyOriginalSamplingDecision({ transaction, tracestate }) break } case 'always_on': { transaction.sampled = true transaction.priority = 2.0 break } case 'always_off': { transaction.sampled = false transaction.priority = 0 break } } } } /** * Sets the sampling decision according to the way we did things prior to * adding support for honoring the W3C traceparent sampled flag. * * Note: this function only exists to appease the complexity linting rule. * * @param {object} params Function parameters. * @param {Transaction} params.transaction The transaction to update. * @param {Tracestate} params.tracestate A tracestate object with embedded * New Relic intrinsics. */ function applyOriginalSamplingDecision({ transaction, tracestate }) { // Our original implementation decided sampling based on the intrinsics // within the New Relic listmember in the tracestate. transaction.sampled = tracestate?.intrinsics ? tracestate.isSampled : null transaction.priority = tracestate?.intrinsics ? tracestate.priority : null } /* The following underscored functions are used exclusively by the _acceptDistributedTracePayload method. They're broken out to reduce its cognitive complexity. */ const _dtPayloadTest = function _dtPayloadTest(payload) { if (!payload) { this.agent.recordSupportability('DistributedTrace/AcceptPayload/Ignored/Null') } return !!payload } const _isDtTest = function _isDtTest() { if (this.isDistributedTrace) { logger.warn( 'Already accepted or created a distributed trace payload for transaction %s, ignoring call', this.id ) let supportabilityMetric = 'DistributedTrace/AcceptPayload/Ignored/CreateBeforeAccept' if (this.parentId) { supportabilityMetric = 'DistributedTrace/AcceptPayload/Ignored/Multiple' } this.agent.recordSupportability(supportabilityMetric) return true } return false } const _dtConfigTest = function _dtConfigTest() { const config = this.agent.config const distTraceEnabled = config.distributed_tracing.enabled const trustedAccount = config.trusted_account_key || config.account_id if (!distTraceEnabled || !trustedAccount) { logger.debug( 'Invalid configuration for distributed trace payload, not accepting ' + '(distributed_tracing.enabled: %s, trustKey: %s', distTraceEnabled, trustedAccount ) this.agent.recordSupportability(DT_ACCEPT_PAYLOAD_EXCEPTION_METRIC) return false } return trustedAccount } const _dtParseTest = function _dtParseTest(payload) { const parsed = this._getParsedPayload(payload) if (!parsed) { return false } if (!parsed.v) { logger.warn('Received a distributed trace payload with no version field', this.id) } if (!parsed.d) { logger.warn('Received a distributed trace payload with no data field', this.id) } if (!parsed.v || !parsed.d) { this.agent.recordSupportability(DT_ACCEPT_PAYLOAD_PARSE_EXCEPTION_METRIC) return false } return parsed } const _dtVersionTest = function _dtVersionTest(parsed) { const majorVersion = parsed.v && typeof parsed.v[0] === 'number' && parsed.v[0] if (majorVersion === null) { logger.warn('Invalid distributed trace payload, not accepting') this.agent.recordSupportability(DT_ACCEPT_PAYLOAD_EXCEPTION_METRIC) } if (majorVersion > 0) { // TODO: Add DistributedTracePayload class? this.agent.recordSupportability('DistributedTrace/AcceptPayload/Ignored/MajorVersion') } return majorVersion } const _dtRequiredKeyTest = function _dtRequiredKeyTest(data) { return REQUIRED_DT_KEYS.every(function checkExists(key) { return data[key] != null }) } const _dtSpanParentTest = function _dtSpanParentTest(requiredKeysExist, data) { // Either parentSpanId or parentId are required. if (!requiredKeysExist || (data.tx == null && data.id == null)) { this.agent.recordSupportability(DT_ACCEPT_PAYLOAD_PARSE_EXCEPTION_METRIC) return false } return true } const _dtDefineAttrsFromTraceData = function _dtDefineAttrsFromTraceData(data, transport) { this.parentType = data.ty this.parentApp = data.ap this.parentAcct = data.ac this.parentTransportType = transport this.parentTransportDuration = Math.max(0, (Date.now() - data.ti) / 1000) this.traceId = data.tr if (data.pr) { this.priority = data.pr this.sampled = data.sa != null ? data.sa : this.sampled } if (data.tx) { this.parentId = data.tx } if (data.id) { this.parentSpanId = data.id } this.isDistributedTrace = true // Track if the distributed trace was created through accepting, since // there is potentially no data difference between creation from // Mobile or Browser trace payloads and creation. this.acceptedDistributedTrace = true this.agent.recordSupportability('DistributedTrace/AcceptPayload/Success') } /** * Parses incoming distributed trace header payload. * * @param {object} payload - The distributed trace payload to accept. * @param {string} [transport='Unknown'] - The transport type that delivered the payload. */ Transaction.prototype._acceptDistributedTracePayload = _acceptDistributedTracePayload function _acceptDistributedTracePayload(payload, transport) { const payloadTest = _dtPayloadTest.bind(this) if (!payloadTest(payload)) { return } const isDtTest = _isDtTest.bind(this) if (isDtTest()) { return } const configTest = _dtConfigTest.bind(this) const configTestResult = configTest() if (!configTestResult) { return } const traceParseTest = _dtParseTest.bind(this) const parsed = traceParseTest(payload) if (!parsed) { return } const traceVersionTest = _dtVersionTest.bind(this) if (traceVersionTest(parsed) > 0) { return } const data = parsed.d if (!data) { logger.warn('No distributed trace data received, not accepting payload') this.agent.recordSupportability(DT_ACCEPT_PAYLOAD_EXCEPTION_METRIC) return } const requiredKeyTest = _dtRequiredKeyTest.bind(this) const requiredKeysExist = requiredKeyTest(data) const spanParentTest = _dtSpanParentTest.bind(this) const spanParentResult = spanParentTest(requiredKeysExist, data) if (!spanParentResult) { return } const trustedAccount = configTestResult const trustedAccountKey = data.tk || data.ac if (trustedAccountKey !== trustedAccount) { this.agent.recordSupportability('DistributedTrace/AcceptPayload/Ignored/UntrustedAccount') return } const defineAttrsFromTraceData = _dtDefineAttrsFromTraceData.bind(this) defineAttrsFromTraceData(data, transport) } /** * Returns parsed payload object after attempting to decode it from base64, * and parsing the JSON string. * * @param {string} payload Payload string to be JSON.parsed * @returns {(object|null)} parsed JSON payload or null */ Transaction.prototype._getParsedPayload = function _getParsedPayload(payload) { let parsed = payload if (typeof payload === 'string') { if (payload.charAt(0) !== '{' && payload.charAt(0) !== '[') { try { payload = Buffer.from(payload, 'base64').toString('utf-8') } catch (err) { logger.warn(err, 'Got unparseable distributed trace payload in transaction %s', this.id) this.agent.recordSupportability(DT_ACCEPT_PAYLOAD_PARSE_EXCEPTION_METRIC) return null } } try { parsed = JSON.parse(payload) } catch (err) { logger.warn(err, 'Failed to parse distributed trace payload in transaction %s', this.id) this.agent.recordSupportability(DT_ACCEPT_PAYLOAD_PARSE_EXCEPTION_METRIC) return null } } return parsed } /** * Creates a distributed trace payload. */ Transaction.prototype._createDistributedTracePayload = _createDistributedTracePayload function _createDistributedTracePayload() { const config = this.agent.config const accountId = config.account_id const appId = config.primary_application_id const distTraceEnabled = config.distributed_tracing.enabled if (!accountId || !appId || !distTraceEnabled) { logger.debug( 'Invalid configuration for distributed trace payload ' + '(distributed_tracing.enabled: %s, account_id: %s, application_id: %s) ' + 'in transaction %s', distTraceEnabled, accountId, appId, this.id ) return new DTPayloadStub() } const currSegment = this.agent.tracer.getSegment() const data = { ty: 'App', ac: accountId, ap: appId, tx: this.id, tr: this.traceId, pr: this.priority, sa: this.sampled, ti: Date.now() } if (config.span_events.enabled && this.sampled && currSegment) { data.id = currSegment.id } if (config.trusted_account_key && config.trusted_account_key !== accountId) { data.tk = config.trusted_account_key } this.isDistributedTrace = true this.agent.recordSupportability('DistributedTrace/CreatePayload/Success') return new DTPayload(data) } /** * Adds distributed trace attributes to instrinsics object. */ Transaction.prototype.addDistributedTraceIntrinsics = addDistributedTraceIntrinsics function addDistributedTraceIntrinsics(attrs) { this._calculatePriority() // *always* add these if DT flag is enabled. attrs.traceId = this.traceId attrs.guid = this.id attrs.priority = this.priority attrs.sampled = !!this.sampled // add the rest only if payload was received if (this.parentType) { attrs['parent.type'] = this.parentType } if (this.parentApp) { attrs['parent.app'] = this.parentApp } if (this.parentAcct) { attrs['parent.account'] = this.parentAcct } if (this.parentTransportType) { attrs['parent.transportType'] = this.parentTransportType } if (this.parentTransportDuration != null) { attrs['parent.transportDuration'] = this.parentTransportDuration } } Transaction.prototype.isSampled = function isSampled() { this._calculatePriority() return this.sampled } /** * Generates a priority for the transaction if it does not have one already. */ Transaction.prototype._calculatePriority = function _calculatePriority() { if (this.priority === null) { // eslint-disable-next-line sonarjs/pseudo-random this.priority = Math.random() // We want to separate the priority roll from the decision roll to // avoid biasing the priority range // eslint-disable-next-line sonarjs/pseudo-random this.sampled = this.agent.transactionSampler.shouldSample(Math.random()) if (this.sampled) { this.priority += 1 } // Truncate the priority after potentially modifying it to avoid floating // point errors. this.priority = ((this.priority * 1e6) | 0) / 1e6 } } function _makeValueSet(obj) { return Object.keys(obj) .map((t) => obj[t]) .reduce(function reduceToMap(o, t) { o[t] = true return o }, Object.create(null)) } Transaction.prototype.addRequestParameters = addRequestParameters /** * Adds query parameters to create attributes in the form * 'request.parameters.{key}'. These attributes will only be created * when 'request.parameters.*' is included in the attribute config. * * Used by the "serverless mode" lambda logic * * @param {Object<string, string>} requestParameters of the request object */ function addRequestParameters(requestParameters) { for (const key in requestParameters) { if (props.hasOwn(requestParameters, key)) { this.trace.attributes.addAttribute( DESTS.NONE, REQUEST_PARAMS_PATH + key, requestParameters[key] ) const segment = this.baseSegment segment.attributes.addAttribute(DESTS.NONE, REQUEST_PARAMS_PATH + key, requestParameters[key]) } } } /** * Increments counters used to report details. * `numSegments` - recorded as supportability metric when transaction ends * `agent.totalActiveSegments` used as a trace level log value when metrics are harvested * `agent.segmentsCreatedInHarvest` used as a trace level log value when metrics are harvested */ Transaction.prototype.incrementCounters = function incrementCounters() { ++this.numSegments this.agent.incrementCounters() } module.exports = Transaction