diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 1795192254f..dc3a1ae6a38 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -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. @@ -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. @@ -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. @@ -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. @@ -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 @@ -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 diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index c9b4c3538db..94f9beff919 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -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 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 13819ed35b0..886003f856a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -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, @@ -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 @@ -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 @@ -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 @@ -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() @@ -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: @@ -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 diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index fdf85ef19b5..0b211ec88df 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -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"