Skip to content

Commit 971ede6

Browse files
committed
Add traceparent logic
1 parent d331927 commit 971ede6

File tree

3 files changed

+91
-5
lines changed

3 files changed

+91
-5
lines changed

newrelic/api/transaction.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ def __init__(self, application, enabled=None, source=None):
286286
self.tracestate = ""
287287
self._priority = None
288288
self._sampled = None
289+
self._traceparent_sampled = None
289290

290291
self._distributed_trace_state = 0
291292

@@ -1004,16 +1005,32 @@ def _update_agent_attributes(self):
10041005
def user_attributes(self):
10051006
return create_attributes(self._custom_params, DST_ALL, self.attribute_filter)
10061007

1007-
def _compute_sampled_and_priority(self):
1008+
def sampling_algo_compute_sampled_and_priority(self):
10081009
if self._priority is None:
1009-
# truncate priority field to 6 digits past the decimal
1010+
# Truncate priority field to 6 digits past the decimal.
10101011
self._priority = float(f"{random.random():.6f}") # noqa: S311
1011-
10121012
if self._sampled is None:
10131013
self._sampled = self._application.compute_sampled()
10141014
if self._sampled:
10151015
self._priority += 1
10161016

1017+
def _compute_sampled_and_priority(self):
1018+
if self._traceparent_sampled is None:
1019+
config = "default" # Use sampling algo.
1020+
elif self._traceparent_sampled:
1021+
config = self.settings.distributed_tracing.sampler.remote_parent_sampled
1022+
else: # self._traceparent_sampled is False.
1023+
config = self.settings.distributed_tracing.sampler.remote_parent_not_sampled
1024+
1025+
if config == 'always_on':
1026+
self._sampled = True
1027+
self._priority = 2.0
1028+
elif config == 'always_off':
1029+
self._sampled = False
1030+
self._priority = 0
1031+
else:
1032+
self.sampling_algo_compute_sampled_and_priority()
1033+
10171034
def _freeze_path(self):
10181035
if self._frozen_path is None:
10191036
self._name_priority = None
@@ -1348,6 +1365,7 @@ def accept_distributed_trace_headers(self, headers, transport_type="HTTP"):
13481365
else:
13491366
self._record_supportability("Supportability/TraceContext/TraceState/NoNrEntry")
13501367

1368+
self._traceparent_sampled = data.get("sa")
13511369
self._accept_distributed_trace_data(data, transport_type)
13521370
self._record_supportability("Supportability/TraceContext/Accept/Success")
13531371
return True

newrelic/common/encoding_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
DELIMITER_FORMAT_RE = re.compile("[ \t]*,[ \t]*")
3535
PARENT_TYPE = {"0": "App", "1": "Browser", "2": "Mobile"}
3636
BASE64_DECODE_STR = getattr(base64, "decodestring", None)
37+
FLAG_SAMPLED = 1
3738

3839

3940
# Functions for encoding/decoding JSON. These wrappers are used in order
@@ -455,7 +456,10 @@ def decode(cls, payload):
455456
if parent_id == "0" * 16 or trace_id == "0" * 32:
456457
return None
457458

458-
return cls(tr=trace_id, id=parent_id)
459+
# Sampled flag
460+
sa = bool(int(fields[3], 2) & FLAG_SAMPLED)
461+
462+
return cls(tr=trace_id, id=parent_id, sa=sa)
459463

460464

461465
class W3CTraceState(OrderedDict):

tests/agent_features/test_distributed_tracing.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@
1717

1818
import pytest
1919
import webtest
20-
from testing_support.fixtures import override_application_settings, validate_attributes
20+
from testing_support.fixtures import override_application_settings, validate_attributes, validate_attributes_complete
2121
from testing_support.validators.validate_error_event_attributes import validate_error_event_attributes
2222
from testing_support.validators.validate_transaction_event_attributes import validate_transaction_event_attributes
2323
from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics
2424

25+
from newrelic.common.object_wrapper import wrap_function_wrapper
2526
from newrelic.api.application import application_instance
2627
from newrelic.api.background_task import BackgroundTask, background_task
2728
from newrelic.api.external_trace import ExternalTrace
@@ -36,6 +37,7 @@
3637
)
3738
from newrelic.api.web_transaction import WSGIWebTransaction
3839
from newrelic.api.wsgi_application import wsgi_application
40+
from newrelic.core.attribute import Attribute
3941

4042
distributed_trace_intrinsics = ["guid", "traceId", "priority", "sampled"]
4143
inbound_payload_intrinsics = [
@@ -409,3 +411,65 @@ def _test_inbound_dt_payload_acceptance():
409411
assert not result
410412

411413
_test_inbound_dt_payload_acceptance()
414+
415+
416+
@pytest.mark.parametrize(
417+
"sampled,remote_parent_sampled,remote_parent_not_sampled,expected_sampled,expected_priority,expected_adaptive_sampling_algo_called",
418+
(
419+
(True, 'default', 'default', None, None, True), # Uses sampling algo.
420+
(True, 'always_on', 'default', True, 2, False), # Always sampled.
421+
(True, 'always_off', 'default', False, 0, False), # Never sampled.
422+
(False, 'default', 'default', None, None, True), # Uses sampling algo.
423+
(False, 'always_on', 'default', None, None, True), # Uses sampling alog.
424+
(False, 'always_off', 'default', None, None, True), # Uses sampling algo.
425+
(True, 'default', 'always_on', None, None, True), # Uses sampling algo.
426+
(True, 'default', 'always_off', None, None, True), # Uses sampling algo.
427+
(False, 'default', 'always_on', True, 2, False), # Always sampled.
428+
(False, 'default', 'always_off', False, 0, False), # Never sampled.
429+
)
430+
)
431+
def test_distributed_trace_w3cparent_sampling_decision(sampled, remote_parent_sampled, remote_parent_not_sampled, expected_sampled, expected_priority, expected_adaptive_sampling_algo_called, wrap_sampling_algo_compute_sampled_and_priority):
432+
required_intrinsics = []
433+
if expected_sampled is not None:
434+
required_intrinsics.append(Attribute(name="sampled", value=expected_sampled, destinations=0b110))
435+
if expected_priority is not None:
436+
required_intrinsics.append(Attribute(name="priority", value=expected_priority, destinations=0b110))
437+
438+
wrap_function_wrapper("newrelic.api.transaction", "Transaction.sampling_algo_compute_sampled_and_priority", wrap_sampling_algo_compute_sampled_and_priority)
439+
440+
test_settings = _override_settings.copy()
441+
test_settings.update({
442+
"distributed_tracing.sampler.remote_parent_sampled": remote_parent_sampled,
443+
"distributed_tracing.sampler.remote_parent_not_sampled": remote_parent_not_sampled,
444+
"span_events.enabled": True,
445+
})
446+
447+
@override_application_settings(test_settings)
448+
@validate_attributes_complete("intrinsic", required_intrinsics)
449+
@background_task(name="test_distributed_trace_attributes")
450+
def _test():
451+
txn = current_transaction()
452+
453+
headers = {
454+
"traceparent": f"00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-{int(sampled):02x}",
455+
"tracestate": f"rojo=f06a0ba902b7,congo=t61rcWkgMzE",
456+
}
457+
accept_distributed_trace_headers(headers)
458+
459+
_test()
460+
461+
assert was_called == expected_adaptive_sampling_algo_called
462+
463+
464+
@pytest.fixture
465+
def wrap_sampling_algo_compute_sampled_and_priority():
466+
global was_called
467+
was_called = False
468+
469+
def _wrap_sampling_algo_compute_sampled_and_priority(wrapped, instance, args, kwargs):
470+
global was_called
471+
was_called = True
472+
return wrapped(*args, **kwargs)
473+
474+
return _wrap_sampling_algo_compute_sampled_and_priority
475+

0 commit comments

Comments
 (0)