Skip to content

Commit 5a72598

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 568641f commit 5a72598

File tree

6 files changed

+96
-31
lines changed

6 files changed

+96
-31
lines changed

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@
5050
from opentelemetry.instrumentation.requests.version import __version__
5151
from opentelemetry.instrumentation.utils import http_status_to_canonical_code
5252
from opentelemetry.trace import SpanKind, get_tracer
53-
from opentelemetry.trace.status import Status, StatusCanonicalCode
53+
from opentelemetry.trace.status import (
54+
EXCEPTION_STATUS_FIELD,
55+
Status,
56+
StatusCanonicalCode,
57+
)
5458

5559
# A key to a context variable to avoid creating duplicate spans when instrumenting
5660
# both, Session.request and Session.send, since Session.request calls into Session.send
@@ -121,8 +125,6 @@ def _instrumented_requests_call(
121125
method = method.upper()
122126
span_name = "HTTP {}".format(method)
123127

124-
exception = None
125-
126128
recorder = RequestsInstrumentor().metric_recorder
127129

128130
labels = {}
@@ -132,6 +134,7 @@ def _instrumented_requests_call(
132134
with get_tracer(
133135
__name__, __version__, tracer_provider
134136
).start_as_current_span(span_name, kind=SpanKind.CLIENT) as span:
137+
exception = None
135138
with recorder.record_duration(labels):
136139
if span.is_recording():
137140
span.set_attribute("component", "http")
@@ -150,16 +153,16 @@ def _instrumented_requests_call(
150153
result = call_wrapped() # *** PROCEED
151154
except Exception as exc: # pylint: disable=W0703
152155
exception = exc
156+
setattr(
157+
exception,
158+
EXCEPTION_STATUS_FIELD,
159+
_exception_to_canonical_code(exception),
160+
)
153161
result = getattr(exc, "response", None)
162+
154163
finally:
155164
context.detach(token)
156165

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-
163166
if result is not None:
164167
if span.is_recording():
165168
span.set_attribute(
@@ -184,8 +187,8 @@ def _instrumented_requests_call(
184187
if span_callback is not None:
185188
span_callback(span, result)
186189

187-
if exception is not None:
188-
raise exception.with_traceback(exception.__traceback__)
190+
if exception is not None:
191+
raise exception.with_traceback(exception.__traceback__)
189192

190193
return result
191194

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-api/src/opentelemetry/trace/status.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
logger = logging.getLogger(__name__)
2020

2121

22+
EXCEPTION_STATUS_FIELD = "_otel_status_code"
23+
24+
2225
class StatusCanonicalCode(enum.Enum):
2326
"""Represents the canonical set of status codes of a finished Span."""
2427

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: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@
4545
from opentelemetry.sdk.util.instrumentation import InstrumentationInfo
4646
from opentelemetry.trace import SpanContext
4747
from opentelemetry.trace.propagation import SPAN_KEY
48-
from opentelemetry.trace.status import Status, StatusCanonicalCode
48+
from opentelemetry.trace.status import (
49+
EXCEPTION_STATUS_FIELD,
50+
Status,
51+
StatusCanonicalCode,
52+
)
4953
from opentelemetry.util import time_ns, types
5054

5155
logger = logging.getLogger(__name__)
@@ -687,9 +691,12 @@ def start_as_current_span(
687691
kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL,
688692
attributes: types.Attributes = None,
689693
links: Sequence[trace_api.Link] = (),
694+
record_exception: bool = True,
690695
) -> Iterator[trace_api.Span]:
691-
span = self.start_span(name, parent, kind, attributes, links)
692-
return self.use_span(span, end_on_exit=True)
696+
span = self.start_span(name, parent, kind, attributes, links,)
697+
return self.use_span(
698+
span, end_on_exit=True, record_exception=record_exception
699+
)
693700

694701
def start_span( # pylint: disable=too-many-locals
695702
self,
@@ -768,7 +775,10 @@ def start_span( # pylint: disable=too-many-locals
768775

769776
@contextmanager
770777
def use_span(
771-
self, span: trace_api.Span, end_on_exit: bool = False
778+
self,
779+
span: trace_api.Span,
780+
end_on_exit: bool = False,
781+
record_exception: bool = True,
772782
) -> Iterator[trace_api.Span]:
773783
try:
774784
token = context_api.attach(context_api.set_value(SPAN_KEY, span))
@@ -778,20 +788,24 @@ def use_span(
778788
context_api.detach(token)
779789

780790
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-
),
791+
# pylint:disable=protected-access
792+
if isinstance(span, Span):
793+
if record_exception:
794+
span.record_exception(error)
795+
796+
if span.status is None and span._set_status_on_exception:
797+
span.set_status(
798+
Status(
799+
canonical_code=getattr(
800+
error,
801+
EXCEPTION_STATUS_FIELD,
802+
StatusCanonicalCode.UNKNOWN,
803+
),
804+
description="{}: {}".format(
805+
type(error).__name__, error
806+
),
807+
)
792808
)
793-
)
794-
795809
raise
796810

797811
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)