Skip to content

Commit 56fbda1

Browse files
authored
Merge pull request #1064 from newrelic/drop-none-attrs
Drop attributes with a value of None
2 parents 1043516 + 5bbf0dc commit 56fbda1

14 files changed

+234
-147
lines changed

newrelic/api/transaction.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@
5252
MAX_NUM_USER_ATTRIBUTES,
5353
create_agent_attributes,
5454
create_attributes,
55-
create_user_attributes,
5655
process_user_attribute,
5756
resolve_logging_context_attributes,
5857
truncate,
5958
)
6059
from newrelic.core.attribute_filter import (
60+
DST_ALL,
6161
DST_ERROR_COLLECTOR,
6262
DST_NONE,
6363
DST_TRANSACTION_TRACER,
@@ -1004,7 +1004,7 @@ def _update_agent_attributes(self):
10041004

10051005
@property
10061006
def user_attributes(self):
1007-
return create_user_attributes(self._custom_params, self.attribute_filter)
1007+
return create_attributes(self._custom_params, DST_ALL, self.attribute_filter)
10081008

10091009
def _compute_sampled_and_priority(self):
10101010
if self._priority is None:

newrelic/core/attribute.py

+35-8
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ class CastingFailureException(Exception):
110110
pass
111111

112112

113+
class NullValueException(ValueError):
114+
pass
115+
116+
113117
class Attribute(_Attribute):
114118
def __repr__(self):
115119
return "Attribute(name=%r, value=%r, destinations=%r)" % (self.name, self.value, bin(self.destinations))
@@ -126,6 +130,13 @@ def create_attributes(attr_dict, destinations, attribute_filter):
126130

127131

128132
def create_agent_attributes(attr_dict, attribute_filter):
133+
"""
134+
Returns a dictionary of Attribute objects with appropriate destinations.
135+
136+
If the attribute's key is in the known list of event attributes, it is assigned
137+
to _DESTINATIONS_WITH_EVENTS, otherwise it is assigned to _DESTINATIONS.
138+
Note attributes with a value of None are filtered out.
139+
"""
129140
attributes = []
130141

131142
for k, v in attr_dict.items():
@@ -143,12 +154,15 @@ def create_agent_attributes(attr_dict, attribute_filter):
143154

144155

145156
def resolve_user_attributes(attr_dict, attribute_filter, target_destination, attr_class=dict):
157+
"""
158+
Returns an attr_class of key value attributes filtered to the target_destination.
159+
160+
process_user_attribute MUST be called before this function to filter out invalid
161+
attributes.
162+
"""
146163
u_attrs = attr_class()
147164

148165
for attr_name, attr_value in attr_dict.items():
149-
if attr_value is None:
150-
continue
151-
152166
dest = attribute_filter.apply(attr_name, DST_ALL)
153167

154168
if dest & target_destination:
@@ -201,11 +215,6 @@ def resolve_logging_context_attributes(attr_dict, attribute_filter, attr_prefix,
201215
return c_attrs
202216

203217

204-
def create_user_attributes(attr_dict, attribute_filter):
205-
destinations = DST_ALL
206-
return create_attributes(attr_dict, destinations, attribute_filter)
207-
208-
209218
def truncate(text, maxsize=MAX_ATTRIBUTE_LENGTH, encoding="utf-8", ending=None):
210219
# Truncate text so that its byte representation
211220
# is no longer than maxsize bytes.
@@ -285,6 +294,15 @@ def process_user_attribute(name, value, max_length=MAX_ATTRIBUTE_LENGTH, ending=
285294
_logger.debug("Attribute value cannot be cast to a string. Dropping attribute: %r=%r", name, value)
286295
return FAILED_RESULT
287296

297+
except NullValueException:
298+
_logger.debug(
299+
"Attribute value is None. There is no difference between omitting the key "
300+
"and sending None. Dropping attribute: %r=%r",
301+
name,
302+
value,
303+
)
304+
return FAILED_RESULT
305+
288306
else:
289307
# Check length after casting
290308

@@ -311,9 +329,18 @@ def sanitize(value):
311329
Insights. Otherwise, convert value to a string.
312330
313331
Raise CastingFailureException, if str(value) somehow fails.
332+
Raise NullValueException, if value is None (null values SHOULD NOT be reported).
314333
"""
315334

316335
valid_value_types = (six.text_type, six.binary_type, bool, float, six.integer_types)
336+
# According to the agent spec, agents should not report None attribute values.
337+
# There is no difference between omitting the key and sending a None, so we can
338+
# reduce the payload size by not sending None values.
339+
if value is None:
340+
raise NullValueException(
341+
"Attribute value is of type: None. Omitting value since there is "
342+
"no difference between omitting the key and sending None."
343+
)
317344

318345
# When working with numpy, note that numpy has its own `int`s, `str`s,
319346
# et cetera. `numpy.str_` and `numpy.float_` inherit from Python's native

newrelic/core/node_mixin.py

+44-67
Original file line numberDiff line numberDiff line change
@@ -13,141 +13,118 @@
1313
# limitations under the License.
1414

1515
import newrelic.core.attribute as attribute
16-
17-
from newrelic.core.attribute_filter import (DST_SPAN_EVENTS,
18-
DST_TRANSACTION_SEGMENTS)
16+
from newrelic.core.attribute_filter import DST_SPAN_EVENTS, DST_TRANSACTION_SEGMENTS
1917

2018

2119
class GenericNodeMixin(object):
2220
@property
2321
def processed_user_attributes(self):
24-
if hasattr(self, '_processed_user_attributes'):
22+
if hasattr(self, "_processed_user_attributes"):
2523
return self._processed_user_attributes
2624

2725
self._processed_user_attributes = u_attrs = {}
28-
user_attributes = getattr(self, 'user_attributes', u_attrs)
26+
user_attributes = getattr(self, "user_attributes", u_attrs)
2927
for k, v in user_attributes.items():
3028
k, v = attribute.process_user_attribute(k, v)
31-
u_attrs[k] = v
29+
# Only record the attribute if it passes processing.
30+
# Failures return (None, None).
31+
if k:
32+
u_attrs[k] = v
3233
return u_attrs
3334

3435
def get_trace_segment_params(self, settings, params=None):
3536
_params = attribute.resolve_agent_attributes(
36-
self.agent_attributes,
37-
settings.attribute_filter,
38-
DST_TRANSACTION_SEGMENTS)
37+
self.agent_attributes, settings.attribute_filter, DST_TRANSACTION_SEGMENTS
38+
)
3939

4040
if params:
4141
_params.update(params)
4242

43-
_params.update(attribute.resolve_user_attributes(
44-
self.processed_user_attributes,
45-
settings.attribute_filter,
46-
DST_TRANSACTION_SEGMENTS))
43+
_params.update(
44+
attribute.resolve_user_attributes(
45+
self.processed_user_attributes, settings.attribute_filter, DST_TRANSACTION_SEGMENTS
46+
)
47+
)
4748

48-
_params['exclusive_duration_millis'] = 1000.0 * self.exclusive
49+
_params["exclusive_duration_millis"] = 1000.0 * self.exclusive
4950
return _params
5051

51-
def span_event(
52-
self,
53-
settings,
54-
base_attrs=None,
55-
parent_guid=None,
56-
attr_class=dict):
52+
def span_event(self, settings, base_attrs=None, parent_guid=None, attr_class=dict):
5753
i_attrs = base_attrs and base_attrs.copy() or attr_class()
58-
i_attrs['type'] = 'Span'
59-
i_attrs['name'] = self.name
60-
i_attrs['guid'] = self.guid
61-
i_attrs['timestamp'] = int(self.start_time * 1000)
62-
i_attrs['duration'] = self.duration
63-
i_attrs['category'] = 'generic'
54+
i_attrs["type"] = "Span"
55+
i_attrs["name"] = self.name
56+
i_attrs["guid"] = self.guid
57+
i_attrs["timestamp"] = int(self.start_time * 1000)
58+
i_attrs["duration"] = self.duration
59+
i_attrs["category"] = "generic"
6460

6561
if parent_guid:
66-
i_attrs['parentId'] = parent_guid
62+
i_attrs["parentId"] = parent_guid
6763

6864
a_attrs = attribute.resolve_agent_attributes(
69-
self.agent_attributes,
70-
settings.attribute_filter,
71-
DST_SPAN_EVENTS,
72-
attr_class=attr_class)
65+
self.agent_attributes, settings.attribute_filter, DST_SPAN_EVENTS, attr_class=attr_class
66+
)
7367

7468
u_attrs = attribute.resolve_user_attributes(
75-
self.processed_user_attributes,
76-
settings.attribute_filter,
77-
DST_SPAN_EVENTS,
78-
attr_class=attr_class)
69+
self.processed_user_attributes, settings.attribute_filter, DST_SPAN_EVENTS, attr_class=attr_class
70+
)
7971

8072
# intrinsics, user attrs, agent attrs
8173
return [i_attrs, u_attrs, a_attrs]
8274

83-
def span_events(self,
84-
settings, base_attrs=None, parent_guid=None, attr_class=dict):
85-
86-
yield self.span_event(
87-
settings,
88-
base_attrs=base_attrs,
89-
parent_guid=parent_guid,
90-
attr_class=attr_class)
75+
def span_events(self, settings, base_attrs=None, parent_guid=None, attr_class=dict):
76+
yield self.span_event(settings, base_attrs=base_attrs, parent_guid=parent_guid, attr_class=attr_class)
9177

9278
for child in self.children:
9379
for event in child.span_events(
94-
settings,
95-
base_attrs=base_attrs,
96-
parent_guid=self.guid,
97-
attr_class=attr_class):
80+
settings, base_attrs=base_attrs, parent_guid=self.guid, attr_class=attr_class
81+
):
9882
yield event
9983

10084

10185
class DatastoreNodeMixin(GenericNodeMixin):
102-
10386
@property
10487
def name(self):
10588
product = self.product
10689
target = self.target
107-
operation = self.operation or 'other'
90+
operation = self.operation or "other"
10891

10992
if target:
110-
name = 'Datastore/statement/%s/%s/%s' % (product, target,
111-
operation)
93+
name = "Datastore/statement/%s/%s/%s" % (product, target, operation)
11294
else:
113-
name = 'Datastore/operation/%s/%s' % (product, operation)
95+
name = "Datastore/operation/%s/%s" % (product, operation)
11496

11597
return name
11698

11799
@property
118100
def db_instance(self):
119-
if hasattr(self, '_db_instance'):
101+
if hasattr(self, "_db_instance"):
120102
return self._db_instance
121103

122104
db_instance_attr = None
123105
if self.database_name:
124-
_, db_instance_attr = attribute.process_user_attribute(
125-
'db.instance', self.database_name)
106+
_, db_instance_attr = attribute.process_user_attribute("db.instance", self.database_name)
126107

127108
self._db_instance = db_instance_attr
128109
return db_instance_attr
129110

130111
def span_event(self, *args, **kwargs):
131-
self.agent_attributes['db.instance'] = self.db_instance
112+
self.agent_attributes["db.instance"] = self.db_instance
132113
attrs = super(DatastoreNodeMixin, self).span_event(*args, **kwargs)
133114
i_attrs = attrs[0]
134115
a_attrs = attrs[2]
135116

136-
i_attrs['category'] = 'datastore'
137-
i_attrs['component'] = self.product
138-
i_attrs['span.kind'] = 'client'
117+
i_attrs["category"] = "datastore"
118+
i_attrs["component"] = self.product
119+
i_attrs["span.kind"] = "client"
139120

140121
if self.instance_hostname:
141-
_, a_attrs['peer.hostname'] = attribute.process_user_attribute(
142-
'peer.hostname', self.instance_hostname)
122+
_, a_attrs["peer.hostname"] = attribute.process_user_attribute("peer.hostname", self.instance_hostname)
143123
else:
144-
a_attrs['peer.hostname'] = 'Unknown'
124+
a_attrs["peer.hostname"] = "Unknown"
145125

146-
peer_address = '%s:%s' % (
147-
self.instance_hostname or 'Unknown',
148-
self.port_path_or_id or 'Unknown')
126+
peer_address = "%s:%s" % (self.instance_hostname or "Unknown", self.port_path_or_id or "Unknown")
149127

150-
_, a_attrs['peer.address'] = attribute.process_user_attribute(
151-
'peer.address', peer_address)
128+
_, a_attrs["peer.address"] = attribute.process_user_attribute("peer.address", peer_address)
152129

153130
return attrs

newrelic/core/root_node.py

+23-18
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,39 @@
1616

1717
import newrelic.core.trace_node
1818
from newrelic.core.node_mixin import GenericNodeMixin
19-
from newrelic.core.attribute import resolve_user_attributes
2019

21-
from newrelic.packages import six
22-
23-
_RootNode = namedtuple('_RootNode',
24-
['name', 'children', 'start_time', 'end_time', 'exclusive',
25-
'duration', 'guid', 'agent_attributes', 'user_attributes',
26-
'path', 'trusted_parent_span', 'tracing_vendors',])
20+
_RootNode = namedtuple(
21+
"_RootNode",
22+
[
23+
"name",
24+
"children",
25+
"start_time",
26+
"end_time",
27+
"exclusive",
28+
"duration",
29+
"guid",
30+
"agent_attributes",
31+
"user_attributes",
32+
"path",
33+
"trusted_parent_span",
34+
"tracing_vendors",
35+
],
36+
)
2737

2838

2939
class RootNode(_RootNode, GenericNodeMixin):
3040
def span_event(self, *args, **kwargs):
3141
span = super(RootNode, self).span_event(*args, **kwargs)
3242
i_attrs = span[0]
33-
i_attrs['transaction.name'] = self.path
34-
i_attrs['nr.entryPoint'] = True
43+
i_attrs["transaction.name"] = self.path
44+
i_attrs["nr.entryPoint"] = True
3545
if self.trusted_parent_span:
36-
i_attrs['trustedParentId'] = self.trusted_parent_span
46+
i_attrs["trustedParentId"] = self.trusted_parent_span
3747
if self.tracing_vendors:
38-
i_attrs['tracingVendors'] = self.tracing_vendors
48+
i_attrs["tracingVendors"] = self.tracing_vendors
3949
return span
4050

4151
def trace_node(self, stats, root, connections):
42-
4352
name = self.path
4453

4554
start_time = newrelic.core.trace_node.node_start_time(root, self)
@@ -57,9 +66,5 @@ def trace_node(self, stats, root, connections):
5766
params = self.get_trace_segment_params(root.settings)
5867

5968
return newrelic.core.trace_node.TraceNode(
60-
start_time=start_time,
61-
end_time=end_time,
62-
name=name,
63-
params=params,
64-
children=children,
65-
label=None)
69+
start_time=start_time, end_time=end_time, name=name, params=params, children=children, label=None
70+
)

newrelic/core/stats_engine.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@
4141
from newrelic.core.attribute import (
4242
MAX_LOG_MESSAGE_LENGTH,
4343
create_agent_attributes,
44-
create_user_attributes,
44+
create_attributes,
4545
process_user_attribute,
4646
resolve_logging_context_attributes,
4747
truncate,
4848
)
49-
from newrelic.core.attribute_filter import DST_ERROR_COLLECTOR
49+
from newrelic.core.attribute_filter import DST_ALL, DST_ERROR_COLLECTOR
5050
from newrelic.core.code_level_metrics import extract_code_from_traceback
5151
from newrelic.core.config import is_expected_error, should_ignore_error
5252
from newrelic.core.database_utils import explain_plan
@@ -824,7 +824,7 @@ def notice_error(self, error=None, attributes=None, expected=None, ignore=None,
824824
)
825825
custom_attributes = {}
826826

827-
user_attributes = create_user_attributes(custom_attributes, settings.attribute_filter)
827+
user_attributes = create_attributes(custom_attributes, DST_ALL, settings.attribute_filter)
828828

829829
# Extract additional details about the exception as agent attributes
830830
agent_attributes = {}

0 commit comments

Comments
 (0)