Skip to content

Commit d903413

Browse files
authored
feat: Support honoring W3C traceparent sampled flag (#3009)
1 parent c20c36b commit d903413

21 files changed

+1466
-866
lines changed

THIRD_PARTY_NOTICES.md

+11-12
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic
9191

9292
### @grpc/grpc-js
9393

94-
This product includes source derived from [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) ([v1.13.0](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.13.0)), distributed under the [Apache-2.0 License](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.13.0/LICENSE):
94+
This product includes source derived from [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) ([v1.12.4](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.12.4)), distributed under the [Apache-2.0 License](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.12.4/LICENSE):
9595

9696
```
9797
Apache License
@@ -509,7 +509,7 @@ This product includes source derived from [@grpc/proto-loader](https://github.co
509509

510510
### @newrelic/security-agent
511511

512-
This product includes source derived from [@newrelic/security-agent](https://github.com/newrelic/csec-node-agent) ([v2.3.2](https://github.com/newrelic/csec-node-agent/tree/v2.3.2)), distributed under the [UNKNOWN License](https://github.com/newrelic/csec-node-agent/blob/v2.3.2/LICENSE):
512+
This product includes source derived from [@newrelic/security-agent](https://github.com/newrelic/csec-node-agent) ([v2.3.1](https://github.com/newrelic/csec-node-agent/tree/v2.3.1)), distributed under the [UNKNOWN License](https://github.com/newrelic/csec-node-agent/blob/v2.3.1/LICENSE):
513513

514514
```
515515
## New Relic Software License v1.0
@@ -1271,7 +1271,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
12711271

12721272
### import-in-the-middle
12731273

1274-
This product includes source derived from [import-in-the-middle](https://github.com/nodejs/import-in-the-middle) ([v1.13.1](https://github.com/nodejs/import-in-the-middle/tree/v1.13.1)), distributed under the [Apache-2.0 License](https://github.com/nodejs/import-in-the-middle/blob/v1.13.1/LICENSE):
1274+
This product includes source derived from [import-in-the-middle](https://github.com/nodejs/import-in-the-middle) ([v1.13.0](https://github.com/nodejs/import-in-the-middle/tree/v1.13.0)), distributed under the [Apache-2.0 License](https://github.com/nodejs/import-in-the-middle/blob/v1.13.0/LICENSE):
12751275

12761276
```
12771277
Apache License
@@ -1615,14 +1615,13 @@ IN THE SOFTWARE.
16151615

16161616
### require-in-the-middle
16171617

1618-
This product includes source derived from [require-in-the-middle](https://github.com/nodejs/require-in-the-middle) ([v7.5.2](https://github.com/nodejs/require-in-the-middle/tree/v7.5.2)), distributed under the [MIT License](https://github.com/nodejs/require-in-the-middle/blob/v7.5.2/LICENSE):
1618+
This product includes source derived from [require-in-the-middle](https://github.com/elastic/require-in-the-middle) ([v7.4.0](https://github.com/elastic/require-in-the-middle/tree/v7.4.0)), distributed under the [MIT License](https://github.com/elastic/require-in-the-middle/blob/v7.4.0/LICENSE):
16191619

16201620
```
16211621
The MIT License (MIT)
16221622
16231623
Copyright (c) 2016-2019, Thomas Watson Steen
1624-
Copyright (c) 2019-2025, Elasticsearch B.V.
1625-
Copyright (c) 2025+, require-in-the-middle contributors
1624+
Copyright (c) 2019-2023, Elasticsearch B.V.
16261625
16271626
Permission is hereby granted, free of charge, to any person obtaining a copy
16281627
of this software and associated documentation files (the "Software"), to deal
@@ -1646,7 +1645,7 @@ SOFTWARE.
16461645

16471646
### semver
16481647

1649-
This product includes source derived from [semver](https://github.com/npm/node-semver) ([v7.7.1](https://github.com/npm/node-semver/tree/v7.7.1)), distributed under the [ISC License](https://github.com/npm/node-semver/blob/v7.7.1/LICENSE):
1648+
This product includes source derived from [semver](https://github.com/npm/node-semver) ([v7.6.3](https://github.com/npm/node-semver/tree/v7.6.3)), distributed under the [ISC License](https://github.com/npm/node-semver/blob/v7.6.3/LICENSE):
16501649

16511650
```
16521651
The ISC License
@@ -1702,7 +1701,7 @@ SOFTWARE.
17021701

17031702
### @aws-sdk/client-s3
17041703

1705-
This product includes source derived from [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3) ([v3.758.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.758.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.758.0/LICENSE):
1704+
This product includes source derived from [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3) ([v3.714.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.714.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.714.0/LICENSE):
17061705

17071706
```
17081707
Apache License
@@ -1911,7 +1910,7 @@ This product includes source derived from [@aws-sdk/client-s3](https://github.co
19111910

19121911
### @aws-sdk/s3-request-presigner
19131912

1914-
This product includes source derived from [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3) ([v3.758.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.758.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.758.0/LICENSE):
1913+
This product includes source derived from [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3) ([v3.714.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.714.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.714.0/LICENSE):
19151914

19161915
```
19171916
Apache License
@@ -3739,7 +3738,7 @@ SOFTWARE.
37393738

37403739
### eslint-plugin-jsdoc
37413740

3742-
This product includes source derived from [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) ([v50.6.8](https://github.com/gajus/eslint-plugin-jsdoc/tree/v50.6.8)), distributed under the [BSD-3-Clause License](https://github.com/gajus/eslint-plugin-jsdoc/blob/v50.6.8/LICENSE):
3741+
This product includes source derived from [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) ([v50.6.1](https://github.com/gajus/eslint-plugin-jsdoc/tree/v50.6.1)), distributed under the [BSD-3-Clause License](https://github.com/gajus/eslint-plugin-jsdoc/blob/v50.6.1/LICENSE):
37433742

37443743
```
37453744
Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/)
@@ -3771,7 +3770,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37713770

37723771
### eslint
37733772

3774-
This product includes source derived from [eslint](https://github.com/eslint/eslint) ([v9.22.0](https://github.com/eslint/eslint/tree/v9.22.0)), distributed under the [MIT License](https://github.com/eslint/eslint/blob/v9.22.0/LICENSE):
3773+
This product includes source derived from [eslint](https://github.com/eslint/eslint) ([v9.17.0](https://github.com/eslint/eslint/tree/v9.17.0)), distributed under the [MIT License](https://github.com/eslint/eslint/blob/v9.17.0/LICENSE):
37753774

37763775
```
37773776
Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
@@ -4089,7 +4088,7 @@ THE SOFTWARE.
40894088

40904089
### koa
40914090

4092-
This product includes source derived from [koa](https://github.com/koajs/koa) ([v2.16.0](https://github.com/koajs/koa/tree/v2.16.0)), distributed under the [MIT License](https://github.com/koajs/koa/blob/v2.16.0/LICENSE):
4091+
This product includes source derived from [koa](https://github.com/koajs/koa) ([v2.15.3](https://github.com/koajs/koa/tree/v2.15.3)), distributed under the [MIT License](https://github.com/koajs/koa/blob/v2.15.3/LICENSE):
40934092

40944093
```
40954094
(The MIT License)

lib/agent.js

+4
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,10 @@ function Agent(config) {
303303
}
304304
util.inherits(Agent, EventEmitter)
305305

306+
Object.defineProperty(Agent.prototype, Symbol.toStringTag, {
307+
value: 'Agent'
308+
})
309+
306310
/**
307311
* The agent is meant to only exist once per application, but the singleton is
308312
* managed by index.js. An agent will be created even if the agent's disabled by

lib/config/default.js

+34
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,7 @@ defaultConfig.definition = () => ({
10831083
}
10841084
}
10851085
},
1086+
10861087
/**
10871088
* Controls the method of cross agent tracing in the agent.
10881089
* Distributed tracing lets you see the path that a request takes through your
@@ -1109,8 +1110,41 @@ defaultConfig.definition = () => ({
11091110
exclude_newrelic_header: {
11101111
formatter: boolean,
11111112
default: false
1113+
},
1114+
1115+
sampler: {
1116+
/**
1117+
* When set to `always_on`, the sampled flag in the `traceparent` header
1118+
* being set to "true" will result in the local transaction being sampled
1119+
* with a priority value of "2". When set to `always_off`, the local
1120+
* transaction will never be sampled. At the default setting, the sampling
1121+
* decision will be determined according to the normal algorithm.
1122+
*
1123+
* This setting takes precedence over the `remote_parent_not_sampled`
1124+
* setting.
1125+
*/
1126+
remote_parent_sampled: {
1127+
formatter: allowList.bind(null, ['always_on', 'always_off', 'default']),
1128+
default: 'default'
1129+
},
1130+
1131+
/**
1132+
* When set to `always_on`, the local transaction will be sampled with a
1133+
* priority of "2".
1134+
* When set to `always_off`, the local transaction will never be sampled.
1135+
* At the default setting, the sampling decision will be determined
1136+
* according to the normal algorithm.
1137+
*
1138+
* This setting only affects decisions when the traceparent sampled flag
1139+
* is set to 0.
1140+
*/
1141+
remote_parent_not_sampled: {
1142+
formatter: allowList.bind(null, ['always_on', 'always_off', 'default']),
1143+
default: 'default'
1144+
}
11121145
}
11131146
},
1147+
11141148
/**
11151149
* Controls the use of cross-application tracing.
11161150
*

lib/otel/segments/utils.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
'use strict'
77

8+
const Traceparent = require('../../w3c/traceparent')
9+
810
/**
911
* Accepts trace context payload if span has a parent. It will use the
1012
* span context to extract the traceId, traceFlags and trace state.
@@ -20,8 +22,12 @@ function propagateTraceContext({ transaction, otelSpan, transport }) {
2022

2123
if (parentSpanId) {
2224
// prefix traceFlags with 0 as it is stored as a parsed int on spanContext
23-
const traceparent = `00-${spanContext.traceId}-${parentSpanId}-0${spanContext.traceFlags}`
24-
transaction.acceptTraceContextPayload(traceparent, spanContext?.traceState?.state, transport)
25+
const traceparent = new Traceparent({
26+
traceId: spanContext.traceId,
27+
parentId: parentSpanId,
28+
flags: `0${spanContext.traceFlags}`
29+
})
30+
transaction.acceptTraceContextPayload(traceparent.toString(), spanContext?.traceState?.state, transport)
2531
}
2632
}
2733

lib/shim/message-shim/subscribe-consume.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ function makeWrapConsumer({ spec, queue, destinationName, destNameIsArg }) {
141141
*/
142142
function createConsumerWrapper({ shim, spec, consumer }) {
143143
return function createConsumeTrans() {
144-
// If there is no transaction or we're in a pre-existing transaction,
144+
// If there is no transaction, or we're in a pre-existing transaction,
145145
// then don't do anything. Note that the latter should never happen.
146146
const args = shim.argsToArray.apply(shim, arguments)
147147
const tx = shim.tracer.getTransaction()

lib/transaction/index.js

+105-21
Original file line numberDiff line numberDiff line change
@@ -888,13 +888,12 @@ function acceptDistributedTraceHeaders(transportType, headers) {
888888
const transport = TRANSPORT_TYPES_SET[transportType] ? transportType : TRANSPORT_TYPES.UNKNOWN
889889

890890
// assumes header keys already lowercase
891-
const traceparent = headers[TRACE_CONTEXT_PARENT_HEADER]
891+
const traceparentHeader = headers[TRACE_CONTEXT_PARENT_HEADER]?.toString('utf8')
892+
const tracestateHeader = headers[TRACE_CONTEXT_STATE_HEADER]?.toString('utf8')
892893

893-
if (traceparent) {
894+
if (traceparentHeader) {
894895
logger.trace('Accepting trace context DT payload for transaction %s', this.id)
895-
// assumes header keys already lowercase
896-
const tracestate = headers[TRACE_CONTEXT_STATE_HEADER]
897-
this.acceptTraceContextPayload(traceparent, tracestate, transport)
896+
this.acceptTraceContextPayload(traceparentHeader, tracestateHeader, transport)
898897
} else if (NEWRELIC_TRACE_HEADER in headers) {
899898
logger.trace('Accepting newrelic DT payload for transaction %s', this.id)
900899
// assumes header keys already lowercase
@@ -951,7 +950,7 @@ function checkForExistingNrTraceHeaders(headers) {
951950
}
952951

953952
Transaction.prototype.acceptTraceContextPayload = acceptTraceContextPayload
954-
function acceptTraceContextPayload(traceparent, tracestate, transport) {
953+
function acceptTraceContextPayload(traceparentHeader, tracestateHeader, transport) {
955954
if (this.isDistributedTrace) {
956955
logger.warn(
957956
'Already accepted or created a distributed trace payload for transaction %s, ignoring call',
@@ -967,26 +966,111 @@ function acceptTraceContextPayload(traceparent, tracestate, transport) {
967966
return
968967
}
969968

970-
const traceContext = this.traceContext.acceptTraceContextPayload(traceparent, tracestate)
969+
const { traceparent, tracestate } = this.traceContext.acceptTraceContextPayload(traceparentHeader, tracestateHeader)
970+
if (traceparent === undefined) {
971+
// If the traceparent wasn't accepted, there isn't anything to do. Per spec,
972+
// the trace state should not be used if the traceparent is not provided
973+
// or is not valid.
974+
return
975+
}
971976

972-
if (traceContext.acceptedTraceparent) {
973-
this.acceptedDistributedTrace = true
974-
this.isDistributedTrace = true
977+
this.acceptedDistributedTrace = true
978+
this.isDistributedTrace = true
975979

976-
this.traceId = traceContext.traceId
977-
this.parentSpanId = traceContext.parentSpanId
978-
this.parentTransportDuration = traceContext.transportDuration
979-
this.parentTransportType = transport
980+
this.traceId = traceparent.traceId
981+
this.parentSpanId = traceparent.parentId
982+
this.parentTransportType = transport
980983

981-
if (traceContext.acceptedTracestate) {
982-
this.parentType = traceContext.parentType
983-
this.parentAcct = traceContext.accountId
984-
this.parentApp = traceContext.appId
985-
this.parentId = traceContext.transactionId
986-
this.sampled = traceContext.sampled
987-
this.priority = traceContext.priority
984+
if (tracestate !== undefined && tracestate.intrinsics?.isValid === true) {
985+
// Add properties that are only available if we have a New Relic tracestate
986+
// list member present.
987+
this.parentType = tracestate.parentType
988+
this.parentAcct = tracestate.parentAccountId
989+
this.parentApp = tracestate.parentAppId
990+
this.parentId = tracestate.transactionId
991+
if (tracestate.timestamp != null) {
992+
this.parentTransportDuration = Math.max(0, (Date.now() - tracestate.timestamp) / 1_000)
988993
}
989994
}
995+
996+
decideSamplingFromW3cData({ transaction: this, traceparent, tracestate })
997+
}
998+
999+
/**
1000+
* Updates the transaction with sampling configuration as determined by the
1001+
* agent's distributed tracing sampler configuration. In short, updates the
1002+
* transaction to always sample, never sample, or let the standard sampling
1003+
* algorithm make the decision.
1004+
*
1005+
* @param {object} params Input parameters.
1006+
* @param {Transaction} params.transaction The transaction to update.
1007+
* @param {Traceparent} params.traceparent The object representation of a
1008+
* traceparent header.
1009+
* @param {Tracestate} params.tracestate The object representation of a
1010+
* tracestate header.
1011+
*/
1012+
function decideSamplingFromW3cData({ transaction, traceparent, tracestate }) {
1013+
const {
1014+
remote_parent_sampled: parentSampled,
1015+
remote_parent_not_sampled: parentNotSampled
1016+
} = transaction.agent.config.distributed_tracing.sampler
1017+
if (traceparent.isSampled === true) {
1018+
switch (parentSampled) {
1019+
case 'default': {
1020+
applyOriginalSamplingDecision({ transaction, tracestate })
1021+
break
1022+
}
1023+
1024+
case 'always_on': {
1025+
transaction.sampled = true
1026+
transaction.priority = 2.0
1027+
break
1028+
}
1029+
1030+
case 'always_off': {
1031+
transaction.sampled = false
1032+
transaction.priority = 0
1033+
break
1034+
}
1035+
}
1036+
} else if (traceparent.isSampled === false) {
1037+
switch (parentNotSampled) {
1038+
case 'default': {
1039+
applyOriginalSamplingDecision({ transaction, tracestate })
1040+
break
1041+
}
1042+
1043+
case 'always_on': {
1044+
transaction.sampled = true
1045+
transaction.priority = 2.0
1046+
break
1047+
}
1048+
1049+
case 'always_off': {
1050+
transaction.sampled = false
1051+
transaction.priority = 0
1052+
break
1053+
}
1054+
}
1055+
}
1056+
}
1057+
1058+
/**
1059+
* Sets the sampling decision according to the way we did things prior to
1060+
* adding support for honoring the W3C traceparent sampled flag.
1061+
*
1062+
* Note: this function only exists to appease the complexity linting rule.
1063+
*
1064+
* @param {object} params Function parameters.
1065+
* @param {Transaction} params.transaction The transaction to update.
1066+
* @param {Tracestate} params.tracestate A tracestate object with embedded
1067+
* New Relic intrinsics.
1068+
*/
1069+
function applyOriginalSamplingDecision({ transaction, tracestate }) {
1070+
// Our original implementation decided sampling based on the intrinsics
1071+
// within the New Relic listmember in the tracestate.
1072+
transaction.sampled = tracestate?.intrinsics ? tracestate.isSampled : null
1073+
transaction.priority = tracestate?.intrinsics ? tracestate.priority : null
9901074
}
9911075

9921076
/*

0 commit comments

Comments
 (0)