Skip to content

Commit e6b1046

Browse files
hmstepanekmergify[bot]lrafeei
authored
Add w3c traceparent header support (#1448)
* Add config settings * Add traceparent logic * Log warning if value is not recognized * Move & rename function_not_called * Use existing function called validators * Fixup: lint log message * Fixup: validator import paths * Fixup: validator import path * Fixup: validator import path * Reformat w/ ruff * Use legacy bitnami for now (#1471) * Use legacy bitnami for now * Revert solr change * Revert zookeeper change * Add graphene-django instrumentation (#1451) * Add graphene-django instrumentation * Increase naming priority * Remove unused import * Add sychronous schema tests * Clean up test files * Remove commented out code * Megalinter fixes * Add operation & resolver tests * Refine tests * MegaLinter fixes * Suggested reviewer changes * Megalinter fixes * Fixup: paths * Fixup: reformat lint changes --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Lalleh Rafeei <[email protected]>
1 parent 39f101c commit e6b1046

File tree

19 files changed

+186
-63
lines changed

19 files changed

+186
-63
lines changed

newrelic/api/transaction.py

Lines changed: 25 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,36 @@ 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+
setting_path = "distributed_tracing.sampler.remote_parent_sampled"
1022+
config = self.settings.distributed_tracing.sampler.remote_parent_sampled
1023+
else: # self._traceparent_sampled is False.
1024+
setting_path = "distributed_tracing.sampler.remote_parent_not_sampled"
1025+
config = self.settings.distributed_tracing.sampler.remote_parent_not_sampled
1026+
1027+
if config == "always_on":
1028+
self._sampled = True
1029+
self._priority = 2.0
1030+
elif config == "always_off":
1031+
self._sampled = False
1032+
self._priority = 0
1033+
else:
1034+
if config != "default":
1035+
_logger.warning("%s=%s is not a recognized value. Using 'default' instead.", setting_path, config)
1036+
self.sampling_algo_compute_sampled_and_priority()
1037+
10171038
def _freeze_path(self):
10181039
if self._frozen_path is None:
10191040
self._name_priority = None
@@ -1348,6 +1369,7 @@ def accept_distributed_trace_headers(self, headers, transport_type="HTTP"):
13481369
else:
13491370
self._record_supportability("Supportability/TraceContext/TraceState/NoNrEntry")
13501371

1372+
self._traceparent_sampled = data.get("sa")
13511373
self._accept_distributed_trace_data(data, transport_type)
13521374
self._record_supportability("Supportability/TraceContext/Accept/Success")
13531375
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):

newrelic/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,8 @@ def _process_configuration(section):
404404
_process_setting(section, "ml_insights_events.enabled", "getboolean", None)
405405
_process_setting(section, "distributed_tracing.enabled", "getboolean", None)
406406
_process_setting(section, "distributed_tracing.exclude_newrelic_header", "getboolean", None)
407+
_process_setting(section, "distributed_tracing.sampler.remote_parent_sampled", "get", None)
408+
_process_setting(section, "distributed_tracing.sampler.remote_parent_not_sampled", "get", None)
407409
_process_setting(section, "span_events.enabled", "getboolean", None)
408410
_process_setting(section, "span_events.max_samples_stored", "getint", None)
409411
_process_setting(section, "span_events.attributes.enabled", "getboolean", None)

newrelic/core/config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,10 @@ class DistributedTracingSettings(Settings):
332332
pass
333333

334334

335+
class DistributedTracingSamplerSettings(Settings):
336+
pass
337+
338+
335339
class ServerlessModeSettings(Settings):
336340
pass
337341

@@ -501,6 +505,7 @@ class EventHarvestConfigHarvestLimitSettings(Settings):
501505
_settings.datastore_tracer.instance_reporting = DatastoreTracerInstanceReportingSettings()
502506
_settings.debug = DebugSettings()
503507
_settings.distributed_tracing = DistributedTracingSettings()
508+
_settings.distributed_tracing.sampler = DistributedTracingSamplerSettings()
504509
_settings.error_collector = ErrorCollectorSettings()
505510
_settings.error_collector.attributes = ErrorCollectorAttributesSettings()
506511
_settings.event_harvest_config = EventHarvestConfigSettings()
@@ -828,6 +833,12 @@ def default_otlp_host(host):
828833
_settings.ml_insights_events.enabled = False
829834

830835
_settings.distributed_tracing.enabled = _environ_as_bool("NEW_RELIC_DISTRIBUTED_TRACING_ENABLED", default=True)
836+
_settings.distributed_tracing.sampler.remote_parent_sampled = os.environ.get(
837+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED", "default"
838+
)
839+
_settings.distributed_tracing.sampler.remote_parent_not_sampled = os.environ.get(
840+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED", "default"
841+
)
831842
_settings.distributed_tracing.exclude_newrelic_header = False
832843
_settings.span_events.enabled = _environ_as_bool("NEW_RELIC_SPAN_EVENTS_ENABLED", default=True)
833844
_settings.span_events.attributes.enabled = True

tests/agent_features/test_async_context_propagation.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
# limitations under the License.
1414

1515
import pytest
16-
from testing_support.fixtures import function_not_called, override_generic_settings
16+
from testing_support.fixtures import override_generic_settings
17+
from testing_support.validators.validate_function_not_called import validate_function_not_called
1718
from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics
1819

1920
from newrelic.api.application import application_instance as application
@@ -128,7 +129,7 @@ def handle_exception(loop, context):
128129

129130

130131
@override_generic_settings(global_settings(), {"enabled": False})
131-
@function_not_called("newrelic.core.stats_engine", "StatsEngine.record_transaction")
132+
@validate_function_not_called("newrelic.core.stats_engine", "StatsEngine.record_transaction")
132133
def test_nr_disabled(event_loop):
133134
import asyncio
134135

tests/agent_features/test_custom_events.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
import time
1616

1717
import pytest
18-
from testing_support.fixtures import function_not_called, override_application_settings, reset_core_stats_engine
18+
from testing_support.fixtures import override_application_settings, reset_core_stats_engine
1919
from testing_support.validators.validate_custom_event import (
2020
validate_custom_event_count,
2121
validate_custom_event_in_application_stats_engine,
2222
)
2323
from testing_support.validators.validate_custom_events import validate_custom_events
24+
from testing_support.validators.validate_function_not_called import validate_function_not_called
2425

2526
from newrelic.api.application import application_instance as application
2627
from newrelic.api.background_task import background_task
@@ -214,14 +215,14 @@ def test_custom_event_settings_check_custom_insights_enabled():
214215

215216

216217
@override_application_settings({"custom_insights_events.enabled": False})
217-
@function_not_called("newrelic.api.transaction", "create_custom_event")
218+
@validate_function_not_called("newrelic.api.transaction", "create_custom_event")
218219
@background_task()
219220
def test_transaction_create_custom_event_not_called():
220221
record_custom_event("FooEvent", _user_params)
221222

222223

223224
@override_application_settings({"custom_insights_events.enabled": False})
224-
@function_not_called("newrelic.core.application", "create_custom_event")
225+
@validate_function_not_called("newrelic.core.application", "create_custom_event")
225226
@background_task()
226227
def test_application_create_custom_event_not_called():
227228
app = application()

tests/agent_features/test_distributed_tracing.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
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
22+
from testing_support.validators.validate_function_called import validate_function_called
23+
from testing_support.validators.validate_function_not_called import validate_function_not_called
2224
from testing_support.validators.validate_transaction_event_attributes import validate_transaction_event_attributes
2325
from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics
2426

@@ -36,6 +38,7 @@
3638
)
3739
from newrelic.api.web_transaction import WSGIWebTransaction
3840
from newrelic.api.wsgi_application import wsgi_application
41+
from newrelic.core.attribute import Attribute
3942

4043
distributed_trace_intrinsics = ["guid", "traceId", "priority", "sampled"]
4144
inbound_payload_intrinsics = [
@@ -410,3 +413,65 @@ def _test_inbound_dt_payload_acceptance():
410413
assert not result
411414

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

tests/agent_features/test_ml_events.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
from importlib import reload
1717

1818
import pytest
19-
from testing_support.fixtures import function_not_called, override_application_settings, reset_core_stats_engine
19+
from testing_support.fixtures import override_application_settings, reset_core_stats_engine
20+
from testing_support.validators.validate_function_not_called import validate_function_not_called
2021
from testing_support.validators.validate_ml_event_count import validate_ml_event_count
2122
from testing_support.validators.validate_ml_event_payload import validate_ml_event_payload
2223
from testing_support.validators.validate_ml_events import validate_ml_events
@@ -331,15 +332,15 @@ def test_ml_event_settings_check_ml_insights_disabled():
331332

332333
@override_application_settings({"ml_insights_events.enabled": False})
333334
@reset_core_stats_engine()
334-
@function_not_called("newrelic.api.transaction", "create_custom_event")
335+
@validate_function_not_called("newrelic.api.transaction", "create_custom_event")
335336
@background_task()
336337
def test_transaction_create_ml_event_not_called():
337338
record_ml_event("FooEvent", {"foo": "bar"})
338339

339340

340341
@override_application_settings({"ml_insights_events.enabled": False})
341342
@reset_core_stats_engine()
342-
@function_not_called("newrelic.core.application", "create_custom_event")
343+
@validate_function_not_called("newrelic.core.application", "create_custom_event")
343344
@background_task()
344345
def test_application_create_ml_event_not_called():
345346
app = application()

tests/agent_features/test_span_events.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
import sys
1616

1717
import pytest
18-
from testing_support.fixtures import dt_enabled, function_not_called, override_application_settings
18+
from testing_support.fixtures import dt_enabled, override_application_settings
19+
from testing_support.validators.validate_function_not_called import validate_function_not_called
1920
from testing_support.validators.validate_span_events import validate_span_events
2021
from testing_support.validators.validate_transaction_event_attributes import validate_transaction_event_attributes
2122
from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics
@@ -415,7 +416,7 @@ def _test():
415416
pass
416417

417418
if not spans_expected:
418-
_test = function_not_called("newrelic.core.attribute", "resolve_agent_attributes")(_test)
419+
_test = validate_function_not_called("newrelic.core.attribute", "resolve_agent_attributes")(_test)
419420

420421
_test()
421422

tests/agent_unittests/test_harvest_loop.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
from pathlib import Path
1919

2020
import pytest
21-
from testing_support.fixtures import failing_endpoint, function_not_called, override_generic_settings
21+
from testing_support.fixtures import failing_endpoint, override_generic_settings
22+
from testing_support.validators.validate_function_not_called import validate_function_not_called
2223

2324
from newrelic.common.agent_http import DeveloperModeClient
2425
from newrelic.common.object_wrapper import function_wrapper, transient_function_wrapper
@@ -646,7 +647,7 @@ def test_serverless_mode_adaptive_sampling(time_to_next_reset, computed_count, c
646647
assert app.adaptive_sampler.computed_count_last == computed_count_last
647648

648649

649-
@function_not_called("newrelic.core.adaptive_sampler", "AdaptiveSampler._reset")
650+
@validate_function_not_called("newrelic.core.adaptive_sampler", "AdaptiveSampler._reset")
650651
@override_generic_settings(settings, {"developer_mode": True})
651652
def test_compute_sampled_no_reset():
652653
app = Application("Python Agent Test (Harvest Loop)")

0 commit comments

Comments
 (0)