Skip to content

Commit eaa838d

Browse files
committed
Add support 4 partial granularity tracing
1 parent 01b71d0 commit eaa838d

18 files changed

+1050
-82
lines changed

newrelic/api/transaction.py

Lines changed: 117 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,8 @@ def __init__(self, application, enabled=None, source=None):
285285
self.tracestate = ""
286286
self._priority = None
287287
self._sampled = None
288-
self._traceparent_sampled = None
288+
# Remote parent sampled is set from the W3C parent header or the Newrelic header if no W3C parent header is present.
289+
self._remote_parent_sampled = None
289290

290291
self._distributed_trace_state = 0
291292

@@ -569,7 +570,7 @@ def __exit__(self, exc, value, tb):
569570
if self._settings.distributed_tracing.enabled:
570571
# Sampled and priority need to be computed at the end of the
571572
# transaction when distributed tracing or span events are enabled.
572-
self._compute_sampled_and_priority()
573+
self._make_sampling_decision()
573574

574575
self._cached_path._name = self.path
575576
agent_attributes = self.agent_attributes
@@ -636,6 +637,7 @@ def __exit__(self, exc, value, tb):
636637
trace_id=self.trace_id,
637638
loop_time=self._loop_time,
638639
root=root_node,
640+
partial_granularity_sampled=hasattr(self, "partial_granularity_sampled"),
639641
)
640642

641643
# Clear settings as we are all done and don't need it
@@ -1004,35 +1006,120 @@ def _update_agent_attributes(self):
10041006
def user_attributes(self):
10051007
return create_attributes(self._custom_params, DST_ALL, self.attribute_filter)
10061008

1007-
def sampling_algo_compute_sampled_and_priority(self):
1008-
if self._priority is None:
1009+
def sampling_algo_compute_sampled_and_priority(self, priority, sampled):
1010+
# self._priority and self._sampled are set when parsing the W3C tracestate
1011+
# or newrelic DT headers and may be overridden in _make_sampling_decision
1012+
# based on the configuration. The only time they are set in here is when the
1013+
# sampling decision must be made by the adaptive sampling algorithm.
1014+
if priority is None:
10091015
# Truncate priority field to 6 digits past the decimal.
1010-
self._priority = float(f"{random.random():.6f}") # noqa: S311
1011-
if self._sampled is None:
1012-
self._sampled = self._application.compute_sampled()
1013-
if self._sampled:
1014-
self._priority += 1
1015-
1016-
def _compute_sampled_and_priority(self):
1017-
if self._traceparent_sampled is None:
1016+
priority = float(f"{random.random():.6f}") # noqa: S311
1017+
if sampled is None:
1018+
_logger.debug("No trusted account id found. Sampling decision will be made by adaptive sampling algorithm.")
1019+
sampled = self._application.compute_sampled()
1020+
if sampled:
1021+
priority += 1
1022+
return priority, sampled
1023+
1024+
def _compute_sampled_and_priority(
1025+
self,
1026+
priority,
1027+
sampled,
1028+
remote_parent_sampled_path,
1029+
remote_parent_sampled_setting,
1030+
remote_parent_not_sampled_path,
1031+
remote_parent_not_sampled_setting,
1032+
):
1033+
if self._remote_parent_sampled is None:
10181034
config = "default" # Use sampling algo.
1019-
elif self._traceparent_sampled:
1020-
setting_path = "distributed_tracing.sampler.remote_parent_sampled"
1021-
config = self.settings.distributed_tracing.sampler.remote_parent_sampled
1022-
else: # self._traceparent_sampled is False.
1023-
setting_path = "distributed_tracing.sampler.remote_parent_not_sampled"
1024-
config = self.settings.distributed_tracing.sampler.remote_parent_not_sampled
1025-
1035+
_logger.debug("Sampling decision made based on no remote parent sampling decision present.")
1036+
elif self._remote_parent_sampled:
1037+
setting_path = remote_parent_sampled_path
1038+
config = remote_parent_sampled_setting
1039+
_logger.debug(
1040+
"Sampling decision made based on remote_parent_sampled=%s and %s=%s.",
1041+
self._remote_parent_sampled,
1042+
setting_path,
1043+
config,
1044+
)
1045+
else: # self._remote_parent_sampled is False.
1046+
setting_path = remote_parent_not_sampled_path
1047+
config = remote_parent_not_sampled_setting
1048+
_logger.debug(
1049+
"Sampling decision made based on remote_parent_sampled=%s and %s=%s.",
1050+
self._remote_parent_sampled,
1051+
setting_path,
1052+
config,
1053+
)
10261054
if config == "always_on":
1027-
self._sampled = True
1028-
self._priority = 2.0
1055+
sampled = True
1056+
priority = 2.0
10291057
elif config == "always_off":
1030-
self._sampled = False
1031-
self._priority = 0
1058+
sampled = False
1059+
priority = 0
10321060
else:
1033-
if config != "default":
1061+
if config not in ("default", "adaptive"):
10341062
_logger.warning("%s=%s is not a recognized value. Using 'default' instead.", setting_path, config)
1035-
self.sampling_algo_compute_sampled_and_priority()
1063+
1064+
_logger.debug(
1065+
"Let adaptive sampler algorithm decide based on sampled=%s and priority=%s.", sampled, priority
1066+
)
1067+
priority, sampled = self.sampling_algo_compute_sampled_and_priority(priority, sampled)
1068+
return priority, sampled
1069+
1070+
def _make_sampling_decision(self):
1071+
# The sampling decision is computed each time a DT header is generated for exit spans as it is needed
1072+
# to send the DT headers. Don't recompute the sampling decision multiple times as it is expensive.
1073+
if hasattr(self, "_sampling_decision_made"):
1074+
return
1075+
priority = self._priority
1076+
sampled = self._sampled
1077+
# Compute sampling decision for full granularity.
1078+
if self.settings.distributed_tracing.sampler.full_granularity.enabled:
1079+
_logger.debug(
1080+
"Full granularity tracing is enabled. Asking if full granularity wants to sample. priority=%s, sampled=%s",
1081+
priority,
1082+
sampled,
1083+
)
1084+
computed_priority, computed_sampled = self._compute_sampled_and_priority(
1085+
priority,
1086+
sampled,
1087+
remote_parent_sampled_path="distributed_tracing.sampler.full_granularity.remote_parent_sampled",
1088+
remote_parent_sampled_setting=self.settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled,
1089+
remote_parent_not_sampled_path="distributed_tracing.sampler.full_granularity.remote_parent_not_sampled",
1090+
remote_parent_not_sampled_setting=self.settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled,
1091+
)
1092+
_logger.debug("Full granularity sampling decision was %s with priority=%s.", sampled, priority)
1093+
if computed_sampled or not self.settings.distributed_tracing.sampler.partial_granularity.enabled:
1094+
self._priority = computed_priority
1095+
self._sampled = computed_sampled
1096+
self._sampling_decision_made = True
1097+
return
1098+
1099+
# If full granularity is not going to sample, let partial granularity decide.
1100+
if self.settings.distributed_tracing.sampler.partial_granularity.enabled:
1101+
_logger.debug("Partial granularity tracing is enabled. Asking if partial granularity wants to sample.")
1102+
self._priority, self._sampled = self._compute_sampled_and_priority(
1103+
priority,
1104+
sampled,
1105+
remote_parent_sampled_path="distributed_tracing.sampler.partial_granularity.remote_parent_sampled",
1106+
remote_parent_sampled_setting=self.settings.distributed_tracing.sampler.partial_granularity.remote_parent_sampled,
1107+
remote_parent_not_sampled_path="distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled",
1108+
remote_parent_not_sampled_setting=self.settings.distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled,
1109+
)
1110+
_logger.debug(
1111+
"Partial granularity sampling decision was %s with priority=%s.", self._sampled, self._priority
1112+
)
1113+
self._sampling_decision_made = True
1114+
if self._sampled:
1115+
self.partial_granularity_sampled = True
1116+
return
1117+
1118+
# This is only reachable if both full and partial granularity tracing are off.
1119+
# Set priority=0 and do not sample. This enables DT headers to still be sent
1120+
# even if the trace is never sampled.
1121+
self._priority = 0
1122+
self._sampled = False
10361123

10371124
def _freeze_path(self):
10381125
if self._frozen_path is None:
@@ -1101,7 +1188,7 @@ def _create_distributed_trace_data(self):
11011188
if not (account_id and application_id and trusted_account_key and settings.distributed_tracing.enabled):
11021189
return
11031190

1104-
self._compute_sampled_and_priority()
1191+
self._make_sampling_decision()
11051192
data = {
11061193
"ty": "App",
11071194
"ac": account_id,
@@ -1204,7 +1291,7 @@ def _accept_distributed_trace_payload(self, payload, transport_type="HTTP"):
12041291
if not any(k in data for k in ("id", "tx")):
12051292
self._record_supportability("Supportability/DistributedTrace/AcceptPayload/ParseException")
12061293
return False
1207-
1294+
self._remote_parent_sampled = data.get("sa")
12081295
settings = self._settings
12091296
account_id = data.get("ac")
12101297
trusted_account_key = settings.trusted_account_key or (
@@ -1254,10 +1341,8 @@ def _accept_distributed_trace_data(self, data, transport_type):
12541341

12551342
self._trace_id = data.get("tr")
12561343

1257-
priority = data.get("pr")
1258-
if priority is not None:
1259-
self._priority = priority
1260-
self._sampled = data.get("sa")
1344+
self._priority = data.get("pr")
1345+
self._sampled = data.get("sa")
12611346

12621347
if "ti" in data:
12631348
transport_start = data["ti"] / 1000.0
@@ -1297,6 +1382,7 @@ def accept_distributed_trace_headers(self, headers, transport_type="HTTP"):
12971382
try:
12981383
traceparent = ensure_str(traceparent).strip()
12991384
data = W3CTraceParent.decode(traceparent)
1385+
self._remote_parent_sampled = data.pop("sa", None)
13001386
except:
13011387
data = None
13021388

@@ -1332,7 +1418,6 @@ def accept_distributed_trace_headers(self, headers, transport_type="HTTP"):
13321418
else:
13331419
self._record_supportability("Supportability/TraceContext/TraceState/NoNrEntry")
13341420

1335-
self._traceparent_sampled = data.get("sa")
13361421
self._accept_distributed_trace_data(data, transport_type)
13371422
self._record_supportability("Supportability/TraceContext/Accept/Success")
13381423
return True

newrelic/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,8 +404,16 @@ 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.adaptive_sampling_target", "getint", None)
407408
_process_setting(section, "distributed_tracing.sampler.remote_parent_sampled", "get", None)
408409
_process_setting(section, "distributed_tracing.sampler.remote_parent_not_sampled", "get", None)
410+
_process_setting(section, "distributed_tracing.sampler.full_granularity.enabled", "getboolean", None)
411+
_process_setting(section, "distributed_tracing.sampler.full_granularity.remote_parent_sampled", "get", None)
412+
_process_setting(section, "distributed_tracing.sampler.full_granularity.remote_parent_not_sampled", "get", None)
413+
_process_setting(section, "distributed_tracing.sampler.partial_granularity.enabled", "getboolean", None)
414+
_process_setting(section, "distributed_tracing.sampler.partial_granularity.type", "get", None)
415+
_process_setting(section, "distributed_tracing.sampler.partial_granularity.remote_parent_sampled", "get", None)
416+
_process_setting(section, "distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled", "get", None)
409417
_process_setting(section, "span_events.enabled", "getboolean", None)
410418
_process_setting(section, "span_events.max_samples_stored", "getint", None)
411419
_process_setting(section, "span_events.attributes.enabled", "getboolean", None)

newrelic/core/agent_protocol.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,9 @@ def _connect_payload(app_name, linked_applications, environment, settings):
297297
connect_settings["browser_monitoring.loader"] = settings["browser_monitoring.loader"]
298298
connect_settings["browser_monitoring.debug"] = settings["browser_monitoring.debug"]
299299
connect_settings["ai_monitoring.enabled"] = settings["ai_monitoring.enabled"]
300+
connect_settings["distributed_tracing.sampler.adaptive_sampling_target"] = settings[
301+
"distributed_tracing.sampler.adaptive_sampling_target"
302+
]
300303

301304
security_settings = {}
302305
security_settings["capture_params"] = settings["capture_params"]

newrelic/core/attribute.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,23 @@
108108
"zeebe.client.resourceFile",
109109
}
110110

111+
SPAN_ENTITY_RELATIONSHIP_ATTRIBUTES = {
112+
"cloud.account.id",
113+
"cloud.platform",
114+
"cloud.region",
115+
"cloud.resource_id",
116+
"db.instance",
117+
"db.system",
118+
"http.url",
119+
"messaging.destination.name",
120+
"messaging.system",
121+
"peer.hostname",
122+
"server.address",
123+
"server.port",
124+
"span.kind",
125+
}
126+
127+
111128
MAX_NUM_USER_ATTRIBUTES = 128
112129
MAX_ATTRIBUTE_LENGTH = 255
113130
MAX_NUM_ML_USER_ATTRIBUTES = 64

newrelic/core/config.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,14 @@ class DistributedTracingSamplerSettings(Settings):
337337
pass
338338

339339

340+
class DistributedTracingSamplerFullGranularitySettings(Settings):
341+
pass
342+
343+
344+
class DistributedTracingSamplerPartialGranularitySettings(Settings):
345+
pass
346+
347+
340348
class ServerlessModeSettings(Settings):
341349
pass
342350

@@ -507,6 +515,8 @@ class EventHarvestConfigHarvestLimitSettings(Settings):
507515
_settings.debug = DebugSettings()
508516
_settings.distributed_tracing = DistributedTracingSettings()
509517
_settings.distributed_tracing.sampler = DistributedTracingSamplerSettings()
518+
_settings.distributed_tracing.sampler.full_granularity = DistributedTracingSamplerFullGranularitySettings()
519+
_settings.distributed_tracing.sampler.partial_granularity = DistributedTracingSamplerPartialGranularitySettings()
510520
_settings.error_collector = ErrorCollectorSettings()
511521
_settings.error_collector.attributes = ErrorCollectorAttributesSettings()
512522
_settings.event_harvest_config = EventHarvestConfigSettings()
@@ -836,12 +846,36 @@ def default_otlp_host(host):
836846
_settings.ml_insights_events.enabled = False
837847

838848
_settings.distributed_tracing.enabled = _environ_as_bool("NEW_RELIC_DISTRIBUTED_TRACING_ENABLED", default=True)
849+
_settings.distributed_tracing.sampler.adaptive_sampling_target = _environ_as_int(
850+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ADAPTIVE_SAMPLING_TARGET", default=10
851+
)
839852
_settings.distributed_tracing.sampler.remote_parent_sampled = os.environ.get(
840853
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED", "default"
841854
)
842855
_settings.distributed_tracing.sampler.remote_parent_not_sampled = os.environ.get(
843856
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED", "default"
844857
)
858+
_settings.distributed_tracing.sampler.full_granularity.enabled = _environ_as_bool(
859+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_ENABLED", default=True
860+
)
861+
_settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled = os.environ.get(
862+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_SAMPLED", None
863+
)
864+
_settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled = os.environ.get(
865+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED", None
866+
)
867+
_settings.distributed_tracing.sampler.partial_granularity.enabled = _environ_as_bool(
868+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_ENABLED", default=False
869+
)
870+
_settings.distributed_tracing.sampler.partial_granularity.type = os.environ.get(
871+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_TYPE", "essential"
872+
)
873+
_settings.distributed_tracing.sampler.partial_granularity.remote_parent_sampled = os.environ.get(
874+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_SAMPLED", "default"
875+
)
876+
_settings.distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled = os.environ.get(
877+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED", "default"
878+
)
845879
_settings.distributed_tracing.exclude_newrelic_header = False
846880
_settings.span_events.enabled = _environ_as_bool("NEW_RELIC_SPAN_EVENTS_ENABLED", default=True)
847881
_settings.event_harvest_config.harvest_limits.span_event_data = _environ_as_int(
@@ -1368,9 +1402,34 @@ def finalize_application_settings(server_side_config=None, settings=_settings):
13681402

13691403
application_settings.attribute_filter = AttributeFilter(flatten_settings(application_settings))
13701404

1405+
simplify_distributed_tracing_sampler_granularity_settings(application_settings)
1406+
13711407
return application_settings
13721408

13731409

1410+
def simplify_distributed_tracing_sampler_granularity_settings(settings):
1411+
# Full granularity settings may appear under:
1412+
# * `distributed_tracing.sampler`
1413+
# * `distributed_tracing.sampler.full_granularity`
1414+
# The `distributed_tracing.sampler.full_granularity` path takes precedence.
1415+
# To simplify logic in the code that uses these settings, store the values that
1416+
# should be used at the `distributed_tracing.sampler.full_granularity` path.
1417+
if not settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled:
1418+
settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled = (
1419+
settings.distributed_tracing.sampler.remote_parent_sampled
1420+
)
1421+
if not settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled:
1422+
settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled = (
1423+
settings.distributed_tracing.sampler.remote_parent_not_sampled
1424+
)
1425+
# Partial granularity tracing is not available in infinite tracing mode.
1426+
if settings.infinite_tracing.enabled and settings.distributed_tracing.sampler.partial_granularity.enabled:
1427+
_logger.warning(
1428+
"Improper configuration. Infinite tracing cannot be enabled at the same time as partial granularity tracing. Setting distributed_tracing.sampler.partial_granularity.enabled=False."
1429+
)
1430+
settings.distributed_tracing.sampler.partial_granularity.enabled = False
1431+
1432+
13741433
def _remove_ignored_configs(server_settings):
13751434
if not server_settings.get("agent_config"):
13761435
return server_settings

newrelic/core/data_collector.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,11 @@ def send_ml_events(self, sampling_info, custom_event_data):
117117

118118
def send_span_events(self, sampling_info, span_event_data):
119119
"""Called to submit sample set for span events."""
120-
120+
# TODO: remove this later after list types are suported.
121+
for span_event in span_event_data:
122+
ids = span_event[1].get("nr.ids")
123+
if ids:
124+
span_event[1]["nr.ids"] = ",".join(ids)
121125
payload = (self.agent_run_id, sampling_info, span_event_data)
122126
return self._protocol.send("span_event_data", payload)
123127

0 commit comments

Comments
 (0)