UNPKG

newrelic

Version:
1,336 lines (1,164 loc) 41.1 kB
/* * Copyright 2020 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' const errorHelper = require('../errors/helper') var hashes = require('../util/hashes') var logger = require('../logger').child({component: 'transaction'}) var Metrics = require('../metrics') var NAMES = require('../metrics/names') var NameState = require('./name-state') var props = require('../util/properties') var Timer = require('../timer') var Trace = require('./trace') var url = require('url') var urltils = require('../util/urltils') const TraceContext = require('./tracecontext').TraceContext /* * * 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. */ function Transaction(agent) { if (!agent) throw new Error('every transaction must be bound to the agent') this.traceFlag = false if (agent.config.logging.diagnostics) { this.traceStacks = [] } else { this.traceStacks = null } 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.syntheticsData = 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 = 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) agent.emit('transactionStarted', this) this.probe('Transaction created', {id: this.id}) } 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 Transaction.prototype.probe = function probe(action, extra) { if (this.traceStacks) { this.traceStacks.push({ stack: (new Error(action)).stack.split('\n'), extra: extra }) } } /** * 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 } /** * @return {bool} 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. */ Transaction.prototype.end = function end() { if (!this.timer.isActive()) return if (this.traceFlag) { logger.warn( {segment: {name: this.name, stacks: this.traceStacks}}, 'Flagged transaction ended.' ) } 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.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). */ 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. * * @return {object} The normalization results after running user and server rules. */ Transaction.prototype._runUserNamingRules = function _runUserNamingRules(requestUrl) { // 1. user normalization rules (set in configuration) var normalized = this.agent.userNormalizer.normalize(requestUrl) if (normalized.matched) { // After applying user naming rule, apply server-side sent rules to // further squash possible MGIs var 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) { var 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. * * @return {object} An object with the derived partial name in `value` and a * boolean flag in `ignore`. */ Transaction.prototype._partialNameFromUri = _partialNameFromUri function _partialNameFromUri(requestUrl, status) { var scrubbedUrl = urltils.scrub(requestUrl) // 0. If there is a name in the name-state stack, use it. var partialName = this._partialName var 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 var 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 { var normalized = this.agent.urlNormalizer.normalize(scrubbedUrl) ignore = ignore || normalized.ignore partialName = normalized.value } } return { ignore: 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: requestURL, statusCode: statusCode, transactionId: this.id, transactionName: this.name}, 'Setting transaction name') } this.url = urltils.scrub(requestURL) this.statusCode = statusCode // Derive the name from the request URL. var partialName = this._partialNameFromUri(requestURL, statusCode) this._partialName = partialName.value if (partialName.ignore) { this.ignore = true } // If a namestate stack exists, copy route parameters over to the trace. if (!this.nameState.isEmpty() && this.baseSegment) { this.nameState.forEachParams(function forEachRouteParams(params) { for (var key in params) { if (props.hasOwn(params, key)) { this.trace.attributes.addAttribute( DESTS.NONE, 'request.parameters.' + key, params[key] ) const segment = this.agent.tracer.getSegment() if (!segment) { logger .trace('Active segment not available, not adding request.parameters attribute for %s', key) } else { segment.attributes.addAttribute( DESTS.NONE, 'request.parameters.' + key, params[key] ) } } } }, this) } // Apply transaction name normalization rules (sent by server) to full name. var fullName = TYPE_METRICS[this.type] + '/' + this._partialName var normalized = this.agent.transactionNameNormalizer.normalize(fullName) if (normalized.ignore) { this.ignore = true } this.name = normalized.value // 5. transaction segment term normalizer this.name = this.agent.txSegmentNormalizer.normalize(this.name).value // 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') } } 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. * * @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. var params = urltils.parseParameters(rawURL) for (var key in params) { if (props.hasOwn(params, key)) { this.trace.attributes.addAttribute( DESTS.NONE, 'request.parameters.' + 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.parameters.' + key, params[key] ) } } } this.baseSegment.markAsWeb() } /** * 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. */ 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.type === 'web' && this.url) { return this.finalizeNameFromUri(this.url, this.statusCode) } this._partialName = this.forceName || name || this._partialName if (!this._partialName) { logger.debug('No name for transaction %s, not finalizing.', this.id) return } var fullName = TYPE_METRICS[this.type] + '/' + this._partialName // Transaction normalizers run on the full metric name, not the user facing // transaction name. var normalized = this.agent.transactionNameNormalizer.normalize(fullName) if (normalized.ignore) { this.ignore = true } this.name = normalized.value if (this.forceIgnore !== null) { this.ignore = this.forceIgnore } this.baseSegment && this.baseSegment.setNameFromTransaction() 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. */ Transaction.prototype.getName = function getName() { if (this.isWeb() && this.url) { return this._partialNameFromUri(this.url, this.statusCode).value } return this._partialName } Transaction.prototype.getFullName = function getFullName() { var name = null if (this.forceName) { name = this.forceName } else if (this.name) { return this.name } else { name = this.getName() } if (!name) { return null } var fullName = TYPE_METRICS[this.type] + '/' + name return this.agent.transactionNameNormalizer.normalize(fullName).value } /** * Returns the full URL of the transaction with query, search, or hash portions * removed. This is only applicable for web transactions. * * Caches to ._scrubbedUrl, pulls in from .parsedUrl if it is available, * otherwise it will parse .url, store it on .parsedUrl, then scrub the URL and * store it in the cache. * * Returns a string or undefined. */ Transaction.prototype.getScrubbedUrl = function getScrubbedUrl() { if (!this.isWeb()) return if (this._scrubbedUrl) return this._scrubbedUrl // If we don't have a parsedUrl, lets populate it from .url if (!this.parsedUrl) { // At time of writing .url should always be set by the time we get here // because that is what .isWeb() checks against. In the future it may be // instead checking a enum or other property so guard ourselves just in // case. if (!this.url) return this.parsedUrl = url.parse(this.url) } var scrubbedParsedUrl = Object.assign(Object.create(null), this.parsedUrl) scrubbedParsedUrl.search = null scrubbedParsedUrl.query = null scrubbedParsedUrl.href = null scrubbedParsedUrl.path = null scrubbedParsedUrl.hash = null this._scrubbedUrl = url.format(scrubbedParsedUrl) return this._scrubbedUrl } /** * 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() { var name = this.name for (var i = 0, l = this._recorders.length; i < l; ++i) { this._recorders[i](name) } } /** * 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} keyApdex A key transaction apdexT, in milliseconds * (optional). */ Transaction.prototype._setApdex = function _setApdex(name, duration, keyApdexInMillis) { var 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 incrementing 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 */ 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. */ Transaction.prototype.includesOutboundRequests = function includesOutboundRequests() { return this.pathHashes.length > 0 } /** * Get unique previous path hashes for a transaction. Does not include * current path hash. */ Transaction.prototype.alternatePathHashes = function alternatePathHashes() { var curHash = hashes.calculatePathHash( this.agent.config.applications()[0], this.getFullName(), this.referringPathHash ) var altHashes = this.pathHashes.slice() var 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. */ Transaction.prototype._linkExceptionToSegment = _linkExceptionToSegment function _linkExceptionToSegment(exception) { const 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. */ Transaction.prototype.addException = _addException function _addException(exception) { if (!this.isActive()) { logger.trace('Transaction is not active. Not capturing error: ', exception) return } this._linkExceptionToSegment(exception) 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 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 true if an error happened during the transaction or if the transaction itself is * considered to be an error. */ Transaction.prototype.hasErrors = function _hasErrors() { var isErroredTransaction = urltils.isError(this.agent.config, this.statusCode) var transactionHasExceptions = this.exceptions.length > 0 var transactionHasuserErrors = this.userErrors.length > 0 return (transactionHasExceptions || transactionHasuserErrors || isErroredTransaction) } // Returns true if all the errors/exceptions collected so far are expected errors Transaction.prototype.hasOnlyExpectedErrors = function hasOnlyExpectedErrors() { if (0 === this.exceptions.length) { 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.error, this.agent.config, urltils ) || errorHelper.shouldIgnoreError( this, exception.error, this.agent.config ) ) if (isUnexpected) { return false } } return true } /** * Returns agent intrinsic attribute for this transaction. */ Transaction.prototype.getIntrinsicAttributes = function getIntrinsicAttributes() { if (!this._intrinsicAttributes.totalTime) { var 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 } } if (this.syntheticsData) { this._intrinsicAttributes.synthetics_resource_id = this.syntheticsData.resourceId this._intrinsicAttributes.synthetics_job_id = this.syntheticsData.jobId this._intrinsicAttributes.synthetics_monitor_id = this.syntheticsData.monitorId } } 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 @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 traceparent = headers[TRACE_CONTEXT_PARENT_HEADER] if (traceparent) { logger.trace('Accepting trace context DT payload for transaction %s', this.id) // assumes header keys already lowercase const tracestate = headers[TRACE_CONTEXT_STATE_HEADER] this.acceptTraceContextPayload(traceparent, tracestate, 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 */ Transaction.prototype.insertDistributedTraceHeaders = insertDistributedTraceHeaders function insertDistributedTraceHeaders(headers) { 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) 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() 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(traceparent, tracestate, 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 traceContext = this.traceContext.acceptTraceContextPayload(traceparent, tracestate) if (traceContext.acceptedTraceparent) { this.acceptedDistributedTrace = true this.isDistributedTrace = true this.traceId = traceContext.traceId this.parentSpanId = traceContext.parentSpanId this.parentTransportDuration = traceContext.transportDuration this.parentTransportType = transport if (traceContext.acceptedTracestate) { this.parentType = traceContext.parentType this.parentAcct = traceContext.accountId this.parentApp = traceContext.appId this.parentId = traceContext.transactionId this.sampled = traceContext.sampled this.priority = traceContext.priority } } } /** * 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) { if (!payload) { this.agent.recordSupportability('DistributedTrace/AcceptPayload/Ignored/Null') return } if (this.isDistributedTrace) { logger.warn( 'Already accepted or created a distributed trace payload for transaction %s, ignoring call', this.id ) if (this.parentId) { this.agent.recordSupportability('DistributedTrace/AcceptPayload/Ignored/Multiple') } else { this.agent.recordSupportability( 'DistributedTrace/AcceptPayload/Ignored/CreateBeforeAccept' ) } return } 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('DistributedTrace/AcceptPayload/Exception') return } const parsed = this._getParsedPayload(payload) if (!parsed) { return } if (!parsed.v || !parsed.d) { 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 ) } this.agent.recordSupportability('DistributedTrace/AcceptPayload/ParseException') return } 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('DistributedTrace/AcceptPayload/Exception') } if (majorVersion > 0) { // TODO: Add DistributedTracePayload class? this.agent.recordSupportability('DistributedTrace/AcceptPayload/Ignored/MajorVersion') return } const data = parsed.d if (!data) { logger.warn('No distributed trace data received, not accepting payload') this.agent.recordSupportability('DistributedTrace/AcceptPayload/Exception') return } const requiredKeysExist = REQUIRED_DT_KEYS.every(function checkExists(key) { return data[key] != null }) // Either parentSpanId or parentId are required. if (!requiredKeysExist || data.tx == null && data.id == null) { this.agent.recordSupportability('DistributedTrace/AcceptPayload/ParseException') return } const trustedAccountKey = data.tk || data.ac if (trustedAccountKey !== trustedAccount) { this.agent.recordSupportability( `DistributedTrace/AcceptPayload/Ignored/UntrustedAccount` ) return } 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') } /** * Returns parsed payload object after attempting to decode it from base64, * and parsing the JSON string. */ 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('DistributedTrace/AcceptPayload/ParseException') 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('DistributedTrace/AcceptPayload/ParseException') 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) { this.priority = Math.random() // We want to separate the priority roll from the decision roll to // avoid biasing the priority range 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 request/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. * @param {Object.<string, string>} requestParameters */ function addRequestParameters(requestParameters) { for (var key in requestParameters) { if (props.hasOwn(requestParameters, key)) { this.trace.attributes.addAttribute( DESTS.NONE, 'request.parameters.' + key, requestParameters[key] ) const segment = this.baseSegment segment.attributes.addAttribute( DESTS.NONE, 'request.parameters.' + key, requestParameters[key] ) } } } module.exports = Transaction