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 Sep 25, 2020
1 parent c1ec444 commit 5c04297
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 14 deletions.
10 changes: 10 additions & 0 deletions opentelemetry-api/src/opentelemetry/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ def start_span(
links: typing.Sequence[Link] = (),
start_time: typing.Optional[int] = None,
set_status_on_exception: bool = True,
record_exception: bool = True,
) -> "Span":
"""Starts a span.
Expand Down Expand Up @@ -269,6 +270,9 @@ def start_span(
be automatically set to UNKNOWN when an uncaught exception is
raised in the span with block. The span status won't be set by
this mechanism if it was previousy set manually.
record_exception: Only relevant if the returned span is used
in a with/context manager. Records any exceptions raised inside
the context manager a span event.
Returns:
The newly-created span.
Expand All @@ -283,6 +287,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 @@ -320,6 +325,9 @@ 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: Only relevant if the returned span is used
in a with/context manager. Records any exceptions raised inside
the context manager a span event.
Yields:
The newly-created span.
Expand Down Expand Up @@ -361,6 +369,7 @@ def start_span(
links: typing.Sequence[Link] = (),
start_time: typing.Optional[int] = None,
set_status_on_exception: bool = True,
record_exception: bool = True,
) -> "Span":
# pylint: disable=unused-argument,no-self-use
return INVALID_SPAN
Expand All @@ -373,6 +382,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"]:
# pylint: disable=unused-argument,no-self-use
yield INVALID_SPAN
Expand Down
1 change: 1 addition & 0 deletions opentelemetry-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
([#1128](https://github.com/open-telemetry/opentelemetry-python/pull/1128))
- Add support for `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_SCHEDULE_DELAY_MILLIS`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` and `OTEL_BSP_EXPORT_TIMEOUT_MILLIS` environment variables
([#1105](https://github.com/open-telemetry/opentelemetry-python/pull/1120))
- `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: 27 additions & 14 deletions opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ class Span(trace_api.Span):
this `Span`.
"""

def __init__(
def __init__( # pylint:disable=R0914
self,
name: str,
context: trace_api.SpanContext,
Expand All @@ -372,6 +372,7 @@ def __init__(
span_processor: SpanProcessor = SpanProcessor(),
instrumentation_info: InstrumentationInfo = None,
set_status_on_exception: bool = True,
record_exception: bool = True,
) -> None:

self.name = name
Expand All @@ -382,6 +383,7 @@ def __init__(
self.resource = resource
self.kind = kind
self._set_status_on_exception = set_status_on_exception
self._record_exception = record_exception

self.span_processor = span_processor
self.status = None
Expand Down Expand Up @@ -705,8 +707,16 @@ 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)
span = self.start_span(
name,
parent,
kind,
attributes,
links,
record_exception=record_exception,
)
return self.use_span(span, end_on_exit=True)

def start_span( # pylint: disable=too-many-locals
Expand All @@ -718,6 +728,7 @@ def start_span( # pylint: disable=too-many-locals
links: Sequence[trace_api.Link] = (),
start_time: Optional[int] = None,
set_status_on_exception: bool = True,
record_exception: bool = True,
) -> trace_api.Span:
if parent is Tracer.CURRENT_SPAN:
parent = trace_api.get_current_span()
Expand Down Expand Up @@ -778,6 +789,7 @@ def start_span( # pylint: disable=too-many-locals
links=links,
instrumentation_info=self.instrumentation_info,
set_status_on_exception=set_status_on_exception,
record_exception=record_exception,
)
span.start(start_time=start_time)
else:
Expand All @@ -796,19 +808,20 @@ 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):
if span._record_exception:
span.record_exception(error)

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

raise

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 @@ -799,6 +799,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 5c04297

Please sign in to comment.