diff --git a/sdk/core/azure-core/azure/core/settings.py b/sdk/core/azure-core/azure/core/settings.py index 566a15b03ab9..e1b4c738cdb8 100644 --- a/sdk/core/azure-core/azure/core/settings.py +++ b/sdk/core/azure-core/azure/core/settings.py @@ -31,7 +31,19 @@ from collections import namedtuple import logging import os -from typing import Any, Union +import six +import sys + +try: + from typing import TYPE_CHECKING +except ImportError: + TYPE_CHECKING = False + +if TYPE_CHECKING: + from typing import Any, Union + + +from azure.core.tracing import AbstractSpan __all__ = ("settings",) @@ -61,9 +73,9 @@ def convert_bool(value): return value # type: ignore val = value.lower() # type: ignore - if val in ["yes", "1", "on"]: + if val in ["yes", "1", "on", "true", "True"]: return True - if val in ["no", "0", "off"]: + if val in ["no", "0", "off", "false", "False"]: return False raise ValueError("Cannot convert {} to boolean value".format(value)) @@ -102,14 +114,63 @@ def convert_logging(value): val = value.upper() # type: ignore level = _levels.get(val) if not level: - raise ValueError( - "Cannot convert {} to log level, valid values are: {}".format( - value, ", ".join(_levels) - ) - ) + raise ValueError("Cannot convert {} to log level, valid values are: {}".format(value, ", ".join(_levels))) return level +def get_opencensus_span(): + # type: () -> OpenCensusSpan + """Returns the OpenCensusSpan if opencensus is installed else returns None""" + try: + from azure.core.tracing.ext.opencensus_span import OpenCensusSpan + + return OpenCensusSpan + except ImportError: + return None + + +def get_opencensus_span_if_opencensus_is_imported(): + if "opencensus" not in sys.modules: + return None + return get_opencensus_span() + + +_tracing_implementation_dict = {"opencensus": get_opencensus_span} + + +def convert_tracing_impl(value): + # type: (Union[str, AbstractSpan]) -> AbstractSpan + """Convert a string to AbstractSpan + + If a AbstractSpan is passed in, it is returned as-is. Otherwise the function + understands the following strings, ignoring case: + + * "opencensus" + + :param value: the value to convert + :type value: string + :returns: AbstractSpan + :raises ValueError: If conversion to AbstractSpan fails + + """ + if value is None: + return get_opencensus_span_if_opencensus_is_imported() + + wrapper_class = value + if isinstance(value, six.string_types): + value = value.lower() + get_wrapper_class = _tracing_implementation_dict.get(value, lambda: _Unset) + wrapper_class = get_wrapper_class() + if wrapper_class is _Unset: + raise ValueError( + "Cannot convert {} to AbstractSpan, valid values are: {}".format( + value, ", ".join(_tracing_implementation_dict) + ) + ) + + return wrapper_class + + class PrioritizedSetting(object): """Return a value for a global setting according to configuration precedence. @@ -138,9 +199,7 @@ class PrioritizedSetting(object): """ - def __init__( - self, name, env_var=None, system_hook=None, default=_Unset, convert=None - ): + def __init__(self, name, env_var=None, system_hook=None, default=_Unset, convert=None): self._name = name self._env_var = env_var @@ -311,11 +370,7 @@ def defaults(self): """ Return implicit default values for all settings, ignoring environment and system. """ - props = { - k: v.default - for (k, v) in self.__class__.__dict__.items() - if isinstance(v, PrioritizedSetting) - } + props = {k: v.default for (k, v) in self.__class__.__dict__.items() if isinstance(v, PrioritizedSetting)} return self._config(props) @property @@ -335,11 +390,7 @@ def config(self, **kwargs): settings.config(log_level=logging.DEBUG) """ - props = { - k: v() - for (k, v) in self.__class__.__dict__.items() - if isinstance(v, PrioritizedSetting) - } + props = {k: v() for (k, v) in self.__class__.__dict__.items() if isinstance(v, PrioritizedSetting)} props.update(kwargs) return self._config(props) @@ -348,17 +399,19 @@ def _config(self, props): # pylint: disable=no-self-use return Config(**props) log_level = PrioritizedSetting( - "log_level", - env_var="AZURE_LOG_LEVEL", - convert=convert_logging, - default=logging.INFO, + "log_level", env_var="AZURE_LOG_LEVEL", convert=convert_logging, default=logging.INFO ) tracing_enabled = PrioritizedSetting( - "tracing_enbled", - env_var="AZURE_TRACING_ENABLED", - convert=convert_bool, - default=False, + "tracing_enbled", env_var="AZURE_TRACING_ENABLED", convert=convert_bool, default=False + ) + + tracing_implementation = PrioritizedSetting( + "tracing_implementation", env_var="AZURE_SDK_TRACING_IMPLEMENTATION", convert=convert_tracing_impl, default=None + ) + + tracing_should_only_propagate = PrioritizedSetting( + "tracing_should_only_propagate", env_var="AZURE_TRACING_ONLY_PROPAGATE", convert=convert_bool, default=False ) diff --git a/sdk/core/azure-core/azure/core/tracing/__init__.py b/sdk/core/azure-core/azure/core/tracing/__init__.py index b2c68383597d..3c4ae3058244 100644 --- a/sdk/core/azure-core/azure/core/tracing/__init__.py +++ b/sdk/core/azure-core/azure/core/tracing/__init__.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. # ------------------------------------ from azure.core.tracing.abstract_span import AbstractSpan -from azure.core.tracing.context import tracing_context - -__all__ = ["tracing_context", "AbstractSpan"] +__all__ = [ + "AbstractSpan", +] diff --git a/sdk/core/azure-core/azure/core/tracing/abstract_span.py b/sdk/core/azure-core/azure/core/tracing/abstract_span.py index 5da1b2123610..1870c68a98c4 100644 --- a/sdk/core/azure-core/azure/core/tracing/abstract_span.py +++ b/sdk/core/azure-core/azure/core/tracing/abstract_span.py @@ -2,6 +2,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +"""Protocol that defines what functions wrappers of tracing libraries should implement.""" + try: from typing import TYPE_CHECKING except ImportError: @@ -19,7 +21,7 @@ class AbstractSpan(Protocol): """Wraps a span from a distributed tracing implementation.""" - def __init__(self, span=None, name=None): + def __init__(self, span=None, name=None): # pylint: disable=super-init-not-called # type: (Optional[Any], Optional[str]) -> None """ If a span is given wraps the span. Else a new span is created. diff --git a/sdk/core/azure-core/azure/core/tracing/common.py b/sdk/core/azure-core/azure/core/tracing/common.py new file mode 100644 index 000000000000..f6e4716a2eb8 --- /dev/null +++ b/sdk/core/azure-core/azure/core/tracing/common.py @@ -0,0 +1,92 @@ +# -------------------------------------------------------------------------- +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# +# The MIT License (MIT) +# +# 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. +# +# -------------------------------------------------------------------------- +"""Common functions shared by both the sync and the async decorators.""" + +from azure.core.tracing.context import tracing_context +from azure.core.tracing.abstract_span import AbstractSpan +from azure.core.settings import settings + + +try: + from typing import TYPE_CHECKING +except ImportError: + TYPE_CHECKING = False + +if TYPE_CHECKING: + from typing import Any, Optional + + +def set_span_contexts(wrapped_span, span_instance=None): + # type: (AbstractSpan, Optional[AbstractSpan]) -> None + """ + Set the sdk context and the implementation context. `span_instance` will be used to set the implementation context + if passed in else will use `wrapped_span.span_instance`. + + :param wrapped_span: The `AbstractSpan` to set as the sdk context + :type wrapped_span: `azure.core.tracing.abstract_span.AbstractSpan` + :param span_instance: The span to set as the current span for the implementation context + """ + tracing_context.current_span.set(wrapped_span) + impl_wrapper = settings.tracing_implementation() + if wrapped_span is not None: + span_instance = wrapped_span.span_instance + if impl_wrapper is not None: + impl_wrapper.set_current_span(span_instance) + + +def get_parent_span(parent_span): + # type: (Any) -> Tuple(AbstractSpan, AbstractSpan, Any) + """ + Returns the current span so that the function's span will be its child. It will create a new span if there is + no current span in any of the context. + + :param parent_span: The parent_span arg that the user passes into the top level function + :returns: the parent_span of the function to be traced + :rtype: `azure.core.tracing.abstract_span.AbstractSpan` + """ + wrapper_class = settings.tracing_implementation() + if wrapper_class is None: + return None + + orig_wrapped_span = tracing_context.current_span.get() + # parent span is given, get from my context, get from the implementation context or make our own + parent_span = orig_wrapped_span if parent_span is None else wrapper_class(parent_span) + if parent_span is None: + current_span = wrapper_class.get_current_span() + parent_span = ( + wrapper_class(span=current_span) + if current_span + else wrapper_class(name="azure-sdk-for-python-first_parent_span") + ) + + return parent_span + + +def should_use_trace(parent_span): + # type: (AbstractSpan) -> bool + """Given Parent Span Returns whether the function should be traced""" + only_propagate = settings.tracing_should_only_propagate() + return bool(parent_span and not only_propagate) diff --git a/sdk/core/azure-core/azure/core/tracing/context.py b/sdk/core/azure-core/azure/core/tracing/context.py index 4409ce1f6756..a00f88f2d854 100644 --- a/sdk/core/azure-core/azure/core/tracing/context.py +++ b/sdk/core/azure-core/azure/core/tracing/context.py @@ -2,7 +2,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +"""The context for the azure.core.tracing. Holds global variables in a thread and async safe way.""" + import threading +from azure.core.settings import settings try: from typing import TYPE_CHECKING @@ -119,7 +122,6 @@ class TracingContext: def __init__(self): # type: () -> None self.current_span = TracingContext._get_context_class("current_span", None) - self.tracing_impl = TracingContext._get_context_class("tracing_impl", None) def with_current_context(self, func): # type: (Callable[[Any], Any]) -> Any @@ -129,7 +131,7 @@ def with_current_context(self, func): :return: The target the pass in instead of the function """ wrapped_span = tracing_context.current_span.get() - wrapper_class = self.tracing_impl.get() + wrapper_class = settings.tracing_implementation() if wrapper_class is not None: current_impl_span = wrapper_class.get_current_span() current_impl_tracer = wrapper_class.get_current_tracer() @@ -140,7 +142,6 @@ def call_with_current_context(*args, **kwargs): wrapper_class.set_current_tracer(current_impl_tracer) current_span = wrapped_span or wrapper_class(current_impl_span) self.current_span.set(current_span) - self.tracing_impl.set(wrapper_class) return func(*args, **kwargs) return call_with_current_context diff --git a/sdk/core/azure-core/azure/core/tracing/decorator.py b/sdk/core/azure-core/azure/core/tracing/decorator.py new file mode 100644 index 000000000000..d033256a799d --- /dev/null +++ b/sdk/core/azure-core/azure/core/tracing/decorator.py @@ -0,0 +1,64 @@ +# -------------------------------------------------------------------------- +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# +# The MIT License (MIT) +# +# 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. +# +# -------------------------------------------------------------------------- +"""The decorator to apply if you want the given function traced.""" + +import functools + +import azure.core.tracing.common as common +from azure.core.settings import settings +from azure.core.tracing.context import tracing_context + + +def distributed_trace(func): + # type: (Callable[[Any], Any]) -> Callable[[Any], Any] + @functools.wraps(func) + def wrapper_use_tracer(self, *args, **kwargs): + # type: (Any) -> Any + passed_in_parent = kwargs.pop("parent_span", None) + orig_wrapped_span = tracing_context.current_span.get() + wrapper_class = settings.tracing_implementation() + original_span_instance = None + if wrapper_class is not None: + original_span_instance = wrapper_class.get_current_span() + parent_span = common.get_parent_span(passed_in_parent) + ans = None + if common.should_use_trace(parent_span): + common.set_span_contexts(parent_span) + name = self.__class__.__name__ + "." + func.__name__ + child = parent_span.span(name=name) + child.start() + common.set_span_contexts(child) + ans = func(self, *args, **kwargs) + child.finish() + common.set_span_contexts(parent_span) + if orig_wrapped_span is None and passed_in_parent is None: + parent_span.finish() + common.set_span_contexts(orig_wrapped_span, span_instance=original_span_instance) + else: + ans = func(self, *args, **kwargs) + return ans + + return wrapper_use_tracer diff --git a/sdk/core/azure-core/azure/core/tracing/decorator_async.py b/sdk/core/azure-core/azure/core/tracing/decorator_async.py new file mode 100644 index 000000000000..6e88d235bc9b --- /dev/null +++ b/sdk/core/azure-core/azure/core/tracing/decorator_async.py @@ -0,0 +1,64 @@ +# -------------------------------------------------------------------------- +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# +# The MIT License (MIT) +# +# 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. +# +# -------------------------------------------------------------------------- +"""The decorator to apply if you want the given function traced.""" + +import functools + +import azure.core.tracing.common as common +from azure.core.settings import settings +from azure.core.tracing.context import tracing_context + + +def distributed_trace_async(func): + # type: (Callable[[Any], Any]) -> Callable[[Any], Any] + @functools.wraps(func) + async def wrapper_use_tracer(self, *args, **kwargs): + # type: (Any) -> Any + passed_in_parent = kwargs.pop("parent_span", None) + orig_wrapped_span = tracing_context.current_span.get() + wrapper_class = settings.tracing_implementation() + original_span_instance = None + if wrapper_class is not None: + original_span_instance = wrapper_class.get_current_span() + parent_span = common.get_parent_span(passed_in_parent) + ans = None + if common.should_use_trace(parent_span): + common.set_span_contexts(parent_span) + name = self.__class__.__name__ + "." + func.__name__ + child = parent_span.span(name=name) + child.start() + common.set_span_contexts(child) + ans = await func(self, *args, **kwargs) + child.finish() + common.set_span_contexts(parent_span) + if orig_wrapped_span is None and passed_in_parent is None: + parent_span.finish() + common.set_span_contexts(orig_wrapped_span, span_instance=original_span_instance) + else: + ans = await func(self, *args, **kwargs) + return ans + + return wrapper_use_tracer diff --git a/sdk/core/azure-core/azure/core/tracing/ext/opencensus_span.py b/sdk/core/azure-core/azure/core/tracing/ext/opencensus_span.py index 49e3409500d1..bab7dce34f32 100644 --- a/sdk/core/azure-core/azure/core/tracing/ext/opencensus_span.py +++ b/sdk/core/azure-core/azure/core/tracing/ext/opencensus_span.py @@ -2,7 +2,9 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -from opencensus.trace import tracer as tracer_module, Span, execution_context +"""Implements azure.core.tracing.AbstractSpan to wrap opencensus spans.""" + +from opencensus.trace import Span, execution_context from opencensus.trace.link import Link from opencensus.trace.propagation import trace_context_http_header_format @@ -29,14 +31,9 @@ def __init__(self, span=None, name="span"): :param name: The name of the OpenCensus span to create if a new span is needed :type name: str """ - tracer = self.get_current_tracer() if not span: - current_span = self.get_current_span() - span = tracer.span(name=name) - # The logic is needed until opencensus fixes their bug - # https://github.com/census-instrumentation/opencensus-python/issues/466 - if current_span and span not in current_span.children: - current_span._child_spans.append(span) + tracer = self.get_current_tracer() + span = tracer.span(name=name) # type: Span self._span_instance = span @property @@ -97,7 +94,7 @@ def link(cls, headers): # type: (Dict[str, str]) -> None """ Given a dictionary, extracts the context and links the context to the current tracer. - + :param headers: A key value pair dictionary :type headers: dict """ diff --git a/sdk/core/azure-core/tests/azure_core_asynctests/test_tracing_decorator_async.py b/sdk/core/azure-core/tests/azure_core_asynctests/test_tracing_decorator_async.py new file mode 100644 index 000000000000..a1d6e19e1b45 --- /dev/null +++ b/sdk/core/azure-core/tests/azure_core_asynctests/test_tracing_decorator_async.py @@ -0,0 +1,150 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""The tests for decorators_async.py""" + +try: + from unittest import mock +except ImportError: + import mock + +import sys +from azure.core import HttpRequest +from azure.core.pipeline import Pipeline, PipelineResponse +from azure.core.pipeline.policies import HTTPPolicy +from azure.core.pipeline.transport import HttpTransport +from azure.core.tracing.context import tracing_context +from azure.core.tracing.decorator import distributed_trace +from azure.core.tracing.decorator_async import distributed_trace_async +from azure.core.tracing.ext.opencensus_span import OpenCensusSpan +from opencensus.trace import tracer as tracer_module +from opencensus.trace.samplers import AlwaysOnSampler +from tracing_common import ContextHelper, MockExporter +import pytest +import time + + +class MockClient: + @distributed_trace + def __init__(self, policies=None, assert_current_span=False): + time.sleep(0.001) + self.request = HttpRequest("GET", "https://bing.com") + if policies is None: + policies = [] + policies.append(mock.Mock(spec=HTTPPolicy, send=self.verify_request)) + self.policies = policies + self.transport = mock.Mock(spec=HttpTransport) + self.pipeline = Pipeline(self.transport, policies=policies) + + self.expected_response = mock.Mock(spec=PipelineResponse) + self.assert_current_span = assert_current_span + + def verify_request(self, request): + current_span = tracing_context.current_span.get() + if self.assert_current_span: + assert current_span is not None + return self.expected_response + + @distributed_trace_async + async def make_request(self, numb_times, **kwargs): + time.sleep(0.001) + if numb_times < 1: + return None + response = self.pipeline.run(self.request, **kwargs) + await self.get_foo() + await self.make_request(numb_times - 1, **kwargs) + return response + + @distributed_trace_async + async def get_foo(self): + time.sleep(0.001) + return 5 + + +@pytest.mark.asyncio +async def test_with_nothing_imported(): + with ContextHelper(): + opencensus = sys.modules["opencensus"] + del sys.modules["opencensus"] + client = MockClient(assert_current_span=True) + with pytest.raises(AssertionError): + await client.make_request(3) + sys.modules["opencensus"] = opencensus + + +@pytest.mark.asyncio +async def test_with_opencensus_imported_but_not_used(): + with ContextHelper(): + client = MockClient(assert_current_span=True) + await client.make_request(3) + + +@pytest.mark.asyncio +async def test_with_opencencus_used(): + with ContextHelper(): + exporter = MockExporter() + trace = tracer_module.Tracer(sampler=AlwaysOnSampler(), exporter=exporter) + parent = trace.start_span(name="OverAll") + client = MockClient(policies=[]) + await client.get_foo(parent_span=parent) + await client.get_foo() + parent.finish() + trace.finish() + exporter.build_tree() + parent = exporter.root + assert len(parent.children) == 3 + assert parent.children[0].span_data.name == "MockClient.__init__" + assert not parent.children[0].children + assert parent.children[1].span_data.name == "MockClient.get_foo" + assert not parent.children[1].children + + +@pytest.mark.parametrize("value", [None, "opencensus"]) +@pytest.mark.asyncio +async def test_span_with_opencensus_complicated(value): + with ContextHelper(tracer_to_use=value): + exporter = MockExporter() + trace = tracer_module.Tracer(sampler=AlwaysOnSampler(), exporter=exporter) + with trace.start_span(name="OverAll") as parent: + client = MockClient() + await client.make_request(2) + with trace.span("child") as child: + await client.make_request(2, parent_span=parent) + assert OpenCensusSpan.get_current_span() == child + await client.make_request(2) + trace.finish() + exporter.build_tree() + parent = exporter.root + assert len(parent.children) == 4 + assert parent.children[0].span_data.name == "MockClient.__init__" + assert parent.children[1].span_data.name == "MockClient.make_request" + assert parent.children[1].children[0].span_data.name == "MockClient.get_foo" + assert parent.children[1].children[1].span_data.name == "MockClient.make_request" + assert parent.children[2].span_data.name == "child" + assert parent.children[2].children[0].span_data.name == "MockClient.make_request" + assert parent.children[3].span_data.name == "MockClient.make_request" + assert parent.children[3].children[0].span_data.name == "MockClient.get_foo" + assert parent.children[3].children[1].span_data.name == "MockClient.make_request" + children = parent.children[1].children + assert len(children) == 2 + + +@pytest.mark.asyncio +async def test_should_only_propagate(): + with ContextHelper(should_only_propagate=True): + exporter = MockExporter() + trace = tracer_module.Tracer(sampler=AlwaysOnSampler(), exporter=exporter) + with trace.start_span(name="OverAll") as parent: + client = MockClient() + await client.make_request(2) + with trace.span("child") as child: + await client.make_request(2, parent_span=parent) + assert OpenCensusSpan.get_current_span() == child + await client.make_request(2) + trace.finish() + exporter.build_tree() + parent = exporter.root + assert len(parent.children) == 1 + assert parent.children[0].span_data.name == "child" + assert not parent.children[0].children diff --git a/sdk/core/azure-core/tests/test_settings.py b/sdk/core/azure-core/tests/test_settings.py index a735ff2bfe37..11e9dc48d523 100644 --- a/sdk/core/azure-core/tests/test_settings.py +++ b/sdk/core/azure-core/tests/test_settings.py @@ -25,7 +25,7 @@ # -------------------------------------------------------------------------- import logging import os - +import sys import pytest # module under test @@ -74,7 +74,7 @@ def test_user_set(self): ps = m.PrioritizedSetting("foo") ps.set_value(40) assert ps() == 40 - + def test_user_unset(self): ps = m.PrioritizedSetting("foo", default=2) ps.set_value(40) @@ -101,9 +101,7 @@ def test_precedence(self): assert ps() == 10 # 1. system value - ps = m.PrioritizedSetting( - "foo", env_var="AZURE_FOO", convert=int, default=10, system_hook=lambda: 20 - ) + ps = m.PrioritizedSetting("foo", env_var="AZURE_FOO", convert=int, default=10, system_hook=lambda: 20) assert ps() == 20 # 2. environment variable @@ -137,11 +135,11 @@ class FakeSettings(object): class TestConverters(object): - @pytest.mark.parametrize("value", ["Yes", "YES", "yes", "1", "ON", "on"]) + @pytest.mark.parametrize("value", ["Yes", "YES", "yes", "1", "ON", "on", "true", "True", True]) def test_convert_bool(self, value): assert m.convert_bool(value) - @pytest.mark.parametrize("value", ["No", "NO", "no", "0", "OFF", "off"]) + @pytest.mark.parametrize("value", ["No", "NO", "no", "0", "OFF", "off", "false", "False", False]) def test_convert_bool_false(self, value): assert not m.convert_bool(value) @@ -169,6 +167,18 @@ def test_convert_logging_bad(self): with pytest.raises(ValueError): m.convert_logging("junk") + def test_convert_implementation(self): + if "opencensus" in sys.modules: + del sys.modules["opencensus"] + assert m.convert_tracing_impl(None) is None + assert m.convert_tracing_impl("opencensus") is not None + import opencensus + + assert m.convert_tracing_impl(None) is not None + assert m.convert_tracing_impl("opencensus") is not None + with pytest.raises(ValueError): + m.convert_tracing_impl("does not exist!!") + _standard_settings = ["log_level", "tracing_enabled"] @@ -205,10 +215,22 @@ def test_config(self): def test_defaults(self): val = m.settings.defaults - assert isinstance(val, tuple) - assert val == m.settings.config(log_level=20, tracing_enabled=False) + # assert isinstance(val, tuple) + defaults = m.settings.config( + log_level=20, tracing_enabled=False, tracing_implementation=None, tracing_should_only_propagate=False + ) + assert val.log_level == defaults.log_level + assert val.tracing_enabled == defaults.tracing_enabled + assert val.tracing_implementation == defaults.tracing_implementation + assert val.tracing_should_only_propagate == defaults.tracing_should_only_propagate os.environ["AZURE_LOG_LEVEL"] = "debug" - assert val == m.settings.config(log_level=20, tracing_enabled=False) + defaults = m.settings.config( + log_level=20, tracing_enabled=False, tracing_implementation=None, tracing_should_only_propagate=False + ) + assert val.log_level == defaults.log_level + assert val.tracing_enabled == defaults.tracing_enabled + assert val.tracing_implementation == defaults.tracing_implementation + assert val.tracing_should_only_propagate == defaults.tracing_should_only_propagate del os.environ["AZURE_LOG_LEVEL"] def test_current(self): diff --git a/sdk/core/azure-core/tests/test_tracing_context.py b/sdk/core/azure-core/tests/test_tracing_context.py index 2b8a32762e21..a537d4200179 100644 --- a/sdk/core/azure-core/tests/test_tracing_context.py +++ b/sdk/core/azure-core/tests/test_tracing_context.py @@ -7,45 +7,62 @@ from unittest import mock except ImportError: import mock -from azure.core.tracing import tracing_context + +from azure.core.tracing.context import tracing_context from azure.core.tracing import AbstractSpan +from azure.core.settings import settings +import os + + +class ContextHelper(object): + def __init__(self, environ={}, tracer_to_use=None): + self.orig_sdk_context_span = tracing_context.current_span.get() + self.os_env = mock.patch.dict(os.environ, environ) + self.tracer_to_use = tracer_to_use + + def __enter__(self): + self.orig_sdk_context_span = tracing_context.current_span.get() + settings.tracing_implementation.set_value(self.tracer_to_use) + self.os_env.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + tracing_context.current_span.set(self.orig_sdk_context_span) + settings.tracing_implementation.unset_value() + self.os_env.stop() class TestContext(unittest.TestCase): def test_get_context_class(self): - slot = tracing_context._get_context_class("temp", 1) - assert slot.get() == 1 - slot.set(2) - assert slot.get() == 2 + with ContextHelper(): + slot = tracing_context._get_context_class("temp", 1) + assert slot.get() == 1 + slot.set(2) + assert slot.get() == 2 def test_current_span(self): - assert tracing_context.current_span.get() is None - val = mock.Mock(spec=AbstractSpan) - tracing_context.current_span.set(val) - assert tracing_context.current_span.get() == val - - def test_tracing_impl(self): - assert tracing_context.tracing_impl.get() is None - val = AbstractSpan - tracing_context.tracing_impl.set(val) - assert tracing_context.tracing_impl.get() == val + with ContextHelper(): + assert tracing_context.current_span.get() is None + val = mock.Mock(spec=AbstractSpan) + tracing_context.current_span.set(val) + assert tracing_context.current_span.get() == val def test_with_current_context(self): - from threading import Thread - mock_impl = AbstractSpan - tracing_context.tracing_impl.set(mock_impl) - current_span = mock.Mock(spec=AbstractSpan) - tracing_context.current_span.set(current_span) + with ContextHelper(tracer_to_use=mock.Mock(AbstractSpan)): + from threading import Thread - def work(): - span = tracing_context.current_span.get() - assert span == current_span - setattr(span, "in_worker", True) + current_span = mock.Mock(spec=AbstractSpan) + tracing_context.current_span.set(current_span) + + def work(): + span = tracing_context.current_span.get() + assert span == current_span + setattr(span, "in_worker", True) - thread = Thread(target=tracing_context.with_current_context(work)) - thread.start() - thread.join() + thread = Thread(target=tracing_context.with_current_context(work)) + thread.start() + thread.join() - span = tracing_context.current_span.get() - assert span == current_span - assert getattr(span, "in_worker", False) + span = tracing_context.current_span.get() + assert span == current_span + assert getattr(span, "in_worker", False) diff --git a/sdk/core/azure-core/tests/test_tracing_decorator.py b/sdk/core/azure-core/tests/test_tracing_decorator.py new file mode 100644 index 000000000000..7b27ee392a0f --- /dev/null +++ b/sdk/core/azure-core/tests/test_tracing_decorator.py @@ -0,0 +1,200 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""The tests for decorators.py and common.py""" + +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +import sys +import os +from azure.core import HttpRequest +from azure.core.pipeline import Pipeline, PipelineResponse +from azure.core.pipeline.policies import HTTPPolicy +from azure.core.pipeline.transport import HttpTransport +from azure.core.tracing import common +from azure.core.tracing.context import tracing_context +from azure.core.tracing.decorator import distributed_trace +from azure.core.settings import settings +from azure.core.tracing.ext.opencensus_span import OpenCensusSpan +from opencensus.trace import tracer as tracer_module +from opencensus.trace.samplers import AlwaysOnSampler +from tracing_common import ContextHelper, MockExporter +import time +import pytest + +try: + from typing import TYPE_CHECKING +except ImportError: + TYPE_CHECKING = False + +if TYPE_CHECKING: + from typing import List + + +class MockClient: + @distributed_trace + def __init__(self, policies=None, assert_current_span=False): + time.sleep(0.001) + self.request = HttpRequest("GET", "https://bing.com") + if policies is None: + policies = [] + policies.append(mock.Mock(spec=HTTPPolicy, send=self.verify_request)) + self.policies = policies + self.transport = mock.Mock(spec=HttpTransport) + self.pipeline = Pipeline(self.transport, policies=policies) + + self.expected_response = mock.Mock(spec=PipelineResponse) + self.assert_current_span = assert_current_span + + def verify_request(self, request): + current_span = tracing_context.current_span.get() + if self.assert_current_span: + assert current_span is not None + return self.expected_response + + @distributed_trace + def make_request(self, numb_times, **kwargs): + time.sleep(0.001) + if numb_times < 1: + return None + response = self.pipeline.run(self.request, **kwargs) + self.get_foo() + self.make_request(numb_times - 1, **kwargs) + return response + + @distributed_trace + def get_foo(self): + time.sleep(0.001) + return 5 + +class TestCommon(object): + def test_set_span_context(self): + with ContextHelper(environ={"AZURE_SDK_TRACING_IMPLEMENTATION": "opencensus"}): + wrapper = settings.tracing_implementation() + assert wrapper is OpenCensusSpan + assert tracing_context.current_span.get() is None + assert wrapper.get_current_span() is None + parent = OpenCensusSpan() + common.set_span_contexts(parent) + assert parent.span_instance == wrapper.get_current_span() + assert tracing_context.current_span.get() == parent + + def test_get_parent_span(self): + with ContextHelper(): + opencensus = sys.modules["opencensus"] + del sys.modules["opencensus"] + + parent = common.get_parent_span(None) + assert parent is None + + sys.modules["opencensus"] = opencensus + parent = common.get_parent_span(None) + assert parent.span_instance.name == "azure-sdk-for-python-first_parent_span" + + tracer = tracer_module.Tracer(sampler=AlwaysOnSampler()) + parent = common.get_parent_span(None) + assert parent.span_instance.name == "azure-sdk-for-python-first_parent_span" + parent.finish() + + some_span = tracer.start_span(name="some_span") + new_parent = common.get_parent_span(None) + assert new_parent.span_instance.name == "some_span" + some_span.finish() + + should_be_old_parent = common.get_parent_span(parent.span_instance) + assert should_be_old_parent.span_instance == parent.span_instance + + def test_should_use_trace(self): + with ContextHelper(environ={"AZURE_TRACING_ONLY_PROPAGATE": "yes"}): + parent_span = OpenCensusSpan() + assert common.should_use_trace(parent_span) == False + assert common.should_use_trace(None) == False + parent_span = OpenCensusSpan() + assert common.should_use_trace(parent_span) + assert common.should_use_trace(None) == False + + +class TestDecorator(object): + def test_with_nothing_imported(self): + with ContextHelper(): + opencensus = sys.modules["opencensus"] + del sys.modules["opencensus"] + client = MockClient(assert_current_span=True) + with pytest.raises(AssertionError): + client.make_request(3) + sys.modules["opencensus"] = opencensus + + def test_with_opencensus_imported_but_not_used(self): + with ContextHelper(): + client = MockClient(assert_current_span=True) + client.make_request(3) + + def test_with_opencencus_used(self): + with ContextHelper(): + exporter = MockExporter() + trace = tracer_module.Tracer(sampler=AlwaysOnSampler(), exporter=exporter) + parent = trace.start_span(name="OverAll") + client = MockClient(policies=[]) + client.get_foo(parent_span=parent) + client.get_foo() + parent.finish() + trace.finish() + exporter.build_tree() + parent = exporter.root + assert len(parent.children) == 3 + assert parent.children[0].span_data.name == "MockClient.__init__" + assert not parent.children[0].children + assert parent.children[1].span_data.name == "MockClient.get_foo" + assert not parent.children[1].children + + @pytest.mark.parametrize("value", ["opencensus", None]) + def test_span_with_opencensus_complicated(self, value): + with ContextHelper(tracer_to_use=value) as ctx: + exporter = MockExporter() + trace = tracer_module.Tracer(sampler=AlwaysOnSampler(), exporter=exporter) + with trace.start_span(name="OverAll") as parent: + client = MockClient() + client.make_request(2) + with trace.span("child") as child: + client.make_request(2, parent_span=parent) + assert OpenCensusSpan.get_current_span() == child + client.make_request(2) + trace.finish() + exporter.build_tree() + parent = exporter.root + assert len(parent.children) == 4 + assert parent.children[0].span_data.name == "MockClient.__init__" + assert parent.children[1].span_data.name == "MockClient.make_request" + assert parent.children[1].children[0].span_data.name == "MockClient.get_foo" + assert parent.children[1].children[1].span_data.name == "MockClient.make_request" + assert parent.children[2].span_data.name == "child" + assert parent.children[2].children[0].span_data.name == "MockClient.make_request" + assert parent.children[3].span_data.name == "MockClient.make_request" + assert parent.children[3].children[0].span_data.name == "MockClient.get_foo" + assert parent.children[3].children[1].span_data.name == "MockClient.make_request" + children = parent.children[1].children + assert len(children) == 2 + + def test_should_only_propagate(self): + with ContextHelper(should_only_propagate=True): + exporter = MockExporter() + trace = tracer_module.Tracer(sampler=AlwaysOnSampler(), exporter=exporter) + with trace.start_span(name="OverAll") as parent: + client = MockClient() + client.make_request(2) + with trace.span("child") as child: + client.make_request(2, parent_span=parent) + assert OpenCensusSpan.get_current_span() == child + client.make_request(2) + trace.finish() + exporter.build_tree() + parent = exporter.root + assert len(parent.children) == 1 + assert parent.children[0].span_data.name == "child" + assert not parent.children[0].children diff --git a/sdk/core/azure-core/tests/test_tracing_implementations.py b/sdk/core/azure-core/tests/test_tracing_implementations.py index 02b0d826b8e1..5793685a9a1e 100644 --- a/sdk/core/azure-core/tests/test_tracing_implementations.py +++ b/sdk/core/azure-core/tests/test_tracing_implementations.py @@ -2,6 +2,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +"""The tests for opencensus_span.py""" + import unittest try: @@ -12,10 +14,46 @@ from azure.core.tracing.ext.opencensus_span import OpenCensusSpan from opencensus.trace import tracer as tracer_module from opencensus.trace.samplers import AlwaysOnSampler -from opencensus.ext.azure.trace_exporter import AzureExporter +from opencensus.trace.base_exporter import Exporter +from opencensus.common.utils import timestamp_to_microseconds import os +class Node: + def __init__(self, span_data): + self.span_data = span_data # type: SpanData + self.parent = None + self.children = [] + + +class MockExporter(Exporter): + def __init__(self): + self.root = None + self._all_nodes = [] + + def export(self, span_datas): + # type: (List[SpanData]) -> None + sp = span_datas[0] # type: SpanData + node = Node(sp) + if not node.span_data.parent_span_id: + self.root = node + self._all_nodes.append(node) + + def build_tree(self): + parent_dict = {} + for node in self._all_nodes: + parent_span_id = node.span_data.parent_span_id + if parent_span_id not in parent_dict: + parent_dict[parent_span_id] = [] + parent_dict[parent_span_id].append(node) + + for node in self._all_nodes: + if node.span_data.span_id in parent_dict: + node.children = sorted( + parent_dict[node.span_data.span_id], key=lambda x: timestamp_to_microseconds(x.span_data.start_time) + ) + + class ContextHelper(object): def __init__(self, environ={}): self.orig_tracer = OpenCensusSpan.get_current_tracer() @@ -64,8 +102,9 @@ def test_no_span_but_in_trace(self): tracer.finish() def test_span(self): + exporter = MockExporter() with ContextHelper() as ctx: - tracer = tracer_module.Tracer(sampler=AlwaysOnSampler()) + tracer = tracer_module.Tracer(sampler=AlwaysOnSampler(), exporter=exporter) assert OpenCensusSpan.get_current_tracer() is tracer wrapped_class = OpenCensusSpan() assert tracer.current_span() == wrapped_class.span_instance @@ -74,8 +113,11 @@ def test_span(self): assert child.span_instance.name == "span" assert child.span_instance.context_tracer.trace_id == tracer.span_context.trace_id assert child.span_instance.parent_span is wrapped_class.span_instance - assert len(wrapped_class.span_instance.children) == 1 - assert wrapped_class.span_instance.children[0] == child.span_instance + tracer.finish() + exporter.build_tree() + parent = exporter.root + assert len(parent.children) == 1 + assert parent.children[0].span_data.span_id == child.span_instance.span_id def test_start_finish(self): with ContextHelper() as ctx: diff --git a/sdk/core/azure-core/tests/tracing_common.py b/sdk/core/azure-core/tests/tracing_common.py new file mode 100644 index 000000000000..7967640180cf --- /dev/null +++ b/sdk/core/azure-core/tests/tracing_common.py @@ -0,0 +1,88 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""Code shared between the async and the sync test_decorator files.""" + +import sys +import os +from azure.core import HttpRequest +from azure.core.pipeline import Pipeline, PipelineResponse +from azure.core.pipeline.policies import HTTPPolicy +from azure.core.pipeline.transport import HttpTransport +from azure.core.tracing import common +from azure.core.tracing.context import tracing_context +from azure.core.settings import settings +from azure.core.tracing.ext.opencensus_span import OpenCensusSpan +from opencensus.trace import tracer as tracer_module +from opencensus.trace.span_data import SpanData +from opencensus.trace.samplers import AlwaysOnSampler +from opencensus.trace.base_exporter import Exporter + +try: + from unittest import mock +except ImportError: + import mock + +class ContextHelper(object): + def __init__(self, environ={}, tracer_to_use=None, should_only_propagate=None): + self.orig_tracer = OpenCensusSpan.get_current_tracer() + self.orig_current_span = OpenCensusSpan.get_current_span() + self.orig_sdk_context_span = tracing_context.current_span.get() + self.os_env = mock.patch.dict(os.environ, environ) + self.tracer_to_use = tracer_to_use + self.should_only_propagate = should_only_propagate + + def __enter__(self): + self.orig_tracer = OpenCensusSpan.get_current_tracer() + self.orig_current_span = OpenCensusSpan.get_current_span() + self.orig_sdk_context_span = tracing_context.current_span.get() + if self.tracer_to_use is not None: + settings.tracing_implementation.set_value(self.tracer_to_use) + if self.should_only_propagate is not None: + settings.tracing_should_only_propagate.set_value(self.should_only_propagate) + self.os_env.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + OpenCensusSpan.set_current_tracer(self.orig_tracer) + OpenCensusSpan.set_current_span(self.orig_current_span) + tracing_context.current_span.set(self.orig_sdk_context_span) + settings.tracing_implementation.unset_value() + settings.tracing_should_only_propagate.unset_value() + self.os_env.stop() + + +class Node: + def __init__(self, span_data): + self.span_data = span_data # type: SpanData + self.parent = None + self.children = [] + + +class MockExporter(Exporter): + def __init__(self): + self.root = None + self._all_nodes = [] + + def export(self, span_datas): + # type: (List[SpanData]) -> None + sp = span_datas[0] # type: SpanData + node = Node(sp) + if not node.span_data.parent_span_id: + self.root = node + self._all_nodes.append(node) + + def build_tree(self): + parent_dict = {} + for node in self._all_nodes: + parent_span_id = node.span_data.parent_span_id + if parent_span_id not in parent_dict: + parent_dict[parent_span_id] = [] + parent_dict[parent_span_id].append(node) + + for node in self._all_nodes: + if node.span_data.span_id in parent_dict: + node.children = sorted( + parent_dict[node.span_data.span_id], key=lambda x: x.span_data.start_time + )