Skip to content
This repository has been archived by the owner on May 23, 2023. It is now read-only.

Implement ScopeManager for in-process propagation (updated) #64

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 67 additions & 12 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ The work of instrumentation libraries generally consists of three steps:
Span object in the process. If the request does not contain an active trace,
the service starts a new trace and a new *root* Span.
2. The service needs to store the current Span in some request-local storage,
where it can be retrieved from when a child Span must be created, e.g. in case
of the service making an RPC to another service.
(called ``Span`` *activation*) where it can be retrieved from when a child Span must
be created, e.g. in case of the service making an RPC to another service.
3. When making outbound calls to another service, the current Span must be
retrieved from request-local storage, a child span must be created (e.g., by
using the ``start_child_span()`` helper), and that child span must be embedded
into the outbound request (e.g., using HTTP headers) via OpenTracing's
inject/extract API.

Below are the code examples for steps 1 and 3. Implementation of request-local
storage needed for step 2 is specific to the service and/or frameworks /
instrumentation libraries it is using (TODO: reference to other OSS projects
with examples of instrumentation).
Below are the code examples for the previously mentioned steps. Implementation
of request-local storage needed for step 2 is specific to the service and/or frameworks /
instrumentation libraries it is using, exposed as a ``ScopeManager`` child contained
as ``Tracer.scope_manager``. See details below.

Inbound request
^^^^^^^^^^^^^^^
Expand All @@ -56,12 +56,12 @@ Somewhere in your server's request handler code:

def handle_request(request):
span = before_request(request, opentracing.tracer)
# use span as Context Manager to ensure span.finish() will be called
with span:
# store span in some request-local storage
with RequestContext(span):
# actual business logic
handle_request_for_real(request)
# store span in some request-local storage using Tracer.scope_manager,
# using the returned `Scope` as Context Manager to ensure
# `Span` will be cleared and (in this case) `Span.finish()` be called.
with tracer.scope_manager.activate(span, True) as scope:
# actual business logic
handle_request_for_real(request)


def before_request(request, tracer):
Expand Down Expand Up @@ -141,6 +141,61 @@ Somewhere in your service that's about to make an outgoing call:

return outbound_span

Scope and within-process propagation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

For getting/setting the current active ``Span`` in the used request-local storage,
OpenTracing requires that every ``Tracer`` contains a ``ScopeManager`` that grants
access to the active ``Span`` through a ``Scope``. Any ``Span`` may be transferred to
another task or thread, but not ``Scope``.

.. code-block:: python

# Access to the active span is straightforward.
scope = tracer.scope_manager.active()
if scope is not None:
scope.span.set_tag('...', '...')

The common case starts a ``Scope`` that's automatically registered for intra-process
propagation via ``ScopeManager``.

Note that ``start_active_span('...', True)`` finishes the span on ``Scope.close()``
(``start_active_span('...', False)`` does not finish it, in contrast).

.. code-block:: python

# Manual activation of the Span.
span = tracer.start_span(operation_name='someWork')
with tracer.scope_manager.activate(span, True) as scope:
# Do things.

# Automatic activation of the Span.
# finish_on_close is a required parameter.
with tracer.start_active_span('someWork', finish_on_close=True) as scope:
# Do things.

# Handling done through a try construct:
span = tracer.start_span(operation_name='someWork')
scope = tracer.scope_manager.activate(span, True)
try:
# Do things.
except Exception as e:
scope.set_tag('error', '...')
finally:
scope.finish()

**If there is a Scope, it will act as the parent to any newly started Span** unless
the programmer passes ``ignore_active_span=True`` at ``start_span()``/``start_active_span()``
time or specified parent context explicitly:

.. code-block:: python

scope = tracer.start_active_span('someWork', ignore_active_span=True)

Each service/framework ought to provide a specific ``ScopeManager`` implementation
that relies on their own request-local storage (thread-local storage, or coroutine-based storage
for asynchronous frameworks, for example).

Development
-----------

Expand Down
6 changes: 6 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Classes
.. autoclass:: opentracing.SpanContext
:members:

.. autoclass:: opentracing.Scope
:members:

.. autoclass:: opentracing.ScopeManager
:members:

.. autoclass:: opentracing.Tracer
:members:

Expand Down
2 changes: 2 additions & 0 deletions opentracing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
from __future__ import absolute_import
from .span import Span # noqa
from .span import SpanContext # noqa
from .scope import Scope # noqa
from .scope_manager import ScopeManager # noqa
from .tracer import child_of # noqa
from .tracer import follows_from # noqa
from .tracer import Reference # noqa
Expand Down
168 changes: 164 additions & 4 deletions opentracing/harness/api_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from __future__ import absolute_import
import time

import mock
import time
import pytest

import opentracing
Expand All @@ -39,11 +40,78 @@ def check_baggage_values(self):
"""If true, the test will validate Baggage items by storing and
retrieving them from the trace context. If false, it will only attempt
to store and retrieve the Baggage items to check the API compliance,
but not actually validate stored values. The latter mode is only
but not actually validate stored values. The latter mode is only
useful for no-op tracer.
"""
return True

def check_scope_manager(self):
"""If true, the test suite will validate the `ScopeManager` propagation
to ensure correct parenting. If false, it will only use the API without
asserting. The latter mode is only useful for no-op tracer.
"""
return True

def is_parent(self, parent, span):
"""Utility method that must be defined by Tracer implementers to define
how the test suite can check when a `Span` is a parent of another one.
It depends by the underlying implementation that is not part of the
OpenTracing API.
"""
return False

def test_start_active_span(self):
# the first usage returns a `Scope` that wraps a root `Span`
tracer = self.tracer()
scope = tracer.start_active_span('Fry', False)

assert scope.span is not None
if self.check_scope_manager():
assert self.is_parent(None, scope.span)

def test_start_active_span_parent(self):
# ensure the `ScopeManager` provides the right parenting
tracer = self.tracer()
with tracer.start_active_span('Fry', False) as parent:
with tracer.start_active_span('Farnsworth', False) as child:
if self.check_scope_manager():
assert self.is_parent(parent.span, child.span)

def test_start_active_span_ignore_active_span(self):
# ensure the `ScopeManager` ignores the active `Scope`
# if the flag is set
tracer = self.tracer()
with tracer.start_active_span('Fry', False) as parent:
with tracer.start_active_span('Farnsworth', False,
ignore_active_span=True) as child:
if self.check_scope_manager():
assert not self.is_parent(parent.span, child.span)

def test_start_active_span_finish_on_close(self):
# ensure a `Span` is finished when the `Scope` close
tracer = self.tracer()
scope = tracer.start_active_span('Fry', False)
with mock.patch.object(scope.span, 'finish') as finish:
scope.close()

assert finish.call_count == 0

def test_start_active_span_not_finish_on_close(self):
# a `Span` is not finished when the flag is set
tracer = self.tracer()
scope = tracer.start_active_span('Fry', True)
with mock.patch.object(scope.span, 'finish') as finish:
scope.close()

if self.check_scope_manager():
assert finish.call_count == 1

def test_scope_as_context_manager(self):
tracer = self.tracer()

with tracer.start_active_span('antiquing', False) as scope:
assert scope.span is not None

def test_start_span(self):
tracer = self.tracer()
span = tracer.start_span(operation_name='Fry')
Expand All @@ -54,6 +122,24 @@ def test_start_span(self):
payload={'hospital': 'Brooklyn Pre-Med Hospital',
'city': 'Old New York'})

def test_start_span_propagation(self):
# `start_span` must inherit the current active `Scope` span
tracer = self.tracer()
with tracer.start_active_span('Fry', False) as parent:
with tracer.start_span(operation_name='Farnsworth') as child:
if self.check_scope_manager():
assert self.is_parent(parent.span, child)

def test_start_span_propagation_ignore_active_span(self):
# `start_span` doesn't inherit the current active `Scope` span
# if the flag is set
tracer = self.tracer()
with tracer.start_active_span('Fry', False) as parent:
with tracer.start_span(operation_name='Farnsworth',
ignore_active_span=True) as child:
if self.check_scope_manager():
assert not self.is_parent(parent.span, child)

def test_start_span_with_parent(self):
tracer = self.tracer()
parent_span = tracer.start_span(operation_name='parent')
Expand Down Expand Up @@ -83,19 +169,20 @@ def test_set_operation_name(self):
span.finish()

def test_span_as_context_manager(self):
tracer = self.tracer()
finish = {'called': False}

def mock_finish(*_):
finish['called'] = True

with self.tracer().start_span(operation_name='antiquing') as span:
with tracer.start_span(operation_name='antiquing') as span:
setattr(span, 'finish', mock_finish)
assert finish['called'] is True

# now try with exception
finish['called'] = False
try:
with self.tracer().start_span(operation_name='antiquing') as span:
with tracer.start_span(operation_name='antiquing') as span:
setattr(span, 'finish', mock_finish)
raise ValueError()
except ValueError:
Expand Down Expand Up @@ -206,3 +293,76 @@ def test_unknown_format(self):
span.tracer.inject(span.context, custom_format, {})
with pytest.raises(opentracing.UnsupportedFormatException):
span.tracer.extract(custom_format, {})

def test_tracer_start_active_span_scope(self):
# the Tracer ScopeManager should store the active Scope
tracer = self.tracer()
scope = tracer.start_active_span('Fry', False)

if self.check_scope_manager():
assert tracer.scope_manager.active == scope

scope.close()

def test_tracer_start_active_span_nesting(self):
# when a Scope is closed, the previous one must be activated
tracer = self.tracer()
with tracer.start_active_span('Fry', False) as parent:
with tracer.start_active_span('Farnsworth', False):
pass

if self.check_scope_manager():
assert tracer.scope_manager.active == parent

if self.check_scope_manager():
assert tracer.scope_manager.active is None

def test_tracer_start_active_span_nesting_finish_on_close(self):
# finish_on_close must be correctly handled
tracer = self.tracer()
parent = tracer.start_active_span('Fry', False)
with mock.patch.object(parent.span, 'finish') as finish:
with tracer.start_active_span('Farnsworth', False):
pass
parent.close()

assert finish.call_count == 0

if self.check_scope_manager():
assert tracer.scope_manager.active is None

def test_tracer_start_active_span_wrong_close_order(self):
# only the active `Scope` can be closed
tracer = self.tracer()
parent = tracer.start_active_span('Fry', False)
child = tracer.start_active_span('Farnsworth', False)
parent.close()

if self.check_scope_manager():
assert tracer.scope_manager.active == child

def test_tracer_start_span_scope(self):
# the Tracer ScopeManager should not store the new Span
tracer = self.tracer()
span = tracer.start_span(operation_name='Fry')

if self.check_scope_manager():
assert tracer.scope_manager.active is None

span.finish()

def test_tracer_scope_manager_active(self):
# a `ScopeManager` has no scopes in its initial state
tracer = self.tracer()

if self.check_scope_manager():
assert tracer.scope_manager.active is None

def test_tracer_scope_manager_activate(self):
# a `ScopeManager` should activate any `Span`
tracer = self.tracer()
span = tracer.start_span(operation_name='Fry')
tracer.scope_manager.activate(span, False)

if self.check_scope_manager():
assert tracer.scope_manager.active.span == span
Loading