From bf8034188073662651a18fc24d8d04fdd1478560 Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Mon, 30 Sep 2019 02:35:57 +0200 Subject: [PATCH 1/5] Refactor current span handling for newly created spans. 1. Make Tracer.start_span() simply create and start the Span, without setting it as the current instance. 2. Add an extra Tracer.start_as_current_span() to create the Span and set it as the current instance automatically. --- .../src/opentelemetry/trace/__init__.py | 88 ++++++++++--- .../src/opentelemetry/sdk/trace/__init__.py | 17 ++- opentelemetry-sdk/tests/trace/test_trace.py | 119 +++++++++++++----- 3 files changed, 171 insertions(+), 53 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 094255aa1e0..b30be2d13bf 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -26,16 +26,16 @@ to use the API package alone without a supporting implementation. The tracer supports creating spans that are "attached" or "detached" from the -context. By default, new spans are "attached" to the context in that they are +context. New spans are "attached" to the context in that they are created as children of the currently active span, and the newly-created span -becomes the new active span:: +can optionally become the new active span:: from opentelemetry.trace import tracer # Create a new root span, set it as the current span in context - with tracer.start_span("parent"): + with tracer.start_as_current_span("parent"): # Attach a new child and update the current span - with tracer.start_span("child"): + with tracer.start_as_current_span("child"): do_work(): # Close child span, set parent as current # Close parent span, set default span as current @@ -62,6 +62,7 @@ """ import enum +import types as python_types import typing from contextlib import contextmanager @@ -226,6 +227,26 @@ def is_recording_events(self) -> bool: events with the add_event operation and attributes using set_attribute. """ + def __enter__(self) -> "Span": + """Invoked when `Span` is used as a context manager. + + Returns the `Span` itself. + """ + return self + + def __exit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]], + exc_val: typing.Optional[BaseException], + exc_tb: typing.Optional[python_types.TracebackType], + ) -> bool: + """Ends context manager and calls end() on the `Span`. + + Returns False. + """ + self.end() + return False + class TraceOptions(int): """A bitmask that represents options specific to the trace. @@ -376,30 +397,63 @@ def get_current_span(self) -> "Span": # pylint: disable=no-self-use return INVALID_SPAN - @contextmanager # type: ignore def start_span( self, name: str, parent: ParentSpan = CURRENT_SPAN, kind: SpanKind = SpanKind.INTERNAL, - ) -> typing.Iterator["Span"]: - """Context manager for span creation. + ) -> "Span": + """Starts a span. - Create a new span. Start the span and set it as the current span in - this tracer's context. + Create a new span. Start the span without setting it as the current + span in this tracer's context. By default the current span will be used as parent, but an explicit parent can also be specified, either a `Span` or a `SpanContext`. If the specified value is `None`, the created span will be a root span. - On exiting the context manager stop the span and set its parent as the + On exiting the context manager ends the span. + + Example:: + + # tracer.get_current_span() will be used as the implicit parent. + # If none is found, the created span will be a root instance. + with tracer.start_span("two") as child: + child.add_event("child's event") + + Applications that need to set the newly created span as the current + instance should use :meth:`start_as_current_span` instead. + + Args: + name: The name of the span to be created. + parent: The span's parent. Defaults to the current span. + kind: The span's kind (relationship to parent). Note that is + meaningful even if there is no parent. + + Returns: + The newly-created span. + """ + # pylint: disable=unused-argument,no-self-use + return INVALID_SPAN + + @contextmanager # type: ignore + def start_as_current_span( + self, + name: str, + parent: ParentSpan = CURRENT_SPAN, + kind: SpanKind = SpanKind.INTERNAL, + ) -> typing.Iterator["Span"]: + """Context manager for creating a new span and set it + as the current span in this tracer's context. + + On exiting the context manager stops the span and set its parent as the current span. Example:: - with tracer.start_span("one") as parent: + with tracer.start_as_current_span("one") as parent: parent.add_event("parent's event") - with tracer.start_span("two") as child: + with tracer.start_as_current_span("two") as child: child.add_event("child's event") tracer.get_current_span() # returns child tracer.get_current_span() # returns parent @@ -407,15 +461,14 @@ def start_span( This is a convenience method for creating spans attached to the tracer's context. Applications that need more control over the span - lifetime should use :meth:`create_span` instead. For example:: + lifetime should use :meth:`start_span` instead. For example:: - with tracer.start_span(name) as span: + with tracer.start_as_current_span(name) as span: do_work() is equivalent to:: - span = tracer.create_span(name) - span.start() + span = tracer.start_span(name) with tracer.use_span(span, end_on_exit=True): do_work() @@ -428,6 +481,7 @@ def start_span( Yields: The newly-created span. """ + # pylint: disable=unused-argument,no-self-use yield INVALID_SPAN @@ -451,7 +505,7 @@ def create_span( Applications that need to create spans detached from the tracer's context should use this method. - with tracer.start_span(name) as span: + with tracer.start_as_current_span(name) as span: do_work() This is equivalent to:: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index f4124e36949..2f0fdc77591 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -321,18 +321,29 @@ def get_current_span(self): """See `opentelemetry.trace.Tracer.get_current_span`.""" return self._current_span_slot.get() - @contextmanager def start_span( self, name: str, parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, - ) -> typing.Iterator["Span"]: + ) -> "Span": """See `opentelemetry.trace.Tracer.start_span`.""" span = self.create_span(name, parent, kind) span.start() - with self.use_span(span, end_on_exit=True): + return span + + @contextmanager + def start_as_current_span( + self, + name: str, + parent: trace_api.ParentSpan = trace_api.Tracer.CURRENT_SPAN, + kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, + ) -> typing.Iterator["Span"]: + """See `opentelemetry.trace.Tracer.start_as_current_span`.""" + + span = self.start_span(name, parent, kind) + with self.use_span(span, True): yield span def create_span( diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 378534453c2..76ea4705c3c 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -31,17 +31,17 @@ def test_start_span_implicit(self): self.assertIsNone(tracer.get_current_span()) - with tracer.start_span("root") as root: - self.assertIs(tracer.get_current_span(), root) + root = tracer.start_span("root") + self.assertIsNotNone(root.start_time) + self.assertIsNone(root.end_time) + self.assertEqual(root.kind, trace_api.SpanKind.INTERNAL) - self.assertIsNotNone(root.start_time) - self.assertIsNone(root.end_time) - self.assertEqual(root.kind, trace_api.SpanKind.INTERNAL) + with tracer.use_span(root, True): + self.assertIs(tracer.get_current_span(), root) with tracer.start_span( "child", kind=trace_api.SpanKind.CLIENT ) as child: - self.assertIs(tracer.get_current_span(), child) self.assertIs(child.parent, root) self.assertEqual(child.kind, trace_api.SpanKind.CLIENT) @@ -63,9 +63,9 @@ def test_start_span_implicit(self): root_context.trace_options, child_context.trace_options ) - # After exiting the child's scope the parent should become the - # current span again. - self.assertIs(tracer.get_current_span(), root) + # Verify start_span() did not set the current span. + self.assertIs(tracer.get_current_span(), root) + self.assertIsNotNone(child.end_time) self.assertIsNone(tracer.get_current_span()) @@ -81,26 +81,25 @@ def test_start_span_explicit(self): self.assertIsNone(tracer.get_current_span()) + root = tracer.start_span("root") + self.assertIsNotNone(root.start_time) + self.assertIsNone(root.end_time) + # Test with the implicit root span - with tracer.start_span("root") as root: + with tracer.use_span(root, True): self.assertIs(tracer.get_current_span(), root) - self.assertIsNotNone(root.start_time) - self.assertIsNone(root.end_time) - with tracer.start_span("stepchild", other_parent) as child: - # The child should become the current span as usual, but its - # parent should be the one passed in, not the - # previously-current span. - self.assertIs(tracer.get_current_span(), child) + # The child's parent should be the one passed in, + # not the current span. self.assertNotEqual(child.parent, root) self.assertIs(child.parent, other_parent) self.assertIsNotNone(child.start_time) self.assertIsNone(child.end_time) - # The child should inherit its context fromr the explicit - # parent, not the previously-current span. + # The child should inherit its context from the explicit + # parent, not the current span. child_context = child.get_context() self.assertEqual(other_parent.trace_id, child_context.trace_id) self.assertNotEqual( @@ -113,6 +112,60 @@ def test_start_span_explicit(self): other_parent.trace_options, child_context.trace_options ) + # Verify start_span() did not set the current span. + self.assertIs(tracer.get_current_span(), root) + + # Verify ending the child did not set the current span. + self.assertIs(tracer.get_current_span(), root) + self.assertIsNotNone(child.end_time) + + def test_start_as_current_span_implicit(self): + tracer = trace.Tracer("test_start_as_current_span_implicit") + + self.assertIsNone(tracer.get_current_span()) + + with tracer.start_as_current_span("root") as root: + self.assertIs(tracer.get_current_span(), root) + + with tracer.start_as_current_span("child") as child: + self.assertIs(tracer.get_current_span(), child) + self.assertIs(child.parent, root) + + # After exiting the child's scope the parent should become the + # current span again. + self.assertIs(tracer.get_current_span(), root) + self.assertIsNotNone(child.end_time) + + self.assertIsNone(tracer.get_current_span()) + self.assertIsNotNone(root.end_time) + + def test_start_as_current_span_explicit(self): + tracer = trace.Tracer("test_start_as_current_span_explicit") + + other_parent = trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + ) + + self.assertIsNone(tracer.get_current_span()) + + # Test with the implicit root span + with tracer.start_as_current_span("root") as root: + self.assertIs(tracer.get_current_span(), root) + + self.assertIsNotNone(root.start_time) + self.assertIsNone(root.end_time) + + with tracer.start_as_current_span( + "stepchild", other_parent + ) as child: + # The child should become the current span as usual, but its + # parent should be the one passed in, not the + # previously-current span. + self.assertIs(tracer.get_current_span(), child) + self.assertNotEqual(child.parent, root) + self.assertIs(child.parent, other_parent) + # After exiting the child's scope the last span on the stack should # become current, not the child's parent. self.assertNotEqual(tracer.get_current_span(), other_parent) @@ -143,7 +196,7 @@ def test_span_members(self): self.assertIsNone(tracer.get_current_span()) - with tracer.start_span("root") as root: + with tracer.start_as_current_span("root") as root: # attributes root.set_attribute("component", "http") root.set_attribute("http.method", "GET") @@ -253,7 +306,7 @@ def test_ended_span(self): span_id=trace.generate_span_id(), ) - with tracer.start_span("root") as root: + with tracer.start_as_current_span("root") as root: # everything should be empty at the beginning self.assertEqual(len(root.attributes), 0) self.assertEqual(len(root.events), 0) @@ -312,9 +365,9 @@ def test_span_processor(self): sp1 = MySpanProcessor("SP1", spans_calls_list) sp2 = MySpanProcessor("SP2", spans_calls_list) - with tracer.start_span("foo"): - with tracer.start_span("bar"): - with tracer.start_span("baz"): + with tracer.start_as_current_span("foo"): + with tracer.start_as_current_span("bar"): + with tracer.start_as_current_span("baz"): pass # at this point lists must be empty @@ -323,13 +376,13 @@ def test_span_processor(self): # add single span processor tracer.add_span_processor(sp1) - with tracer.start_span("foo"): + with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) - with tracer.start_span("bar"): + with tracer.start_as_current_span("bar"): expected_list.append(span_event_start_fmt("SP1", "bar")) - with tracer.start_span("baz"): + with tracer.start_as_current_span("baz"): expected_list.append(span_event_start_fmt("SP1", "baz")) expected_list.append(span_event_end_fmt("SP1", "baz")) @@ -346,15 +399,15 @@ def test_span_processor(self): # go for multiple span processors tracer.add_span_processor(sp2) - with tracer.start_span("foo"): + with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) expected_list.append(span_event_start_fmt("SP2", "foo")) - with tracer.start_span("bar"): + with tracer.start_as_current_span("bar"): expected_list.append(span_event_start_fmt("SP1", "bar")) expected_list.append(span_event_start_fmt("SP2", "bar")) - with tracer.start_span("baz"): + with tracer.start_as_current_span("baz"): expected_list.append(span_event_start_fmt("SP1", "baz")) expected_list.append(span_event_start_fmt("SP2", "baz")) @@ -379,9 +432,9 @@ def test_add_span_processor_after_span_creation(self): # Span processors are created but not added to the tracer yet sp = MySpanProcessor("SP1", spans_calls_list) - with tracer.start_span("foo"): - with tracer.start_span("bar"): - with tracer.start_span("baz"): + with tracer.start_as_current_span("foo"): + with tracer.start_as_current_span("bar"): + with tracer.start_as_current_span("baz"): # add span processor after spans have been created tracer.add_span_processor(sp) From 6f7fdd3fa12cebbfc1a4c433cc8ca3c325a8589d Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Thu, 3 Oct 2019 16:20:58 +0200 Subject: [PATCH 2/5] Update opentelemetry-api/src/opentelemetry/trace/__init__.py Co-Authored-By: Chris Kleinknecht --- opentelemetry-api/src/opentelemetry/trace/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index b30be2d13bf..25860b27b7b 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -240,7 +240,7 @@ def __exit__( exc_val: typing.Optional[BaseException], exc_tb: typing.Optional[python_types.TracebackType], ) -> bool: - """Ends context manager and calls end() on the `Span`. + """Ends context manager and calls ``end()`` on the `Span`. Returns False. """ From a9dd24ee89717229329d65322dd458d06d8d7366 Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Thu, 10 Oct 2019 04:51:51 +0200 Subject: [PATCH 3/5] Apply feedback. --- .../src/opentelemetry/trace/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 25860b27b7b..f47be987a45 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -239,8 +239,8 @@ def __exit__( exc_type: typing.Optional[typing.Type[BaseException]], exc_val: typing.Optional[BaseException], exc_tb: typing.Optional[python_types.TracebackType], - ) -> bool: - """Ends context manager and calls ``end()`` on the `Span`. + ) -> typing.Optional[bool]: + """Ends context manager and calls end() on the `Span`. Returns False. """ @@ -412,13 +412,14 @@ def start_span( parent can also be specified, either a `Span` or a `SpanContext`. If the specified value is `None`, the created span will be a root span. - On exiting the context manager ends the span. + The span can be used as context manager. On exiting, the span will be + ended. Example:: # tracer.get_current_span() will be used as the implicit parent. # If none is found, the created span will be a root instance. - with tracer.start_span("two") as child: + with tracer.start_span("one") as child: child.add_event("child's event") Applications that need to set the newly created span as the current @@ -436,7 +437,6 @@ def start_span( # pylint: disable=unused-argument,no-self-use return INVALID_SPAN - @contextmanager # type: ignore def start_as_current_span( self, name: str, @@ -483,7 +483,7 @@ def start_as_current_span( """ # pylint: disable=unused-argument,no-self-use - yield INVALID_SPAN + return INVALID_SPAN def create_span( self, From feb9334323eb60f25dbd9d5350b6fb4c9ed05d3e Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Thu, 10 Oct 2019 21:52:57 +0200 Subject: [PATCH 4/5] Restore the contextmanager behavior for API's start_span(). This is for keeping mypy happy (mostly). --- opentelemetry-api/src/opentelemetry/trace/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index ecd412e37ce..f174e63ddfc 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -437,6 +437,7 @@ def start_span( # pylint: disable=unused-argument,no-self-use return INVALID_SPAN + @contextmanager # type: ignore def start_as_current_span( self, name: str, @@ -483,7 +484,7 @@ def start_as_current_span( """ # pylint: disable=unused-argument,no-self-use - return INVALID_SPAN + yield INVALID_SPAN def create_span( self, From aaad6c350d97d713986de23619a0036c130be49d Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Thu, 10 Oct 2019 22:00:08 +0200 Subject: [PATCH 5/5] Apply more feedback. --- opentelemetry-api/src/opentelemetry/trace/__init__.py | 2 +- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index f174e63ddfc..1b0a3e758f7 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -240,7 +240,7 @@ def __exit__( exc_val: typing.Optional[BaseException], exc_tb: typing.Optional[python_types.TracebackType], ) -> typing.Optional[bool]: - """Ends context manager and calls end() on the `Span`. + """Ends context manager and calls `end` on the `Span`. Returns False. """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 64566e554c7..c7b33a353db 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -334,7 +334,6 @@ def start_span( span.start() return span - @contextmanager def start_as_current_span( self, name: str, @@ -344,8 +343,7 @@ def start_as_current_span( """See `opentelemetry.trace.Tracer.start_as_current_span`.""" span = self.start_span(name, parent, kind) - with self.use_span(span, True): - yield span + return self.use_span(span, end_on_exit=True) def create_span( self,