Skip to content

Commit

Permalink
changes according to review
Browse files Browse the repository at this point in the history
  • Loading branch information
dubloom committed Feb 6, 2025
1 parent 8202b1b commit 9ae4725
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 154 deletions.
45 changes: 45 additions & 0 deletions ddtrace/_trace/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
52 changes: 0 additions & 52 deletions ddtrace/_trace/tracer.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
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.
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
75 changes: 0 additions & 75 deletions tests/tracer/test_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down

0 comments on commit 9ae4725

Please sign in to comment.