diff --git a/ddtrace/_trace/span.py b/ddtrace/_trace/span.py index 446239a8091..dba0df89c6c 100644 --- a/ddtrace/_trace/span.py +++ b/ddtrace/_trace/span.py @@ -552,6 +552,51 @@ def set_exc_info( core.dispatch("span.exception", (self, exc_type, exc_val, exc_tb)) + def record_exception( + self, + exception: Exception, + attributes: Optional[Dict[str, str]] = 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": str(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 = [ diff --git a/ddtrace/_trace/tracer.py b/ddtrace/_trace/tracer.py index 406f01a1654..6e595bbe7c1 100644 --- a/ddtrace/_trace/tracer.py +++ b/ddtrace/_trace/tracer.py @@ -1,13 +1,10 @@ import functools -from io import StringIO from itertools import chain import logging import os from os import environ from os import getpid from threading import RLock -from time import time_ns -import traceback from typing import Any from typing import Callable from typing import Dict @@ -1247,55 +1244,6 @@ def set_tags(self, tags: Dict[str, str]) -> None: """ self._tags.update(tags) - def record_exception( - self, - exception: Exception, - attributes: Optional[Dict[str, str]] = 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. - """ - current_span = self.current_span() - if current_span is None: - return - - if timestamp is None: - timestamp = time_ns() - - exc_type, exc_val, exc_tb = type(exception), exception, exception.__traceback__ - - if escaped: - current_span.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": str(escaped), - "exception.stacktrace": tb, - } - if attributes: - # User provided attributes must take precedence over attrs - attrs.update(attributes) - - current_span._add_event(name="recorded exception", attributes=attrs, timestamp=timestamp) - def shutdown(self, timeout: Optional[float] = None) -> None: """Shutdown the tracer and flush finished traces. Avoid calling shutdown multiple times. diff --git a/releasenotes/notes/feat-add-dd-record-exception-033fd0436dfd2723.yaml b/releasenotes/notes/feat-add-dd-record-exception-033fd0436dfd2723.yaml index e5f202e39fb..8a6231e3ce8 100644 --- a/releasenotes/notes/feat-add-dd-record-exception-033fd0436dfd2723.yaml +++ b/releasenotes/notes/feat-add-dd-record-exception-033fd0436dfd2723.yaml @@ -1,30 +1,6 @@ --- -#instructions: -# The style guide below provides explanations, instructions, and templates to write your own release note. -# Once finished, all irrelevant sections (including this instruction section) should be removed, -# and the release note should be committed with the rest of the changes. -# -# The main goal of a release note is to provide a brief overview of a change and provide actionable steps to the user. -# The release note should clearly communicate what the change is, why the change was made, and how a user can migrate their code. -# -# The release note should also clearly distinguish between announcements and user instructions. Use: -# * Past tense for previous/existing behavior (ex: ``resulted, caused, failed``) -# * Third person present tense for the change itself (ex: ``adds, fixes, upgrades``) -# * Active present infinitive for user instructions (ex: ``set, use, add``) -# -# Release notes should: -# * Use plain language -# * Be concise -# * Include actionable steps with the necessary code changes -# * Include relevant links (bug issues, upstream issues or release notes, documentation pages) -# * Use full sentences with sentence-casing and punctuation. -# * Before using Datadog specific acronyms/terminology, a release note must first introduce them with a definition. -# -# Release notes should not: -# * Be vague. Example: ``fixes an issue in tracing``. -# * Use overly technical language -# * Use dynamic links (``stable/latest/1.x`` URLs). Instead, use static links (specific version, commit hash) whenever possible so that they don't break in the future. features: - | - tracing: This introduces the record_exception tracer api. It allows to manually add an exception as a span event. - It also allows to tag a span with an error tuple of the recorded exception by setting the escaped parameter as true. \ No newline at end of file + tracing: 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. \ No newline at end of file diff --git a/tests/tracer/test_span.py b/tests/tracer/test_span.py index 1725f0d7675..5d1c6a3b367 100644 --- a/tests/tracer/test_span.py +++ b/tests/tracer/test_span.py @@ -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", diff --git a/tests/tracer/test_tracer.py b/tests/tracer/test_tracer.py index 5e68e6bc588..1c45f424679 100644 --- a/tests/tracer/test_tracer.py +++ b/tests/tracer/test_tracer.py @@ -164,81 +164,6 @@ def f(): ), ) - def test_tracer_record_exception(self): - @self.tracer.wrap() - def f(): - try: - raise RuntimeError("bim") - except RuntimeError as e: - self.tracer.record_exception(e) - - f() - self.assert_structure( - dict( - name="tests.tracer.test_tracer.f", - error=0, - ), - ) - self.spans[0].assert_span_event_count(1) - self.spans[0].assert_span_event_attributes( - 0, {"exception.type": "builtins.RuntimeError", "exception.message": "bim", "exception.escaped": "False"} - ) - - def test_tracer_record_multiple_exceptions(self): - @self.tracer.wrap() - def f(): - try: - raise RuntimeError("bim") - except RuntimeError as e: - self.tracer.record_exception(e) - - try: - raise RuntimeError("bam") - except RuntimeError as e: - self.tracer.record_exception(e) - - f() - self.assert_structure( - dict( - name="tests.tracer.test_tracer.f", - error=0, - ) - ) - self.spans[0].assert_span_event_count(2) - self.spans[0].assert_span_event_attributes( - 0, {"exception.type": "builtins.RuntimeError", "exception.message": "bim", "exception.escaped": "False"} - ) - self.spans[0].assert_span_event_attributes( - 1, {"exception.type": "builtins.RuntimeError", "exception.message": "bam", "exception.escaped": "False"} - ) - - def test_tracer_record_escaped_exception(self): - exc = RuntimeError("bim") - - @self.tracer.wrap() - def f(): - try: - raise exc - except RuntimeError as e: - self.tracer.record_exception(e, escaped=True) - - f() - - self.assert_structure( - dict( - name="tests.tracer.test_tracer.f", - error=1, - meta={ - "error.message": str(exc), - "error.type": "%s.%s" % (exc.__class__.__module__, exc.__class__.__name__), - }, - ), - ) - self.spans[0].assert_span_event_count(1) - self.spans[0].assert_span_event_attributes( - 0, {"exception.type": "builtins.RuntimeError", "exception.message": "bim", "exception.escaped": "True"} - ) - def test_tracer_wrap_multiple_calls(self): @self.tracer.wrap() def f():