Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fcaeb10
Fix error when shutdown_agent called from harvest thread (#1552)
TimPansino Oct 23, 2025
669f51e
Add support 4 partial granularity tracing
hmstepanek Jun 6, 2025
996ee66
fix(aiomysql): avoid wrapping pooled connections multiple times (#1553)
canonrock16 Oct 24, 2025
6d7be8c
Fix structlog tests (#1556)
TimPansino Oct 27, 2025
f9ab47b
Bump the github_actions group with 4 updates (#1555)
dependabot[bot] Oct 27, 2025
d1e7b60
Add instrumentation for new kinesis method (#1557)
TimPansino Oct 27, 2025
09eaa22
Move config consolidation into global settings
hmstepanek Oct 27, 2025
b9efe7e
Fix error when shutdown_agent called from harvest thread (#1552)
TimPansino Oct 23, 2025
8a3eb42
fix(aiomysql): avoid wrapping pooled connections multiple times (#1553)
canonrock16 Oct 24, 2025
3a83b40
Fix structlog tests (#1556)
TimPansino Oct 27, 2025
7a484b3
Bump the github_actions group with 4 updates (#1555)
dependabot[bot] Oct 27, 2025
edbfd79
Add instrumentation for new kinesis method (#1557)
TimPansino Oct 27, 2025
f52e9bd
[MegaLinter] Apply linters fixes
hmstepanek Oct 27, 2025
923ad1d
Trigger tests
hmstepanek Oct 28, 2025
953f379
Add free-threaded Python to CI (#1562)
TimPansino Oct 30, 2025
e024468
Move inifinte tracing override to server side config
hmstepanek Oct 30, 2025
d16d5dc
Add free-threaded Python to CI (#1562)
TimPansino Oct 30, 2025
9c14833
Merge branch 'main' into partial-granularity-type-support
hmstepanek Oct 30, 2025
db65db3
[MegaLinter] Apply linters fixes
hmstepanek Oct 30, 2025
7fd81a6
Trigger tests
hmstepanek Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 47 additions & 17 deletions newrelic/api/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ def __exit__(self, exc, value, tb):
trace_id=self.trace_id,
loop_time=self._loop_time,
root=root_node,
partial_granularity_sampled=hasattr(self, "partial_granularity_sampled"),
)

# Clear settings as we are all done and don't need it
Expand Down Expand Up @@ -1073,23 +1074,52 @@ def _make_sampling_decision(self):
return
priority = self._priority
sampled = self._sampled
_logger.debug(
"Full granularity tracing is enabled. Asking if full granularity wants to sample. priority=%s, sampled=%s",
priority,
sampled,
)
computed_priority, computed_sampled = self._compute_sampled_and_priority(
priority,
sampled,
remote_parent_sampled_path="distributed_tracing.sampler.remote_parent_sampled",
remote_parent_sampled_setting=self.settings.distributed_tracing.sampler.remote_parent_sampled,
remote_parent_not_sampled_path="distributed_tracing.sampler.remote_parent_not_sampled",
remote_parent_not_sampled_setting=self.settings.distributed_tracing.sampler.remote_parent_not_sampled,
)
_logger.debug("Full granularity sampling decision was %s with priority=%s.", sampled, priority)
self._priority = computed_priority
self._sampled = computed_sampled
self._sampling_decision_made = True
# Compute sampling decision for full granularity.
if self.settings.distributed_tracing.sampler.full_granularity.enabled:
_logger.debug(
"Full granularity tracing is enabled. Asking if full granularity wants to sample. priority=%s, sampled=%s",
priority,
sampled,
)
computed_priority, computed_sampled = self._compute_sampled_and_priority(
priority,
sampled,
remote_parent_sampled_path="distributed_tracing.sampler.full_granularity.remote_parent_sampled",
remote_parent_sampled_setting=self.settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled,
remote_parent_not_sampled_path="distributed_tracing.sampler.full_granularity.remote_parent_not_sampled",
remote_parent_not_sampled_setting=self.settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled,
)
_logger.debug("Full granularity sampling decision was %s with priority=%s.", sampled, priority)
if computed_sampled or not self.settings.distributed_tracing.sampler.partial_granularity.enabled:
self._priority = computed_priority
self._sampled = computed_sampled
self._sampling_decision_made = True
return

# If full granularity is not going to sample, let partial granularity decide.
if self.settings.distributed_tracing.sampler.partial_granularity.enabled:
_logger.debug("Partial granularity tracing is enabled. Asking if partial granularity wants to sample.")
self._priority, self._sampled = self._compute_sampled_and_priority(
priority,
sampled,
remote_parent_sampled_path="distributed_tracing.sampler.partial_granularity.remote_parent_sampled",
remote_parent_sampled_setting=self.settings.distributed_tracing.sampler.partial_granularity.remote_parent_sampled,
remote_parent_not_sampled_path="distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled",
remote_parent_not_sampled_setting=self.settings.distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled,
)
_logger.debug(
"Partial granularity sampling decision was %s with priority=%s.", self._sampled, self._priority
)
self._sampling_decision_made = True
if self._sampled:
self.partial_granularity_sampled = True
return

# This is only reachable if both full and partial granularity tracing are off.
# Set priority=0 and do not sample. This enables DT headers to still be sent
# even if the trace is never sampled.
self._priority = 0
self._sampled = False

def _freeze_path(self):
if self._frozen_path is None:
Expand Down
7 changes: 7 additions & 0 deletions newrelic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,13 @@ def _process_configuration(section):
_process_setting(section, "distributed_tracing.sampler.adaptive_sampling_target", "getint", None)
_process_setting(section, "distributed_tracing.sampler.remote_parent_sampled", "get", None)
_process_setting(section, "distributed_tracing.sampler.remote_parent_not_sampled", "get", None)
_process_setting(section, "distributed_tracing.sampler.full_granularity.enabled", "getboolean", None)
_process_setting(section, "distributed_tracing.sampler.full_granularity.remote_parent_sampled", "get", None)
_process_setting(section, "distributed_tracing.sampler.full_granularity.remote_parent_not_sampled", "get", None)
_process_setting(section, "distributed_tracing.sampler.partial_granularity.enabled", "getboolean", None)
_process_setting(section, "distributed_tracing.sampler.partial_granularity.type", "get", None)
_process_setting(section, "distributed_tracing.sampler.partial_granularity.remote_parent_sampled", "get", None)
_process_setting(section, "distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled", "get", None)
_process_setting(section, "span_events.enabled", "getboolean", None)
_process_setting(section, "span_events.max_samples_stored", "getint", None)
_process_setting(section, "span_events.attributes.enabled", "getboolean", None)
Expand Down
17 changes: 17 additions & 0 deletions newrelic/core/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,23 @@
"zeebe.client.resourceFile",
}

SPAN_ENTITY_RELATIONSHIP_ATTRIBUTES = {
"cloud.account.id",
"cloud.platform",
"cloud.region",
"cloud.resource_id",
"db.instance",
"db.system",
"http.url",
"messaging.destination.name",
"messaging.system",
"peer.hostname",
"server.address",
"server.port",
"span.kind",
}


MAX_NUM_USER_ATTRIBUTES = 128
MAX_ATTRIBUTE_LENGTH = 255
MAX_NUM_ML_USER_ATTRIBUTES = 64
Expand Down
56 changes: 56 additions & 0 deletions newrelic/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,14 @@ class DistributedTracingSamplerSettings(Settings):
pass


class DistributedTracingSamplerFullGranularitySettings(Settings):
pass


class DistributedTracingSamplerPartialGranularitySettings(Settings):
pass


class ServerlessModeSettings(Settings):
pass

Expand Down Expand Up @@ -507,6 +515,8 @@ class EventHarvestConfigHarvestLimitSettings(Settings):
_settings.debug = DebugSettings()
_settings.distributed_tracing = DistributedTracingSettings()
_settings.distributed_tracing.sampler = DistributedTracingSamplerSettings()
_settings.distributed_tracing.sampler.full_granularity = DistributedTracingSamplerFullGranularitySettings()
_settings.distributed_tracing.sampler.partial_granularity = DistributedTracingSamplerPartialGranularitySettings()
_settings.error_collector = ErrorCollectorSettings()
_settings.error_collector.attributes = ErrorCollectorAttributesSettings()
_settings.event_harvest_config = EventHarvestConfigSettings()
Expand Down Expand Up @@ -845,6 +855,27 @@ def default_otlp_host(host):
_settings.distributed_tracing.sampler.remote_parent_not_sampled = os.environ.get(
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED", "default"
)
_settings.distributed_tracing.sampler.full_granularity.enabled = _environ_as_bool(
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_ENABLED", default=True
)
_settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled = os.environ.get(
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_SAMPLED", None
)
_settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled = os.environ.get(
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED", None
)
_settings.distributed_tracing.sampler.partial_granularity.enabled = _environ_as_bool(
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_ENABLED", default=False
)
_settings.distributed_tracing.sampler.partial_granularity.type = os.environ.get(
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_TYPE", "essential"
)
_settings.distributed_tracing.sampler.partial_granularity.remote_parent_sampled = os.environ.get(
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_SAMPLED", "default"
)
_settings.distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled = os.environ.get(
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED", "default"
)
_settings.distributed_tracing.exclude_newrelic_header = False
_settings.span_events.enabled = _environ_as_bool("NEW_RELIC_SPAN_EVENTS_ENABLED", default=True)
_settings.event_harvest_config.harvest_limits.span_event_data = _environ_as_int(
Expand Down Expand Up @@ -1371,9 +1402,34 @@ def finalize_application_settings(server_side_config=None, settings=_settings):

application_settings.attribute_filter = AttributeFilter(flatten_settings(application_settings))

simplify_distributed_tracing_sampler_granularity_settings(application_settings)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized that simplify_distributed_tracing_sampler_granularity_settings() should either

  1. be moved before the attribute_filter application, but right after apply_server_side_settings()
  2. be removed all together and put the logic from that function in apply_server_side_settings() (maybe after the custom_insights_events.max_attribute_value override?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the logic for this has now moved to apply_server_side_settings(), the call to this function can be removed


return application_settings


def simplify_distributed_tracing_sampler_granularity_settings(settings):
# Full granularity settings may appear under:
# * `distributed_tracing.sampler`
# * `distributed_tracing.sampler.full_granularity`
# The `distributed_tracing.sampler.full_granularity` path takes precedence.
# To simplify logic in the code that uses these settings, store the values that
# should be used at the `distributed_tracing.sampler.full_granularity` path.
if not settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled:
settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled = (
settings.distributed_tracing.sampler.remote_parent_sampled
)
if not settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled:
settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled = (
settings.distributed_tracing.sampler.remote_parent_not_sampled
)
# Partial granularity tracing is not available in infinite tracing mode.
if settings.infinite_tracing.enabled and settings.distributed_tracing.sampler.partial_granularity.enabled:
_logger.warning(
"Improper configuration. Infinite tracing cannot be enabled at the same time as partial granularity tracing. Setting distributed_tracing.sampler.partial_granularity.enabled=False."
)
settings.distributed_tracing.sampler.partial_granularity.enabled = False


def _remove_ignored_configs(server_settings):
if not server_settings.get("agent_config"):
return server_settings
Expand Down
9 changes: 8 additions & 1 deletion newrelic/core/data_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,14 @@ def send_ml_events(self, sampling_info, custom_event_data):

def send_span_events(self, sampling_info, span_event_data):
"""Called to submit sample set for span events."""

# TODO: remove this later after list types are suported.
for span_event in span_event_data:
try:
ids = span_event[1].get("nr.ids")
if ids:
span_event[1]["nr.ids"] = ",".join(ids)
except:
pass
payload = (self.agent_run_id, sampling_info, span_event_data)
return self._protocol.send("span_event_data", payload)

Expand Down
19 changes: 17 additions & 2 deletions newrelic/core/database_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,15 @@ def trace_node(self, stats, root, connections):
start_time=start_time, end_time=end_time, name=name, params=params, children=children, label=None
)

def span_event(self, settings, base_attrs=None, parent_guid=None, attr_class=dict):
def span_event(
self,
settings,
base_attrs=None,
parent_guid=None,
attr_class=dict,
partial_granularity_sampled=False,
ct_exit_spans=None,
):
sql = self.formatted

if sql:
Expand All @@ -288,4 +296,11 @@ def span_event(self, settings, base_attrs=None, parent_guid=None, attr_class=dic

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

return super().span_event(settings, base_attrs=base_attrs, parent_guid=parent_guid, attr_class=attr_class)
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,
)
19 changes: 17 additions & 2 deletions newrelic/core/external_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,15 @@ def trace_node(self, stats, root, connections):
start_time=start_time, end_time=end_time, name=name, params=params, children=children, label=None
)

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

i_attrs = (base_attrs and base_attrs.copy()) or attr_class()
Expand All @@ -180,4 +188,11 @@ def span_event(self, settings, base_attrs=None, parent_guid=None, attr_class=dic
if self.method:
_, i_attrs["http.method"] = attribute.process_user_attribute("http.method", self.method)

return super().span_event(settings, base_attrs=i_attrs, parent_guid=parent_guid, attr_class=attr_class)
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,
)
19 changes: 17 additions & 2 deletions newrelic/core/function_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,23 @@ def trace_node(self, stats, root, connections):
start_time=start_time, end_time=end_time, name=name, params=params, children=children, label=self.label
)

def span_event(self, settings, base_attrs=None, parent_guid=None, attr_class=dict):
def span_event(
self,
settings,
base_attrs=None,
parent_guid=None,
attr_class=dict,
partial_granularity_sampled=False,
ct_exit_spans=None,
):
i_attrs = (base_attrs and base_attrs.copy()) or attr_class()
i_attrs["name"] = f"{self.group}/{self.name}"

return super().span_event(settings, base_attrs=i_attrs, parent_guid=parent_guid, attr_class=attr_class)
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,
)
19 changes: 17 additions & 2 deletions newrelic/core/loop_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,23 @@ def trace_node(self, stats, root, connections):
start_time=start_time, end_time=end_time, name=name, params=params, children=children, label=None
)

def span_event(self, settings, base_attrs=None, parent_guid=None, attr_class=dict):
def span_event(
self,
settings,
base_attrs=None,
parent_guid=None,
attr_class=dict,
partial_granularity_sampled=False,
ct_exit_spans=None,
):
i_attrs = (base_attrs and base_attrs.copy()) or attr_class()
i_attrs["name"] = f"EventLoop/Wait/{self.name}"

return super().span_event(settings, base_attrs=i_attrs, parent_guid=parent_guid, attr_class=attr_class)
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,
)
Loading
Loading