Skip to content

Commit 107c0a6

Browse files
TimPansinolrafeeihmstepanekumaannamalaimergify[bot]
authored
Errors Inbox Improvements (#791)
* Errors inbox attributes and tests (#778) * Initial errors inbox commit Co-authored-by: Timothy Pansino <[email protected]> Co-authored-by: Hannah Stepanek <[email protected]> Co-authored-by: Uma Annamalai <[email protected]> * Add enduser.id field * Move validate_error_trace_attributes into validators directory * Add error callback attributes test * Add tests for enduser.id & error.group.name Co-authored-by: Timothy Pansino <[email protected]> * Uncomment code_coverage * Drop commented out line --------- Co-authored-by: Timothy Pansino <[email protected]> Co-authored-by: Hannah Stepanek <[email protected]> Co-authored-by: Uma Annamalai <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Timothy Pansino <[email protected]> * Error Group Callback API (#785) * Error group initial implementation * Rewrite error callback to pass map of info * Fixed incorrect validators causing errors Co-authored-by: Uma Annamalai <[email protected]> Co-authored-by: Hannah Stepanek <[email protected]> * Fix validation of error trace attributes * Expanded error callback test * Add incorrect type to error callback testing * Change error group callback to private setting * Add testing for error group callback inputs * Separate error group callback tests * Add explicit testing for the set API * Ensure error group is string * Fix python 2 type validation --------- Co-authored-by: Uma Annamalai <[email protected]> Co-authored-by: Hannah Stepanek <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * User Tracking for Errors Inbox (#789) * Add user tracking feature for errors inbox. * Address review comments, * Add high_security test. * Cleanup invalid tests test. * Update user_id string check. * Remove set_id outside txn test. --------- Co-authored-by: Timothy Pansino <[email protected]> --------- Co-authored-by: Lalleh Rafeei <[email protected]> Co-authored-by: Timothy Pansino <[email protected]> Co-authored-by: Hannah Stepanek <[email protected]> Co-authored-by: Uma Annamalai <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Uma Annamalai <[email protected]>
1 parent 637879a commit 107c0a6

15 files changed

+778
-261
lines changed

newrelic/agent.py

+4
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,9 @@ def __asgi_application(*args, **kwargs):
155155
from newrelic.api.profile_trace import ProfileTraceWrapper as __ProfileTraceWrapper
156156
from newrelic.api.profile_trace import profile_trace as __profile_trace
157157
from newrelic.api.profile_trace import wrap_profile_trace as __wrap_profile_trace
158+
from newrelic.api.settings import set_error_group_callback as __set_error_group_callback
158159
from newrelic.api.supportability import wrap_api_call as __wrap_api_call
160+
from newrelic.api.transaction import set_user_id as __set_user_id
159161
from newrelic.api.transaction_name import (
160162
TransactionNameWrapper as __TransactionNameWrapper,
161163
)
@@ -223,6 +225,8 @@ def __asgi_application(*args, **kwargs):
223225
get_linking_metadata = __wrap_api_call(__get_linking_metadata, "get_linking_metadata")
224226
add_custom_span_attribute = __wrap_api_call(__add_custom_span_attribute, "add_custom_span_attribute")
225227
current_transaction = __wrap_api_call(__current_transaction, "current_transaction")
228+
set_user_id = __wrap_api_call(__set_user_id, "set_user_id")
229+
set_error_group_callback = __wrap_api_call(__set_error_group_callback, "set_error_group_callback")
226230
set_transaction_name = __wrap_api_call(__set_transaction_name, "set_transaction_name")
227231
end_of_transaction = __wrap_api_call(__end_of_transaction, "end_of_transaction")
228232
set_background_task = __wrap_api_call(__set_background_task, "set_background_task")

newrelic/api/settings.py

+28-2
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,42 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import logging
16+
1517
import newrelic.core.config
1618

1719
settings = newrelic.core.config.global_settings
1820

21+
_logger = logging.getLogger(__name__)
22+
23+
1924
RECORDSQL_OFF = 'off'
2025
RECORDSQL_RAW = 'raw'
2126
RECORDSQL_OBFUSCATED = 'obfuscated'
2227

2328
COMPRESSED_CONTENT_ENCODING_DEFLATE = 'deflate'
2429
COMPRESSED_CONTENT_ENCODING_GZIP = 'gzip'
2530

26-
STRIP_EXCEPTION_MESSAGE = ("Message removed by New Relic "
27-
"'strip_exception_messages' setting")
31+
STRIP_EXCEPTION_MESSAGE = ("Message removed by New Relic 'strip_exception_messages' setting")
32+
33+
34+
def set_error_group_callback(callback, application=None):
35+
"""Set the current callback to be used to determine error groups."""
36+
from newrelic.api.application import application_instance
37+
38+
if callback is not None and not callable(callback):
39+
_logger.error("Error group callback must be a callable, or None to unset this setting.")
40+
return
41+
42+
# Check for activated application if it exists and was not given.
43+
application = application_instance(activate=False) if application is None else application
44+
45+
# Get application settings if it exists, or fallback to global settings object
46+
_settings = application.settings if application is not None else settings()
47+
48+
if _settings is None:
49+
_logger.error("Failed to set error_group_callback in application settings. Report this issue to New Relic support.")
50+
return
51+
52+
if _settings.error_collector:
53+
_settings.error_collector._error_group_callback = callback

newrelic/api/time_trace.py

+54-8
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
from newrelic.core.config import is_expected_error, should_ignore_error
3131
from newrelic.core.trace_cache import trace_cache
3232

33+
from newrelic.packages import six
34+
3335
_logger = logging.getLogger(__name__)
3436

3537

@@ -255,13 +257,15 @@ def _observe_exception(self, exc_info=None, ignore=None, expected=None, status_c
255257
if getattr(value, "_nr_ignored", None):
256258
return
257259

258-
module, name, fullnames, message = parse_exc_info((exc, value, tb))
260+
module, name, fullnames, message_raw = parse_exc_info((exc, value, tb))
259261
fullname = fullnames[0]
260262

261263
# Check to see if we need to strip the message before recording it.
262264

263265
if settings.strip_exception_messages.enabled and fullname not in settings.strip_exception_messages.allowlist:
264266
message = STRIP_EXCEPTION_MESSAGE
267+
else:
268+
message = message_raw
265269

266270
# Where expected or ignore are a callable they should return a
267271
# tri-state variable with the following behavior.
@@ -344,7 +348,7 @@ def _observe_exception(self, exc_info=None, ignore=None, expected=None, status_c
344348
is_expected = is_expected_error(exc_info, status_code=status_code, settings=settings)
345349

346350
# Record a supportability metric if error attributes are being
347-
# overiden.
351+
# overridden.
348352
if "error.class" in self.agent_attributes:
349353
transaction._record_supportability("Supportability/SpanEvent/Errors/Dropped")
350354

@@ -353,19 +357,31 @@ def _observe_exception(self, exc_info=None, ignore=None, expected=None, status_c
353357
self._add_agent_attribute("error.message", message)
354358
self._add_agent_attribute("error.expected", is_expected)
355359

356-
return fullname, message, tb, is_expected
360+
return fullname, message, message_raw, tb, is_expected
357361

358362
def notice_error(self, error=None, attributes=None, expected=None, ignore=None, status_code=None):
359363
attributes = attributes if attributes is not None else {}
360364

365+
# If no exception details provided, use current exception.
366+
367+
# Pull from sys.exc_info if no exception is passed
368+
if not error or None in error:
369+
error = sys.exc_info()
370+
371+
# If no exception to report, exit
372+
if not error or None in error:
373+
return
374+
375+
exc, value, tb = error
376+
361377
recorded = self._observe_exception(
362378
error,
363379
ignore=ignore,
364380
expected=expected,
365381
status_code=status_code,
366382
)
367383
if recorded:
368-
fullname, message, tb, is_expected = recorded
384+
fullname, message, message_raw, tb, is_expected = recorded
369385
transaction = self.transaction
370386
settings = transaction and transaction.settings
371387

@@ -392,16 +408,45 @@ def notice_error(self, error=None, attributes=None, expected=None, ignore=None,
392408
)
393409
custom_params = {}
394410

395-
if settings and settings.code_level_metrics and settings.code_level_metrics.enabled:
396-
source = extract_code_from_traceback(tb)
397-
else:
398-
source = None
411+
# Extract additional details about the exception
412+
413+
source = None
414+
error_group_name = None
415+
if settings:
416+
if settings.code_level_metrics and settings.code_level_metrics.enabled:
417+
source = extract_code_from_traceback(tb)
418+
419+
if settings.error_collector and settings.error_collector.error_group_callback is not None:
420+
try:
421+
# Call callback to obtain error group name
422+
input_attributes = {}
423+
input_attributes.update(transaction._custom_params)
424+
input_attributes.update(attributes)
425+
error_group_name_raw = settings.error_collector.error_group_callback(value, {
426+
"traceback": tb,
427+
"error.class": exc,
428+
"error.message": message_raw,
429+
"error.expected": is_expected,
430+
"custom_params": input_attributes,
431+
"transactionName": getattr(transaction, "name", None),
432+
"response.status": getattr(transaction, "_response_code", None),
433+
"request.method": getattr(transaction, "_request_method", None),
434+
"request.uri": getattr(transaction, "_request_uri", None),
435+
})
436+
if error_group_name_raw:
437+
_, error_group_name = process_user_attribute("error.group.name", error_group_name_raw)
438+
if error_group_name is None or not isinstance(error_group_name, six.string_types):
439+
raise ValueError("Invalid attribute value for error.group.name. Expected string, got: %s" % repr(error_group_name_raw))
440+
except Exception:
441+
_logger.error("Encountered error when calling error group callback:\n%s", "".join(traceback.format_exception(*sys.exc_info())))
442+
error_group_name = None
399443

400444
transaction._create_error_node(
401445
settings,
402446
fullname,
403447
message,
404448
is_expected,
449+
error_group_name,
405450
custom_params,
406451
self.guid,
407452
tb,
@@ -634,6 +679,7 @@ def get_service_linking_metadata(application=None, settings=None):
634679
if not settings:
635680
if application is None:
636681
from newrelic.api.application import application_instance
682+
637683
application = application_instance(activate=False)
638684

639685
if application is not None:

newrelic/api/transaction.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
obfuscate,
4747
)
4848
from newrelic.core.attribute import (
49+
MAX_ATTRIBUTE_LENGTH,
4950
MAX_LOG_MESSAGE_LENGTH,
5051
MAX_NUM_USER_ATTRIBUTES,
5152
create_agent_attributes,
@@ -1547,7 +1548,9 @@ def notice_error(self, error=None, attributes=None, expected=None, ignore=None,
15471548
status_code=status_code,
15481549
)
15491550

1550-
def _create_error_node(self, settings, fullname, message, expected, custom_params, span_id, tb, source):
1551+
def _create_error_node(
1552+
self, settings, fullname, message, expected, error_group_name, custom_params, span_id, tb, source
1553+
):
15511554
# Only remember up to limit of what can be caught for a
15521555
# single transaction. This could be trimmed further
15531556
# later if there are already recorded errors and would
@@ -1576,9 +1579,8 @@ def _create_error_node(self, settings, fullname, message, expected, custom_param
15761579
span_id=span_id,
15771580
stack_trace=exception_stack(tb),
15781581
custom_params=custom_params,
1579-
file_name=None,
1580-
line_number=None,
15811582
source=source,
1583+
error_group_name=error_group_name,
15821584
)
15831585

15841586
# TODO: Errors are recorded in time order. If
@@ -1812,6 +1814,21 @@ def add_custom_parameters(items):
18121814
return add_custom_attributes(items)
18131815

18141816

1817+
def set_user_id(user_id):
1818+
transaction = current_transaction()
1819+
1820+
if not user_id or not transaction:
1821+
return
1822+
1823+
if not isinstance(user_id, six.string_types):
1824+
_logger.warning("The set_user_id API requires a string-based user ID.")
1825+
return
1826+
1827+
user_id = truncate(user_id, MAX_ATTRIBUTE_LENGTH)
1828+
1829+
transaction._add_agent_attribute("enduser.id", user_id)
1830+
1831+
18151832
def add_framework_info(name, version=None):
18161833
transaction = current_transaction()
18171834
if transaction:

newrelic/core/attribute.py

+27-25
Original file line numberDiff line numberDiff line change
@@ -42,46 +42,48 @@
4242

4343
_TRANSACTION_EVENT_DEFAULT_ATTRIBUTES = set(
4444
(
45-
"host.displayName",
46-
"request.method",
47-
"request.headers.contentType",
48-
"request.headers.contentLength",
49-
"request.uri",
50-
"response.status",
51-
"request.headers.accept",
52-
"response.headers.contentLength",
53-
"response.headers.contentType",
54-
"request.headers.host",
55-
"request.headers.userAgent",
56-
"message.queueName",
57-
"message.routingKey",
58-
"http.url",
59-
"http.statusCode",
60-
"aws.requestId",
61-
"aws.operation",
6245
"aws.lambda.arn",
6346
"aws.lambda.coldStart",
6447
"aws.lambda.eventSource.arn",
48+
"aws.operation",
49+
"aws.requestId",
50+
"code.filepath",
51+
"code.function",
52+
"code.lineno",
53+
"code.namespace",
6554
"db.collection",
6655
"db.instance",
6756
"db.operation",
6857
"db.statement",
58+
"enduser.id",
6959
"error.class",
70-
"error.message",
7160
"error.expected",
72-
"peer.hostname",
73-
"peer.address",
61+
"error.message",
62+
"error.group.name",
7463
"graphql.field.name",
7564
"graphql.field.parentType",
7665
"graphql.field.path",
7766
"graphql.field.returnType",
7867
"graphql.operation.name",
79-
"graphql.operation.type",
8068
"graphql.operation.query",
81-
"code.filepath",
82-
"code.function",
83-
"code.lineno",
84-
"code.namespace",
69+
"graphql.operation.type",
70+
"host.displayName",
71+
"http.statusCode",
72+
"http.url",
73+
"message.queueName",
74+
"message.routingKey",
75+
"peer.address",
76+
"peer.hostname",
77+
"request.headers.accept",
78+
"request.headers.contentLength",
79+
"request.headers.contentType",
80+
"request.headers.host",
81+
"request.headers.userAgent",
82+
"request.method",
83+
"request.uri",
84+
"response.headers.contentLength",
85+
"response.headers.contentType",
86+
"response.status",
8587
)
8688
)
8789

newrelic/core/config.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,9 @@ class TransactionTracerAttributesSettings(Settings):
138138

139139

140140
class ErrorCollectorSettings(Settings):
141-
pass
141+
@property
142+
def error_group_callback(self):
143+
return self._error_group_callback
142144

143145

144146
class ErrorCollectorAttributesSettings(Settings):
@@ -698,6 +700,7 @@ def default_host(license_key):
698700
_settings.error_collector.ignore_status_codes = _parse_status_codes("100-102 200-208 226 300-308 404", set())
699701
_settings.error_collector.expected_classes = []
700702
_settings.error_collector.expected_status_codes = set()
703+
_settings.error_collector._error_group_callback = None
701704
_settings.error_collector.attributes.enabled = True
702705
_settings.error_collector.attributes.exclude = []
703706
_settings.error_collector.attributes.include = []

newrelic/core/error_node.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@
2424
"span_id",
2525
"stack_trace",
2626
"custom_params",
27-
"file_name",
28-
"line_number",
2927
"source",
28+
"error_group_name",
3029
],
3130
)

0 commit comments

Comments
 (0)