UNPKG

newrelic

Version:
600 lines (504 loc) 20 kB
/* * Copyright 2020 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' const logger = require('../logger').child({component: 'TraceContext'}) const hashes = require('../util/hashes') const TRACE_CONTEXT_PARENT_HEADER = 'traceparent' const TRACE_CONTEXT_STATE_HEADER = 'tracestate' const PARENT_TYPES = ['App', 'Browser', 'Mobile'] const APP_PARENT_TYPE = PARENT_TYPES.indexOf('App') const W3C_TRACEPARENT_VERSION = '00' const NR_TRACESTATE_VERSION = 0 // 255 (ff) explicitly not allowed for version const VERSION_VALID_RGX = /^((?![f]{2})[a-f0-9]{2})$/ const TRACEID_VALID_RGX = /^((?![0]{32})[a-f0-9]{32})$/ const PARENTID_VALID_RGX = /^((?![0]{16})[a-f0-9]{16})$/ const FLAGS_VALID_RGX = /^([a-f0-9]{2})$/ const FLAGS = { sampled: 0x00000001 } /** * The class responsible for accepting, validating, and producing w3c tracecontext headers. */ class TraceContext { /** * Create a TraceContext object * @param {Transaction} transaction - a transaction object to attach to. */ constructor(transaction) { this.transaction = transaction this.tracingVendors = null this.trustedParentId = null this._traceStateRaw = null this.flags = { get sampled() { return transaction.sampled } } } /** * Creates a W3C TraceContext traceparent header payload. */ createTraceparent() { // In case we receive a trace ID that isn't the proper length, zero pad let traceId = this.transaction.traceId traceId = traceId.padStart(32, '0') // If we had to pad, there's a chance this is an invalid upper-case header // originating from a newrelic format DT payload being accepted. if (traceId !== this.transaction.traceId && !TRACEID_VALID_RGX.test(traceId)) { traceId = traceId.toLowerCase() } // If no segment/span is in context, generate one so we can have a valid traceparent const segment = this.transaction.agent.tracer.getSegment() let parentId = segment && segment.id if (!parentId) { parentId = hashes.makeId(16) logger.debug( 'No segment/span in context. Generated new traceparent parentId (%s) for traceId (%s)', parentId, traceId ) } return `${W3C_TRACEPARENT_VERSION}-${traceId}-${parentId}-${this.createFlagsHex()}` } /** * Creates a W3C TraceContext tracestate header payload. */ createTracestate() { const config = this.transaction.agent.config const trustedAccountKey = config.trusted_account_key const version = NR_TRACESTATE_VERSION const parentType = APP_PARENT_TYPE const appId = config.primary_application_id const accountId = config.account_id if (!accountId || !appId || !trustedAccountKey) { logger.debug( 'Unable to create tracestate header due to missing required fields ' + '(account_id: %s, primary_application_id: %s, trusted_account_key: %s) in transaction %s' + 'This may occur if a trace is created prior to the agent fully starting.', accountId, appId, trustedAccountKey, this.transaction.id ) this.transaction.agent.recordSupportability('TraceContext/TraceState/Create/Exception') return this._traceStateRaw || '' } // If no segment/span is in context, we do not send one as // we technically do not have a "span" on the agent side and // this trace data is newrelic specific. let spanId = '' if (config.span_events.enabled) { const segment = this.transaction.agent.tracer.getSegment() if (segment) { spanId = segment.id } else { logger.debug('No segment/span in context. Not sending spanId in tracestate.') } } else { logger.trace('Span events disabled. Not sending spanId in tracestate.') } const transactionId = config.transaction_events.enabled ? this.transaction.id : '' const sampled = this.transaction.sampled ? '1' : '0' const priority = this.transaction.priority ? this.transaction.priority.toFixed(6) : '' const timestamp = Date.now() const nrTraceState = `${trustedAccountKey}@nr=${version}-${parentType}-${accountId}` + `-${appId}-${spanId}-${transactionId}-${sampled}-${priority}-${timestamp}` if (this._traceStateRaw) { return `${nrTraceState},${this._traceStateRaw}` } return nrTraceState } /** * Takes a headers object and modifies it in place by adding Trace Context headers * @param {object} headers - Headers for an HTTP request */ addTraceContextHeaders(headers) { if (!headers) { return } const traceParent = this.createTraceparent() headers[TRACE_CONTEXT_PARENT_HEADER] = traceParent logger.trace('traceparent added with %s', traceParent) const tracestate = this.createTracestate() if (tracestate) { headers[TRACE_CONTEXT_STATE_HEADER] = tracestate logger.trace('tracestate added with %s', tracestate) } this.transaction.agent.recordSupportability('TraceContext/Create/Success') } /** * @typedef TraceContextData * @property {boolean} acceptedTraceparent - Whether a W3C traceparent headers was * parsed, validated, and accepted * @property {boolean} acceptedTracestate - Whether a New Relic tracestate headers was * parsed, validated, and accepted * @property {boolean} entryValid - Whether the matching NR tracestate string is valid * @property {Intrinsics} intrinsics - All the parts of the New Relic tracestate string * parsed and split out into an object * @property {string} newTraceState - The raw tracestate without the New Relic entry */ /** * Takes a TraceContext headers from an HTTP request, parses them, validates them, and * applies the values to the internal state, returning an object with the * relevant Trace Context data and validation information. * * @param {string} traceparent - W3C traceparent header from an HTTP request * @param {string} tracestate - W3C tracestate header from an HTTP request * @returns {Object} returns an Object with the traceparent data and validation info */ acceptTraceContextPayload(traceparent, tracestate) { const traceContextData = { acceptedTraceparent: false, acceptedTracestate: false, acceptedNRTracestate: false, traceId: null, parentSpanId: null, parentType: null, accountId: null, appId: null, transactionId: null, sampled: null, priority: null, transportDuration: null } // // Parsing traceparent // if (!traceparent) { // From the W3C spec: If the vendor failed to parse traceparent, it MUST NOT // attempt to parse tracestate return traceContextData } logger.trace('Accepting TraceContext for transaction %s', this.transaction.id) const parsedParent = this._validateAndParseTraceParentHeader(traceparent) // Log if there is a version mismatch in traceparent if (parsedParent.version !== W3C_TRACEPARENT_VERSION) { logger.trace( 'Incoming traceparent version: %s, agent traceparent version: %s', parsedParent.version, W3C_TRACEPARENT_VERSION ) } if (parsedParent.entryValid) { logger.trace('Accepted traceparent for transaction %s', this.transaction.id) traceContextData.acceptedTraceparent = true traceContextData.traceId = parsedParent.traceId traceContextData.parentSpanId = parsedParent.parentId } else { logger.trace( 'Invalid traceparent for transaction %s: %s', this.transaction.id, traceparent ) this.transaction.agent.recordSupportability( 'TraceContext/TraceParent/Parse/Exception' ) // From the W3C spec: If the vendor failed to parse traceparent, it MUST NOT // attempt to parse tracestate return traceContextData } // // Parsing tracestate // if (!tracestate) { logger.trace('No tracestate for transaction %s', this.transaction.id) return traceContextData } const parsedState = this._validateAndParseTraceStateHeader(tracestate) if (!parsedState.traceStateValid) { logger.trace('Invalid tracestate for transaction %s: %s', this.transaction.id, tracestate ) this.transaction.agent.recordSupportability('TraceContext/TraceState/Parse/Exception') return traceContextData } // Keep the raw, non-NewRelic tracestate string stored so that we can propagate it this._traceStateRaw = parsedState.newTraceState // These need to be kept to be added to root span events as an attribute this.tracingVendors = parsedState.vendors if ( parsedState.intrinsics && parsedState.intrinsics.version !== NR_TRACESTATE_VERSION ) { logger.trace( 'Incoming tracestate version: %s, agent tracestate version: %s', parsedState.intrinsics.version, NR_TRACESTATE_VERSION ) } if (parsedState.entryValid) { logger.trace('Accepted tracestate for transaction %s', this.transaction.id) traceContextData.acceptedTracestate = true traceContextData.parentType = parsedState.intrinsics.parentType traceContextData.accountId = parsedState.intrinsics.accountId traceContextData.appId = parsedState.intrinsics.appId traceContextData.transactionId = parsedState.intrinsics.transactionId traceContextData.sampled = parsedState.intrinsics.sampled traceContextData.priority = parsedState.intrinsics.priority traceContextData.transportDuration = Math.max(0, (Date.now() - parsedState.intrinsics.timestamp) / 1000) this.trustedParentId = parsedState.intrinsics.spanId this._traceStateRaw = parsedState.newTraceState this.transaction.agent.recordSupportability('TraceContext/Accept/Success') } else if (parsedState.entryFound) { logger.error('Invalid tracestate for transaction %s: %s', this.transaction.id, tracestate) this.transaction.agent.recordSupportability( 'TraceContext/TraceState/InvalidNrEntry' ) } return traceContextData } /** * Validate a traceparent header string and return an object with the relevant parts * parsed out if valid. * * @param {string} traceparent - a W3C traceparent header string * @returns {Object} returns an Object with the traceparent data and validation info */ _validateAndParseTraceParentHeader(traceparent) { const traceParentInfo = { entryValid: false, version: null, traceId: null, parentId: null, flags: null } if (!traceparent) { return traceParentInfo } const trimmed = traceparent.trim() const parts = trimmed.split('-') // No extra data allowed this version. if (parts[0] === W3C_TRACEPARENT_VERSION && parts.length !== 4) { return traceParentInfo } const [version, traceId, parentId, flags] = parts const isValid = VERSION_VALID_RGX.test(version) && TRACEID_VALID_RGX.test(traceId) && PARENTID_VALID_RGX.test(parentId) && FLAGS_VALID_RGX.test(flags) if (isValid) { traceParentInfo.entryValid = true traceParentInfo.version = version traceParentInfo.traceId = traceId traceParentInfo.parentId = parentId traceParentInfo.flags = flags } return traceParentInfo } // Not used now, but will be useful when traceparent has more flags parseFlagsHex(flags) { const flagsInt = parseInt(flags, 16) return Object.keys(FLAGS).reduce((o, key) => { o[key] = Boolean(flagsInt & FLAGS[key]) return o }, {}) } createFlagsHex() { const flagsNum = Object.keys(this.flags).reduce((num, key) => { if (this.flags[key]) { num += FLAGS[key] } return num }, 0) return flagsNum.toString(16).padStart(2, '0') } /** * @typedef TraceStateData * @property {boolean} entryFound - Whether a New Relic tracestate string with a match * trusted account key field is found * @property {boolean} entryValid - Whether the matching NR tracestate string is valid * @property {string} entryInvalidReason - Why the tracestate did not validate * @property {Intrinsics} intrinsics - All the parts of the New Relic tracestate string * parsed and split out into an object * @property {string} newTraceState - The raw tracestate without the New Relic entry * @property {array} vendors - All the vendor strings found in the tracestate */ /** * Accepts a W3C tracestate header string and returns an object with information about * the validity and intrinsics of the parsed tracestate string * * @param {string} tracestate - A raw W3C tracestate header string * @returns {TraceStateData} returns an object with validation information and * instrinsics on any relevant New Relic tracestate strings found */ _validateAndParseTraceStateHeader(tracestate) { let tsd = { entryFound: false, entryValid: undefined, entryInvalidReason: undefined, traceStateValid: undefined, intrinsics: undefined, newTraceState: undefined, vendors: undefined } // See if there's a New Relic Trace State const trustedKey = this.transaction.agent.config.trusted_account_key const hasTrustKey = Boolean(trustedKey) const expectedNrKey = `${trustedKey}@nr` if (!hasTrustKey) { logger.debug( 'Unable to accept any New Relic tracestate list members. ' + 'Missing trusted_account_key. ' + 'This may occur if a trace is received prior to the agent fully starting.' ) this.transaction.agent.recordSupportability('TraceContext/TraceState/Accept/Exception') } let nrTraceStateValue = null const finalListMembers = [] const vendors = [] const incomingListMembers = tracestate.split(',') for (let i = 0; i < incomingListMembers.length; i++) { const listMember = incomingListMembers[i].trim() // Multiple tracestate headers may get combined. Empty headers // can result in a header such as tracestate: 'foo=1, ' which // should still be considered valid with the empty item discarded. if (listMember !== '') { const listMemberParts = listMember.split('=') if (listMemberParts.length !== 2) { tsd.traceStateValid = false logger.debug('Unable to parse tracestate list members.') this.transaction.agent .recordSupportability('TraceContext/TraceState/Parse/Exception/ListMember') return tsd } const [vendorKey, vendorValue] = listMemberParts if (hasTrustKey && (vendorKey === expectedNrKey)) { // Matching members do not get added to vendors. // We'll replace the first valid entry and drop the rest // (which would be invalid members if they exist). // We only want the first one. nrTraceStateValue = nrTraceStateValue || vendorValue } else { vendors.push(vendorKey) finalListMembers.push(listMember) } } } tsd.traceStateValid = true // Rebuild potentially cleaned-up listmembers tsd.newTraceState = finalListMembers.join(',') if (vendors.length > 0) { tsd.vendors = vendors.join(',') } if (!hasTrustKey) { return tsd } if (nrTraceStateValue) { tsd.entryFound = true const intrinsicsValidation = this._validateAndParseIntrinsics(nrTraceStateValue) if (intrinsicsValidation.entryValid) { tsd.entryValid = true tsd.intrinsics = intrinsicsValidation } else { tsd.entryInvalidReason = intrinsicsValidation.invalidReason tsd.entryValid = false } } else { // TraceParent has been accepted, but no trustedKey on tracestate this.transaction.agent.recordSupportability('TraceContext/TraceState/NoNrEntry') } return tsd } /** * @typedef Intrinsics * @property {number} version - TraceContext spec version used * @property {number} parentType - The type of component that produced this tracestate * @property {string} accountId * @property {string} appId * @property {string} spanId * @property {string} transactionId * @property {integer} sampled - 1 or 0, whether the receiving agent should sample * @property {number} priority - floating point of the priority the agent should use, * rounded to 6 decimal places * @property {number} timestamp - when the payload was created, milliseconds since epoch * @property {boolean} entryValid - if all entries in the Intrinsics object is valid */ /** * Accepts a New Relic intrinsics string and returls a validation object w/ * the validity and intrinsics of the tracestate * * @param {string} nrTracestateValue - The value part of a New Relic tracestate entry * @returns {Intrinsics} returns an Intrinsics object with validation information and * instrinsics on any relevant New Relic tracestate strings found */ _validateAndParseIntrinsics(nrTracestateValue) { const intrinsics = this._parseIntrinsics(nrTracestateValue) // Functions that return true when the field is invalid const isNull = v => v == null const intrinsicInvalidations = { version: isNaN, // required, int parentType: isNull, // required, str accountId: isNull, // required, str appId: isNull, // required, str sampled: v => v == null ? false : isNaN(v), // not required, int priority: v => v == null ? false : isNaN(v), // not required, float timestamp: isNaN // required, int } // If a field is found invalid, flag the entry as not valid intrinsics.entryValid = true for (const key of Object.keys(intrinsicInvalidations)) { const invalidation = intrinsicInvalidations[key] if (invalidation && invalidation(intrinsics[key])) { intrinsics.entryValid = false intrinsics.entryInvalidReason = `${key} failed invalidation test` } } // Convert to types expected by Transaction if (intrinsics.sampled != null) { intrinsics.sampled = Boolean(intrinsics.sampled) } intrinsics.parentType = PARENT_TYPES[intrinsics.parentType] if (!intrinsics.parentType) intrinsics.entryValid = false return intrinsics } /** * Parses intrinsics of a New Relic tracestate entry's value */ _parseIntrinsics(nrTracestateValue) { const intrinsics = this._extractTraceStateIntrinsics(nrTracestateValue) const intrinsicConversions = { version: parseInt, parentType: parseInt, // these two can be null, don't try to parse a null sampled: v => v == null ? v : parseInt(v, 10), priority: v => v == null ? v : parseFloat(v), timestamp: parseInt, } for (const key of Object.keys(intrinsicConversions)) { const conversion = intrinsicConversions[key] if (conversion) { intrinsics[key] = conversion(intrinsics[key]) } } return intrinsics } _extractTraceStateIntrinsics(nrTracestate) { const splitValues = nrTracestate.split('-') // convert empty strings to null splitValues.forEach((value, i) => { if (value === '') splitValues[i] = null }) const intrinsics = { version: splitValues[0], parentType: splitValues[1], accountId: splitValues[2], appId: splitValues[3], spanId: splitValues[4], transactionId: splitValues[5], sampled: splitValues[6], priority: splitValues[7], timestamp: splitValues[8] } return intrinsics } } module.exports.TraceContext = TraceContext module.exports.TRACE_CONTEXT_PARENT_HEADER = TRACE_CONTEXT_PARENT_HEADER module.exports.TRACE_CONTEXT_STATE_HEADER = TRACE_CONTEXT_STATE_HEADER