Skip to content

Commit 29f7ef6

Browse files
committed
Record exception on context manager exit
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.
1 parent 14fad78 commit 29f7ef6

File tree

5 files changed

+82
-30
lines changed

5 files changed

+82
-30
lines changed

instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,6 @@ def _instrumented_requests_call(
121121
method = method.upper()
122122
span_name = "HTTP {}".format(method)
123123

124-
exception = None
125-
126124
recorder = RequestsInstrumentor().metric_recorder
127125

128126
labels = {}
@@ -132,6 +130,7 @@ def _instrumented_requests_call(
132130
with get_tracer(
133131
__name__, __version__, tracer_provider
134132
).start_as_current_span(span_name, kind=SpanKind.CLIENT) as span:
133+
exception = None
135134
with recorder.record_duration(labels):
136135
if span.is_recording():
137136
span.set_attribute("component", "http")
@@ -150,16 +149,15 @@ def _instrumented_requests_call(
150149
result = call_wrapped() # *** PROCEED
151150
except Exception as exc: # pylint: disable=W0703
152151
exception = exc
152+
setattr(
153+
exception,
154+
"_otel_status_code",
155+
_exception_to_canonical_code(exception),
156+
)
153157
result = getattr(exc, "response", None)
154158
finally:
155159
context.detach(token)
156160

157-
if exception is not None and span.is_recording():
158-
span.set_status(
159-
Status(_exception_to_canonical_code(exception))
160-
)
161-
span.record_exception(exception)
162-
163161
if result is not None:
164162
if span.is_recording():
165163
span.set_attribute(
@@ -184,8 +182,8 @@ def _instrumented_requests_call(
184182
if span_callback is not None:
185183
span_callback(span, result)
186184

187-
if exception is not None:
188-
raise exception.with_traceback(exception.__traceback__)
185+
if exception is not None:
186+
raise exception.with_traceback(exception.__traceback__)
189187

190188
return result
191189

opentelemetry-api/src/opentelemetry/trace/__init__.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ def start_as_current_span(
282282
kind: SpanKind = SpanKind.INTERNAL,
283283
attributes: types.Attributes = None,
284284
links: typing.Sequence[Link] = (),
285+
record_exception: bool = True,
285286
) -> typing.Iterator["Span"]:
286287
"""Context manager for creating a new span and set it
287288
as the current span in this tracer's context.
@@ -310,7 +311,7 @@ def start_as_current_span(
310311
is equivalent to::
311312
312313
span = tracer.start_span(name)
313-
with tracer.use_span(span, end_on_exit=True):
314+
with tracer.use_span(span, end_on_exit=True, record_exception=True):
314315
do_work()
315316
316317
Args:
@@ -320,6 +321,8 @@ def start_as_current_span(
320321
meaningful even if there is no parent.
321322
attributes: The span's attributes.
322323
links: Links span to other spans
324+
record_exception: Whether to record any exceptions raised within the
325+
context as error event on the span.
323326
324327
Yields:
325328
The newly-created span.
@@ -328,7 +331,10 @@ def start_as_current_span(
328331
@contextmanager # type: ignore
329332
@abc.abstractmethod
330333
def use_span(
331-
self, span: "Span", end_on_exit: bool = False
334+
self,
335+
span: "Span",
336+
end_on_exit: bool = False,
337+
record_exception: bool = True,
332338
) -> typing.Iterator[None]:
333339
"""Context manager for setting the passed span as the
334340
current span in the context, as well as resetting the
@@ -345,6 +351,8 @@ def use_span(
345351
span: The span to start and make current.
346352
end_on_exit: Whether to end the span automatically when leaving the
347353
context manager.
354+
record_exception: Whether to record any exceptions raised within the
355+
context as error event on the span.
348356
"""
349357

350358

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

382391
@contextmanager # type: ignore
383392
def use_span(
384-
self, span: "Span", end_on_exit: bool = False
393+
self,
394+
span: "Span",
395+
end_on_exit: bool = False,
396+
record_exception: bool = True,
385397
) -> typing.Iterator[None]:
386398
# pylint: disable=unused-argument,no-self-use
387399
yield

opentelemetry-sdk/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
([#1105](https://github.com/open-telemetry/opentelemetry-python/pull/1120))
1111
- Allow for Custom Trace and Span IDs Generation - `IdsGenerator` for TracerProvider
1212
([#1153](https://github.com/open-telemetry/opentelemetry-python/pull/1153))
13+
- `start_as_current_span` and `use_span` can now optionally auto-record any exceptions raised inside the context manager.
1314

1415
## Version 0.13b0
1516

opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ class Span(trace_api.Span):
357357
this `Span`.
358358
"""
359359

360-
def __init__(
360+
def __init__( # pylint:disable=R0914
361361
self,
362362
name: str,
363363
context: trace_api.SpanContext,
@@ -687,9 +687,12 @@ def start_as_current_span(
687687
kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL,
688688
attributes: types.Attributes = None,
689689
links: Sequence[trace_api.Link] = (),
690+
record_exception: bool = True,
690691
) -> Iterator[trace_api.Span]:
691-
span = self.start_span(name, parent, kind, attributes, links)
692-
return self.use_span(span, end_on_exit=True)
692+
span = self.start_span(name, parent, kind, attributes, links,)
693+
return self.use_span(
694+
span, end_on_exit=True, record_exception=record_exception
695+
)
693696

694697
def start_span( # pylint: disable=too-many-locals
695698
self,
@@ -768,7 +771,10 @@ def start_span( # pylint: disable=too-many-locals
768771

769772
@contextmanager
770773
def use_span(
771-
self, span: trace_api.Span, end_on_exit: bool = False
774+
self,
775+
span: trace_api.Span,
776+
end_on_exit: bool = False,
777+
record_exception: bool = False,
772778
) -> Iterator[trace_api.Span]:
773779
try:
774780
token = context_api.attach(context_api.set_value(SPAN_KEY, span))
@@ -778,20 +784,23 @@ def use_span(
778784
context_api.detach(token)
779785

780786
except Exception as error: # pylint: disable=broad-except
781-
if (
782-
isinstance(span, Span)
783-
and span.status is None
784-
and span._set_status_on_exception # pylint:disable=protected-access # noqa
785-
):
786-
span.set_status(
787-
Status(
788-
canonical_code=StatusCanonicalCode.UNKNOWN,
789-
description="{}: {}".format(
790-
type(error).__name__, error
791-
),
792-
)
787+
# pylint:disable=protected-access
788+
if isinstance(span, Span):
789+
status_code = getattr(
790+
error, "_otel_status_code", StatusCanonicalCode.UNKNOWN
793791
)
794-
792+
if record_exception:
793+
span.record_exception(error)
794+
795+
if span.status is None and span._set_status_on_exception:
796+
span.set_status(
797+
Status(
798+
canonical_code=status_code,
799+
description="{}: {}".format(
800+
type(error).__name__, error
801+
),
802+
)
803+
)
795804
raise
796805

797806
finally:

opentelemetry-sdk/tests/trace/test_trace.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,38 @@ def test_record_exception(self):
800800
exception_event.attributes["exception.stacktrace"],
801801
)
802802

803+
def test_record_exception_context_manager(self):
804+
try:
805+
with self.tracer.start_as_current_span("span") as span:
806+
raise RuntimeError("example error")
807+
except RuntimeError:
808+
pass
809+
finally:
810+
self.assertEqual(len(span.events), 1)
811+
event = span.events[0]
812+
self.assertEqual("exception", event.name)
813+
self.assertEqual(
814+
"RuntimeError", event.attributes["exception.type"]
815+
)
816+
self.assertEqual(
817+
"example error", event.attributes["exception.message"]
818+
)
819+
820+
stacktrace = """in test_record_exception_context_manager
821+
raise RuntimeError("example error")
822+
RuntimeError: example error"""
823+
self.assertIn(stacktrace, event.attributes["exception.stacktrace"])
824+
825+
try:
826+
with self.tracer.start_as_current_span(
827+
"span", record_exception=False
828+
) as span:
829+
raise RuntimeError("example error")
830+
except RuntimeError:
831+
pass
832+
finally:
833+
self.assertEqual(len(span.events), 0)
834+
803835

804836
def span_event_start_fmt(span_processor_name, span_name):
805837
return span_processor_name + ":" + span_name + ":start"

0 commit comments

Comments
 (0)