Skip to content

Commit 64e58b4

Browse files
committed
Add support 4 partial granularity tracing
1 parent e3f2de1 commit 64e58b4

File tree

13 files changed

+232
-56
lines changed

13 files changed

+232
-56
lines changed

newrelic/api/transaction.py

Lines changed: 83 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ 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+
self._remote_parent_sampled = None
289289

290290
self._distributed_trace_state = 0
291291

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

574574
self._cached_path._name = self.path
575575
agent_attributes = self.agent_attributes
@@ -636,6 +636,7 @@ def __exit__(self, exc, value, tb):
636636
trace_id=self.trace_id,
637637
loop_time=self._loop_time,
638638
root=root_node,
639+
partial_granularity_sampled=hasattr(self, "partial_granularity_sampled"),
639640
)
640641

641642
# Clear settings as we are all done and don't need it
@@ -1004,35 +1005,87 @@ 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 sampling_algo_compute_sampled_and_priority(self):
1008-
if self._priority is None:
1008+
def sampling_algo_compute_sampled_and_priority(self, priority, sampled):
1009+
# self._priority and self._sampled are set when parsing the W3C tracestate
1010+
# or newrelic DT headers and may be overridden in _make_sampling_decision
1011+
# based on the configuration. The only time they are set in here is when the
1012+
# sampling decision must be made by the adaptive sampling algorithm.
1013+
if priority is None:
10091014
# 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:
1015+
priority = float(f"{random.random():.6f}") # noqa: S311
1016+
if sampled is None:
1017+
_logger.debug("No trusted account id found. Sampling decision will be made by adaptive sampling algorithm.")
1018+
sampled = self._application.compute_sampled()
1019+
if sampled:
1020+
priority += 1
1021+
return priority, sampled
1022+
1023+
def _compute_sampled_and_priority(self, priority, sampled, remote_parent_sampled_path, remote_parent_sampled_setting, remote_parent_not_sampled_path, remote_parent_not_sampled_setting):
1024+
if self._remote_parent_sampled is None:
10181025
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-
1026+
_logger.debug("Sampling decision made based on no remote parent sampling decision present.")
1027+
elif self._remote_parent_sampled:
1028+
setting_path = remote_parent_sampled_path
1029+
config = remote_parent_sampled_setting
1030+
_logger.debug("Sampling decision made based on remote_parent_sampled=%s and %s=%s.", self._remote_parent_sampled, setting_path, config)
1031+
else: # self._remote_parent_sampled is False.
1032+
setting_path = remote_parent_not_sampled_path
1033+
config = remote_parent_not_sampled_setting
1034+
_logger.debug("Sampling decision made based on remote_parent_sampled=%s and %s=%s.", self._remote_parent_sampled, setting_path, config)
10261035
if config == "always_on":
1027-
self._sampled = True
1028-
self._priority = 2.0
1036+
sampled = True
1037+
priority = 2.0
10291038
elif config == "always_off":
1030-
self._sampled = False
1031-
self._priority = 0
1039+
sampled = False
1040+
priority = 0
10321041
else:
1033-
if config != "default":
1042+
if config not in ("default", "adaptive"):
10341043
_logger.warning("%s=%s is not a recognized value. Using 'default' instead.", setting_path, config)
1035-
self.sampling_algo_compute_sampled_and_priority()
1044+
1045+
_logger.debug("Let adaptive sampler algorithm decide based on sampled=%s and priority=%s.", sampled, priority)
1046+
priority, sampled = self.sampling_algo_compute_sampled_and_priority(priority, sampled)
1047+
return priority, sampled
1048+
1049+
def _make_sampling_decision(self):
1050+
# The sampling decision is computed each time a DT header is generated for exit spans as it is needed
1051+
# to send the DT headers. Don't recompute the sampling decision multiple times as it is expensive.
1052+
if hasattr(self, "_sampling_decision_made"):
1053+
return
1054+
priority = self._priority
1055+
sampled = self._sampled
1056+
# Compute sampling decision for full granularity.
1057+
if self.settings.distributed_tracing.sampler.full_granularity.enabled:
1058+
_logger.debug("Full granularity tracing is enabled. Asking if full granularity wants to sample. priority=%s, sampled=%s", priority, sampled)
1059+
computed_priority, computed_sampled = self._compute_sampled_and_priority(
1060+
priority,
1061+
sampled,
1062+
remote_parent_sampled_path = "distributed_tracing.sampler.full_granularity.remote_parent_sampled",
1063+
remote_parent_sampled_setting = self.settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled,
1064+
remote_parent_not_sampled_path = "distributed_tracing.sampler.full_granularity.remote_parent_not_sampled",
1065+
remote_parent_not_sampled_setting = self.settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled,
1066+
)
1067+
_logger.debug("Full granularity sampling decision was %s with priority=%s.", sampled, priority)
1068+
if computed_sampled:
1069+
self._priority = computed_priority
1070+
self._sampled = computed_sampled
1071+
self._sampling_decision_made = True
1072+
return
1073+
1074+
# If full granularity is not going to sample, let partial granularity decide.
1075+
if self.settings.distributed_tracing.sampler.partial_granularity.enabled:
1076+
_logger.debug("Partial granularity tracing is enabled. Asking if partial granularity wants to sample.")
1077+
self._priority, self._sampled = self._compute_sampled_and_priority(
1078+
priority,
1079+
sampled,
1080+
remote_parent_sampled_path = "distributed_tracing.sampler.partial_granularity.remote_parent_sampled",
1081+
remote_parent_sampled_setting = self.settings.distributed_tracing.sampler.partial_granularity.remote_parent_sampled,
1082+
remote_parent_not_sampled_path = "distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled",
1083+
remote_parent_not_sampled_setting = self.settings.distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled,
1084+
)
1085+
_logger.debug("Partial granularity sampling decision was %s with priority=%s.", self._sampled, self._priority)
1086+
self._sampling_decision_made = True
1087+
if self._sampled:
1088+
self.partial_granularity_sampled = True
10361089

10371090
def _freeze_path(self):
10381091
if self._frozen_path is None:
@@ -1101,7 +1154,7 @@ def _create_distributed_trace_data(self):
11011154
if not (account_id and application_id and trusted_account_key and settings.distributed_tracing.enabled):
11021155
return
11031156

1104-
self._compute_sampled_and_priority()
1157+
self._make_sampling_decision()
11051158
data = {
11061159
"ty": "App",
11071160
"ac": account_id,
@@ -1184,6 +1237,7 @@ def _accept_distributed_trace_payload(self, payload, transport_type="HTTP"):
11841237
return False
11851238

11861239
try:
1240+
self._remote_parent_sampled = payload.get("sa")
11871241
version = payload.get("v")
11881242
major_version = version and int(version[0])
11891243

@@ -1254,10 +1308,8 @@ def _accept_distributed_trace_data(self, data, transport_type):
12541308

12551309
self._trace_id = data.get("tr")
12561310

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

12621314
if "ti" in data:
12631315
transport_start = data["ti"] / 1000.0
@@ -1297,6 +1349,7 @@ def accept_distributed_trace_headers(self, headers, transport_type="HTTP"):
12971349
try:
12981350
traceparent = ensure_str(traceparent).strip()
12991351
data = W3CTraceParent.decode(traceparent)
1352+
self._remote_parent_sampled = data.get("sa")
13001353
except:
13011354
data = None
13021355

@@ -1332,7 +1385,6 @@ def accept_distributed_trace_headers(self, headers, transport_type="HTTP"):
13321385
else:
13331386
self._record_supportability("Supportability/TraceContext/TraceState/NoNrEntry")
13341387

1335-
self._traceparent_sampled = data.get("sa")
13361388
self._accept_distributed_trace_data(data, transport_type)
13371389
self._record_supportability("Supportability/TraceContext/Accept/Success")
13381390
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ 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["distributed_tracing.sampler.adaptive_sampling_target"]
300301

301302
security_settings = {}
302303
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: 45 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()
@@ -837,12 +847,32 @@ def default_otlp_host(host):
837847
_settings.ml_insights_events.enabled = False
838848

839849
_settings.distributed_tracing.enabled = _environ_as_bool("NEW_RELIC_DISTRIBUTED_TRACING_ENABLED", default=True)
850+
_settings.distributed_tracing.sampler.adaptive_sampling_target = _environ_as_int(
851+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ADAPTIVE_SAMPLING_TARGET", default=10
852+
)
840853
_settings.distributed_tracing.sampler.remote_parent_sampled = os.environ.get(
841854
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED", "default"
842855
)
843856
_settings.distributed_tracing.sampler.remote_parent_not_sampled = os.environ.get(
844857
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED", "default"
845858
)
859+
_settings.distributed_tracing.sampler.full_granularity.enabled = _environ_as_bool("NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_ENABLED", default=True)
860+
_settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled = os.environ.get(
861+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_SAMPLED", None
862+
)
863+
_settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled = os.environ.get(
864+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED", None
865+
)
866+
_settings.distributed_tracing.sampler.partial_granularity.enabled = _environ_as_bool("NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_ENABLED", default=False)
867+
_settings.distributed_tracing.sampler.partial_granularity.type = os.environ.get(
868+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_TYPE", "essential"
869+
)
870+
_settings.distributed_tracing.sampler.partial_granularity.remote_parent_sampled = os.environ.get(
871+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_SAMPLED", "default"
872+
)
873+
_settings.distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled = os.environ.get(
874+
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED", "default"
875+
)
846876
_settings.distributed_tracing.exclude_newrelic_header = False
847877
_settings.span_events.enabled = _environ_as_bool("NEW_RELIC_SPAN_EVENTS_ENABLED", default=True)
848878
_settings.event_harvest_config.harvest_limits.span_event_data = _environ_as_int(
@@ -1369,9 +1399,24 @@ def finalize_application_settings(server_side_config=None, settings=_settings):
13691399

13701400
application_settings.attribute_filter = AttributeFilter(flatten_settings(application_settings))
13711401

1402+
simplify_distributed_tracing_sampler_granularity_settings(application_settings)
1403+
13721404
return application_settings
13731405

13741406

1407+
def simplify_distributed_tracing_sampler_granularity_settings(settings):
1408+
# Full granularity settings may appear under:
1409+
# * `distributed_tracing.sampler`
1410+
# * `distributed_tracing.sampler.full_granularity`
1411+
# The `distributed_tracing.sampler.full_granularity` path takes precedence.
1412+
# To simplify logic in the code that uses these settings, store the values that
1413+
# should be used at the `distributed_tracing.sampler.full_granularity` path.
1414+
if not settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled:
1415+
settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled = settings.distributed_tracing.sampler.remote_parent_sampled
1416+
if not settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled:
1417+
settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled = settings.distributed_tracing.sampler.remote_parent_not_sampled
1418+
1419+
13751420
def _remove_ignored_configs(server_settings):
13761421
if not server_settings.get("agent_config"):
13771422
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

newrelic/core/database_node.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ def trace_node(self, stats, root, connections):
279279
start_time=start_time, end_time=end_time, name=name, params=params, children=children, label=None
280280
)
281281

282-
def span_event(self, *args, **kwargs):
282+
def span_event(self, settings, base_attrs=None, parent_guid=None, attr_class=dict, partial_granularity_sampled=False, ct_exit_spans=None):
283283
sql = self.formatted
284284

285285
if sql:
@@ -288,4 +288,4 @@ def span_event(self, *args, **kwargs):
288288

289289
self.agent_attributes["db.statement"] = sql
290290

291-
return super().span_event(*args, **kwargs)
291+
return super().span_event(settings, base_attrs=base_attrs, parent_guid=parent_guid, attr_class=attr_class, partial_granularity_sampled=partial_granularity_sampled, ct_exit_spans=ct_exit_spans)

newrelic/core/external_node.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def trace_node(self, stats, root, connections):
169169
start_time=start_time, end_time=end_time, name=name, params=params, children=children, label=None
170170
)
171171

172-
def span_event(self, settings, base_attrs=None, parent_guid=None, attr_class=dict):
172+
def span_event(self, settings, base_attrs=None, parent_guid=None, attr_class=dict, partial_granularity_sampled=False, ct_exit_spans=None):
173173
self.agent_attributes["http.url"] = self.http_url
174174

175175
i_attrs = (base_attrs and base_attrs.copy()) or attr_class()
@@ -180,4 +180,4 @@ def span_event(self, settings, base_attrs=None, parent_guid=None, attr_class=dic
180180
if self.method:
181181
i_attrs["http.method"] = self.method
182182

183-
return super().span_event(settings, base_attrs=i_attrs, parent_guid=parent_guid, attr_class=attr_class)
183+
return super().span_event(settings, base_attrs=i_attrs, parent_guid=parent_guid, attr_class=attr_class, partial_granularity_sampled=partial_granularity_sampled, ct_exit_spans=ct_exit_spans)

0 commit comments

Comments
 (0)