Skip to content

Commit 803f582

Browse files
authored
Record exception on context manager exit (open-telemetry#1162)
1 parent 6fac795 commit 803f582

File tree

6 files changed

+94
-30
lines changed

6 files changed

+94
-30
lines changed

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

Lines changed: 13 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,15 @@ 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)
154162
finally:
155163
context.detach(token)
156164

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-
163165
if result is not None:
164166
if span.is_recording():
165167
span.set_attribute(
@@ -184,8 +186,8 @@ def _instrumented_requests_call(
184186
if span_callback is not None:
185187
span_callback(span, result)
186188

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

190192
return result
191193

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

Lines changed: 14 additions & 2 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.
@@ -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
@@ -14,6 +14,7 @@
1414
([#1203](https://github.com/open-telemetry/opentelemetry-python/pull/1203))
1515
- Protect access to Span implementation
1616
([#1188](https://github.com/open-telemetry/opentelemetry-python/pull/1188))
17+
- `start_as_current_span` and `use_span` can now optionally auto-record any exceptions raised inside the context manager. ([#1162](https://github.com/open-telemetry/opentelemetry-python/pull/1162))
1718

1819
## Version 0.13b0
1920

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__)
@@ -699,9 +703,12 @@ def start_as_current_span(
699703
kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL,
700704
attributes: types.Attributes = None,
701705
links: Sequence[trace_api.Link] = (),
706+
record_exception: bool = True,
702707
) -> Iterator[trace_api.Span]:
703-
span = self.start_span(name, parent, kind, attributes, links)
704-
return self.use_span(span, end_on_exit=True)
708+
span = self.start_span(name, parent, kind, attributes, links,)
709+
return self.use_span(
710+
span, end_on_exit=True, record_exception=record_exception
711+
)
705712

706713
def start_span( # pylint: disable=too-many-locals
707714
self,
@@ -780,7 +787,10 @@ def start_span( # pylint: disable=too-many-locals
780787

781788
@contextmanager
782789
def use_span(
783-
self, span: trace_api.Span, end_on_exit: bool = False
790+
self,
791+
span: trace_api.Span,
792+
end_on_exit: bool = False,
793+
record_exception: bool = True,
784794
) -> Iterator[trace_api.Span]:
785795
try:
786796
token = context_api.attach(context_api.set_value(SPAN_KEY, span))
@@ -790,20 +800,24 @@ def use_span(
790800
context_api.detach(token)
791801

792802
except Exception as error: # pylint: disable=broad-except
793-
if (
794-
isinstance(span, Span)
795-
and span.status is None
796-
and span._set_status_on_exception # pylint:disable=protected-access # noqa
797-
):
798-
span.set_status(
799-
Status(
800-
canonical_code=StatusCanonicalCode.UNKNOWN,
801-
description="{}: {}".format(
802-
type(error).__name__, error
803-
),
803+
# pylint:disable=protected-access
804+
if isinstance(span, Span):
805+
if record_exception:
806+
span.record_exception(error)
807+
808+
if span.status is None and span._set_status_on_exception:
809+
span.set_status(
810+
Status(
811+
canonical_code=getattr(
812+
error,
813+
EXCEPTION_STATUS_FIELD,
814+
StatusCanonicalCode.UNKNOWN,
815+
),
816+
description="{}: {}".format(
817+
type(error).__name__, error
818+
),
819+
)
804820
)
805-
)
806-
807821
raise
808822

809823
finally:

opentelemetry-sdk/tests/trace/test_trace.py

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

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

809841
def span_event_start_fmt(span_processor_name, span_name):
810842
return span_processor_name + ":" + span_name + ":start"

0 commit comments

Comments
 (0)