From e4d89490e8705c7a0d5ea331db35c63d4cb2de44 Mon Sep 17 00:00:00 2001 From: Johannes Liebermann Date: Thu, 24 Oct 2019 16:46:23 +0200 Subject: [PATCH] OpenTracing Bridge - Initial implementation (#211) Initial implementation, without baggage support. --- .../README.rst | 19 + .../setup.cfg | 46 ++ .../setup.py | 26 ++ .../ext/opentracing_shim/__init__.py | 259 +++++++++++ .../ext/opentracing_shim/util.py | 54 +++ .../ext/opentracing_shim/version.py | 15 + .../tests/__init__.py | 0 .../tests/test_shim.py | 433 ++++++++++++++++++ .../tests/test_util.py | 66 +++ .../src/opentelemetry/trace/__init__.py | 10 +- .../src/opentelemetry/sdk/trace/__init__.py | 7 +- opentelemetry-sdk/tests/trace/test_trace.py | 5 +- tox.ini | 9 +- 13 files changed, 941 insertions(+), 8 deletions(-) create mode 100644 ext/opentelemetry-ext-opentracing-shim/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/setup.cfg create mode 100644 ext/opentelemetry-ext-opentracing-shim/setup.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/util.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/test_util.py diff --git a/ext/opentelemetry-ext-opentracing-shim/README.rst b/ext/opentelemetry-ext-opentracing-shim/README.rst new file mode 100644 index 0000000000..2e81391219 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/README.rst @@ -0,0 +1,19 @@ +OpenTracing Shim for OpenTelemetry +============================================================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-opentracing-shim.svg + :target: https://pypi.org/project/opentelemetry-opentracing-shim/ + +Installation +------------ + +:: + + pip install opentelemetry-opentracing-shim + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.cfg b/ext/opentelemetry-ext-opentracing-shim/setup.cfg new file mode 100644 index 0000000000..c3b750f80f --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/setup.cfg @@ -0,0 +1,46 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-opentracing-shim +description = OpenTracing Shim for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-opentracing-shim +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentracing + opentelemetry-api + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-opentracing-shim/setup.py b/ext/opentelemetry-ext-opentracing-shim/setup.py new file mode 100644 index 0000000000..bbec88b500 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/setup.py @@ -0,0 +1,26 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "opentracing_shim", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py new file mode 100644 index 0000000000..1a6479fc58 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -0,0 +1,259 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +import opentracing + +from opentelemetry.ext.opentracing_shim import util + +logger = logging.getLogger(__name__) + + +def create_tracer(otel_tracer): + return TracerShim(otel_tracer) + + +class SpanContextShim(opentracing.SpanContext): + def __init__(self, otel_context): + self._otel_context = otel_context + + def unwrap(self): + """Returns the wrapped OpenTelemetry `SpanContext` object.""" + + return self._otel_context + + @property + def baggage(self): + logger.warning( + "Using unimplemented property baggage on class %s.", + self.__class__.__name__, + ) + # TODO: Implement. + + +class SpanShim(opentracing.Span): + def __init__(self, tracer, context, span): + super().__init__(tracer, context) + self._otel_span = span + + def unwrap(self): + """Returns the wrapped OpenTelemetry `Span` object.""" + + return self._otel_span + + def set_operation_name(self, operation_name): + self._otel_span.update_name(operation_name) + return self + + def finish(self, finish_time=None): + end_time = finish_time + if end_time is not None: + end_time = util.time_seconds_to_ns(finish_time) + self._otel_span.end(end_time=end_time) + + def set_tag(self, key, value): + self._otel_span.set_attribute(key, value) + return self + + def log_kv(self, key_values, timestamp=None): + if timestamp is not None: + event_timestamp = util.time_seconds_to_ns(timestamp) + else: + event_timestamp = None + + event_name = util.event_name_from_kv(key_values) + self._otel_span.add_event(event_name, event_timestamp, key_values) + return self + + def set_baggage_item(self, key, value): + logger.warning( + "Calling unimplemented method set_baggage_item() on class %s", + self.__class__.__name__, + ) + # TODO: Implement. + + def get_baggage_item(self, key): + logger.warning( + "Calling unimplemented method get_baggage_item() on class %s", + self.__class__.__name__, + ) + # TODO: Implement. + + # TODO: Verify calls to deprecated methods `log_event()` and `log()` on + # base class work properly (it's probably fine because both methods call + # `log_kv()`). + + +class ScopeShim(opentracing.Scope): + """A `ScopeShim` wraps the OpenTelemetry functionality related to span + activation/deactivation while using OpenTracing `Scope` objects for + presentation. + + There are two ways to construct a `ScopeShim` object: using the default + initializer and using the `from_context_manager()` class method. + + It is necessary to have both ways for constructing `ScopeShim` objects + because in some cases we need to create the object from a context manager, + in which case our only way of retrieving a `Span` object is by calling the + `__enter__()` method on the context manager, which makes the span active in + the OpenTelemetry tracer; whereas in other cases we need to accept a + `SpanShim` object and wrap it in a `ScopeShim`. + """ + + def __init__(self, manager, span, span_cm=None): + super().__init__(manager, span) + self._span_cm = span_cm + + # TODO: Change type of `manager` argument to `opentracing.ScopeManager`? We + # need to get rid of `manager.tracer` for this. + @classmethod + def from_context_manager(cls, manager, span_cm): + """Constructs a `ScopeShim` from an OpenTelemetry `Span` context + manager (as returned by `Tracer.use_span()`). + """ + + otel_span = span_cm.__enter__() + span_context = SpanContextShim(otel_span.get_context()) + span = SpanShim(manager.tracer, span_context, otel_span) + return cls(manager, span, span_cm) + + def close(self): + if self._span_cm is not None: + # We don't have error information to pass to `__exit__()` so we + # pass `None` in all arguments. If the OpenTelemetry tracer + # implementation requires this information, the `__exit__()` method + # on `opentracing.Scope` should be overridden and modified to pass + # the relevant values to this `close()` method. + self._span_cm.__exit__(None, None, None) + else: + self._span.unwrap().end() + + +class ScopeManagerShim(opentracing.ScopeManager): + def __init__(self, tracer): + # The only thing the `__init__()` method on the base class does is + # initialize `self._noop_span` and `self._noop_scope` with no-op + # objects. Therefore, it doesn't seem useful to call it. + # pylint: disable=super-init-not-called + self._tracer = tracer + + def activate(self, span, finish_on_close): + span_cm = self._tracer.unwrap().use_span( + span.unwrap(), end_on_exit=finish_on_close + ) + return ScopeShim.from_context_manager(self, span_cm=span_cm) + + @property + def active(self): + span = self._tracer.unwrap().get_current_span() + if span is None: + return None + + span_context = SpanContextShim(span.get_context()) + wrapped_span = SpanShim(self._tracer, span_context, span) + return ScopeShim(self, span=wrapped_span) + # TODO: The returned `ScopeShim` instance here always ends the + # corresponding span, regardless of the `finish_on_close` value used + # when activating the span. This is because here we return a *new* + # `ScopeShim` rather than returning a saved instance of `ScopeShim`. + # https://github.com/open-telemetry/opentelemetry-python/pull/211/files#r335398792 + + @property + def tracer(self): + return self._tracer + + +class TracerShim(opentracing.Tracer): + def __init__(self, tracer): + super().__init__(scope_manager=ScopeManagerShim(self)) + self._otel_tracer = tracer + + def unwrap(self): + """Returns the wrapped OpenTelemetry `Tracer` object.""" + + return self._otel_tracer + + 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, + ): + 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, + references=None, + tags=None, + start_time=None, + ignore_active_span=False, + ): + # Use active span as parent when no explicit parent is specified. + if not ignore_active_span and not child_of: + child_of = self.active_span + + # Use the specified parent or the active span if possible. Otherwise, + # use a `None` parent, which triggers the creation of a new trace. + parent = child_of.unwrap() if child_of else None + span = self._otel_tracer.create_span(operation_name, parent) + + if references: + for ref in references: + span.add_link(ref.referenced_context.unwrap()) + + if tags: + for key, value in tags.items(): + span.set_attribute(key, value) + + # The OpenTracing API expects time values to be `float` values which + # represent the number of seconds since the epoch. OpenTelemetry + # represents time values as nanoseconds since the epoch. + start_time_ns = start_time + if start_time_ns is not None: + start_time_ns = util.time_seconds_to_ns(start_time) + + span.start(start_time=start_time_ns) + context = SpanContextShim(span.get_context()) + return SpanShim(self, context, span) + + def inject(self, span_context, format, carrier): + # pylint: disable=redefined-builtin + logger.warning( + "Calling unimplemented method inject() on class %s", + self.__class__.__name__, + ) + # TODO: Implement. + + def extract(self, format, carrier): + # pylint: disable=redefined-builtin + logger.warning( + "Calling unimplemented method extract() on class %s", + self.__class__.__name__, + ) + # TODO: Implement. diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/util.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/util.py new file mode 100644 index 0000000000..97e2415e44 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/util.py @@ -0,0 +1,54 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# A default event name to be used for logging events when a better event name +# can't be derived from the event's key-value pairs. +DEFAULT_EVENT_NAME = "log" + + +def time_seconds_to_ns(time_seconds): + """Converts a time value in seconds to a time value in nanoseconds. + + `time_seconds` is a `float` as returned by `time.time()` which represents + the number of seconds since the epoch. + + The returned value is an `int` representing the number of nanoseconds since + the epoch. + """ + + return int(time_seconds * 1e9) + + +def time_seconds_from_ns(time_nanoseconds): + """Converts a time value in nanoseconds to a time value in seconds. + + `time_nanoseconds` is an `int` representing the number of nanoseconds since + the epoch. + + The returned value is a `float` representing the number of seconds since + the epoch. + """ + + return time_nanoseconds / 1e9 + + +def event_name_from_kv(key_values): + """A helper function which returns an event name from the given dict, or a + default event name. + """ + + if key_values is None or "event" not in key_values: + return DEFAULT_EVENT_NAME + + return key_values["event"] diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py new file mode 100644 index 0000000000..a457c2b665 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.1.dev0" diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py new file mode 100644 index 0000000000..b6691911dd --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -0,0 +1,433 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +import unittest + +import opentracing + +import opentelemetry.ext.opentracing_shim as opentracingshim +from opentelemetry import trace +from opentelemetry.ext.opentracing_shim import util +from opentelemetry.sdk.trace import Tracer + + +class TestShim(unittest.TestCase): + # pylint: disable=too-many-public-methods + + def setUp(self): + """Create an OpenTelemetry tracer and a shim before every test case.""" + + self.tracer = trace.tracer() + self.shim = opentracingshim.create_tracer(self.tracer) + + @classmethod + def setUpClass(cls): + """Set preferred tracer implementation only once rather than before + every test method. + """ + + trace.set_preferred_tracer_implementation(lambda T: Tracer()) + + def test_shim_type(self): + # Verify shim is an OpenTracing tracer. + self.assertIsInstance(self.shim, opentracing.Tracer) + + def test_start_active_span(self): + """Test span creation and activation using `start_active_span()`.""" + + with self.shim.start_active_span("TestSpan") as scope: + # Verify correct type of Scope and Span objects. + self.assertIsInstance(scope, opentracing.Scope) + self.assertIsInstance(scope.span, opentracing.Span) + + # Verify span is started. + self.assertIsNotNone(scope.span.unwrap().start_time) + + # Verify span is active. + self.assertEqual( + self.shim.active_span.context.unwrap(), + scope.span.context.unwrap(), + ) + # TODO: We can't check for equality of self.shim.active_span and + # scope.span because the same OpenTelemetry span is returned inside + # different SpanShim objects. A possible solution is described + # here: + # https://github.com/open-telemetry/opentelemetry-python/issues/161#issuecomment-534136274 + + # Verify span has ended. + self.assertIsNotNone(scope.span.unwrap().end_time) + + # Verify no span is active. + self.assertIsNone(self.shim.active_span) + + def test_start_span(self): + """Test span creation using `start_span()`.""" + + with self.shim.start_span("TestSpan") as span: + # Verify correct type of Span object. + self.assertIsInstance(span, opentracing.Span) + + # Verify span is started. + self.assertIsNotNone(span.unwrap().start_time) + + # Verify `start_span()` does NOT make the span active. + self.assertIsNone(self.shim.active_span) + + # Verify span has ended. + self.assertIsNotNone(span.unwrap().end_time) + + def test_start_span_no_contextmanager(self): + """Test `start_span()` without a `with` statement.""" + + span = self.shim.start_span("TestSpan") + + # Verify span is started. + self.assertIsNotNone(span.unwrap().start_time) + + # Verify `start_span()` does NOT make the span active. + self.assertIsNone(self.shim.active_span) + + span.finish() + + def test_explicit_span_finish(self): + """Test `finish()` method on `Span` objects.""" + + span = self.shim.start_span("TestSpan") + + # Verify span hasn't ended. + self.assertIsNone(span.unwrap().end_time) + + span.finish() + + # Verify span has ended. + self.assertIsNotNone(span.unwrap().end_time) + + def test_explicit_start_time(self): + """Test `start_time` argument.""" + + now = time.time() + with self.shim.start_active_span("TestSpan", start_time=now) as scope: + result = util.time_seconds_from_ns(scope.span.unwrap().start_time) + # Tolerate inaccuracies of less than a microsecond. + # TODO: Put a link to an explanation in the docs. + # TODO: This seems to work consistently, but we should find out the + # biggest possible loss of precision. + self.assertAlmostEqual(result, now, places=6) + + def test_explicit_end_time(self): + """Test `end_time` argument of `finish()` method.""" + + span = self.shim.start_span("TestSpan") + now = time.time() + span.finish(now) + + end_time = util.time_seconds_from_ns(span.unwrap().end_time) + # Tolerate inaccuracies of less than a microsecond. + # TODO: Put a link to an explanation in the docs. + # TODO: This seems to work consistently, but we should find out the + # biggest possible loss of precision. + self.assertAlmostEqual(end_time, now, places=6) + + def test_explicit_span_activation(self): + """Test manual activation and deactivation of a span.""" + + span = self.shim.start_span("TestSpan") + + # Verify no span is currently active. + self.assertIsNone(self.shim.active_span) + + with self.shim.scope_manager.activate( + span, finish_on_close=True + ) as scope: + # Verify span is active. + self.assertEqual( + self.shim.active_span.context.unwrap(), + scope.span.context.unwrap(), + ) + + # Verify no span is active. + self.assertIsNone(self.shim.active_span) + + def test_start_active_span_finish_on_close(self): + """Test `finish_on_close` argument of `start_active_span()`.""" + + with self.shim.start_active_span( + "TestSpan", finish_on_close=True + ) as scope: + # Verify span hasn't ended. + self.assertIsNone(scope.span.unwrap().end_time) + + # Verify span has ended. + self.assertIsNotNone(scope.span.unwrap().end_time) + + with self.shim.start_active_span( + "TestSpan", finish_on_close=False + ) as scope: + # Verify span hasn't ended. + self.assertIsNone(scope.span.unwrap().end_time) + + # Verify span hasn't ended after scope had been closed. + self.assertIsNone(scope.span.unwrap().end_time) + + scope.span.finish() + + def test_activate_finish_on_close(self): + """Test `finish_on_close` argument of `activate()`.""" + + span = self.shim.start_span("TestSpan") + + with self.shim.scope_manager.activate( + span, finish_on_close=True + ) as scope: + # Verify span is active. + self.assertEqual( + self.shim.active_span.context.unwrap(), + scope.span.context.unwrap(), + ) + + # Verify span has ended. + self.assertIsNotNone(span.unwrap().end_time) + + span = self.shim.start_span("TestSpan") + + with self.shim.scope_manager.activate( + span, finish_on_close=False + ) as scope: + # Verify span is active. + self.assertEqual( + self.shim.active_span.context.unwrap(), + scope.span.context.unwrap(), + ) + + # Verify span hasn't ended. + self.assertIsNone(span.unwrap().end_time) + + span.finish() + + def test_explicit_scope_close(self): + """Test `close()` method on `ScopeShim`.""" + + with self.shim.start_active_span("ParentSpan") as parent: + # Verify parent span is active. + self.assertEqual( + self.shim.active_span.context.unwrap(), + parent.span.context.unwrap(), + ) + + child = self.shim.start_active_span("ChildSpan") + + # Verify child span is active. + self.assertEqual( + self.shim.active_span.context.unwrap(), + child.span.context.unwrap(), + ) + + # Verify child span hasn't ended. + self.assertIsNone(child.span.unwrap().end_time) + + child.close() + + # Verify child span has ended. + self.assertIsNotNone(child.span.unwrap().end_time) + + # Verify parent span becomes active again. + self.assertEqual( + self.shim.active_span.context.unwrap(), + parent.span.context.unwrap(), + ) + + def test_parent_child_implicit(self): + """Test parent-child relationship and activation/deactivation of spans + without specifying the parent span upon creation. + """ + + with self.shim.start_active_span("ParentSpan") as parent: + # Verify parent span is the active span. + self.assertEqual( + self.shim.active_span.context.unwrap(), + parent.span.context.unwrap(), + ) + + with self.shim.start_active_span("ChildSpan") as child: + # Verify child span is the active span. + self.assertEqual( + self.shim.active_span.context.unwrap(), + child.span.context.unwrap(), + ) + + # Verify parent-child relationship. + parent_trace_id = parent.span.unwrap().get_context().trace_id + child_trace_id = child.span.unwrap().get_context().trace_id + + self.assertEqual(parent_trace_id, child_trace_id) + self.assertEqual( + child.span.unwrap().parent, parent.span.unwrap() + ) + + # Verify parent span becomes the active span again. + self.assertEqual( + self.shim.active_span.context.unwrap(), + parent.span.context.unwrap() + # TODO: Check equality of the spans themselves rather than + # their context once the SpanShim reconstruction problem has + # been addressed (see previous TODO). + ) + + # Verify there is no active span. + self.assertIsNone(self.shim.active_span) + + def test_parent_child_explicit_span(self): + """Test parent-child relationship of spans when specifying a `Span` + object as a parent upon creation. + """ + + with self.shim.start_span("ParentSpan") as parent: + with self.shim.start_active_span( + "ChildSpan", child_of=parent + ) as child: + parent_trace_id = parent.unwrap().get_context().trace_id + child_trace_id = child.span.unwrap().get_context().trace_id + + self.assertEqual(child_trace_id, parent_trace_id) + self.assertEqual(child.span.unwrap().parent, parent.unwrap()) + + with self.shim.start_span("ParentSpan") as parent: + child = self.shim.start_span("ChildSpan", child_of=parent) + + parent_trace_id = parent.unwrap().get_context().trace_id + child_trace_id = child.unwrap().get_context().trace_id + + self.assertEqual(child_trace_id, parent_trace_id) + self.assertEqual(child.unwrap().parent, parent.unwrap()) + + child.finish() + + def test_parent_child_explicit_span_context(self): + """Test parent-child relationship of spans when specifying a + `SpanContext` object as a parent upon creation. + """ + + with self.shim.start_span("ParentSpan") as parent: + with self.shim.start_active_span( + "ChildSpan", child_of=parent.context + ) as child: + parent_trace_id = parent.unwrap().get_context().trace_id + child_trace_id = child.span.unwrap().get_context().trace_id + + self.assertEqual(child_trace_id, parent_trace_id) + self.assertEqual( + child.span.unwrap().parent, parent.context.unwrap() + ) + + with self.shim.start_span("ParentSpan") as parent: + with self.shim.start_span( + "SpanWithContextParent", child_of=parent.context + ) as child: + parent_trace_id = parent.unwrap().get_context().trace_id + child_trace_id = child.unwrap().get_context().trace_id + + self.assertEqual(child_trace_id, parent_trace_id) + self.assertEqual( + child.unwrap().parent, parent.context.unwrap() + ) + + def test_references(self): + """Test span creation using the `references` argument.""" + + with self.shim.start_span("ParentSpan") as parent: + ref = opentracing.child_of(parent.context) + + with self.shim.start_active_span( + "ChildSpan", references=[ref] + ) as child: + self.assertEqual( + child.span.unwrap().links[0].context, + parent.context.unwrap(), + ) + + def test_set_operation_name(self): + """Test `set_operation_name()` method.""" + + with self.shim.start_active_span("TestName") as scope: + self.assertEqual(scope.span.unwrap().name, "TestName") + + scope.span.set_operation_name("NewName") + self.assertEqual(scope.span.unwrap().name, "NewName") + + def test_tags(self): + """Test tags behavior using the `tags` argument and the `set_tags()` + method. + """ + + tags = {"foo": "bar"} + with self.shim.start_active_span("TestSetTag", tags=tags) as scope: + scope.span.set_tag("baz", "qux") + + self.assertEqual(scope.span.unwrap().attributes["foo"], "bar") + self.assertEqual(scope.span.unwrap().attributes["baz"], "qux") + + def test_span_tracer(self): + """Test the `tracer` property on `Span` objects.""" + + with self.shim.start_active_span("TestSpan") as scope: + self.assertEqual(scope.span.tracer, self.shim) + + def test_log_kv(self): + """Test the `log_kv()` method on `Span` objects.""" + + with self.shim.start_span("TestSpan") as span: + span.log_kv({"foo": "bar"}) + self.assertEqual(span.unwrap().events[0].attributes["foo"], "bar") + # Verify timestamp was generated automatically. + self.assertIsNotNone(span.unwrap().events[0].timestamp) + + # Test explicit timestamp. + now = time.time() + span.log_kv({"foo": "bar"}, now) + result = util.time_seconds_from_ns( + span.unwrap().events[1].timestamp + ) + self.assertEqual(span.unwrap().events[1].attributes["foo"], "bar") + # Tolerate inaccuracies of less than a microsecond. + # TODO: Put a link to an explanation in the docs. + # TODO: This seems to work consistently, but we should find out the + # biggest possible loss of precision. + self.assertAlmostEqual(result, now, places=6) + + def test_span_context(self): + """Test construction of `SpanContextShim` objects.""" + + otel_context = trace.SpanContext(1234, 5678) + context = opentracingshim.SpanContextShim(otel_context) + + self.assertIsInstance(context, opentracing.SpanContext) + self.assertEqual(context.unwrap().trace_id, 1234) + self.assertEqual(context.unwrap().span_id, 5678) + + def test_span_on_error(self): + """Verify error tag and logs are created on span when an exception is + raised. + """ + + # Raise an exception while a span is active. + with self.assertRaises(Exception): + with self.shim.start_active_span("TestName") as scope: + raise Exception + + # Verify exception details have been added to span. + self.assertEqual(scope.span.unwrap().attributes["error"], True) + self.assertEqual( + scope.span.unwrap().events[0].attributes["error.kind"], Exception + ) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_util.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_util.py new file mode 100644 index 0000000000..84bdc73a6a --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_util.py @@ -0,0 +1,66 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +import unittest + +from opentelemetry.ext.opentracing_shim import util +from opentelemetry.util import time_ns + + +class TestUtil(unittest.TestCase): + def test_event_name_from_kv(self): + # Test basic behavior. + event_name = "send HTTP request" + res = util.event_name_from_kv({"event": event_name, "foo": "bar"}) + self.assertEqual(res, event_name) + + # Test None. + res = util.event_name_from_kv(None) + self.assertEqual(res, util.DEFAULT_EVENT_NAME) + + # Test empty dict. + res = util.event_name_from_kv({}) + self.assertEqual(res, util.DEFAULT_EVENT_NAME) + + # Test missing `event` field. + res = util.event_name_from_kv({"foo": "bar"}) + self.assertEqual(res, util.DEFAULT_EVENT_NAME) + + def test_time_seconds_to_ns(self): + time_seconds = time.time() + result = util.time_seconds_to_ns(time_seconds) + + self.assertEqual(result, int(time_seconds * 1e9)) + + def test_time_seconds_from_ns(self): + time_nanoseconds = time_ns() + result = util.time_seconds_from_ns(time_nanoseconds) + + self.assertEqual(result, time_nanoseconds / 1e9) + + def test_time_conversion_precision(self): + """Verify time conversion from seconds to nanoseconds and vice versa is + accurate enough. + """ + + time_seconds = 1570484241.9501917 + time_nanoseconds = util.time_seconds_to_ns(time_seconds) + result = util.time_seconds_from_ns(time_nanoseconds) + + # Tolerate inaccuracies of less than a microsecond. + # TODO: Put a link to an explanation in the docs. + # TODO: This seems to work consistently, but we should find out the + # biggest possible loss of precision. + self.assertAlmostEqual(result, time_seconds, places=6) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index fff5d556b9..2a21212fae 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -180,12 +180,16 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: """ def add_event( - self, name: str, attributes: types.Attributes = None + self, + name: str, + timestamp: int = None, + attributes: types.Attributes = None, ) -> None: """Adds an `Event`. - Adds a single `Event` with the name and, optionally, attributes passed - as arguments. + Adds a single `Event` with the name and, optionally, a timestamp and + attributes passed as arguments. Implementations should generate a + timestamp if the `timestamp` argument is omitted. """ def add_lazy_event(self, event: Event) -> None: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 7b274f852f..f8a058d87f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -199,12 +199,15 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: self.attributes[key] = value def add_event( - self, name: str, attributes: types.Attributes = None + self, + name: str, + timestamp: int = None, + attributes: types.Attributes = None, ) -> None: self.add_lazy_event( trace_api.Event( name, - time_ns(), + time_ns() if timestamp is None else timestamp, Span.empty_attributes if attributes is None else attributes, ) ) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index fa8547a0a5..d1e3033d52 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -227,8 +227,10 @@ def test_span_members(self): # events root.add_event("event0") - root.add_event("event1", {"name": "birthday"}) now = time_ns() + root.add_event( + "event1", timestamp=now, attributes={"name": "birthday"} + ) root.add_lazy_event( trace_api.Event("event2", now, {"name": "hello"}) ) @@ -240,6 +242,7 @@ def test_span_members(self): self.assertEqual(root.events[1].name, "event1") self.assertEqual(root.events[1].attributes, {"name": "birthday"}) + self.assertEqual(root.events[1].timestamp, now) self.assertEqual(root.events[2].name, "event2") self.assertEqual(root.events[2].attributes, {"name": "hello"}) diff --git a/tox.ini b/tox.ini index 19816d3d9c..a6abe8e158 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,8 @@ skipsdist = True skip_missing_interpreters = True envlist = - py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger} - pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger} + py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,opentracing-shim} + pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,opentracing-shim} lint py37-{mypy,mypyinstalled} docs @@ -26,6 +26,7 @@ changedir = test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-example-app: examples/opentelemetry-example-app/tests + test-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests commands_pre = ; Install without -e to test the actual installation @@ -41,6 +42,7 @@ commands_pre = http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests jaeger: pip install {toxinidir}/opentelemetry-sdk jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger + opentracing-shim: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-opentracing-shim ; Using file:// here because otherwise tox invokes just "pip install ; opentelemetry-api", leading to an error @@ -74,6 +76,7 @@ commands_pre = pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi pip install -e {toxinidir}/examples/opentelemetry-example-app + pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim commands = ; Prefer putting everything in one pylint command to profit from duplication @@ -90,6 +93,8 @@ commands = ext/opentelemetry-ext-http-requests/tests/ \ ext/opentelemetry-ext-jaeger/src/opentelemetry \ ext/opentelemetry-ext-jaeger/tests/ \ + ext/opentelemetry-ext-opentracing-shim/src/ \ + ext/opentelemetry-ext-opentracing-shim/tests/ \ ext/opentelemetry-ext-wsgi/tests/ \ examples/opentelemetry-example-app/src/opentelemetry_example_app/ \ examples/opentelemetry-example-app/tests/