Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add record_exception method in datadog api #12185

Merged
merged 23 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
aaedab9
chore: update changelog for version 2.19.2 (#12088)
Yun-Kim Jan 27, 2025
e3045a1
fix(profiling): fix SystemError when collecting memory profiler event…
nsrip-dd Jan 27, 2025
55767a7
chore(tracing): refactor web server integrations to use the core modu…
wconti27 Jan 28, 2025
16d5280
ci(tracer): make serverless test unrequired (#12121)
christophe-papazian Jan 28, 2025
4f0bcb5
chore(asm): clean libddwaf loading (#12102)
christophe-papazian Jan 28, 2025
c4448ea
fix(llmobs): propagate distributed headers via signal dispatching, no…
Yun-Kim Jan 28, 2025
cb41f8e
feat(provider): expose context provider in ddtrace.trace (#12135)
mabdinur Jan 29, 2025
af9098c
chore(ci): skip non-linux OCI package creation (#12036)
randomanderson Jan 30, 2025
c93ff44
feat: add a record_exception method to the tracer
dubloom Jan 31, 2025
9943d70
chore: add record_exception_tests
dubloom Jan 31, 2025
296cff2
chore: enrich documentation and add release note
dubloom Jan 31, 2025
8cefbef
fix: unhappy dd-bot
dubloom Jan 31, 2025
349990f
fix: sphinx does not know the word unhandled
dubloom Feb 3, 2025
8202b1b
chore: change place of assert_span_event_count/_attributes
dubloom Feb 5, 2025
9ae4725
changes according to review
dubloom Feb 6, 2025
5f49dda
Merge branch 'main' into dubloom/dd-record-exception
dubloom Feb 10, 2025
601845b
chore: add return line in releasenotes
dubloom Feb 10, 2025
109beda
Merge branch 'main' into dubloom/dd-record-exception
dubloom Feb 12, 2025
cfb8d04
Merge branch 'main' into dubloom/dd-record-exception
dubloom Feb 13, 2025
1f7b436
review: quinna comments
dubloom Feb 13, 2025
771a84b
suggestions from munir
dubloom Feb 17, 2025
ab467c4
Merge branch 'main' into dubloom/dd-record-exception
dubloom Feb 17, 2025
b2a4a96
Merge branch 'main' into dubloom/dd-record-exception
dubloom Feb 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions ddtrace/_trace/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,51 @@ def set_exc_info(

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

def record_exception(
self,
exception: BaseException,
attributes: Optional[Dict[str, _JSONType]] = None,
timestamp: Optional[int] = None,
escaped=False,
) -> None:
"""
Records an exception as span event.
If the exception is uncaught, :obj:`escaped` should be set :obj:`True`. It
will tag the span with an error tuple.

:param Exception exception: the exception to record
:param dict attributes: optional attributes to add to the span event. It will override
the base attributes if :obj:`attributes` contains existing keys.
:param int timestamp: the timestamp of the span event. Will be set to now() if timestamp is :obj:`None`.
:param bool escaped: sets to :obj:`False` for a handled exception and :obj:`True` for a uncaught exception.
"""
if timestamp is None:
timestamp = time_ns()

exc_type, exc_val, exc_tb = type(exception), exception, exception.__traceback__

if escaped:
self.set_exc_info(exc_type, exc_val, exc_tb)

# get the traceback
buff = StringIO()
traceback.print_exception(exc_type, exc_val, exc_tb, file=buff, limit=config._span_traceback_max_size)
tb = buff.getvalue()

# Set exception attributes in a manner that is consistent with the opentelemetry sdk
# https://github.com/open-telemetry/opentelemetry-python/blob/v1.24.0/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py#L998
attrs = {
"exception.type": "%s.%s" % (exception.__class__.__module__, exception.__class__.__name__),
"exception.message": str(exception),
"exception.escaped": escaped,
"exception.stacktrace": tb,
}
if attributes:
# User provided attributes must take precedence over attrs
attrs.update(attributes)

self._add_event(name="recorded exception", attributes=attrs, timestamp=timestamp)

def _pprint(self) -> str:
"""Return a human readable version of the span."""
data = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
features:
- |
tracing: Introduces a record_exception method that adds an exception to a Span as a span event.
Refer to [Span.record_exception](https://ddtrace.readthedocs.io/en/stable/api.html#ddtrace.trace.Span.record_exception)
for more details.
55 changes: 55 additions & 0 deletions tests/tracer/test_span.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,61 @@ def test_span_pointers(self):
},
]

def test_span_record_exception(self):
span = self.start_span("span")
try:
raise RuntimeError("bim")
except RuntimeError as e:
span.record_exception(e)
span.finish()

span.assert_span_event_count(1)
span.assert_span_event_attributes(
0, {"exception.type": "builtins.RuntimeError", "exception.message": "bim", "exception.escaped": False}
)

def test_span_record_multiple_exceptions(self):
span = self.start_span("span")
try:
raise RuntimeError("bim")
except RuntimeError as e:
span.record_exception(e)

try:
raise RuntimeError("bam")
except RuntimeError as e:
span.record_exception(e)
span.finish()

span.assert_span_event_count(2)
span.assert_span_event_attributes(
0, {"exception.type": "builtins.RuntimeError", "exception.message": "bim", "exception.escaped": False}
)
span.assert_span_event_attributes(
1, {"exception.type": "builtins.RuntimeError", "exception.message": "bam", "exception.escaped": False}
)

def test_span_record_escaped_exception(self):
exc = RuntimeError("bim")
span = self.start_span("span")
try:
raise exc
except RuntimeError as e:
span.record_exception(e, escaped=True)
span.finish()

span.assert_matches(
error=1,
meta={
"error.message": str(exc),
"error.type": "%s.%s" % (exc.__class__.__module__, exc.__class__.__name__),
},
)
span.assert_span_event_count(1)
span.assert_span_event_attributes(
0, {"exception.type": "builtins.RuntimeError", "exception.message": "bim", "exception.escaped": True}
)


@pytest.mark.parametrize(
"value,assertion",
Expand Down
23 changes: 23 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,29 @@ def assert_metrics(self, metrics, exact=False):
self, key, self._metrics[key], value
)

def assert_span_event_count(self, count):
"""Assert this span has the expected number of span_events"""
assert len(self._events) == count, "Span count {0} != {1}".format(len(self._events), count)

def assert_span_event_attributes(self, event_idx, attrs):
"""
Assertion method to ensure this span's span event match as expected

Example::

span = TestSpan(span)
span.assert_span_event(0, {"exception.type": "builtins.RuntimeError"})

:param event_idx: id of the span event
:type event_idx: integer
"""
span_event_attrs = self._events[event_idx].attributes
for name, value in attrs.items():
assert name in span_event_attrs, "{0!r} does not have property {1!r}".format(span_event_attrs, name)
assert span_event_attrs[name] == value, "{0!r} property {1}: {2!r} != {3!r}".format(
span_event_attrs, name, span_event_attrs[name], value
)


class TracerSpanContainer(TestSpanContainer):
"""
Expand Down
Loading