From fd972847e523e2e755e8eca552c5381febb59e4b Mon Sep 17 00:00:00 2001 From: Emanuele Palazzetti Date: Mon, 13 Nov 2017 10:36:17 +0100 Subject: [PATCH 1/4] backward compatibility with newer OpenTracing API --- basictracer/tracer.py | 22 ++++++++++++++++++++-- requirements-test.txt | 3 +++ setup.py | 3 ++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/basictracer/tracer.py b/basictracer/tracer.py index 982721f..876c605 100644 --- a/basictracer/tracer.py +++ b/basictracer/tracer.py @@ -44,13 +44,14 @@ def register_required_propagators(self): self.register_propagator(Format.HTTP_HEADERS, TextPropagator()) self.register_propagator(Format.BINARY, BinaryPropagator()) - def start_span( + def start_manual( self, operation_name=None, child_of=None, references=None, tags=None, - start_time=None): + start_time=None, + ignore_active_span=False): start_time = time.time() if start_time is None else start_time @@ -84,6 +85,23 @@ def start_span( tags=tags, start_time=start_time) + def start_span( + self, + operation_name=None, + child_of=None, + references=None, + tags=None, + start_time=None): + """Deprecated: use `start_manual()` or `start_active()` instead.""" + return self.start_manual( + operation_name=operation_name, + child_of=child_of, + references=references, + tags=tags, + start_time=start_time, + ignore_active_span=True, + ) + def inject(self, span_context, format, carrier): if format in self._propagators: self._propagators[format].inject(span_context, carrier) diff --git a/requirements-test.txt b/requirements-test.txt index 5bf2305..a0315f3 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -3,3 +3,6 @@ -r requirements.txt -e .[tests] + +# TODO: using the latest proposal version; remove that after it has been merged upstream +-e git+https://github.com/palazzem/opentracing-python.git@palazzem/scope-manager#egg=opentracing-1.3.0 diff --git a/setup.py b/setup.py index adea027..306794c 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,8 @@ platforms='any', install_requires=[ 'protobuf>=3.0.0b2.post2', - 'opentracing>=1.2.1,<1.3', + # TODO: pin the right version after the proposal has been merged on master + # 'opentracing>=1.2.1,<1.3', 'six>=1.10.0,<2.0', ], extras_require={ From 14795c856392c4d035aec3f09140794fcc7c6f97 Mon Sep 17 00:00:00 2001 From: Emanuele Palazzetti Date: Mon, 13 Nov 2017 11:32:37 +0100 Subject: [PATCH 2/4] use flake8 in CI --- .flake8 | 3 +++ .travis.yml | 5 ++--- Makefile | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..c9f4ba2 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +exclude = + basictracer/wire_pb2.py diff --git a/.travis.yml b/.travis.yml index a280fba..4c9d369 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,9 @@ language: python -python: +python: - "2.7" install: - make bootstrap script: - - make test - + - make test lint diff --git a/Makefile b/Makefile index 0d7cec7..fe99c6d 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ clean-test: rm -fr htmlcov/ lint: - flake8 $(project) tests + flake8 --config=.flake8 $(project) tests test: $(pytest) $(test_args) From 698931c9e61fac0439f6719f6a0d407df35f27f0 Mon Sep 17 00:00:00 2001 From: Emanuele Palazzetti Date: Mon, 13 Nov 2017 21:17:38 +0100 Subject: [PATCH 3/4] provide ScopeManager implementation --- basictracer/scope.py | 58 +++++++++++++++++++++++++++++++++ basictracer/scope_manager.py | 63 ++++++++++++++++++++++++++++++++++++ basictracer/tracer.py | 37 +++++++++++++++++++-- tests/test_api.py | 7 ++++ tests/utils.py | 35 ++++++++++++++++++++ 5 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 basictracer/scope.py create mode 100644 basictracer/scope_manager.py create mode 100644 tests/utils.py diff --git a/basictracer/scope.py b/basictracer/scope.py new file mode 100644 index 0000000..395bf48 --- /dev/null +++ b/basictracer/scope.py @@ -0,0 +1,58 @@ +# 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 import Scope + + +class BasicScope(Scope): + """BasicScope is the implementation of `opentracing.Scope`""" + + def __init__(self, manager, span, finish_on_close=True): + """ + Initialize a `Scope` for the given `Span` object + + :param span: the `Span` used for this `Scope` + :param finish_on_close: whether span should automatically be + finished when `Scope#close()` is called + """ + self._manager = manager + self._span = span + self._finish_on_close = finish_on_close + self._to_restore = manager.active() + + def span(self): + """ + Return the `Span` that's been scoped by this `Scope`. + """ + return self._span + + def close(self): + """Finish the `Span` when the `Scope` context expires, unless + `finish_on_close` has been set + """ + 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/basictracer/scope_manager.py b/basictracer/scope_manager.py new file mode 100644 index 0000000..e874146 --- /dev/null +++ b/basictracer/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 + +import threading + +from opentracing import ScopeManager + +from .scope import BasicScope + + +class ThreadLocalScopeManager(ScopeManager): + """ScopeManager implementation that stores the current active `Scope` + in a thread-local storage + """ + def __init__(self): + self._tls_scope = threading.local() + + def activate(self, span, finish_on_close=True): + """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. By default, `Span` will + automatically be finished when `Scope#close()` is called. + """ + scope = BasicScope(self, span, finish_on_close=finish_on_close) + setattr(self._tls_scope, 'active', scope) + return scope + + 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_active()` + time. + + :return: the `Scope` that is active, or `None` if not available. + """ + return getattr(self._tls_scope, 'active', None) diff --git a/basictracer/tracer.py b/basictracer/tracer.py index 876c605..c242094 100644 --- a/basictracer/tracer.py +++ b/basictracer/tracer.py @@ -3,6 +3,7 @@ import opentracing from opentracing import Format, Tracer from opentracing import UnsupportedFormatException +from .scope_manager import ThreadLocalScopeManager from .context import SpanContext from .recorder import SpanRecorder, DefaultSampler from .span import BasicSpan @@ -11,7 +12,7 @@ class BasicTracer(Tracer): - def __init__(self, recorder=None, sampler=None): + def __init__(self, recorder=None, sampler=None, scope_manager=None): """Initialize a BasicTracer instance. Note that the returned BasicTracer has *no* propagators registered. The @@ -26,6 +27,8 @@ def __init__(self, recorder=None, sampler=None): super(BasicTracer, self).__init__() self.recorder = NoopRecorder() if recorder is None else recorder self.sampler = DefaultSampler(1) if sampler is None else sampler + self._scope_manager = ThreadLocalScopeManager() \ + if scope_manager is None else scope_manager self._propagators = {} def register_propagator(self, format, propagator): @@ -44,6 +47,28 @@ def register_required_propagators(self): self.register_propagator(Format.HTTP_HEADERS, TextPropagator()) self.register_propagator(Format.BINARY, BinaryPropagator()) + def start_active(self, + operation_name=None, + child_of=None, + references=None, + tags=None, + start_time=None, + ignore_active_scope=False, + finish_on_close=True): + + # create a new Span + span = self.start_manual( + operation_name=operation_name, + child_of=child_of, + references=references, + tags=tags, + start_time=start_time, + ignore_active_scope=ignore_active_scope, + ) + + return self.scope_manager.activate(span, + finish_on_close=finish_on_close) + def start_manual( self, operation_name=None, @@ -51,7 +76,7 @@ def start_manual( references=None, tags=None, start_time=None, - ignore_active_span=False): + ignore_active_scope=False): start_time = time.time() if start_time is None else start_time @@ -65,6 +90,12 @@ def start_manual( # TODO only the first reference is currently used parent_ctx = references[0].referenced_context + # retrieve the active SpanContext + if not ignore_active_scope 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=generate_id()) if parent_ctx is not None: @@ -99,7 +130,7 @@ def start_span( references=references, tags=tags, start_time=start_time, - ignore_active_span=True, + ignore_active_scope=True, ) def inject(self, span_context, format, carrier): diff --git a/tests/test_api.py b/tests/test_api.py index c24ec64..548f1db 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -31,3 +31,10 @@ def tracer(self): def check_baggage_values(self): return True + + def is_parent(self, parent, span): + # use `Span` ids to check parenting + if parent is None: + return span.parent_id is None + + return parent.context.span_id == span.parent_id diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..b8198fa --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,35 @@ +# 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 unittest import TestCase + +from basictracer import BasicTracer +from basictracer.recorder import InMemoryRecorder + + +class TracerTestCase(TestCase): + """Common TestCase to avoid duplication""" + + def setUp(self): + # initialize an in-memory tracer + self.recorder = InMemoryRecorder() + self.tracer = BasicTracer(recorder=self.recorder) From 0b4d3c397bc3d84d6232b9ad65ab52d534eeb87b Mon Sep 17 00:00:00 2001 From: Emanuele Palazzetti Date: Tue, 14 Nov 2017 12:18:34 +0100 Subject: [PATCH 4/4] updating docstrings and tests --- basictracer/scope.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/basictracer/scope.py b/basictracer/scope.py index 395bf48..0acba23 100644 --- a/basictracer/scope.py +++ b/basictracer/scope.py @@ -27,8 +27,7 @@ class BasicScope(Scope): """BasicScope is the implementation of `opentracing.Scope`""" def __init__(self, manager, span, finish_on_close=True): - """ - Initialize a `Scope` for the given `Span` object + """Initialize a `Scope` for the given `Span` object :param span: the `Span` used for this `Scope` :param finish_on_close: whether span should automatically be @@ -40,14 +39,12 @@ def __init__(self, manager, span, finish_on_close=True): self._to_restore = manager.active() def span(self): - """ - Return the `Span` that's been scoped by this `Scope`. - """ + """Return the `Span` that's been scoped by this `Scope`.""" return self._span def close(self): - """Finish the `Span` when the `Scope` context expires, unless - `finish_on_close` has been set + """Finish the `Span` when the `Scope` context ends, unless + `finish_on_close` has been set. """ if self._manager.active() is not self: return