Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
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
953f379
Add free-threaded Python to CI (#1562)
TimPansino Oct 30, 2025
3ed4a12
Bump github/codeql-action in the github_actions group (#1566)
dependabot[bot] Nov 3, 2025
2dd463d
Region aware/ Claude 3+ bedrock support (#1561)
umaannamalai Nov 3, 2025
b9d9d3b
Fix notice_error logic for non-iterable exceptions. (#1564)
umaannamalai Nov 3, 2025
4cb31b1
Revert "Fix notice_error logic for non-iterable exceptions. (#1564)" …
umaannamalai Nov 3, 2025
8577eb7
Add additional trace points for AWS Kinesis (#1569)
TimPansino Nov 3, 2025
8c91a74
Enable environment variables for attribute filters (#1558)
lrafeei Nov 3, 2025
68252f0
Update version of cibuildwheel to latest (#1570)
TimPansino Nov 3, 2025
81d01e1
Force uv to use non-emulated Python on windows arm64 (#1567)
TimPansino Nov 4, 2025
722436f
Merge branch 'main' into sync-with-main
hmstepanek Nov 5, 2025
879f21a
Add support 4 partial granularity tracing
hmstepanek Jun 6, 2025
270c06d
Move config consolidation into global settings
hmstepanek Oct 27, 2025
c04525d
[MegaLinter] Apply linters fixes
hmstepanek Oct 27, 2025
eae574b
Move inifinte tracing override to server side config
hmstepanek Oct 30, 2025
2994854
[MegaLinter] Apply linters fixes
hmstepanek Oct 30, 2025
2040052
Fix failing event loop tests
hmstepanek Nov 5, 2025
687e3a2
Merge branch 'develop-hybrid-core-tracing' into partial-granularity-t…
mergify[bot] Nov 5, 2025
c5f5a82
Fixup: linter
hmstepanek Nov 5, 2025
00cd224
[MegaLinter] Apply linters fixes
hmstepanek Nov 5, 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
60 changes: 58 additions & 2 deletions newrelic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,47 @@ def _process_setting(section, option, getter, mapper):
_raise_configuration_error(section, option)


def _process_dt_setting(section, option_p1, option_p2, getter):
try:
# The type of a value is dictated by the getter
# function supplied.

value1 = getattr(_config_object, getter)(section, option_p1)
value2 = getattr(_config_object, getter)(section, option_p2)

# Now need to apply the option from the
# configuration file to the internal settings
# object. Walk the object path and assign it.

target = _settings
fields = option_p1.split(".", 1)

while True:
if len(fields) == 1:
value = value1 or value2 or "default"
setattr(target, fields[0], value)
break
target = getattr(target, fields[0])
fields = fields[1].split(".", 1)

# Cache the configuration so can be dumped out to
# log file when whole main configuration has been
# processed. This ensures that the log file and log
# level entries have been set.

_cache_object.append((option_p1, value1))
_cache_object.append((option_p2, value2))

except configparser.NoSectionError:
pass

except configparser.NoOptionError:
pass

except Exception:
_raise_configuration_error(section, option_p1)


# Processing of all the settings for specified section except
# for log file and log level which are applied separately to
# ensure they are set as soon as possible.
Expand Down Expand Up @@ -405,8 +446,23 @@ def _process_configuration(section):
_process_setting(section, "distributed_tracing.enabled", "getboolean", None)
_process_setting(section, "distributed_tracing.exclude_newrelic_header", "getboolean", None)
_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_dt_setting(
section,
"distributed_tracing.sampler.full_granularity.remote_parent_sampled",
"distributed_tracing.sampler.remote_parent_sampled",
"get",
)
_process_dt_setting(
section,
"distributed_tracing.sampler.full_granularity.remote_parent_not_sampled",
"distributed_tracing.sampler.remote_parent_not_sampled",
"get",
)
_process_setting(section, "distributed_tracing.sampler.full_granularity.enabled", "getboolean", 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
43 changes: 39 additions & 4 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,11 +855,26 @@ def default_otlp_host(host):
_settings.distributed_tracing.sampler.adaptive_sampling_target = _environ_as_int(
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ADAPTIVE_SAMPLING_TARGET", default=10
)
_settings.distributed_tracing.sampler.remote_parent_sampled = os.environ.get(
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_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.remote_parent_not_sampled = os.environ.get(
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED", "default"
_settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled = os.environ.get(
"NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_SAMPLED", None
) or os.environ.get("NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED", "default")
_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
) or os.environ.get("NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED", "default")
_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)
Expand Down Expand Up @@ -1366,6 +1391,16 @@ def apply_server_side_settings(server_side_config=None, settings=_settings):
min(settings_snapshot.custom_insights_events.max_attribute_value, 4095),
)

# Partial granularity tracing is not available in infinite tracing mode.
if (
settings_snapshot.infinite_tracing.enabled
and settings_snapshot.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."
)
apply_config_setting(settings_snapshot, "distributed_tracing.sampler.partial_granularity.enabled", False)

# This will be removed at some future point
# Special case for account_id which will be sent instead of
# cross_process_id in the future
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