Skip to content

Commit c1764f8

Browse files
authored
Fix notice_error Rules Matching Settings Handling (#383)
* Remove active traces from tests to expose bug * Handle settings passing in error matching rules * Better edge case handling * Apply linter fixes * Correct settings origin to transaction * Change logs in context expected errors * Apply linters fixes
1 parent a62b4c9 commit c1764f8

File tree

7 files changed

+242
-146
lines changed

7 files changed

+242
-146
lines changed

newrelic/api/log.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,28 @@
1313
# limitations under the License.
1414

1515
import json
16-
from newrelic.common.object_names import parse_exc_info
17-
import newrelic.packages.six as six
1816
from logging import Formatter, LogRecord
17+
1918
from newrelic.api.time_trace import get_linking_metadata
19+
from newrelic.common.object_names import parse_exc_info
2020
from newrelic.core.config import is_expected_error
2121

2222

2323
def format_exc_info(exc_info):
2424
_, _, fullnames, message = parse_exc_info(exc_info)
2525
fullname = fullnames[0]
2626

27-
return {
27+
formatted = {
2828
"error.class": fullname,
2929
"error.message": message,
30-
"error.expected": is_expected_error(exc_info),
3130
}
3231

32+
expected = is_expected_error(exc_info)
33+
if expected is not None:
34+
formatted["error.expected"] = expected
35+
36+
return formatted
37+
3338

3439
class NewRelicContextFormatter(Formatter):
3540
DEFAULT_LOG_RECORD_KEYS = frozenset(vars(LogRecord("", 0, "", 0, "", (), None)))
@@ -57,7 +62,7 @@ def log_record_to_dict(cls, record):
5762
if len(record.__dict__) > len(DEFAULT_LOG_RECORD_KEYS):
5863
for key in record.__dict__:
5964
if key not in DEFAULT_LOG_RECORD_KEYS:
60-
output['extra.' + key] = getattr(record, key)
65+
output["extra." + key] = getattr(record, key)
6166

6267
if record.exc_info:
6368
output.update(format_exc_info(record.exc_info))
@@ -72,4 +77,4 @@ def safe_str(object, *args, **kwargs):
7277
except:
7378
return "<unprintable %s object>" % type(object).__name__
7479

75-
return json.dumps(self.log_record_to_dict(record), default=safe_str, separators=(',', ':'))
80+
return json.dumps(self.log_record_to_dict(record), default=safe_str, separators=(",", ":"))

newrelic/api/time_trace.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,9 @@ def _observe_exception(self, exc_info=None, ignore=None, expected=None, status_c
221221
if not exc_info or None in exc_info:
222222
exc_info = sys.exc_info()
223223

224-
# If no exception to report, exit
225-
if not exc_info or None in exc_info:
226-
return
224+
# If no exception to report, exit
225+
if not exc_info or None in exc_info:
226+
return
227227

228228
exc, value, tb = exc_info
229229

@@ -281,7 +281,7 @@ def _observe_exception(self, exc_info=None, ignore=None, expected=None, status_c
281281

282282
# Default rule matching
283283
if should_ignore is None:
284-
should_ignore = should_ignore_error(exc_info, status_code=status_code)
284+
should_ignore = should_ignore_error(exc_info, status_code=status_code, settings=settings)
285285
if should_ignore:
286286
return
287287

@@ -304,7 +304,7 @@ def _observe_exception(self, exc_info=None, ignore=None, expected=None, status_c
304304

305305
# Default rule matching
306306
if is_expected is None:
307-
is_expected = is_expected_error(exc_info, status_code=status_code)
307+
is_expected = is_expected_error(exc_info, status_code=status_code, settings=settings)
308308

309309
# Record a supportability metric if error attributes are being
310310
# overiden.

newrelic/api/transaction.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,11 @@ def __exit__(self, exc, value, tb):
390390
if not self._settings:
391391
return
392392

393+
# Record error if one was registered.
394+
395+
if exc is not None and value is not None and tb is not None:
396+
self.root_span.notice_error((exc, value, tb))
397+
393398
self._state = self.STATE_STOPPED
394399

395400
# Force the root span out of the cache if it's there
@@ -418,11 +423,6 @@ def __exit__(self, exc, value, tb):
418423
)
419424
return
420425

421-
# Record error if one was registered.
422-
423-
if exc is not None and value is not None and tb is not None:
424-
root.notice_error((exc, value, tb))
425-
426426
# Record the end time for transaction and then
427427
# calculate the duration.
428428

@@ -937,7 +937,7 @@ def user_attributes(self):
937937
def _compute_sampled_and_priority(self):
938938
if self._priority is None:
939939
# truncate priority field to 6 digits past the decimal
940-
self._priority = float("%.6f" % random.random())
940+
self._priority = float("%.6f" % random.random()) # nosec
941941

942942
if self._sampled is None:
943943
self._sampled = self._application.compute_sampled()

newrelic/core/config.py

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@
4141
try:
4242
import grpc
4343

44-
from newrelic.core.infinite_tracing_pb2 import (
45-
Span as _, # NOQA # pylint: disable=W0611,C0412
44+
from newrelic.core.infinite_tracing_pb2 import ( # pylint: disable=W0611,C0412 # noqa: F401
45+
Span,
4646
)
4747
except ImportError:
4848
grpc = None
@@ -1034,13 +1034,12 @@ def apply_server_side_settings(server_side_config=None, settings=_settings):
10341034
apply_config_setting(settings_snapshot, "event_harvest_config.whitelist", frozenset(harvest_limits))
10351035

10361036
# Override span event harvest config
1037-
span_event_harvest_config = server_side_config.get('span_event_harvest_config', {})
1037+
span_event_harvest_config = server_side_config.get("span_event_harvest_config", {})
10381038
span_event_harvest_limit = span_event_harvest_config.get("harvest_limit", None)
10391039
if span_event_harvest_limit is not None:
10401040
apply_config_setting(
1041-
settings_snapshot,
1042-
'event_harvest_config.harvest_limits.span_event_data',
1043-
span_event_harvest_limit)
1041+
settings_snapshot, "event_harvest_config.harvest_limits.span_event_data", span_event_harvest_limit
1042+
)
10441043

10451044
# This will be removed at some future point
10461045
# Special case for account_id which will be sent instead of
@@ -1103,53 +1102,71 @@ def ignore_status_code(status):
11031102
def is_expected_error(
11041103
exc_info,
11051104
status_code=None,
1105+
settings=None,
11061106
):
1107+
"""Check if an error is expected based on rules matching. Default is False when settings lookup fails."""
11071108
return error_matches_rules(
11081109
"expected",
11091110
exc_info,
11101111
status_code=status_code,
1112+
settings=settings,
11111113
)
11121114

11131115

11141116
def should_ignore_error(
11151117
exc_info,
11161118
status_code=None,
1119+
settings=None,
11171120
):
1121+
"""Check if an error should be ignored based on rules matching. Default is True when settings lookup fails."""
11181122
return error_matches_rules(
11191123
"ignore",
11201124
exc_info,
11211125
status_code=status_code,
1126+
settings=settings,
11221127
)
11231128

11241129

11251130
def error_matches_rules(
11261131
rules_prefix,
11271132
exc_info,
11281133
status_code=None,
1134+
settings=None,
11291135
):
1136+
"""
1137+
Attempt to match exception to rules based on prefix.
1138+
1139+
rules_prefix is one of [ignore, expected]
1140+
exc_info is an exception tuple of (exc, val, tb)
1141+
status_code is an optional value or callable taking in exc_info that returns an int-like object
1142+
origin is either the current application or trace.
1143+
"""
11301144
# Delay imports to prevent lockups
1131-
from newrelic.api.application import application_instance
11321145
from newrelic.core.trace_cache import trace_cache
11331146

1134-
trace = trace_cache().current_trace()
1135-
settings = trace and trace.settings
1136-
1137-
if not settings:
1138-
# Retrieve application settings
1139-
application = application_instance()
1140-
settings = application and application.settings
1141-
1142-
# Default to global settings
1143-
settings = settings or global_settings()
1144-
11451147
if not settings:
1146-
return False
1148+
# Pull from current transaction if no settings provided
1149+
tc = trace_cache()
1150+
transaction = tc.current_transaction()
1151+
settings = transaction and transaction.settings
1152+
1153+
if not settings:
1154+
# Pull from active trace if no settings on transaction
1155+
trace = tc.current_trace()
1156+
settings = trace and trace.settings
1157+
1158+
if not settings:
1159+
# Unable to find rules to match with
1160+
_logger.error(
1161+
"Failed to retrieve exception rules: No settings supplied, or found on transaction or trace."
1162+
)
1163+
return None
11471164

11481165
# Retrieve settings based on prefix
11491166
classes_rules = getattr(settings.error_collector, "%s_classes" % rules_prefix, set())
11501167
status_codes_rules = getattr(settings.error_collector, "%s_status_codes" % rules_prefix, set())
11511168

1152-
module, name, fullnames, message = parse_exc_info(exc_info)
1169+
_, _, fullnames, _ = parse_exc_info(exc_info)
11531170
fullname = fullnames[0]
11541171

11551172
# Check class names

newrelic/core/stats_engine.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ def add(self, sample, priority=None): # pylint: disable=E0202
316316
self.num_seen += 1
317317

318318
if priority is None:
319-
priority = random.random()
319+
priority = random.random() # nosec
320320

321321
entry = (priority, self.num_seen, sample)
322322
if self.num_seen == self.capacity:
@@ -584,9 +584,9 @@ def notice_error(self, error=None, attributes=None, expected=None, ignore=None,
584584
if not error or None in error:
585585
error = sys.exc_info()
586586

587-
# If no exception to report, exit
588-
if not error or None in error:
589-
return
587+
# If no exception to report, exit
588+
if not error or None in error:
589+
return
590590

591591
exc, value, tb = error
592592

@@ -637,7 +637,7 @@ def notice_error(self, error=None, attributes=None, expected=None, ignore=None,
637637

638638
# Default rule matching
639639
if should_ignore is None:
640-
should_ignore = should_ignore_error(error, status_code=status_code)
640+
should_ignore = should_ignore_error(error, status_code=status_code, settings=settings)
641641
if should_ignore:
642642
return
643643

@@ -660,7 +660,7 @@ def notice_error(self, error=None, attributes=None, expected=None, ignore=None,
660660

661661
# Default rule matching
662662
if is_expected is None:
663-
is_expected = is_expected_error(error, status_code=status_code)
663+
is_expected = is_expected_error(error, status_code=status_code, settings=settings)
664664

665665
# Only add attributes if High Security Mode is off.
666666

tests/agent_features/test_logs_in_context.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ class ExceptionForTest(ValueError):
113113
pass
114114

115115

116-
def test_newrelic_logger_error(log_buffer):
116+
@background_task()
117+
def test_newrelic_logger_error_inside_transaction(log_buffer):
117118
try:
118119
raise ExceptionForTest
119120
except ExceptionForTest:
@@ -134,7 +135,8 @@ def test_newrelic_logger_error(log_buffer):
134135
assert filename.endswith("/test_logs_in_context.py")
135136
assert isinstance(line_number, int)
136137

137-
assert message == {
138+
expected = {
139+
u"entity.name": u"Python Agent Test (agent_features)",
138140
u"entity.type": u"SERVICE",
139141
u"message": u"oops",
140142
u"log.level": u"ERROR",
@@ -145,6 +147,49 @@ def test_newrelic_logger_error(log_buffer):
145147
u"error.message": u"",
146148
u"error.expected": False,
147149
}
150+
expected_extra_txn_keys = (
151+
"trace.id",
152+
"span.id",
153+
"entity.guid",
154+
)
155+
156+
for k, v in expected.items():
157+
assert message.pop(k) == v
158+
159+
assert set(message.keys()) == set(expected_extra_txn_keys)
160+
161+
162+
def test_newrelic_logger_error_outside_transaction(log_buffer):
163+
try:
164+
raise ExceptionForTest
165+
except ExceptionForTest:
166+
_logger.exception(u"oops")
167+
168+
log_buffer.seek(0)
169+
message = json.load(log_buffer)
170+
171+
timestamp = message.pop("timestamp")
172+
thread_id = message.pop("thread.id")
173+
process_id = message.pop("process.id")
174+
filename = message.pop("file.name")
175+
line_number = message.pop("line.number")
176+
177+
assert isinstance(timestamp, int)
178+
assert isinstance(thread_id, int)
179+
assert isinstance(process_id, int)
180+
assert filename.endswith("/test_logs_in_context.py")
181+
assert isinstance(line_number, int)
182+
183+
assert message == {
184+
u"entity.type": u"SERVICE",
185+
u"message": u"oops",
186+
u"log.level": u"ERROR",
187+
u"logger.name": u"test_logs_in_context",
188+
u"thread.name": u"MainThread",
189+
u"process.name": u"MainProcess",
190+
u"error.class": u"test_logs_in_context:ExceptionForTest",
191+
u"error.message": u"",
192+
}
148193

149194

150195
EXPECTED_KEYS_TXN = (

0 commit comments

Comments
 (0)