diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py index d0336184ed2..0dbd5ee4ccb 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py @@ -121,8 +121,6 @@ def _instrumented_requests_call( method = method.upper() span_name = "HTTP {}".format(method) - exception = None - recorder = RequestsInstrumentor().metric_recorder labels = {} @@ -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") @@ -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( @@ -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 diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 301314e28e1..7e0f6b9a3ed 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -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. @@ -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: @@ -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. @@ -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 @@ -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. """ @@ -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 diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 71864282885..70948cd7533 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -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 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 0134ec7e775..1ff53494f1a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -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, @@ -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)) @@ -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: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 8c5544578d4..dedc07bb59b 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -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"