Skip to content

Commit

Permalink
Record exception on context manager exit
Browse files Browse the repository at this point in the history
This updates the tracer context manager to automatically record
exceptions as events on exit if an exception was raised within the
context manager's context.
  • Loading branch information
owais committed Oct 4, 2020
1 parent 14fad78 commit 2222d26
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,6 @@ def _instrumented_requests_call(
method = method.upper()
span_name = "HTTP {}".format(method)

exception = None

recorder = RequestsInstrumentor().metric_recorder

labels = {}
Expand All @@ -132,6 +130,7 @@ def _instrumented_requests_call(
with get_tracer(
__name__, __version__, tracer_provider
).start_as_current_span(span_name, kind=SpanKind.CLIENT) as span:
exception = None
with recorder.record_duration(labels):
if span.is_recording():
span.set_attribute("component", "http")
Expand All @@ -150,16 +149,15 @@ def _instrumented_requests_call(
result = call_wrapped() # *** PROCEED
except Exception as exc: # pylint: disable=W0703
exception = exc
setattr(
exception,
"_otel_status_code",
_exception_to_canonical_code(exception),
)
result = getattr(exc, "response", None)
finally:
context.detach(token)

if exception is not None and span.is_recording():
span.set_status(
Status(_exception_to_canonical_code(exception))
)
span.record_exception(exception)

if result is not None:
if span.is_recording():
span.set_attribute(
Expand All @@ -184,8 +182,8 @@ def _instrumented_requests_call(
if span_callback is not None:
span_callback(span, result)

if exception is not None:
raise exception.with_traceback(exception.__traceback__)
if exception is not None:
raise exception.with_traceback(exception.__traceback__)

return result

Expand Down
18 changes: 15 additions & 3 deletions opentelemetry-api/src/opentelemetry/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ def start_as_current_span(
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: typing.Sequence[Link] = (),
record_exception: bool = True,
) -> typing.Iterator["Span"]:
"""Context manager for creating a new span and set it
as the current span in this tracer's context.
Expand Down Expand Up @@ -310,7 +311,7 @@ def start_as_current_span(
is equivalent to::
span = tracer.start_span(name)
with tracer.use_span(span, end_on_exit=True):
with tracer.use_span(span, end_on_exit=True, record_exception=True):
do_work()
Args:
Expand All @@ -320,6 +321,8 @@ def start_as_current_span(
meaningful even if there is no parent.
attributes: The span's attributes.
links: Links span to other spans
record_exception: Whether to record any exceptions raised within the
context as error event on the span.
Yields:
The newly-created span.
Expand All @@ -328,7 +331,10 @@ def start_as_current_span(
@contextmanager # type: ignore
@abc.abstractmethod
def use_span(
self, span: "Span", end_on_exit: bool = False
self,
span: "Span",
end_on_exit: bool = False,
record_exception: bool = True,
) -> typing.Iterator[None]:
"""Context manager for setting the passed span as the
current span in the context, as well as resetting the
Expand All @@ -345,6 +351,8 @@ def use_span(
span: The span to start and make current.
end_on_exit: Whether to end the span automatically when leaving the
context manager.
record_exception: Whether to record any exceptions raised within the
context as error event on the span.
"""


Expand Down Expand Up @@ -375,13 +383,17 @@ def start_as_current_span(
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: typing.Sequence[Link] = (),
record_exception: bool = True,
) -> typing.Iterator["Span"]:
# pylint: disable=unused-argument,no-self-use
yield INVALID_SPAN

@contextmanager # type: ignore
def use_span(
self, span: "Span", end_on_exit: bool = False
self,
span: "Span",
end_on_exit: bool = False,
record_exception: bool = True,
) -> typing.Iterator[None]:
# pylint: disable=unused-argument,no-self-use
yield
Expand Down
1 change: 1 addition & 0 deletions opentelemetry-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
([#1105](https://github.com/open-telemetry/opentelemetry-python/pull/1120))
- Allow for Custom Trace and Span IDs Generation - `IdsGenerator` for TracerProvider
([#1153](https://github.com/open-telemetry/opentelemetry-python/pull/1153))
- `start_as_current_span` and `use_span` can now optionally auto-record any exceptions raised inside the context manager.

## Version 0.13b0

Expand Down
41 changes: 25 additions & 16 deletions opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,9 +687,12 @@ def start_as_current_span(
kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: Sequence[trace_api.Link] = (),
record_exception: bool = True,
) -> Iterator[trace_api.Span]:
span = self.start_span(name, parent, kind, attributes, links)
return self.use_span(span, end_on_exit=True)
span = self.start_span(name, parent, kind, attributes, links,)
return self.use_span(
span, end_on_exit=True, record_exception=record_exception
)

def start_span( # pylint: disable=too-many-locals
self,
Expand Down Expand Up @@ -768,7 +771,10 @@ def start_span( # pylint: disable=too-many-locals

@contextmanager
def use_span(
self, span: trace_api.Span, end_on_exit: bool = False
self,
span: trace_api.Span,
end_on_exit: bool = False,
record_exception: bool = False,
) -> Iterator[trace_api.Span]:
try:
token = context_api.attach(context_api.set_value(SPAN_KEY, span))
Expand All @@ -778,20 +784,23 @@ def use_span(
context_api.detach(token)

except Exception as error: # pylint: disable=broad-except
if (
isinstance(span, Span)
and span.status is None
and span._set_status_on_exception # pylint:disable=protected-access # noqa
):
span.set_status(
Status(
canonical_code=StatusCanonicalCode.UNKNOWN,
description="{}: {}".format(
type(error).__name__, error
),
)
# pylint:disable=protected-access
if isinstance(span, Span):
status_code = getattr(
error, "_otel_status_code", StatusCanonicalCode.UNKNOWN
)

if record_exception:
span.record_exception(error)

if span.status is None and span._set_status_on_exception:
span.set_status(
Status(
canonical_code=status_code,
description="{}: {}".format(
type(error).__name__, error
),
)
)
raise

finally:
Expand Down
32 changes: 32 additions & 0 deletions opentelemetry-sdk/tests/trace/test_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,38 @@ def test_record_exception(self):
exception_event.attributes["exception.stacktrace"],
)

def test_record_exception_context_manager(self):
try:
with self.tracer.start_as_current_span("span") as span:
raise RuntimeError("example error")
except RuntimeError:
pass
finally:
self.assertEqual(len(span.events), 1)
event = span.events[0]
self.assertEqual("exception", event.name)
self.assertEqual(
"RuntimeError", event.attributes["exception.type"]
)
self.assertEqual(
"example error", event.attributes["exception.message"]
)

stacktrace = """in test_record_exception_context_manager
raise RuntimeError("example error")
RuntimeError: example error"""
self.assertIn(stacktrace, event.attributes["exception.stacktrace"])

try:
with self.tracer.start_as_current_span(
"span", record_exception=False
) as span:
raise RuntimeError("example error")
except RuntimeError:
pass
finally:
self.assertEqual(len(span.events), 0)


def span_event_start_fmt(span_processor_name, span_name):
return span_processor_name + ":" + span_name + ":start"
Expand Down

0 comments on commit 2222d26

Please sign in to comment.