diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d98c2c5995..5ff555f4d0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3138](https://github.com/open-telemetry/opentelemetry-python/pull/3138)) - Add exponential histogram ([#2964](https://github.com/open-telemetry/opentelemetry-python/pull/2964)) +- Add OpenCensus trace bridge/shim + ([#3210](https://github.com/open-telemetry/opentelemetry-python/pull/3210)) ## Version 1.16.0/0.37b0 (2023-02-17) diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/__init__.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/__init__.py index 2ef69255bc2..bd49fd19876 100644 --- a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/__init__.py +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/__init__.py @@ -22,3 +22,16 @@ already instrumented using OpenCensus to start using OpenTelemetry with minimal effort, without having to rewrite large portions of the codebase. """ + +from opentelemetry.shim.opencensus._patch import install_shim, uninstall_shim + +__all__ = [ + "install_shim", + "uninstall_shim", +] + +# TODO: Decide when this should be called. +# 1. defensive import in opentelemetry-api +# 2. defensive import directly in OpenCensus, although that would require a release +# 3. ask the user to do it +# install_shim() diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_patch.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_patch.py new file mode 100644 index 00000000000..42b1b189ce7 --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_patch.py @@ -0,0 +1,56 @@ +# Copyright The 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 logging import getLogger +from typing import Optional + +from opencensus.trace.tracer import Tracer +from opencensus.trace.tracers.noop_tracer import NoopTracer + +from opentelemetry import trace +from opentelemetry.shim.opencensus._shim_tracer import ShimTracer +from opentelemetry.shim.opencensus.version import __version__ + +_logger = getLogger(__name__) + + +def install_shim( + tracer_provider: Optional[trace.TracerProvider] = None, +) -> None: + otel_tracer = trace.get_tracer( + "opentelemetry-opencensus-shim", + __version__, + tracer_provider=tracer_provider, + ) + shim_tracer = ShimTracer(NoopTracer(), otel_tracer=otel_tracer) + + def fget_tracer(self) -> ShimTracer: + return shim_tracer + + def fset_tracer(self, value) -> None: + # ignore attempts to set the value + pass + + # Tracer's constructor sets self.tracer to either a NoopTracer or ContextTracer depending + # on sampler: + # https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/tracer.py#L63. + # We monkeypatch Tracer.tracer with a property to return the shim instance instead. This + # makes all instances of Tracer (even those already created) use the ShimTracer singleton. + Tracer.tracer = property(fget_tracer, fset_tracer) + _logger.info("Installed OpenCensus shim") + + +def uninstall_shim() -> None: + if hasattr(Tracer, "tracer"): + del Tracer.tracer diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_span.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_span.py new file mode 100644 index 00000000000..fdc6f724a40 --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_span.py @@ -0,0 +1,163 @@ +# Copyright The 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 +from datetime import datetime +from typing import TYPE_CHECKING + +import wrapt +from opencensus.trace.base_span import BaseSpan +from opencensus.trace.span import SpanKind +from opencensus.trace.status import Status +from opencensus.trace.time_event import MessageEvent + +from opentelemetry import context, trace + +if TYPE_CHECKING: + from opentelemetry.shim.opencensus._shim_tracer import ShimTracer + +_logger = logging.getLogger(__name__) + +# Copied from Java +# https://github.com/open-telemetry/opentelemetry-java/blob/0d3a04669e51b33ea47b29399a7af00012d25ccb/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/SpanConverter.java#L24-L27 +_MESSAGE_EVENT_ATTRIBUTE_KEY_TYPE = "message.event.type" +_MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_UNCOMPRESSED = ( + "message.event.size.uncompressed" +) +_MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_COMPRESSED = "message.event.size.compressed" + +_MESSAGE_EVENT_TYPE_STR_MAPPING = { + 0: "TYPE_UNSPECIFIED", + 1: "SENT", + 2: "RECEIVED", +} + + +def _opencensus_time_to_nanos(timestamp: str) -> int: + """Converts an OpenCensus formatted time string (ISO 8601 with Z) to time.time_ns style + unix timestamp + """ + # format taken from + # https://github.com/census-instrumentation/opencensus-python/blob/c38c71b9285e71de94d0185ff3c5bf65ee163345/opencensus/common/utils/__init__.py#L76 + # + # datetime.fromisoformat() does not work with the added "Z" until python 3.11 + seconds_float = datetime.strptime( + timestamp, "%Y-%m-%dT%H:%M:%S.%fZ" + ).timestamp() + return round(seconds_float * 1e9) + + +# pylint: disable=abstract-method +class ShimSpan(wrapt.ObjectProxy): + def __init__( + self, + wrapped: BaseSpan, + *, + otel_span: trace.Span, + shim_tracer: "ShimTracer", + ) -> None: + super().__init__(wrapped) + self._self_otel_span = otel_span + self._self_shim_tracer = shim_tracer + self._self_token: object = None + + # Set a few values for BlankSpan members (they appear to be part of the "public" API + # even though they are not documented in BaseSpan). Some instrumentations may use these + # and not expect an AttributeError to be raised. Set values from OTel where possible + # and let ObjectProxy defer to the wrapped BlankSpan otherwise. + sc = self._self_otel_span.get_span_context() + self.same_process_as_parent_span = not sc.is_remote + self.span_id = sc.span_id + + def span(self, name="child_span"): + return self._self_shim_tracer.start_span(name=name) + + def add_attribute(self, attribute_key, attribute_value): + self._self_otel_span.set_attribute(attribute_key, attribute_value) + + def add_annotation(self, description, **attrs): + self._self_otel_span.add_event(description, attrs) + + def add_message_event(self, message_event: MessageEvent): + attrs = { + _MESSAGE_EVENT_ATTRIBUTE_KEY_TYPE: _MESSAGE_EVENT_TYPE_STR_MAPPING[ + message_event.type + ], + } + if message_event.uncompressed_size_bytes is not None: + attrs[ + _MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_UNCOMPRESSED + ] = message_event.uncompressed_size_bytes + if message_event.compressed_size_bytes is not None: + attrs[ + _MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_COMPRESSED + ] = message_event.compressed_size_bytes + + timestamp = _opencensus_time_to_nanos(message_event.timestamp) + self._self_otel_span.add_event( + str(message_event.id), + attrs, + timestamp=timestamp, + ) + + # pylint: disable=no-self-use + def add_link(self, link): + """span links do not work with the shim because the OpenCensus Tracer does not accept + links in start_span(). Same issue applies to SpanKind. Also see: + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/opencensus.md#known-incompatibilities + """ + _logger.warning( + "OpenTelemetry does not support links added after a span is created." + ) + + @property + def span_kind(self): + """Setting span_kind does not work with the shim because the OpenCensus Tracer does not + accept the param in start_span() and there's no way to set OTel span kind after + start_span(). + """ + return SpanKind.UNSPECIFIED + + @span_kind.setter + def span_kind(self, value): + _logger.warning( + "OpenTelemetry does not support setting span kind after a span is created." + ) + + def set_status(self, status: Status): + self._self_otel_span.set_status( + trace.StatusCode.OK if status.is_ok else trace.StatusCode.ERROR, + status.description, + ) + + def finish(self): + """Note this method does not pop the span from current context. Use Tracer.end_span() + or a `with span: ...` statement (contextmanager) to do that. + """ + self._self_otel_span.end() + + def __enter__(self): + self._self_otel_span.__enter__() + return self + + # pylint: disable=arguments-differ + def __exit__(self, exception_type, exception_value, traceback): + self._self_otel_span.__exit__( + exception_type, exception_value, traceback + ) + # OpenCensus Span.__exit__() calls Tracer.end_span() + # https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/span.py#L390 + # but that would cause the OTel span to be ended twice. Instead just detach it from + # context directly. + context.detach(self._self_token) diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_tracer.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_tracer.py new file mode 100644 index 00000000000..0ce2d011201 --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_shim_tracer.py @@ -0,0 +1,90 @@ +# Copyright The 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 wrapt +from opencensus.trace.blank_span import BlankSpan +from opencensus.trace.tracers.base import Tracer as BaseTracer + +from opentelemetry import context, trace +from opentelemetry.shim.opencensus._shim_span import ShimSpan + +_logger = logging.getLogger(__name__) + +_SHIM_SPAN_KEY = context.create_key("opencensus-shim-span-key") + + +def set_shim_span_in_context( + span: ShimSpan, ctx: context.Context +) -> context.Context: + return context.set_value(_SHIM_SPAN_KEY, span, ctx) + + +def get_shim_span_in_context() -> ShimSpan: + return context.get_value(_SHIM_SPAN_KEY) + + +# pylint: disable=abstract-method +class ShimTracer(wrapt.ObjectProxy): + def __init__( + self, wrapped: BaseTracer, *, otel_tracer: trace.Tracer + ) -> None: + super().__init__(wrapped) + self._self_otel_tracer = otel_tracer + + # For now, finish() is not implemented by the shim. It would require keeping a list of all + # spans created so they can all be finished. + # def finish(self): + # """End spans and send to reporter.""" + + def span(self, name="span"): + return self.start_span(name=name) + + def start_span(self, name="span"): + span = self._self_otel_tracer.start_span(name) + shim_span = ShimSpan( + BlankSpan(name=name, context_tracer=self), + otel_span=span, + shim_tracer=self, + ) + + ctx = trace.set_span_in_context(span) + ctx = set_shim_span_in_context(shim_span, ctx) + + # OpenCensus's ContextTracer calls execution_context.set_current_span(span) which is + # equivalent to the below. This can cause context to leak but is equivalent. + # pylint: disable=protected-access + shim_span._self_token = context.attach(ctx) + return shim_span + + def end_span(self): + """Finishes the current span in the context and pops restores the context from before + the span was started. + """ + span = self.current_span() + if not span: + _logger.warning("No active span, cannot do end_span.") + return + + span.finish() + # pylint: disable=protected-access + context.detach(span._self_token) + + # pylint: disable=no-self-use + def current_span(self): + return get_shim_span_in_context() + + def add_attribute_to_current_span(self, attribute_key, attribute_value): + self.current_span().add_attribute(attribute_key, attribute_value) diff --git a/shim/opentelemetry-opencensus-shim/tests/test_patch.py b/shim/opentelemetry-opencensus-shim/tests/test_patch.py new file mode 100644 index 00000000000..697ddfc3520 --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/tests/test_patch.py @@ -0,0 +1,84 @@ +# Copyright The 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 + +from opencensus.trace.tracer import Tracer +from opencensus.trace.tracers.noop_tracer import NoopTracer + +from opentelemetry.shim.opencensus import install_shim, uninstall_shim +from opentelemetry.shim.opencensus._shim_tracer import ShimTracer + + +class TestPatch(unittest.TestCase): + def setUp(self): + uninstall_shim() + + def tearDown(self): + uninstall_shim() + + def test_install_shim(self): + # Initially the shim is not installed. The Tracer class has no tracer property, it is + # instance level only. + self.assertFalse(hasattr(Tracer, "tracer")) + + install_shim() + + # The actual Tracer class should now be patched with a tracer property + self.assertTrue(hasattr(Tracer, "tracer")) + self.assertIsInstance(Tracer.tracer, property) + + def test_install_shim_affects_existing_tracers(self): + # Initially the shim is not installed. A OC Tracer instance should have a NoopTracer + oc_tracer = Tracer() + self.assertIsInstance(oc_tracer.tracer, NoopTracer) + self.assertNotIsInstance(oc_tracer.tracer, ShimTracer) + + install_shim() + + # The property should cause existing instances to get the singleton ShimTracer + self.assertIsInstance(oc_tracer.tracer, ShimTracer) + + def test_install_shim_affects_new_tracers(self): + install_shim() + + # The property should cause existing instances to get the singleton ShimTracer + oc_tracer = Tracer() + self.assertIsInstance(oc_tracer.tracer, ShimTracer) + + def test_uninstall_shim_resets_tracer(self): + install_shim() + uninstall_shim() + + # The actual Tracer class should not be patched + self.assertFalse(hasattr(Tracer, "tracer")) + + def test_uninstall_shim_resets_existing_tracers(self): + oc_tracer = Tracer() + orig = oc_tracer.tracer + install_shim() + uninstall_shim() + + # Accessing the tracer member should no longer use the property, and instead should get + # its original NoopTracer + self.assertIs(oc_tracer.tracer, orig) + + def test_uninstall_shim_resets_new_tracers(self): + install_shim() + uninstall_shim() + + # Accessing the tracer member should get the NoopTracer + oc_tracer = Tracer() + self.assertIsInstance(oc_tracer.tracer, NoopTracer) + self.assertNotIsInstance(oc_tracer.tracer, ShimTracer) diff --git a/shim/opentelemetry-opencensus-shim/tests/test_shim.py b/shim/opentelemetry-opencensus-shim/tests/test_shim.py index 922026ac453..df6abcfe1e1 100644 --- a/shim/opentelemetry-opencensus-shim/tests/test_shim.py +++ b/shim/opentelemetry-opencensus-shim/tests/test_shim.py @@ -12,9 +12,117 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging import unittest +from unittest.mock import patch + +from opencensus.trace.blank_span import BlankSpan as OcBlankSpan +from opencensus.trace.link import Link as OcLink +from opencensus.trace.span import SpanKind +from opencensus.trace.tracer import Tracer as OcTracer +from opencensus.trace.tracers.noop_tracer import NoopTracer as OcNoopTracer + +from opentelemetry.shim.opencensus import install_shim, uninstall_shim +from opentelemetry.shim.opencensus._shim_span import ShimSpan +from opentelemetry.shim.opencensus._shim_tracer import ShimTracer class TestShim(unittest.TestCase): - def test_shim(self): - pass + def setUp(self): + uninstall_shim() + install_shim() + + def tearDown(self): + uninstall_shim() + + def assert_hasattr(self, obj, key): + self.assertTrue(hasattr(obj, key)) + + def test_shim_tracer_wraps_noop_tracer(self): + oc_tracer = OcTracer() + + self.assertIsInstance(oc_tracer.tracer, ShimTracer) + + # wrapt.ObjectProxy does the magic here. The ShimTracer should look like the real OC + # NoopTracer. + self.assertIsInstance(oc_tracer.tracer, OcNoopTracer) + self.assert_hasattr(oc_tracer.tracer, "finish") + self.assert_hasattr(oc_tracer.tracer, "span") + self.assert_hasattr(oc_tracer.tracer, "start_span") + self.assert_hasattr(oc_tracer.tracer, "end_span") + self.assert_hasattr(oc_tracer.tracer, "current_span") + self.assert_hasattr(oc_tracer.tracer, "add_attribute_to_current_span") + self.assert_hasattr(oc_tracer.tracer, "list_collected_spans") + + def test_shim_tracer_starts_shim_spans(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("foo") as span: + self.assertIsInstance(span, ShimSpan) + + def test_shim_span_wraps_blank_span(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("foo") as span: + # wrapt.ObjectProxy does the magic here. The ShimSpan should look like the real OC + # BlankSpan. + self.assertIsInstance(span, OcBlankSpan) + + # members + self.assert_hasattr(span, "name") + self.assert_hasattr(span, "parent_span") + self.assert_hasattr(span, "start_time") + self.assert_hasattr(span, "end_time") + self.assert_hasattr(span, "span_id") + self.assert_hasattr(span, "attributes") + self.assert_hasattr(span, "stack_trace") + self.assert_hasattr(span, "annotations") + self.assert_hasattr(span, "message_events") + self.assert_hasattr(span, "links") + self.assert_hasattr(span, "status") + self.assert_hasattr(span, "same_process_as_parent_span") + self.assert_hasattr(span, "_child_spans") + self.assert_hasattr(span, "context_tracer") + self.assert_hasattr(span, "span_kind") + + # methods + self.assert_hasattr(span, "on_create") + self.assert_hasattr(span, "children") + self.assert_hasattr(span, "span") + self.assert_hasattr(span, "add_attribute") + self.assert_hasattr(span, "add_annotation") + self.assert_hasattr(span, "add_message_event") + self.assert_hasattr(span, "add_link") + self.assert_hasattr(span, "set_status") + self.assert_hasattr(span, "start") + self.assert_hasattr(span, "finish") + self.assert_hasattr(span, "__iter__") + self.assert_hasattr(span, "__enter__") + self.assert_hasattr(span, "__exit__") + + def test_add_link_logs_a_warning(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("foo") as span: + with self.assertLogs(level=logging.WARNING): + span.add_link(OcLink("1", "1")) + + def test_set_span_kind_logs_a_warning(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("foo") as span: + with self.assertLogs(level=logging.WARNING): + span.span_kind = SpanKind.CLIENT + + # pylint: disable=no-self-use,no-member,protected-access + def test_shim_span_contextmanager_calls_does_not_call_end(self): + # This was a bug in first implementation where the underlying OTel span.end() was + # called after span.__exit__ which caused double-ending the span. + oc_tracer = OcTracer() + oc_span = oc_tracer.start_span("foo") + + with patch.object( + oc_span, + "_self_otel_span", + wraps=oc_span._self_otel_span, + ) as spy_otel_span: + with oc_span: + pass + + spy_otel_span.end.assert_not_called() diff --git a/shim/opentelemetry-opencensus-shim/tests/test_shim_with_sdk.py b/shim/opentelemetry-opencensus-shim/tests/test_shim_with_sdk.py new file mode 100644 index 00000000000..9bccc82ecbe --- /dev/null +++ b/shim/opentelemetry-opencensus-shim/tests/test_shim_with_sdk.py @@ -0,0 +1,248 @@ +# Copyright The 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 unittest +from datetime import datetime + +from opencensus.trace import time_event +from opencensus.trace.status import Status as OcStatus +from opencensus.trace.tracer import Tracer as OcTracer + +from opentelemetry import trace +from opentelemetry.sdk.trace import ReadableSpan, TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) +from opentelemetry.sdk.trace.sampling import ALWAYS_ON +from opentelemetry.shim.opencensus import install_shim, uninstall_shim + +_TIMESTAMP = datetime.fromisoformat("2023-01-01T00:00:00.000000") + + +class TestShimWithSdk(unittest.TestCase): + def setUp(self): + uninstall_shim() + self.tracer_provider = TracerProvider( + sampler=ALWAYS_ON, shutdown_on_exit=False + ) + self.mem_exporter = InMemorySpanExporter() + self.tracer_provider.add_span_processor( + SimpleSpanProcessor(self.mem_exporter) + ) + install_shim(self.tracer_provider) + + def tearDown(self): + uninstall_shim() + + def test_start_span_interacts_with_context(self): + oc_tracer = OcTracer() + span = oc_tracer.start_span("foo") + + # Should have created a real OTel span in implicit context under the hood. OpenCensus + # does not require another step to set the span in context. + otel_span = trace.get_current_span() + self.assertNotEqual(span.span_id, 0) + self.assertEqual(span.span_id, otel_span.get_span_context().span_id) + + # This should end the span and remove it from context + oc_tracer.end_span() + self.assertIs(trace.get_current_span(), trace.INVALID_SPAN) + + def test_context_manager_interacts_with_context(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("foo") as span: + # Should have created a real OTel span in implicit context under the hood + otel_span = trace.get_current_span() + + self.assertNotEqual(span.span_id, 0) + self.assertEqual( + span.span_id, otel_span.get_span_context().span_id + ) + + # The span should now be popped from context + self.assertIs(trace.get_current_span(), trace.INVALID_SPAN) + + def test_exports_a_span(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("span1"): + pass + + self.assertEqual(len(self.mem_exporter.get_finished_spans()), 1) + + def test_span_attributes(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("span1") as span: + span.add_attribute("key1", "value1") + span.add_attribute("key2", "value2") + + exported_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] + self.assertDictEqual( + dict(exported_span.attributes), + {"key1": "value1", "key2": "value2"}, + ) + + def test_span_annotations(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("span1") as span: + span.add_annotation("description", key1="value1", key2="value2") + + exported_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] + self.assertEqual(len(exported_span.events), 1) + event = exported_span.events[0] + self.assertEqual(event.name, "description") + self.assertDictEqual( + dict(event.attributes), {"key1": "value1", "key2": "value2"} + ) + + def test_span_message_event(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("span1") as span: + span.add_message_event( + time_event.MessageEvent( + _TIMESTAMP, "id_sent", time_event.Type.SENT, "20", "10" + ) + ) + span.add_message_event( + time_event.MessageEvent( + _TIMESTAMP, + "id_received", + time_event.Type.RECEIVED, + "20", + "10", + ) + ) + span.add_message_event( + time_event.MessageEvent( + _TIMESTAMP, + "id_unspecified", + None, + "20", + "10", + ) + ) + + exported_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] + self.assertEqual(len(exported_span.events), 3) + event1, event2, event3 = exported_span.events + + self.assertEqual(event1.name, "id_sent") + self.assertDictEqual( + dict(event1.attributes), + { + "message.event.size.compressed": "10", + "message.event.size.uncompressed": "20", + "message.event.type": "SENT", + }, + ) + self.assertEqual(event2.name, "id_received") + self.assertDictEqual( + dict(event2.attributes), + { + "message.event.size.compressed": "10", + "message.event.size.uncompressed": "20", + "message.event.type": "RECEIVED", + }, + ) + self.assertEqual(event3.name, "id_unspecified") + self.assertDictEqual( + dict(event3.attributes), + { + "message.event.size.compressed": "10", + "message.event.size.uncompressed": "20", + "message.event.type": "TYPE_UNSPECIFIED", + }, + ) + + def test_span_status(self): + oc_tracer = OcTracer() + with oc_tracer.start_span("span_ok") as span: + # OTel will log about the message being set on a not OK span + with self.assertLogs(level=logging.WARNING) as rec: + span.set_status(OcStatus(0, "message")) + self.assertIn( + "description should only be set when status_code is set to StatusCode.ERROR", + rec.output[0], + ) + + with oc_tracer.start_span("span_exception") as span: + span.set_status( + OcStatus.from_exception(Exception("exception message")) + ) + + self.assertEqual(len(self.mem_exporter.get_finished_spans()), 2) + ok_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] + exc_span: ReadableSpan = self.mem_exporter.get_finished_spans()[1] + + self.assertTrue(ok_span.status.is_ok) + # should be none even though we provided it because OTel drops the description when + # status is not ERROR + self.assertIsNone(ok_span.status.description) + + self.assertFalse(exc_span.status.is_ok) + self.assertEqual(exc_span.status.description, "exception message") + + def assert_related(self, *, child: ReadableSpan, parent: ReadableSpan): + self.assertEqual( + child.parent.span_id, parent.get_span_context().span_id + ) + + def test_otel_sandwich(self): + oc_tracer = OcTracer() + otel_tracer = self.tracer_provider.get_tracer(__name__) + with oc_tracer.start_span("opencensus_outer"): + with otel_tracer.start_as_current_span("otel_middle"): + with oc_tracer.start_span("opencensus_inner"): + pass + + self.assertEqual(len(self.mem_exporter.get_finished_spans()), 3) + opencensus_inner: ReadableSpan = ( + self.mem_exporter.get_finished_spans()[0] + ) + otel_middle: ReadableSpan = self.mem_exporter.get_finished_spans()[1] + opencensus_outer: ReadableSpan = ( + self.mem_exporter.get_finished_spans()[2] + ) + + self.assertEqual(opencensus_outer.name, "opencensus_outer") + self.assertEqual(otel_middle.name, "otel_middle") + self.assertEqual(opencensus_inner.name, "opencensus_inner") + + self.assertIsNone(opencensus_outer.parent) + self.assert_related(parent=opencensus_outer, child=otel_middle) + self.assert_related(parent=otel_middle, child=opencensus_inner) + + def test_opencensus_sandwich(self): + oc_tracer = OcTracer() + otel_tracer = self.tracer_provider.get_tracer(__name__) + with otel_tracer.start_as_current_span("otel_outer"): + with oc_tracer.start_span("opencensus_middle"): + with otel_tracer.start_as_current_span("otel_inner"): + pass + + self.assertEqual(len(self.mem_exporter.get_finished_spans()), 3) + otel_inner: ReadableSpan = self.mem_exporter.get_finished_spans()[0] + opencensus_middle: ReadableSpan = ( + self.mem_exporter.get_finished_spans()[1] + ) + otel_outer: ReadableSpan = self.mem_exporter.get_finished_spans()[2] + + self.assertEqual(otel_outer.name, "otel_outer") + self.assertEqual(opencensus_middle.name, "opencensus_middle") + self.assertEqual(otel_inner.name, "otel_inner") + + self.assertIsNone(otel_outer.parent) + self.assert_related(parent=otel_outer, child=opencensus_middle) + self.assert_related(parent=opencensus_middle, child=otel_inner)