From 28f2f43b3b43b6bddd6277c81ed2adab69b04b2e Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Fri, 16 Feb 2018 03:32:23 +0100 Subject: [PATCH 01/15] Implement ScopeManager for in-process propagation (updated) (#64) * add Scope/ScopeManager interfaces * provide start_active_span and start_span with ScopeManager propagation * updating docstrings and tests --- README.rst | 79 ++++++++++++--- docs/api.rst | 6 ++ opentracing/__init__.py | 2 + opentracing/harness/api_check.py | 168 ++++++++++++++++++++++++++++++- opentracing/scope.py | 69 +++++++++++++ opentracing/scope_manager.py | 63 ++++++++++++ opentracing/tracer.py | 65 +++++++++++- tests/test_api.py | 3 + tests/test_api_check_mixin.py | 47 +++++++++ tests/test_scope.py | 46 +++++++++ tests/test_scope_manager.py | 34 +++++++ 11 files changed, 564 insertions(+), 18 deletions(-) create mode 100644 opentracing/scope.py create mode 100644 opentracing/scope_manager.py create mode 100644 tests/test_scope.py create mode 100644 tests/test_scope_manager.py diff --git a/README.rst b/README.rst index 4ac10af..5d216db 100644 --- a/README.rst +++ b/README.rst @@ -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 ^^^^^^^^^^^^^^^ @@ -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): @@ -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 ----------- diff --git a/docs/api.rst b/docs/api.rst index c7f9d11..5e1bf6f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -9,6 +9,12 @@ Classes .. autoclass:: opentracing.SpanContext :members: +.. autoclass:: opentracing.Scope + :members: + +.. autoclass:: opentracing.ScopeManager + :members: + .. autoclass:: opentracing.Tracer :members: diff --git a/opentracing/__init__.py b/opentracing/__init__.py index bc8bc26..4faf9de 100644 --- a/opentracing/__init__.py +++ b/opentracing/__init__.py @@ -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 diff --git a/opentracing/harness/api_check.py b/opentracing/harness/api_check.py index 36c6fc4..f46d011 100644 --- a/opentracing/harness/api_check.py +++ b/opentracing/harness/api_check.py @@ -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 @@ -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') @@ -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') @@ -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: @@ -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 diff --git a/opentracing/scope.py b/opentracing/scope.py new file mode 100644 index 0000000..9aa321a --- /dev/null +++ b/opentracing/scope.py @@ -0,0 +1,69 @@ +# Copyright (c) 2017 The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + + +class Scope(object): + """A `Scope` formalizes the activation and deactivation of a `Span`, + usually from a CPU standpoint. Many times a `Span` will be extant (in that + `Span#finish()` has not been called) despite being in a non-runnable state + from a CPU/scheduler standpoint. For instance, a `Span` representing the + client side of an RPC will be unfinished but blocked on IO while the RPC is + still outstanding. A `Scope` defines when a given `Span` is scheduled + and on the path. + """ + def __init__(self, manager, span): + """Initializes a `Scope` for the given `Span` object. + + :param manager: the `ScopeManager` that created this `Scope` + :param span: the `Span` used for this `Scope` + """ + self._manager = manager + self._span = span + + @property + def span(self): + """Returns the `Span` wrapped by this `Scope`.""" + return self._span + + @property + def manager(self): + """Returns the `ScopeManager` that created this `Scope`.""" + return self._manager + + def close(self): + """Marks the end of the active period for this `Scope`, + updating `ScopeManager#active` in the process. + + NOTE: Calling `close()` more than once on a single `Scope` instance + leads to undefined behavior. + """ + pass + + def __enter__(self): + """Allows `Scope` to be used inside a Python Context Manager.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Calls `close()` when the execution is outside the Python + Context Manager. + """ + self.close() diff --git a/opentracing/scope_manager.py b/opentracing/scope_manager.py new file mode 100644 index 0000000..72b0d10 --- /dev/null +++ b/opentracing/scope_manager.py @@ -0,0 +1,63 @@ +# Copyright (c) 2017 The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + +from .span import Span, SpanContext +from .scope import Scope + + +class ScopeManager(object): + """The `ScopeManager` interface abstracts both the activation of `Span` + instances (via `ScopeManager#activate(span, finish_on_close)`) and + access to an active `Span` / `Scope` (via `ScopeManager#active`). + """ + def __init__(self): + # TODO: `tracer` should not be None, but we don't have a reference; + # should we move the NOOP SpanContext, Span, Scope to somewhere + # else so that they're globally reachable? + self._noop_span = Span(tracer=None, context=SpanContext()) + self._noop_scope = Scope(self, self._noop_span) + + def activate(self, span, finish_on_close): + """Makes a `Span` instance active. + + :param span: the `Span` that should become active. + :param finish_on_close: whether span should be automatically + finished when `Scope#close()` is called. + + :return: a `Scope` instance to control the end of the active period for + the `Span`. It is a programming error to neglect to call + `Scope#close()` on the returned instance. + """ + return self._noop_scope + + @property + def active(self): + """Returns the currently active `Scope` which can be used to access the + currently active `Scope#span`. + + If there is a non-null `Scope`, its wrapped `Span` becomes an implicit + parent of any newly-created `Span` at `Tracer#start_active_span()` + time. + + :return: the `Scope` that is active, or `None` if not available. + """ + return self._noop_scope diff --git a/opentracing/tracer.py b/opentracing/tracer.py index 57709c3..42ba1e2 100644 --- a/opentracing/tracer.py +++ b/opentracing/tracer.py @@ -24,6 +24,8 @@ from collections import namedtuple from .span import Span from .span import SpanContext +from .scope import Scope +from .scope_manager import ScopeManager from .propagation import Format, UnsupportedFormatException @@ -37,16 +39,73 @@ class Tracer(object): _supported_formats = [Format.TEXT_MAP, Format.BINARY, Format.HTTP_HEADERS] - def __init__(self): + def __init__(self, scope_manager=None): + self._scope_manager = ScopeManager() if scope_manager is None \ + else scope_manager self._noop_span_context = SpanContext() self._noop_span = Span(tracer=self, context=self._noop_span_context) + self._noop_scope = Scope(self._scope_manager, self._noop_span) + + @property + def scope_manager(self): + """ScopeManager accessor""" + return self._scope_manager + + def start_active_span(self, + operation_name, + finish_on_close, + child_of=None, + references=None, + tags=None, + start_time=None, + ignore_active_span=False): + """Returns a newly started and activated `Scope`. + + The returned `Scope` supports with-statement contexts. For example: + + with tracer.start_active_span('...', False) as scope: + scope.span.set_tag('http.method', 'GET') + do_some_work() + # Span is not finished outside the `Scope` `with`. + + It's also possible to finish the `Span` when the `Scope` context + expires: + + with tracer.start_active_span('...', True) as scope: + scope.span.set_tag('http.method', 'GET') + do_some_work() + # Span finishes when the Scope is closed as + # `finish_on_close` is `True` + + :param operation_name: name of the operation represented by the new + span from the perspective of the current service. + :param finish_on_close: whether span should automatically be finished + when `Scope#close()` is called. + :param child_of: (optional) a Span or SpanContext instance representing + the parent in a REFERENCE_CHILD_OF Reference. If specified, the + `references` parameter must be omitted. + :param references: (optional) a list of Reference objects that identify + one or more parent SpanContexts. (See the Reference documentation + for detail). + :param tags: an optional dictionary of Span Tags. The caller gives up + ownership of that dictionary, because the Tracer may use it as-is + to avoid extra data copying. + :param start_time: an explicit Span start time as a unix timestamp per + time.time(). + :param ignore_active_span: (optional) an explicit flag that ignores + the current active `Scope` and creates a root `Span`. + + :return: a `Scope`, already registered via the `ScopeManager`. + """ + return self._noop_scope def start_span(self, operation_name=None, child_of=None, references=None, tags=None, - start_time=None): + start_time=None, + ignore_active_span=False): """Starts and returns a new Span representing a unit of work. @@ -82,6 +141,8 @@ def start_span(self, to avoid extra data copying. :param start_time: an explicit Span start time as a unix timestamp per time.time() + :param ignore_active_span: an explicit flag that ignores the current + active `Scope` and creates a root `Span`. :return: Returns an already-started Span instance. """ diff --git a/tests/test_api.py b/tests/test_api.py index 7e800e2..f32df62 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -33,3 +33,6 @@ def tracer(self): def check_baggage_values(self): return False + + def check_scope_manager(self): + return False diff --git a/tests/test_api_check_mixin.py b/tests/test_api_check_mixin.py index a1c0cad..03463aa 100644 --- a/tests/test_api_check_mixin.py +++ b/tests/test_api_check_mixin.py @@ -33,6 +33,10 @@ def test_default_baggage_check_mode(self): api_check = APICompatibilityCheckMixin() assert api_check.check_baggage_values() is True + def test_default_scope_manager_check_mode(self): + api_check = APICompatibilityCheckMixin() + assert api_check.check_scope_manager() is True + def test_baggage_check_works(self): api_check = APICompatibilityCheckMixin() setattr(api_check, 'tracer', lambda: Tracer()) @@ -45,3 +49,46 @@ def test_baggage_check_works(self): # second check that assert on empty baggage will fail too with self.assertRaises(AssertionError): api_check.test_context_baggage() + + def test_scope_manager_check_works(self): + api_check = APICompatibilityCheckMixin() + setattr(api_check, 'tracer', lambda: Tracer()) + + # these tests are expected to succeed + api_check.test_start_active_span_ignore_active_span() + api_check.test_start_span_propagation_ignore_active_span() + + # no-op tracer doesn't have a ScopeManager implementation + # so these tests are expected to work, but asserts to fail + with self.assertRaises(AssertionError): + api_check.test_start_active_span() + + with self.assertRaises(AssertionError): + api_check.test_start_active_span_parent() + + with self.assertRaises(AssertionError): + api_check.test_start_span_propagation() + + with self.assertRaises(AssertionError): + api_check.test_tracer_start_active_span_scope() + + with self.assertRaises(AssertionError): + api_check.test_tracer_start_active_span_nesting() + + with self.assertRaises(AssertionError): + api_check.test_tracer_start_active_span_nesting_finish_on_close() + + with self.assertRaises(AssertionError): + api_check.test_tracer_start_active_span_wrong_close_order() + + with self.assertRaises(AssertionError): + api_check.test_tracer_start_span_scope() + + with self.assertRaises(AssertionError): + api_check.test_tracer_scope_manager_active() + + with self.assertRaises(AssertionError): + api_check.test_tracer_scope_manager_activate() + + with self.assertRaises(AssertionError): + api_check.test_start_active_span_not_finish_on_close() diff --git a/tests/test_scope.py b/tests/test_scope.py new file mode 100644 index 0000000..f4a536b --- /dev/null +++ b/tests/test_scope.py @@ -0,0 +1,46 @@ +# Copyright (c) 2017 The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + +import mock + +from opentracing.scope_manager import ScopeManager +from opentracing.tracer import Tracer +from opentracing.scope import Scope +from opentracing.span import Span, SpanContext + + +def test_scope_wrapper(): + # ensure `Scope` wraps the `Span` argument + span = Span(tracer=Tracer(), context=SpanContext()) + scope = Scope(ScopeManager, span) + assert scope.span == span + + +def test_scope_context_manager(): + # ensure `Scope` can be used in a Context Manager that + # calls the `close()` method + span = Span(tracer=Tracer(), context=SpanContext()) + scope = Scope(ScopeManager(), span) + with mock.patch.object(scope, 'close') as close: + with scope: + pass + assert close.call_count == 1 diff --git a/tests/test_scope_manager.py b/tests/test_scope_manager.py new file mode 100644 index 0000000..01ae0cd --- /dev/null +++ b/tests/test_scope_manager.py @@ -0,0 +1,34 @@ +# Copyright (c) 2017 The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + +from opentracing.scope_manager import ScopeManager +from opentracing.tracer import Tracer +from opentracing.span import Span, SpanContext + + +def test_scope_manager(): + # ensure the activation returns the noop `Scope` that is always active + scope_manager = ScopeManager() + span = Span(tracer=Tracer(), context=SpanContext()) + scope = scope_manager.activate(span, False) + assert scope == scope_manager._noop_scope + assert scope == scope_manager.active From 90fea0bb4612584acdfbfc34fa70f46a8ba348a0 Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Tue, 20 Feb 2018 13:46:37 +0100 Subject: [PATCH 02/15] Preparing release 2.0.0rc1 --- CHANGELOG.rst | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cc78b77..7a9fb4d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,12 @@ History ======= +2.0.0rc1 (2018-02-20) +--------------------- + +- Implement ScopeManager for in-process propagation. + + 1.3.0 (2018-01-14) ------------------ diff --git a/setup.py b/setup.py index fb631fb..8341e36 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='opentracing', - version='1.3.0', + version='2.0.0rc1', author='The OpenTracing Authors', author_email='opentracing@googlegroups.com', description='OpenTracing API for Python. See documentation at http://opentracing.io', From 5493ad57a975395d5e078b75b36424f93215c1fc Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Mon, 26 Mar 2018 19:59:20 +0200 Subject: [PATCH 03/15] Tracer.active_span as a shorthand for Tracer.scope_manager.active.span. (#69) --- opentracing/harness/api_check.py | 9 +++++++++ opentracing/tracer.py | 11 +++++++++++ tests/test_noop_tracer.py | 5 +++++ 3 files changed, 25 insertions(+) diff --git a/opentracing/harness/api_check.py b/opentracing/harness/api_check.py index f46d011..fee2c93 100644 --- a/opentracing/harness/api_check.py +++ b/opentracing/harness/api_check.py @@ -60,6 +60,15 @@ def is_parent(self, parent, span): """ return False + def test_active_span(self): + tracer = self.tracer() + span = tracer.start_span('Fry') + + if self.check_scope_manager(): + assert tracer.active_span is None + with tracer.scope_manager.activate(span, True): + assert tracer.active_span is span + def test_start_active_span(self): # the first usage returns a `Scope` that wraps a root `Span` tracer = self.tracer() diff --git a/opentracing/tracer.py b/opentracing/tracer.py index 42ba1e2..8386665 100644 --- a/opentracing/tracer.py +++ b/opentracing/tracer.py @@ -51,6 +51,17 @@ def scope_manager(self): """ScopeManager accessor""" return self._scope_manager + @property + def active_span(self): + """Provides access to the the active Span. This is a shorthand for + Tracer.scope_manager.active.span, and None will be returned if + Scope.span is None. + + :return: returns the active Span. + """ + scope = self._scope_manager.active + return None if scope is None else scope.span + def start_active_span(self, operation_name, finish_on_close, diff --git a/tests/test_noop_tracer.py b/tests/test_noop_tracer.py index cf87176..bbccf58 100644 --- a/tests/test_noop_tracer.py +++ b/tests/test_noop_tracer.py @@ -31,3 +31,8 @@ def test_tracer(): child = tracer.start_span(operation_name='child', references=child_of(span)) assert span == child + + +def test_tracer_active_span(): + tracer = Tracer() + assert tracer.active_span is tracer.scope_manager.active.span From 41a1e8a11eebf45add103e9b19c9396608adacaa Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Mon, 26 Mar 2018 20:00:18 +0200 Subject: [PATCH 04/15] Have start_active_span() finish_on_span=True as default. (#67) --- README.rst | 4 +-- opentracing/harness/api_check.py | 48 +++++++++++++++++++------------- opentracing/tracer.py | 22 ++++++++------- tests/test_api_check_mixin.py | 5 ++-- 4 files changed, 46 insertions(+), 33 deletions(-) diff --git a/README.rst b/README.rst index 5d216db..e8f1ede 100644 --- a/README.rst +++ b/README.rst @@ -159,8 +159,8 @@ another task or thread, but not ``Scope``. 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). +Note that ``start_active_span('...')`` automatically finishes the span on ``Scope.close()`` +(``start_active_span('...', finish_on_close=False)`` does not finish it, in contrast). .. code-block:: python diff --git a/opentracing/harness/api_check.py b/opentracing/harness/api_check.py index fee2c93..b9ec246 100644 --- a/opentracing/harness/api_check.py +++ b/opentracing/harness/api_check.py @@ -72,7 +72,7 @@ def test_active_span(self): 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) + scope = tracer.start_active_span('Fry') assert scope.span is not None if self.check_scope_manager(): @@ -81,8 +81,8 @@ def test_start_active_span(self): 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: + with tracer.start_active_span('Fry') as parent: + with tracer.start_active_span('Farnsworth') as child: if self.check_scope_manager(): assert self.is_parent(parent.span, child.span) @@ -90,25 +90,35 @@ 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, + with tracer.start_active_span('Fry') as parent: + with tracer.start_active_span('Farnsworth', 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): + def test_start_active_span_not_finish_on_close(self): # ensure a `Span` is finished when the `Scope` close tracer = self.tracer() - scope = tracer.start_active_span('Fry', False) + scope = tracer.start_active_span('Fry', finish_on_close=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): + def test_start_active_span_finish_on_close(self): # a `Span` is not finished when the flag is set tracer = self.tracer() - scope = tracer.start_active_span('Fry', True) + scope = tracer.start_active_span('Fry', finish_on_close=True) + with mock.patch.object(scope.span, 'finish') as finish: + scope.close() + + if self.check_scope_manager(): + assert finish.call_count == 1 + + def test_start_active_span_default_finish_on_close(self): + # a `Span` is finished when no flag is set + tracer = self.tracer() + scope = tracer.start_active_span('Fry') with mock.patch.object(scope.span, 'finish') as finish: scope.close() @@ -118,7 +128,7 @@ def test_start_active_span_not_finish_on_close(self): def test_scope_as_context_manager(self): tracer = self.tracer() - with tracer.start_active_span('antiquing', False) as scope: + with tracer.start_active_span('antiquing') as scope: assert scope.span is not None def test_start_span(self): @@ -134,7 +144,7 @@ def test_start_span(self): 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_active_span('Fry') as parent: with tracer.start_span(operation_name='Farnsworth') as child: if self.check_scope_manager(): assert self.is_parent(parent.span, child) @@ -143,7 +153,7 @@ 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_active_span('Fry') as parent: with tracer.start_span(operation_name='Farnsworth', ignore_active_span=True) as child: if self.check_scope_manager(): @@ -306,7 +316,7 @@ def test_unknown_format(self): 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) + scope = tracer.start_active_span('Fry') if self.check_scope_manager(): assert tracer.scope_manager.active == scope @@ -316,8 +326,8 @@ def test_tracer_start_active_span_scope(self): 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): + with tracer.start_active_span('Fry') as parent: + with tracer.start_active_span('Farnsworth'): pass if self.check_scope_manager(): @@ -329,9 +339,9 @@ def test_tracer_start_active_span_nesting(self): 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) + parent = tracer.start_active_span('Fry', finish_on_close=False) with mock.patch.object(parent.span, 'finish') as finish: - with tracer.start_active_span('Farnsworth', False): + with tracer.start_active_span('Farnsworth'): pass parent.close() @@ -343,8 +353,8 @@ def test_tracer_start_active_span_nesting_finish_on_close(self): 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 = tracer.start_active_span('Fry') + child = tracer.start_active_span('Farnsworth') parent.close() if self.check_scope_manager(): diff --git a/opentracing/tracer.py b/opentracing/tracer.py index 8386665..232ab58 100644 --- a/opentracing/tracer.py +++ b/opentracing/tracer.py @@ -64,34 +64,34 @@ def active_span(self): def start_active_span(self, operation_name, - finish_on_close, child_of=None, references=None, tags=None, start_time=None, - ignore_active_span=False): + ignore_active_span=False, + finish_on_close=True): """Returns a newly started and activated `Scope`. The returned `Scope` supports with-statement contexts. For example: - with tracer.start_active_span('...', False) as scope: + with tracer.start_active_span('...') as scope: scope.span.set_tag('http.method', 'GET') do_some_work() - # Span is not finished outside the `Scope` `with`. + # Span.finish() is called as part of Scope deactivation through + # the with statement. - It's also possible to finish the `Span` when the `Scope` context + It's also possible to not finish the `Span` when the `Scope` context expires: - with tracer.start_active_span('...', True) as scope: + with tracer.start_active_span('...', + finish_on_close=False) as scope: scope.span.set_tag('http.method', 'GET') do_some_work() - # Span finishes when the Scope is closed as - # `finish_on_close` is `True` + # Span.finish() is not called as part of Scope deactivation as + # `finish_on_close` is `False`. :param operation_name: name of the operation represented by the new span from the perspective of the current service. - :param finish_on_close: whether span should automatically be finished - when `Scope#close()` is called. :param child_of: (optional) a Span or SpanContext instance representing the parent in a REFERENCE_CHILD_OF Reference. If specified, the `references` parameter must be omitted. @@ -105,6 +105,8 @@ def start_active_span(self, time.time(). :param ignore_active_span: (optional) an explicit flag that ignores the current active `Scope` and creates a root `Span`. + :param finish_on_close: whether span should automatically be finished + when `Scope#close()` is called. :return: a `Scope`, already registered via the `ScopeManager`. """ diff --git a/tests/test_api_check_mixin.py b/tests/test_api_check_mixin.py index 03463aa..dbde63f 100644 --- a/tests/test_api_check_mixin.py +++ b/tests/test_api_check_mixin.py @@ -58,7 +58,8 @@ def test_scope_manager_check_works(self): api_check.test_start_active_span_ignore_active_span() api_check.test_start_span_propagation_ignore_active_span() - # no-op tracer doesn't have a ScopeManager implementation + # no-op tracer has a no-op ScopeManager implementation, + # which means no *actual* propagation is done, # so these tests are expected to work, but asserts to fail with self.assertRaises(AssertionError): api_check.test_start_active_span() @@ -91,4 +92,4 @@ def test_scope_manager_check_works(self): api_check.test_tracer_scope_manager_activate() with self.assertRaises(AssertionError): - api_check.test_start_active_span_not_finish_on_close() + api_check.test_start_active_span_finish_on_close() From e0a8b759d52619e82a3228a16147ad7a3ab63561 Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Sat, 31 Mar 2018 16:03:42 +0200 Subject: [PATCH 05/15] Update MockTracer to use the Scopes API. (#77) This includes ThreadLocalScopeManager in the ext submodule. --- opentracing/ext/scope.py | 51 ++++++++++++++++++++++++++ opentracing/ext/scope_manager.py | 63 ++++++++++++++++++++++++++++++++ opentracing/mocktracer/tracer.py | 34 ++++++++++++++++- tests/ext/__init__.py | 0 tests/ext/test_scope.py | 59 ++++++++++++++++++++++++++++++ tests/ext/test_scope_manager.py | 62 +++++++++++++++++++++++++++++++ 6 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 opentracing/ext/scope.py create mode 100644 opentracing/ext/scope_manager.py create mode 100644 tests/ext/__init__.py create mode 100644 tests/ext/test_scope.py create mode 100644 tests/ext/test_scope_manager.py diff --git a/opentracing/ext/scope.py b/opentracing/ext/scope.py new file mode 100644 index 0000000..707a2dd --- /dev/null +++ b/opentracing/ext/scope.py @@ -0,0 +1,51 @@ +# Copyright (c) The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + +from opentracing import Scope + + +class ThreadLocalScope(Scope): + """ThreadLocalScope is an implementation of `opentracing.Scope` + using thread-local storage.""" + + def __init__(self, manager, span, finish_on_close): + """Initialize a `Scope` for the given `Span` object. + + :param span: the `Span` wrapped by this `Scope`. + :param finish_on_close: whether span should automatically be + finished when `Scope#close()` is called. + """ + super(ThreadLocalScope, self).__init__(manager, span) + self._finish_on_close = finish_on_close + self._to_restore = manager.active + + def close(self): + """Mark the end of the active period for this {@link Scope}, + updating ScopeManager#active in the process. + """ + if self.manager.active is not self: + return + + if self._finish_on_close: + self.span.finish() + + setattr(self._manager._tls_scope, 'active', self._to_restore) diff --git a/opentracing/ext/scope_manager.py b/opentracing/ext/scope_manager.py new file mode 100644 index 0000000..1be3d65 --- /dev/null +++ b/opentracing/ext/scope_manager.py @@ -0,0 +1,63 @@ +# Copyright (c) The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + +import threading + +from opentracing import ScopeManager + +from .scope import ThreadLocalScope + + +class ThreadLocalScopeManager(ScopeManager): + """ScopeManager implementation that stores the current active `Scope` + using thread-local storage. + """ + def __init__(self): + self._tls_scope = threading.local() + + def activate(self, span, finish_on_close): + """Make a `Span` instance active. + + :param span: the `Span` that should become active. + :param finish_on_close: whether span should automatically be + finished when `Scope#close()` is called. + + :return: a `Scope` instance to control the end of the active period for + the `Span`. It is a programming error to neglect to call + `Scope#close()` on the returned instance. + """ + scope = ThreadLocalScope(self, span, finish_on_close) + setattr(self._tls_scope, 'active', scope) + return scope + + @property + def active(self): + """Return the currently active `Scope` which can be used to access the + currently active `Scope#span`. + + If there is a non-null `Scope`, its wrapped `Span` becomes an implicit + parent of any newly-created `Span` at `Tracer#start_span()`/ + `Tracer#start_active_span()` time. + + :return: the `Scope` that is active, or `None` if not available. + """ + return getattr(self._tls_scope, 'active', None) diff --git a/opentracing/mocktracer/tracer.py b/opentracing/mocktracer/tracer.py index 1f5bf55..0f15287 100644 --- a/opentracing/mocktracer/tracer.py +++ b/opentracing/mocktracer/tracer.py @@ -24,6 +24,7 @@ import opentracing from opentracing import Format, Tracer from opentracing import UnsupportedFormatException +from opentracing.ext.scope_manager import ThreadLocalScopeManager from .context import SpanContext from .span import MockSpan @@ -31,15 +32,17 @@ class MockTracer(Tracer): - def __init__(self): + def __init__(self, scope_manager=None): """Initialize a MockTracer instance. By default, MockTracer registers propagators for Format.TEXT_MAP, Format.HTTP_HEADERS and Format.BINARY. The user should call register_propagator() for each additional inject/extract format. """ + scope_manager = ThreadLocalScopeManager() \ + if scope_manager is None else scope_manager + super(MockTracer, self).__init__(scope_manager) - super(MockTracer, self).__init__() self._propagators = {} self._finished_spans = [] self._spans_lock = Lock() @@ -83,6 +86,27 @@ def _generate_id(self): self._next_id += 1 return self._next_id + def start_active_span(self, + operation_name, + child_of=None, + references=None, + tags=None, + start_time=None, + ignore_active_span=False, + finish_on_close=True): + + # create a new Span + span = self.start_span( + operation_name=operation_name, + child_of=child_of, + references=references, + tags=tags, + start_time=start_time, + ignore_active_span=ignore_active_span, + ) + + return self.scope_manager.activate(span, finish_on_close) + def start_span(self, operation_name=None, child_of=None, @@ -103,6 +127,12 @@ def start_span(self, # TODO only the first reference is currently used parent_ctx = references[0].referenced_context + # retrieve the active SpanContext + if not ignore_active_span and parent_ctx is None: + scope = self.scope_manager.active + if scope is not None: + parent_ctx = scope.span.context + # Assemble the child ctx ctx = SpanContext(span_id=self._generate_id()) if parent_ctx is not None: diff --git a/tests/ext/__init__.py b/tests/ext/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ext/test_scope.py b/tests/ext/test_scope.py new file mode 100644 index 0000000..f1774ac --- /dev/null +++ b/tests/ext/test_scope.py @@ -0,0 +1,59 @@ +# Copyright (c) The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import +import mock + +from opentracing.span import Span +from opentracing.ext.scope_manager import ThreadLocalScopeManager + + +def test_ext_scope_implicit_stack(): + scope_manager = ThreadLocalScopeManager() + + background_span = mock.MagicMock(spec=Span) + foreground_span = mock.MagicMock(spec=Span) + + with scope_manager.activate(background_span, True) as background_scope: + assert background_scope is not None + + # Activate a new Scope on top of the background one. + with scope_manager.activate(foreground_span, True) as foreground_scope: + assert foreground_scope is not None + assert scope_manager.active is foreground_scope + + # And now the background_scope should be reinstated. + assert scope_manager.active is background_scope + + assert background_span.finish.call_count == 1 + assert foreground_span.finish.call_count == 1 + + assert scope_manager.active is None + + +def test_when_different_span_is_active(): + scope_manager = ThreadLocalScopeManager() + + span = mock.MagicMock(spec=Span) + active = scope_manager.activate(span, False) + scope_manager.activate(mock.MagicMock(spec=Span), False) + active.close() + + assert span.finish.call_count == 0 diff --git a/tests/ext/test_scope_manager.py b/tests/ext/test_scope_manager.py new file mode 100644 index 0000000..b19af9d --- /dev/null +++ b/tests/ext/test_scope_manager.py @@ -0,0 +1,62 @@ +# Copyright (c) The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import +import mock + +from opentracing.tracer import Tracer +from opentracing.ext.scope_manager import ThreadLocalScopeManager + + +def test_ext_scope_manager_missing_active(): + scope_manager = ThreadLocalScopeManager() + assert scope_manager.active is None + + +def test_ext_scope_manager_activate(): + scope_manager = ThreadLocalScopeManager() + tracer = Tracer() + span = tracer.start_span('test') + + with mock.patch.object(span, 'finish') as finish: + scope = scope_manager.activate(span, False) + assert scope is not None + assert scope_manager.active is scope + + scope.close() + assert finish.call_count == 0 + + assert scope_manager.active is None + + +def test_ext_scope_manager_finish_close(): + scope_manager = ThreadLocalScopeManager() + tracer = Tracer() + span = tracer.start_span('test') + + with mock.patch.object(span, 'finish') as finish: + scope = scope_manager.activate(span, True) + assert scope is not None + assert scope_manager.active is scope + + scope.close() + assert finish.call_count == 1 + + assert scope_manager.active is None From 2ffae7e45404d1000d6fd4aef24fb03b8f794462 Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Fri, 6 Apr 2018 17:07:35 +0200 Subject: [PATCH 06/15] Testbed import (#80) Python-examples import as testbed/ examples. --- Makefile | 7 +- README.rst | 5 + requirements-testbed.txt | 5 + setup.py | 8 +- testbed/README.md | 75 +++++++++ testbed/__init__.py | 1 + testbed/__main__.py | 50 ++++++ testbed/span_propagation.py | 158 ++++++++++++++++++ .../test_active_span_replacement/README.md | 18 ++ .../test_active_span_replacement/__init__.py | 0 .../test_asyncio.py | 52 ++++++ .../test_gevent.py | 48 ++++++ .../test_threads.py | 48 ++++++ .../test_tornado.py | 54 ++++++ testbed/test_client_server/README.md | 17 ++ testbed/test_client_server/__init__.py | 0 testbed/test_client_server/test_asyncio.py | 79 +++++++++ testbed/test_client_server/test_gevent.py | 77 +++++++++ testbed/test_client_server/test_threads.py | 76 +++++++++ testbed/test_client_server/test_tornado.py | 82 +++++++++ testbed/test_common_request_handler/README.md | 25 +++ .../test_common_request_handler/__init__.py | 0 .../request_handler.py | 38 +++++ .../test_asyncio.py | 129 ++++++++++++++ .../test_gevent.py | 114 +++++++++++++ .../test_threads.py | 113 +++++++++++++ .../test_tornado.py | 128 ++++++++++++++ testbed/test_late_span_finish/README.md | 16 ++ testbed/test_late_span_finish/__init__.py | 0 testbed/test_late_span_finish/test_asyncio.py | 50 ++++++ testbed/test_late_span_finish/test_gevent.py | 46 +++++ testbed/test_late_span_finish/test_threads.py | 44 +++++ testbed/test_late_span_finish/test_tornado.py | 52 ++++++ testbed/test_listener_per_request/README.md | 17 ++ testbed/test_listener_per_request/__init__.py | 0 .../response_listener.py | 6 + .../test_listener_per_request/test_asyncio.py | 46 +++++ .../test_listener_per_request/test_gevent.py | 44 +++++ .../test_listener_per_request/test_threads.py | 44 +++++ .../test_listener_per_request/test_tornado.py | 50 ++++++ testbed/test_multiple_callbacks/README.md | 40 +++++ testbed/test_multiple_callbacks/__init__.py | 0 .../test_multiple_callbacks/test_asyncio.py | 59 +++++++ .../test_multiple_callbacks/test_gevent.py | 53 ++++++ .../test_multiple_callbacks/test_threads.py | 60 +++++++ .../test_multiple_callbacks/test_tornado.py | 64 +++++++ testbed/test_nested_callbacks/README.md | 41 +++++ testbed/test_nested_callbacks/__init__.py | 0 testbed/test_nested_callbacks/test_asyncio.py | 58 +++++++ testbed/test_nested_callbacks/test_gevent.py | 50 ++++++ testbed/test_nested_callbacks/test_threads.py | 56 +++++++ testbed/test_nested_callbacks/test_tornado.py | 64 +++++++ .../test_subtask_span_propagation/README.md | 38 +++++ .../test_subtask_span_propagation/__init__.py | 0 .../test_asyncio.py | 35 ++++ .../test_gevent.py | 32 ++++ .../test_threads.py | 33 ++++ .../test_tornado.py | 40 +++++ testbed/testcase.py | 20 +++ testbed/utils.py | 88 ++++++++++ 60 files changed, 2651 insertions(+), 2 deletions(-) create mode 100644 requirements-testbed.txt create mode 100644 testbed/README.md create mode 100644 testbed/__init__.py create mode 100644 testbed/__main__.py create mode 100644 testbed/span_propagation.py create mode 100644 testbed/test_active_span_replacement/README.md create mode 100644 testbed/test_active_span_replacement/__init__.py create mode 100644 testbed/test_active_span_replacement/test_asyncio.py create mode 100644 testbed/test_active_span_replacement/test_gevent.py create mode 100644 testbed/test_active_span_replacement/test_threads.py create mode 100644 testbed/test_active_span_replacement/test_tornado.py create mode 100644 testbed/test_client_server/README.md create mode 100644 testbed/test_client_server/__init__.py create mode 100644 testbed/test_client_server/test_asyncio.py create mode 100644 testbed/test_client_server/test_gevent.py create mode 100644 testbed/test_client_server/test_threads.py create mode 100644 testbed/test_client_server/test_tornado.py create mode 100644 testbed/test_common_request_handler/README.md create mode 100644 testbed/test_common_request_handler/__init__.py create mode 100644 testbed/test_common_request_handler/request_handler.py create mode 100644 testbed/test_common_request_handler/test_asyncio.py create mode 100644 testbed/test_common_request_handler/test_gevent.py create mode 100644 testbed/test_common_request_handler/test_threads.py create mode 100644 testbed/test_common_request_handler/test_tornado.py create mode 100644 testbed/test_late_span_finish/README.md create mode 100644 testbed/test_late_span_finish/__init__.py create mode 100644 testbed/test_late_span_finish/test_asyncio.py create mode 100644 testbed/test_late_span_finish/test_gevent.py create mode 100644 testbed/test_late_span_finish/test_threads.py create mode 100644 testbed/test_late_span_finish/test_tornado.py create mode 100644 testbed/test_listener_per_request/README.md create mode 100644 testbed/test_listener_per_request/__init__.py create mode 100644 testbed/test_listener_per_request/response_listener.py create mode 100644 testbed/test_listener_per_request/test_asyncio.py create mode 100644 testbed/test_listener_per_request/test_gevent.py create mode 100644 testbed/test_listener_per_request/test_threads.py create mode 100644 testbed/test_listener_per_request/test_tornado.py create mode 100644 testbed/test_multiple_callbacks/README.md create mode 100644 testbed/test_multiple_callbacks/__init__.py create mode 100644 testbed/test_multiple_callbacks/test_asyncio.py create mode 100644 testbed/test_multiple_callbacks/test_gevent.py create mode 100644 testbed/test_multiple_callbacks/test_threads.py create mode 100644 testbed/test_multiple_callbacks/test_tornado.py create mode 100644 testbed/test_nested_callbacks/README.md create mode 100644 testbed/test_nested_callbacks/__init__.py create mode 100644 testbed/test_nested_callbacks/test_asyncio.py create mode 100644 testbed/test_nested_callbacks/test_gevent.py create mode 100644 testbed/test_nested_callbacks/test_threads.py create mode 100644 testbed/test_nested_callbacks/test_tornado.py create mode 100644 testbed/test_subtask_span_propagation/README.md create mode 100644 testbed/test_subtask_span_propagation/__init__.py create mode 100644 testbed/test_subtask_span_propagation/test_asyncio.py create mode 100644 testbed/test_subtask_span_propagation/test_gevent.py create mode 100644 testbed/test_subtask_span_propagation/test_threads.py create mode 100644 testbed/test_subtask_span_propagation/test_tornado.py create mode 100644 testbed/testcase.py create mode 100644 testbed/utils.py diff --git a/Makefile b/Makefile index 9dc015c..8724559 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ pytest := PYTHONDONTWRITEBYTECODE=1 py.test --tb short -rxs \ html_report := --cov-report=html test_args := --cov-report xml --cov-report term-missing -.PHONY: clean-pyc clean-build docs clean +.PHONY: clean-pyc clean-build docs clean testbed .DEFAULT_GOAL : help help: @@ -17,6 +17,7 @@ help: @echo "clean-test - remove test and coverage artifacts" @echo "lint - check style with flake8" @echo "test - run tests quickly with the default Python" + @echo "testbed - run testbed scenarios with the default Python" @echo "coverage - check code coverage quickly with the default Python" @echo "docs - generate Sphinx HTML documentation, including API docs" @echo "release - package and upload a release" @@ -29,6 +30,7 @@ check-virtual-env: bootstrap: check-virtual-env pip install -r requirements.txt pip install -r requirements-test.txt + pip install -r requirements-testbed.txt python setup.py develop clean: clean-build clean-pyc clean-test @@ -57,6 +59,9 @@ lint: test: $(pytest) $(test_args) +testbed: + python -m testbed + jenkins: pip install -r requirements.txt pip install -r requirements-test.txt diff --git a/README.rst b/README.rst index 16f5578..9c39c77 100644 --- a/README.rst +++ b/README.rst @@ -205,6 +205,11 @@ Tests make bootstrap make test +Testbed suite +^^^^^^^^^^^^^ + +A testbed suite designed to test API changes and experimental features is included under the *testbed* directory. For more information, see the `Testbed README `_. + Instrumentation Tests --------------------- diff --git a/requirements-testbed.txt b/requirements-testbed.txt new file mode 100644 index 0000000..3a943e2 --- /dev/null +++ b/requirements-testbed.txt @@ -0,0 +1,5 @@ +# add dependencies in setup.py + +-r requirements.txt + +-e .[testbed] diff --git a/setup.py b/setup.py index 8341e36..7842ee5 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,12 @@ 'pytest-mock', 'Sphinx', 'sphinx_rtd_theme' - ] + ], + 'testbed': [ + 'six>=1.10.0,<2.0', + 'gevent==1.2', + 'tornado', + ], + ':python_version == "2.7"': ['futures'], }, ) diff --git a/testbed/README.md b/testbed/README.md new file mode 100644 index 0000000..2ace9ea --- /dev/null +++ b/testbed/README.md @@ -0,0 +1,75 @@ +# Testbed suite for the OpenTracing API + +Testbed suite designed to test API changes. + +## Build and test. + +```sh +make testbed +``` + +Depending on whether Python 2 or 3 is being used, the `asyncio` tests will be automatically disabled. + +Alternatively, due to the organization of the suite, it's possible to run directly the tests using `py.test`: + +```sh + py.test -s testbed/test_multiple_callbacks/test_threads.py +``` + +## Tested frameworks + +Currently the examples cover `threading`, `tornado`, `gevent` and `asyncio` (which requires Python 3). The implementation of `ScopeManager` for each framework is a basic, simple one, and can be found in [span_propagation.py](span_propagation.py). See details below. + +### threading + +`ThreadScopeManager` uses thread-local storage (through `threading.local()`), and does not provide automatic propagation from thread to thread, which needs to be done manually. + +### gevent + +`GeventScopeManager` uses greenlet-local storage (through `gevent.local.local()`), and does not provide automatic propagation from parent greenlets to their children, which needs to be done manually. + +### tornado + +`TornadoScopeManager` uses a variation of `tornado.stack_context.StackContext` to both store **and** automatically propagate the context from parent coroutines to their children. + +Because of this, in order to make the `TornadoScopeManager` work, calls need to be started like this: + +```python +with tracer_stack_context(): + my_coroutine() +``` + +At the moment of writing this, yielding over multiple children is not supported, as the context is effectively shared, and switching from coroutine to coroutine messes up the current active `Span`. + +### asyncio + +`AsyncioScopeManager` uses the current `Task` (through `Task.current_task()`) to store the active `Span`, and does not provide automatic propagation from parent `Task` to their children, which needs to be done manually. + +## List of patterns + +- [Active Span replacement](test_active_span_replacement) - Start an isolated task and query for its results in another task/thread. +- [Client-Server](test_client_server) - Typical client-server example. +- [Common Request Handler](test_common_request_handler) - One request handler for all requests. +- [Late Span finish](test_late_span_finish) - Late parent `Span` finish. +- [Multiple callbacks](test_multiple_callbacks) - Multiple callbacks spawned at the same time. +- [Nested callbacks](test_nested_callbacks) - One callback at a time, defined ina pipeline fashion. +- [Subtask Span propagation](test_subtask_span_propagation) - `Span` propagation for subtasks/coroutines. + +## Adding new patterns + +A new pattern is composed of a directory under *testbed* with the *test_* prefix, and containing the files for each platform, also with the *test_* prefix: + +``` +testbed/ + test_new_pattern/ + test_threads.py + test_tornado.py + test_asyncio.py + test_gevent.py +``` + +Supporting all the platforms is optional, and a warning will be displayed when doing `make testbed` in such case. + +## Flake8 support + +Currently `flake8` does not support the Python 3 `await`/`async` syntax, and does not offer a way to ignore such syntax. diff --git a/testbed/__init__.py b/testbed/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/testbed/__init__.py @@ -0,0 +1 @@ + diff --git a/testbed/__main__.py b/testbed/__main__.py new file mode 100644 index 0000000..1dd2ceb --- /dev/null +++ b/testbed/__main__.py @@ -0,0 +1,50 @@ +from importlib import import_module +import logging +import os +import six +import unittest + + +enabled_platforms = [ + 'threads', + 'tornado', + 'gevent', +] +if six.PY3: + enabled_platforms.append('asyncio') + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__package__) + + +def import_test_module(test_name, platform): + full_path = '%s.%s.test_%s' % (__package__, test_name, platform) + try: + return import_module(full_path) + except ImportError: + pass + + return None + + +def get_test_directories(): + """Return all the directories starting with test_ under this package.""" + return [directory for directory in os.listdir(os.path.dirname(__file__)) + if directory.startswith('test_')] + + +main_suite = unittest.TestSuite() +loader = unittest.TestLoader() + +for test_dir in get_test_directories(): + for platform in enabled_platforms: + + test_module = import_test_module(test_dir, platform) + if test_module is None: + logger.warning('Could not load %s for %s' % (test_dir, platform)) + continue + + suite = loader.loadTestsFromModule(test_module) + main_suite.addTests(suite) + +unittest.TextTestRunner(verbosity=3).run(main_suite) diff --git a/testbed/span_propagation.py b/testbed/span_propagation.py new file mode 100644 index 0000000..70c54d0 --- /dev/null +++ b/testbed/span_propagation.py @@ -0,0 +1,158 @@ +import six +import threading +from tornado.stack_context import StackContext +import gevent.local + +from opentracing import ScopeManager, Scope + +if six.PY3: + import asyncio + + +# +# asyncio section. +# +class AsyncioScopeManager(ScopeManager): + def activate(self, span, finish_on_close): + scope = AsyncioScope(self, span, finish_on_close) + + loop = asyncio.get_event_loop() + task = asyncio.Task.current_task(loop=loop) + setattr(task, '__active', scope) + + return scope + + def _get_current_task(self): + loop = asyncio.get_event_loop() + return asyncio.Task.current_task(loop=loop) + + @property + def active(self): + task = self._get_current_task() + return getattr(task, '__active', None) + + +class AsyncioScope(Scope): + def __init__(self, manager, span, finish_on_close): + super(AsyncioScope, self).__init__(manager, span) + self._finish_on_close = finish_on_close + self._to_restore = manager.active + + def close(self): + if self.manager.active is not self: + return + + task = self.manager._get_current_task() + setattr(task, '__active', self._to_restore) + + if self._finish_on_close: + self.span.finish() + +# +# gevent section. +# +class GeventScopeManager(ScopeManager): + def __init__(self): + self._locals = gevent.local.local() + + def activate(self, span, finish_on_close): + scope = GeventScope(self, span, finish_on_close) + setattr(self._locals, 'active', scope) + + return scope + + @property + def active(self): + return getattr(self._locals, 'active', None) + + +class GeventScope(Scope): + def __init__(self, manager, span, finish_on_close): + super(GeventScope, self).__init__(manager, span) + self._finish_on_close = finish_on_close + self._to_restore = manager.active + + def close(self): + if self.manager.active is not self: + return + + setattr(self.manager._locals, 'active', self._to_restore) + + if self._finish_on_close: + self.span.finish() + +# +# tornado section. +# +class TornadoScopeManager(ScopeManager): + def activate(self, span, finish_on_close): + context = self._get_context() + if context is None: + raise Exception('No StackContext detected') + + scope = TornadoScope(self, span, finish_on_close) + context.active = scope + + return scope + + def _get_context(self): + return TracerRequestContextManager.current_context() + + @property + def active(self): + context = self._get_context() + if context is None: + return None + + return context.active + + +class TornadoScope(Scope): + def __init__(self, manager, span, finish_on_close): + super(TornadoScope, self).__init__(manager, span) + self._finish_on_close = finish_on_close + self._to_restore = manager.active + + def close(self): + context = self.manager._get_context() + if context is None or context.active is not self: + return + + context.active = self._to_restore + + if self._finish_on_close: + self.span.finish() + + +class TracerRequestContext(object): + __slots__ = ('active', ) + + def __init__(self, active=None): + self.active = active + + +class TracerRequestContextManager(object): + _state = threading.local() + _state.context = None + + @classmethod + def current_context(cls): + return getattr(cls._state, 'context', None) + + def __init__(self, context): + self._context = context + + def __enter__(self): + self._prev_context = self.__class__.current_context() + self.__class__._state.context = self._context + return self._context + + def __exit__(self, *_): + self.__class__._state.context = self._prev_context + self._prev_context = None + return False + + +def tracer_stack_context(): + context = TracerRequestContext() + return StackContext(lambda: TracerRequestContextManager(context)) diff --git a/testbed/test_active_span_replacement/README.md b/testbed/test_active_span_replacement/README.md new file mode 100644 index 0000000..96e8da4 --- /dev/null +++ b/testbed/test_active_span_replacement/README.md @@ -0,0 +1,18 @@ +# Active Span replacement example. + +This example shows a `Span` being created and then passed to an asynchronous task, which will temporary activate it to finish its processing, and further restore the previously active `Span`. + +`threading` implementation: +```python +# Create a new Span for this task +with self.tracer.start_active_span('task'): + + with self.tracer.scope_manager.activate(span, True): + # Simulate work strictly related to the initial Span + pass + + # Use the task span as parent of a new subtask + with self.tracer.start_active_span('subtask'): + pass + +``` diff --git a/testbed/test_active_span_replacement/__init__.py b/testbed/test_active_span_replacement/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testbed/test_active_span_replacement/test_asyncio.py b/testbed/test_active_span_replacement/test_asyncio.py new file mode 100644 index 0000000..d7563b5 --- /dev/null +++ b/testbed/test_active_span_replacement/test_asyncio.py @@ -0,0 +1,52 @@ +from __future__ import print_function + +import asyncio + +from opentracing.mocktracer import MockTracer +from ..testcase import OpenTracingTestCase +from ..span_propagation import AsyncioScopeManager +from ..utils import stop_loop_when + + +class TestAsyncio(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(AsyncioScopeManager()) + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Start an isolated task and query for its result -and finish it- + # in another task/thread + span = self.tracer.start_span('initial') + self.submit_another_task(span) + + stop_loop_when(self.loop, + lambda: len(self.tracer.finished_spans()) >= 3) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ['initial', 'subtask', 'task']) + + # task/subtask are part of the same trace, + # and subtask is a child of task + self.assertSameTrace(spans[1], spans[2]) + self.assertIsChildOf(spans[1], spans[2]) + + # initial task is not related in any way to those two tasks + self.assertNotSameTrace(spans[0], spans[1]) + self.assertEqual(spans[0].parent_id, None) + + async def task(self, span): + # Create a new Span for this task + with self.tracer.start_active_span('task'): + + with self.tracer.scope_manager.activate(span, True): + # Simulate work strictly related to the initial Span + pass + + # Use the task span as parent of a new subtask + with self.tracer.start_active_span('subtask'): + pass + + def submit_another_task(self, span): + self.loop.create_task(self.task(span)) diff --git a/testbed/test_active_span_replacement/test_gevent.py b/testbed/test_active_span_replacement/test_gevent.py new file mode 100644 index 0000000..6bb173d --- /dev/null +++ b/testbed/test_active_span_replacement/test_gevent.py @@ -0,0 +1,48 @@ +from __future__ import print_function + +import gevent + +from opentracing.mocktracer import MockTracer +from ..span_propagation import GeventScopeManager +from ..testcase import OpenTracingTestCase + + +class TestGevent(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(GeventScopeManager()) + + def test_main(self): + # Start an isolated task and query for its result -and finish it- + # in another task/thread + span = self.tracer.start_span('initial') + self.submit_another_task(span) + + gevent.wait(timeout=5.0) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ['initial', 'subtask', 'task']) + + # task/subtask are part of the same trace, + # and subtask is a child of task + self.assertSameTrace(spans[1], spans[2]) + self.assertIsChildOf(spans[1], spans[2]) + + # initial task is not related in any way to those two tasks + self.assertNotSameTrace(spans[0], spans[1]) + self.assertEqual(spans[0].parent_id, None) + + def task(self, span): + # Create a new Span for this task + with self.tracer.start_active_span('task'): + + with self.tracer.scope_manager.activate(span, True): + # Simulate work strictly related to the initial Span + pass + + # Use the task span as parent of a new subtask + with self.tracer.start_active_span('subtask'): + pass + + def submit_another_task(self, span): + gevent.spawn(self.task, span) diff --git a/testbed/test_active_span_replacement/test_threads.py b/testbed/test_active_span_replacement/test_threads.py new file mode 100644 index 0000000..f5a1a81 --- /dev/null +++ b/testbed/test_active_span_replacement/test_threads.py @@ -0,0 +1,48 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from opentracing.mocktracer import MockTracer +from ..testcase import OpenTracingTestCase + + +class TestThreads(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + # Start an isolated task and query for its result -and finish it- + # in another task/thread + span = self.tracer.start_span('initial') + self.submit_another_task(span) + + self.executor.shutdown(True) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ['initial', 'subtask', 'task']) + + # task/subtask are part of the same trace, + # and subtask is a child of task + self.assertSameTrace(spans[1], spans[2]) + self.assertIsChildOf(spans[1], spans[2]) + + # initial task is not related in any way to those two tasks + self.assertNotSameTrace(spans[0], spans[1]) + self.assertEqual(spans[0].parent_id, None) + + def task(self, span): + # Create a new Span for this task + with self.tracer.start_active_span('task'): + + with self.tracer.scope_manager.activate(span, True): + # Simulate work strictly related to the initial Span + pass + + # Use the task span as parent of a new subtask + with self.tracer.start_active_span('subtask'): + pass + + def submit_another_task(self, span): + self.executor.submit(self.task, span) diff --git a/testbed/test_active_span_replacement/test_tornado.py b/testbed/test_active_span_replacement/test_tornado.py new file mode 100644 index 0000000..8e5a6cc --- /dev/null +++ b/testbed/test_active_span_replacement/test_tornado.py @@ -0,0 +1,54 @@ +from __future__ import print_function + +from tornado import gen, ioloop + +from opentracing.mocktracer import MockTracer +from ..span_propagation import TornadoScopeManager, tracer_stack_context +from ..testcase import OpenTracingTestCase +from ..utils import stop_loop_when + + +class TestTornado(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(TornadoScopeManager()) + self.loop = ioloop.IOLoop.current() + + def test_main(self): + # Start an isolated task and query for its result -and finish it- + # in another task/thread + span = self.tracer.start_span('initial') + with tracer_stack_context(): + self.submit_another_task(span) + + stop_loop_when(self.loop, + lambda: len(self.tracer.finished_spans()) >= 3) + self.loop.start() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ['initial', 'subtask', 'task']) + + # task/subtask are part of the same trace, + # and subtask is a child of task + self.assertSameTrace(spans[1], spans[2]) + self.assertIsChildOf(spans[1], spans[2]) + + # initial task is not related in any way to those two tasks + self.assertNotSameTrace(spans[0], spans[1]) + self.assertEqual(spans[0].parent_id, None) + + @gen.coroutine + def task(self, span): + # Create a new Span for this task + with self.tracer.start_active_span('task'): + + with self.tracer.scope_manager.activate(span, True): + # Simulate work strictly related to the initial Span + pass + + # Use the task span as parent of a new subtask + with self.tracer.start_active_span('subtask'): + pass + + def submit_another_task(self, span): + self.loop.add_callback(self.task, span) diff --git a/testbed/test_client_server/README.md b/testbed/test_client_server/README.md new file mode 100644 index 0000000..09470f2 --- /dev/null +++ b/testbed/test_client_server/README.md @@ -0,0 +1,17 @@ +# Client-Server example. + +This example shows a `Span` created by a `Client`, which will send a `Message`/`SpanContext` to a `Server`, which will in turn extract such context and use it as parent of a new (server-side) `Span`. + +`Client.send()` is used to send messages and inject the `SpanContext` using the `TEXT_MAP` format, and `Server.process()` will process received messages and will extract the context used as parent. + +```python +def send(self): + with self.tracer.start_active_span('send') as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + message = {} + self.tracer.inject(scope.span.context, + opentracing.Format.TEXT_MAP, + message) + self.queue.put(message) +``` diff --git a/testbed/test_client_server/__init__.py b/testbed/test_client_server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testbed/test_client_server/test_asyncio.py b/testbed/test_client_server/test_asyncio.py new file mode 100644 index 0000000..98df892 --- /dev/null +++ b/testbed/test_client_server/test_asyncio.py @@ -0,0 +1,79 @@ +from __future__ import print_function + + +import asyncio + +import opentracing +from opentracing.ext import tags +from opentracing.mocktracer import MockTracer +from ..span_propagation import AsyncioScopeManager +from ..testcase import OpenTracingTestCase +from ..utils import get_logger, get_one_by_tag, stop_loop_when + + +logger = get_logger(__name__) + + +class Server(object): + def __init__(self, *args, **kwargs): + tracer = kwargs.pop('tracer') + queue = kwargs.pop('queue') + super(Server, self).__init__(*args, **kwargs) + + self.tracer = tracer + self.queue = queue + + async def run(self): + value = await self.queue.get() + self.process(value) + + def process(self, message): + logger.info('Processing message in server') + + ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) + with self.tracer.start_active_span('receive', + child_of=ctx) as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + + +class Client(object): + def __init__(self, tracer, queue): + self.tracer = tracer + self.queue = queue + + async def send(self): + with self.tracer.start_active_span('send') as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + message = {} + self.tracer.inject(scope.span.context, + opentracing.Format.TEXT_MAP, + message) + await self.queue.put(message) + + logger.info('Sent message from client') + + +class TestAsyncio(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(AsyncioScopeManager()) + self.queue = asyncio.Queue() + self.loop = asyncio.get_event_loop() + self.server = Server(tracer=self.tracer, queue=self.queue) + + def test(self): + client = Client(self.tracer, self.queue) + self.loop.create_task(self.server.run()) + self.loop.create_task(client.send()) + + stop_loop_when(self.loop, + lambda: len(self.tracer.finished_spans()) >= 2) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertIsNotNone(get_one_by_tag(spans, + tags.SPAN_KIND, + tags.SPAN_KIND_RPC_SERVER)) + self.assertIsNotNone(get_one_by_tag(spans, + tags.SPAN_KIND, + tags.SPAN_KIND_RPC_CLIENT)) diff --git a/testbed/test_client_server/test_gevent.py b/testbed/test_client_server/test_gevent.py new file mode 100644 index 0000000..b29a8bf --- /dev/null +++ b/testbed/test_client_server/test_gevent.py @@ -0,0 +1,77 @@ +from __future__ import print_function + + +import gevent +import gevent.queue + +import opentracing +from opentracing.ext import tags +from opentracing.mocktracer import MockTracer +from ..span_propagation import GeventScopeManager +from ..testcase import OpenTracingTestCase +from ..utils import get_logger, get_one_by_tag + + +logger = get_logger(__name__) + + +class Server(object): + def __init__(self, *args, **kwargs): + tracer = kwargs.pop('tracer') + queue = kwargs.pop('queue') + super(Server, self).__init__(*args, **kwargs) + + self.tracer = tracer + self.queue = queue + + def run(self): + value = self.queue.get() + self.process(value) + + def process(self, message): + logger.info('Processing message in server') + + ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) + with self.tracer.start_active_span('receive', + child_of=ctx) as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + + +class Client(object): + def __init__(self, tracer, queue): + self.tracer = tracer + self.queue = queue + + def send(self): + with self.tracer.start_active_span('send') as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + message = {} + self.tracer.inject(scope.span.context, + opentracing.Format.TEXT_MAP, + message) + self.queue.put(message) + + logger.info('Sent message from client') + + +class TestGevent(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(GeventScopeManager()) + self.queue = gevent.queue.Queue() + self.server = Server(tracer=self.tracer, queue=self.queue) + + def test(self): + client = Client(self.tracer, self.queue) + gevent.spawn(self.server.run) + gevent.spawn(client.send) + + gevent.wait(timeout=5.0) + + spans = self.tracer.finished_spans() + self.assertIsNotNone(get_one_by_tag(spans, + tags.SPAN_KIND, + tags.SPAN_KIND_RPC_SERVER)) + self.assertIsNotNone(get_one_by_tag(spans, + tags.SPAN_KIND, + tags.SPAN_KIND_RPC_CLIENT)) diff --git a/testbed/test_client_server/test_threads.py b/testbed/test_client_server/test_threads.py new file mode 100644 index 0000000..16e2522 --- /dev/null +++ b/testbed/test_client_server/test_threads.py @@ -0,0 +1,76 @@ +from __future__ import print_function + +from threading import Thread +from six.moves import queue + +import opentracing +from opentracing.ext import tags +from opentracing.mocktracer import MockTracer +from ..testcase import OpenTracingTestCase +from ..utils import await_until, get_logger, get_one_by_tag + + +logger = get_logger(__name__) + + +class Server(Thread): + def __init__(self, *args, **kwargs): + tracer = kwargs.pop('tracer') + queue = kwargs.pop('queue') + super(Server, self).__init__(*args, **kwargs) + + self.daemon = True + self.tracer = tracer + self.queue = queue + + def run(self): + value = self.queue.get() + self.process(value) + + def process(self, message): + logger.info('Processing message in server') + + ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) + with self.tracer.start_active_span('receive', + child_of=ctx) as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + + +class Client(object): + def __init__(self, tracer, queue): + self.tracer = tracer + self.queue = queue + + def send(self): + with self.tracer.start_active_span('send') as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + message = {} + self.tracer.inject(scope.span.context, + opentracing.Format.TEXT_MAP, + message) + self.queue.put(message) + + logger.info('Sent message from client') + + +class TestThreads(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer() + self.queue = queue.Queue() + self.server = Server(tracer=self.tracer, queue=self.queue) + self.server.start() + + def test(self): + client = Client(self.tracer, self.queue) + client.send() + + await_until(lambda: len(self.tracer.finished_spans()) >= 2) + + spans = self.tracer.finished_spans() + self.assertIsNotNone(get_one_by_tag(spans, + tags.SPAN_KIND, + tags.SPAN_KIND_RPC_SERVER)) + self.assertIsNotNone(get_one_by_tag(spans, + tags.SPAN_KIND, + tags.SPAN_KIND_RPC_CLIENT)) diff --git a/testbed/test_client_server/test_tornado.py b/testbed/test_client_server/test_tornado.py new file mode 100644 index 0000000..8b4edf2 --- /dev/null +++ b/testbed/test_client_server/test_tornado.py @@ -0,0 +1,82 @@ +from __future__ import print_function + + +from tornado import gen, ioloop, queues + +import opentracing +from opentracing.ext import tags +from opentracing.mocktracer import MockTracer +from ..span_propagation import TornadoScopeManager, tracer_stack_context +from ..testcase import OpenTracingTestCase +from ..utils import get_logger, get_one_by_tag, stop_loop_when + + +logger = get_logger(__name__) + + +class Server(object): + def __init__(self, *args, **kwargs): + tracer = kwargs.pop('tracer') + queue = kwargs.pop('queue') + super(Server, self).__init__(*args, **kwargs) + + self.tracer = tracer + self.queue = queue + + @gen.coroutine + def run(self): + value = yield self.queue.get() + self.process(value) + + def process(self, message): + logger.info('Processing message in server') + + ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) + with self.tracer.start_active_span('receive', + child_of=ctx) as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + + +class Client(object): + def __init__(self, tracer, queue): + self.tracer = tracer + self.queue = queue + + @gen.coroutine + def send(self): + with self.tracer.start_active_span('send') as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + message = {} + self.tracer.inject(scope.span.context, + opentracing.Format.TEXT_MAP, + message) + yield self.queue.put(message) + + logger.info('Sent message from client') + + +class TestTornado(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(TornadoScopeManager()) + self.queue = queues.Queue() + self.loop = ioloop.IOLoop.current() + self.server = Server(tracer=self.tracer, queue=self.queue) + + def test(self): + client = Client(self.tracer, self.queue) + with tracer_stack_context(): + self.loop.add_callback(self.server.run) + self.loop.add_callback(client.send) + + stop_loop_when(self.loop, + lambda: len(self.tracer.finished_spans()) >= 2) + self.loop.start() + + spans = self.tracer.finished_spans() + self.assertIsNotNone(get_one_by_tag(spans, + tags.SPAN_KIND, + tags.SPAN_KIND_RPC_SERVER)) + self.assertIsNotNone(get_one_by_tag(spans, + tags.SPAN_KIND, + tags.SPAN_KIND_RPC_CLIENT)) diff --git a/testbed/test_common_request_handler/README.md b/testbed/test_common_request_handler/README.md new file mode 100644 index 0000000..7fc1c7d --- /dev/null +++ b/testbed/test_common_request_handler/README.md @@ -0,0 +1,25 @@ +# Common Request Handler example. + +This example shows a `Span` used with `RequestHandler`, which is used as a middleware (as in web frameworks) to manage a new `Span` per operation through its `before_request()`/`after_response()` methods. + +Implementation details: +- For `threading`, no active `Span` is consumed as the tasks may be run concurrently on different threads, and an explicit `SpanContext` has to be saved to be used as parent. +- For `gevent` and `asyncio`, as no automatic `Span` propagation is done, an explicit `Span` has to be saved to be used as parent (observe an instrumentation library could help to do that implicitly - we stick to the simplest case, though). +- For `tornado`, as the `StackContext` automatically propapates the context (even is the tasks are called through different coroutines), we **do** leverage the active `Span`. + + +RequestHandler implementation: +```python + def before_request(self, request, request_context): + + # If we should ignore the active Span, use any passed SpanContext + # as the parent. Else, use the active one. + if self.ignore_active_span: + # Used by threading, gevent and asyncio. + span = self.tracer.start_span('send', + child_of=self.context, + ignore_active_span=True) + else: + # Used by tornado. + span = self.tracer.start_span('send') +``` diff --git a/testbed/test_common_request_handler/__init__.py b/testbed/test_common_request_handler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testbed/test_common_request_handler/request_handler.py b/testbed/test_common_request_handler/request_handler.py new file mode 100644 index 0000000..ec5e0b7 --- /dev/null +++ b/testbed/test_common_request_handler/request_handler.py @@ -0,0 +1,38 @@ +from __future__ import print_function + +from opentracing.ext import tags + +from ..utils import get_logger + + +logger = get_logger(__name__) + + +class RequestHandler(object): + def __init__(self, tracer, context=None, ignore_active_span=True): + self.tracer = tracer + self.context = context + self.ignore_active_span = ignore_active_span + + def before_request(self, request, request_context): + logger.info('Before request %s' % request) + + # If we should ignore the active Span, use any passed SpanContext + # as the parent. Else, use the active one. + if self.ignore_active_span: + span = self.tracer.start_span('send', + child_of=self.context, + ignore_active_span=True) + else: + span = self.tracer.start_span('send') + + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + request_context['span'] = span + + def after_request(self, request, request_context): + logger.info('After request %s' % request) + + span = request_context.get('span') + if span is not None: + span.finish() diff --git a/testbed/test_common_request_handler/test_asyncio.py b/testbed/test_common_request_handler/test_asyncio.py new file mode 100644 index 0000000..5367545 --- /dev/null +++ b/testbed/test_common_request_handler/test_asyncio.py @@ -0,0 +1,129 @@ +from __future__ import print_function + +import functools + +import asyncio + +from opentracing.ext import tags +from opentracing.mocktracer import MockTracer +from ..span_propagation import AsyncioScopeManager +from ..testcase import OpenTracingTestCase +from ..utils import get_logger, get_one_by_operation_name, stop_loop_when +from .request_handler import RequestHandler + + +logger = get_logger(__name__) + + +class Client(object): + def __init__(self, request_handler, loop): + self.request_handler = request_handler + self.loop = loop + + async def send_task(self, message): + request_context = {} + + async def before_handler(): + self.request_handler.before_request(message, request_context) + + async def after_handler(): + self.request_handler.after_request(message, request_context) + + await before_handler() + await after_handler() + + return '%s::response' % message + + def send(self, message): + return self.send_task(message) + + def send_sync(self, message): + return self.loop.run_until_complete(self.send_task(message)) + + +class TestAsyncio(OpenTracingTestCase): + """ + There is only one instance of 'RequestHandler' per 'Client'. Methods of + 'RequestHandler' are executed in different Tasks, and no Span propagation + among them is done automatically. + Therefore we cannot use current active span and activate span. + So one issue here is setting correct parent span. + """ + + def setUp(self): + self.tracer = MockTracer(AsyncioScopeManager()) + self.loop = asyncio.get_event_loop() + self.client = Client(RequestHandler(self.tracer), self.loop) + + def test_two_callbacks(self): + res_future1 = self.loop.create_task(self.client.send('message1')) + res_future2 = self.loop.create_task(self.client.send('message2')) + + stop_loop_when(self.loop, + lambda: len(self.tracer.finished_spans()) >= 2) + self.loop.run_forever() + + self.assertEquals('message1::response', res_future1.result()) + self.assertEquals('message2::response', res_future2.result()) + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 2) + + for span in spans: + self.assertEquals(span.tags.get(tags.SPAN_KIND, None), + tags.SPAN_KIND_RPC_CLIENT) + + self.assertNotSameTrace(spans[0], spans[1]) + self.assertIsNone(spans[0].parent_id) + self.assertIsNone(spans[1].parent_id) + + def test_parent_not_picked(self): + """Active parent should not be picked up by child.""" + + async def do(): + with self.tracer.start_active_span('parent'): + response = await self.client.send_task('no_parent') + self.assertEquals('no_parent::response', response) + + self.loop.run_until_complete(do()) + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 2) + + child_span = get_one_by_operation_name(spans, 'send') + self.assertIsNotNone(child_span) + + parent_span = get_one_by_operation_name(spans, 'parent') + self.assertIsNotNone(parent_span) + + # Here check that there is no parent-child relation. + self.assertIsNotChildOf(child_span, parent_span) + + def test_bad_solution_to_set_parent(self): + """Solution is bad because parent is per client + (we don't have better choice)""" + + async def do(): + with self.tracer.start_active_span('parent') as scope: + req_handler = RequestHandler(self.tracer, scope.span.context) + client = Client(req_handler, self.loop) + response = await client.send_task('correct_parent') + + self.assertEquals('correct_parent::response', response) + + # Send second request, now there is no active parent, + # but it will be set, ups + response = await client.send_task('wrong_parent') + self.assertEquals('wrong_parent::response', response) + + self.loop.run_until_complete(do()) + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 3) + + spans = sorted(spans, key=lambda x: x.start_time) + parent_span = get_one_by_operation_name(spans, 'parent') + self.assertIsNotNone(parent_span) + + self.assertIsChildOf(spans[1], parent_span) + self.assertIsChildOf(spans[2], parent_span) diff --git a/testbed/test_common_request_handler/test_gevent.py b/testbed/test_common_request_handler/test_gevent.py new file mode 100644 index 0000000..a3b4f0e --- /dev/null +++ b/testbed/test_common_request_handler/test_gevent.py @@ -0,0 +1,114 @@ +from __future__ import print_function + +import gevent + +from opentracing.ext import tags +from opentracing.mocktracer import MockTracer +from ..span_propagation import GeventScopeManager +from ..testcase import OpenTracingTestCase +from ..utils import get_logger, get_one_by_operation_name +from .request_handler import RequestHandler + + +logger = get_logger(__name__) + + +class Client(object): + def __init__(self, request_handler): + self.request_handler = request_handler + + def send_task(self, message): + request_context = {} + + def before_handler(): + self.request_handler.before_request(message, request_context) + + def after_handler(): + self.request_handler.after_request(message, request_context) + + gevent.spawn(before_handler).join() + gevent.spawn(after_handler).join() + + return '%s::response' % message + + def send(self, message): + return gevent.spawn(self.send_task, message) + + def send_sync(self, message, timeout=5.0): + return gevent.spawn(self.send_task, message).get(timeout=timeout) + + +class TestGevent(OpenTracingTestCase): + """ + There is only one instance of 'RequestHandler' per 'Client'. Methods of + 'RequestHandler' are executed in different greenlets, and no Span + propagation among them is done automatically. + Therefore we cannot use current active span and activate span. + So one issue here is setting correct parent span. + """ + + def setUp(self): + self.tracer = MockTracer(GeventScopeManager()) + self.client = Client(RequestHandler(self.tracer)) + + def test_two_callbacks(self): + response_greenlet1 = gevent.spawn(self.client.send_task, 'message1') + response_greenlet2 = gevent.spawn(self.client.send_task, 'message2') + + gevent.joinall([response_greenlet1, response_greenlet2]) + + self.assertEquals('message1::response', response_greenlet1.get()) + self.assertEquals('message2::response', response_greenlet2.get()) + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 2) + + for span in spans: + self.assertEquals(span.tags.get(tags.SPAN_KIND, None), + tags.SPAN_KIND_RPC_CLIENT) + + self.assertNotSameTrace(spans[0], spans[1]) + self.assertIsNone(spans[0].parent_id) + self.assertIsNone(spans[1].parent_id) + + def test_parent_not_picked(self): + """Active parent should not be picked up by child.""" + + with self.tracer.start_active_span('parent'): + response = self.client.send_sync('no_parent') + self.assertEquals('no_parent::response', response) + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 2) + + child_span = get_one_by_operation_name(spans, 'send') + self.assertIsNotNone(child_span) + + parent_span = get_one_by_operation_name(spans, 'parent') + self.assertIsNotNone(parent_span) + + # Here check that there is no parent-child relation. + self.assertIsNotChildOf(child_span, parent_span) + + def test_bad_solution_to_set_parent(self): + """Solution is bad because parent is per client + (we don't have better choice)""" + + with self.tracer.start_active_span('parent') as scope: + client = Client(RequestHandler(self.tracer, scope.span.context)) + response = client.send_sync('correct_parent') + + self.assertEquals('correct_parent::response', response) + + response = client.send_sync('wrong_parent') + self.assertEquals('wrong_parent::response', response) + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 3) + + spans = sorted(spans, key=lambda x: x.start_time) + parent_span = get_one_by_operation_name(spans, 'parent') + self.assertIsNotNone(parent_span) + + self.assertIsChildOf(spans[1], parent_span) + self.assertIsChildOf(spans[2], parent_span) diff --git a/testbed/test_common_request_handler/test_threads.py b/testbed/test_common_request_handler/test_threads.py new file mode 100644 index 0000000..4135698 --- /dev/null +++ b/testbed/test_common_request_handler/test_threads.py @@ -0,0 +1,113 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from opentracing.ext import tags +from opentracing.mocktracer import MockTracer +from ..testcase import OpenTracingTestCase +from ..utils import get_logger, get_one_by_operation_name +from .request_handler import RequestHandler + + +logger = get_logger(__name__) + + +class Client(object): + def __init__(self, request_handler, executor): + self.request_handler = request_handler + self.executor = executor + + def send_task(self, message): + request_context = {} + + def before_handler(): + self.request_handler.before_request(message, request_context) + + def after_handler(): + self.request_handler.after_request(message, request_context) + + self.executor.submit(before_handler).result() + self.executor.submit(after_handler).result() + + return '%s::response' % message + + def send(self, message): + return self.executor.submit(self.send_task, message) + + def send_sync(self, message, timeout=5.0): + f = self.executor.submit(self.send_task, message) + return f.result(timeout=timeout) + + +class TestThreads(OpenTracingTestCase): + """ + There is only one instance of 'RequestHandler' per 'Client'. Methods of + 'RequestHandler' are executed concurrently in different threads which are + reused (executor). Therefore we cannot use current active span and + activate span. So one issue here is setting correct parent span. + """ + + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + self.client = Client(RequestHandler(self.tracer), self.executor) + + def test_two_callbacks(self): + response_future1 = self.client.send('message1') + response_future2 = self.client.send('message2') + + self.assertEquals('message1::response', response_future1.result(5.0)) + self.assertEquals('message2::response', response_future2.result(5.0)) + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 2) + + for span in spans: + self.assertEquals(span.tags.get(tags.SPAN_KIND, None), + tags.SPAN_KIND_RPC_CLIENT) + + self.assertNotSameTrace(spans[0], spans[1]) + self.assertIsNone(spans[0].parent_id) + self.assertIsNone(spans[1].parent_id) + + def test_parent_not_picked(self): + """Active parent should not be picked up by child.""" + + with self.tracer.start_active_span('parent'): + response = self.client.send_sync('no_parent') + self.assertEquals('no_parent::response', response) + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 2) + + child_span = get_one_by_operation_name(spans, 'send') + self.assertIsNotNone(child_span) + + parent_span = get_one_by_operation_name(spans, 'parent') + self.assertIsNotNone(parent_span) + + # Here check that there is no parent-child relation. + self.assertIsNotChildOf(child_span, parent_span) + + def test_bad_solution_to_set_parent(self): + """Solution is bad because parent is per client + (we don't have better choice)""" + + with self.tracer.start_active_span('parent') as scope: + client = Client(RequestHandler(self.tracer, scope.span.context), + self.executor) + response = client.send_sync('correct_parent') + self.assertEquals('correct_parent::response', response) + + response = client.send_sync('wrong_parent') + self.assertEquals('wrong_parent::response', response) + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 3) + + spans = sorted(spans, key=lambda x: x.start_time) + parent_span = get_one_by_operation_name(spans, 'parent') + self.assertIsNotNone(parent_span) + + self.assertIsChildOf(spans[1], parent_span) + self.assertIsChildOf(spans[2], parent_span) diff --git a/testbed/test_common_request_handler/test_tornado.py b/testbed/test_common_request_handler/test_tornado.py new file mode 100644 index 0000000..c0ce431 --- /dev/null +++ b/testbed/test_common_request_handler/test_tornado.py @@ -0,0 +1,128 @@ +from __future__ import print_function + +import functools + +from tornado import gen, ioloop + +from opentracing.ext import tags +from opentracing.mocktracer import MockTracer +from ..span_propagation import TornadoScopeManager, tracer_stack_context +from ..testcase import OpenTracingTestCase +from ..utils import get_logger, get_one_by_operation_name, stop_loop_when +from .request_handler import RequestHandler + + +logger = get_logger(__name__) + + +class Client(object): + def __init__(self, request_handler, loop): + self.request_handler = request_handler + self.loop = loop + + @gen.coroutine + def send_task(self, message): + request_context = {} + + @gen.coroutine + def before_handler(): + self.request_handler.before_request(message, request_context) + + @gen.coroutine + def after_handler(): + self.request_handler.after_request(message, request_context) + + yield before_handler() + yield after_handler() + + raise gen.Return('%s::response' % message) + + def send(self, message): + return self.send_task(message) + + def send_sync(self, message, timeout=5.0): + return self.loop.run_sync(functools.partial(self.send_task, message), + timeout) + + +class TestTornado(OpenTracingTestCase): + """ + There is only one instance of 'RequestHandler' per 'Client'. Methods of + 'RequestHandler' are executed in different coroutines but the StackContext + is the same, so we can leverage it for accessing the active span. + """ + + def setUp(self): + self.tracer = MockTracer(TornadoScopeManager()) + self.loop = ioloop.IOLoop.current() + self.client = Client(RequestHandler(self.tracer), self.loop) + + def test_two_callbacks(self): + res_future1 = self.client.send('message1') + res_future2 = self.client.send('message2') + + stop_loop_when(self.loop, + lambda: len(self.tracer.finished_spans()) >= 2) + self.loop.start() + + self.assertEquals('message1::response', res_future1.result()) + self.assertEquals('message2::response', res_future2.result()) + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 2) + + for span in spans: + self.assertEquals(span.tags.get(tags.SPAN_KIND, None), + tags.SPAN_KIND_RPC_CLIENT) + + self.assertNotSameTrace(spans[0], spans[1]) + self.assertIsNone(spans[0].parent_id) + self.assertIsNone(spans[1].parent_id) + + def test_parent_not_picked(self): + """Active parent should not be picked up by child + as we pass ignore_active_span=True to the RequestHandler""" + + with tracer_stack_context(): + with self.tracer.start_active_span('parent'): + response = self.client.send_sync('no_parent') + self.assertEquals('no_parent::response', response) + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 2) + + child_span = get_one_by_operation_name(spans, 'send') + self.assertIsNotNone(child_span) + + parent_span = get_one_by_operation_name(spans, 'parent') + self.assertIsNotNone(parent_span) + + # Here check that there is no parent-child relation. + self.assertIsNotChildOf(child_span, parent_span) + + def test_good_solution_to_set_parent(self): + """Solution is good because, though the RequestHandler being shared, + the context will be properly detected.""" + + with tracer_stack_context(): + with self.tracer.start_active_span('parent'): + req_handler = RequestHandler(self.tracer, + ignore_active_span=False) + client = Client(req_handler, self.loop) + response = client.send_sync('correct_parent') + + self.assertEquals('correct_parent::response', response) + + # Should NOT be a child of the previously activated Span + response = client.send_sync('wrong_parent') + self.assertEquals('wrong_parent::response', response) + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 3) + + spans = sorted(spans, key=lambda x: x.start_time) + parent_span = get_one_by_operation_name(spans, 'parent') + self.assertIsNotNone(parent_span) + + self.assertIsChildOf(spans[1], parent_span) + self.assertIsNotChildOf(spans[2], parent_span) # Proper parent (none). diff --git a/testbed/test_late_span_finish/README.md b/testbed/test_late_span_finish/README.md new file mode 100644 index 0000000..4250bd0 --- /dev/null +++ b/testbed/test_late_span_finish/README.md @@ -0,0 +1,16 @@ +# Late Span finish example. + +This example shows a `Span` for a top-level operation, with independent, unknown lifetime, acting as parent of a few asynchronous subtasks (which must re-activate it but not finish it). + +```python + # Fire away a few subtasks, passing a parent Span whose lifetime + # is not tied at all to the children. + def submit_subtasks(self, parent_span): + def task(name, interval): + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span(name): + time.sleep(interval) + + self.executor.submit(task, 'task1', 0.1) + self.executor.submit(task, 'task2', 0.3) +``` diff --git a/testbed/test_late_span_finish/__init__.py b/testbed/test_late_span_finish/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testbed/test_late_span_finish/test_asyncio.py b/testbed/test_late_span_finish/test_asyncio.py new file mode 100644 index 0000000..29228fb --- /dev/null +++ b/testbed/test_late_span_finish/test_asyncio.py @@ -0,0 +1,50 @@ +from __future__ import print_function + +import asyncio + +from opentracing.mocktracer import MockTracer +from ..span_propagation import AsyncioScopeManager +from ..testcase import OpenTracingTestCase +from ..utils import get_logger, stop_loop_when + + +logger = get_logger(__name__) + + +class TestAsyncio(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(AsyncioScopeManager()) + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Create a Span and use it as (explicit) parent of a pair of subtasks. + parent_span = self.tracer.start_span('parent') + self.submit_subtasks(parent_span) + + stop_loop_when(self.loop, + lambda: len(self.tracer.finished_spans()) >= 2) + self.loop.run_forever() + + # Late-finish the parent Span now. + parent_span.finish() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ['task1', 'task2', 'parent']) + + for i in range(2): + self.assertSameTrace(spans[i], spans[-1]) + self.assertIsChildOf(spans[i], spans[-1]) + self.assertTrue(spans[i].finish_time <= spans[-1].finish_time) + + # Fire away a few subtasks, passing a parent Span whose lifetime + # is not tied at all to the children. + def submit_subtasks(self, parent_span): + async def task(name): + logger.info('Running %s' % name) + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span(name): + asyncio.sleep(0.1) + + self.loop.create_task(task('task1')) + self.loop.create_task(task('task2')) diff --git a/testbed/test_late_span_finish/test_gevent.py b/testbed/test_late_span_finish/test_gevent.py new file mode 100644 index 0000000..e30d0e6 --- /dev/null +++ b/testbed/test_late_span_finish/test_gevent.py @@ -0,0 +1,46 @@ +from __future__ import print_function + +import gevent + +from opentracing.mocktracer import MockTracer +from ..testcase import OpenTracingTestCase +from ..span_propagation import GeventScopeManager +from ..utils import get_logger + + +logger = get_logger(__name__) + + +class TestGevent(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(GeventScopeManager()) + + def test_main(self): + # Create a Span and use it as (explicit) parent of a pair of subtasks. + parent_span = self.tracer.start_span('parent') + self.submit_subtasks(parent_span) + + gevent.wait(timeout=5.0) + + # Late-finish the parent Span now. + parent_span.finish() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ['task1', 'task2', 'parent']) + + for i in range(2): + self.assertSameTrace(spans[i], spans[-1]) + self.assertIsChildOf(spans[i], spans[-1]) + self.assertTrue(spans[i].finish_time <= spans[-1].finish_time) + + # Fire away a few subtasks, passing a parent Span whose lifetime + # is not tied at all to the children. + def submit_subtasks(self, parent_span): + def task(name): + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span(name): + gevent.sleep(0.1) + + gevent.spawn(task, 'task1') + gevent.spawn(task, 'task2') diff --git a/testbed/test_late_span_finish/test_threads.py b/testbed/test_late_span_finish/test_threads.py new file mode 100644 index 0000000..4cd018b --- /dev/null +++ b/testbed/test_late_span_finish/test_threads.py @@ -0,0 +1,44 @@ +from __future__ import print_function + +import time +from concurrent.futures import ThreadPoolExecutor + +from opentracing.mocktracer import MockTracer +from ..testcase import OpenTracingTestCase + + +class TestThreads(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + # Create a Span and use it as (explicit) parent of a pair of subtasks. + parent_span = self.tracer.start_span('parent') + self.submit_subtasks(parent_span) + + # Wait for the threadpool to be done. + self.executor.shutdown(True) + + # Late-finish the parent Span now. + parent_span.finish() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ['task1', 'task2', 'parent']) + + for i in range(2): + self.assertSameTrace(spans[i], spans[-1]) + self.assertIsChildOf(spans[i], spans[-1]) + self.assertTrue(spans[i].finish_time <= spans[-1].finish_time) + + # Fire away a few subtasks, passing a parent Span whose lifetime + # is not tied at all to the children. + def submit_subtasks(self, parent_span): + def task(name, interval): + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span(name): + time.sleep(interval) + + self.executor.submit(task, 'task1', 0.1) + self.executor.submit(task, 'task2', 0.3) diff --git a/testbed/test_late_span_finish/test_tornado.py b/testbed/test_late_span_finish/test_tornado.py new file mode 100644 index 0000000..4b7c6f9 --- /dev/null +++ b/testbed/test_late_span_finish/test_tornado.py @@ -0,0 +1,52 @@ +from __future__ import print_function + +from tornado import gen, ioloop + +from opentracing.mocktracer import MockTracer +from ..span_propagation import TornadoScopeManager, tracer_stack_context +from ..testcase import OpenTracingTestCase +from ..utils import get_logger, stop_loop_when + + +logger = get_logger(__name__) + + +class TestTornado(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(TornadoScopeManager()) + self.loop = ioloop.IOLoop.current() + + def test_main(self): + # Create a Span and use it as (explicit) parent of a pair of subtasks. + with tracer_stack_context(): + parent_span = self.tracer.start_span('parent') + self.submit_subtasks(parent_span) + + stop_loop_when(self.loop, + lambda: len(self.tracer.finished_spans()) >= 2) + self.loop.start() + + # Late-finish the parent Span now. + parent_span.finish() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ['task1', 'task2', 'parent']) + + for i in range(2): + self.assertSameTrace(spans[i], spans[-1]) + self.assertIsChildOf(spans[i], spans[-1]) + self.assertTrue(spans[i].finish_time <= spans[-1].finish_time) + + # Fire away a few subtasks, passing a parent Span whose lifetime + # is not tied at all to the children. + def submit_subtasks(self, parent_span): + @gen.coroutine + def task(name): + logger.info('Running %s' % name) + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span(name): + gen.sleep(0.1) + + self.loop.add_callback(task, 'task1') + self.loop.add_callback(task, 'task2') diff --git a/testbed/test_listener_per_request/README.md b/testbed/test_listener_per_request/README.md new file mode 100644 index 0000000..19f0902 --- /dev/null +++ b/testbed/test_listener_per_request/README.md @@ -0,0 +1,17 @@ +# Listener Response example. + +This example shows a `Span` created upon a message being sent to a `Client`, and its handling along a related, **not shared** `ResponseListener` object with a `on_response(self, response)` method to finish it. + +```python + def _task(self, message, listener): + res = '%s::response' % message + listener.on_response(res) + return res + + def send_sync(self, message): + span = self.tracer.start_span('send') + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + listener = ResponseListener(span) + return self.executor.submit(self._task, message, listener).result() +``` diff --git a/testbed/test_listener_per_request/__init__.py b/testbed/test_listener_per_request/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testbed/test_listener_per_request/response_listener.py b/testbed/test_listener_per_request/response_listener.py new file mode 100644 index 0000000..710eb05 --- /dev/null +++ b/testbed/test_listener_per_request/response_listener.py @@ -0,0 +1,6 @@ +class ResponseListener(object): + def __init__(self, span): + self.span = span + + def on_response(self, res): + self.span.finish() diff --git a/testbed/test_listener_per_request/test_asyncio.py b/testbed/test_listener_per_request/test_asyncio.py new file mode 100644 index 0000000..1876e42 --- /dev/null +++ b/testbed/test_listener_per_request/test_asyncio.py @@ -0,0 +1,46 @@ +from __future__ import print_function + +import asyncio + +from opentracing.ext import tags +from opentracing.mocktracer import MockTracer +from ..span_propagation import AsyncioScopeManager +from ..testcase import OpenTracingTestCase +from ..utils import get_one_by_tag + +from .response_listener import ResponseListener + + +class Client(object): + def __init__(self, tracer, loop): + self.tracer = tracer + self.loop = loop + + async def task(self, message, listener): + res = '%s::response' % message + listener.on_response(res) + return res + + def send_sync(self, message): + span = self.tracer.start_span('send') + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + listener = ResponseListener(span) + return self.loop.run_until_complete(self.task(message, listener)) + + +class TestAsyncio(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(AsyncioScopeManager()) + self.loop = asyncio.get_event_loop() + + def test_main(self): + client = Client(self.tracer, self.loop) + res = client.send_sync('message') + self.assertEquals(res, 'message::response') + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + + span = get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + self.assertIsNotNone(span) diff --git a/testbed/test_listener_per_request/test_gevent.py b/testbed/test_listener_per_request/test_gevent.py new file mode 100644 index 0000000..438f8ed --- /dev/null +++ b/testbed/test_listener_per_request/test_gevent.py @@ -0,0 +1,44 @@ +from __future__ import print_function + +import gevent + +from opentracing.ext import tags +from opentracing.mocktracer import MockTracer +from ..span_propagation import GeventScopeManager +from ..testcase import OpenTracingTestCase +from ..utils import get_one_by_tag + +from .response_listener import ResponseListener + + +class Client(object): + def __init__(self, tracer): + self.tracer = tracer + + def task(self, message, listener): + res = '%s::response' % message + listener.on_response(res) + return res + + def send_sync(self, message): + span = self.tracer.start_span('send') + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + listener = ResponseListener(span) + return gevent.spawn(self.task, message, listener).get() + + +class TestGevent(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(GeventScopeManager()) + + def test_main(self): + client = Client(self.tracer) + res = client.send_sync('message') + self.assertEquals(res, 'message::response') + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + + span = get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + self.assertIsNotNone(span) diff --git a/testbed/test_listener_per_request/test_threads.py b/testbed/test_listener_per_request/test_threads.py new file mode 100644 index 0000000..d69b998 --- /dev/null +++ b/testbed/test_listener_per_request/test_threads.py @@ -0,0 +1,44 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from opentracing.ext import tags +from opentracing.mocktracer import MockTracer +from ..testcase import OpenTracingTestCase +from ..utils import get_one_by_tag + +from .response_listener import ResponseListener + + +class Client(object): + def __init__(self, tracer): + self.tracer = tracer + self.executor = ThreadPoolExecutor(max_workers=3) + + def _task(self, message, listener): + res = '%s::response' % message + listener.on_response(res) + return res + + def send_sync(self, message): + span = self.tracer.start_span('send') + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + listener = ResponseListener(span) + return self.executor.submit(self._task, message, listener).result() + + +class TestThreads(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer() + + def test_main(self): + client = Client(self.tracer) + res = client.send_sync('message') + self.assertEquals(res, 'message::response') + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + + span = get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + self.assertIsNotNone(span) diff --git a/testbed/test_listener_per_request/test_tornado.py b/testbed/test_listener_per_request/test_tornado.py new file mode 100644 index 0000000..9241208 --- /dev/null +++ b/testbed/test_listener_per_request/test_tornado.py @@ -0,0 +1,50 @@ +from __future__ import print_function + +import functools + +from tornado import gen, ioloop + +from opentracing.ext import tags +from opentracing.mocktracer import MockTracer +from ..span_propagation import TornadoScopeManager +from ..testcase import OpenTracingTestCase +from ..utils import get_one_by_tag + +from .response_listener import ResponseListener + + +class Client(object): + def __init__(self, tracer, loop): + self.tracer = tracer + self.loop = loop + + @gen.coroutine + def task(self, message, listener): + res = '%s::response' % message + listener.on_response(res) + return res + + def send_sync(self, message): + span = self.tracer.start_span('send') + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + listener = ResponseListener(span) + task_func = functools.partial(self.task, message, listener) + return self.loop.run_sync(task_func) + + +class TestTornado(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(TornadoScopeManager()) + self.loop = ioloop.IOLoop.current() + + def test_main(self): + client = Client(self.tracer, self.loop) + res = client.send_sync('message') + self.assertEquals(res, 'message::response') + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + + span = get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + self.assertIsNotNone(span) diff --git a/testbed/test_multiple_callbacks/README.md b/testbed/test_multiple_callbacks/README.md new file mode 100644 index 0000000..b6870fc --- /dev/null +++ b/testbed/test_multiple_callbacks/README.md @@ -0,0 +1,40 @@ +# Multiple callbacks example. + +This example shows a `Span` created for a top-level operation, covering a set of asynchronous operations (representing callbacks), and have this `Span` finished when **all** of them have been executed. + +`Client.send()` is used to create a new asynchronous operation (callback), and in turn every operation both restores the active `Span`, and creates a child `Span` (useful for measuring the performance of each callback). + +Implementation details: +- For `threading`, a thread-safe counter is put in each `Span` to keep track of the pending callbacks, and call `Span.finish()` when the count becomes 0. +- For `gevent`, `tornado` and `asyncio` the children corotuines representing the subtasks are simply yielded over, so no counter is needed. +- For `tornado`, the invoked coroutines do not set any active `Span` as doing so messes the used `StackContext`. So yielding over **multiple** coroutines is not supported. + +`threading` implementation: +```python + def task(self, interval, parent_span): + logger.info('Starting task') + + try: + scope = self.tracer.scope_manager.activate(parent_span, False) + with self.tracer.start_active_span('task'): + time.sleep(interval) + finally: + scope.close() + if parent_span._ref_count.decr() == 0: + parent_span.finish() +``` + +`asyncio` implementation: +```python + async def task(self, interval, parent_span): + logger.info('Starting task') + + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span('task'): + await asyncio.sleep(interval) + + # Invoke and yield over the corotuines. + with self.tracer.start_active_span('parent'): + tasks = self.submit_callbacks() + await asyncio.gather(*tasks) +``` diff --git a/testbed/test_multiple_callbacks/__init__.py b/testbed/test_multiple_callbacks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testbed/test_multiple_callbacks/test_asyncio.py b/testbed/test_multiple_callbacks/test_asyncio.py new file mode 100644 index 0000000..c2371a4 --- /dev/null +++ b/testbed/test_multiple_callbacks/test_asyncio.py @@ -0,0 +1,59 @@ +from __future__ import print_function + +import random + +import asyncio + +from opentracing.mocktracer import MockTracer +from ..span_propagation import AsyncioScopeManager +from ..testcase import OpenTracingTestCase +from ..utils import RefCount, get_logger, stop_loop_when + + +random.seed() +logger = get_logger(__name__) + + +class TestAsyncio(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(AsyncioScopeManager()) + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Need to run within a Task, as the scope manager depends + # on Task.current_task() + async def main_task(): + with self.tracer.start_active_span('parent'): + tasks = self.submit_callbacks() + await asyncio.gather(*tasks) + + self.loop.create_task(main_task()) + + stop_loop_when(self.loop, + lambda: len(self.tracer.finished_spans()) >= 4) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 4) + self.assertNamesEqual(spans, ['task', 'task', 'task', 'parent']) + + for i in range(3): + self.assertSameTrace(spans[i], spans[-1]) + self.assertIsChildOf(spans[i], spans[-1]) + + async def task(self, interval, parent_span): + logger.info('Starting task') + + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span('task'): + await asyncio.sleep(interval) + + def submit_callbacks(self): + parent_span = self.tracer.scope_manager.active.span + tasks = [] + for i in range(3): + interval = 0.1 + random.randint(200, 500) * 0.001 + t = self.loop.create_task(self.task(interval, parent_span)) + tasks.append(t) + + return tasks diff --git a/testbed/test_multiple_callbacks/test_gevent.py b/testbed/test_multiple_callbacks/test_gevent.py new file mode 100644 index 0000000..8b0ff6b --- /dev/null +++ b/testbed/test_multiple_callbacks/test_gevent.py @@ -0,0 +1,53 @@ +from __future__ import print_function + +import random + +import gevent + +from opentracing.mocktracer import MockTracer +from ..testcase import OpenTracingTestCase +from ..span_propagation import GeventScopeManager +from ..utils import get_logger + + +random.seed() +logger = get_logger(__name__) + + +class TestGevent(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(GeventScopeManager()) + + def test_main(self): + def main_task(): + with self.tracer.start_active_span('parent'): + tasks = self.submit_callbacks() + gevent.joinall(tasks) + + gevent.spawn(main_task) + gevent.wait(timeout=5.0) + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 4) + self.assertNamesEqual(spans, ['task', 'task', 'task', 'parent']) + + for i in range(3): + self.assertSameTrace(spans[i], spans[-1]) + self.assertIsChildOf(spans[i], spans[-1]) + + def task(self, interval, parent_span): + logger.info('Starting task') + + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span('task'): + gevent.sleep(interval) + + def submit_callbacks(self): + parent_span = self.tracer.scope_manager.active.span + tasks = [] + for i in range(3): + interval = 0.1 + random.randint(200, 500) * 0.001 + t = gevent.spawn(self.task, interval, parent_span) + tasks.append(t) + + return tasks diff --git a/testbed/test_multiple_callbacks/test_threads.py b/testbed/test_multiple_callbacks/test_threads.py new file mode 100644 index 0000000..af8e14c --- /dev/null +++ b/testbed/test_multiple_callbacks/test_threads.py @@ -0,0 +1,60 @@ +from __future__ import print_function + +import random +import time + +from concurrent.futures import ThreadPoolExecutor + +from opentracing.mocktracer import MockTracer +from ..testcase import OpenTracingTestCase +from ..utils import RefCount, get_logger + + +random.seed() +logger = get_logger(__name__) + + +class TestThreads(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + try: + scope = self.tracer.start_active_span('parent', + finish_on_close=False) + scope.span._ref_count = RefCount(1) + self.submit_callbacks(scope.span) + finally: + scope.close() + if scope.span._ref_count.decr() == 0: + scope.span.finish() + + self.executor.shutdown(True) + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 4) + self.assertNamesEqual(spans, ['task', 'task', 'task', 'parent']) + + for i in range(3): + self.assertSameTrace(spans[i], spans[-1]) + self.assertIsChildOf(spans[i], spans[-1]) + + def task(self, interval, parent_span): + logger.info('Starting task') + + try: + scope = self.tracer.scope_manager.activate(parent_span, False) + with self.tracer.start_active_span('task'): + time.sleep(interval) + finally: + scope.close() + if parent_span._ref_count.decr() == 0: + parent_span.finish() + + def submit_callbacks(self, parent_span): + for i in range(3): + parent_span._ref_count.incr() + self.executor.submit(self.task, + 0.1 + random.randint(200, 500) * .001, + parent_span) diff --git a/testbed/test_multiple_callbacks/test_tornado.py b/testbed/test_multiple_callbacks/test_tornado.py new file mode 100644 index 0000000..ca81c6c --- /dev/null +++ b/testbed/test_multiple_callbacks/test_tornado.py @@ -0,0 +1,64 @@ +from __future__ import print_function + +import random + +from tornado import gen, ioloop + +from opentracing.mocktracer import MockTracer +from ..span_propagation import TornadoScopeManager, tracer_stack_context +from ..testcase import OpenTracingTestCase +from ..utils import get_logger, stop_loop_when + + +random.seed() +logger = get_logger(__name__) + + +class TestTornado(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(TornadoScopeManager()) + self.loop = ioloop.IOLoop.current() + + def test_main(self): + @gen.coroutine + def main_task(): + with self.tracer.start_active_span('parent'): + tasks = self.submit_callbacks() + yield tasks + + with tracer_stack_context(): + self.loop.add_callback(main_task) + + stop_loop_when(self.loop, + lambda: len(self.tracer.finished_spans()) == 4) + self.loop.start() + + spans = self.tracer.finished_spans() + self.assertEquals(len(spans), 4) + self.assertNamesEqual(spans, ['task', 'task', 'task', 'parent']) + + for i in range(3): + self.assertSameTrace(spans[i], spans[-1]) + self.assertIsChildOf(spans[i], spans[-1]) + + @gen.coroutine + def task(self, interval, parent_span): + logger.info('Starting task') + + # NOTE: No need to reactivate the parent_span, as TracerStackContext + # keeps track of it, BUT a limitation is that, yielding + # upon multiple coroutines, we cannot mess with the context, + # so no active span set here. + assert self.tracer.active_span is not None + with self.tracer.start_span('task'): + yield gen.sleep(interval) + + def submit_callbacks(self): + parent_span = self.tracer.scope_manager.active.span + tasks = [] + for i in range(3): + interval = 0.1 + random.randint(200, 500) * 0.001 + t = self.task(interval, parent_span) + tasks.append(t) + + return tasks diff --git a/testbed/test_nested_callbacks/README.md b/testbed/test_nested_callbacks/README.md new file mode 100644 index 0000000..b6da45e --- /dev/null +++ b/testbed/test_nested_callbacks/README.md @@ -0,0 +1,41 @@ +# Nested callbacks example. + +This example shows a `Span` for a top-level operation, and how it can be passed down on a list of nested callbacks (always one at a time), have it as the active one for each of them, and finished **only** when the last one executes. For Python, we have decided to do it in a **fire-and-forget** fashion. + +Implementation details: +- For `threading`, `gevent` and `tornado` the `Span` is manually passed down the call chain, activating it in each corotuine/task. +- For `tornado`, the active `Span` is not passed nor activated down the chain as the custom `StackContext` automatically propagates it. + +`threading` implementation: +```python + def submit(self): + span = self.tracer.scope_manager.active.span + + def task1(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag('key1', '1') + + def task2(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag('key2', '2') + ... +``` + +`tornado` implementation: +```python + @gen.coroutine + def submit(self): + span = self.tracer.scope_manager.active.span + + @gen.coroutine + def task1(): + self.assertEqual(span, self.tracer.scope_manager.active.span) + span.set_tag('key1', '1') + + @gen.coroutine + def task2(): + self.assertEqual(span, + self.tracer.scope_manager.active.span) + span.set_tag('key2', '2') + +``` diff --git a/testbed/test_nested_callbacks/__init__.py b/testbed/test_nested_callbacks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testbed/test_nested_callbacks/test_asyncio.py b/testbed/test_nested_callbacks/test_asyncio.py new file mode 100644 index 0000000..1cc971c --- /dev/null +++ b/testbed/test_nested_callbacks/test_asyncio.py @@ -0,0 +1,58 @@ +from __future__ import print_function + + +import asyncio + +from opentracing.mocktracer import MockTracer +from ..span_propagation import AsyncioScopeManager +from ..testcase import OpenTracingTestCase +from ..utils import stop_loop_when + + +class TestAsyncio(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(AsyncioScopeManager()) + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Start a Span and let the callback-chain + # finish it when the task is done + async def task(): + with self.tracer.start_active_span('one', finish_on_close=False): + self.submit() + + self.loop.create_task(task()) + + stop_loop_when(self.loop, + lambda: len(self.tracer.finished_spans()) == 1) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].operation_name, 'one') + + for i in range(1, 4): + self.assertEqual(spans[0].tags.get('key%s' % i, None), str(i)) + + def submit(self): + span = self.tracer.scope_manager.active.span + + async def task1(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag('key1', '1') + + async def task2(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag('key2', '2') + + async def task3(): + with self.tracer.scope_manager.activate(span, + False): + span.set_tag('key3', '3') + span.finish() + + self.loop.create_task(task3()) + + self.loop.create_task(task2()) + + self.loop.create_task(task1()) diff --git a/testbed/test_nested_callbacks/test_gevent.py b/testbed/test_nested_callbacks/test_gevent.py new file mode 100644 index 0000000..976eced --- /dev/null +++ b/testbed/test_nested_callbacks/test_gevent.py @@ -0,0 +1,50 @@ +from __future__ import print_function + + +import gevent + +from opentracing.mocktracer import MockTracer +from ..span_propagation import GeventScopeManager +from ..testcase import OpenTracingTestCase + + +class TestGevent(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(GeventScopeManager()) + + def test_main(self): + # Start a Span and let the callback-chain + # finish it when the task is done + with self.tracer.start_active_span('one', finish_on_close=False): + self.submit() + + gevent.wait() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].operation_name, 'one') + + for i in range(1, 4): + self.assertEqual(spans[0].tags.get('key%s' % i, None), str(i)) + + def submit(self): + span = self.tracer.scope_manager.active.span + + def task1(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag('key1', '1') + + def task2(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag('key2', '2') + + def task3(): + with self.tracer.scope_manager.activate(span, + True): + span.set_tag('key3', '3') + + gevent.spawn(task3) + + gevent.spawn(task2) + + gevent.spawn(task1) diff --git a/testbed/test_nested_callbacks/test_threads.py b/testbed/test_nested_callbacks/test_threads.py new file mode 100644 index 0000000..5473e8f --- /dev/null +++ b/testbed/test_nested_callbacks/test_threads.py @@ -0,0 +1,56 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from opentracing.mocktracer import MockTracer +from ..testcase import OpenTracingTestCase +from ..utils import await_until + + +class TestThreads(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def tearDown(self): + self.executor.shutdown(False) + + def test_main(self): + # Start a Span and let the callback-chain + # finish it when the task is done + with self.tracer.start_active_span('one', finish_on_close=False): + self.submit() + + # Cannot shutdown the executor and wait for the callbacks + # to be run, as in such case only the first will be executed, + # and the rest will get canceled. + await_until(lambda: len(self.tracer.finished_spans()) == 1, 5) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].operation_name, 'one') + + for i in range(1, 4): + self.assertEqual(spans[0].tags.get('key%s' % i, None), str(i)) + + def submit(self): + span = self.tracer.scope_manager.active.span + + def task1(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag('key1', '1') + + def task2(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag('key2', '2') + + def task3(): + with self.tracer.scope_manager.activate(span, + True): + span.set_tag('key3', '3') + + self.executor.submit(task3) + + self.executor.submit(task2) + + self.executor.submit(task1) diff --git a/testbed/test_nested_callbacks/test_tornado.py b/testbed/test_nested_callbacks/test_tornado.py new file mode 100644 index 0000000..28ed3cc --- /dev/null +++ b/testbed/test_nested_callbacks/test_tornado.py @@ -0,0 +1,64 @@ +from __future__ import print_function + + +from tornado import gen, ioloop + +from opentracing.mocktracer import MockTracer +from ..testcase import OpenTracingTestCase +from ..span_propagation import TornadoScopeManager, tracer_stack_context +from ..utils import stop_loop_when + + +class TestTornado(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(TornadoScopeManager()) + self.loop = ioloop.IOLoop.current() + + def test_main(self): + # Start a Span and let the callback-chain + # finish it when the task is done + with tracer_stack_context(): + with self.tracer.start_active_span('one', finish_on_close=False): + self.submit() + + stop_loop_when(self.loop, + lambda: len(self.tracer.finished_spans()) == 1) + self.loop.start() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].operation_name, 'one') + + for i in range(1, 4): + self.assertEqual(spans[0].tags.get('key%s' % i, None), str(i)) + + # Since StackContext propagates the active Span + # from the first callback, we don't need to re-activate + # it later on anymore. + @gen.coroutine + def submit(self): + span = self.tracer.scope_manager.active.span + + @gen.coroutine + def task1(): + self.assertEqual(span, self.tracer.scope_manager.active.span) + span.set_tag('key1', '1') + + @gen.coroutine + def task2(): + self.assertEqual(span, + self.tracer.scope_manager.active.span) + span.set_tag('key2', '2') + + @gen.coroutine + def task3(): + self.assertEqual(span, + self.tracer.scope_manager.active.span) + span.set_tag('key3', '3') + span.finish() + + yield task3() + + yield task2() + + yield task1() diff --git a/testbed/test_subtask_span_propagation/README.md b/testbed/test_subtask_span_propagation/README.md new file mode 100644 index 0000000..4abfb26 --- /dev/null +++ b/testbed/test_subtask_span_propagation/README.md @@ -0,0 +1,38 @@ +# Subtask Span propagation example. + +This example shows an active `Span` being simply propagated to the sutasks -either threads or coroutines-, and finished **by** the parent task. In real-life scenarios instrumentation libraries may help with `Span` propagation **if** not offered by default (see implementation details below), but we show here the case without such help. + +Implementation details: +- For `threading`, `gevent` and `asyncio` the `Span` is manually passed down the call chain, being manually reactivated it in each corotuine/task. +- For `tornado`, the active `Span` is not passed nor activated down the chain as the custom `StackContext` automatically propagates it. + +`threading` implementation: +```python + def parent_task(self, message): + with self.tracer.start_active_span('parent') as scope: + f = self.executor.submit(self.child_task, message, scope.span) + res = f.result() + + return res + + def child_task(self, message, span): + with self.tracer.scope_manager.activate(span, False): + with self.tracer.start_active_span('child'): + return '%s::response' % message +``` + +`tornado` implementation: +```python + def parent_task(self, message): + with self.tracer.start_active_span('parent'): + res = yield self.child_task(message) + + raise gen.Return(res) + + @gen.coroutine + def child_task(self, message): + # No need to pass/activate the parent Span, as + # it stays in the context. + with self.tracer.start_active_span('child'): + raise gen.Return('%s::response' % message) +``` diff --git a/testbed/test_subtask_span_propagation/__init__.py b/testbed/test_subtask_span_propagation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testbed/test_subtask_span_propagation/test_asyncio.py b/testbed/test_subtask_span_propagation/test_asyncio.py new file mode 100644 index 0000000..ce1126e --- /dev/null +++ b/testbed/test_subtask_span_propagation/test_asyncio.py @@ -0,0 +1,35 @@ +from __future__ import absolute_import, print_function + +import functools + +import asyncio + +from opentracing.mocktracer import MockTracer +from ..span_propagation import AsyncioScopeManager +from ..testcase import OpenTracingTestCase + + +class TestAsyncio(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(AsyncioScopeManager()) + self.loop = asyncio.get_event_loop() + + def test_main(self): + res = self.loop.run_until_complete(self.parent_task('message')) + self.assertEqual(res, 'message::response') + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + self.assertNamesEqual(spans, ['child', 'parent']) + self.assertIsChildOf(spans[0], spans[1]) + + async def parent_task(self, message): # noqa + with self.tracer.start_active_span('parent') as scope: + res = await self.child_task(message, scope.span) + + return res + + async def child_task(self, message, span): + with self.tracer.scope_manager.activate(span, False): + with self.tracer.start_active_span('child'): + return '%s::response' % message diff --git a/testbed/test_subtask_span_propagation/test_gevent.py b/testbed/test_subtask_span_propagation/test_gevent.py new file mode 100644 index 0000000..f1ffa4b --- /dev/null +++ b/testbed/test_subtask_span_propagation/test_gevent.py @@ -0,0 +1,32 @@ +from __future__ import absolute_import, print_function + +import gevent + +from opentracing.mocktracer import MockTracer +from ..span_propagation import GeventScopeManager +from ..testcase import OpenTracingTestCase + + +class TestGevent(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(GeventScopeManager()) + + def test_main(self): + res = gevent.spawn(self.parent_task, 'message').get() + self.assertEqual(res, 'message::response') + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + self.assertNamesEqual(spans, ['child', 'parent']) + self.assertIsChildOf(spans[0], spans[1]) + + def parent_task(self, message): + with self.tracer.start_active_span('parent') as scope: + res = gevent.spawn(self.child_task, message, scope.span).get() + + return res + + def child_task(self, message, span): + with self.tracer.scope_manager.activate(span, False): + with self.tracer.start_active_span('child'): + return '%s::response' % message diff --git a/testbed/test_subtask_span_propagation/test_threads.py b/testbed/test_subtask_span_propagation/test_threads.py new file mode 100644 index 0000000..b504922 --- /dev/null +++ b/testbed/test_subtask_span_propagation/test_threads.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import, print_function + +from concurrent.futures import ThreadPoolExecutor + +from opentracing.mocktracer import MockTracer +from ..testcase import OpenTracingTestCase + + +class TestThreads(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + res = self.executor.submit(self.parent_task, 'message').result() + self.assertEqual(res, 'message::response') + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + self.assertNamesEqual(spans, ['child', 'parent']) + self.assertIsChildOf(spans[0], spans[1]) + + def parent_task(self, message): + with self.tracer.start_active_span('parent') as scope: + f = self.executor.submit(self.child_task, message, scope.span) + res = f.result() + + return res + + def child_task(self, message, span): + with self.tracer.scope_manager.activate(span, False): + with self.tracer.start_active_span('child'): + return '%s::response' % message diff --git a/testbed/test_subtask_span_propagation/test_tornado.py b/testbed/test_subtask_span_propagation/test_tornado.py new file mode 100644 index 0000000..bb9c895 --- /dev/null +++ b/testbed/test_subtask_span_propagation/test_tornado.py @@ -0,0 +1,40 @@ +from __future__ import absolute_import, print_function + +import functools + +from tornado import gen, ioloop + +from opentracing.mocktracer import MockTracer +from ..span_propagation import TornadoScopeManager, tracer_stack_context +from ..testcase import OpenTracingTestCase + + +class TestTornado(OpenTracingTestCase): + def setUp(self): + self.tracer = MockTracer(TornadoScopeManager()) + self.loop = ioloop.IOLoop.current() + + def test_main(self): + parent_task = functools.partial(self.parent_task, 'message') + with tracer_stack_context(): + res = self.loop.run_sync(parent_task) + self.assertEqual(res, 'message::response') + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + self.assertNamesEqual(spans, ['child', 'parent']) + self.assertIsChildOf(spans[0], spans[1]) + + @gen.coroutine + def parent_task(self, message): + with self.tracer.start_active_span('parent'): + res = yield self.child_task(message) + + raise gen.Return(res) + + @gen.coroutine + def child_task(self, message): + # No need to pass/activate the parent Span, as + # it stays in the context. + with self.tracer.start_active_span('child'): + raise gen.Return('%s::response' % message) diff --git a/testbed/testcase.py b/testbed/testcase.py new file mode 100644 index 0000000..2785c74 --- /dev/null +++ b/testbed/testcase.py @@ -0,0 +1,20 @@ +import unittest + + +class OpenTracingTestCase(unittest.TestCase): + def assertSameTrace(self, spanA, spanB): + return self.assertEqual(spanA.context.trace_id, + spanB.context.trace_id) + + def assertNotSameTrace(self, spanA, spanB): + return self.assertNotEqual(spanA.context.trace_id, + spanB.context.trace_id) + + def assertIsChildOf(self, spanA, spanB): + return self.assertEqual(spanA.parent_id, spanB.context.span_id) + + def assertIsNotChildOf(self, spanA, spanB): + return self.assertNotEqual(spanA.parent_id, spanB.context.span_id) + + def assertNamesEqual(self, spans, names): + self.assertEqual(list(map(lambda x: x.operation_name, spans)), names) diff --git a/testbed/utils.py b/testbed/utils.py new file mode 100644 index 0000000..e591a65 --- /dev/null +++ b/testbed/utils.py @@ -0,0 +1,88 @@ +from __future__ import print_function + +import logging +import six +import threading +import time + + +class RefCount(object): + """Thread-safe counter""" + def __init__(self, count=1): + self._lock = threading.Lock() + self._count = count + + def incr(self): + with self._lock: + self._count += 1 + return self._count + + def decr(self): + with self._lock: + self._count -= 1 + return self._count + + +def await_until(func, timeout=5.0): + """Polls for func() to return True""" + end_time = time.time() + timeout + while time.time() < end_time and not func(): + time.sleep(0.01) + + +def stop_loop_when(loop, cond_func, timeout=5.0): + """ + Registers a periodic callback that stops the loop when cond_func() == True. + Compatible with both Tornado and asyncio. + """ + if cond_func() or timeout <= 0.0: + loop.stop() + return + + timeout -= 0.1 + loop.call_later(0.1, stop_loop_when, loop, cond_func, timeout) + + +def get_logger(name): + """Returns a logger with log level set to INFO""" + logging.basicConfig(level=logging.INFO) + return logging.getLogger(name) + + +def get_one_by_tag(spans, key, value): + """Return a single Span with a tag value/key from a list, + errors if more than one is found.""" + + found = [] + for span in spans: + if span.tags.get(key) == value: + found.append(span) + + if len(found) > 1: + raise RuntimeError('Too many values') + + return found[0] if len(found) > 0 else None + + +def get_one_by_operation_name(spans, name): + """Return a single Span with a name from a list, + errors if more than one is found.""" + found = [] + for span in spans: + if span.operation_name == name: + found.append(span) + + if len(found) > 1: + raise RuntimeError('Too many values') + + return found[0] if len(found) > 0 else None + + +def get_tags_count(span, prefix): + """Returns the tag count with the given prefix from a Span""" + test_keys = set() + for key in six.iterkeys(span.tags): + if key.startswith(prefix): + test_keys.add(key) + + return len(test_keys) From c80a8eb0d47ab073e704b7fd084c4a38c8d5b0eb Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Mon, 9 Apr 2018 12:43:42 +0200 Subject: [PATCH 07/15] Include the testbed target in travis.yml. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 37c6741..8039b4e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,5 @@ install: - make bootstrap script: - - make test lint + - make test testbed lint From e453f86f960474937f6bbd596d9e229ec4ab8902 Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Mon, 9 Apr 2018 13:19:24 +0200 Subject: [PATCH 08/15] Leave testbed out of the sdist step. --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index af58e4f..83edb53 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ recursive-include opentracing * recursive-include example *.py recursive-include example *.thrift recursive-include tests *.py +prune testbed include * global-exclude *.pyc graft docs From f9d6c51c708ce117505057278154675e2df31414 Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Mon, 9 Apr 2018 13:23:23 +0200 Subject: [PATCH 09/15] Preparing release 2.0.0rc2 --- CHANGELOG.rst | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7a9fb4d..3abaf91 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,7 +3,7 @@ History ======= -2.0.0rc1 (2018-02-20) +2.0.0rc2 (2018-04-09) --------------------- - Implement ScopeManager for in-process propagation. diff --git a/setup.py b/setup.py index 7842ee5..1e50814 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='opentracing', - version='2.0.0rc1', + version='2.0.0rc2', author='The OpenTracing Authors', author_email='opentracing@googlegroups.com', description='OpenTracing API for Python. See documentation at http://opentracing.io', From 306aff6e4ab11df1780d62e2bf2a3e5a7d78ef6a Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Tue, 5 Jun 2018 19:30:49 +0200 Subject: [PATCH 10/15] Do not produce bytecode files for testbed/. (#85) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8724559..27aff9f 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,7 @@ test: $(pytest) $(test_args) testbed: - python -m testbed + PYTHONDONTWRITEBYTECODE=1 python -m testbed jenkins: pip install -r requirements.txt From cb3eb7b4ceb95c9050894a89f07769220ab9e022 Mon Sep 17 00:00:00 2001 From: Kyle Verhoog Date: Thu, 14 Jun 2018 11:33:00 -0400 Subject: [PATCH 11/15] Fix some v2.0.0 docs (#82) * Lint doc line lengths. * More formatting. * Apply a similar formatting as the requests library. --- CHANGELOG.rst | 4 +- docs/conf.py | 2 +- opentracing/ext/scope.py | 8 +- opentracing/propagation.py | 46 ++++---- opentracing/scope.py | 44 ++++--- opentracing/scope_manager.py | 34 +++--- opentracing/span.py | 108 ++++++++++-------- opentracing/tracer.py | 216 ++++++++++++++++++++++------------- 8 files changed, 270 insertions(+), 192 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3abaf91..17e425b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,7 +27,7 @@ History 1.2.1 (2016-09-22) ------------------ -- Make Span.log(self, **kwargs) smarter +- Make Span.log(self, \**kwargs) smarter 1.2.0 (2016-09-21) @@ -115,7 +115,7 @@ History ------------------ - Change inheritance to match api-go: TraceContextSource extends codecs, -Tracer extends TraceContextSource + Tracer extends TraceContextSource - Create API harness diff --git a/docs/conf.py b/docs/conf.py index ffd1887..206eacf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,7 +16,7 @@ html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] -html_static_path = ['_static'] +html_static_path = [] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] intersphinx_mapping = { diff --git a/opentracing/ext/scope.py b/opentracing/ext/scope.py index 707a2dd..7731818 100644 --- a/opentracing/ext/scope.py +++ b/opentracing/ext/scope.py @@ -31,16 +31,16 @@ def __init__(self, manager, span, finish_on_close): """Initialize a `Scope` for the given `Span` object. :param span: the `Span` wrapped by this `Scope`. - :param finish_on_close: whether span should automatically be - finished when `Scope#close()` is called. + :param finish_on_close: whether :class:`Span` should automatically be + finished when :meth:`Scope.close()` is called. """ super(ThreadLocalScope, self).__init__(manager, span) self._finish_on_close = finish_on_close self._to_restore = manager.active def close(self): - """Mark the end of the active period for this {@link Scope}, - updating ScopeManager#active in the process. + """Mark the end of the active period for this :class:`Scope`, + updating :attr:`ScopeManager.active` in the process. """ if self.manager.active is not self: return diff --git a/opentracing/propagation.py b/opentracing/propagation.py index cb3257a..74b6888 100644 --- a/opentracing/propagation.py +++ b/opentracing/propagation.py @@ -23,9 +23,9 @@ class UnsupportedFormatException(Exception): """UnsupportedFormatException should be used when the provided format - value is unknown or disallowed by the Tracer. + value is unknown or disallowed by the :class:`Tracer`. - See Tracer.inject() and Tracer.extract(). + See :meth:`Tracer.inject()` and :meth:`Tracer.extract()`. """ pass @@ -34,16 +34,16 @@ class InvalidCarrierException(Exception): """InvalidCarrierException should be used when the provided carrier instance does not match what the `format` argument requires. - See Tracer.inject() and Tracer.extract(). + See :meth:`Tracer.inject()` and :meth:`Tracer.extract()`. """ pass class SpanContextCorruptedException(Exception): - """SpanContextCorruptedException should be used when the underlying span - context state is seemingly present but not well-formed. + """SpanContextCorruptedException should be used when the underlying + :class:`SpanContext` state is seemingly present but not well-formed. - See Tracer.inject() and Tracer.extract(). + See :meth:`Tracer.inject()` and :meth:`Tracer.extract()`. """ pass @@ -51,8 +51,8 @@ class SpanContextCorruptedException(Exception): class Format(object): """A namespace for builtin carrier formats. - These static constants are intended for use in the Tracer.inject() and - Tracer.extract() methods. E.g.:: + These static constants are intended for use in the :meth:`Tracer.inject()` + and :meth:`Tracer.extract()` methods. E.g.:: tracer.inject(span.context, Format.BINARY, binary_carrier) @@ -62,29 +62,29 @@ class Format(object): """ The BINARY format represents SpanContexts in an opaque bytearray carrier. - For both Tracer.inject() and Tracer.extract() the carrier should be a - bytearray instance. Tracer.inject() must append to the bytearray carrier - (rather than replace its contents). + For both :meth:`Tracer.inject()` and :meth:`Tracer.extract()` the carrier + should be a bytearray instance. :meth:`Tracer.inject()` must append to the + bytearray carrier (rather than replace its contents). """ TEXT_MAP = 'text_map' """ - The TEXT_MAP format represents SpanContexts in a python dict mapping from - strings to strings. + The TEXT_MAP format represents :class:`SpanContext`\ s in a python ``dict`` + mapping from strings to strings. Both the keys and the values have unrestricted character sets (unlike the HTTP_HEADERS format). - NOTE: The TEXT_MAP carrier dict may contain unrelated data (e.g., - arbitrary gRPC metadata). As such, the Tracer implementation should use a - prefix or other convention to distinguish Tracer-specific key:value - pairs. + NOTE: The TEXT_MAP carrier ``dict`` may contain unrelated data (e.g., + arbitrary gRPC metadata). As such, the :class:`Tracer` implementation + should use a prefix or other convention to distinguish tracer-specific + key:value pairs. """ HTTP_HEADERS = 'http_headers' """ - The HTTP_HEADERS format represents SpanContexts in a python dict mapping - from character-restricted strings to strings. + The HTTP_HEADERS format represents :class:`SpanContext`\ s in a python + ``dict`` mapping from character-restricted strings to strings. Keys and values in the HTTP_HEADERS carrier must be suitable for use as HTTP headers (without modification or further escaping). That is, the @@ -92,8 +92,8 @@ class Format(object): be preserved by various intermediaries, and the values should be URL-escaped. - NOTE: The HTTP_HEADERS carrier dict may contain unrelated data (e.g., - arbitrary gRPC metadata). As such, the Tracer implementation should use a - prefix or other convention to distinguish Tracer-specific key:value - pairs. + NOTE: The HTTP_HEADERS carrier ``dict`` may contain unrelated data (e.g., + arbitrary gRPC metadata). As such, the :class:`Tracer` implementation + should use a prefix or other convention to distinguish tracer-specific + key:value pairs. """ diff --git a/opentracing/scope.py b/opentracing/scope.py index 9aa321a..dc28583 100644 --- a/opentracing/scope.py +++ b/opentracing/scope.py @@ -22,38 +22,46 @@ class Scope(object): - """A `Scope` formalizes the activation and deactivation of a `Span`, - usually from a CPU standpoint. Many times a `Span` will be extant (in that - `Span#finish()` has not been called) despite being in a non-runnable state - from a CPU/scheduler standpoint. For instance, a `Span` representing the - client side of an RPC will be unfinished but blocked on IO while the RPC is - still outstanding. A `Scope` defines when a given `Span` is scheduled - and on the path. + """A scope formalizes the activation and deactivation of a :class:`Span`, + usually from a CPU standpoint. Many times a :class:`Span` will be extant + (in that :meth:`Span.finish()` has not been called) despite being in a + non-runnable state from a CPU/scheduler standpoint. For instance, a + :class:`Span` representing the client side of an RPC will be unfinished but + blocked on IO while the RPC is still outstanding. A scope defines when a + given :class:`Span` is scheduled and on the path. + + :param manager: the :class:`ScopeManager` that created this :class:`Scope`. + :type manager: ScopeManager + + :param span: the :class:`Span` used for this :class:`Scope`. + :type span: Span """ def __init__(self, manager, span): - """Initializes a `Scope` for the given `Span` object. - - :param manager: the `ScopeManager` that created this `Scope` - :param span: the `Span` used for this `Scope` - """ + """Initializes a scope for *span*.""" self._manager = manager self._span = span @property def span(self): - """Returns the `Span` wrapped by this `Scope`.""" + """Returns the :class:`Span` wrapped by this :class:`Scope`. + + :rtype: Span + """ return self._span @property def manager(self): - """Returns the `ScopeManager` that created this `Scope`.""" + """Returns the :class:`ScopeManager` that created this :class:`Scope`. + + :rtype: ScopeManager + """ return self._manager def close(self): - """Marks the end of the active period for this `Scope`, - updating `ScopeManager#active` in the process. + """Marks the end of the active period for this :class:`Scope`, updating + :attr:`ScopeManager.active` in the process. - NOTE: Calling `close()` more than once on a single `Scope` instance + NOTE: Calling this method more than once on a single :class:`Scope` leads to undefined behavior. """ pass @@ -63,7 +71,7 @@ def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): - """Calls `close()` when the execution is outside the Python + """Calls :meth:`close()` when the execution is outside the Python Context Manager. """ self.close() diff --git a/opentracing/scope_manager.py b/opentracing/scope_manager.py index 72b0d10..eb6bb6c 100644 --- a/opentracing/scope_manager.py +++ b/opentracing/scope_manager.py @@ -25,9 +25,8 @@ class ScopeManager(object): - """The `ScopeManager` interface abstracts both the activation of `Span` - instances (via `ScopeManager#activate(span, finish_on_close)`) and - access to an active `Span` / `Scope` (via `ScopeManager#active`). + """The :class:`ScopeManager` interface abstracts both the activation of + a :class:`Span` and access to an active :class:`Span`/:class:`Scope`. """ def __init__(self): # TODO: `tracer` should not be None, but we don't have a reference; @@ -37,27 +36,30 @@ def __init__(self): self._noop_scope = Scope(self, self._noop_span) def activate(self, span, finish_on_close): - """Makes a `Span` instance active. + """Makes a :class:`Span` active. - :param span: the `Span` that should become active. - :param finish_on_close: whether span should be automatically - finished when `Scope#close()` is called. + :param span: the :class:`Span` that should become active. + :param finish_on_close: whether :class:`Span` should be automatically + finished when :meth:`Scope.close()` is called. - :return: a `Scope` instance to control the end of the active period for - the `Span`. It is a programming error to neglect to call - `Scope#close()` on the returned instance. + :rtype: Scope + :return: a :class:`Scope` to control the end of the active period for + *span*. It is a programming error to neglect to call + :meth:`Scope.close()` on the returned instance. """ return self._noop_scope @property def active(self): - """Returns the currently active `Scope` which can be used to access the - currently active `Scope#span`. + """Returns the currently active :class:`Scope` which can be used to access the + currently active :attr:`Scope.span`. - If there is a non-null `Scope`, its wrapped `Span` becomes an implicit - parent of any newly-created `Span` at `Tracer#start_active_span()` - time. + If there is a non-null :class:`Scope`, its wrapped :class:`Span` + becomes an implicit parent of any newly-created :class:`Span` at + :meth:`Tracer.start_active_span()` time. - :return: the `Scope` that is active, or `None` if not available. + :rtype: Scope + :return: the :class:`Scope` that is active, or ``None`` if not + available. """ return self._noop_scope diff --git a/opentracing/span.py b/opentracing/span.py index b5c87e7..a99bf61 100644 --- a/opentracing/span.py +++ b/opentracing/span.py @@ -24,14 +24,15 @@ class SpanContext(object): - """SpanContext represents Span state that must propagate to descendant - Spans and across process boundaries. + """SpanContext represents :class:`Span` state that must propagate to + descendant :class:`Span`\ s and across process boundaries. SpanContext is logically divided into two pieces: the user-level "Baggage" - (see set_baggage_item and get_baggage_item) that propagates across Span - boundaries and any Tracer-implementation-specific fields that are needed to - identify or otherwise contextualize the associated Span instance (e.g., a - tuple). + (see :meth:`Span.set_baggage_item` and :meth:`Span.get_baggage_item`) that + propagates across :class:`Span` boundaries and any + tracer-implementation-specific fields that are needed to identify or + otherwise contextualize the associated :class:`Span` (e.g., a ``(trace_id, + span_id, sampled)`` tuple). """ EMPTY_BAGGAGE = {} # TODO would be nice to make this immutable @@ -39,15 +40,17 @@ class SpanContext(object): @property def baggage(self): """ - Return baggage associated with this SpanContext. - If no baggage has been added to the span, returns an empty dict. + Return baggage associated with this :class:`SpanContext`. + If no baggage has been added to the :class:`Span`, returns an empty + dict. The caller must not modify the returned dictionary. - See also: Span.set_baggage_item() / Span.get_baggage_item() + See also: :meth:`Span.set_baggage_item()` / + :meth:`Span.get_baggage_item()` :rtype: dict - :return: returns baggage associated with this SpanContext or {}. + :return: baggage associated with this :class:`SpanContext` or ``{}``. """ return SpanContext.EMPTY_BAGGAGE @@ -60,14 +63,13 @@ class Span(object): and these relationships transitively form a DAG. It is common for spans to have at most one parent, and thus most traces are merely tree structures. - Span implements a Context Manager API that allows the following usage: - - .. code-block:: python + Span implements a context manager API that allows the following usage:: with tracer.start_span(operation_name='go_fishing') as span: call_some_service() - In the Context Manager syntax it's not necessary to call span.finish() + In the context manager syntax it's not necessary to call + :meth:`Span.finish()` """ def __init__(self, tracer, context): @@ -76,20 +78,24 @@ def __init__(self, tracer, context): @property def context(self): - """Provides access to the SpanContext associated with this Span. + """Provides access to the :class:`SpanContext` associated with this + :class:`Span`. - The SpanContext contains state that propagates from Span to Span in a - larger trace. + The :class:`SpanContext` contains state that propagates from + :class:`Span` to :class:`Span` in a larger trace. - :return: returns the SpanContext associated with this Span. + :rtype: SpanContext + :return: the :class:`SpanContext` associated with this :class:`Span`. """ return self._context @property def tracer(self): - """Provides access to the Tracer that created this Span. + """Provides access to the :class:`Tracer` that created this + :class:`Span`. - :return: returns the Tracer that created this Span. + :rtype: Tracer + :return: the :class:`Tracer` that created this :class:`Span`. """ return self._tracer @@ -97,42 +103,51 @@ def set_operation_name(self, operation_name): """Changes the operation name. :param operation_name: the new operation name - :return: Returns the Span itself, for call chaining. + :type operation_name: str + + :rtype: Span + :return: the :class:`Span` itself, for call chaining. """ return self def finish(self, finish_time=None): - """Indicates that the work represented by this span has completed or + """Indicates that the work represented by this :class:`Span` has completed or terminated. - With the exception of the `Span.context` property, the semantics of all - other Span methods are undefined after `finish()` has been invoked. + With the exception of the :attr:`Span.context` property, the semantics + of all other :class:`Span` methods are undefined after + :meth:`Span.finish()` has been invoked. - :param finish_time: an explicit Span finish timestamp as a unix - timestamp per time.time() + :param finish_time: an explicit :class:`Span` finish timestamp as a + unix timestamp per :meth:`time.time()` + :type finish_time: float """ pass def set_tag(self, key, value): - """Attaches a key/value pair to the span. + """Attaches a key/value pair to the :class:`Span`. The value must be a string, a bool, or a numeric type. If the user calls set_tag multiple times for the same key, - the behavior of the tracer is undefined, i.e. it is implementation - specific whether the tracer will retain the first value, or the last - value, or pick one randomly, or even keep all of them. + the behavior of the :class:`Tracer` is undefined, i.e. it is + implementation specific whether the :class:`Tracer` will retain the + first value, or the last value, or pick one randomly, or even keep all + of them. :param key: key or name of the tag. Must be a string. + :type key: str + :param value: value of the tag. + :type value: string or bool or int or float - :return: Returns the Span itself, for call chaining. :rtype: Span + :return: the :class:`Span` itself, for call chaining. """ return self def log_kv(self, key_values, timestamp=None): - """Adds a log record to the Span. + """Adds a log record to the :class:`Span`. For example:: @@ -145,24 +160,24 @@ def log_kv(self, key_values, timestamp=None): :param key_values: A dict of string keys and values of any type :type key_values: dict - :param timestamp: A unix timestamp per time.time(); current time if - None + :param timestamp: A unix timestamp per :meth:`time.time()`; current + time if ``None`` :type timestamp: float - :return: Returns the Span itself, for call chaining. :rtype: Span + :return: the :class:`Span` itself, for call chaining. """ return self def set_baggage_item(self, key, value): - """Stores a Baggage item in the span as a key/value pair. + """Stores a Baggage item in the :class:`Span` as a key/value pair. Enables powerful distributed context propagation functionality where arbitrary application data can be carried along the full path of request execution throughout the system. Note 1: Baggage is only propagated to the future (recursive) children - of this Span. + of this :class:`Span`. Note 2: Baggage is sent in-band with every subsequent local and remote calls, so this feature must be used with care. @@ -173,34 +188,35 @@ def set_baggage_item(self, key, value): :param value: Baggage item value :type value: str - :rtype : Span + :rtype: Span :return: itself, for chaining the calls. """ return self def get_baggage_item(self, key): - """Retrieves value of the Baggage item with the given key. + """Retrieves value of the baggage item with the given key. - :param key: key of the Baggage item + :param key: key of the baggage item :type key: str - :rtype : str - :return: value of the Baggage item with given key, or None. + :rtype: str + :return: value of the baggage item with given key, or ``None``. """ return None def __enter__(self): - """Invoked when span is used as a context manager. + """Invoked when :class:`Span` is used as a context manager. - :return: returns the Span itself + :rtype: Span + :return: the :class:`Span` itself """ return self def __exit__(self, exc_type, exc_val, exc_tb): - """Ends context manager and calls finish() on the span. + """Ends context manager and calls finish() on the :class:`Span`. If exception has occurred during execution, it is automatically added - as a tag to the span. + as a tag to the :class:`Span`. """ if exc_type: self.log_kv({ diff --git a/opentracing/tracer.py b/opentracing/tracer.py index 232ab58..478b560 100644 --- a/opentracing/tracer.py +++ b/opentracing/tracer.py @@ -53,11 +53,12 @@ def scope_manager(self): @property def active_span(self): - """Provides access to the the active Span. This is a shorthand for - Tracer.scope_manager.active.span, and None will be returned if - Scope.span is None. + """Provides access to the the active :class:`Span`. This is a shorthand for + :attr:`Tracer.scope_manager.active.span`, and ``None`` will be + returned if :attr:`Scope.span` is ``None``. - :return: returns the active Span. + :rtype: Span + :return: the active :class:`Span`. """ scope = self._scope_manager.active return None if scope is None else scope.span @@ -70,18 +71,19 @@ def start_active_span(self, start_time=None, ignore_active_span=False, finish_on_close=True): - """Returns a newly started and activated `Scope`. + """Returns a newly started and activated :class:`Scope`. - The returned `Scope` supports with-statement contexts. For example: + The returned :class:`Scope` supports with-statement contexts. For + example:: with tracer.start_active_span('...') as scope: scope.span.set_tag('http.method', 'GET') do_some_work() - # Span.finish() is called as part of Scope deactivation through + # Span.finish() is called as part of scope deactivation through # the with statement. - It's also possible to not finish the `Span` when the `Scope` context - expires: + It's also possible to not finish the :class:`Span` when the + :class:`Scope` context expires:: with tracer.start_active_span('...', finish_on_close=False) as scope: @@ -91,24 +93,39 @@ def start_active_span(self, # `finish_on_close` is `False`. :param operation_name: name of the operation represented by the new - span from the perspective of the current service. - :param child_of: (optional) a Span or SpanContext instance representing - the parent in a REFERENCE_CHILD_OF Reference. If specified, the - `references` parameter must be omitted. - :param references: (optional) a list of Reference objects that identify - one or more parent SpanContexts. (See the Reference documentation + :class:`Span` from the perspective of the current service. + :type operation_name: str + + :param child_of: (optional) a :class:`Span` or :class:`SpanContext` + instance representing the parent in a REFERENCE_CHILD_OF reference. + If specified, the `references` parameter must be omitted. + :type child_of: Span or SpanContext + + :param references: (optional) references that identify one or more + parent :class:`SpanContext`\ s. (See the Reference documentation for detail). - :param tags: an optional dictionary of Span Tags. The caller gives up - ownership of that dictionary, because the Tracer may use it as-is - to avoid extra data copying. - :param start_time: an explicit Span start time as a unix timestamp per - time.time(). + :type references: :obj:`list` of :class:`Reference` + + :param tags: an optional dictionary of :class:`Span` tags. The caller + gives up ownership of that dictionary, because the :class:`Tracer` + may use it as-is to avoid extra data copying. + :type tags: dict + + :param start_time: an explicit :class:`Span` start time as a unix + timestamp per :meth:`time.time()`. + :type start_time: float + :param ignore_active_span: (optional) an explicit flag that ignores - the current active `Scope` and creates a root `Span`. - :param finish_on_close: whether span should automatically be finished - when `Scope#close()` is called. + the current active :class:`Scope` and creates a root :class:`Span`. + :type ignore_active_span: bool + + :param finish_on_close: whether :class:`Span` should automatically be + finished when :meth:`Scope.close()` is called. + :type finish_on_close: bool - :return: a `Scope`, already registered via the `ScopeManager`. + :rtype: Scope + :return: a :class:`Scope`, already registered via the + :class:`ScopeManager`. """ return self._noop_scope @@ -119,22 +136,23 @@ def start_span(self, tags=None, start_time=None, ignore_active_span=False): - """Starts and returns a new Span representing a unit of work. + """Starts and returns a new :class:`Span` representing a unit of work. - Starting a root Span (a Span with no causal references):: + Starting a root :class:`Span` (a :class:`Span` with no causal + references):: tracer.start_span('...') - Starting a child Span (see also start_child_span()):: + Starting a child :class:`Span` (see also :meth:`start_child_span()`):: tracer.start_span( '...', child_of=parent_span) - Starting a child Span in a more verbose way:: + Starting a child :class:`Span` in a more verbose way:: tracer.start_span( '...', @@ -142,22 +160,34 @@ def start_span(self, :param operation_name: name of the operation represented by the new - span from the perspective of the current service. - :param child_of: (optional) a Span or SpanContext instance representing - the parent in a REFERENCE_CHILD_OF Reference. If specified, the - `references` parameter must be omitted. - :param references: (optional) a list of Reference objects that identify - one or more parent SpanContexts. (See the Reference documentation - for detail) - :param tags: an optional dictionary of Span Tags. The caller gives up - ownership of that dictionary, because the Tracer may use it as-is - to avoid extra data copying. + :class:`Span` from the perspective of the current service. + :type operation_name: str + + :param child_of: (optional) a :class:`Span` or :class:`SpanContext` + representing the parent in a REFERENCE_CHILD_OF reference. If + specified, the `references` parameter must be omitted. + :type child_of: Span or SpanContext + + :param references: (optional) references that identify one or more + parent :class:`SpanContext`\ s. (See the Reference documentation + for detail). + :type references: :obj:`list` of :class:`Reference` + + :param tags: an optional dictionary of :class:`Span` tags. The caller + gives up ownership of that dictionary, because the :class:`Tracer` + may use it as-is to avoid extra data copying. + :type tags: dict + :param start_time: an explicit Span start time as a unix timestamp per - time.time() + :meth:`time.time()` + :type start_time: float + :param ignore_active_span: an explicit flag that ignores the current - active `Scope` and creates a root `Span`. + active :class:`Scope` and creates a root :class:`Span`. + :type ignore_active_span: bool - :return: Returns an already-started Span instance. + :rtype: Span + :return: an already-started :class:`Span` instance. """ return self._noop_span @@ -165,16 +195,18 @@ def inject(self, span_context, format, carrier): """Injects `span_context` into `carrier`. The type of `carrier` is determined by `format`. See the - opentracing.propagation.Format class/namespace for the built-in - OpenTracing formats. + :class:`Format` class/namespace for the built-in OpenTracing formats. - Implementations MUST raise opentracing.UnsupportedFormatException if + Implementations *must* raise :exc:`UnsupportedFormatException` if `format` is unknown or disallowed. - :param span_context: the SpanContext instance to inject + :param span_context: the :class:`SpanContext` instance to inject + :type span_context: SpanContext + :param format: a python object instance that represents a given carrier format. `format` may be of any type, and `format` equality - is defined by python `==` equality. + is defined by python ``==`` equality. + :type format: Format :param carrier: the format-specific carrier object to inject into """ if format in Tracer._supported_formats: @@ -182,27 +214,30 @@ def inject(self, span_context, format, carrier): raise UnsupportedFormatException(format) def extract(self, format, carrier): - """Returns a SpanContext instance extracted from a `carrier` of the - given `format`, or None if no such SpanContext could be found. + """Returns a :class:`SpanContext` instance extracted from a `carrier` of the + given `format`, or ``None`` if no such :class:`SpanContext` could be + found. The type of `carrier` is determined by `format`. See the - opentracing.propagation.Format class/namespace for the built-in - OpenTracing formats. + :class:`Format` class/namespace for the built-in OpenTracing formats. - Implementations MUST raise opentracing.UnsupportedFormatException if + Implementations *must* raise :exc:`UnsupportedFormatException` if `format` is unknown or disallowed. - Implementations may raise opentracing.InvalidCarrierException, - opentracing.SpanContextCorruptedException, or implementation-specific - errors if there are problems with `carrier`. + Implementations may raise :exc:`InvalidCarrierException`, + :exc:`SpanContextCorruptedException`, or implementation-specific errors + if there are problems with `carrier`. + :param format: a python object instance that represents a given carrier format. `format` may be of any type, and `format` equality - is defined by python `==` equality. + is defined by python ``==`` equality. + :param carrier: the format-specific carrier object to extract from - :return: a SpanContext instance extracted from `carrier` or None if no - such span context could be found. + :rtype: SpanContext + :return: a :class:`SpanContext` extracted from `carrier` or ``None`` if + no such :class:`SpanContext` could be found. """ if format in Tracer._supported_formats: return self._noop_span_context @@ -222,22 +257,23 @@ class ReferenceType(object): # We use namedtuple since references are meant to be immutable. # We subclass it to expose a standard docstring. class Reference(namedtuple('Reference', ['type', 'referenced_context'])): - """A Reference pairs a reference type with a referenced SpanContext. + """A Reference pairs a reference type with a referenced :class:`SpanContext`. - References are used by Tracer.start_span() to describe the relationships - between Spans. + References are used by :meth:`Tracer.start_span()` to describe the + relationships between :class:`Span`\ s. - Tracer implementations must ignore references where referenced_context is - None. This behavior allows for simpler code when an inbound RPC request - contains no tracing information and as a result tracer.extract() returns - None:: + :class:`Tracer` implementations must ignore references where + referenced_context is ``None``. This behavior allows for simpler code when + an inbound RPC request contains no tracing information and as a result + :meth:`Tracer.extract()` returns ``None``:: parent_ref = tracer.extract(opentracing.HTTP_HEADERS, request.headers) span = tracer.start_span( 'operation', references=child_of(parent_ref) ) - See `child_of` and `follows_from` helpers for creating these references. + See :meth:`child_of` and :meth:`follows_from` helpers for creating these + references. """ pass @@ -245,11 +281,14 @@ class Reference(namedtuple('Reference', ['type', 'referenced_context'])): def child_of(referenced_context=None): """child_of is a helper that creates CHILD_OF References. - :param referenced_context: the (causal parent) SpanContext to reference. - If None is passed, this reference must be ignored by the tracer. + :param referenced_context: the (causal parent) :class:`SpanContext` to + reference. If ``None`` is passed, this reference must be ignored by + the :class:`Tracer`. + :type referenced_context: SpanContext :rtype: Reference - :return: A Reference suitable for Tracer.start_span(..., references=...) + :return: A reference suitable for ``Tracer.start_span(..., + references=...)`` """ return Reference( type=ReferenceType.CHILD_OF, @@ -259,11 +298,14 @@ def child_of(referenced_context=None): def follows_from(referenced_context=None): """follows_from is a helper that creates FOLLOWS_FROM References. - :param referenced_context: the (causal parent) SpanContext to reference - If None is passed, this reference must be ignored by the tracer. + :param referenced_context: the (causal parent) :class:`SpanContext` to + reference. If ``None`` is passed, this reference must be ignored by the + :class:`Tracer`. + :type referenced_context: SpanContext :rtype: Reference - :return: A Reference suitable for Tracer.start_span(..., references=...) + :return: A Reference suitable for ``Tracer.start_span(..., + references=...)`` """ return Reference( type=ReferenceType.FOLLOWS_FROM, @@ -271,9 +313,10 @@ def follows_from(referenced_context=None): def start_child_span(parent_span, operation_name, tags=None, start_time=None): - """A shorthand method that starts a child_of span for a given parent span. + """A shorthand method that starts a `child_of` :class:`Span` for a given + parent :class:`Span`. - Equivalent to calling + Equivalent to calling:: parent_span.tracer().start_span( operation_name, @@ -281,16 +324,25 @@ def start_child_span(parent_span, operation_name, tags=None, start_time=None): tags=tags, start_time=start_time) - :param parent_span: the Span which will act as the parent in the returned - Span's child_of reference. - :param operation_name: the operation name for the child Span instance - :param tags: optional dict of Span Tags. The caller gives up ownership of - that dict, because the Tracer may use it as-is to avoid extra data - copying. - :param start_time: an explicit Span start time as a unix timestamp per - time.time(). + :param parent_span: the :class:`Span` which will act as the parent in the + returned :class:`Span`\ s child_of reference. + :type parent_span: Span + + :param operation_name: the operation name for the child :class:`Span` + instance + :type operation_name: str + + :param tags: optional dict of :class:`Span` tags. The caller gives up + ownership of that dict, because the :class:`Tracer` may use it as-is to + avoid extra data copying. + :type tags: dict + + :param start_time: an explicit :class:`Span` start time as a unix timestamp + per :meth:`time.time()`. + :type start_time: float - :return: Returns an already-started Span instance. + :rtype: Span + :return: an already-started :class:`Span` instance. """ return parent_span.tracer.start_span( operation_name=operation_name, From 26d6f0fc7f58746d95a2e9b88e779c0cce3c4bda Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Wed, 27 Jun 2018 02:12:18 +0200 Subject: [PATCH 12/15] Further small documentation refinements (#88) * Add proper documentation for MockTracer. * Further small refinements. --- docs/api.rst | 5 +++++ opentracing/mocktracer/tracer.py | 37 ++++++++++++++++++++++++++------ opentracing/scope.py | 2 +- opentracing/tracer.py | 7 ++++-- 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 5e1bf6f..4ea1f7e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -45,3 +45,8 @@ Exceptions .. autoclass:: opentracing.UnsupportedFormatException :members: + +MockTracer +-------------- +.. autoclass:: opentracing.mocktracer.MockTracer + :members: diff --git a/opentracing/mocktracer/tracer.py b/opentracing/mocktracer/tracer.py index 0f15287..7d63ef6 100644 --- a/opentracing/mocktracer/tracer.py +++ b/opentracing/mocktracer/tracer.py @@ -31,14 +31,23 @@ class MockTracer(Tracer): + """MockTracer makes it easy to test the semantics of OpenTracing + instrumentation. + + By using a MockTracer as a :class:`~opentracing.Tracer` implementation + for tests, a developer can assert that :class:`~opentracing.Span` + properties and relationships with other + **Spans** are defined as expected by instrumentation code. + + By default, MockTracer registers propagators for :attr:`Format.TEXT_MAP`, + :attr:`Format.HTTP_HEADERS` and :attr:`Format.BINARY`. The user should + call :func:`register_propagator()` for each additional inject/extract + format. + """ def __init__(self, scope_manager=None): - """Initialize a MockTracer instance. + """Initialize a MockTracer instance.""" - By default, MockTracer registers propagators for Format.TEXT_MAP, - Format.HTTP_HEADERS and Format.BINARY. The user should call - register_propagator() for each additional inject/extract format. - """ scope_manager = ThreadLocalScopeManager() \ if scope_manager is None else scope_manager super(MockTracer, self).__init__(scope_manager) @@ -56,8 +65,9 @@ def __init__(self, scope_manager=None): def register_propagator(self, format, propagator): """Register a propagator with this MockTracer. - :param string format: a Format identifier like Format.TEXT_MAP - :param Propagator propagator: a Propagator instance to handle + :param string format: a :class:`~opentracing.Format` + identifier like :attr:`~opentracing.Format.TEXT_MAP` + :param **Propagator** propagator: a **Propagator** instance to handle inject/extract calls involving `format` """ self._propagators[format] = propagator @@ -70,10 +80,23 @@ def _register_required_propagators(self): self.register_propagator(Format.BINARY, BinaryPropagator()) def finished_spans(self): + """Return a copy of all finished **Spans** started by this MockTracer + (since construction or the last call to :meth:`~MockTracer.reset()`) + + :rtype: list + :return: a copy of the finished **Spans**. + """ with self._spans_lock: return list(self._finished_spans) def reset(self): + """Clear the finished **Spans** queue. + + Note that this does **not** have any effect on **Spans** created by + MockTracer that have not finished yet; those + will still be enqueued in :meth:`~MockTracer.finished_spans()` + when they :func:`finish()`. + """ with self._spans_lock: self._finished_spans = [] diff --git a/opentracing/scope.py b/opentracing/scope.py index dc28583..aca1973 100644 --- a/opentracing/scope.py +++ b/opentracing/scope.py @@ -67,7 +67,7 @@ def close(self): pass def __enter__(self): - """Allows `Scope` to be used inside a Python Context Manager.""" + """Allows :class:`Scope` to be used inside a Python Context Manager.""" return self def __exit__(self, exc_type, exc_val, exc_tb): diff --git a/opentracing/tracer.py b/opentracing/tracer.py index 478b560..9b042ee 100644 --- a/opentracing/tracer.py +++ b/opentracing/tracer.py @@ -48,7 +48,10 @@ def __init__(self, scope_manager=None): @property def scope_manager(self): - """ScopeManager accessor""" + """Provides access to the current :class:`~opentracing.ScopeManager`. + + :rtype: :class:`~opentracing.ScopeManager` + """ return self._scope_manager @property @@ -57,7 +60,7 @@ def active_span(self): :attr:`Tracer.scope_manager.active.span`, and ``None`` will be returned if :attr:`Scope.span` is ``None``. - :rtype: Span + :rtype: :class:`~opentracing.Span` :return: the active :class:`Span`. """ scope = self._scope_manager.active From fc6fbd457c97e86e4c4f816db3b104802a193cd3 Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Wed, 27 Jun 2018 22:29:46 +0200 Subject: [PATCH 13/15] Scope managers integration (#83) * Prepare the scope manager in testbed for production usage. * Mention the ScopeManager implementations in README.rst. * Prepare the inline documentation for our scope managers. --- Makefile | 1 - README.rst | 17 ++ docs/api.rst | 18 +- opentracing/ext/scope_manager.py | 63 ---- opentracing/ext/scope_manager/__init__.py | 79 +++++ opentracing/ext/scope_manager/asyncio.py | 138 +++++++++ opentracing/ext/scope_manager/constants.py | 26 ++ opentracing/ext/scope_manager/gevent.py | 114 +++++++ opentracing/ext/scope_manager/tornado.py | 280 ++++++++++++++++++ opentracing/harness/api_check.py | 62 +--- opentracing/harness/scope_check.py | 160 ++++++++++ requirements-testbed.txt | 5 - setup.py | 5 +- testbed/README.md | 23 +- testbed/span_propagation.py | 158 ---------- .../test_asyncio.py | 2 +- .../test_gevent.py | 2 +- .../test_tornado.py | 3 +- testbed/test_client_server/test_asyncio.py | 2 +- testbed/test_client_server/test_gevent.py | 2 +- testbed/test_client_server/test_tornado.py | 3 +- .../test_asyncio.py | 2 +- .../test_gevent.py | 2 +- .../test_tornado.py | 3 +- testbed/test_late_span_finish/test_asyncio.py | 2 +- testbed/test_late_span_finish/test_gevent.py | 2 +- testbed/test_late_span_finish/test_tornado.py | 3 +- .../test_listener_per_request/test_asyncio.py | 2 +- .../test_listener_per_request/test_gevent.py | 2 +- .../test_listener_per_request/test_tornado.py | 2 +- .../test_multiple_callbacks/test_asyncio.py | 2 +- .../test_multiple_callbacks/test_gevent.py | 2 +- .../test_multiple_callbacks/test_tornado.py | 3 +- testbed/test_nested_callbacks/test_asyncio.py | 2 +- testbed/test_nested_callbacks/test_gevent.py | 2 +- testbed/test_nested_callbacks/test_tornado.py | 3 +- .../test_asyncio.py | 2 +- .../test_gevent.py | 2 +- .../test_tornado.py | 3 +- tests/conftest.py | 29 ++ tests/ext/scope_manager/__init__.py | 0 .../ext/scope_manager/test_asyncio.py | 44 +-- tests/ext/scope_manager/test_gevent.py | 36 +++ .../test_threadlocal.py} | 42 +-- tests/ext/scope_manager/test_tornado.py | 38 +++ tests/ext/test_scope.py | 59 ---- tests/test_api_check_mixin.py | 15 - tests/test_scope_check_mixin.py | 66 +++++ 48 files changed, 1067 insertions(+), 466 deletions(-) delete mode 100644 opentracing/ext/scope_manager.py create mode 100644 opentracing/ext/scope_manager/__init__.py create mode 100644 opentracing/ext/scope_manager/asyncio.py create mode 100644 opentracing/ext/scope_manager/constants.py create mode 100644 opentracing/ext/scope_manager/gevent.py create mode 100644 opentracing/ext/scope_manager/tornado.py create mode 100644 opentracing/harness/scope_check.py delete mode 100644 requirements-testbed.txt delete mode 100644 testbed/span_propagation.py create mode 100644 tests/conftest.py create mode 100644 tests/ext/scope_manager/__init__.py rename opentracing/ext/scope.py => tests/ext/scope_manager/test_asyncio.py (53%) create mode 100644 tests/ext/scope_manager/test_gevent.py rename tests/ext/{test_scope_manager.py => scope_manager/test_threadlocal.py} (53%) create mode 100644 tests/ext/scope_manager/test_tornado.py delete mode 100644 tests/ext/test_scope.py create mode 100644 tests/test_scope_check_mixin.py diff --git a/Makefile b/Makefile index 27aff9f..6e20292 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,6 @@ check-virtual-env: bootstrap: check-virtual-env pip install -r requirements.txt pip install -r requirements-test.txt - pip install -r requirements-testbed.txt python setup.py develop clean: clean-build clean-pyc clean-test diff --git a/README.rst b/README.rst index 9c39c77..43b0308 100644 --- a/README.rst +++ b/README.rst @@ -192,6 +192,23 @@ Each service/framework ought to provide a specific ``ScopeManager`` implementati that relies on their own request-local storage (thread-local storage, or coroutine-based storage for asynchronous frameworks, for example). +Scope managers +^^^^^^^^^^^^^^ + +This project includes a set of ``ScopeManager`` implementations under the ``opentracing.ext.scope_manager`` submodule, which can be imported on demand: + +.. code-block:: python + + from opentracing.ext.scope_manager import ThreadLocalScopeManager + +There exist implementations for ``thread-local`` (the default), ``gevent``, ``Tornado`` and ``asyncio``: + +.. code-block:: python + + from opentracing.ext.scope_manager.gevent import GeventScopeManager # requires gevent + from opentracing.ext.scope_manager.tornado import TornadoScopeManager # requires Tornado + from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager # requires Python 3.4 or newer. + Development ----------- diff --git a/docs/api.rst b/docs/api.rst index 4ea1f7e..a09e18f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -45,8 +45,24 @@ Exceptions .. autoclass:: opentracing.UnsupportedFormatException :members: - + MockTracer -------------- .. autoclass:: opentracing.mocktracer.MockTracer :members: + +Scope managers +-------------- +.. autoclass:: opentracing.ext.scope_manager.ThreadLocalScopeManager + :members: + +.. autoclass:: opentracing.ext.scope_manager.gevent.GeventScopeManager + :members: + +.. autoclass:: opentracing.ext.scope_manager.tornado.TornadoScopeManager + :members: + +.. autofunction:: opentracing.ext.scope_manager.tornado.tracer_stack_context + +.. autoclass:: opentracing.ext.scope_manager.asyncio.AsyncioScopeManager + :members: \ No newline at end of file diff --git a/opentracing/ext/scope_manager.py b/opentracing/ext/scope_manager.py deleted file mode 100644 index 1be3d65..0000000 --- a/opentracing/ext/scope_manager.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (c) The OpenTracing Authors. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from __future__ import absolute_import - -import threading - -from opentracing import ScopeManager - -from .scope import ThreadLocalScope - - -class ThreadLocalScopeManager(ScopeManager): - """ScopeManager implementation that stores the current active `Scope` - using thread-local storage. - """ - def __init__(self): - self._tls_scope = threading.local() - - def activate(self, span, finish_on_close): - """Make a `Span` instance active. - - :param span: the `Span` that should become active. - :param finish_on_close: whether span should automatically be - finished when `Scope#close()` is called. - - :return: a `Scope` instance to control the end of the active period for - the `Span`. It is a programming error to neglect to call - `Scope#close()` on the returned instance. - """ - scope = ThreadLocalScope(self, span, finish_on_close) - setattr(self._tls_scope, 'active', scope) - return scope - - @property - def active(self): - """Return the currently active `Scope` which can be used to access the - currently active `Scope#span`. - - If there is a non-null `Scope`, its wrapped `Span` becomes an implicit - parent of any newly-created `Span` at `Tracer#start_span()`/ - `Tracer#start_active_span()` time. - - :return: the `Scope` that is active, or `None` if not available. - """ - return getattr(self._tls_scope, 'active', None) diff --git a/opentracing/ext/scope_manager/__init__.py b/opentracing/ext/scope_manager/__init__.py new file mode 100644 index 0000000..ce6ea7a --- /dev/null +++ b/opentracing/ext/scope_manager/__init__.py @@ -0,0 +1,79 @@ +# Copyright (c) The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + +import threading + +from opentracing import Scope, ScopeManager + + +class ThreadLocalScopeManager(ScopeManager): + """ + :class:`~opentracing.ScopeManager` implementation that stores the + current active :class:`~opentracing.Scope` using thread-local storage. + """ + def __init__(self): + self._tls_scope = threading.local() + + def activate(self, span, finish_on_close): + """ + Make a :class:`~opentracing.Span` instance active. + + :param span: the :class:`~opentracing.Span` that should become active. + :param finish_on_close: whether *span* should automatically be + finished when :meth:`Scope.close()` is called. + + :return: a :class:`~opentracing.Scope` instance to control the end + of the active period for the :class:`~opentracing.Span`. + It is a programming error to neglect to call :meth:`Scope.close()` + on the returned instance. + """ + scope = _ThreadLocalScope(self, span, finish_on_close) + setattr(self._tls_scope, 'active', scope) + return scope + + @property + def active(self): + """ + Return the currently active :class:`~opentracing.Scope` which + can be used to access the currently active + :attr:`Scope.span`. + + :return: the :class:`~opentracing.Scope` that is active, + or ``None`` if not available. + """ + return getattr(self._tls_scope, 'active', None) + + +class _ThreadLocalScope(Scope): + def __init__(self, manager, span, finish_on_close): + super(_ThreadLocalScope, self).__init__(manager, span) + self._finish_on_close = finish_on_close + self._to_restore = manager.active + + def close(self): + if self.manager.active is not self: + return + + if self._finish_on_close: + self.span.finish() + + setattr(self._manager._tls_scope, 'active', self._to_restore) diff --git a/opentracing/ext/scope_manager/asyncio.py b/opentracing/ext/scope_manager/asyncio.py new file mode 100644 index 0000000..c8c4b70 --- /dev/null +++ b/opentracing/ext/scope_manager/asyncio.py @@ -0,0 +1,138 @@ +# Copyright (c) The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + +import asyncio + +from opentracing import Scope +from opentracing.ext.scope_manager import ThreadLocalScopeManager +from .constants import ACTIVE_ATTR + + +class AsyncioScopeManager(ThreadLocalScopeManager): + """ + :class:`~opentracing.ScopeManager` implementation for **asyncio** + that stores the :class:`~opentracing.Scope` in the current + :class:`Task` (:meth:`Task.current_task()`), falling back to + thread-local storage if none was being executed. + + Automatic :class:`~opentracing.Span` propagation from + parent coroutines to their children is not provided, which needs to be + done manually: + + .. code-block:: python + + async def child_coroutine(span): + # activate the parent Span, but do not finish it upon + # deactivation. That will be done by the parent coroutine. + with tracer.scope_manager.activate(span, finish_on_close=False): + with tracer.start_active_span('child') as scope: + ... + + async def parent_coroutine(): + with tracer.start_active_span('parent') as scope: + ... + await child_coroutine(span) + ... + + """ + + def activate(self, span, finish_on_close): + """ + Make a :class:`~opentracing.Span` instance active. + + :param span: the :class:`~opentracing.Span` that should become active. + :param finish_on_close: whether *span* should automatically be + finished when :meth:`Scope.close()` is called. + + If no :class:`Task` is being executed, thread-local + storage will be used to store the :class:`~opentracing.Scope`. + + :return: a :class:`~opentracing.Scope` instance to control the end + of the active period for the :class:`~opentracing.Span`. + It is a programming error to neglect to call :meth:`Scope.close()` + on the returned instance. + """ + + task = self._get_task() + if not task: + return super(AsyncioScopeManager, self).activate(span, + finish_on_close) + + scope = _AsyncioScope(self, span, finish_on_close) + self._set_task_scope(scope, task) + + return scope + + @property + def active(self): + """ + Return the currently active :class:`~opentracing.Scope` which + can be used to access the currently active + :attr:`Scope.span`. + + :return: the :class:`~opentracing.Scope` that is active, + or ``None`` if not available. + """ + + task = self._get_task() + if not task: + return super(AsyncioScopeManager, self).active + + return self._get_task_scope(task) + + def _get_task(self): + try: + # Prevent failure when run from a thread + # without an event loop. + loop = asyncio.get_event_loop() + except RuntimeError: + return None + + return asyncio.Task.current_task(loop=loop) + + def _set_task_scope(self, scope, task=None): + if task is None: + task = self._get_task() + + setattr(task, ACTIVE_ATTR, scope) + + def _get_task_scope(self, task=None): + if task is None: + task = self._get_task() + + return getattr(task, ACTIVE_ATTR, None) + + +class _AsyncioScope(Scope): + def __init__(self, manager, span, finish_on_close): + super(_AsyncioScope, self).__init__(manager, span) + self._finish_on_close = finish_on_close + self._to_restore = manager.active + + def close(self): + if self.manager.active is not self: + return + + self.manager._set_task_scope(self._to_restore) + + if self._finish_on_close: + self.span.finish() diff --git a/opentracing/ext/scope_manager/constants.py b/opentracing/ext/scope_manager/constants.py new file mode 100644 index 0000000..d5897aa --- /dev/null +++ b/opentracing/ext/scope_manager/constants.py @@ -0,0 +1,26 @@ +# Copyright (c) The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +from __future__ import absolute_import + +# --------------------------------------------------------------------------- +# ACTIVE_ATTR (string) is the name of the attribute storing the active Scope +# in an external object, e.g. a coroutine or greenlet. +# --------------------------------------------------------------------------- +ACTIVE_ATTR = '__ot_active' diff --git a/opentracing/ext/scope_manager/gevent.py b/opentracing/ext/scope_manager/gevent.py new file mode 100644 index 0000000..6a3c1d9 --- /dev/null +++ b/opentracing/ext/scope_manager/gevent.py @@ -0,0 +1,114 @@ +# Copyright (c) The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + +import gevent + +from opentracing import Scope, ScopeManager +from .constants import ACTIVE_ATTR + + +class GeventScopeManager(ScopeManager): + """ + :class:`~opentracing.ScopeManager` implementation for **gevent** + that stores the :class:`~opentracing.Scope` in the current greenlet + (:func:`gevent.getcurrent()`). + + Automatic :class:`~opentracing.Span` propagation from parent greenlets to + their children is not provided, which needs to be + done manually: + + .. code-block:: python + + def child_greenlet(span): + # activate the parent Span, but do not finish it upon + # deactivation. That will be done by the parent greenlet. + with tracer.scope_manager.activate(span, finish_on_close=False): + with tracer.start_active_span('child') as scope: + ... + + def parent_greenlet(): + with tracer.start_active_span('parent') as scope: + ... + gevent.spawn(child_greenlet, span).join() + ... + + """ + + def activate(self, span, finish_on_close): + """ + Make a :class:`~opentracing.Span` instance active. + + :param span: the :class:`~opentracing.Span` that should become active. + :param finish_on_close: whether *span* should automatically be + finished when :meth:`Scope.close()` is called. + + :return: a :class:`~opentracing.Scope` instance to control the end + of the active period for the :class:`~opentracing.Span`. + It is a programming error to neglect to call :meth:`Scope.close()` + on the returned instance. + """ + + scope = _GeventScope(self, span, finish_on_close) + self._set_greenlet_scope(scope) + + return scope + + @property + def active(self): + """ + Return the currently active :class:`~opentracing.Scope` which + can be used to access the currently active + :attr:`Scope.span`. + + :return: the :class:`~opentracing.Scope` that is active, + or ``None`` if not available. + """ + + return self._get_greenlet_scope() + + def _get_greenlet_scope(self, greenlet=None): + if greenlet is None: + greenlet = gevent.getcurrent() + + return getattr(greenlet, ACTIVE_ATTR, None) + + def _set_greenlet_scope(self, scope, greenlet=None): + if greenlet is None: + greenlet = gevent.getcurrent() + + setattr(greenlet, ACTIVE_ATTR, scope) + + +class _GeventScope(Scope): + def __init__(self, manager, span, finish_on_close): + super(_GeventScope, self).__init__(manager, span) + self._finish_on_close = finish_on_close + self._to_restore = manager.active + + def close(self): + if self.manager.active is not self: + return + + self.manager._set_greenlet_scope(self._to_restore) + + if self._finish_on_close: + self.span.finish() diff --git a/opentracing/ext/scope_manager/tornado.py b/opentracing/ext/scope_manager/tornado.py new file mode 100644 index 0000000..958cc8f --- /dev/null +++ b/opentracing/ext/scope_manager/tornado.py @@ -0,0 +1,280 @@ +# Copyright (c) The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + +import threading +import tornado.stack_context + +from opentracing import Scope +from opentracing.ext.scope_manager import ThreadLocalScopeManager + + +# Implementation based on +# github.com/uber-common/opentracing-python-instrumentation/ + +class TornadoScopeManager(ThreadLocalScopeManager): + """ + :class:`~opentracing.ScopeManager` implementation for **Tornado** + that stores the :class:`~opentracing.Scope` using a custom + :class:`StackContext`, falling back to thread-local storage if + none was found. + + Using it under :func:`tracer_stack_context()` will + also automatically propagate the active :class:`~opentracing.Span` + from parent coroutines to their children: + + .. code-block:: python + + @tornado.gen.coroutine + def child_coroutine(): + # No need to pass 'parent' and activate it here, + # as it is automatically propagated. + with tracer.start_active_span('child') as scope: + ... + + @tornado.gen.coroutine + def parent_coroutine(): + with tracer.start_active_span('parent') as scope: + ... + yield child_coroutine() + ... + + with tracer_stack_context(): + loop.add_callback(parent_coroutine) + + + .. note:: + The current version does not support :class:`~opentracing.Span` + activation in children coroutines when the parent yields over + **multiple** of them, as the context is effectively shared by all, + and the active :class:`~opentracing.Span` state is messed up: + + .. code-block:: python + + @tornado.gen.coroutine + def coroutine(input): + # No span should be activated here. + # The parent Span will remain active, though. + with tracer.start_span('child', child_of=tracer.active_span): + ... + + @tornado.gen.coroutine + def handle_request_wrapper(): + res1 = corotuine('A') + res2 = corotuine('B') + + yield [res1, res2] + """ + + def activate(self, span, finish_on_close): + """ + Make a :class:`~opentracing.Span` instance active. + + :param span: the :class:`~opentracing.Span` that should become active. + :param finish_on_close: whether *span* should automatically be + finished when :meth:`Scope.close()` is called. + + If no :func:`tracer_stack_context()` is detected, thread-local + storage will be used to store the :class:`~opentracing.Scope`. + Observe that in this case the active :class:`~opentracing.Span` + will not be automatically propagated to the child corotuines. + + :return: a :class:`~opentracing.Scope` instance to control the end + of the active period for the :class:`~opentracing.Span`. + It is a programming error to neglect to call :meth:`Scope.close()` + on the returned instance. + """ + + context = self._get_context() + if context is None: + return super(TornadoScopeManager, self).activate(span, + finish_on_close) + + scope = _TornadoScope(self, span, finish_on_close) + context.active = scope + + return scope + + @property + def active(self): + """ + Return the currently active :class:`~opentracing.Scope` which + can be used to access the currently active + :attr:`Scope.span`. + + :return: the :class:`~opentracing.Scope` that is active, + or ``None`` if not available. + """ + + context = self._get_context() + if not context: + return super(TornadoScopeManager, self).active + + return context.active + + def _get_context(self): + return _TracerRequestContextManager.current_context() + + +class _TornadoScope(Scope): + def __init__(self, manager, span, finish_on_close): + super(_TornadoScope, self).__init__(manager, span) + self._finish_on_close = finish_on_close + self._to_restore = manager.active + + def close(self): + context = self.manager._get_context() + if context is None or context.active is not self: + return + + context.active = self._to_restore + + if self._finish_on_close: + self.span.finish() + + +class ThreadSafeStackContext(tornado.stack_context.StackContext): + """ + Thread safe version of Tornado's StackContext (up to 4.3) + Copy of implementation by caspersj@, until tornado-extras is open-sourced. + Tornado's StackContext works as follows: + - When entering a context, create an instance of StackContext and + add add this instance to the current "context stack" + - If execution transfers to another thread (using the wraps helper + method), copy the current "context stack" and apply that in the new + thread when execution starts + - A context stack can be entered/exited by traversing the stack and + calling enter/exit on all elements. This is how the `wraps` helper + method enters/exits in new threads. + - StackContext has an internal pointer to a context factory (i.e. + RequestContext), and an internal stack of applied contexts (instances + of RequestContext) for each instance of StackContext. RequestContext + instances are entered/exited from the stack as the StackContext + is entered/exited + - However, the enter/exit logic and maintenance of this stack of + RequestContext instances is not thread safe. + ``` + def __init__(self, context_factory): + self.context_factory = context_factory + self.contexts = [] + self.active = True + def enter(self): + context = self.context_factory() + self.contexts.append(context) + context.__enter__() + def exit(self, type, value, traceback): + context = self.contexts.pop() + context.__exit__(type, value, traceback) + ``` + Unexpected semantics of Tornado's default StackContext implementation: + - There exist a race on `self.contexts`, where thread A enters a + context, thread B enters a context, and thread A exits its context. + In this case, the exit by thread A pops the instance created by + thread B and calls exit on this instance. + - There exists a race between `enter` and `exit` where thread A + executes the two first statements of enter (create instance and + add to contexts) and thread B executes exit, calling exit on an + instance that has been initialized but not yet exited (and + subsequently this instance will then be entered). + The ThreadSafeStackContext changes the internal contexts stack to be + thread local, fixing both of the above issues. + """ + + def __init__(self, *args, **kwargs): + class LocalContexts(threading.local): + def __init__(self): + super(LocalContexts, self).__init__() + self._contexts = [] + + def append(self, item): + self._contexts.append(item) + + def pop(self): + return self._contexts.pop() + + super(ThreadSafeStackContext, self).__init__(*args, **kwargs) + + if hasattr(self, 'contexts'): + # only patch if context exists + self.contexts = LocalContexts() + + +class _TracerRequestContext(object): + __slots__ = ('active', ) + + def __init__(self, active=None): + self.active = active + + +class _TracerRequestContextManager(object): + _state = threading.local() + _state.context = None + + @classmethod + def current_context(cls): + return getattr(cls._state, 'context', None) + + def __init__(self, context): + self._context = context + + def __enter__(self): + self._prev_context = self.__class__.current_context() + self.__class__._state.context = self._context + return self._context + + def __exit__(self, *_): + self.__class__._state.context = self._prev_context + self._prev_context = None + return False + + +def tracer_stack_context(): + """ + Create a custom Tornado's :class:`StackContext` that allows + :class:`TornadoScopeManager` to store the active + :class:`~opentracing.Span` in the thread-local request context. + + Suppose you have a method ``handle_request(request)`` in the + http server. Instead of calling it directly, use a wrapper: + + .. code-block:: python + + from opentracing.ext.scope_manager.tornado import tracer_stack_context + + @tornado.gen.coroutine + def handle_request_wrapper(request, actual_handler, *args, **kwargs) + + request_wrapper = TornadoRequestWrapper(request=request) + span = http_server.before_request(request=request_wrapper) + + with tracer_stack_context(): + with tracer.scope_manager.activate(span, True): + return actual_handler(*args, **kwargs) + + :return: + Return a custom :class:`StackContext` that allows + :class:`TornadoScopeManager` to activate and propagate + :class:`~opentracing.Span` instances. + """ + context = _TracerRequestContext() + return ThreadSafeStackContext( + lambda: _TracerRequestContextManager(context) + ) diff --git a/opentracing/harness/api_check.py b/opentracing/harness/api_check.py index b9ec246..d38d445 100644 --- a/opentracing/harness/api_check.py +++ b/opentracing/harness/api_check.py @@ -66,8 +66,11 @@ def test_active_span(self): if self.check_scope_manager(): assert tracer.active_span is None + assert tracer.scope_manager.active is None + with tracer.scope_manager.activate(span, True): assert tracer.active_span is span + assert tracer.scope_manager.active.span is span def test_start_active_span(self): # the first usage returns a `Scope` that wraps a root `Span` @@ -125,12 +128,6 @@ def test_start_active_span_default_finish_on_close(self): 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') as scope: - assert scope.span is not None - def test_start_span(self): tracer = self.tracer() span = tracer.start_span(operation_name='Fry') @@ -323,43 +320,6 @@ def test_tracer_start_active_span_scope(self): 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') as parent: - with tracer.start_active_span('Farnsworth'): - 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', finish_on_close=False) - with mock.patch.object(parent.span, 'finish') as finish: - with tracer.start_active_span('Farnsworth'): - 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') - child = tracer.start_active_span('Farnsworth') - 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() @@ -369,19 +329,3 @@ def test_tracer_start_span_scope(self): 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 diff --git a/opentracing/harness/scope_check.py b/opentracing/harness/scope_check.py new file mode 100644 index 0000000..b70df48 --- /dev/null +++ b/opentracing/harness/scope_check.py @@ -0,0 +1,160 @@ +# Copyright (c) The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +from __future__ import absolute_import + +import mock + +from opentracing.span import Span + + +class ScopeCompatibilityCheckMixin(object): + """ + A mixin class for validation that a given scope manager implementation + satisfies the requirements of the OpenTracing API. + """ + + def scope_manager(self): + raise NotImplementedError('Subclass must implement scope_manager()') + + def run_test(self, test_fn): + """ + Utility method that can be optionally defined by ScopeManager + implementers to run the passed test_fn() function + in a given environment, such as a coroutine or greenlet. + By default, it simply runs the passed test_fn() function + in the current thread. + """ + + test_fn() + + def test_missing_active_external(self): + # test that 'active' does not fail outside the run_test() + # implementation (greenlet or coroutine). + scope_manager = self.scope_manager() + assert scope_manager.active is None + + def test_missing_active(self): + def fn(): + scope_manager = self.scope_manager() + assert scope_manager.active is None + + self.run_test(fn) + + def test_activate(self): + def fn(): + scope_manager = self.scope_manager() + span = mock.MagicMock(spec=Span) + + scope = scope_manager.activate(span, False) + assert scope is not None + assert scope_manager.active is scope + + scope.close() + assert span.finish.call_count == 0 + assert scope_manager.active is None + + self.run_test(fn) + + def test_activate_external(self): + # test that activate() does not fail outside the run_test() + # implementation (greenlet or corotuine). + scope_manager = self.scope_manager() + span = mock.MagicMock(spec=Span) + + scope = scope_manager.activate(span, False) + assert scope is not None + assert scope_manager.active is scope + + scope.close() + assert span.finish.call_count == 0 + assert scope_manager.active is None + + def test_activate_finish_on_close(self): + def fn(): + scope_manager = self.scope_manager() + span = mock.MagicMock(spec=Span) + + scope = scope_manager.activate(span, True) + assert scope is not None + assert scope_manager.active is scope + + scope.close() + assert span.finish.call_count == 1 + assert scope_manager.active is None + + self.run_test(fn) + + def test_activate_nested(self): + def fn(): + # when a Scope is closed, the previous one must be re-activated. + scope_manager = self.scope_manager() + parent_span = mock.MagicMock(spec=Span) + child_span = mock.MagicMock(spec=Span) + + with scope_manager.activate(parent_span, True) as parent: + assert parent is not None + assert scope_manager.active is parent + + with scope_manager.activate(child_span, True) as child: + assert child is not None + assert scope_manager.active is child + + assert scope_manager.active is parent + + assert parent_span.finish.call_count == 1 + assert child_span.finish.call_count == 1 + + assert scope_manager.active is None + + self.run_test(fn) + + def test_activate_finish_on_close_nested(self): + def fn(): + # finish_on_close must be correctly handled + scope_manager = self.scope_manager() + parent_span = mock.MagicMock(spec=Span) + child_span = mock.MagicMock(spec=Span) + + parent = scope_manager.activate(parent_span, False) + with scope_manager.activate(child_span, True): + pass + parent.close() + + assert parent_span.finish.call_count == 0 + assert child_span.finish.call_count == 1 + assert scope_manager.active is None + + self.run_test(fn) + + def test_close_wrong_order(self): + def fn(): + # only the active `Scope` can be closed + scope_manager = self.scope_manager() + parent_span = mock.MagicMock(spec=Span) + child_span = mock.MagicMock(spec=Span) + + parent = scope_manager.activate(parent_span, True) + child = scope_manager.activate(child_span, True) + parent.close() + + assert parent_span.finish.call_count == 0 + assert scope_manager.active == child + + self.run_test(fn) diff --git a/requirements-testbed.txt b/requirements-testbed.txt deleted file mode 100644 index 3a943e2..0000000 --- a/requirements-testbed.txt +++ /dev/null @@ -1,5 +0,0 @@ -# add dependencies in setup.py - --r requirements.txt - --e .[testbed] diff --git a/setup.py b/setup.py index 1e50814..d0aec21 100644 --- a/setup.py +++ b/setup.py @@ -34,9 +34,8 @@ 'pytest-cov', 'pytest-mock', 'Sphinx', - 'sphinx_rtd_theme' - ], - 'testbed': [ + 'sphinx_rtd_theme', + 'six>=1.10.0,<2.0', 'gevent==1.2', 'tornado', diff --git a/testbed/README.md b/testbed/README.md index 2ace9ea..53c89e4 100644 --- a/testbed/README.md +++ b/testbed/README.md @@ -18,32 +18,17 @@ Alternatively, due to the organization of the suite, it's possible to run direct ## Tested frameworks -Currently the examples cover `threading`, `tornado`, `gevent` and `asyncio` (which requires Python 3). The implementation of `ScopeManager` for each framework is a basic, simple one, and can be found in [span_propagation.py](span_propagation.py). See details below. +Currently the examples cover `threading`, `tornado`, `gevent` and `asyncio` (which requires Python 3). Each example uses their respective `ScopeManager` instance from `opentracing.ext.scope_manager`, along with their related requirements and limitations. -### threading +### threading, asyncio and gevent -`ThreadScopeManager` uses thread-local storage (through `threading.local()`), and does not provide automatic propagation from thread to thread, which needs to be done manually. - -### gevent - -`GeventScopeManager` uses greenlet-local storage (through `gevent.local.local()`), and does not provide automatic propagation from parent greenlets to their children, which needs to be done manually. +No automatic `Span` propagation between parent and children tasks is provided, and thus the `Span` need to be manually passed down the chain. ### tornado `TornadoScopeManager` uses a variation of `tornado.stack_context.StackContext` to both store **and** automatically propagate the context from parent coroutines to their children. -Because of this, in order to make the `TornadoScopeManager` work, calls need to be started like this: - -```python -with tracer_stack_context(): - my_coroutine() -``` - -At the moment of writing this, yielding over multiple children is not supported, as the context is effectively shared, and switching from coroutine to coroutine messes up the current active `Span`. - -### asyncio - -`AsyncioScopeManager` uses the current `Task` (through `Task.current_task()`) to store the active `Span`, and does not provide automatic propagation from parent `Task` to their children, which needs to be done manually. +Currently, yielding over multiple children is not supported, as the context is effectively shared, and switching from coroutine to coroutine messes up the current active `Span`. ## List of patterns diff --git a/testbed/span_propagation.py b/testbed/span_propagation.py deleted file mode 100644 index 70c54d0..0000000 --- a/testbed/span_propagation.py +++ /dev/null @@ -1,158 +0,0 @@ -import six -import threading -from tornado.stack_context import StackContext -import gevent.local - -from opentracing import ScopeManager, Scope - -if six.PY3: - import asyncio - - -# -# asyncio section. -# -class AsyncioScopeManager(ScopeManager): - def activate(self, span, finish_on_close): - scope = AsyncioScope(self, span, finish_on_close) - - loop = asyncio.get_event_loop() - task = asyncio.Task.current_task(loop=loop) - setattr(task, '__active', scope) - - return scope - - def _get_current_task(self): - loop = asyncio.get_event_loop() - return asyncio.Task.current_task(loop=loop) - - @property - def active(self): - task = self._get_current_task() - return getattr(task, '__active', None) - - -class AsyncioScope(Scope): - def __init__(self, manager, span, finish_on_close): - super(AsyncioScope, self).__init__(manager, span) - self._finish_on_close = finish_on_close - self._to_restore = manager.active - - def close(self): - if self.manager.active is not self: - return - - task = self.manager._get_current_task() - setattr(task, '__active', self._to_restore) - - if self._finish_on_close: - self.span.finish() - -# -# gevent section. -# -class GeventScopeManager(ScopeManager): - def __init__(self): - self._locals = gevent.local.local() - - def activate(self, span, finish_on_close): - scope = GeventScope(self, span, finish_on_close) - setattr(self._locals, 'active', scope) - - return scope - - @property - def active(self): - return getattr(self._locals, 'active', None) - - -class GeventScope(Scope): - def __init__(self, manager, span, finish_on_close): - super(GeventScope, self).__init__(manager, span) - self._finish_on_close = finish_on_close - self._to_restore = manager.active - - def close(self): - if self.manager.active is not self: - return - - setattr(self.manager._locals, 'active', self._to_restore) - - if self._finish_on_close: - self.span.finish() - -# -# tornado section. -# -class TornadoScopeManager(ScopeManager): - def activate(self, span, finish_on_close): - context = self._get_context() - if context is None: - raise Exception('No StackContext detected') - - scope = TornadoScope(self, span, finish_on_close) - context.active = scope - - return scope - - def _get_context(self): - return TracerRequestContextManager.current_context() - - @property - def active(self): - context = self._get_context() - if context is None: - return None - - return context.active - - -class TornadoScope(Scope): - def __init__(self, manager, span, finish_on_close): - super(TornadoScope, self).__init__(manager, span) - self._finish_on_close = finish_on_close - self._to_restore = manager.active - - def close(self): - context = self.manager._get_context() - if context is None or context.active is not self: - return - - context.active = self._to_restore - - if self._finish_on_close: - self.span.finish() - - -class TracerRequestContext(object): - __slots__ = ('active', ) - - def __init__(self, active=None): - self.active = active - - -class TracerRequestContextManager(object): - _state = threading.local() - _state.context = None - - @classmethod - def current_context(cls): - return getattr(cls._state, 'context', None) - - def __init__(self, context): - self._context = context - - def __enter__(self): - self._prev_context = self.__class__.current_context() - self.__class__._state.context = self._context - return self._context - - def __exit__(self, *_): - self.__class__._state.context = self._prev_context - self._prev_context = None - return False - - -def tracer_stack_context(): - context = TracerRequestContext() - return StackContext(lambda: TracerRequestContextManager(context)) diff --git a/testbed/test_active_span_replacement/test_asyncio.py b/testbed/test_active_span_replacement/test_asyncio.py index d7563b5..e1356fb 100644 --- a/testbed/test_active_span_replacement/test_asyncio.py +++ b/testbed/test_active_span_replacement/test_asyncio.py @@ -4,7 +4,7 @@ from opentracing.mocktracer import MockTracer from ..testcase import OpenTracingTestCase -from ..span_propagation import AsyncioScopeManager +from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager from ..utils import stop_loop_when diff --git a/testbed/test_active_span_replacement/test_gevent.py b/testbed/test_active_span_replacement/test_gevent.py index 6bb173d..dbb443a 100644 --- a/testbed/test_active_span_replacement/test_gevent.py +++ b/testbed/test_active_span_replacement/test_gevent.py @@ -3,7 +3,7 @@ import gevent from opentracing.mocktracer import MockTracer -from ..span_propagation import GeventScopeManager +from opentracing.ext.scope_manager.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase diff --git a/testbed/test_active_span_replacement/test_tornado.py b/testbed/test_active_span_replacement/test_tornado.py index 8e5a6cc..b87d8fb 100644 --- a/testbed/test_active_span_replacement/test_tornado.py +++ b/testbed/test_active_span_replacement/test_tornado.py @@ -3,7 +3,8 @@ from tornado import gen, ioloop from opentracing.mocktracer import MockTracer -from ..span_propagation import TornadoScopeManager, tracer_stack_context +from opentracing.ext.scope_manager.tornado import TornadoScopeManager, \ + tracer_stack_context from ..testcase import OpenTracingTestCase from ..utils import stop_loop_when diff --git a/testbed/test_client_server/test_asyncio.py b/testbed/test_client_server/test_asyncio.py index 98df892..c19bfa0 100644 --- a/testbed/test_client_server/test_asyncio.py +++ b/testbed/test_client_server/test_asyncio.py @@ -6,7 +6,7 @@ import opentracing from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from ..span_propagation import AsyncioScopeManager +from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_logger, get_one_by_tag, stop_loop_when diff --git a/testbed/test_client_server/test_gevent.py b/testbed/test_client_server/test_gevent.py index b29a8bf..1123842 100644 --- a/testbed/test_client_server/test_gevent.py +++ b/testbed/test_client_server/test_gevent.py @@ -7,7 +7,7 @@ import opentracing from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from ..span_propagation import GeventScopeManager +from opentracing.ext.scope_manager.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_logger, get_one_by_tag diff --git a/testbed/test_client_server/test_tornado.py b/testbed/test_client_server/test_tornado.py index 8b4edf2..a1e1075 100644 --- a/testbed/test_client_server/test_tornado.py +++ b/testbed/test_client_server/test_tornado.py @@ -6,7 +6,8 @@ import opentracing from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from ..span_propagation import TornadoScopeManager, tracer_stack_context +from opentracing.ext.scope_manager.tornado import TornadoScopeManager, \ + tracer_stack_context from ..testcase import OpenTracingTestCase from ..utils import get_logger, get_one_by_tag, stop_loop_when diff --git a/testbed/test_common_request_handler/test_asyncio.py b/testbed/test_common_request_handler/test_asyncio.py index 5367545..1653d75 100644 --- a/testbed/test_common_request_handler/test_asyncio.py +++ b/testbed/test_common_request_handler/test_asyncio.py @@ -6,7 +6,7 @@ from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from ..span_propagation import AsyncioScopeManager +from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_logger, get_one_by_operation_name, stop_loop_when from .request_handler import RequestHandler diff --git a/testbed/test_common_request_handler/test_gevent.py b/testbed/test_common_request_handler/test_gevent.py index a3b4f0e..6603fe9 100644 --- a/testbed/test_common_request_handler/test_gevent.py +++ b/testbed/test_common_request_handler/test_gevent.py @@ -4,7 +4,7 @@ from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from ..span_propagation import GeventScopeManager +from opentracing.ext.scope_manager.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_logger, get_one_by_operation_name from .request_handler import RequestHandler diff --git a/testbed/test_common_request_handler/test_tornado.py b/testbed/test_common_request_handler/test_tornado.py index c0ce431..a56560e 100644 --- a/testbed/test_common_request_handler/test_tornado.py +++ b/testbed/test_common_request_handler/test_tornado.py @@ -6,7 +6,8 @@ from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from ..span_propagation import TornadoScopeManager, tracer_stack_context +from opentracing.ext.scope_manager.tornado import TornadoScopeManager, \ + tracer_stack_context from ..testcase import OpenTracingTestCase from ..utils import get_logger, get_one_by_operation_name, stop_loop_when from .request_handler import RequestHandler diff --git a/testbed/test_late_span_finish/test_asyncio.py b/testbed/test_late_span_finish/test_asyncio.py index 29228fb..2fa2017 100644 --- a/testbed/test_late_span_finish/test_asyncio.py +++ b/testbed/test_late_span_finish/test_asyncio.py @@ -3,7 +3,7 @@ import asyncio from opentracing.mocktracer import MockTracer -from ..span_propagation import AsyncioScopeManager +from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_logger, stop_loop_when diff --git a/testbed/test_late_span_finish/test_gevent.py b/testbed/test_late_span_finish/test_gevent.py index e30d0e6..fa498a7 100644 --- a/testbed/test_late_span_finish/test_gevent.py +++ b/testbed/test_late_span_finish/test_gevent.py @@ -3,8 +3,8 @@ import gevent from opentracing.mocktracer import MockTracer +from opentracing.ext.scope_manager.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase -from ..span_propagation import GeventScopeManager from ..utils import get_logger diff --git a/testbed/test_late_span_finish/test_tornado.py b/testbed/test_late_span_finish/test_tornado.py index 4b7c6f9..be775c6 100644 --- a/testbed/test_late_span_finish/test_tornado.py +++ b/testbed/test_late_span_finish/test_tornado.py @@ -3,7 +3,8 @@ from tornado import gen, ioloop from opentracing.mocktracer import MockTracer -from ..span_propagation import TornadoScopeManager, tracer_stack_context +from opentracing.ext.scope_manager.tornado import TornadoScopeManager, \ + tracer_stack_context from ..testcase import OpenTracingTestCase from ..utils import get_logger, stop_loop_when diff --git a/testbed/test_listener_per_request/test_asyncio.py b/testbed/test_listener_per_request/test_asyncio.py index 1876e42..a7cec3a 100644 --- a/testbed/test_listener_per_request/test_asyncio.py +++ b/testbed/test_listener_per_request/test_asyncio.py @@ -4,7 +4,7 @@ from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from ..span_propagation import AsyncioScopeManager +from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_one_by_tag diff --git a/testbed/test_listener_per_request/test_gevent.py b/testbed/test_listener_per_request/test_gevent.py index 438f8ed..7dc6434 100644 --- a/testbed/test_listener_per_request/test_gevent.py +++ b/testbed/test_listener_per_request/test_gevent.py @@ -4,7 +4,7 @@ from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from ..span_propagation import GeventScopeManager +from opentracing.ext.scope_manager.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_one_by_tag diff --git a/testbed/test_listener_per_request/test_tornado.py b/testbed/test_listener_per_request/test_tornado.py index 9241208..7b30214 100644 --- a/testbed/test_listener_per_request/test_tornado.py +++ b/testbed/test_listener_per_request/test_tornado.py @@ -6,7 +6,7 @@ from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from ..span_propagation import TornadoScopeManager +from opentracing.ext.scope_manager.tornado import TornadoScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_one_by_tag diff --git a/testbed/test_multiple_callbacks/test_asyncio.py b/testbed/test_multiple_callbacks/test_asyncio.py index c2371a4..713ff03 100644 --- a/testbed/test_multiple_callbacks/test_asyncio.py +++ b/testbed/test_multiple_callbacks/test_asyncio.py @@ -5,7 +5,7 @@ import asyncio from opentracing.mocktracer import MockTracer -from ..span_propagation import AsyncioScopeManager +from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager from ..testcase import OpenTracingTestCase from ..utils import RefCount, get_logger, stop_loop_when diff --git a/testbed/test_multiple_callbacks/test_gevent.py b/testbed/test_multiple_callbacks/test_gevent.py index 8b0ff6b..0005aab 100644 --- a/testbed/test_multiple_callbacks/test_gevent.py +++ b/testbed/test_multiple_callbacks/test_gevent.py @@ -5,8 +5,8 @@ import gevent from opentracing.mocktracer import MockTracer +from opentracing.ext.scope_manager.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase -from ..span_propagation import GeventScopeManager from ..utils import get_logger diff --git a/testbed/test_multiple_callbacks/test_tornado.py b/testbed/test_multiple_callbacks/test_tornado.py index ca81c6c..af659dc 100644 --- a/testbed/test_multiple_callbacks/test_tornado.py +++ b/testbed/test_multiple_callbacks/test_tornado.py @@ -5,7 +5,8 @@ from tornado import gen, ioloop from opentracing.mocktracer import MockTracer -from ..span_propagation import TornadoScopeManager, tracer_stack_context +from opentracing.ext.scope_manager.tornado import TornadoScopeManager, \ + tracer_stack_context from ..testcase import OpenTracingTestCase from ..utils import get_logger, stop_loop_when diff --git a/testbed/test_nested_callbacks/test_asyncio.py b/testbed/test_nested_callbacks/test_asyncio.py index 1cc971c..5d04603 100644 --- a/testbed/test_nested_callbacks/test_asyncio.py +++ b/testbed/test_nested_callbacks/test_asyncio.py @@ -4,7 +4,7 @@ import asyncio from opentracing.mocktracer import MockTracer -from ..span_propagation import AsyncioScopeManager +from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager from ..testcase import OpenTracingTestCase from ..utils import stop_loop_when diff --git a/testbed/test_nested_callbacks/test_gevent.py b/testbed/test_nested_callbacks/test_gevent.py index 976eced..9470de8 100644 --- a/testbed/test_nested_callbacks/test_gevent.py +++ b/testbed/test_nested_callbacks/test_gevent.py @@ -4,7 +4,7 @@ import gevent from opentracing.mocktracer import MockTracer -from ..span_propagation import GeventScopeManager +from opentracing.ext.scope_manager.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase diff --git a/testbed/test_nested_callbacks/test_tornado.py b/testbed/test_nested_callbacks/test_tornado.py index 28ed3cc..24af6ec 100644 --- a/testbed/test_nested_callbacks/test_tornado.py +++ b/testbed/test_nested_callbacks/test_tornado.py @@ -4,8 +4,9 @@ from tornado import gen, ioloop from opentracing.mocktracer import MockTracer +from opentracing.ext.scope_manager.tornado import TornadoScopeManager, \ + tracer_stack_context from ..testcase import OpenTracingTestCase -from ..span_propagation import TornadoScopeManager, tracer_stack_context from ..utils import stop_loop_when diff --git a/testbed/test_subtask_span_propagation/test_asyncio.py b/testbed/test_subtask_span_propagation/test_asyncio.py index ce1126e..afae378 100644 --- a/testbed/test_subtask_span_propagation/test_asyncio.py +++ b/testbed/test_subtask_span_propagation/test_asyncio.py @@ -5,7 +5,7 @@ import asyncio from opentracing.mocktracer import MockTracer -from ..span_propagation import AsyncioScopeManager +from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager from ..testcase import OpenTracingTestCase diff --git a/testbed/test_subtask_span_propagation/test_gevent.py b/testbed/test_subtask_span_propagation/test_gevent.py index f1ffa4b..1a78405 100644 --- a/testbed/test_subtask_span_propagation/test_gevent.py +++ b/testbed/test_subtask_span_propagation/test_gevent.py @@ -3,7 +3,7 @@ import gevent from opentracing.mocktracer import MockTracer -from ..span_propagation import GeventScopeManager +from opentracing.ext.scope_manager.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase diff --git a/testbed/test_subtask_span_propagation/test_tornado.py b/testbed/test_subtask_span_propagation/test_tornado.py index bb9c895..9023722 100644 --- a/testbed/test_subtask_span_propagation/test_tornado.py +++ b/testbed/test_subtask_span_propagation/test_tornado.py @@ -5,7 +5,8 @@ from tornado import gen, ioloop from opentracing.mocktracer import MockTracer -from ..span_propagation import TornadoScopeManager, tracer_stack_context +from opentracing.ext.scope_manager.tornado import TornadoScopeManager, \ + tracer_stack_context from ..testcase import OpenTracingTestCase diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5dff313 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,29 @@ +# Copyright (c) 2016 The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +from __future__ import absolute_import + +import six + +PYTHON3_FILES = [ + 'ext/scope_manager/test_asyncio.py', +] + +if six.PY2: + collect_ignore = PYTHON3_FILES diff --git a/tests/ext/scope_manager/__init__.py b/tests/ext/scope_manager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/opentracing/ext/scope.py b/tests/ext/scope_manager/test_asyncio.py similarity index 53% rename from opentracing/ext/scope.py rename to tests/ext/scope_manager/test_asyncio.py index 7731818..6c01c46 100644 --- a/opentracing/ext/scope.py +++ b/tests/ext/scope_manager/test_asyncio.py @@ -20,32 +20,32 @@ from __future__ import absolute_import -from opentracing import Scope +from concurrent.futures import ThreadPoolExecutor +from unittest import TestCase +import asyncio -class ThreadLocalScope(Scope): - """ThreadLocalScope is an implementation of `opentracing.Scope` - using thread-local storage.""" +from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager +from opentracing.harness.scope_check import ScopeCompatibilityCheckMixin - def __init__(self, manager, span, finish_on_close): - """Initialize a `Scope` for the given `Span` object. - :param span: the `Span` wrapped by this `Scope`. - :param finish_on_close: whether :class:`Span` should automatically be - finished when :meth:`Scope.close()` is called. - """ - super(ThreadLocalScope, self).__init__(manager, span) - self._finish_on_close = finish_on_close - self._to_restore = manager.active +class AsyncioCompabilityCheck(TestCase, ScopeCompatibilityCheckMixin): - def close(self): - """Mark the end of the active period for this :class:`Scope`, - updating :attr:`ScopeManager.active` in the process. - """ - if self.manager.active is not self: - return + def scope_manager(self): + return AsyncioScopeManager() - if self._finish_on_close: - self.span.finish() + def run_test(self, test_fn): + @asyncio.coroutine + def async_test_fn(): + test_fn() + asyncio.get_event_loop().run_until_complete(async_test_fn()) - setattr(self._manager._tls_scope, 'active', self._to_restore) + def test_no_event_loop(self): + # no event loop exists by default in + # new threads, so make sure we don't fail there. + def test_fn(): + manager = self.scope_manager() + assert manager.active is None + + executor = ThreadPoolExecutor(max_workers=1) + executor.submit(test_fn).result() diff --git a/tests/ext/scope_manager/test_gevent.py b/tests/ext/scope_manager/test_gevent.py new file mode 100644 index 0000000..4a50d7e --- /dev/null +++ b/tests/ext/scope_manager/test_gevent.py @@ -0,0 +1,36 @@ +# Copyright (c) The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + +from unittest import TestCase + +import gevent + +from opentracing.ext.scope_manager.gevent import GeventScopeManager +from opentracing.harness.scope_check import ScopeCompatibilityCheckMixin + + +class GeventCompabilityCheck(TestCase, ScopeCompatibilityCheckMixin): + def scope_manager(self): + return GeventScopeManager() + + def run_test(self, test_fn): + gevent.spawn(test_fn).get() diff --git a/tests/ext/test_scope_manager.py b/tests/ext/scope_manager/test_threadlocal.py similarity index 53% rename from tests/ext/test_scope_manager.py rename to tests/ext/scope_manager/test_threadlocal.py index b19af9d..7a73be6 100644 --- a/tests/ext/test_scope_manager.py +++ b/tests/ext/scope_manager/test_threadlocal.py @@ -19,44 +19,12 @@ # THE SOFTWARE. from __future__ import absolute_import -import mock +from unittest import TestCase -from opentracing.tracer import Tracer from opentracing.ext.scope_manager import ThreadLocalScopeManager +from opentracing.harness.scope_check import ScopeCompatibilityCheckMixin -def test_ext_scope_manager_missing_active(): - scope_manager = ThreadLocalScopeManager() - assert scope_manager.active is None - - -def test_ext_scope_manager_activate(): - scope_manager = ThreadLocalScopeManager() - tracer = Tracer() - span = tracer.start_span('test') - - with mock.patch.object(span, 'finish') as finish: - scope = scope_manager.activate(span, False) - assert scope is not None - assert scope_manager.active is scope - - scope.close() - assert finish.call_count == 0 - - assert scope_manager.active is None - - -def test_ext_scope_manager_finish_close(): - scope_manager = ThreadLocalScopeManager() - tracer = Tracer() - span = tracer.start_span('test') - - with mock.patch.object(span, 'finish') as finish: - scope = scope_manager.activate(span, True) - assert scope is not None - assert scope_manager.active is scope - - scope.close() - assert finish.call_count == 1 - - assert scope_manager.active is None +class ThreadLocalCompabilityCheck(TestCase, ScopeCompatibilityCheckMixin): + def scope_manager(self): + return ThreadLocalScopeManager() diff --git a/tests/ext/scope_manager/test_tornado.py b/tests/ext/scope_manager/test_tornado.py new file mode 100644 index 0000000..4ca94a9 --- /dev/null +++ b/tests/ext/scope_manager/test_tornado.py @@ -0,0 +1,38 @@ +# Copyright (c) The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import absolute_import + +from unittest import TestCase + +from tornado import ioloop + +from opentracing.ext.scope_manager.tornado import TornadoScopeManager +from opentracing.ext.scope_manager.tornado import tracer_stack_context +from opentracing.harness.scope_check import ScopeCompatibilityCheckMixin + + +class TornadoCompabilityCheck(TestCase, ScopeCompatibilityCheckMixin): + def scope_manager(self): + return TornadoScopeManager() + + def run_test(self, test_fn): + with tracer_stack_context(): + ioloop.IOLoop.current().run_sync(test_fn) diff --git a/tests/ext/test_scope.py b/tests/ext/test_scope.py deleted file mode 100644 index f1774ac..0000000 --- a/tests/ext/test_scope.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright (c) The OpenTracing Authors. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from __future__ import absolute_import -import mock - -from opentracing.span import Span -from opentracing.ext.scope_manager import ThreadLocalScopeManager - - -def test_ext_scope_implicit_stack(): - scope_manager = ThreadLocalScopeManager() - - background_span = mock.MagicMock(spec=Span) - foreground_span = mock.MagicMock(spec=Span) - - with scope_manager.activate(background_span, True) as background_scope: - assert background_scope is not None - - # Activate a new Scope on top of the background one. - with scope_manager.activate(foreground_span, True) as foreground_scope: - assert foreground_scope is not None - assert scope_manager.active is foreground_scope - - # And now the background_scope should be reinstated. - assert scope_manager.active is background_scope - - assert background_span.finish.call_count == 1 - assert foreground_span.finish.call_count == 1 - - assert scope_manager.active is None - - -def test_when_different_span_is_active(): - scope_manager = ThreadLocalScopeManager() - - span = mock.MagicMock(spec=Span) - active = scope_manager.activate(span, False) - scope_manager.activate(mock.MagicMock(spec=Span), False) - active.close() - - assert span.finish.call_count == 0 diff --git a/tests/test_api_check_mixin.py b/tests/test_api_check_mixin.py index dbde63f..6590e51 100644 --- a/tests/test_api_check_mixin.py +++ b/tests/test_api_check_mixin.py @@ -73,23 +73,8 @@ def test_scope_manager_check_works(self): with self.assertRaises(AssertionError): api_check.test_tracer_start_active_span_scope() - with self.assertRaises(AssertionError): - api_check.test_tracer_start_active_span_nesting() - - with self.assertRaises(AssertionError): - api_check.test_tracer_start_active_span_nesting_finish_on_close() - - with self.assertRaises(AssertionError): - api_check.test_tracer_start_active_span_wrong_close_order() - with self.assertRaises(AssertionError): api_check.test_tracer_start_span_scope() - with self.assertRaises(AssertionError): - api_check.test_tracer_scope_manager_active() - - with self.assertRaises(AssertionError): - api_check.test_tracer_scope_manager_activate() - with self.assertRaises(AssertionError): api_check.test_start_active_span_finish_on_close() diff --git a/tests/test_scope_check_mixin.py b/tests/test_scope_check_mixin.py new file mode 100644 index 0000000..6887bd4 --- /dev/null +++ b/tests/test_scope_check_mixin.py @@ -0,0 +1,66 @@ +# Copyright (c) The OpenTracing Authors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +from __future__ import absolute_import +import unittest +from opentracing import ScopeManager +from opentracing.harness.scope_check import ScopeCompatibilityCheckMixin + + +class VerifyScopeCompatibilityCheck(unittest.TestCase): + def test_scope_manager_exception(self): + scope_check = ScopeCompatibilityCheckMixin() + with self.assertRaises(NotImplementedError): + scope_check.scope_manager() + + def test_missing_active_works(self): + scope_check = ScopeCompatibilityCheckMixin() + setattr(scope_check, 'scope_manager', lambda: ScopeManager()) + + with self.assertRaises(AssertionError): + scope_check.test_missing_active() + + with self.assertRaises(AssertionError): + scope_check.test_missing_active_external() + + def test_activate_works(self): + scope_check = ScopeCompatibilityCheckMixin() + setattr(scope_check, 'scope_manager', lambda: ScopeManager()) + + with self.assertRaises(AssertionError): + scope_check.test_activate() + + with self.assertRaises(AssertionError): + scope_check.test_activate_external() + + with self.assertRaises(AssertionError): + scope_check.test_activate_finish_on_close() + + with self.assertRaises(AssertionError): + scope_check.test_activate_nested() + + with self.assertRaises(AssertionError): + scope_check.test_activate_finish_on_close_nested() + + def test_close_wrong_order(self): + scope_check = ScopeCompatibilityCheckMixin() + setattr(scope_check, 'scope_manager', lambda: ScopeManager()) + + # this test is expected to succeed. + scope_check.test_close_wrong_order() From d242faad6db75634f8d4a059192eefe8880a8fba Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Fri, 29 Jun 2018 15:44:37 +0200 Subject: [PATCH 14/15] Rename start_active_span() to start_active_scope(). --- README.rst | 10 ++--- opentracing/ext/scope_manager/asyncio.py | 4 +- opentracing/ext/scope_manager/gevent.py | 4 +- opentracing/ext/scope_manager/tornado.py | 4 +- opentracing/harness/api_check.py | 38 +++++++++---------- opentracing/mocktracer/tracer.py | 16 ++++---- opentracing/scope_manager.py | 2 +- opentracing/tracer.py | 20 +++++----- .../test_active_span_replacement/README.md | 4 +- .../test_asyncio.py | 4 +- .../test_gevent.py | 4 +- .../test_threads.py | 4 +- .../test_tornado.py | 4 +- testbed/test_client_server/README.md | 2 +- testbed/test_client_server/test_asyncio.py | 4 +- testbed/test_client_server/test_gevent.py | 4 +- testbed/test_client_server/test_threads.py | 4 +- testbed/test_client_server/test_tornado.py | 4 +- .../test_asyncio.py | 4 +- .../test_gevent.py | 4 +- .../test_threads.py | 4 +- .../test_tornado.py | 4 +- testbed/test_late_span_finish/README.md | 2 +- testbed/test_late_span_finish/test_asyncio.py | 2 +- testbed/test_late_span_finish/test_gevent.py | 2 +- testbed/test_late_span_finish/test_threads.py | 2 +- testbed/test_late_span_finish/test_tornado.py | 2 +- testbed/test_multiple_callbacks/README.md | 6 +-- .../test_multiple_callbacks/test_asyncio.py | 4 +- .../test_multiple_callbacks/test_gevent.py | 4 +- .../test_multiple_callbacks/test_threads.py | 4 +- .../test_multiple_callbacks/test_tornado.py | 2 +- testbed/test_nested_callbacks/test_asyncio.py | 2 +- testbed/test_nested_callbacks/test_gevent.py | 2 +- testbed/test_nested_callbacks/test_threads.py | 2 +- testbed/test_nested_callbacks/test_tornado.py | 2 +- .../test_subtask_span_propagation/README.md | 8 ++-- .../test_asyncio.py | 4 +- .../test_gevent.py | 4 +- .../test_threads.py | 4 +- .../test_tornado.py | 4 +- tests/test_api_check_mixin.py | 10 ++--- 42 files changed, 112 insertions(+), 112 deletions(-) diff --git a/README.rst b/README.rst index 480f148..a3ab49f 100644 --- a/README.rst +++ b/README.rst @@ -155,8 +155,8 @@ another task or thread, but not ``Scope``. The common case starts a ``Scope`` that's automatically registered for intra-process propagation via ``ScopeManager``. -Note that ``start_active_span('...')`` automatically finishes the span on ``Scope.close()`` -(``start_active_span('...', finish_on_close=False)`` does not finish it, in contrast). +Note that ``start_active_scope('...')`` automatically finishes the span on ``Scope.close()`` +(``start_active_scope('...', finish_on_close=False)`` does not finish it, in contrast). .. code-block:: python @@ -167,7 +167,7 @@ Note that ``start_active_span('...')`` automatically finishes the span on ``Scop # Automatic activation of the Span. # finish_on_close is a required parameter. - with tracer.start_active_span('someWork', finish_on_close=True) as scope: + with tracer.start_active_scope('someWork', finish_on_close=True) as scope: # Do things. # Handling done through a try construct: @@ -181,12 +181,12 @@ Note that ``start_active_span('...')`` automatically finishes the span on ``Scop 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()`` +the programmer passes ``ignore_active_span=True`` at ``start_span()``/``start_active_scope()`` time or specified parent context explicitly: .. code-block:: python - scope = tracer.start_active_span('someWork', ignore_active_span=True) + scope = tracer.start_active_scope('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 diff --git a/opentracing/ext/scope_manager/asyncio.py b/opentracing/ext/scope_manager/asyncio.py index c8c4b70..6d35d1f 100644 --- a/opentracing/ext/scope_manager/asyncio.py +++ b/opentracing/ext/scope_manager/asyncio.py @@ -44,11 +44,11 @@ async def child_coroutine(span): # activate the parent Span, but do not finish it upon # deactivation. That will be done by the parent coroutine. with tracer.scope_manager.activate(span, finish_on_close=False): - with tracer.start_active_span('child') as scope: + with tracer.start_active_scope('child') as scope: ... async def parent_coroutine(): - with tracer.start_active_span('parent') as scope: + with tracer.start_active_scope('parent') as scope: ... await child_coroutine(span) ... diff --git a/opentracing/ext/scope_manager/gevent.py b/opentracing/ext/scope_manager/gevent.py index 6a3c1d9..2070e99 100644 --- a/opentracing/ext/scope_manager/gevent.py +++ b/opentracing/ext/scope_manager/gevent.py @@ -42,11 +42,11 @@ def child_greenlet(span): # activate the parent Span, but do not finish it upon # deactivation. That will be done by the parent greenlet. with tracer.scope_manager.activate(span, finish_on_close=False): - with tracer.start_active_span('child') as scope: + with tracer.start_active_scope('child') as scope: ... def parent_greenlet(): - with tracer.start_active_span('parent') as scope: + with tracer.start_active_scope('parent') as scope: ... gevent.spawn(child_greenlet, span).join() ... diff --git a/opentracing/ext/scope_manager/tornado.py b/opentracing/ext/scope_manager/tornado.py index 958cc8f..6e7123c 100644 --- a/opentracing/ext/scope_manager/tornado.py +++ b/opentracing/ext/scope_manager/tornado.py @@ -47,12 +47,12 @@ class TornadoScopeManager(ThreadLocalScopeManager): def child_coroutine(): # No need to pass 'parent' and activate it here, # as it is automatically propagated. - with tracer.start_active_span('child') as scope: + with tracer.start_active_scope('child') as scope: ... @tornado.gen.coroutine def parent_coroutine(): - with tracer.start_active_span('parent') as scope: + with tracer.start_active_scope('parent') as scope: ... yield child_coroutine() ... diff --git a/opentracing/harness/api_check.py b/opentracing/harness/api_check.py index 67836f9..33acc16 100644 --- a/opentracing/harness/api_check.py +++ b/opentracing/harness/api_check.py @@ -67,56 +67,56 @@ def test_active_span(self): assert tracer.active_span is span assert tracer.scope_manager.active.span is span - def test_start_active_span(self): + def test_start_active_scope(self): # the first usage returns a `Scope` that wraps a root `Span` tracer = self.tracer() - scope = tracer.start_active_span('Fry') + scope = tracer.start_active_scope('Fry') 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): + def test_start_active_scope_parent(self): # ensure the `ScopeManager` provides the right parenting tracer = self.tracer() - with tracer.start_active_span('Fry') as parent: - with tracer.start_active_span('Farnsworth') as child: + with tracer.start_active_scope('Fry') as parent: + with tracer.start_active_scope('Farnsworth') as child: if self.check_scope_manager(): assert self.is_parent(parent.span, child.span) - def test_start_active_span_ignore_active_span(self): + def test_start_active_scope_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') as parent: - with tracer.start_active_span('Farnsworth', - ignore_active_span=True) as child: + with tracer.start_active_scope('Fry') as parent: + with tracer.start_active_scope('Farnsworth', + 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_not_finish_on_close(self): + def test_start_active_scope_not_finish_on_close(self): # ensure a `Span` is finished when the `Scope` close tracer = self.tracer() - scope = tracer.start_active_span('Fry', finish_on_close=False) + scope = tracer.start_active_scope('Fry', finish_on_close=False) with mock.patch.object(scope.span, 'finish') as finish: scope.close() assert finish.call_count == 0 - def test_start_active_span_finish_on_close(self): + def test_start_active_scope_finish_on_close(self): # a `Span` is not finished when the flag is set tracer = self.tracer() - scope = tracer.start_active_span('Fry', finish_on_close=True) + scope = tracer.start_active_scope('Fry', finish_on_close=True) with mock.patch.object(scope.span, 'finish') as finish: scope.close() if self.check_scope_manager(): assert finish.call_count == 1 - def test_start_active_span_default_finish_on_close(self): + def test_start_active_scope_default_finish_on_close(self): # a `Span` is finished when no flag is set tracer = self.tracer() - scope = tracer.start_active_span('Fry') + scope = tracer.start_active_scope('Fry') with mock.patch.object(scope.span, 'finish') as finish: scope.close() @@ -136,7 +136,7 @@ def test_start_span(self): def test_start_span_propagation(self): # `start_span` must inherit the current active `Scope` span tracer = self.tracer() - with tracer.start_active_span('Fry') as parent: + with tracer.start_active_scope('Fry') as parent: with tracer.start_span(operation_name='Farnsworth') as child: if self.check_scope_manager(): assert self.is_parent(parent.span, child) @@ -145,7 +145,7 @@ 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') as parent: + with tracer.start_active_scope('Fry') as parent: with tracer.start_span(operation_name='Farnsworth', ignore_active_span=True) as child: if self.check_scope_manager(): @@ -305,10 +305,10 @@ def test_unknown_format(self): with pytest.raises(opentracing.UnsupportedFormatException): span.tracer.extract(custom_format, {}) - def test_tracer_start_active_span_scope(self): + def test_tracer_start_active_scope_scope(self): # the Tracer ScopeManager should store the active Scope tracer = self.tracer() - scope = tracer.start_active_span('Fry') + scope = tracer.start_active_scope('Fry') if self.check_scope_manager(): assert tracer.scope_manager.active == scope diff --git a/opentracing/mocktracer/tracer.py b/opentracing/mocktracer/tracer.py index 7d63ef6..7013472 100644 --- a/opentracing/mocktracer/tracer.py +++ b/opentracing/mocktracer/tracer.py @@ -109,14 +109,14 @@ def _generate_id(self): self._next_id += 1 return self._next_id - def start_active_span(self, - operation_name, - child_of=None, - references=None, - tags=None, - start_time=None, - ignore_active_span=False, - finish_on_close=True): + def start_active_scope(self, + operation_name, + child_of=None, + references=None, + tags=None, + start_time=None, + ignore_active_span=False, + finish_on_close=True): # create a new Span span = self.start_span( diff --git a/opentracing/scope_manager.py b/opentracing/scope_manager.py index eb6bb6c..58ce73b 100644 --- a/opentracing/scope_manager.py +++ b/opentracing/scope_manager.py @@ -56,7 +56,7 @@ def active(self): If there is a non-null :class:`Scope`, its wrapped :class:`Span` becomes an implicit parent of any newly-created :class:`Span` at - :meth:`Tracer.start_active_span()` time. + :meth:`Tracer.start_active_scope()` time. :rtype: Scope :return: the :class:`Scope` that is active, or ``None`` if not diff --git a/opentracing/tracer.py b/opentracing/tracer.py index ded9649..884775e 100644 --- a/opentracing/tracer.py +++ b/opentracing/tracer.py @@ -59,20 +59,20 @@ def active_span(self): scope = self._scope_manager.active return None if scope is None else scope.span - def start_active_span(self, - operation_name, - child_of=None, - references=None, - tags=None, - start_time=None, - ignore_active_span=False, - finish_on_close=True): + def start_active_scope(self, + operation_name, + child_of=None, + references=None, + tags=None, + start_time=None, + ignore_active_span=False, + finish_on_close=True): """Returns a newly started and activated :class:`Scope`. The returned :class:`Scope` supports with-statement contexts. For example:: - with tracer.start_active_span('...') as scope: + with tracer.start_active_scope('...') as scope: scope.span.set_tag('http.method', 'GET') do_some_work() # Span.finish() is called as part of scope deactivation through @@ -81,7 +81,7 @@ def start_active_span(self, It's also possible to not finish the :class:`Span` when the :class:`Scope` context expires:: - with tracer.start_active_span('...', + with tracer.start_active_scope('...', finish_on_close=False) as scope: scope.span.set_tag('http.method', 'GET') do_some_work() diff --git a/testbed/test_active_span_replacement/README.md b/testbed/test_active_span_replacement/README.md index 96e8da4..cc116ab 100644 --- a/testbed/test_active_span_replacement/README.md +++ b/testbed/test_active_span_replacement/README.md @@ -5,14 +5,14 @@ This example shows a `Span` being created and then passed to an asynchronous tas `threading` implementation: ```python # Create a new Span for this task -with self.tracer.start_active_span('task'): +with self.tracer.start_active_scope('task'): with self.tracer.scope_manager.activate(span, True): # Simulate work strictly related to the initial Span pass # Use the task span as parent of a new subtask - with self.tracer.start_active_span('subtask'): + with self.tracer.start_active_scope('subtask'): pass ``` diff --git a/testbed/test_active_span_replacement/test_asyncio.py b/testbed/test_active_span_replacement/test_asyncio.py index e1356fb..ad260a4 100644 --- a/testbed/test_active_span_replacement/test_asyncio.py +++ b/testbed/test_active_span_replacement/test_asyncio.py @@ -38,14 +38,14 @@ def test_main(self): async def task(self, span): # Create a new Span for this task - with self.tracer.start_active_span('task'): + with self.tracer.start_active_scope('task'): with self.tracer.scope_manager.activate(span, True): # Simulate work strictly related to the initial Span pass # Use the task span as parent of a new subtask - with self.tracer.start_active_span('subtask'): + with self.tracer.start_active_scope('subtask'): pass def submit_another_task(self, span): diff --git a/testbed/test_active_span_replacement/test_gevent.py b/testbed/test_active_span_replacement/test_gevent.py index dbb443a..c758a67 100644 --- a/testbed/test_active_span_replacement/test_gevent.py +++ b/testbed/test_active_span_replacement/test_gevent.py @@ -34,14 +34,14 @@ def test_main(self): def task(self, span): # Create a new Span for this task - with self.tracer.start_active_span('task'): + with self.tracer.start_active_scope('task'): with self.tracer.scope_manager.activate(span, True): # Simulate work strictly related to the initial Span pass # Use the task span as parent of a new subtask - with self.tracer.start_active_span('subtask'): + with self.tracer.start_active_scope('subtask'): pass def submit_another_task(self, span): diff --git a/testbed/test_active_span_replacement/test_threads.py b/testbed/test_active_span_replacement/test_threads.py index f5a1a81..7db2731 100644 --- a/testbed/test_active_span_replacement/test_threads.py +++ b/testbed/test_active_span_replacement/test_threads.py @@ -34,14 +34,14 @@ def test_main(self): def task(self, span): # Create a new Span for this task - with self.tracer.start_active_span('task'): + with self.tracer.start_active_scope('task'): with self.tracer.scope_manager.activate(span, True): # Simulate work strictly related to the initial Span pass # Use the task span as parent of a new subtask - with self.tracer.start_active_span('subtask'): + with self.tracer.start_active_scope('subtask'): pass def submit_another_task(self, span): diff --git a/testbed/test_active_span_replacement/test_tornado.py b/testbed/test_active_span_replacement/test_tornado.py index b87d8fb..6a4d57f 100644 --- a/testbed/test_active_span_replacement/test_tornado.py +++ b/testbed/test_active_span_replacement/test_tornado.py @@ -41,14 +41,14 @@ def test_main(self): @gen.coroutine def task(self, span): # Create a new Span for this task - with self.tracer.start_active_span('task'): + with self.tracer.start_active_scope('task'): with self.tracer.scope_manager.activate(span, True): # Simulate work strictly related to the initial Span pass # Use the task span as parent of a new subtask - with self.tracer.start_active_span('subtask'): + with self.tracer.start_active_scope('subtask'): pass def submit_another_task(self, span): diff --git a/testbed/test_client_server/README.md b/testbed/test_client_server/README.md index 09470f2..61533ed 100644 --- a/testbed/test_client_server/README.md +++ b/testbed/test_client_server/README.md @@ -6,7 +6,7 @@ This example shows a `Span` created by a `Client`, which will send a `Message`/` ```python def send(self): - with self.tracer.start_active_span('send') as scope: + with self.tracer.start_active_scope('send') as scope: scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) message = {} diff --git a/testbed/test_client_server/test_asyncio.py b/testbed/test_client_server/test_asyncio.py index c19bfa0..bd9515f 100644 --- a/testbed/test_client_server/test_asyncio.py +++ b/testbed/test_client_server/test_asyncio.py @@ -31,7 +31,7 @@ def process(self, message): logger.info('Processing message in server') ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) - with self.tracer.start_active_span('receive', + with self.tracer.start_active_scope('receive', child_of=ctx) as scope: scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) @@ -42,7 +42,7 @@ def __init__(self, tracer, queue): self.queue = queue async def send(self): - with self.tracer.start_active_span('send') as scope: + with self.tracer.start_active_scope('send') as scope: scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) message = {} diff --git a/testbed/test_client_server/test_gevent.py b/testbed/test_client_server/test_gevent.py index 1123842..4ba24fd 100644 --- a/testbed/test_client_server/test_gevent.py +++ b/testbed/test_client_server/test_gevent.py @@ -32,7 +32,7 @@ def process(self, message): logger.info('Processing message in server') ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) - with self.tracer.start_active_span('receive', + with self.tracer.start_active_scope('receive', child_of=ctx) as scope: scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) @@ -43,7 +43,7 @@ def __init__(self, tracer, queue): self.queue = queue def send(self): - with self.tracer.start_active_span('send') as scope: + with self.tracer.start_active_scope('send') as scope: scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) message = {} diff --git a/testbed/test_client_server/test_threads.py b/testbed/test_client_server/test_threads.py index 16e2522..f8e8fbe 100644 --- a/testbed/test_client_server/test_threads.py +++ b/testbed/test_client_server/test_threads.py @@ -31,7 +31,7 @@ def process(self, message): logger.info('Processing message in server') ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) - with self.tracer.start_active_span('receive', + with self.tracer.start_active_scope('receive', child_of=ctx) as scope: scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) @@ -42,7 +42,7 @@ def __init__(self, tracer, queue): self.queue = queue def send(self): - with self.tracer.start_active_span('send') as scope: + with self.tracer.start_active_scope('send') as scope: scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) message = {} diff --git a/testbed/test_client_server/test_tornado.py b/testbed/test_client_server/test_tornado.py index a1e1075..1642b92 100644 --- a/testbed/test_client_server/test_tornado.py +++ b/testbed/test_client_server/test_tornado.py @@ -33,7 +33,7 @@ def process(self, message): logger.info('Processing message in server') ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) - with self.tracer.start_active_span('receive', + with self.tracer.start_active_scope('receive', child_of=ctx) as scope: scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) @@ -45,7 +45,7 @@ def __init__(self, tracer, queue): @gen.coroutine def send(self): - with self.tracer.start_active_span('send') as scope: + with self.tracer.start_active_scope('send') as scope: scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) message = {} diff --git a/testbed/test_common_request_handler/test_asyncio.py b/testbed/test_common_request_handler/test_asyncio.py index 1653d75..f456fa4 100644 --- a/testbed/test_common_request_handler/test_asyncio.py +++ b/testbed/test_common_request_handler/test_asyncio.py @@ -81,7 +81,7 @@ def test_parent_not_picked(self): """Active parent should not be picked up by child.""" async def do(): - with self.tracer.start_active_span('parent'): + with self.tracer.start_active_scope('parent'): response = await self.client.send_task('no_parent') self.assertEquals('no_parent::response', response) @@ -104,7 +104,7 @@ def test_bad_solution_to_set_parent(self): (we don't have better choice)""" async def do(): - with self.tracer.start_active_span('parent') as scope: + with self.tracer.start_active_scope('parent') as scope: req_handler = RequestHandler(self.tracer, scope.span.context) client = Client(req_handler, self.loop) response = await client.send_task('correct_parent') diff --git a/testbed/test_common_request_handler/test_gevent.py b/testbed/test_common_request_handler/test_gevent.py index 6603fe9..568ab98 100644 --- a/testbed/test_common_request_handler/test_gevent.py +++ b/testbed/test_common_request_handler/test_gevent.py @@ -74,7 +74,7 @@ def test_two_callbacks(self): def test_parent_not_picked(self): """Active parent should not be picked up by child.""" - with self.tracer.start_active_span('parent'): + with self.tracer.start_active_scope('parent'): response = self.client.send_sync('no_parent') self.assertEquals('no_parent::response', response) @@ -94,7 +94,7 @@ def test_bad_solution_to_set_parent(self): """Solution is bad because parent is per client (we don't have better choice)""" - with self.tracer.start_active_span('parent') as scope: + with self.tracer.start_active_scope('parent') as scope: client = Client(RequestHandler(self.tracer, scope.span.context)) response = client.send_sync('correct_parent') diff --git a/testbed/test_common_request_handler/test_threads.py b/testbed/test_common_request_handler/test_threads.py index 4135698..fdd9b69 100644 --- a/testbed/test_common_request_handler/test_threads.py +++ b/testbed/test_common_request_handler/test_threads.py @@ -73,7 +73,7 @@ def test_two_callbacks(self): def test_parent_not_picked(self): """Active parent should not be picked up by child.""" - with self.tracer.start_active_span('parent'): + with self.tracer.start_active_scope('parent'): response = self.client.send_sync('no_parent') self.assertEquals('no_parent::response', response) @@ -93,7 +93,7 @@ def test_bad_solution_to_set_parent(self): """Solution is bad because parent is per client (we don't have better choice)""" - with self.tracer.start_active_span('parent') as scope: + with self.tracer.start_active_scope('parent') as scope: client = Client(RequestHandler(self.tracer, scope.span.context), self.executor) response = client.send_sync('correct_parent') diff --git a/testbed/test_common_request_handler/test_tornado.py b/testbed/test_common_request_handler/test_tornado.py index a56560e..e3b7e66 100644 --- a/testbed/test_common_request_handler/test_tornado.py +++ b/testbed/test_common_request_handler/test_tornado.py @@ -85,7 +85,7 @@ def test_parent_not_picked(self): as we pass ignore_active_span=True to the RequestHandler""" with tracer_stack_context(): - with self.tracer.start_active_span('parent'): + with self.tracer.start_active_scope('parent'): response = self.client.send_sync('no_parent') self.assertEquals('no_parent::response', response) @@ -106,7 +106,7 @@ def test_good_solution_to_set_parent(self): the context will be properly detected.""" with tracer_stack_context(): - with self.tracer.start_active_span('parent'): + with self.tracer.start_active_scope('parent'): req_handler = RequestHandler(self.tracer, ignore_active_span=False) client = Client(req_handler, self.loop) diff --git a/testbed/test_late_span_finish/README.md b/testbed/test_late_span_finish/README.md index 4250bd0..d74ea97 100644 --- a/testbed/test_late_span_finish/README.md +++ b/testbed/test_late_span_finish/README.md @@ -8,7 +8,7 @@ This example shows a `Span` for a top-level operation, with independent, unknown def submit_subtasks(self, parent_span): def task(name, interval): with self.tracer.scope_manager.activate(parent_span, False): - with self.tracer.start_active_span(name): + with self.tracer.start_active_scope(name): time.sleep(interval) self.executor.submit(task, 'task1', 0.1) diff --git a/testbed/test_late_span_finish/test_asyncio.py b/testbed/test_late_span_finish/test_asyncio.py index 2fa2017..44318f4 100644 --- a/testbed/test_late_span_finish/test_asyncio.py +++ b/testbed/test_late_span_finish/test_asyncio.py @@ -43,7 +43,7 @@ def submit_subtasks(self, parent_span): async def task(name): logger.info('Running %s' % name) with self.tracer.scope_manager.activate(parent_span, False): - with self.tracer.start_active_span(name): + with self.tracer.start_active_scope(name): asyncio.sleep(0.1) self.loop.create_task(task('task1')) diff --git a/testbed/test_late_span_finish/test_gevent.py b/testbed/test_late_span_finish/test_gevent.py index fa498a7..ce964c7 100644 --- a/testbed/test_late_span_finish/test_gevent.py +++ b/testbed/test_late_span_finish/test_gevent.py @@ -39,7 +39,7 @@ def test_main(self): def submit_subtasks(self, parent_span): def task(name): with self.tracer.scope_manager.activate(parent_span, False): - with self.tracer.start_active_span(name): + with self.tracer.start_active_scope(name): gevent.sleep(0.1) gevent.spawn(task, 'task1') diff --git a/testbed/test_late_span_finish/test_threads.py b/testbed/test_late_span_finish/test_threads.py index 4cd018b..acf7c15 100644 --- a/testbed/test_late_span_finish/test_threads.py +++ b/testbed/test_late_span_finish/test_threads.py @@ -37,7 +37,7 @@ def test_main(self): def submit_subtasks(self, parent_span): def task(name, interval): with self.tracer.scope_manager.activate(parent_span, False): - with self.tracer.start_active_span(name): + with self.tracer.start_active_scope(name): time.sleep(interval) self.executor.submit(task, 'task1', 0.1) diff --git a/testbed/test_late_span_finish/test_tornado.py b/testbed/test_late_span_finish/test_tornado.py index be775c6..5bdcaae 100644 --- a/testbed/test_late_span_finish/test_tornado.py +++ b/testbed/test_late_span_finish/test_tornado.py @@ -46,7 +46,7 @@ def submit_subtasks(self, parent_span): def task(name): logger.info('Running %s' % name) with self.tracer.scope_manager.activate(parent_span, False): - with self.tracer.start_active_span(name): + with self.tracer.start_active_scope(name): gen.sleep(0.1) self.loop.add_callback(task, 'task1') diff --git a/testbed/test_multiple_callbacks/README.md b/testbed/test_multiple_callbacks/README.md index b6870fc..dc82c68 100644 --- a/testbed/test_multiple_callbacks/README.md +++ b/testbed/test_multiple_callbacks/README.md @@ -16,7 +16,7 @@ Implementation details: try: scope = self.tracer.scope_manager.activate(parent_span, False) - with self.tracer.start_active_span('task'): + with self.tracer.start_active_scope('task'): time.sleep(interval) finally: scope.close() @@ -30,11 +30,11 @@ Implementation details: logger.info('Starting task') with self.tracer.scope_manager.activate(parent_span, False): - with self.tracer.start_active_span('task'): + with self.tracer.start_active_scope('task'): await asyncio.sleep(interval) # Invoke and yield over the corotuines. - with self.tracer.start_active_span('parent'): + with self.tracer.start_active_scope('parent'): tasks = self.submit_callbacks() await asyncio.gather(*tasks) ``` diff --git a/testbed/test_multiple_callbacks/test_asyncio.py b/testbed/test_multiple_callbacks/test_asyncio.py index 713ff03..5d5b929 100644 --- a/testbed/test_multiple_callbacks/test_asyncio.py +++ b/testbed/test_multiple_callbacks/test_asyncio.py @@ -23,7 +23,7 @@ def test_main(self): # Need to run within a Task, as the scope manager depends # on Task.current_task() async def main_task(): - with self.tracer.start_active_span('parent'): + with self.tracer.start_active_scope('parent'): tasks = self.submit_callbacks() await asyncio.gather(*tasks) @@ -45,7 +45,7 @@ async def task(self, interval, parent_span): logger.info('Starting task') with self.tracer.scope_manager.activate(parent_span, False): - with self.tracer.start_active_span('task'): + with self.tracer.start_active_scope('task'): await asyncio.sleep(interval) def submit_callbacks(self): diff --git a/testbed/test_multiple_callbacks/test_gevent.py b/testbed/test_multiple_callbacks/test_gevent.py index 0005aab..877f303 100644 --- a/testbed/test_multiple_callbacks/test_gevent.py +++ b/testbed/test_multiple_callbacks/test_gevent.py @@ -20,7 +20,7 @@ def setUp(self): def test_main(self): def main_task(): - with self.tracer.start_active_span('parent'): + with self.tracer.start_active_scope('parent'): tasks = self.submit_callbacks() gevent.joinall(tasks) @@ -39,7 +39,7 @@ def task(self, interval, parent_span): logger.info('Starting task') with self.tracer.scope_manager.activate(parent_span, False): - with self.tracer.start_active_span('task'): + with self.tracer.start_active_scope('task'): gevent.sleep(interval) def submit_callbacks(self): diff --git a/testbed/test_multiple_callbacks/test_threads.py b/testbed/test_multiple_callbacks/test_threads.py index af8e14c..6ae30bc 100644 --- a/testbed/test_multiple_callbacks/test_threads.py +++ b/testbed/test_multiple_callbacks/test_threads.py @@ -21,7 +21,7 @@ def setUp(self): def test_main(self): try: - scope = self.tracer.start_active_span('parent', + scope = self.tracer.start_active_scope('parent', finish_on_close=False) scope.span._ref_count = RefCount(1) self.submit_callbacks(scope.span) @@ -45,7 +45,7 @@ def task(self, interval, parent_span): try: scope = self.tracer.scope_manager.activate(parent_span, False) - with self.tracer.start_active_span('task'): + with self.tracer.start_active_scope('task'): time.sleep(interval) finally: scope.close() diff --git a/testbed/test_multiple_callbacks/test_tornado.py b/testbed/test_multiple_callbacks/test_tornado.py index af659dc..b5428c8 100644 --- a/testbed/test_multiple_callbacks/test_tornado.py +++ b/testbed/test_multiple_callbacks/test_tornado.py @@ -23,7 +23,7 @@ def setUp(self): def test_main(self): @gen.coroutine def main_task(): - with self.tracer.start_active_span('parent'): + with self.tracer.start_active_scope('parent'): tasks = self.submit_callbacks() yield tasks diff --git a/testbed/test_nested_callbacks/test_asyncio.py b/testbed/test_nested_callbacks/test_asyncio.py index 5d04603..6a5a7c8 100644 --- a/testbed/test_nested_callbacks/test_asyncio.py +++ b/testbed/test_nested_callbacks/test_asyncio.py @@ -18,7 +18,7 @@ def test_main(self): # Start a Span and let the callback-chain # finish it when the task is done async def task(): - with self.tracer.start_active_span('one', finish_on_close=False): + with self.tracer.start_active_scope('one', finish_on_close=False): self.submit() self.loop.create_task(task()) diff --git a/testbed/test_nested_callbacks/test_gevent.py b/testbed/test_nested_callbacks/test_gevent.py index 9470de8..bee8052 100644 --- a/testbed/test_nested_callbacks/test_gevent.py +++ b/testbed/test_nested_callbacks/test_gevent.py @@ -15,7 +15,7 @@ def setUp(self): def test_main(self): # Start a Span and let the callback-chain # finish it when the task is done - with self.tracer.start_active_span('one', finish_on_close=False): + with self.tracer.start_active_scope('one', finish_on_close=False): self.submit() gevent.wait() diff --git a/testbed/test_nested_callbacks/test_threads.py b/testbed/test_nested_callbacks/test_threads.py index 5473e8f..e78895b 100644 --- a/testbed/test_nested_callbacks/test_threads.py +++ b/testbed/test_nested_callbacks/test_threads.py @@ -18,7 +18,7 @@ def tearDown(self): def test_main(self): # Start a Span and let the callback-chain # finish it when the task is done - with self.tracer.start_active_span('one', finish_on_close=False): + with self.tracer.start_active_scope('one', finish_on_close=False): self.submit() # Cannot shutdown the executor and wait for the callbacks diff --git a/testbed/test_nested_callbacks/test_tornado.py b/testbed/test_nested_callbacks/test_tornado.py index 24af6ec..cf25c99 100644 --- a/testbed/test_nested_callbacks/test_tornado.py +++ b/testbed/test_nested_callbacks/test_tornado.py @@ -19,7 +19,7 @@ def test_main(self): # Start a Span and let the callback-chain # finish it when the task is done with tracer_stack_context(): - with self.tracer.start_active_span('one', finish_on_close=False): + with self.tracer.start_active_scope('one', finish_on_close=False): self.submit() stop_loop_when(self.loop, diff --git a/testbed/test_subtask_span_propagation/README.md b/testbed/test_subtask_span_propagation/README.md index 4abfb26..d837f9e 100644 --- a/testbed/test_subtask_span_propagation/README.md +++ b/testbed/test_subtask_span_propagation/README.md @@ -9,7 +9,7 @@ Implementation details: `threading` implementation: ```python def parent_task(self, message): - with self.tracer.start_active_span('parent') as scope: + with self.tracer.start_active_scope('parent') as scope: f = self.executor.submit(self.child_task, message, scope.span) res = f.result() @@ -17,14 +17,14 @@ Implementation details: def child_task(self, message, span): with self.tracer.scope_manager.activate(span, False): - with self.tracer.start_active_span('child'): + with self.tracer.start_active_scope('child'): return '%s::response' % message ``` `tornado` implementation: ```python def parent_task(self, message): - with self.tracer.start_active_span('parent'): + with self.tracer.start_active_scope('parent'): res = yield self.child_task(message) raise gen.Return(res) @@ -33,6 +33,6 @@ Implementation details: def child_task(self, message): # No need to pass/activate the parent Span, as # it stays in the context. - with self.tracer.start_active_span('child'): + with self.tracer.start_active_scope('child'): raise gen.Return('%s::response' % message) ``` diff --git a/testbed/test_subtask_span_propagation/test_asyncio.py b/testbed/test_subtask_span_propagation/test_asyncio.py index afae378..22e707e 100644 --- a/testbed/test_subtask_span_propagation/test_asyncio.py +++ b/testbed/test_subtask_span_propagation/test_asyncio.py @@ -24,12 +24,12 @@ def test_main(self): self.assertIsChildOf(spans[0], spans[1]) async def parent_task(self, message): # noqa - with self.tracer.start_active_span('parent') as scope: + with self.tracer.start_active_scope('parent') as scope: res = await self.child_task(message, scope.span) return res async def child_task(self, message, span): with self.tracer.scope_manager.activate(span, False): - with self.tracer.start_active_span('child'): + with self.tracer.start_active_scope('child'): return '%s::response' % message diff --git a/testbed/test_subtask_span_propagation/test_gevent.py b/testbed/test_subtask_span_propagation/test_gevent.py index 1a78405..05362d1 100644 --- a/testbed/test_subtask_span_propagation/test_gevent.py +++ b/testbed/test_subtask_span_propagation/test_gevent.py @@ -21,12 +21,12 @@ def test_main(self): self.assertIsChildOf(spans[0], spans[1]) def parent_task(self, message): - with self.tracer.start_active_span('parent') as scope: + with self.tracer.start_active_scope('parent') as scope: res = gevent.spawn(self.child_task, message, scope.span).get() return res def child_task(self, message, span): with self.tracer.scope_manager.activate(span, False): - with self.tracer.start_active_span('child'): + with self.tracer.start_active_scope('child'): return '%s::response' % message diff --git a/testbed/test_subtask_span_propagation/test_threads.py b/testbed/test_subtask_span_propagation/test_threads.py index b504922..3348a04 100644 --- a/testbed/test_subtask_span_propagation/test_threads.py +++ b/testbed/test_subtask_span_propagation/test_threads.py @@ -21,7 +21,7 @@ def test_main(self): self.assertIsChildOf(spans[0], spans[1]) def parent_task(self, message): - with self.tracer.start_active_span('parent') as scope: + with self.tracer.start_active_scope('parent') as scope: f = self.executor.submit(self.child_task, message, scope.span) res = f.result() @@ -29,5 +29,5 @@ def parent_task(self, message): def child_task(self, message, span): with self.tracer.scope_manager.activate(span, False): - with self.tracer.start_active_span('child'): + with self.tracer.start_active_scope('child'): return '%s::response' % message diff --git a/testbed/test_subtask_span_propagation/test_tornado.py b/testbed/test_subtask_span_propagation/test_tornado.py index 9023722..4af32b2 100644 --- a/testbed/test_subtask_span_propagation/test_tornado.py +++ b/testbed/test_subtask_span_propagation/test_tornado.py @@ -28,7 +28,7 @@ def test_main(self): @gen.coroutine def parent_task(self, message): - with self.tracer.start_active_span('parent'): + with self.tracer.start_active_scope('parent'): res = yield self.child_task(message) raise gen.Return(res) @@ -37,5 +37,5 @@ def parent_task(self, message): def child_task(self, message): # No need to pass/activate the parent Span, as # it stays in the context. - with self.tracer.start_active_span('child'): + with self.tracer.start_active_scope('child'): raise gen.Return('%s::response' % message) diff --git a/tests/test_api_check_mixin.py b/tests/test_api_check_mixin.py index dbc659c..bc2c1da 100644 --- a/tests/test_api_check_mixin.py +++ b/tests/test_api_check_mixin.py @@ -50,26 +50,26 @@ def test_scope_manager_check_works(self): setattr(api_check, 'tracer', lambda: Tracer()) # these tests are expected to succeed - api_check.test_start_active_span_ignore_active_span() + api_check.test_start_active_scope_ignore_active_span() api_check.test_start_span_propagation_ignore_active_span() # no-op tracer has a no-op ScopeManager implementation, # which means no *actual* propagation is done, # so these tests are expected to work, but asserts to fail with self.assertRaises(AssertionError): - api_check.test_start_active_span() + api_check.test_start_active_scope() with self.assertRaises(AssertionError): - api_check.test_start_active_span_parent() + api_check.test_start_active_scope_parent() with self.assertRaises(AssertionError): api_check.test_start_span_propagation() with self.assertRaises(AssertionError): - api_check.test_tracer_start_active_span_scope() + api_check.test_tracer_start_active_scope_scope() with self.assertRaises(AssertionError): api_check.test_tracer_start_span_scope() with self.assertRaises(AssertionError): - api_check.test_start_active_span_finish_on_close() + api_check.test_start_active_scope_finish_on_close() From 15eb7a7e931847e21419d8359500b663ff69f36d Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Mon, 2 Jul 2018 16:04:54 +0200 Subject: [PATCH 15/15] Rename opentracing.ext.scope_manager to opentracing.ext.scope_managers (#90) Rename opentracing.ext.scope_manager to opentracing.scope_managers --- README.rst | 10 +++++----- docs/api.rst | 12 ++++++------ opentracing/mocktracer/tracer.py | 2 +- .../scope_manager => scope_managers}/__init__.py | 0 .../{ext/scope_manager => scope_managers}/asyncio.py | 2 +- .../scope_manager => scope_managers}/constants.py | 0 .../{ext/scope_manager => scope_managers}/gevent.py | 0 .../{ext/scope_manager => scope_managers}/tornado.py | 4 ++-- testbed/README.md | 2 +- testbed/test_active_span_replacement/test_asyncio.py | 2 +- testbed/test_active_span_replacement/test_gevent.py | 2 +- testbed/test_active_span_replacement/test_tornado.py | 2 +- testbed/test_client_server/test_asyncio.py | 2 +- testbed/test_client_server/test_gevent.py | 2 +- testbed/test_client_server/test_tornado.py | 2 +- testbed/test_common_request_handler/test_asyncio.py | 2 +- testbed/test_common_request_handler/test_gevent.py | 2 +- testbed/test_common_request_handler/test_tornado.py | 2 +- testbed/test_late_span_finish/test_asyncio.py | 2 +- testbed/test_late_span_finish/test_gevent.py | 2 +- testbed/test_late_span_finish/test_tornado.py | 2 +- testbed/test_listener_per_request/test_asyncio.py | 2 +- testbed/test_listener_per_request/test_gevent.py | 2 +- testbed/test_listener_per_request/test_tornado.py | 2 +- testbed/test_multiple_callbacks/test_asyncio.py | 2 +- testbed/test_multiple_callbacks/test_gevent.py | 2 +- testbed/test_multiple_callbacks/test_tornado.py | 2 +- testbed/test_nested_callbacks/test_asyncio.py | 2 +- testbed/test_nested_callbacks/test_gevent.py | 2 +- testbed/test_nested_callbacks/test_tornado.py | 2 +- .../test_subtask_span_propagation/test_asyncio.py | 2 +- testbed/test_subtask_span_propagation/test_gevent.py | 2 +- .../test_subtask_span_propagation/test_tornado.py | 2 +- tests/conftest.py | 2 +- .../scope_manager => scope_managers}/__init__.py | 0 .../scope_manager => scope_managers}/test_asyncio.py | 2 +- .../scope_manager => scope_managers}/test_gevent.py | 2 +- .../test_threadlocal.py | 2 +- .../scope_manager => scope_managers}/test_tornado.py | 4 ++-- 39 files changed, 46 insertions(+), 46 deletions(-) rename opentracing/{ext/scope_manager => scope_managers}/__init__.py (100%) rename opentracing/{ext/scope_manager => scope_managers}/asyncio.py (98%) rename opentracing/{ext/scope_manager => scope_managers}/constants.py (100%) rename opentracing/{ext/scope_manager => scope_managers}/gevent.py (100%) rename opentracing/{ext/scope_manager => scope_managers}/tornado.py (98%) rename tests/{ext/scope_manager => scope_managers}/__init__.py (100%) rename tests/{ext/scope_manager => scope_managers}/test_asyncio.py (96%) rename tests/{ext/scope_manager => scope_managers}/test_gevent.py (95%) rename tests/{ext/scope_manager => scope_managers}/test_threadlocal.py (95%) rename tests/{ext/scope_manager => scope_managers}/test_tornado.py (91%) diff --git a/README.rst b/README.rst index 480f148..1847af6 100644 --- a/README.rst +++ b/README.rst @@ -195,19 +195,19 @@ for asynchronous frameworks, for example). Scope managers ^^^^^^^^^^^^^^ -This project includes a set of ``ScopeManager`` implementations under the ``opentracing.ext.scope_manager`` submodule, which can be imported on demand: +This project includes a set of ``ScopeManager`` implementations under the ``opentracing.scope_managers`` submodule, which can be imported on demand: .. code-block:: python - from opentracing.ext.scope_manager import ThreadLocalScopeManager + from opentracing.scope_managers import ThreadLocalScopeManager There exist implementations for ``thread-local`` (the default), ``gevent``, ``Tornado`` and ``asyncio``: .. code-block:: python - from opentracing.ext.scope_manager.gevent import GeventScopeManager # requires gevent - from opentracing.ext.scope_manager.tornado import TornadoScopeManager # requires Tornado - from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager # requires Python 3.4 or newer. + from opentracing.scope_managers.gevent import GeventScopeManager # requires gevent + from opentracing.scope_managers.tornado import TornadoScopeManager # requires Tornado + from opentracing.scope_managers.asyncio import AsyncioScopeManager # requires Python 3.4 or newer. Development ----------- diff --git a/docs/api.rst b/docs/api.rst index a09e18f..f60b923 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -53,16 +53,16 @@ MockTracer Scope managers -------------- -.. autoclass:: opentracing.ext.scope_manager.ThreadLocalScopeManager +.. autoclass:: opentracing.scope_managers.ThreadLocalScopeManager :members: -.. autoclass:: opentracing.ext.scope_manager.gevent.GeventScopeManager +.. autoclass:: opentracing.scope_managers.gevent.GeventScopeManager :members: -.. autoclass:: opentracing.ext.scope_manager.tornado.TornadoScopeManager +.. autoclass:: opentracing.scope_managers.tornado.TornadoScopeManager :members: -.. autofunction:: opentracing.ext.scope_manager.tornado.tracer_stack_context +.. autofunction:: opentracing.scope_managers.tornado.tracer_stack_context -.. autoclass:: opentracing.ext.scope_manager.asyncio.AsyncioScopeManager - :members: \ No newline at end of file +.. autoclass:: opentracing.scope_managers.asyncio.AsyncioScopeManager + :members: diff --git a/opentracing/mocktracer/tracer.py b/opentracing/mocktracer/tracer.py index 7d63ef6..bea6545 100644 --- a/opentracing/mocktracer/tracer.py +++ b/opentracing/mocktracer/tracer.py @@ -24,7 +24,7 @@ import opentracing from opentracing import Format, Tracer from opentracing import UnsupportedFormatException -from opentracing.ext.scope_manager import ThreadLocalScopeManager +from opentracing.scope_managers import ThreadLocalScopeManager from .context import SpanContext from .span import MockSpan diff --git a/opentracing/ext/scope_manager/__init__.py b/opentracing/scope_managers/__init__.py similarity index 100% rename from opentracing/ext/scope_manager/__init__.py rename to opentracing/scope_managers/__init__.py diff --git a/opentracing/ext/scope_manager/asyncio.py b/opentracing/scope_managers/asyncio.py similarity index 98% rename from opentracing/ext/scope_manager/asyncio.py rename to opentracing/scope_managers/asyncio.py index c8c4b70..c5ffb49 100644 --- a/opentracing/ext/scope_manager/asyncio.py +++ b/opentracing/scope_managers/asyncio.py @@ -23,7 +23,7 @@ import asyncio from opentracing import Scope -from opentracing.ext.scope_manager import ThreadLocalScopeManager +from opentracing.scope_managers import ThreadLocalScopeManager from .constants import ACTIVE_ATTR diff --git a/opentracing/ext/scope_manager/constants.py b/opentracing/scope_managers/constants.py similarity index 100% rename from opentracing/ext/scope_manager/constants.py rename to opentracing/scope_managers/constants.py diff --git a/opentracing/ext/scope_manager/gevent.py b/opentracing/scope_managers/gevent.py similarity index 100% rename from opentracing/ext/scope_manager/gevent.py rename to opentracing/scope_managers/gevent.py diff --git a/opentracing/ext/scope_manager/tornado.py b/opentracing/scope_managers/tornado.py similarity index 98% rename from opentracing/ext/scope_manager/tornado.py rename to opentracing/scope_managers/tornado.py index 958cc8f..cd13c0e 100644 --- a/opentracing/ext/scope_manager/tornado.py +++ b/opentracing/scope_managers/tornado.py @@ -24,7 +24,7 @@ import tornado.stack_context from opentracing import Scope -from opentracing.ext.scope_manager import ThreadLocalScopeManager +from opentracing.scope_managers import ThreadLocalScopeManager # Implementation based on @@ -257,7 +257,7 @@ def tracer_stack_context(): .. code-block:: python - from opentracing.ext.scope_manager.tornado import tracer_stack_context + from opentracing.scope_managers.tornado import tracer_stack_context @tornado.gen.coroutine def handle_request_wrapper(request, actual_handler, *args, **kwargs) diff --git a/testbed/README.md b/testbed/README.md index 53c89e4..b4043c9 100644 --- a/testbed/README.md +++ b/testbed/README.md @@ -18,7 +18,7 @@ Alternatively, due to the organization of the suite, it's possible to run direct ## Tested frameworks -Currently the examples cover `threading`, `tornado`, `gevent` and `asyncio` (which requires Python 3). Each example uses their respective `ScopeManager` instance from `opentracing.ext.scope_manager`, along with their related requirements and limitations. +Currently the examples cover `threading`, `tornado`, `gevent` and `asyncio` (which requires Python 3). Each example uses their respective `ScopeManager` instance from `opentracing.scope_managers`, along with their related requirements and limitations. ### threading, asyncio and gevent diff --git a/testbed/test_active_span_replacement/test_asyncio.py b/testbed/test_active_span_replacement/test_asyncio.py index e1356fb..cf95b67 100644 --- a/testbed/test_active_span_replacement/test_asyncio.py +++ b/testbed/test_active_span_replacement/test_asyncio.py @@ -4,7 +4,7 @@ from opentracing.mocktracer import MockTracer from ..testcase import OpenTracingTestCase -from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager +from opentracing.scope_managers.asyncio import AsyncioScopeManager from ..utils import stop_loop_when diff --git a/testbed/test_active_span_replacement/test_gevent.py b/testbed/test_active_span_replacement/test_gevent.py index dbb443a..051ca6f 100644 --- a/testbed/test_active_span_replacement/test_gevent.py +++ b/testbed/test_active_span_replacement/test_gevent.py @@ -3,7 +3,7 @@ import gevent from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.gevent import GeventScopeManager +from opentracing.scope_managers.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase diff --git a/testbed/test_active_span_replacement/test_tornado.py b/testbed/test_active_span_replacement/test_tornado.py index b87d8fb..0586116 100644 --- a/testbed/test_active_span_replacement/test_tornado.py +++ b/testbed/test_active_span_replacement/test_tornado.py @@ -3,7 +3,7 @@ from tornado import gen, ioloop from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.tornado import TornadoScopeManager, \ +from opentracing.scope_managers.tornado import TornadoScopeManager, \ tracer_stack_context from ..testcase import OpenTracingTestCase from ..utils import stop_loop_when diff --git a/testbed/test_client_server/test_asyncio.py b/testbed/test_client_server/test_asyncio.py index c19bfa0..eba8ce4 100644 --- a/testbed/test_client_server/test_asyncio.py +++ b/testbed/test_client_server/test_asyncio.py @@ -6,7 +6,7 @@ import opentracing from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager +from opentracing.scope_managers.asyncio import AsyncioScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_logger, get_one_by_tag, stop_loop_when diff --git a/testbed/test_client_server/test_gevent.py b/testbed/test_client_server/test_gevent.py index 1123842..d634de9 100644 --- a/testbed/test_client_server/test_gevent.py +++ b/testbed/test_client_server/test_gevent.py @@ -7,7 +7,7 @@ import opentracing from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.gevent import GeventScopeManager +from opentracing.scope_managers.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_logger, get_one_by_tag diff --git a/testbed/test_client_server/test_tornado.py b/testbed/test_client_server/test_tornado.py index a1e1075..8118930 100644 --- a/testbed/test_client_server/test_tornado.py +++ b/testbed/test_client_server/test_tornado.py @@ -6,7 +6,7 @@ import opentracing from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.tornado import TornadoScopeManager, \ +from opentracing.scope_managers.tornado import TornadoScopeManager, \ tracer_stack_context from ..testcase import OpenTracingTestCase from ..utils import get_logger, get_one_by_tag, stop_loop_when diff --git a/testbed/test_common_request_handler/test_asyncio.py b/testbed/test_common_request_handler/test_asyncio.py index 1653d75..126c7d9 100644 --- a/testbed/test_common_request_handler/test_asyncio.py +++ b/testbed/test_common_request_handler/test_asyncio.py @@ -6,7 +6,7 @@ from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager +from opentracing.scope_managers.asyncio import AsyncioScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_logger, get_one_by_operation_name, stop_loop_when from .request_handler import RequestHandler diff --git a/testbed/test_common_request_handler/test_gevent.py b/testbed/test_common_request_handler/test_gevent.py index 6603fe9..975d2ca 100644 --- a/testbed/test_common_request_handler/test_gevent.py +++ b/testbed/test_common_request_handler/test_gevent.py @@ -4,7 +4,7 @@ from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.gevent import GeventScopeManager +from opentracing.scope_managers.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_logger, get_one_by_operation_name from .request_handler import RequestHandler diff --git a/testbed/test_common_request_handler/test_tornado.py b/testbed/test_common_request_handler/test_tornado.py index a56560e..3f0bfd6 100644 --- a/testbed/test_common_request_handler/test_tornado.py +++ b/testbed/test_common_request_handler/test_tornado.py @@ -6,7 +6,7 @@ from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.tornado import TornadoScopeManager, \ +from opentracing.scope_managers.tornado import TornadoScopeManager, \ tracer_stack_context from ..testcase import OpenTracingTestCase from ..utils import get_logger, get_one_by_operation_name, stop_loop_when diff --git a/testbed/test_late_span_finish/test_asyncio.py b/testbed/test_late_span_finish/test_asyncio.py index 2fa2017..782ade2 100644 --- a/testbed/test_late_span_finish/test_asyncio.py +++ b/testbed/test_late_span_finish/test_asyncio.py @@ -3,7 +3,7 @@ import asyncio from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager +from opentracing.scope_managers.asyncio import AsyncioScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_logger, stop_loop_when diff --git a/testbed/test_late_span_finish/test_gevent.py b/testbed/test_late_span_finish/test_gevent.py index fa498a7..93d72b4 100644 --- a/testbed/test_late_span_finish/test_gevent.py +++ b/testbed/test_late_span_finish/test_gevent.py @@ -3,7 +3,7 @@ import gevent from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.gevent import GeventScopeManager +from opentracing.scope_managers.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_logger diff --git a/testbed/test_late_span_finish/test_tornado.py b/testbed/test_late_span_finish/test_tornado.py index be775c6..a85d867 100644 --- a/testbed/test_late_span_finish/test_tornado.py +++ b/testbed/test_late_span_finish/test_tornado.py @@ -3,7 +3,7 @@ from tornado import gen, ioloop from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.tornado import TornadoScopeManager, \ +from opentracing.scope_managers.tornado import TornadoScopeManager, \ tracer_stack_context from ..testcase import OpenTracingTestCase from ..utils import get_logger, stop_loop_when diff --git a/testbed/test_listener_per_request/test_asyncio.py b/testbed/test_listener_per_request/test_asyncio.py index a7cec3a..b5731f9 100644 --- a/testbed/test_listener_per_request/test_asyncio.py +++ b/testbed/test_listener_per_request/test_asyncio.py @@ -4,7 +4,7 @@ from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager +from opentracing.scope_managers.asyncio import AsyncioScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_one_by_tag diff --git a/testbed/test_listener_per_request/test_gevent.py b/testbed/test_listener_per_request/test_gevent.py index 7dc6434..5300e38 100644 --- a/testbed/test_listener_per_request/test_gevent.py +++ b/testbed/test_listener_per_request/test_gevent.py @@ -4,7 +4,7 @@ from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.gevent import GeventScopeManager +from opentracing.scope_managers.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_one_by_tag diff --git a/testbed/test_listener_per_request/test_tornado.py b/testbed/test_listener_per_request/test_tornado.py index 7b30214..1a282bb 100644 --- a/testbed/test_listener_per_request/test_tornado.py +++ b/testbed/test_listener_per_request/test_tornado.py @@ -6,7 +6,7 @@ from opentracing.ext import tags from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.tornado import TornadoScopeManager +from opentracing.scope_managers.tornado import TornadoScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_one_by_tag diff --git a/testbed/test_multiple_callbacks/test_asyncio.py b/testbed/test_multiple_callbacks/test_asyncio.py index 713ff03..1da3146 100644 --- a/testbed/test_multiple_callbacks/test_asyncio.py +++ b/testbed/test_multiple_callbacks/test_asyncio.py @@ -5,7 +5,7 @@ import asyncio from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager +from opentracing.scope_managers.asyncio import AsyncioScopeManager from ..testcase import OpenTracingTestCase from ..utils import RefCount, get_logger, stop_loop_when diff --git a/testbed/test_multiple_callbacks/test_gevent.py b/testbed/test_multiple_callbacks/test_gevent.py index 0005aab..f680507 100644 --- a/testbed/test_multiple_callbacks/test_gevent.py +++ b/testbed/test_multiple_callbacks/test_gevent.py @@ -5,7 +5,7 @@ import gevent from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.gevent import GeventScopeManager +from opentracing.scope_managers.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase from ..utils import get_logger diff --git a/testbed/test_multiple_callbacks/test_tornado.py b/testbed/test_multiple_callbacks/test_tornado.py index af659dc..b4b7b7b 100644 --- a/testbed/test_multiple_callbacks/test_tornado.py +++ b/testbed/test_multiple_callbacks/test_tornado.py @@ -5,7 +5,7 @@ from tornado import gen, ioloop from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.tornado import TornadoScopeManager, \ +from opentracing.scope_managers.tornado import TornadoScopeManager, \ tracer_stack_context from ..testcase import OpenTracingTestCase from ..utils import get_logger, stop_loop_when diff --git a/testbed/test_nested_callbacks/test_asyncio.py b/testbed/test_nested_callbacks/test_asyncio.py index 5d04603..21ae115 100644 --- a/testbed/test_nested_callbacks/test_asyncio.py +++ b/testbed/test_nested_callbacks/test_asyncio.py @@ -4,7 +4,7 @@ import asyncio from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager +from opentracing.scope_managers.asyncio import AsyncioScopeManager from ..testcase import OpenTracingTestCase from ..utils import stop_loop_when diff --git a/testbed/test_nested_callbacks/test_gevent.py b/testbed/test_nested_callbacks/test_gevent.py index 9470de8..9bc9d35 100644 --- a/testbed/test_nested_callbacks/test_gevent.py +++ b/testbed/test_nested_callbacks/test_gevent.py @@ -4,7 +4,7 @@ import gevent from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.gevent import GeventScopeManager +from opentracing.scope_managers.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase diff --git a/testbed/test_nested_callbacks/test_tornado.py b/testbed/test_nested_callbacks/test_tornado.py index 24af6ec..5977972 100644 --- a/testbed/test_nested_callbacks/test_tornado.py +++ b/testbed/test_nested_callbacks/test_tornado.py @@ -4,7 +4,7 @@ from tornado import gen, ioloop from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.tornado import TornadoScopeManager, \ +from opentracing.scope_managers.tornado import TornadoScopeManager, \ tracer_stack_context from ..testcase import OpenTracingTestCase from ..utils import stop_loop_when diff --git a/testbed/test_subtask_span_propagation/test_asyncio.py b/testbed/test_subtask_span_propagation/test_asyncio.py index afae378..73b4ccd 100644 --- a/testbed/test_subtask_span_propagation/test_asyncio.py +++ b/testbed/test_subtask_span_propagation/test_asyncio.py @@ -5,7 +5,7 @@ import asyncio from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager +from opentracing.scope_managers.asyncio import AsyncioScopeManager from ..testcase import OpenTracingTestCase diff --git a/testbed/test_subtask_span_propagation/test_gevent.py b/testbed/test_subtask_span_propagation/test_gevent.py index 1a78405..d3619ee 100644 --- a/testbed/test_subtask_span_propagation/test_gevent.py +++ b/testbed/test_subtask_span_propagation/test_gevent.py @@ -3,7 +3,7 @@ import gevent from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.gevent import GeventScopeManager +from opentracing.scope_managers.gevent import GeventScopeManager from ..testcase import OpenTracingTestCase diff --git a/testbed/test_subtask_span_propagation/test_tornado.py b/testbed/test_subtask_span_propagation/test_tornado.py index 9023722..5d31579 100644 --- a/testbed/test_subtask_span_propagation/test_tornado.py +++ b/testbed/test_subtask_span_propagation/test_tornado.py @@ -5,7 +5,7 @@ from tornado import gen, ioloop from opentracing.mocktracer import MockTracer -from opentracing.ext.scope_manager.tornado import TornadoScopeManager, \ +from opentracing.scope_managers.tornado import TornadoScopeManager, \ tracer_stack_context from ..testcase import OpenTracingTestCase diff --git a/tests/conftest.py b/tests/conftest.py index 5dff313..6063d5a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,7 +22,7 @@ import six PYTHON3_FILES = [ - 'ext/scope_manager/test_asyncio.py', + 'scope_managers/test_asyncio.py', ] if six.PY2: diff --git a/tests/ext/scope_manager/__init__.py b/tests/scope_managers/__init__.py similarity index 100% rename from tests/ext/scope_manager/__init__.py rename to tests/scope_managers/__init__.py diff --git a/tests/ext/scope_manager/test_asyncio.py b/tests/scope_managers/test_asyncio.py similarity index 96% rename from tests/ext/scope_manager/test_asyncio.py rename to tests/scope_managers/test_asyncio.py index 6c01c46..14079c4 100644 --- a/tests/ext/scope_manager/test_asyncio.py +++ b/tests/scope_managers/test_asyncio.py @@ -25,7 +25,7 @@ import asyncio -from opentracing.ext.scope_manager.asyncio import AsyncioScopeManager +from opentracing.scope_managers.asyncio import AsyncioScopeManager from opentracing.harness.scope_check import ScopeCompatibilityCheckMixin diff --git a/tests/ext/scope_manager/test_gevent.py b/tests/scope_managers/test_gevent.py similarity index 95% rename from tests/ext/scope_manager/test_gevent.py rename to tests/scope_managers/test_gevent.py index 4a50d7e..5fb4b10 100644 --- a/tests/ext/scope_manager/test_gevent.py +++ b/tests/scope_managers/test_gevent.py @@ -24,7 +24,7 @@ import gevent -from opentracing.ext.scope_manager.gevent import GeventScopeManager +from opentracing.scope_managers.gevent import GeventScopeManager from opentracing.harness.scope_check import ScopeCompatibilityCheckMixin diff --git a/tests/ext/scope_manager/test_threadlocal.py b/tests/scope_managers/test_threadlocal.py similarity index 95% rename from tests/ext/scope_manager/test_threadlocal.py rename to tests/scope_managers/test_threadlocal.py index 7a73be6..003f8d2 100644 --- a/tests/ext/scope_manager/test_threadlocal.py +++ b/tests/scope_managers/test_threadlocal.py @@ -21,7 +21,7 @@ from __future__ import absolute_import from unittest import TestCase -from opentracing.ext.scope_manager import ThreadLocalScopeManager +from opentracing.scope_managers import ThreadLocalScopeManager from opentracing.harness.scope_check import ScopeCompatibilityCheckMixin diff --git a/tests/ext/scope_manager/test_tornado.py b/tests/scope_managers/test_tornado.py similarity index 91% rename from tests/ext/scope_manager/test_tornado.py rename to tests/scope_managers/test_tornado.py index 4ca94a9..99177f3 100644 --- a/tests/ext/scope_manager/test_tornado.py +++ b/tests/scope_managers/test_tornado.py @@ -24,8 +24,8 @@ from tornado import ioloop -from opentracing.ext.scope_manager.tornado import TornadoScopeManager -from opentracing.ext.scope_manager.tornado import tracer_stack_context +from opentracing.scope_managers.tornado import TornadoScopeManager +from opentracing.scope_managers.tornado import tracer_stack_context from opentracing.harness.scope_check import ScopeCompatibilityCheckMixin