Skip to content

Commit 1810838

Browse files
dubloomYun-Kimncybulnsrip-ddwconti27
authored andcommitted
feat: add record_exception method in datadog api (#12185)
Some users of Error Tracking product asked for the possibility to manually report an exception to ET. It was possible but only using Otel API. It seems that the users did not find it and it is easy to implement. This PR implements the record_exception in dd api according to the Otel [specifications](https://opentelemetry.io/docs/specs/otel/trace/exceptions/). ## Checklist - [X] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) --------- Co-authored-by: Yun Kim <[email protected]> Co-authored-by: Nicole Cybul <[email protected]> Co-authored-by: Nick Ripley <[email protected]> Co-authored-by: William Conti <[email protected]> Co-authored-by: Christophe Papazian <[email protected]> Co-authored-by: Munir Abdinur <[email protected]> Co-authored-by: Laplie Anderson <[email protected]> Co-authored-by: Brett Langdon <[email protected]>
1 parent 80cc528 commit 1810838

File tree

4 files changed

+129
-0
lines changed

4 files changed

+129
-0
lines changed

ddtrace/_trace/span.py

+45
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,51 @@ def set_exc_info(
550550

551551
core.dispatch("span.exception", (self, exc_type, exc_val, exc_tb))
552552

553+
def record_exception(
554+
self,
555+
exception: BaseException,
556+
attributes: Optional[Dict[str, _JSONType]] = None,
557+
timestamp: Optional[int] = None,
558+
escaped=False,
559+
) -> None:
560+
"""
561+
Records an exception as span event.
562+
If the exception is uncaught, :obj:`escaped` should be set :obj:`True`. It
563+
will tag the span with an error tuple.
564+
565+
:param Exception exception: the exception to record
566+
:param dict attributes: optional attributes to add to the span event. It will override
567+
the base attributes if :obj:`attributes` contains existing keys.
568+
:param int timestamp: the timestamp of the span event. Will be set to now() if timestamp is :obj:`None`.
569+
:param bool escaped: sets to :obj:`False` for a handled exception and :obj:`True` for a uncaught exception.
570+
"""
571+
if timestamp is None:
572+
timestamp = time_ns()
573+
574+
exc_type, exc_val, exc_tb = type(exception), exception, exception.__traceback__
575+
576+
if escaped:
577+
self.set_exc_info(exc_type, exc_val, exc_tb)
578+
579+
# get the traceback
580+
buff = StringIO()
581+
traceback.print_exception(exc_type, exc_val, exc_tb, file=buff, limit=config._span_traceback_max_size)
582+
tb = buff.getvalue()
583+
584+
# Set exception attributes in a manner that is consistent with the opentelemetry sdk
585+
# https://github.com/open-telemetry/opentelemetry-python/blob/v1.24.0/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py#L998
586+
attrs = {
587+
"exception.type": "%s.%s" % (exception.__class__.__module__, exception.__class__.__name__),
588+
"exception.message": str(exception),
589+
"exception.escaped": escaped,
590+
"exception.stacktrace": tb,
591+
}
592+
if attributes:
593+
# User provided attributes must take precedence over attrs
594+
attrs.update(attributes)
595+
596+
self._add_event(name="recorded exception", attributes=attrs, timestamp=timestamp)
597+
553598
def _pprint(self) -> str:
554599
"""Return a human readable version of the span."""
555600
data = [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
tracing: Introduces a record_exception method that adds an exception to a Span as a span event.
5+
Refer to [Span.record_exception](https://ddtrace.readthedocs.io/en/stable/api.html#ddtrace.trace.Span.record_exception)
6+
for more details.

tests/tracer/test_span.py

+55
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,61 @@ def test_span_pointers(self):
533533
},
534534
]
535535

536+
def test_span_record_exception(self):
537+
span = self.start_span("span")
538+
try:
539+
raise RuntimeError("bim")
540+
except RuntimeError as e:
541+
span.record_exception(e)
542+
span.finish()
543+
544+
span.assert_span_event_count(1)
545+
span.assert_span_event_attributes(
546+
0, {"exception.type": "builtins.RuntimeError", "exception.message": "bim", "exception.escaped": False}
547+
)
548+
549+
def test_span_record_multiple_exceptions(self):
550+
span = self.start_span("span")
551+
try:
552+
raise RuntimeError("bim")
553+
except RuntimeError as e:
554+
span.record_exception(e)
555+
556+
try:
557+
raise RuntimeError("bam")
558+
except RuntimeError as e:
559+
span.record_exception(e)
560+
span.finish()
561+
562+
span.assert_span_event_count(2)
563+
span.assert_span_event_attributes(
564+
0, {"exception.type": "builtins.RuntimeError", "exception.message": "bim", "exception.escaped": False}
565+
)
566+
span.assert_span_event_attributes(
567+
1, {"exception.type": "builtins.RuntimeError", "exception.message": "bam", "exception.escaped": False}
568+
)
569+
570+
def test_span_record_escaped_exception(self):
571+
exc = RuntimeError("bim")
572+
span = self.start_span("span")
573+
try:
574+
raise exc
575+
except RuntimeError as e:
576+
span.record_exception(e, escaped=True)
577+
span.finish()
578+
579+
span.assert_matches(
580+
error=1,
581+
meta={
582+
"error.message": str(exc),
583+
"error.type": "%s.%s" % (exc.__class__.__module__, exc.__class__.__name__),
584+
},
585+
)
586+
span.assert_span_event_count(1)
587+
span.assert_span_event_attributes(
588+
0, {"exception.type": "builtins.RuntimeError", "exception.message": "bim", "exception.escaped": True}
589+
)
590+
536591

537592
@pytest.mark.parametrize(
538593
"value,assertion",

tests/utils.py

+23
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,29 @@ def assert_metrics(self, metrics, exact=False):
848848
self, key, self._metrics[key], value
849849
)
850850

851+
def assert_span_event_count(self, count):
852+
"""Assert this span has the expected number of span_events"""
853+
assert len(self._events) == count, "Span count {0} != {1}".format(len(self._events), count)
854+
855+
def assert_span_event_attributes(self, event_idx, attrs):
856+
"""
857+
Assertion method to ensure this span's span event match as expected
858+
859+
Example::
860+
861+
span = TestSpan(span)
862+
span.assert_span_event(0, {"exception.type": "builtins.RuntimeError"})
863+
864+
:param event_idx: id of the span event
865+
:type event_idx: integer
866+
"""
867+
span_event_attrs = self._events[event_idx].attributes
868+
for name, value in attrs.items():
869+
assert name in span_event_attrs, "{0!r} does not have property {1!r}".format(span_event_attrs, name)
870+
assert span_event_attrs[name] == value, "{0!r} property {1}: {2!r} != {3!r}".format(
871+
span_event_attrs, name, span_event_attrs[name], value
872+
)
873+
851874

852875
class TracerSpanContainer(TestSpanContainer):
853876
"""

0 commit comments

Comments
 (0)