Skip to content

Commit b4c5c23

Browse files
authored
Merge branch 'develop-hybrid-core-tracing' into hybrid-agent-wsgi-traces
2 parents 5b484d8 + 48eecd9 commit b4c5c23

File tree

7 files changed

+270
-91
lines changed

7 files changed

+270
-91
lines changed

newrelic/core/application.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,12 +1375,6 @@ def harvest(self, shutdown=False, flexible=False):
13751375
spans_sampled = spans.num_samples
13761376
internal_count_metric("Supportability/SpanEvent/TotalEventsSeen", spans_seen)
13771377
internal_count_metric("Supportability/SpanEvent/TotalEventsSent", spans_sampled)
1378-
if configuration.distributed_tracing.sampler.partial_granularity.enabled:
1379-
internal_count_metric(
1380-
f"Supportability/Python/PartialGranularity/{configuration.distributed_tracing.sampler.partial_granularity.type}",
1381-
1,
1382-
)
1383-
13841378
stats.reset_span_events()
13851379

13861380
# Send error events

newrelic/core/attribute.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@
8787
"message.routingKey",
8888
"messaging.destination.name",
8989
"messaging.system",
90+
"nr.durations",
91+
"nr.ids",
92+
"nr.pg",
9093
"peer.address",
9194
"peer.hostname",
9295
"request.headers.accept",
@@ -125,6 +128,8 @@
125128
"span.kind",
126129
}
127130

131+
SPAN_ERROR_ATTRIBUTES = {"error.class", "error.message", "error.expected"}
132+
128133

129134
MAX_NUM_USER_ATTRIBUTES = 128
130135
MAX_ATTRIBUTE_LENGTH = 255

newrelic/core/node_mixin.py

Lines changed: 82 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -79,71 +79,91 @@ def span_event(
7979
if not partial_granularity_sampled:
8080
# intrinsics, user attrs, agent attrs
8181
return [i_attrs, u_attrs, a_attrs]
82-
else:
83-
if ct_exit_spans is None:
84-
ct_exit_spans = {}
85-
86-
partial_granularity_type = settings.distributed_tracing.sampler.partial_granularity.type
87-
exit_span_attrs_present = attribute.SPAN_ENTITY_RELATIONSHIP_ATTRIBUTES & set(a_attrs)
88-
# If this is the entry node or an LLM span always return it.
89-
if i_attrs.get("nr.entryPoint") or i_attrs["name"].startswith("Llm/"):
90-
if partial_granularity_type == "reduced":
91-
return [i_attrs, u_attrs, a_attrs]
92-
else:
93-
return [i_attrs, {}, {}]
94-
# If the span is not an exit span, skip it by returning None.
95-
if not exit_span_attrs_present:
96-
return None
97-
# If the span is an exit span and we are in reduced mode (meaning no attribute dropping),
98-
# just return the exit span as is.
82+
83+
if ct_exit_spans is None:
84+
ct_exit_spans = {"instrumented": 0, "kept": 0, "dropped_ids": 0}
85+
ct_exit_spans["instrumented"] += 1
86+
87+
partial_granularity_type = settings.distributed_tracing.sampler.partial_granularity.type
88+
a_attrs_set = set(a_attrs)
89+
exit_span_attrs_present = attribute.SPAN_ENTITY_RELATIONSHIP_ATTRIBUTES & a_attrs_set
90+
exit_span_error_attrs_present = attribute.SPAN_ERROR_ATTRIBUTES & a_attrs_set
91+
# If this is an entry span, add `nr.pg` to indicate transaction is partial
92+
# granularity sampled.
93+
if i_attrs.get("nr.entryPoint"):
94+
a_attrs["nr.pg"] = True
95+
# If this is the entry node or an LLM span always return it.
96+
if i_attrs.get("nr.entryPoint") or i_attrs["name"].startswith("Llm/"):
97+
ct_exit_spans["kept"] += 1
9998
if partial_granularity_type == "reduced":
10099
return [i_attrs, u_attrs, a_attrs]
101100
else:
102-
a_minimized_attrs = attr_class({key: a_attrs[key] for key in exit_span_attrs_present})
103-
# If we are in essential mode return the span with minimized attributes.
104-
if partial_granularity_type == "essential":
105-
return [i_attrs, {}, a_minimized_attrs]
106-
# If the span is an exit span but span compression (compact) is enabled,
107-
# we need to check for uniqueness before returning it.
108-
# Combine all the entity relationship attr values into a string to be
109-
# used as the hash to check for uniqueness.
110-
span_attrs = "".join([str(a_minimized_attrs[key]) for key in exit_span_attrs_present])
111-
new_exit_span = span_attrs not in ct_exit_spans
112-
# If this is a new exit span, add it to the known ct_exit_spans and
113-
# return it.
114-
if new_exit_span:
115-
# nr.ids is the list of span guids that share this unqiue exit span.
116-
a_minimized_attrs["nr.ids"] = []
117-
a_minimized_attrs["nr.durations"] = self.duration
118-
ct_exit_spans[span_attrs] = [i_attrs, a_minimized_attrs]
119-
return [i_attrs, {}, a_minimized_attrs]
120-
# If this is an exit span we've already seen, add it's guid to the list
121-
# of ids on the seen span, compute the new duration & start time, and
122-
# return None.
101+
return [i_attrs, {}, {key: a_attrs.get(key) for key in exit_span_error_attrs_present | {"nr.pg"}}]
102+
# If the span is not an exit span, skip it by returning None.
103+
if not exit_span_attrs_present:
104+
return None
105+
# If the span is an exit span and we are in reduced mode (meaning no attribute dropping),
106+
# just return the exit span as is.
107+
if partial_granularity_type == "reduced":
108+
ct_exit_spans["kept"] += 1
109+
return [i_attrs, u_attrs, a_attrs]
110+
else:
111+
a_minimized_attrs = attr_class(
112+
{key: a_attrs[key] for key in (exit_span_attrs_present | exit_span_error_attrs_present)}
113+
)
114+
# If we are in essential mode return the span with minimized attributes.
115+
if partial_granularity_type == "essential":
116+
ct_exit_spans["kept"] += 1
117+
return [i_attrs, {}, a_minimized_attrs]
118+
# If the span is an exit span but span compression (compact) is enabled,
119+
# we need to check for uniqueness before returning it.
120+
# Combine all the entity relationship attr values into a string to be
121+
# used as the hash to check for uniqueness.
122+
span_attrs = "".join([str(a_minimized_attrs[key]) for key in exit_span_attrs_present])
123+
new_exit_span = span_attrs not in ct_exit_spans
124+
# If this is a new exit span, add it to the known ct_exit_spans and
125+
# return it.
126+
if new_exit_span:
127+
# nr.ids is the list of span guids that share this unqiue exit span.
128+
a_minimized_attrs["nr.ids"] = []
129+
a_minimized_attrs["nr.durations"] = self.duration
130+
ct_exit_spans[span_attrs] = [i_attrs, a_minimized_attrs]
131+
ct_exit_spans["kept"] += 1
132+
return [i_attrs, {}, a_minimized_attrs]
133+
# If this is an exit span we've already seen, add the error attributes
134+
# (last occurring error takes precedence), add it's guid to the list
135+
# of ids on the seen span, compute the new duration & start time, and
136+
# return None.
137+
ct_exit_spans[span_attrs][1].update(
138+
attr_class({key: a_minimized_attrs[key] for key in exit_span_error_attrs_present})
139+
)
140+
# Max size for `nr.ids` = 1024. Max length = 63 (each span id is 16 bytes + 8 bytes for list type).
141+
if len(ct_exit_spans[span_attrs][1]["nr.ids"]) < 63:
123142
ct_exit_spans[span_attrs][1]["nr.ids"].append(self.guid)
124-
# Max size for `nr.ids` = 1024. Max length = 63 (each span id is 16 bytes + 8 bytes for list type).
125-
ct_exit_spans[span_attrs][1]["nr.ids"] = ct_exit_spans[span_attrs][1]["nr.ids"][:63]
126-
# Compute the new start and end time for all compressed spans and use
127-
# that to set the duration for all compressed spans.
128-
current_start_time = ct_exit_spans[span_attrs][0]["timestamp"]
129-
current_end_time = (
130-
ct_exit_spans[span_attrs][0]["timestamp"] / 1000 + ct_exit_spans[span_attrs][1]["nr.durations"]
131-
)
132-
new_start_time = i_attrs["timestamp"]
133-
new_end_time = i_attrs["timestamp"] / 1000 + i_attrs["duration"]
134-
set_start_time = min(new_start_time, current_start_time)
135-
# If the new span starts after the old span's end time or the new span
136-
# ends before the current span starts; add the durations.
137-
if current_end_time < new_start_time / 1000 or new_end_time < current_start_time / 1000:
138-
set_duration = ct_exit_spans[span_attrs][1]["nr.durations"] + i_attrs["duration"]
139-
# Otherwise, if the new and old span's overlap in time, use the newest
140-
# end time and subtract the start time from it to calculate the new
141-
# duration.
142-
else:
143-
set_duration = max(current_end_time, new_end_time) - set_start_time / 1000
144-
ct_exit_spans[span_attrs][0]["timestamp"] = set_start_time
145-
ct_exit_spans[span_attrs][1]["nr.durations"] = set_duration
146-
return None
143+
else:
144+
ct_exit_spans["dropped_ids"] += 1
145+
146+
ct_exit_spans[span_attrs][1]["nr.ids"] = ct_exit_spans[span_attrs][1]["nr.ids"][:63]
147+
# Compute the new start and end time for all compressed spans and use
148+
# that to set the duration for all compressed spans.
149+
current_start_time = ct_exit_spans[span_attrs][0]["timestamp"]
150+
current_end_time = (
151+
ct_exit_spans[span_attrs][0]["timestamp"] / 1000 + ct_exit_spans[span_attrs][1]["nr.durations"]
152+
)
153+
new_start_time = i_attrs["timestamp"]
154+
new_end_time = i_attrs["timestamp"] / 1000 + i_attrs["duration"]
155+
set_start_time = min(new_start_time, current_start_time)
156+
# If the new span starts after the old span's end time or the new span
157+
# ends before the current span starts; add the durations.
158+
if current_end_time < new_start_time / 1000 or new_end_time < current_start_time / 1000:
159+
set_duration = ct_exit_spans[span_attrs][1]["nr.durations"] + i_attrs["duration"]
160+
# Otherwise, if the new and old span's overlap in time, use the newest
161+
# end time and subtract the start time from it to calculate the new
162+
# duration.
163+
else:
164+
set_duration = max(current_end_time, new_end_time) - set_start_time / 1000
165+
ct_exit_spans[span_attrs][0]["timestamp"] = set_start_time
166+
ct_exit_spans[span_attrs][1]["nr.durations"] = set_duration
147167

148168
def span_events(
149169
self,
@@ -162,13 +182,11 @@ def span_events(
162182
partial_granularity_sampled=partial_granularity_sampled,
163183
ct_exit_spans=ct_exit_spans,
164184
)
165-
ct_exit_spans["instrumented"] += 1
166185
parent_id = parent_guid
167186
if span: # span will be None if the span is an inprocess span or repeated exit span.
168-
ct_exit_spans["kept"] += 1
169187
yield span
170188
# Compressed spans are always reparented onto the entry span.
171-
if not settings.distributed_tracing.sampler.partial_granularity.type == "compact" or span[0].get(
189+
if settings.distributed_tracing.sampler.partial_granularity.type != "compact" or span[0].get(
172190
"nr.entryPoint"
173191
):
174192
parent_id = self.guid
@@ -181,9 +199,7 @@ def span_events(
181199
partial_granularity_sampled=partial_granularity_sampled,
182200
ct_exit_spans=ct_exit_spans,
183201
):
184-
ct_exit_spans["instrumented"] += 1
185202
if event: # event will be None if the span is an inprocess span or repeated exit span.
186-
ct_exit_spans["kept"] += 1
187203
yield event
188204

189205

newrelic/core/stats_engine.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,18 +1192,26 @@ def record_transaction(self, transaction):
11921192
self._span_events.add(event, priority=transaction.priority)
11931193
if transaction.partial_granularity_sampled:
11941194
partial_gran_type = settings.distributed_tracing.sampler.partial_granularity.type
1195-
self.record_custom_metrics(
1196-
[
1197-
(
1198-
f"Supportability/DistributedTrace/PartialGranularity/{partial_gran_type}/Span/Instrumented",
1199-
{"count": transaction.instrumented},
1200-
),
1201-
(
1202-
f"Supportability/DistributedTrace/PartialGranularity/{partial_gran_type}/Span/Kept",
1203-
{"count": transaction.kept},
1204-
),
1205-
]
1195+
self.record_custom_metric(
1196+
f"Supportability/Python/PartialGranularity/{partial_gran_type}", {"count": 1}
12061197
)
1198+
instrumented = getattr(transaction, "instrumented", 0)
1199+
if instrumented:
1200+
self.record_custom_metric(
1201+
f"Supportability/DistributedTrace/PartialGranularity/{partial_gran_type}/Span/Instrumented",
1202+
{"count": instrumented},
1203+
)
1204+
kept = getattr(transaction, "kept", 0)
1205+
if instrumented:
1206+
self.record_custom_metric(
1207+
f"Supportability/DistributedTrace/PartialGranularity/{partial_gran_type}/Span/Kept",
1208+
{"count": kept},
1209+
)
1210+
dropped_ids = getattr(transaction, "dropped_ids", 0)
1211+
if dropped_ids:
1212+
self.record_custom_metric(
1213+
"Supportability/Python/PartialGranularity/NrIds/Dropped", {"count": dropped_ids}
1214+
)
12071215

12081216
# Merge in log events
12091217

newrelic/core/transaction_node.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,7 @@ def span_events(self, settings, attr_class=dict):
634634
("priority", self.priority),
635635
)
636636
)
637-
ct_exit_spans = {"instrumented": 0, "kept": 0}
637+
ct_exit_spans = {"instrumented": 0, "kept": 0, "dropped_ids": 0}
638638
yield from self.root.span_events(
639639
settings,
640640
base_attrs,
@@ -646,6 +646,8 @@ def span_events(self, settings, attr_class=dict):
646646
# If this transaction is partial granularity sampled, record the number of spans
647647
# instrumented and the number of spans kept to monitor cost savings of partial
648648
# granularity tracing.
649+
# Also record the number of span ids dropped (fragmentation) in compact mode.
649650
if self.partial_granularity_sampled:
650651
self.instrumented = ct_exit_spans["instrumented"]
651652
self.kept = ct_exit_spans["kept"]
653+
self.dropped_ids = ct_exit_spans["dropped_ids"]

0 commit comments

Comments
 (0)