diff --git a/bridge/opentracing/setup.py b/bridge/opentracing/setup.py new file mode 100644 index 00000000000..ec8836115c5 --- /dev/null +++ b/bridge/opentracing/setup.py @@ -0,0 +1,61 @@ +# 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", "ot_otel_bridge", "version.py") +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup( + name="ot_otel_bridge", + version=PACKAGE_INFO["__version__"], + author="OpenTelemetry Authors", + author_email="cncf-opentelemetry-contributors@lists.cncf.io", + 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", + ], + description="OpenTracing OpenTelemetry Python bridge", + include_package_data=True, + long_description="OpenTracing OpenTelemetry Python bridge", + install_requires=[ + "opentelemetry-api==0.1.dev0", + "opentelemetry-sdk==0.1.dev0", + "typing; python_version<'3.5'", + "basictracer>=3.0,<4", + ], + extras_require={}, + license="Apache-2.0", + package_dir={"": "src"}, + packages=setuptools.find_namespace_packages( + where="src", include="ot_otel_bridge.*" + ), + url=( + "https://github.com/open-telemetry/opentelemetry-python" + "/tree/master/bridge/opentracing" + ), + zip_safe=False, +) diff --git a/bridge/opentracing/src/ot_otel_bridge/__init__.py b/bridge/opentracing/src/ot_otel_bridge/__init__.py new file mode 100644 index 00000000000..d853a7bcf65 --- /dev/null +++ b/bridge/opentracing/src/ot_otel_bridge/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/bridge/opentracing/src/ot_otel_bridge/span.py b/bridge/opentracing/src/ot_otel_bridge/span.py new file mode 100644 index 00000000000..5cbbf5a34f9 --- /dev/null +++ b/bridge/opentracing/src/ot_otel_bridge/span.py @@ -0,0 +1,55 @@ +# 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. + + +from basictracer.span import BasicSpan + +from opentelemetry import trace as trace_api +from opentelemetry.sdk.trace import Span as OtelSpan + + +class BridgeSpan(BasicSpan): + def __init__( + self, + tracer, + operation_name=None, + context=None, + parent_id=None, + tags=None, + start_time=None, + otel_parent=None, + ): + super(BridgeSpan, self).__init__( + tracer, operation_name, context, parent_id, tags, start_time + ) + + otel_context = trace_api.SpanContext(context.trace_id, context.span_id) + if otel_parent is None: + otel_parent = trace_api.SpanContext(context.trace_id, parent_id) + otel_tags = tags + + self.otel_span = OtelSpan( + name=operation_name, + context=otel_context, + parent=otel_parent, + attributes=otel_tags, + ) + + def set_operation_name(self, operation_name): + super(BridgeSpan, self).set_operation_name(operation_name) + self.otel_span.update_name(operation_name) + + def set_tag(self, key, value): + super(BridgeSpan, self).set_tag(key, value) + self.otel_span.set_attribute(key, value) diff --git a/bridge/opentracing/src/ot_otel_bridge/tracer.py b/bridge/opentracing/src/ot_otel_bridge/tracer.py new file mode 100644 index 00000000000..7755eda4320 --- /dev/null +++ b/bridge/opentracing/src/ot_otel_bridge/tracer.py @@ -0,0 +1,135 @@ +# 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. + + +""" +OpenTracing bridge: an OpenTracing tracer implementation using the +OpenTelemetry API. +""" + +import time + +import opentracing +from basictracer import BasicTracer +from basictracer.context import SpanContext +from basictracer.text_propagator import TextPropagator +from basictracer.util import generate_id +from opentracing.propagation import Format + +from .span import BridgeSpan + + +def tracer(**kwargs): # pylint: disable=unused-argument + return _BridgeTracer() + + +class _BridgeTracer(BasicTracer): + def __init__(self): + """Initialize the bridge Tracer.""" + super(_BridgeTracer, self).__init__(recorder=None, scope_manager=None) + self.register_propagator(Format.TEXT_MAP, TextPropagator()) + self.register_propagator(Format.HTTP_HEADERS, TextPropagator()) + + # code for start_active_span() and start_span() inspired from the base + # class BasicTracer from basictracer-python (Apache 2.0) and adapted: + # https://github.com/opentracing/basictracer-python/blob/96ebe40eabd83f9976e71b8e5c6f20ded2e57df3/basictracer/tracer.py#L51 + + 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, + ): + # Do not call super(): we want to call start_span() in this subclass. + + # 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, + references=None, + tags=None, + start_time=None, + ignore_active_span=False, + ): + # Do not call super(): create a BridgeSpan instead + + start_time = time.time() if start_time is None else start_time + + # pylint: disable=len-as-condition + + # See if we have a parent_ctx in `references` + parent_ctx = None + if child_of is not None: + parent_ctx = ( + child_of + if isinstance(child_of, opentracing.SpanContext) + else child_of.context + ) + elif references is not None and len(references) > 0: + # 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=generate_id()) + if parent_ctx is not None: + # pylint: disable=protected-access + if parent_ctx._baggage is not None: + ctx._baggage = parent_ctx._baggage.copy() + ctx.trace_id = parent_ctx.trace_id + ctx.sampled = parent_ctx.sampled + else: + ctx.trace_id = generate_id() + ctx.sampled = self.sampler.sampled(ctx.trace_id) + + otel_parent = None + if isinstance(child_of, opentracing.Span) and hasattr( + child_of, "otel_span" + ): + otel_parent = child_of.otel_span + + # Tie it all together + return BridgeSpan( + self, + operation_name=operation_name, + context=ctx, + parent_id=(None if parent_ctx is None else parent_ctx.span_id), + tags=tags, + start_time=start_time, + otel_parent=otel_parent, + ) + + def __enter__(self): + return self diff --git a/bridge/opentracing/src/ot_otel_bridge/version.py b/bridge/opentracing/src/ot_otel_bridge/version.py new file mode 100644 index 00000000000..a457c2b6651 --- /dev/null +++ b/bridge/opentracing/src/ot_otel_bridge/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/bridge/opentracing/tests/__init__.py b/bridge/opentracing/tests/__init__.py new file mode 100644 index 00000000000..d853a7bcf65 --- /dev/null +++ b/bridge/opentracing/tests/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/bridge/opentracing/tests/test_otbridge.py b/bridge/opentracing/tests/test_otbridge.py new file mode 100644 index 00000000000..f847f394900 --- /dev/null +++ b/bridge/opentracing/tests/test_otbridge.py @@ -0,0 +1,114 @@ +# 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 unittest + +import opentracing +from opentracing.ext import tags +from opentracing.propagation import Format + +from opentelemetry.sdk.trace import Span as OtelSpan +from opentelemetry.trace import SpanContext as OtelSpanContext +from ot_otel_bridge.span import BridgeSpan +from ot_otel_bridge.tracer import tracer + + +class TestOTBridge(unittest.TestCase): + def setUp(self): + opentracing.tracer = tracer() + + def test_basic(self): + with opentracing.tracer.start_active_span("TestBasic") as scope: + self.assertIsInstance(scope.span, opentracing.Span) + self.assertIsInstance(scope.span, BridgeSpan) + self.assertIsInstance(scope.span.otel_span, OtelSpan) + self.assertIsInstance( + scope.span.otel_span.get_context(), OtelSpanContext + ) + + def test_name(self): + with opentracing.tracer.start_active_span("TestName") as scope: + self.assertEqual(scope.span.otel_span.name, "TestName") + scope.span.set_operation_name("NewName") + self.assertEqual(scope.span.otel_span.name, "NewName") + + def test_tags(self): + with opentracing.tracer.start_active_span("TestTags") as scope: + scope.span.set_tag("my_tag_key", "my_tag_value") + self.assertTrue("my_tag_key" in scope.span.otel_span.attributes) + self.assertEqual( + scope.span.otel_span.attributes["my_tag_key"], "my_tag_value" + ) + + def test_baggage(self): + # TODO: At the moment, the bridge does not do anything with the baggage + # so this is just checking for consistency. + with opentracing.tracer.start_active_span("TestBaggage") as scope: + scope.span.set_baggage_item("my_baggage_item", "the_baggage") + self.assertEqual( + scope.span.get_baggage_item("my_baggage_item"), "the_baggage" + ) + + def test_log(self): + # TODO: At the moment, the bridge does not do anything with logs + with opentracing.tracer.start_active_span("TestLog") as scope: + scope.span.log_kv({"event": "string-format", "value": "the_log"}) + scope.span.log_event("message", payload={"number": 42}) + scope.span.log_event("message", payload={"number": 43}) + self.assertEqual(len(scope.span.logs), 3) + + def test_subspan(self): + with opentracing.tracer.start_active_span("TestGrandparent") as scope1: + with opentracing.tracer.start_active_span("TestParent") as scope2: + with opentracing.tracer.start_active_span( + "TestChild" + ) as scope3: + ctx1 = scope1.span.otel_span.get_context() + ctx2 = scope2.span.otel_span.get_context() + ctx3 = scope3.span.otel_span.get_context() + + self.assertEqual(ctx1.trace_id, ctx2.trace_id) + self.assertEqual(ctx1.trace_id, ctx3.trace_id) + + self.assertEqual( + ctx1.span_id, scope2.span.otel_span.parent.span_id + ) + self.assertEqual( + ctx2.span_id, scope3.span.otel_span.parent.span_id + ) + + self.assertNotEqual(ctx1.span_id, ctx2.span_id) + self.assertNotEqual(ctx1.span_id, ctx3.span_id) + + def test_inject_extract(self): + headers = {} + with opentracing.tracer.start_active_span("ClientSide") as scope: + client_ctx = scope.span.otel_span.get_context() + opentracing.tracer.inject( + scope.span.context, Format.HTTP_HEADERS, headers + ) + + span_ctx = opentracing.tracer.extract(Format.HTTP_HEADERS, headers) + span_tags = {tags.SPAN_KIND: tags.SPAN_KIND_RPC_SERVER} + with opentracing.tracer.start_active_span( + "ServerSide", child_of=span_ctx, tags=span_tags + ) as scope: + server_ctx = scope.span.otel_span.get_context() + self.assertEqual(client_ctx.trace_id, server_ctx.trace_id) + self.assertEqual( + client_ctx.trace_id, scope.span.otel_span.parent.trace_id + ) + self.assertEqual( + client_ctx.span_id, scope.span.otel_span.parent.span_id + ) diff --git a/tox.ini b/tox.ini index 5cbdfa2d5e9..3d7ea69955f 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,ext-wsgi,ext-http-requests} - pypy35-test-{api,sdk,ext-wsgi,ext-http-requests} + py3{4,5,6,7,8}-test-{api,sdk,ext-wsgi,ext-http-requests,bridge-ot} + pypy35-test-{api,sdk,ext-wsgi,ext-http-requests,bridge-ot} lint py37-mypy docs @@ -24,6 +24,7 @@ changedir = test-sdk: opentelemetry-sdk/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-http-requests: ext/opentelemetry-ext-http-requests/tests + test-bridge-ot: bridge/opentracing/tests commands_pre = python -m pip install -U pip setuptools wheel @@ -32,6 +33,7 @@ commands_pre = ext: pip install -e {toxinidir}/opentelemetry-api wsgi: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi http-requests: pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests + bridge-ot: pip install -e {toxinidir}/opentelemetry-sdk -e {toxinidir}/bridge/opentracing commands = mypy: mypy --namespace-packages opentelemetry-api/src/opentelemetry/ @@ -52,14 +54,15 @@ commands_pre = pip install -e {toxinidir}/opentelemetry-sdk pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests + pip install -e {toxinidir}/bridge/opentracing commands = ; Prefer putting everything in one pylint command to profit from duplication ; warnings. - black --check --diff opentelemetry-api/src/opentelemetry opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry opentelemetry-sdk/tests/ ext/opentelemetry-ext-http-requests/src/ ext/opentelemetry-ext-http-requests/tests/ ext/opentelemetry-ext-wsgi/tests/ - pylint opentelemetry-api/src/opentelemetry opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry opentelemetry-sdk/tests/ ext/opentelemetry-ext-http-requests/src/ ext/opentelemetry-ext-http-requests/tests/ ext/opentelemetry-ext-wsgi/tests/ - flake8 opentelemetry-api/src/ opentelemetry-api/tests/ opentelemetry-sdk/src/ opentelemetry-sdk/tests/ ext/opentelemetry-ext-wsgi/src/ ext/opentelemetry-ext-wsgi/tests/ ext/opentelemetry-ext-http-requests/src/ - isort --check-only --diff --recursive opentelemetry-api/src opentelemetry-sdk/src ext/opentelemetry-ext-wsgi/src ext/opentelemetry-ext-wsgi/tests ext/opentelemetry-ext-http-requests/src/ + black --check --diff opentelemetry-api/src/opentelemetry opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry opentelemetry-sdk/tests/ ext/opentelemetry-ext-http-requests/src/ ext/opentelemetry-ext-http-requests/tests/ ext/opentelemetry-ext-wsgi/tests/ bridge/opentracing/src/ot_otel_bridge bridge/opentracing/tests + pylint opentelemetry-api/src/opentelemetry opentelemetry-api/tests/ opentelemetry-sdk/src/opentelemetry opentelemetry-sdk/tests/ ext/opentelemetry-ext-http-requests/src/ ext/opentelemetry-ext-http-requests/tests/ ext/opentelemetry-ext-wsgi/tests/ bridge/opentracing/src/ot_otel_bridge bridge/opentracing/tests/ + flake8 opentelemetry-api/src/ opentelemetry-api/tests/ opentelemetry-sdk/src/ opentelemetry-sdk/tests/ ext/opentelemetry-ext-wsgi/src/ ext/opentelemetry-ext-wsgi/tests/ ext/opentelemetry-ext-http-requests/src/ bridge/opentracing/src/ot_otel_bridge/ bridge/opentracing/tests/ + isort --check-only --diff --recursive opentelemetry-api/src opentelemetry-sdk/src ext/opentelemetry-ext-wsgi/src ext/opentelemetry-ext-wsgi/tests ext/opentelemetry-ext-http-requests/src/ bridge/opentracing/src/ot_otel_bridge/ bridge/opentracing/tests/ [testenv:docs] deps =