From 5ce416484d21dc9ac4c0f62892de62e4c37ca9fd Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Tue, 8 Oct 2019 13:28:15 -0700 Subject: [PATCH 01/28] Introducing change_context to replace set_current_span --- .../tracing/ext/opencensus_span/__init__.py | 14 ++++++++++++++ .../azure/core/tracing/abstract_span.py | 6 ++++++ .../azure-core/azure/core/tracing/common.py | 17 ++++++++++++----- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/sdk/core/azure-core-tracing-opencensus/azure/core/tracing/ext/opencensus_span/__init__.py b/sdk/core/azure-core-tracing-opencensus/azure/core/tracing/ext/opencensus_span/__init__.py index 15489fd6a02a..a25ec35b905a 100644 --- a/sdk/core/azure-core-tracing-opencensus/azure/core/tracing/ext/opencensus_span/__init__.py +++ b/sdk/core/azure-core-tracing-opencensus/azure/core/tracing/ext/opencensus_span/__init__.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. # ------------------------------------ """Implements azure.core.tracing.AbstractSpan to wrap opencensus spans.""" +import warnings from opencensus.trace import Span, execution_context from opencensus.trace.tracer import Tracer @@ -226,8 +227,21 @@ def set_current_span(cls, span): :param span: The span to set the current span as :type span: :class: opencensus.trace.Span """ + warnings.warn("set_current_span is deprecated, use change_context instead", DeprecationWarning) return execution_context.set_current_span(span) + @classmethod + def change_context(cls, span): + # type: (Span) -> ContextManager + """Change the context for the life of this context manager. + """ + original_span = cls.get_current_span() + try: + execution_context.set_current_span(span) + yield + finally: + execution_context.set_current_span(original_span) + @classmethod def set_current_tracer(cls, tracer): # type: (Tracer) -> None 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 9c1c5e081dd6..0053cd37c3c4 100644 --- a/sdk/core/azure-core/azure/core/tracing/abstract_span.py +++ b/sdk/core/azure-core/azure/core/tracing/abstract_span.py @@ -162,6 +162,12 @@ def set_current_tracer(cls, tracer): Set the given tracer as the current tracer in the execution context. """ + @classmethod + def change_context(cls, span): + # type: (Span) -> ContextManager + """Change the context for the life of this context manager. + """ + @classmethod def with_current_context(cls, func): # type: (Callable) -> Callable diff --git a/sdk/core/azure-core/azure/core/tracing/common.py b/sdk/core/azure-core/azure/core/tracing/common.py index 6b2f2c1398b5..a2658e6ce27b 100644 --- a/sdk/core/azure-core/azure/core/tracing/common.py +++ b/sdk/core/azure-core/azure/core/tracing/common.py @@ -25,6 +25,7 @@ # -------------------------------------------------------------------------- """Common functions shared by both the sync and the async decorators.""" from contextlib import contextmanager +import warnings from azure.core.tracing.abstract_span import AbstractSpan from azure.core.settings import settings @@ -74,12 +75,18 @@ def change_context(span): if span_impl_type is None or span is None: yield else: - original_span = span_impl_type.get_current_span() try: - span_impl_type.set_current_span(span) - yield - finally: - span_impl_type.set_current_span(original_span) + with span_impl_type.change_context(span): + yield + except AttributeError: + # This plugin does not support "change_context" + warnings.warn('Your tracing plugin should be updated to support "change_context"', DeprecationWarning) + original_span = span_impl_type.get_current_span() + try: + span_impl_type.set_current_span(span) + yield + finally: + span_impl_type.set_current_span(original_span) def with_current_context(func): From cf76954fa8f341e331a373e17e01e42c69266507 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Tue, 8 Oct 2019 13:28:58 -0700 Subject: [PATCH 02/28] OpenTelemetry plugin:first pass --- .../HISTORY.md | 10 + .../MANIFEST.in | 8 + .../README.md | 61 ++++ .../azure/__init__.py | 1 + .../azure/core/__init__.py | 1 + .../azure/core/tracing/__init__.py | 1 + .../azure/core/tracing/ext/__init__.py | 1 + .../ext/opentelemetry_span/__init__.py | 275 ++++++++++++++++++ .../dev_requirements.txt | 2 + .../sdk_packaging.toml | 2 + .../setup.cfg | 2 + .../azure-core-tracing-opentelemetry/setup.py | 66 +++++ 12 files changed, 430 insertions(+) create mode 100644 sdk/core/azure-core-tracing-opentelemetry/HISTORY.md create mode 100644 sdk/core/azure-core-tracing-opentelemetry/MANIFEST.in create mode 100644 sdk/core/azure-core-tracing-opentelemetry/README.md create mode 100644 sdk/core/azure-core-tracing-opentelemetry/azure/__init__.py create mode 100644 sdk/core/azure-core-tracing-opentelemetry/azure/core/__init__.py create mode 100644 sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/__init__.py create mode 100644 sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/__init__.py create mode 100644 sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py create mode 100644 sdk/core/azure-core-tracing-opentelemetry/dev_requirements.txt create mode 100644 sdk/core/azure-core-tracing-opentelemetry/sdk_packaging.toml create mode 100644 sdk/core/azure-core-tracing-opentelemetry/setup.cfg create mode 100644 sdk/core/azure-core-tracing-opentelemetry/setup.py diff --git a/sdk/core/azure-core-tracing-opentelemetry/HISTORY.md b/sdk/core/azure-core-tracing-opentelemetry/HISTORY.md new file mode 100644 index 000000000000..84eccb33312a --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/HISTORY.md @@ -0,0 +1,10 @@ + +# Release History + +------------------- + +## 2019-10-07 Version 1.0.0b4 + +### Features + +- Opencensus implementation of azure-core tracing protocol \ No newline at end of file diff --git a/sdk/core/azure-core-tracing-opentelemetry/MANIFEST.in b/sdk/core/azure-core-tracing-opentelemetry/MANIFEST.in new file mode 100644 index 000000000000..53cf3b003aae --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/MANIFEST.in @@ -0,0 +1,8 @@ +recursive-include tests *.py *.yaml +include *.md +include azure/__init__.py +include azure/core/__init__.py +include azure/core/tracing/__init__.py +include azure/core/tracing/ext/__init__.py +recursive-include examples *.py + diff --git a/sdk/core/azure-core-tracing-opentelemetry/README.md b/sdk/core/azure-core-tracing-opentelemetry/README.md new file mode 100644 index 000000000000..e0400c64ee52 --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/README.md @@ -0,0 +1,61 @@ + + +# Azure Core Tracing OpenCensus client library for Python + +## Getting started + +Install the opencensus python for Python with [pip](https://pypi.org/project/pip/): + +```bash +pip install azure-core-tracing-opencensus --pre +``` + +Now you can use opencensus for Python as usual with any SDKs that is compatible +with azure-core tracing. This includes (not exhaustive list), azure-storage-blob, azure-keyvault-secrets, azure-eventhub, etc. + +## Key concepts + +* You don't need to pass any context, SDK will get it for you +* The opencensus threading plugin is installed with this package + +## Examples + +There is no explicit context to pass, you just create your usual opencensus and tracer and +call any SDK code that is compatible with azure-core tracing. This is an example +using Azure Monitor exporter, but you can use any exporter (Zipkin, etc.). + +```python +from opencensus.ext.azure.trace_exporter import AzureExporter + +from opencensus.trace.tracer import Tracer +from opencensus.trace.samplers import AlwaysOnSampler + +from azure.storage.blob import BlobServiceClient + +exporter = AzureExporter( + instrumentation_key="uuid of the instrumentation key (see your Azure Monitor account)" +) + +tracer = Tracer(exporter=exporter, sampler=AlwaysOnSampler()) +with tracer.span(name="MyApplication") as span: + client = BlobServiceClient.from_connection_string('connectionstring') + client.delete_container('mycontainer') # Call will be traced +``` + + +## Troubleshooting + +This client raises exceptions defined in [Azure Core](https://github.com/Azure/azure-sdk-for-python/blob/master/sdk/core/azure-core/docs/exceptions.md). + + +## Next steps + +More documentation on OpenCensus configuration can be found on the [OpenCensus website](https://opencensus.io) + + +## Contributing +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/sdk/core/azure-core-tracing-opentelemetry/azure/__init__.py b/sdk/core/azure-core-tracing-opentelemetry/azure/__init__.py new file mode 100644 index 000000000000..0d1f7edf5dc6 --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/azure/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore diff --git a/sdk/core/azure-core-tracing-opentelemetry/azure/core/__init__.py b/sdk/core/azure-core-tracing-opentelemetry/azure/core/__init__.py new file mode 100644 index 000000000000..0d1f7edf5dc6 --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/azure/core/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore diff --git a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/__init__.py b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/__init__.py new file mode 100644 index 000000000000..0d1f7edf5dc6 --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore diff --git a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/__init__.py b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/__init__.py new file mode 100644 index 000000000000..0d1f7edf5dc6 --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore diff --git a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py new file mode 100644 index 000000000000..5cf9c8583150 --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py @@ -0,0 +1,275 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""Implements azure.core.tracing.AbstractSpan to wrap OpenTelemetry spans.""" + +from opentelemetry.trace import Span, Tracer, SpanKind as OpenTelemetrySpanKind, tracer +from opentelemetry.context import Context +from opentelemetry.propagators import extract, inject + +from azure.core.tracing import SpanKind # pylint: disable=no-name-in-module + +try: + from typing import TYPE_CHECKING +except ImportError: + TYPE_CHECKING = False + +if TYPE_CHECKING: + from typing import Dict, Optional, Union, Callable + + from azure.core.pipeline.transport import HttpRequest, HttpResponse + +__version__ = "1.0.0b4" + + +def _get_headers_from_http_request_headers(headers: "Mapping[str, Any]", key: str): + """Return headers that matches this key. + + Must comply to opentelemetry.context.propagation.httptextformat.Getter: + Getter = typing.Callable[[_T, str], typing.List[str]] + """ + return [headers[key]] + + +def _set_headers_from_http_request_headers(headers: "Mapping[str, Any]", key: str, value: str): + """Set headers in the given headers dict. + + Must comply to opentelemetry.context.propagation.httptextformat.Setter: + Setter = typing.Callable[[_T, str, str], None] + """ + headers[key] = value + + +class OpenTelemetrySpan(object): + """Wraps a given OpenTelemetry Span so that it implements azure.core.tracing.AbstractSpan""" + + def __init__(self, span=None, name="span"): + # type: (Optional[Span], Optional[str]) -> None + """ + If a span is not passed in, creates a new tracer. If the instrumentation key for Azure Exporter is given, will + configure the azure exporter else will just create a new tracer. + + :param span: The OpenTelemetry span to wrap + :type span: :class: OpenTelemetry.trace.Span + :param name: The name of the OpenTelemetry span to create if a new span is needed + :type name: str + """ + tracer = self.get_current_tracer() + self._span_instance = span or tracer.create_span(name=name) + self._span_component = "component" + self._http_user_agent = "http.user_agent" + self._http_method = "http.method" + self._http_url = "http.url" + self._http_status_code = "http.status_code" + self._current_ctxt_manager = None + + @property + def span_instance(self): + # type: () -> Span + """ + :return: The OpenTelemetry span that is being wrapped. + """ + return self._span_instance + + def span(self, name="span"): + # type: (Optional[str]) -> OpenCensusSpan + """ + Create a child span for the current span and append it to the child spans list in the span instance. + :param name: Name of the child span + :type name: str + :return: The OpenCensusSpan that is wrapping the child span instance + """ + return self.__class__(name=name) + + @property + def kind(self): + # type: () -> Optional[SpanKind] + """Get the span kind of this span.""" + value = self.span_instance.kind + return ( + SpanKind.CLIENT if value == OpenCensusSpanKind.CLIENT else + SpanKind.PRODUCER if value == OpenCensusSpanKind.PRODUCER else + SpanKind.SERVER if value == OpenCensusSpanKind.SERVER else + SpanKind.CONSUMER if value == OpenCensusSpanKind.CONSUMER else + SpanKind.INTERNAL if value == OpenCensusSpanKind.INTERNAL else + SpanKind.UNSPECIFIED if value == OpenCensusSpanKind.UNSPECIFIED else + None + ) + + + @kind.setter + def kind(self, value): + # type: (SpanKind) -> None + """Set the span kind of this span.""" + kind = ( + OpenTelemetrySpanKind.CLIENT if value == SpanKind.CLIENT else + OpenTelemetrySpanKind.PRODUCER if value == SpanKind.PRODUCER else + OpenTelemetrySpanKind.SERVER if value == SpanKind.SERVER else + OpenTelemetrySpanKind.CONSUMER if value == SpanKind.CONSUMER else + OpenTelemetrySpanKind.INTERNAL if value == SpanKind.INTERNAL else + OpenTelemetrySpanKind.UNSPECIFIED if value == SpanKind.UNSPECIFIED else + None + ) + if kind is None: + raise ValueError("Kind {} is not supported in OpenTelemetry".format(value)) + self.span_instance.kind = kind + + def __enter__(self): + """Start a span.""" + self._span_instance.start() + self._current_ctxt_manager = self.get_current_tracer().use_span(self._span_instance, end_on_exit=True) + self._current_ctxt_manager.__enter__() + return self + + def __exit__(self, exception_type, exception_value, traceback): + """Finish a span.""" + if not self._current_ctxt_manager: + raise ValueError("Trying to manually exit a ctxt manager that didn't started") + self._current_ctxt_manager.__exit__(exception_type, exception_value, traceback) + + def start(self): + # type: () -> None + """Set the start time for a span.""" + self.span_instance.start() + + def finish(self): + # type: () -> None + """Set the end time for a span.""" + self.span_instance.end() + + def to_header(self): + # type: () -> Dict[str, str] + """ + Returns a dictionary with the header labels and values. + :return: A key value pair dictionary + """ + temp_headers = {} # type: Dict[str, str] + inject(self.get_current_tracer(), _set_headers_from_http_request_headers, temp_headers) + return temp_headers + + def add_attribute(self, key, value): + # type: (str, Union[str, int]) -> None + """ + Add attribute (key value pair) to the current span. + + :param key: The key of the key value pair + :type key: str + :param value: The value of the key value pair + :type value: str + """ + self.span_instance.set_attribute(key, value) + + def set_http_attributes(self, request, response=None): + # type: (HttpRequest, Optional[HttpResponse]) -> None + """ + Add correct attributes for a http client span. + + :param request: The request made + :type request: HttpRequest + :param response: The response received by the server. Is None if no response received. + :type response: HttpResponse + """ + self.kind = SpanKind.CLIENT + self.add_attribute(self._span_component, "http") + self.add_attribute(self._http_method, request.method) + self.add_attribute(self._http_url, request.url) + user_agent = request.headers.get("User-Agent") + if user_agent: + self.add_attribute(self._http_user_agent, user_agent) + if response: + self.add_attribute(self._http_status_code, response.status_code) + else: + self.add_attribute(self._http_status_code, 504) + + def get_trace_parent(self): + """Return traceparent string as defined in W3C trace context specification. + + Example: + Value = 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 + base16(version) = 00 + base16(trace-id) = 4bf92f3577b34da6a3ce929d0e0e4736 + base16(parent-id) = 00f067aa0ba902b7 + base16(trace-flags) = 01 // sampled + + :return: a traceparent string + :rtype: str + """ + return self.to_header()['traceparent'] + + @classmethod + def link(cls, traceparent): + # type: (str) -> None + """ + Links the context to the current tracer. + + :param traceparent: A complete traceparent + :type traceparent: str + """ + cls.link_from_headers({ + 'traceparent': traceparent + }) + + @classmethod + def link_from_headers(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 + """ + ctx = extract(_get_headers_from_http_request_headers, headers) + current_span = cls.get_current_span() + current_span.add_link(ctx) + + @classmethod + def get_current_span(cls): + # type: () -> Span + """ + Get the current span from the execution context. Return None otherwise. + """ + return cls.get_current_tracer().get_current_span() + + @classmethod + def get_current_tracer(cls): + # type: () -> Tracer + """ + Get the current tracer from the execution context. Return None otherwise. + """ + return tracer() + + @classmethod + def change_context(cls, span): + # type: (Span) -> ContextManager + """Change the context for the life of this context manager. + """ + return cls.get_current_tracer().use_span(span, end_on_exit=False) + + @classmethod + def set_current_span(cls, span): + # type: (Span) -> None + """Not supported by OpenTelemetry. + """ + raise NotImplementedError("set_current_span is not supported by OpenTelemetry plugin. Use ChangeContext instead.") + + @classmethod + def set_current_tracer(cls, tracer): + # type: (Tracer) -> None + """ + Set the given tracer as the current tracer in the execution context. + :param tracer: The tracer to set the current tracer as + :type tracer: :class: OpenTelemetry.trace.Tracer + """ + # Do nothing, if you're able to get two tracer with OpenTelemetry that's a surprise! + pass + + @classmethod + def with_current_context(cls, func): + # type: (Callable) -> Callable + """Passes the current spans to the new context the function will be run in. + + :param func: The function that will be run in the new context + :return: The target the pass in instead of the function + """ + return Context.with_current_context(func) diff --git a/sdk/core/azure-core-tracing-opentelemetry/dev_requirements.txt b/sdk/core/azure-core-tracing-opentelemetry/dev_requirements.txt new file mode 100644 index 000000000000..a4b74a7f9e44 --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/dev_requirements.txt @@ -0,0 +1,2 @@ +-e ../../../tools/azure-sdk-tools +../azure-core \ No newline at end of file diff --git a/sdk/core/azure-core-tracing-opentelemetry/sdk_packaging.toml b/sdk/core/azure-core-tracing-opentelemetry/sdk_packaging.toml new file mode 100644 index 000000000000..e7687fdae93b --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/sdk_packaging.toml @@ -0,0 +1,2 @@ +[packaging] +auto_update = false \ No newline at end of file diff --git a/sdk/core/azure-core-tracing-opentelemetry/setup.cfg b/sdk/core/azure-core-tracing-opentelemetry/setup.cfg new file mode 100644 index 000000000000..3c6e79cf31da --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/sdk/core/azure-core-tracing-opentelemetry/setup.py b/sdk/core/azure-core-tracing-opentelemetry/setup.py new file mode 100644 index 000000000000..589a936e288c --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/setup.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +#------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +#-------------------------------------------------------------------------- + +import re +import os.path +from io import open +from setuptools import find_packages, setup # type: ignore + +# Change the PACKAGE_NAME only to change folder and different name +PACKAGE_NAME = "azure-core-tracing-opentelemetry" +PACKAGE_PPRINT_NAME = "Azure Core OpenTelemetry plugin" + +package_folder_path = "azure/core/tracing/ext/opentelemetry_span" + +# Version extraction inspired from 'requests' +with open(os.path.join(package_folder_path, '__init__.py'), 'r') as fd: + version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', # type: ignore + fd.read(), re.MULTILINE).group(1) + +if not version: + raise RuntimeError('Cannot find version information') + +with open('README.md', encoding='utf-8') as f: + readme = f.read() +with open('HISTORY.md', encoding='utf-8') as f: + history = f.read() + +setup( + name=PACKAGE_NAME, + version=version, + description='Microsoft Azure {} Library for Python'.format(PACKAGE_PPRINT_NAME), + long_description=readme + '\n\n' + history, + long_description_content_type='text/markdown', + license='MIT License', + author='Microsoft Corporation', + author_email='azpysdkhelp@microsoft.com', + url='https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/core/azure-core-tracing-opentelemetry', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'License :: OSI Approved :: MIT License', + ], + zip_safe=False, + packages=[ + 'azure.core.tracing.ext.opentelemetry_span', + ], + install_requires=[ + 'opentelemetry-api', + 'opentelemetry-ext-azure-monitor', + 'azure-core<2.0.0,>=1.0.0b4', + ], + extras_require={ + ":python_version<'3.5'": ['typing'], + } +) From bc3171595851a1423a5452f44324f548fdacc204 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Tue, 8 Oct 2019 14:26:58 -0700 Subject: [PATCH 03/28] Plug OpenTelemetry in azure-core --- sdk/core/azure-core/azure/core/settings.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/sdk/core/azure-core/azure/core/settings.py b/sdk/core/azure-core/azure/core/settings.py index f8e942c03163..40eceee31a99 100644 --- a/sdk/core/azure-core/azure/core/settings.py +++ b/sdk/core/azure-core/azure/core/settings.py @@ -123,26 +123,27 @@ def convert_logging(value): return level -def get_opencensus_span(): +def _get_opencensus_span(): # type: () -> Optional[Type[AbstractSpan]] """Returns the OpenCensusSpan if opencensus is installed else returns None""" try: from azure.core.tracing.ext.opencensus_span import OpenCensusSpan # pylint:disable=redefined-outer-name - return OpenCensusSpan # type: ignore except ImportError: return None - -def get_opencensus_span_if_opencensus_is_imported(): +def _get_opentelemetry_span(): # type: () -> Optional[Type[AbstractSpan]] - if "opencensus" not in sys.modules: + """Returns the OpenTelemetrySpan if opentelemetry is installed else returns None""" + try: + from azure.core.tracing.ext.opentelemetry_span import OpenTelemetrySpan # pylint:disable=redefined-outer-name + return OpenTelemetrySpan # type: ignore + except ImportError: return None - return get_opencensus_span() - _tracing_implementation_dict = { - "opencensus": get_opencensus_span + "opencensus": _get_opencensus_span, + "opentelemetry": _get_opentelemetry_span, } # type: Dict[str, Callable[[], Optional[Type[AbstractSpan]]]] @@ -154,6 +155,7 @@ def convert_tracing_impl(value): understands the following strings, ignoring case: * "opencensus" + * "opentelemetry" :param value: the value to convert :type value: string @@ -162,7 +164,7 @@ def convert_tracing_impl(value): """ if value is None: - return get_opencensus_span_if_opencensus_is_imported() + return _get_opencensus_span() if not isinstance(value, six.string_types): return value From 1e4457a9a18f36eb5008729bfd1be986df12cdf1 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Tue, 8 Oct 2019 15:25:08 -0700 Subject: [PATCH 04/28] Restore proper settings and tracing tests in azure-core --- .../tests/test_settings.py | 49 ------------------- sdk/core/azure-core/azure/core/settings.py | 2 +- sdk/core/azure-core/tests/test_settings.py | 45 +++++++++++++++++ 3 files changed, 46 insertions(+), 50 deletions(-) delete mode 100644 sdk/core/azure-core-tracing-opencensus/tests/test_settings.py diff --git a/sdk/core/azure-core-tracing-opencensus/tests/test_settings.py b/sdk/core/azure-core-tracing-opencensus/tests/test_settings.py deleted file mode 100644 index 72a97cb50f53..000000000000 --- a/sdk/core/azure-core-tracing-opencensus/tests/test_settings.py +++ /dev/null @@ -1,49 +0,0 @@ -# -------------------------------------------------------------------------- -# -# 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. -# -# -------------------------------------------------------------------------- -import logging -import os -import sys -import pytest - -# module under test -import azure.core.settings as m - - -class TestConverters(object): - def test_convert_implementation(self): - opencensus = sys.modules["opencensus"] - del sys.modules["opencensus"] - try: - 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!!") - finally: - import opencensus diff --git a/sdk/core/azure-core/azure/core/settings.py b/sdk/core/azure-core/azure/core/settings.py index 40eceee31a99..ea3d28a8fdae 100644 --- a/sdk/core/azure-core/azure/core/settings.py +++ b/sdk/core/azure-core/azure/core/settings.py @@ -164,7 +164,7 @@ def convert_tracing_impl(value): """ if value is None: - return _get_opencensus_span() + value = 'opencensus' if not isinstance(value, six.string_types): return value diff --git a/sdk/core/azure-core/tests/test_settings.py b/sdk/core/azure-core/tests/test_settings.py index a4abc5308d55..5519fec01df7 100644 --- a/sdk/core/azure-core/tests/test_settings.py +++ b/sdk/core/azure-core/tests/test_settings.py @@ -23,11 +23,17 @@ # THE SOFTWARE. # # -------------------------------------------------------------------------- +import collections import logging import os import sys import pytest +try: + from unittest import mock +except ImportError: + import mock + # module under test import azure.core.settings as m @@ -167,6 +173,45 @@ def test_convert_logging_bad(self): with pytest.raises(ValueError): m.convert_logging("junk") + def test_convert_implementation(self): + # Mostly it's here to be sure if a new plugin is added, someone check the tests + assert len(m._tracing_implementation_dict) == 2 + + with mock.patch.dict('azure.core.settings._tracing_implementation_dict', opencensus=lambda: None): + assert m.convert_tracing_impl(None) is None + assert m.convert_tracing_impl("opencensus") is None + + opencensus_span = mock.Mock() + with mock.patch.dict('azure.core.settings._tracing_implementation_dict', opencensus=lambda: opencensus_span): + assert m.convert_tracing_impl(None) is opencensus_span + assert m.convert_tracing_impl("opencensus") is opencensus_span + + opentelemetry_span = mock.Mock() + with mock.patch.dict('azure.core.settings._tracing_implementation_dict', opentelemetry= lambda: opentelemetry_span): + assert m.convert_tracing_impl("opentelemetry") is opentelemetry_span + # Take this opportunity to test case insensitive + assert m.convert_tracing_impl("OPENTELEMETRY") is opentelemetry_span + # 2.7 and unicode string should work + assert m.convert_tracing_impl(u"opentelemetry") is opentelemetry_span + + with pytest.raises(ValueError): + assert m.convert_tracing_impl("does not exist!!") + + def test_tracing_impl_loader(self): + mod = collections.namedtuple('mod', ['OpenCensusSpan']) + opencensus_span = mock.Mock() + with mock.patch.dict('sys.modules', {'azure.core.tracing.ext.opencensus_span': mod(opencensus_span)}): + assert m._get_opencensus_span() is opencensus_span + + mod = collections.namedtuple('mod', ['OpenTelemetrySpan']) + opentelemetry_span = mock.Mock() + with mock.patch.dict('sys.modules', {'azure.core.tracing.ext.opentelemetry_span': mod(opentelemetry_span)}): + assert m._get_opentelemetry_span() is opentelemetry_span + + with mock.patch.dict('sys.modules', {}): + assert m._get_opencensus_span() is None + assert m._get_opentelemetry_span() is None + _standard_settings = ["log_level", "tracing_enabled"] From b160216b803ff1d08a4b69f74e39b7df8b39841d Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Tue, 8 Oct 2019 16:00:53 -0700 Subject: [PATCH 05/28] Create HttpSpanMixin --- .../tracing/ext/opencensus_span/__init__.py | 29 +---------------- .../ext/opentelemetry_span/__init__.py | 31 ++----------------- .../azure-core/azure/core/tracing/__init__.py | 4 +-- .../azure/core/tracing/abstract_span.py | 31 +++++++++++++++++++ 4 files changed, 36 insertions(+), 59 deletions(-) diff --git a/sdk/core/azure-core-tracing-opencensus/azure/core/tracing/ext/opencensus_span/__init__.py b/sdk/core/azure-core-tracing-opencensus/azure/core/tracing/ext/opencensus_span/__init__.py index a25ec35b905a..1303fe34d688 100644 --- a/sdk/core/azure-core-tracing-opencensus/azure/core/tracing/ext/opencensus_span/__init__.py +++ b/sdk/core/azure-core-tracing-opencensus/azure/core/tracing/ext/opencensus_span/__init__.py @@ -11,7 +11,7 @@ from opencensus.trace.link import Link from opencensus.trace.propagation import trace_context_http_header_format -from azure.core.tracing import SpanKind # pylint: disable=no-name-in-module +from azure.core.tracing import SpanKind, HttpSpanMixin # pylint: disable=no-name-in-module try: from typing import TYPE_CHECKING @@ -41,11 +41,6 @@ def __init__(self, span=None, name="span"): """ tracer = self.get_current_tracer() self._span_instance = span or tracer.start_span(name=name) - self._span_component = "component" - self._http_user_agent = "http.user_agent" - self._http_method = "http.method" - self._http_url = "http.url" - self._http_status_code = "http.status_code" @property def span_instance(self): @@ -139,28 +134,6 @@ def add_attribute(self, key, value): """ self.span_instance.add_attribute(key, value) - def set_http_attributes(self, request, response=None): - # type: (HttpRequest, Optional[HttpResponse]) -> None - """ - Add correct attributes for a http client span. - - :param request: The request made - :type request: HttpRequest - :param response: The response received by the server. Is None if no response received. - :type response: HttpResponse - """ - self.kind = SpanKind.CLIENT - self.span_instance.add_attribute(self._span_component, "http") - self.span_instance.add_attribute(self._http_method, request.method) - self.span_instance.add_attribute(self._http_url, request.url) - user_agent = request.headers.get("User-Agent") - if user_agent: - self.span_instance.add_attribute(self._http_user_agent, user_agent) - if response: - self._span_instance.add_attribute(self._http_status_code, response.status_code) - else: - self._span_instance.add_attribute(self._http_status_code, 504) - def get_trace_parent(self): """Return traceparent string as defined in W3C trace context specification. diff --git a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py index 5cf9c8583150..92c298c17965 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py +++ b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py @@ -8,7 +8,7 @@ from opentelemetry.context import Context from opentelemetry.propagators import extract, inject -from azure.core.tracing import SpanKind # pylint: disable=no-name-in-module +from azure.core.tracing import SpanKind, HttpSpanMixin # pylint: disable=no-name-in-module try: from typing import TYPE_CHECKING @@ -41,7 +41,7 @@ def _set_headers_from_http_request_headers(headers: "Mapping[str, Any]", key: st headers[key] = value -class OpenTelemetrySpan(object): +class OpenTelemetrySpan(HttpSpanMixin, object): """Wraps a given OpenTelemetry Span so that it implements azure.core.tracing.AbstractSpan""" def __init__(self, span=None, name="span"): @@ -57,11 +57,6 @@ def __init__(self, span=None, name="span"): """ tracer = self.get_current_tracer() self._span_instance = span or tracer.create_span(name=name) - self._span_component = "component" - self._http_user_agent = "http.user_agent" - self._http_method = "http.method" - self._http_url = "http.url" - self._http_status_code = "http.status_code" self._current_ctxt_manager = None @property @@ -160,28 +155,6 @@ def add_attribute(self, key, value): """ self.span_instance.set_attribute(key, value) - def set_http_attributes(self, request, response=None): - # type: (HttpRequest, Optional[HttpResponse]) -> None - """ - Add correct attributes for a http client span. - - :param request: The request made - :type request: HttpRequest - :param response: The response received by the server. Is None if no response received. - :type response: HttpResponse - """ - self.kind = SpanKind.CLIENT - self.add_attribute(self._span_component, "http") - self.add_attribute(self._http_method, request.method) - self.add_attribute(self._http_url, request.url) - user_agent = request.headers.get("User-Agent") - if user_agent: - self.add_attribute(self._http_user_agent, user_agent) - if response: - self.add_attribute(self._http_status_code, response.status_code) - else: - self.add_attribute(self._http_status_code, 504) - def get_trace_parent(self): """Return traceparent string as defined in W3C trace context specification. diff --git a/sdk/core/azure-core/azure/core/tracing/__init__.py b/sdk/core/azure-core/azure/core/tracing/__init__.py index fa3da2d26b99..34848e0d6686 100644 --- a/sdk/core/azure-core/azure/core/tracing/__init__.py +++ b/sdk/core/azure-core/azure/core/tracing/__init__.py @@ -2,8 +2,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -from azure.core.tracing.abstract_span import AbstractSpan, SpanKind +from azure.core.tracing.abstract_span import AbstractSpan, SpanKind, HttpSpanMixin __all__ = [ - "AbstractSpan", "SpanKind" + "AbstractSpan", "SpanKind", "HttpSpanMixin" ] 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 0053cd37c3c4..d136bb2c408d 100644 --- a/sdk/core/azure-core/azure/core/tracing/abstract_span.py +++ b/sdk/core/azure-core/azure/core/tracing/abstract_span.py @@ -176,3 +176,34 @@ def with_current_context(cls, func): :param func: The function that will be run in the new context :return: The target the pass in instead of the function """ + +class HttpSpanMixin(object): + """Can be used to get HTTP span attributes settings for free. + """ + _SPAN_COMPONENT = "component" + _HTTP_USER_AGENT = "http.user_agent" + _HTTP_METHOD = "http.method" + _HTTP_URL = "http.url" + _HTTP_STATUS_CODE = "http.status_code" + + def set_http_attributes(self, request, response=None): + # type: (HttpRequest, Optional[HttpResponse]) -> None + """ + Add correct attributes for a http client span. + + :param request: The request made + :type request: HttpRequest + :param response: The response received by the server. Is None if no response received. + :type response: HttpResponse + """ + self.kind = SpanKind.CLIENT + self.add_attribute(self._SPAN_COMPONENT, "http") + self.add_attribute(self._HTTP_METHOD, request.method) + self.add_attribute(self._HTTP_URL, request.url) + user_agent = request.headers.get("User-Agent") + if user_agent: + self.add_attribute(self._HTTP_USER_AGENT, user_agent) + if response: + self.add_attribute(self._HTTP_STATUS_CODE, response.status_code) + else: + self.add_attribute(self._HTTP_STATUS_CODE, 504) From 724daffd9d644c59fa5ca0de2b1be9888a38bd64 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Tue, 8 Oct 2019 16:01:21 -0700 Subject: [PATCH 06/28] Need opentelemetry-sdk for testing --- sdk/core/azure-core-tracing-opentelemetry/dev_requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/core/azure-core-tracing-opentelemetry/dev_requirements.txt b/sdk/core/azure-core-tracing-opentelemetry/dev_requirements.txt index a4b74a7f9e44..47a1b07ae4c2 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/dev_requirements.txt +++ b/sdk/core/azure-core-tracing-opentelemetry/dev_requirements.txt @@ -1,2 +1,3 @@ -e ../../../tools/azure-sdk-tools -../azure-core \ No newline at end of file +../azure-core +opentelemetry-sdk \ No newline at end of file From 788a3392a24e14e5787566805ec31ae82f8899ed Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Tue, 8 Oct 2019 17:28:52 -0700 Subject: [PATCH 07/28] WIP core test --- .../azure-core/tests/test_tracing_policy.py | 160 +++++++++++++++ sdk/core/azure-core/tests/tracing_common.py | 187 ++++++++++++++++++ 2 files changed, 347 insertions(+) create mode 100644 sdk/core/azure-core/tests/test_tracing_policy.py create mode 100644 sdk/core/azure-core/tests/tracing_common.py diff --git a/sdk/core/azure-core/tests/test_tracing_policy.py b/sdk/core/azure-core/tests/test_tracing_policy.py new file mode 100644 index 000000000000..930fb053f592 --- /dev/null +++ b/sdk/core/azure-core/tests/test_tracing_policy.py @@ -0,0 +1,160 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""Tests for the distributed tracing policy.""" +import logging + +from azure.core.pipeline import PipelineResponse, PipelineRequest, PipelineContext +from azure.core.pipeline.policies import DistributedTracingPolicy, UserAgentPolicy +from azure.core.pipeline.transport import HttpRequest, HttpResponse +from tracing_common import FakeSpan +import time +import pytest + +try: + from unittest import mock +except ImportError: + import mock + + +def test_distributed_tracing_policy_solo(): + """Test policy with no other policy and happy path""" + with FakeSpan(name="parent"): + policy = DistributedTracingPolicy() + + request = HttpRequest("GET", "http://127.0.0.1/temp?query=query") + request.headers["x-ms-client-request-id"] = "some client request id" + + pipeline_request = PipelineRequest(request, PipelineContext(None)) + policy.on_request(pipeline_request) + + response = HttpResponse(request, None) + response.headers = request.headers + response.status_code = 202 + response.headers["x-ms-request-id"] = "some request id" + + ctx = trace.span_context + header = trace.propagator.to_headers(ctx) + assert request.headers.get("traceparent") == header.get("traceparent") + + policy.on_response(pipeline_request, PipelineResponse(request, response, PipelineContext(None))) + time.sleep(0.001) + policy.on_request(pipeline_request) + try: + raise ValueError("Transport trouble") + except: + policy.on_exception(pipeline_request) + + network_span = parent.children[0] + assert network_span.span_data.name == "/temp" + assert network_span.span_data.attributes.get("http.method") == "GET" + assert network_span.span_data.attributes.get("component") == "http" + assert network_span.span_data.attributes.get("http.url") == "http://127.0.0.1/temp?query=query" + assert network_span.span_data.attributes.get("http.user_agent") is None + assert network_span.span_data.attributes.get("x-ms-request-id") == "some request id" + assert network_span.span_data.attributes.get("x-ms-client-request-id") == "some client request id" + assert network_span.span_data.attributes.get("http.status_code") == 202 + + network_span = parent.children[1] + assert network_span.span_data.name == "/temp" + assert network_span.span_data.attributes.get("http.method") == "GET" + assert network_span.span_data.attributes.get("component") == "http" + assert network_span.span_data.attributes.get("http.url") == "http://127.0.0.1/temp?query=query" + assert network_span.span_data.attributes.get("x-ms-client-request-id") == "some client request id" + assert network_span.span_data.attributes.get("http.user_agent") is None + assert network_span.span_data.attributes.get("x-ms-request-id") == None + assert network_span.span_data.attributes.get("http.status_code") == 504 + + +def test_distributed_tracing_policy_badurl(caplog): + """Test policy with a bad url that will throw, and be sure policy ignores it""" + with FakeSpan(name="parent"): + policy = DistributedTracingPolicy() + + request = HttpRequest("GET", "http://[[[") + request.headers["x-ms-client-request-id"] = "some client request id" + + pipeline_request = PipelineRequest(request, PipelineContext(None)) + with caplog.at_level(logging.WARNING, logger="azure.core.pipeline.policies.distributed_tracing"): + policy.on_request(pipeline_request) + assert "Unable to start network span" in caplog.text + + response = HttpResponse(request, None) + response.headers = request.headers + response.status_code = 202 + response.headers["x-ms-request-id"] = "some request id" + + ctx = trace.span_context + header = trace.propagator.to_headers(ctx) + assert request.headers.get("traceparent") is None # Got not network trace + + policy.on_response(pipeline_request, PipelineResponse(request, response, PipelineContext(None))) + time.sleep(0.001) + policy.on_request(pipeline_request) + try: + raise ValueError("Transport trouble") + except: + policy.on_exception(pipeline_request) + + assert len(parent.children) == 0 + + +def test_distributed_tracing_policy_with_user_agent(): + """Test policy working with user agent.""" + with mock.patch.dict('os.environ', {"AZURE_HTTP_USER_AGENT": "mytools"}): + with FakeSpan(name="parent"): + policy = DistributedTracingPolicy() + + request = HttpRequest("GET", "http://127.0.0.1") + request.headers["x-ms-client-request-id"] = "some client request id" + + pipeline_request = PipelineRequest(request, PipelineContext(None)) + + user_agent = UserAgentPolicy() + user_agent.on_request(pipeline_request) + policy.on_request(pipeline_request) + + response = HttpResponse(request, None) + response.headers = request.headers + response.status_code = 202 + response.headers["x-ms-request-id"] = "some request id" + pipeline_response = PipelineResponse(request, response, PipelineContext(None)) + + ctx = trace.span_context + header = trace.propagator.to_headers(ctx) + assert request.headers.get("traceparent") == header.get("traceparent") + + policy.on_response(pipeline_request, pipeline_response) + + time.sleep(0.001) + policy.on_request(pipeline_request) + try: + raise ValueError("Transport trouble") + except: + policy.on_exception(pipeline_request) + + user_agent.on_response(pipeline_request, pipeline_response) + + network_span = parent.children[0] + assert network_span.span_data.name == "/" + assert network_span.span_data.attributes.get("http.method") == "GET" + assert network_span.span_data.attributes.get("component") == "http" + assert network_span.span_data.attributes.get("http.url") == "http://127.0.0.1" + assert network_span.span_data.attributes.get("http.user_agent").endswith("mytools") + assert network_span.span_data.attributes.get("x-ms-request-id") == "some request id" + assert network_span.span_data.attributes.get("x-ms-client-request-id") == "some client request id" + assert network_span.span_data.attributes.get("http.status_code") == 202 + + network_span = parent.children[1] + assert network_span.span_data.name == "/" + assert network_span.span_data.attributes.get("http.method") == "GET" + assert network_span.span_data.attributes.get("component") == "http" + assert network_span.span_data.attributes.get("http.url") == "http://127.0.0.1" + assert network_span.span_data.attributes.get("http.user_agent").endswith("mytools") + assert network_span.span_data.attributes.get("x-ms-client-request-id") == "some client request id" + assert network_span.span_data.attributes.get("x-ms-request-id") is None + assert network_span.span_data.attributes.get("http.status_code") == 504 + # Exception should propagate status for Opencensus + assert network_span.span_data.status.message == 'Transport trouble' + 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..855244e816bc --- /dev/null +++ b/sdk/core/azure-core/tests/tracing_common.py @@ -0,0 +1,187 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""Fake implementation of AbstractSpan for tests.""" +from azure.core.tracing import HttpSpanMixin, SpanKind + + +class FakeSpan(HttpSpanMixin, object): + + def __init__(self, span=None, name="span"): + # type: (Optional[Span], Optional[str]) -> None + """ + If a span is not passed in, creates a new tracer. If the instrumentation key for Azure Exporter is given, will + configure the azure exporter else will just create a new tracer. + + :param span: The OpenTelemetry span to wrap + :type span: :class: OpenTelemetry.trace.Span + :param name: The name of the OpenTelemetry span to create if a new span is needed + :type name: str + """ + self._span = span + self._name = name + self._kind = SpanKind.UNSPECIFIED + + @property + def span_instance(self): + # type: () -> Span + """ + :return: The OpenTelemetry span that is being wrapped. + """ + return self._span + + def span(self, name="span"): + # type: (Optional[str]) -> OpenCensusSpan + """ + Create a child span for the current span and append it to the child spans list in the span instance. + :param name: Name of the child span + :type name: str + :return: The OpenCensusSpan that is wrapping the child span instance + """ + return self.__class__(name=name) + + @property + def kind(self): + # type: () -> Optional[SpanKind] + """Get the span kind of this span.""" + return self._kind + + + @kind.setter + def kind(self, value): + # type: (SpanKind) -> None + """Set the span kind of this span.""" + self._kind = value + + def __enter__(self): + """Start a span.""" + return self + + def __exit__(self, exception_type, exception_value, traceback): + """Finish a span.""" + pass + + def start(self): + # type: () -> None + """Set the start time for a span.""" + pass + + def finish(self): + # type: () -> None + """Set the end time for a span.""" + pass + + def to_header(self): + # type: () -> Dict[str, str] + """ + Returns a dictionary with the header labels and values. + :return: A key value pair dictionary + """ + temp_headers = {} # type: Dict[str, str] + # FIXME + return temp_headers + + def add_attribute(self, key, value): + # type: (str, Union[str, int]) -> None + """ + Add attribute (key value pair) to the current span. + + :param key: The key of the key value pair + :type key: str + :param value: The value of the key value pair + :type value: str + """ + self.span_instance.set_attribute(key, value) + + def get_trace_parent(self): + """Return traceparent string as defined in W3C trace context specification. + + Example: + Value = 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 + base16(version) = 00 + base16(trace-id) = 4bf92f3577b34da6a3ce929d0e0e4736 + base16(parent-id) = 00f067aa0ba902b7 + base16(trace-flags) = 01 // sampled + + :return: a traceparent string + :rtype: str + """ + return self.to_header()['traceparent'] + + @classmethod + def link(cls, traceparent): + # type: (str) -> None + """ + Links the context to the current tracer. + + :param traceparent: A complete traceparent + :type traceparent: str + """ + cls.link_from_headers({ + 'traceparent': traceparent + }) + + @classmethod + def link_from_headers(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 + """ + ctx = extract(_get_headers_from_http_request_headers, headers) + current_span = cls.get_current_span() + current_span.add_link(ctx) + + @classmethod + def get_current_span(cls): + # type: () -> Span + """ + Get the current span from the execution context. Return None otherwise. + """ + return cls.get_current_tracer().get_current_span() + + @classmethod + def get_current_tracer(cls): + # type: () -> Tracer + """ + Get the current tracer from the execution context. Return None otherwise. + """ + return tracer() + + @classmethod + def change_context(cls, span): + # type: (Span) -> ContextManager + """Change the context for the life of this context manager. + """ + return cls.get_current_tracer().use_span(span, end_on_exit=False) + + @classmethod + def set_current_span(cls, span): + # type: (Span) -> None + """Not supported by OpenTelemetry. + """ + raise NotImplementedError("set_current_span is not supported by OpenTelemetry plugin. Use ChangeContext instead.") + + @classmethod + def set_current_tracer(cls, tracer): + # type: (Tracer) -> None + """ + Set the given tracer as the current tracer in the execution context. + :param tracer: The tracer to set the current tracer as + :type tracer: :class: OpenTelemetry.trace.Tracer + """ + # Do nothing, if you're able to get two tracer with OpenTelemetry that's a surprise! + pass + + @classmethod + def with_current_context(cls, func): + # type: (Callable) -> Callable + """Passes the current spans to the new context the function will be run in. + + :param func: The function that will be run in the new context + :return: The target the pass in instead of the function + """ + return Context.with_current_context(func) From 94b5dd16a36673f36ad70ef65b042bf288590c6a Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Wed, 13 Nov 2019 09:41:49 -0800 Subject: [PATCH 08/28] Use stable azure-core --- sdk/core/azure-core-tracing-opentelemetry/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/azure-core-tracing-opentelemetry/setup.py b/sdk/core/azure-core-tracing-opentelemetry/setup.py index 589a936e288c..48a56df8c49d 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/setup.py +++ b/sdk/core/azure-core-tracing-opentelemetry/setup.py @@ -58,7 +58,7 @@ install_requires=[ 'opentelemetry-api', 'opentelemetry-ext-azure-monitor', - 'azure-core<2.0.0,>=1.0.0b4', + 'azure-core<2.0.0,>=1.0.0', ], extras_require={ ":python_version<'3.5'": ['typing'], From 88b499f6186f5ec928aacc8db59bdc631dd1ff87 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Wed, 13 Nov 2019 09:42:14 -0800 Subject: [PATCH 09/28] Support Python 3.8 --- sdk/core/azure-core-tracing-opentelemetry/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/core/azure-core-tracing-opentelemetry/setup.py b/sdk/core/azure-core-tracing-opentelemetry/setup.py index 48a56df8c49d..f0756bc9ad86 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/setup.py +++ b/sdk/core/azure-core-tracing-opentelemetry/setup.py @@ -49,6 +49,7 @@ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'License :: OSI Approved :: MIT License', ], zip_safe=False, From de574d13cef876ad3418aea93297aee84d57bb8f Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Wed, 13 Nov 2019 12:01:41 -0800 Subject: [PATCH 10/28] OT Readme --- .../README.md | 36 ++++++++++++------- .../azure-core-tracing-opentelemetry/setup.py | 2 +- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/sdk/core/azure-core-tracing-opentelemetry/README.md b/sdk/core/azure-core-tracing-opentelemetry/README.md index e0400c64ee52..3db2d863b5f5 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/README.md +++ b/sdk/core/azure-core-tracing-opentelemetry/README.md @@ -1,43 +1,53 @@ -# Azure Core Tracing OpenCensus client library for Python +# Azure Core Tracing OpenTelemetry client library for Python ## Getting started -Install the opencensus python for Python with [pip](https://pypi.org/project/pip/): +Install the opentelemetry python for Python with [pip](https://pypi.org/project/pip/): ```bash -pip install azure-core-tracing-opencensus --pre +pip install azure-core-tracing-opentelemetry --pre ``` -Now you can use opencensus for Python as usual with any SDKs that is compatible +Now you can use opentelemetry for Python as usual with any SDKs that is compatible with azure-core tracing. This includes (not exhaustive list), azure-storage-blob, azure-keyvault-secrets, azure-eventhub, etc. ## Key concepts * You don't need to pass any context, SDK will get it for you -* The opencensus threading plugin is installed with this package ## Examples -There is no explicit context to pass, you just create your usual opencensus and tracer and +There is no explicit context to pass, you just create your usual opentelemetry tracer and call any SDK code that is compatible with azure-core tracing. This is an example using Azure Monitor exporter, but you can use any exporter (Zipkin, etc.). ```python -from opencensus.ext.azure.trace_exporter import AzureExporter +from opentelemetry.ext.azure_monitor import AzureMonitorSpanExporter -from opencensus.trace.tracer import Tracer -from opencensus.trace.samplers import AlwaysOnSampler +from opentelemetry import trace +from opentelemetry.sdk.trace import Tracer + +from azure.core.settings import settings +from azure.core.tracing.ext.opentelemetry_span import OpenTelemetrySpan from azure.storage.blob import BlobServiceClient -exporter = AzureExporter( +# Declare that you want to trace Azure SDK with OpenTelemetry +settings.tracing_implementation = OpenTelemetrySpan + +exporter = AzureMonitorSpanExporter( instrumentation_key="uuid of the instrumentation key (see your Azure Monitor account)" ) -tracer = Tracer(exporter=exporter, sampler=AlwaysOnSampler()) -with tracer.span(name="MyApplication") as span: +trace.set_preferred_tracer_implementation(lambda T: Tracer()) +tracer = trace.tracer() +tracer.add_span_processor( + SimpleExportSpanProcessor(exporter) +) + +with tracer.start_as_current_span(name="MyApplication"): client = BlobServiceClient.from_connection_string('connectionstring') client.delete_container('mycontainer') # Call will be traced ``` @@ -50,7 +60,7 @@ This client raises exceptions defined in [Azure Core](https://github.com/Azure/a ## Next steps -More documentation on OpenCensus configuration can be found on the [OpenCensus website](https://opencensus.io) +More documentation on OpenTelemetry configuration can be found on the [OpenTelemetry website](https://opentelemetry.io) ## Contributing diff --git a/sdk/core/azure-core-tracing-opentelemetry/setup.py b/sdk/core/azure-core-tracing-opentelemetry/setup.py index f0756bc9ad86..ffcb23f8ac75 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/setup.py +++ b/sdk/core/azure-core-tracing-opentelemetry/setup.py @@ -57,7 +57,7 @@ 'azure.core.tracing.ext.opentelemetry_span', ], install_requires=[ - 'opentelemetry-api', + 'opentelemetry-api>0.2a0', 'opentelemetry-ext-azure-monitor', 'azure-core<2.0.0,>=1.0.0', ], From 16cce5d79c8eba06ea4353571133e5ab7e86b3c5 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Wed, 13 Nov 2019 12:11:59 -0800 Subject: [PATCH 11/28] Do not include YAML --- sdk/core/azure-core-tracing-opentelemetry/MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/azure-core-tracing-opentelemetry/MANIFEST.in b/sdk/core/azure-core-tracing-opentelemetry/MANIFEST.in index 53cf3b003aae..f008c7967322 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/MANIFEST.in +++ b/sdk/core/azure-core-tracing-opentelemetry/MANIFEST.in @@ -1,4 +1,4 @@ -recursive-include tests *.py *.yaml +recursive-include tests *.py include *.md include azure/__init__.py include azure/core/__init__.py From 151354d9a02fbe46e0fd90942cf0bfe20e666acf Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Wed, 13 Nov 2019 13:42:07 -0800 Subject: [PATCH 12/28] Dependency update --- sdk/core/azure-core-tracing-opentelemetry/setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/core/azure-core-tracing-opentelemetry/setup.py b/sdk/core/azure-core-tracing-opentelemetry/setup.py index ffcb23f8ac75..216ef174a1d4 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/setup.py +++ b/sdk/core/azure-core-tracing-opentelemetry/setup.py @@ -57,8 +57,8 @@ 'azure.core.tracing.ext.opentelemetry_span', ], install_requires=[ - 'opentelemetry-api>0.2a0', - 'opentelemetry-ext-azure-monitor', + 'opentelemetry-api>=0.2a0', + 'opentelemetry-ext-azure-monitor>=0.2a0', 'azure-core<2.0.0,>=1.0.0', ], extras_require={ From 963964676cf3afcf8cd967872e86cab8ce79edd5 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Fri, 20 Dec 2019 10:03:30 -0800 Subject: [PATCH 13/28] Update dep --- sdk/core/azure-core-tracing-opentelemetry/setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/core/azure-core-tracing-opentelemetry/setup.py b/sdk/core/azure-core-tracing-opentelemetry/setup.py index 216ef174a1d4..abbafc03b88e 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/setup.py +++ b/sdk/core/azure-core-tracing-opentelemetry/setup.py @@ -57,8 +57,8 @@ 'azure.core.tracing.ext.opentelemetry_span', ], install_requires=[ - 'opentelemetry-api>=0.2a0', - 'opentelemetry-ext-azure-monitor>=0.2a0', + 'opentelemetry-api>=0.3a0', + 'opentelemetry-azure-monitor-exporter>=0.1a0', 'azure-core<2.0.0,>=1.0.0', ], extras_require={ From d46c47b6e2863cca2ec3e100f86e971aafa6a435 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Fri, 20 Dec 2019 12:05:34 -0800 Subject: [PATCH 14/28] Add decorator tests in azure-core --- .../test_tracing_decorator_async.py | 163 +++++++++++++++++ sdk/core/azure-core/tests/conftest.py | 10 ++ sdk/core/azure-core/tests/test_settings.py | 4 - .../tests/test_tracing_decorator.py | 166 ++++++++++++++++++ sdk/core/azure-core/tests/tracing_common.py | 17 +- 5 files changed, 353 insertions(+), 7 deletions(-) create mode 100644 sdk/core/azure-core/tests/azure_core_asynctests/test_tracing_decorator_async.py create mode 100644 sdk/core/azure-core/tests/test_tracing_decorator.py 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..356f52a7d7d1 --- /dev/null +++ b/sdk/core/azure-core/tests/azure_core_asynctests/test_tracing_decorator_async.py @@ -0,0 +1,163 @@ +# ------------------------------------ +# 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 +import time + +import pytest +from azure.core.pipeline import Pipeline, PipelineResponse +from azure.core.pipeline.policies import HTTPPolicy +from azure.core.pipeline.transport import HttpTransport, HttpRequest +from azure.core.settings import settings +from azure.core.tracing.decorator import distributed_trace +from azure.core.tracing.decorator_async import distributed_trace_async +from tracing_common import FakeSpan + + +@pytest.fixture(scope="module") +def fake_span(): + settings.tracing_implementation.set_value(FakeSpan) + + +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): + if self.assert_current_span: + assert execution_context.get_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(merge_span=True) + kwargs['merge_span'] = True + await self.make_request(numb_times - 1, **kwargs) + return response + + @distributed_trace_async + async def merge_span_method(self): + return await self.get_foo(merge_span=True) + + @distributed_trace_async + async def no_merge_span_method(self): + return await self.get_foo() + + @distributed_trace_async + async def get_foo(self): + time.sleep(0.001) + return 5 + + @distributed_trace_async(name_of_span="different name") + async def check_name_is_different(self): + time.sleep(0.001) + + @distributed_trace_async + async def raising_exception(self): + raise ValueError("Something went horribly wrong here") + + +@pytest.mark.usefixtures("fake_span") +class TestAsyncDecorator(object): + + @pytest.mark.asyncio + async def test_decorator_has_different_name(self): + with FakeSpan(name="parent") as parent: + client = MockClient() + await client.check_name_is_different() + assert len(parent.children) == 2 + assert parent.children[0].name == "MockClient.__init__" + assert parent.children[1].name == "different name" + + + @pytest.mark.asyncio + async def test_used(self): + with FakeSpan(name="parent") as parent: + client = MockClient(policies=[]) + await client.get_foo(parent_span=parent) + await client.get_foo() + + assert len(parent.children) == 3 + assert parent.children[0].name == "MockClient.__init__" + assert not parent.children[0].children + assert parent.children[1].name == "MockClient.get_foo" + assert not parent.children[1].children + + @pytest.mark.asyncio + async def test_span_merge_span(self): + with FakeSpan(name="parent") as parent: + client = MockClient() + await client.merge_span_method() + await client.no_merge_span_method() + + assert len(parent.children) == 3 + assert parent.children[0].name == "MockClient.__init__" + assert not parent.children[0].children + assert parent.children[1].name == "MockClient.merge_span_method" + assert not parent.children[1].children + assert parent.children[2].name == "MockClient.no_merge_span_method" + assert parent.children[2].children[0].name == "MockClient.get_foo" + + + @pytest.mark.asyncio + async def test_span_complicated(self): + with FakeSpan(name="parent") as parent: + client = MockClient() + await client.make_request(2) + with parent.span("child") as child: + time.sleep(0.001) + await client.make_request(2, parent_span=parent) + assert FakeSpan.get_current_span() == child + await client.make_request(2) + + assert len(parent.children) == 4 + assert parent.children[0].name == "MockClient.__init__" + assert not parent.children[0].children + assert parent.children[1].name == "MockClient.make_request" + assert not parent.children[1].children + assert parent.children[2].name == "child" + assert parent.children[2].children[0].name == "MockClient.make_request" + assert parent.children[3].name == "MockClient.make_request" + assert not parent.children[3].children + + @pytest.mark.asyncio + async def test_span_with_exception(self): + """Assert that if an exception is raised, the next sibling method is actually a sibling span. + """ + with FakeSpan(name="parent") as parent: + client = MockClient() + try: + await client.raising_exception() + except: + pass + await client.get_foo() + + assert len(parent.children) == 3 + assert parent.children[0].name == "MockClient.__init__" + assert parent.children[1].name == "MockClient.raising_exception" + # Exception should propagate status for Opencensus + assert parent.children[1].status == 'Something went horribly wrong here' + assert parent.children[2].name == "MockClient.get_foo" diff --git a/sdk/core/azure-core/tests/conftest.py b/sdk/core/azure-core/tests/conftest.py index 8e13ced18b57..52e881468a69 100644 --- a/sdk/core/azure-core/tests/conftest.py +++ b/sdk/core/azure-core/tests/conftest.py @@ -29,3 +29,13 @@ collect_ignore = [] if sys.version_info < (3, 5): collect_ignore.append("azure_core_asynctests") + + +# If opencensus is loadable while doing these tests, register an empty tracer to avoid this: +# https://github.com/census-instrumentation/opencensus-python/issues/442 +try: + from azure.core.tracing.ext.opencensus_span import OpenCensusSpan + from opencensus.trace.tracer import Tracer + Tracer() +except ImportError: + pass diff --git a/sdk/core/azure-core/tests/test_settings.py b/sdk/core/azure-core/tests/test_settings.py index 5519fec01df7..4fcec1139dc5 100644 --- a/sdk/core/azure-core/tests/test_settings.py +++ b/sdk/core/azure-core/tests/test_settings.py @@ -208,10 +208,6 @@ def test_tracing_impl_loader(self): with mock.patch.dict('sys.modules', {'azure.core.tracing.ext.opentelemetry_span': mod(opentelemetry_span)}): assert m._get_opentelemetry_span() is opentelemetry_span - with mock.patch.dict('sys.modules', {}): - assert m._get_opencensus_span() is None - assert m._get_opentelemetry_span() is None - _standard_settings = ["log_level", "tracing_enabled"] 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..cdfaf3044b70 --- /dev/null +++ b/sdk/core/azure-core/tests/test_tracing_decorator.py @@ -0,0 +1,166 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""The tests for decorators.py and common.py""" + +try: + from unittest import mock +except ImportError: + import mock + +import sys +import time + +import pytest +from azure.core.pipeline import Pipeline, PipelineResponse +from azure.core.pipeline.policies import HTTPPolicy +from azure.core.pipeline.transport import HttpTransport, HttpRequest +from azure.core.settings import settings +from azure.core.tracing import common +from azure.core.tracing.decorator import distributed_trace +from tracing_common import FakeSpan + + +@pytest.fixture(scope="module") +def fake_span(): + settings.tracing_implementation.set_value(FakeSpan) + + +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): + if self.assert_current_span: + assert execution_context.get_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(merge_span=True) + kwargs['merge_span'] = True + self.make_request(numb_times - 1, **kwargs) + return response + + @distributed_trace + def merge_span_method(self): + return self.get_foo(merge_span=True) + + @distributed_trace + def no_merge_span_method(self): + return self.get_foo() + + @distributed_trace + def get_foo(self): + time.sleep(0.001) + return 5 + + @distributed_trace(name_of_span="different name") + def check_name_is_different(self): + time.sleep(0.001) + + @distributed_trace + def raising_exception(self): + raise ValueError("Something went horribly wrong here") + + +def random_function(): + pass + + +def test_get_function_and_class_name(): + client = MockClient() + assert common.get_function_and_class_name(client.get_foo, client) == "MockClient.get_foo" + assert common.get_function_and_class_name(random_function) == "random_function" + + +@pytest.mark.usefixtures("fake_span") +class TestDecorator(object): + def test_decorator_has_different_name(self): + with FakeSpan(name="parent") as parent: + client = MockClient() + client.check_name_is_different() + + assert len(parent.children) == 2 + assert parent.children[0].name == "MockClient.__init__" + assert parent.children[1].name == "different name" + + def test_used(self): + with FakeSpan(name="parent") as parent: + client = MockClient(policies=[]) + client.get_foo(parent_span=parent) + client.get_foo() + + assert len(parent.children) == 3 + assert parent.children[0].name == "MockClient.__init__" + assert not parent.children[0].children + assert parent.children[1].name == "MockClient.get_foo" + assert not parent.children[1].children + + def test_span_merge_span(self): + with FakeSpan(name="parent") as parent: + client = MockClient() + client.merge_span_method() + client.no_merge_span_method() + + assert len(parent.children) == 3 + assert parent.children[0].name == "MockClient.__init__" + assert not parent.children[0].children + assert parent.children[1].name == "MockClient.merge_span_method" + assert not parent.children[1].children + assert parent.children[2].name == "MockClient.no_merge_span_method" + assert parent.children[2].children[0].name == "MockClient.get_foo" + + def test_span_complicated(self): + with FakeSpan(name="parent") as parent: + client = MockClient() + client.make_request(2) + with parent.span("child") as child: + time.sleep(0.001) + client.make_request(2, parent_span=parent) + assert FakeSpan.get_current_span() == child + client.make_request(2) + + assert len(parent.children) == 4 + assert parent.children[0].name == "MockClient.__init__" + assert not parent.children[0].children + assert parent.children[1].name == "MockClient.make_request" + assert not parent.children[1].children + assert parent.children[2].name == "child" + assert parent.children[2].children[0].name == "MockClient.make_request" + assert parent.children[3].name == "MockClient.make_request" + assert not parent.children[3].children + + def test_span_with_exception(self): + """Assert that if an exception is raised, the next sibling method is actually a sibling span. + """ + with FakeSpan(name="parent") as parent: + client = MockClient() + try: + client.raising_exception() + except: + pass + client.get_foo() + + assert len(parent.children) == 3 + assert parent.children[0].name == "MockClient.__init__" + assert parent.children[1].name == "MockClient.raising_exception" + # Exception should propagate status for Opencensus + assert parent.children[1].status == 'Something went horribly wrong here' + assert parent.children[2].name == "MockClient.get_foo" diff --git a/sdk/core/azure-core/tests/tracing_common.py b/sdk/core/azure-core/tests/tracing_common.py index ba938c8dbe71..2fde8879c224 100644 --- a/sdk/core/azure-core/tests/tracing_common.py +++ b/sdk/core/azure-core/tests/tracing_common.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. # ------------------------------------ """Fake implementation of AbstractSpan for tests.""" +from contextlib import contextmanager from azure.core.tracing import HttpSpanMixin, SpanKind @@ -31,6 +32,13 @@ def __init__(self, span=None, name="span"): self.CONTEXT.append(self) self.status = None + def __str__(self): + buffer = "Name: {}\n".format(self.name) + buffer += "Children:\n" + subchildren = "\n".join(str(child) for child in self.children) + buffer += "\n".join("\t{}".format(line) for line in subchildren.splitlines()) + return buffer + @property def span_instance(self): # type: () -> Span @@ -160,13 +168,16 @@ def get_current_tracer(cls): raise NotImplementedError() @classmethod + @contextmanager def change_context(cls, span): # type: (Span) -> ContextManager """Change the context for the life of this context manager. """ - cls.CONTEXT.append(span) - yield - cls.CONTEXT.pop() + try: + cls.CONTEXT.append(span) + yield + finally: + cls.CONTEXT.pop() @classmethod def set_current_span(cls, span): From 1ce3b5664fa485226ff2b8806569e1d0f8cb96ee Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Fri, 20 Dec 2019 12:06:37 -0800 Subject: [PATCH 15/28] Remove OpenCensus specific tests --- .../test_tracing_decorator_async.py | 209 ----------------- .../tests/test_tracing_decorator.py | 212 ------------------ 2 files changed, 421 deletions(-) delete mode 100644 sdk/core/azure-core-tracing-opencensus/tests/asynctests/test_tracing_decorator_async.py delete mode 100644 sdk/core/azure-core-tracing-opencensus/tests/test_tracing_decorator.py diff --git a/sdk/core/azure-core-tracing-opencensus/tests/asynctests/test_tracing_decorator_async.py b/sdk/core/azure-core-tracing-opencensus/tests/asynctests/test_tracing_decorator_async.py deleted file mode 100644 index 1f7a3eecdf35..000000000000 --- a/sdk/core/azure-core-tracing-opencensus/tests/asynctests/test_tracing_decorator_async.py +++ /dev/null @@ -1,209 +0,0 @@ -# ------------------------------------ -# 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 -import time - -import pytest -from azure.core.pipeline import Pipeline, PipelineResponse -from azure.core.pipeline.policies import HTTPPolicy -from azure.core.pipeline.transport import HttpTransport, HttpRequest -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 - - -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): - if self.assert_current_span: - assert execution_context.get_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(merge_span=True) - kwargs['merge_span'] = True - await self.make_request(numb_times - 1, **kwargs) - return response - - @distributed_trace_async - async def merge_span_method(self): - return await self.get_foo(merge_span=True) - - @distributed_trace_async - async def no_merge_span_method(self): - return await self.get_foo() - - @distributed_trace_async - async def get_foo(self): - time.sleep(0.001) - return 5 - - @distributed_trace_async(name_of_span="different name") - async def check_name_is_different(self): - time.sleep(0.001) - - @distributed_trace_async - async def raising_exception(self): - raise ValueError("Something went horribly wrong here") - - -@pytest.mark.asyncio -async def test_decorator_has_different_name(): - with ContextHelper(): - exporter = MockExporter() - trace = tracer_module.Tracer(sampler=AlwaysOnSampler(), exporter=exporter) - with trace.span("overall"): - client = MockClient() - await client.check_name_is_different() - trace.finish() - exporter.build_tree() - parent = exporter.root - assert len(parent.children) == 2 - assert parent.children[0].span_data.name == "MockClient.__init__" - assert parent.children[1].span_data.name == "different name" - - -@pytest.mark.skip(reason="Don't think this test makes sense anymore") -@pytest.mark.asyncio -async def test_with_nothing_imported(): - with ContextHelper(): - opencensus = sys.modules["opencensus"] - del sys.modules["opencensus"] - try: - client = MockClient(assert_current_span=True) - with pytest.raises(AssertionError): - await client.make_request(3) - finally: - sys.modules["opencensus"] = opencensus - - -@pytest.mark.skip(reason="Don't think this test makes sense anymore") -@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", ["opencensus", None]) -@pytest.mark.asyncio -async def test_span_with_opencensus_merge_span(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() - await client.merge_span_method() - await client.no_merge_span_method() - 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.merge_span_method" - assert not parent.children[1].children - assert parent.children[2].span_data.name == "MockClient.no_merge_span_method" - assert parent.children[2].children[0].span_data.name == "MockClient.get_foo" - - -@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: - time.sleep(0.001) - 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 not parent.children[0].children - assert parent.children[1].span_data.name == "MockClient.make_request" - assert not parent.children[1].children - 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 not parent.children[3].children - -@pytest.mark.parametrize("value", [None, "opencensus"]) -@pytest.mark.asyncio -async def test_span_with_exception(value): - """Assert that if an exception is raised, the next sibling method is actually a sibling span. - """ - with ContextHelper(tracer_to_use=value): - exporter = MockExporter() - trace = tracer_module.Tracer(sampler=AlwaysOnSampler(), exporter=exporter) - with trace.span("overall"): - client = MockClient() - try: - await client.raising_exception() - except: - pass - await client.get_foo() - trace.finish() - exporter.build_tree() - parent = exporter.root - assert len(parent.children) == 3 - assert parent.children[0].span_data.name == "MockClient.__init__" - assert parent.children[1].span_data.name == "MockClient.raising_exception" - # Exception should propagate status for Opencensus - assert parent.children[1].span_data.status.message == 'Something went horribly wrong here' - assert parent.children[2].span_data.name == "MockClient.get_foo" diff --git a/sdk/core/azure-core-tracing-opencensus/tests/test_tracing_decorator.py b/sdk/core/azure-core-tracing-opencensus/tests/test_tracing_decorator.py deleted file mode 100644 index 453b09d5f902..000000000000 --- a/sdk/core/azure-core-tracing-opencensus/tests/test_tracing_decorator.py +++ /dev/null @@ -1,212 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -"""The tests for decorators.py and common.py""" - -try: - from unittest import mock -except ImportError: - import mock - -import sys -import time - -import pytest -from azure.core.pipeline import Pipeline, PipelineResponse -from azure.core.pipeline.policies import HTTPPolicy -from azure.core.pipeline.transport import HttpTransport, HttpRequest -from azure.core.settings import settings -from azure.core.tracing import common -from azure.core.tracing.decorator import distributed_trace -from azure.core.tracing.ext.opencensus_span import OpenCensusSpan -from opencensus.trace import tracer as tracer_module, execution_context -from opencensus.trace.samplers import AlwaysOnSampler -from tracing_common import ContextHelper, MockExporter - - -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): - if self.assert_current_span: - assert execution_context.get_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(merge_span=True) - kwargs['merge_span'] = True - self.make_request(numb_times - 1, **kwargs) - return response - - @distributed_trace - def merge_span_method(self): - return self.get_foo(merge_span=True) - - @distributed_trace - def no_merge_span_method(self): - return self.get_foo() - - @distributed_trace - def get_foo(self): - time.sleep(0.001) - return 5 - - @distributed_trace(name_of_span="different name") - def check_name_is_different(self): - time.sleep(0.001) - - @distributed_trace - def raising_exception(self): - raise ValueError("Something went horribly wrong here") - - -def random_function(): - pass - - -class TestCommon(object): - def test_get_function_and_class_name(self): - with ContextHelper(): - client = MockClient() - assert common.get_function_and_class_name(client.get_foo, client) == "MockClient.get_foo" - assert common.get_function_and_class_name(random_function) == "random_function" - - -class TestDecorator(object): - def test_decorator_has_different_name(self): - with ContextHelper(): - exporter = MockExporter() - trace = tracer_module.Tracer(sampler=AlwaysOnSampler(), exporter=exporter) - with trace.span("overall"): - client = MockClient() - client.check_name_is_different() - trace.finish() - exporter.build_tree() - parent = exporter.root - assert len(parent.children) == 2 - assert parent.children[0].span_data.name == "MockClient.__init__" - assert parent.children[1].span_data.name == "different name" - - @pytest.mark.skip(reason="Don't think this test makes sense anymore") - def test_with_nothing_imported(self): - with ContextHelper(): - opencensus = sys.modules["opencensus"] - del sys.modules["opencensus"] - try: - client = MockClient(assert_current_span=True) - with pytest.raises(AssertionError): - client.make_request(3) - finally: - sys.modules["opencensus"] = opencensus - - @pytest.mark.skip(reason="Don't think this test makes sense anymore") - 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_merge_span(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.merge_span_method() - client.no_merge_span_method() - 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.merge_span_method" - assert not parent.children[1].children - assert parent.children[2].span_data.name == "MockClient.no_merge_span_method" - assert parent.children[2].children[0].span_data.name == "MockClient.get_foo" - - @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: - time.sleep(0.001) - 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 not parent.children[0].children - assert parent.children[1].span_data.name == "MockClient.make_request" - assert not parent.children[1].children - 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 not parent.children[3].children - - @pytest.mark.parametrize("value", ["opencensus", None]) - def test_span_with_exception(self, value): - """Assert that if an exception is raised, the next sibling method is actually a sibling span. - """ - with ContextHelper(tracer_to_use=value): - exporter = MockExporter() - trace = tracer_module.Tracer(sampler=AlwaysOnSampler(), exporter=exporter) - with trace.span("overall"): - client = MockClient() - try: - client.raising_exception() - except: - pass - client.get_foo() - trace.finish() - exporter.build_tree() - parent = exporter.root - assert len(parent.children) == 3 - assert parent.children[0].span_data.name == "MockClient.__init__" - assert parent.children[1].span_data.name == "MockClient.raising_exception" - # Exception should propagate status for Opencensus - assert parent.children[1].span_data.status.message == 'Something went horribly wrong here' - assert parent.children[2].span_data.name == "MockClient.get_foo" From 1f5f92a32a8d7575d9758ef02fde1606802a14eb Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Fri, 20 Dec 2019 12:09:17 -0800 Subject: [PATCH 16/28] Remove useless OpenCensus tests --- .../tests/conftest.py | 31 ---- .../tests/test_tracing_policy.py | 169 ------------------ 2 files changed, 200 deletions(-) delete mode 100644 sdk/core/azure-core-tracing-opencensus/tests/conftest.py delete mode 100644 sdk/core/azure-core-tracing-opencensus/tests/test_tracing_policy.py diff --git a/sdk/core/azure-core-tracing-opencensus/tests/conftest.py b/sdk/core/azure-core-tracing-opencensus/tests/conftest.py deleted file mode 100644 index f9bb5ef13940..000000000000 --- a/sdk/core/azure-core-tracing-opencensus/tests/conftest.py +++ /dev/null @@ -1,31 +0,0 @@ -# -------------------------------------------------------------------------- -# -# 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. -# -# -------------------------------------------------------------------------- -import sys - -# Ignore collection of async tests for Python 2 -collect_ignore = [] -if sys.version_info < (3, 5): - collect_ignore.append("asynctests") diff --git a/sdk/core/azure-core-tracing-opencensus/tests/test_tracing_policy.py b/sdk/core/azure-core-tracing-opencensus/tests/test_tracing_policy.py deleted file mode 100644 index 57e467007432..000000000000 --- a/sdk/core/azure-core-tracing-opencensus/tests/test_tracing_policy.py +++ /dev/null @@ -1,169 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -"""Tests for the distributed tracing policy.""" -import logging - -from azure.core.pipeline import PipelineResponse, PipelineRequest, PipelineContext -from azure.core.pipeline.policies import DistributedTracingPolicy, UserAgentPolicy -from azure.core.pipeline.transport import HttpRequest, HttpResponse -from opencensus.trace import tracer as tracer_module -from opencensus.trace.samplers import AlwaysOnSampler -from azure.core.tracing.ext.opencensus_span import OpenCensusSpan -from tracing_common import ContextHelper, MockExporter -import time -import pytest - - -def test_distributed_tracing_policy_solo(): - """Test policy with no other policy and happy path""" - with ContextHelper(): - exporter = MockExporter() - trace = tracer_module.Tracer(sampler=AlwaysOnSampler(), exporter=exporter) - with trace.span("parent"): - policy = DistributedTracingPolicy() - - request = HttpRequest("GET", "http://127.0.0.1/temp?query=query") - request.headers["x-ms-client-request-id"] = "some client request id" - - pipeline_request = PipelineRequest(request, PipelineContext(None)) - policy.on_request(pipeline_request) - - response = HttpResponse(request, None) - response.headers = request.headers - response.status_code = 202 - response.headers["x-ms-request-id"] = "some request id" - - ctx = trace.span_context - header = trace.propagator.to_headers(ctx) - assert request.headers.get("traceparent") == header.get("traceparent") - - policy.on_response(pipeline_request, PipelineResponse(request, response, PipelineContext(None))) - time.sleep(0.001) - policy.on_request(pipeline_request) - policy.on_exception(pipeline_request) - - trace.finish() - exporter.build_tree() - parent = exporter.root - network_span = parent.children[0] - assert network_span.span_data.name == "/temp" - assert network_span.span_data.attributes.get("http.method") == "GET" - assert network_span.span_data.attributes.get("component") == "http" - assert network_span.span_data.attributes.get("http.url") == "http://127.0.0.1/temp?query=query" - assert network_span.span_data.attributes.get("http.user_agent") is None - assert network_span.span_data.attributes.get("x-ms-request-id") == "some request id" - assert network_span.span_data.attributes.get("x-ms-client-request-id") == "some client request id" - assert network_span.span_data.attributes.get("http.status_code") == 202 - - network_span = parent.children[1] - assert network_span.span_data.name == "/temp" - assert network_span.span_data.attributes.get("http.method") == "GET" - assert network_span.span_data.attributes.get("component") == "http" - assert network_span.span_data.attributes.get("http.url") == "http://127.0.0.1/temp?query=query" - assert network_span.span_data.attributes.get("x-ms-client-request-id") == "some client request id" - assert network_span.span_data.attributes.get("http.user_agent") is None - assert network_span.span_data.attributes.get("x-ms-request-id") == None - assert network_span.span_data.attributes.get("http.status_code") == 504 - - -def test_distributed_tracing_policy_badurl(caplog): - """Test policy with a bad url that will throw, and be sure policy ignores it""" - with ContextHelper(): - exporter = MockExporter() - trace = tracer_module.Tracer(sampler=AlwaysOnSampler(), exporter=exporter) - with trace.span("parent"): - policy = DistributedTracingPolicy() - - request = HttpRequest("GET", "http://[[[") - request.headers["x-ms-client-request-id"] = "some client request id" - - pipeline_request = PipelineRequest(request, PipelineContext(None)) - with caplog.at_level(logging.WARNING, logger="azure.core.pipeline.policies.distributed_tracing"): - policy.on_request(pipeline_request) - assert "Unable to start network span" in caplog.text - - response = HttpResponse(request, None) - response.headers = request.headers - response.status_code = 202 - response.headers["x-ms-request-id"] = "some request id" - - ctx = trace.span_context - header = trace.propagator.to_headers(ctx) - assert request.headers.get("traceparent") is None # Got not network trace - - policy.on_response(pipeline_request, PipelineResponse(request, response, PipelineContext(None))) - time.sleep(0.001) - policy.on_request(pipeline_request) - policy.on_exception(pipeline_request) - - trace.finish() - exporter.build_tree() - parent = exporter.root - assert len(parent.children) == 0 - - -def test_distributed_tracing_policy_with_user_agent(): - """Test policy working with user agent.""" - with ContextHelper(environ={"AZURE_HTTP_USER_AGENT": "mytools"}): - exporter = MockExporter() - trace = tracer_module.Tracer(sampler=AlwaysOnSampler(), exporter=exporter) - with trace.span("parent"): - policy = DistributedTracingPolicy() - - request = HttpRequest("GET", "http://127.0.0.1") - request.headers["x-ms-client-request-id"] = "some client request id" - - pipeline_request = PipelineRequest(request, PipelineContext(None)) - - user_agent = UserAgentPolicy() - user_agent.on_request(pipeline_request) - policy.on_request(pipeline_request) - - response = HttpResponse(request, None) - response.headers = request.headers - response.status_code = 202 - response.headers["x-ms-request-id"] = "some request id" - pipeline_response = PipelineResponse(request, response, PipelineContext(None)) - - ctx = trace.span_context - header = trace.propagator.to_headers(ctx) - assert request.headers.get("traceparent") == header.get("traceparent") - - policy.on_response(pipeline_request, pipeline_response) - - time.sleep(0.001) - policy.on_request(pipeline_request) - try: - raise ValueError("Transport trouble") - except: - policy.on_exception(pipeline_request) - - user_agent.on_response(pipeline_request, pipeline_response) - - trace.finish() - exporter.build_tree() - parent = exporter.root - network_span = parent.children[0] - assert network_span.span_data.name == "/" - assert network_span.span_data.attributes.get("http.method") == "GET" - assert network_span.span_data.attributes.get("component") == "http" - assert network_span.span_data.attributes.get("http.url") == "http://127.0.0.1" - assert network_span.span_data.attributes.get("http.user_agent").endswith("mytools") - assert network_span.span_data.attributes.get("x-ms-request-id") == "some request id" - assert network_span.span_data.attributes.get("x-ms-client-request-id") == "some client request id" - assert network_span.span_data.attributes.get("http.status_code") == 202 - - network_span = parent.children[1] - assert network_span.span_data.name == "/" - assert network_span.span_data.attributes.get("http.method") == "GET" - assert network_span.span_data.attributes.get("component") == "http" - assert network_span.span_data.attributes.get("http.url") == "http://127.0.0.1" - assert network_span.span_data.attributes.get("http.user_agent").endswith("mytools") - assert network_span.span_data.attributes.get("x-ms-client-request-id") == "some client request id" - assert network_span.span_data.attributes.get("x-ms-request-id") is None - assert network_span.span_data.attributes.get("http.status_code") == 504 - # Exception should propagate status for Opencensus - assert network_span.span_data.status.message == 'Transport trouble' - From c40e9c52757b851e820b6b8b6cb2c2fcbee9096d Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Fri, 20 Dec 2019 15:51:19 -0800 Subject: [PATCH 17/28] OT testing --- .../ext/opentelemetry_span/__init__.py | 31 ++-- .../ext/opentelemetry_span/_version.py | 6 + .../dev_requirements.txt | 2 +- .../tests/conftest.py | 14 ++ .../tests/test_threading.py | 26 +++ .../tests/test_tracing_implementations.py | 161 ++++++++++++++++++ 6 files changed, 224 insertions(+), 16 deletions(-) create mode 100644 sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/_version.py create mode 100644 sdk/core/azure-core-tracing-opentelemetry/tests/conftest.py create mode 100644 sdk/core/azure-core-tracing-opentelemetry/tests/test_threading.py create mode 100644 sdk/core/azure-core-tracing-opentelemetry/tests/test_tracing_implementations.py diff --git a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py index 92c298c17965..651a85a3dfd0 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py +++ b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py @@ -10,6 +10,8 @@ from azure.core.tracing import SpanKind, HttpSpanMixin # pylint: disable=no-name-in-module +from ._version import VERSION + try: from typing import TYPE_CHECKING except ImportError: @@ -20,7 +22,7 @@ from azure.core.pipeline.transport import HttpRequest, HttpResponse -__version__ = "1.0.0b4" +__version__ = VERSION def _get_headers_from_http_request_headers(headers: "Mapping[str, Any]", key: str): @@ -29,7 +31,7 @@ def _get_headers_from_http_request_headers(headers: "Mapping[str, Any]", key: st Must comply to opentelemetry.context.propagation.httptextformat.Getter: Getter = typing.Callable[[_T, str], typing.List[str]] """ - return [headers[key]] + return [headers.get(key, "")] def _set_headers_from_http_request_headers(headers: "Mapping[str, Any]", key: str, value: str): @@ -55,8 +57,8 @@ def __init__(self, span=None, name="span"): :param name: The name of the OpenTelemetry span to create if a new span is needed :type name: str """ - tracer = self.get_current_tracer() - self._span_instance = span or tracer.create_span(name=name) + current_tracer = self.get_current_tracer() + self._span_instance = span or current_tracer.start_span(name=name) self._current_ctxt_manager = None @property @@ -83,12 +85,11 @@ def kind(self): """Get the span kind of this span.""" value = self.span_instance.kind return ( - SpanKind.CLIENT if value == OpenCensusSpanKind.CLIENT else - SpanKind.PRODUCER if value == OpenCensusSpanKind.PRODUCER else - SpanKind.SERVER if value == OpenCensusSpanKind.SERVER else - SpanKind.CONSUMER if value == OpenCensusSpanKind.CONSUMER else - SpanKind.INTERNAL if value == OpenCensusSpanKind.INTERNAL else - SpanKind.UNSPECIFIED if value == OpenCensusSpanKind.UNSPECIFIED else + SpanKind.CLIENT if value == OpenTelemetrySpanKind.CLIENT else + SpanKind.PRODUCER if value == OpenTelemetrySpanKind.PRODUCER else + SpanKind.SERVER if value == OpenTelemetrySpanKind.SERVER else + SpanKind.CONSUMER if value == OpenTelemetrySpanKind.CONSUMER else + SpanKind.INTERNAL if value == OpenTelemetrySpanKind.INTERNAL else None ) @@ -103,7 +104,7 @@ def kind(self, value): OpenTelemetrySpanKind.SERVER if value == SpanKind.SERVER else OpenTelemetrySpanKind.CONSUMER if value == SpanKind.CONSUMER else OpenTelemetrySpanKind.INTERNAL if value == SpanKind.INTERNAL else - OpenTelemetrySpanKind.UNSPECIFIED if value == SpanKind.UNSPECIFIED else + OpenTelemetrySpanKind.INTERNAL if value == SpanKind.UNSPECIFIED else None ) if kind is None: @@ -112,7 +113,7 @@ def kind(self, value): def __enter__(self): """Start a span.""" - self._span_instance.start() + self.start() self._current_ctxt_manager = self.get_current_tracer().use_span(self._span_instance, end_on_exit=True) self._current_ctxt_manager.__enter__() return self @@ -120,7 +121,7 @@ def __enter__(self): def __exit__(self, exception_type, exception_value, traceback): """Finish a span.""" if not self._current_ctxt_manager: - raise ValueError("Trying to manually exit a ctxt manager that didn't started") + raise ValueError("Trying to manually exit a ctxt manager that didn't start") self._current_ctxt_manager.__exit__(exception_type, exception_value, traceback) def start(self): @@ -194,7 +195,7 @@ def link_from_headers(cls, headers): """ ctx = extract(_get_headers_from_http_request_headers, headers) current_span = cls.get_current_span() - current_span.add_link(ctx) + current_span.links.append(ctx) @classmethod def get_current_span(cls): @@ -224,7 +225,7 @@ def set_current_span(cls, span): # type: (Span) -> None """Not supported by OpenTelemetry. """ - raise NotImplementedError("set_current_span is not supported by OpenTelemetry plugin. Use ChangeContext instead.") + raise NotImplementedError("set_current_span is not supported by OpenTelemetry plugin. Use change_context instead.") @classmethod def set_current_tracer(cls, tracer): diff --git a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/_version.py b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/_version.py new file mode 100644 index 000000000000..6159d061136f --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/_version.py @@ -0,0 +1,6 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +VERSION = "1.0.0b4" diff --git a/sdk/core/azure-core-tracing-opentelemetry/dev_requirements.txt b/sdk/core/azure-core-tracing-opentelemetry/dev_requirements.txt index 47a1b07ae4c2..8cd864f5defd 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/dev_requirements.txt +++ b/sdk/core/azure-core-tracing-opentelemetry/dev_requirements.txt @@ -1,3 +1,3 @@ -e ../../../tools/azure-sdk-tools ../azure-core -opentelemetry-sdk \ No newline at end of file +opentelemetry-sdk>=0.3a0 \ No newline at end of file diff --git a/sdk/core/azure-core-tracing-opentelemetry/tests/conftest.py b/sdk/core/azure-core-tracing-opentelemetry/tests/conftest.py new file mode 100644 index 000000000000..a834f86a40f6 --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/tests/conftest.py @@ -0,0 +1,14 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from opentelemetry import trace +from opentelemetry.sdk.trace import Tracer + +import pytest + + +@pytest.fixture(scope="session") +def tracer(): + trace.set_preferred_tracer_implementation(lambda T: Tracer()) + return trace.tracer() diff --git a/sdk/core/azure-core-tracing-opentelemetry/tests/test_threading.py b/sdk/core/azure-core-tracing-opentelemetry/tests/test_threading.py new file mode 100644 index 000000000000..ffdea46be91f --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/tests/test_threading.py @@ -0,0 +1,26 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import threading + +from azure.core.tracing.ext.opentelemetry_span import OpenTelemetrySpan + + +def test_get_span_from_thread(tracer): + + result = [] + def get_span_from_thread(output): + current_span = OpenTelemetrySpan.get_current_span() + output.append(current_span) + + with tracer.start_as_current_span(name="TestSpan") as span: + + thread = threading.Thread( + target=OpenTelemetrySpan.with_current_context(get_span_from_thread), + args=(result,) + ) + thread.start() + thread.join() + + assert span is result[0] diff --git a/sdk/core/azure-core-tracing-opentelemetry/tests/test_tracing_implementations.py b/sdk/core/azure-core-tracing-opentelemetry/tests/test_tracing_implementations.py new file mode 100644 index 000000000000..bff5133860fd --- /dev/null +++ b/sdk/core/azure-core-tracing-opentelemetry/tests/test_tracing_implementations.py @@ -0,0 +1,161 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""The tests for opencensus_span.py""" + +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from opentelemetry.trace import SpanKind as OpenTelemetrySpanKind + +from azure.core.tracing.ext.opentelemetry_span import OpenTelemetrySpan +from azure.core.tracing import SpanKind +import os + +import pytest + + +class TestOpentelemetryWrapper: + def test_span_passed_in(self, tracer): + with tracer.start_as_current_span(name="parent") as parent: + wrapped_span = OpenTelemetrySpan(parent) + + assert wrapped_span.span_instance.name == "parent" + assert parent is tracer.get_current_span() + assert wrapped_span.span_instance is tracer.get_current_span() + + assert parent is tracer.get_current_span() + + def test_no_span_passed_in_with_no_environ(self, tracer): + with tracer.start_as_current_span("Root") as parent: + with OpenTelemetrySpan() as wrapped_span: + + assert wrapped_span.span_instance.name == "span" + assert wrapped_span.span_instance is tracer.get_current_span() + + assert parent is tracer.get_current_span() + + + def test_span(self, tracer): + with tracer.start_as_current_span("Root") as parent: + assert OpenTelemetrySpan.get_current_tracer() is tracer + with OpenTelemetrySpan() as wrapped_span: + assert wrapped_span.span_instance is tracer.get_current_span() + + with wrapped_span.span() as child: + assert child.span_instance.name == "span" + assert child.span_instance is tracer.get_current_span() + assert child.span_instance.parent is wrapped_span.span_instance + + def test_start_finish(self, tracer): + with tracer.start_as_current_span("Root") as parent: + wrapped_class = OpenTelemetrySpan() + assert wrapped_class.span_instance.end_time is None + wrapped_class.start() + wrapped_class.finish() + assert wrapped_class.span_instance.start_time is not None + assert wrapped_class.span_instance.end_time is not None + + def test_change_context(self, tracer): + with tracer.start_as_current_span("Root") as parent: + with OpenTelemetrySpan() as wrapped_class: + with OpenTelemetrySpan.change_context(parent): + assert tracer.get_current_span() is parent + + def test_to_header(self, tracer): + with tracer.start_as_current_span("Root") as parent: + wrapped_class = OpenTelemetrySpan() + headers = wrapped_class.to_header() + assert "traceparent" in headers + assert headers["traceparent"].startswith("00-") + + traceparent = wrapped_class.get_trace_parent() + assert traceparent.startswith("00-") + + assert traceparent == headers["traceparent"] + + def test_links(self, tracer): + with tracer.start_as_current_span("Root") as parent: + og_header = {"traceparent": "00-2578531519ed94423ceae67588eff2c9-231ebdc614cb9ddd-01"} + with OpenTelemetrySpan() as wrapped_class: + OpenTelemetrySpan.link_from_headers(og_header) + + assert len(wrapped_class.span_instance.links) == 1 + link = wrapped_class.span_instance.links[0] + + assert link.trace_id == int("2578531519ed94423ceae67588eff2c9", 16) + assert link.span_id == int("231ebdc614cb9ddd", 16) + + with OpenTelemetrySpan() as wrapped_class: + OpenTelemetrySpan.link("00-2578531519ed94423ceae67588eff2c9-231ebdc614cb9ddd-01") + + assert len(wrapped_class.span_instance.links) == 1 + link = wrapped_class.span_instance.links[0] + + assert link.trace_id == int("2578531519ed94423ceae67588eff2c9", 16) + assert link.span_id == int("231ebdc614cb9ddd", 16) + + + def test_add_attribute(self, tracer): + with tracer.start_as_current_span("Root") as parent: + wrapped_class = OpenTelemetrySpan(span=parent) + wrapped_class.add_attribute("test", "test2") + assert wrapped_class.span_instance.attributes["test"] == "test2" + assert parent.attributes["test"] == "test2" + + def test_set_http_attributes(self, tracer): + with tracer.start_as_current_span("Root") as parent: + wrapped_class = OpenTelemetrySpan(span=parent) + request = mock.Mock() + setattr(request, "method", "GET") + setattr(request, "url", "some url") + response = mock.Mock() + setattr(request, "headers", {}) + setattr(response, "status_code", 200) + wrapped_class.set_http_attributes(request) + assert wrapped_class.span_instance.kind == OpenTelemetrySpanKind.CLIENT + assert wrapped_class.span_instance.attributes.get("http.method") == request.method + assert wrapped_class.span_instance.attributes.get("component") == "http" + assert wrapped_class.span_instance.attributes.get("http.url") == request.url + assert wrapped_class.span_instance.attributes.get("http.status_code") == 504 + assert wrapped_class.span_instance.attributes.get("http.user_agent") is None + request.headers["User-Agent"] = "some user agent" + wrapped_class.set_http_attributes(request, response) + assert wrapped_class.span_instance.attributes.get("http.status_code") == response.status_code + assert wrapped_class.span_instance.attributes.get("http.user_agent") == request.headers.get("User-Agent") + + def test_span_kind(self, tracer): + with tracer.start_as_current_span("Root") as parent: + wrapped_class = OpenTelemetrySpan(span=parent) + + wrapped_class.kind = SpanKind.UNSPECIFIED + assert wrapped_class.span_instance.kind == OpenTelemetrySpanKind.INTERNAL + assert wrapped_class.kind == SpanKind.INTERNAL + + wrapped_class.kind = SpanKind.SERVER + assert wrapped_class.span_instance.kind == OpenTelemetrySpanKind.SERVER + assert wrapped_class.kind == SpanKind.SERVER + + wrapped_class.kind = SpanKind.CLIENT + assert wrapped_class.span_instance.kind == OpenTelemetrySpanKind.CLIENT + assert wrapped_class.kind == SpanKind.CLIENT + + wrapped_class.kind = SpanKind.PRODUCER + assert wrapped_class.span_instance.kind == OpenTelemetrySpanKind.PRODUCER + assert wrapped_class.kind == SpanKind.PRODUCER + + wrapped_class.kind = SpanKind.CONSUMER + assert wrapped_class.span_instance.kind == OpenTelemetrySpanKind.CONSUMER + assert wrapped_class.kind == SpanKind.CONSUMER + + wrapped_class.kind = SpanKind.INTERNAL + assert wrapped_class.span_instance.kind == OpenTelemetrySpanKind.INTERNAL + assert wrapped_class.kind == SpanKind.INTERNAL + + with pytest.raises(ValueError): + wrapped_class.kind = "somethingstuid" From 7f65e8c7fbd2a8aa521d3042d170edfd454f784c Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Fri, 20 Dec 2019 15:53:25 -0800 Subject: [PATCH 18/28] Versionning --- sdk/core/azure-core-tracing-opentelemetry/HISTORY.md | 4 ++-- .../azure/core/tracing/ext/opentelemetry_span/_version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/core/azure-core-tracing-opentelemetry/HISTORY.md b/sdk/core/azure-core-tracing-opentelemetry/HISTORY.md index 84eccb33312a..13b9c1776416 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/HISTORY.md +++ b/sdk/core/azure-core-tracing-opentelemetry/HISTORY.md @@ -3,8 +3,8 @@ ------------------- -## 2019-10-07 Version 1.0.0b4 +## 1.0.0 Unreleased ### Features -- Opencensus implementation of azure-core tracing protocol \ No newline at end of file +- Opentelemetry implementation of azure-core tracing protocol \ No newline at end of file diff --git a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/_version.py b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/_version.py index 6159d061136f..8eedef9ba349 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/_version.py +++ b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/_version.py @@ -3,4 +3,4 @@ # Licensed under the MIT License. # ------------------------------------ -VERSION = "1.0.0b4" +VERSION = "1.0.0" From 15f3745ae34d4133c6971a247c7254100767fdae Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Mon, 23 Dec 2019 12:10:09 -0800 Subject: [PATCH 19/28] Fix Link for OT --- .../azure/core/tracing/ext/opentelemetry_span/__init__.py | 4 ++-- .../tests/test_tracing_implementations.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py index 651a85a3dfd0..434911f2fa84 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py +++ b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py @@ -4,7 +4,7 @@ # ------------------------------------ """Implements azure.core.tracing.AbstractSpan to wrap OpenTelemetry spans.""" -from opentelemetry.trace import Span, Tracer, SpanKind as OpenTelemetrySpanKind, tracer +from opentelemetry.trace import Span, Link, Tracer, SpanKind as OpenTelemetrySpanKind, tracer from opentelemetry.context import Context from opentelemetry.propagators import extract, inject @@ -195,7 +195,7 @@ def link_from_headers(cls, headers): """ ctx = extract(_get_headers_from_http_request_headers, headers) current_span = cls.get_current_span() - current_span.links.append(ctx) + current_span.links.append(Link(ctx)) @classmethod def get_current_span(cls): diff --git a/sdk/core/azure-core-tracing-opentelemetry/tests/test_tracing_implementations.py b/sdk/core/azure-core-tracing-opentelemetry/tests/test_tracing_implementations.py index bff5133860fd..9cda91b01faa 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/tests/test_tracing_implementations.py +++ b/sdk/core/azure-core-tracing-opentelemetry/tests/test_tracing_implementations.py @@ -88,8 +88,8 @@ def test_links(self, tracer): assert len(wrapped_class.span_instance.links) == 1 link = wrapped_class.span_instance.links[0] - assert link.trace_id == int("2578531519ed94423ceae67588eff2c9", 16) - assert link.span_id == int("231ebdc614cb9ddd", 16) + assert link.context.trace_id == int("2578531519ed94423ceae67588eff2c9", 16) + assert link.context.span_id == int("231ebdc614cb9ddd", 16) with OpenTelemetrySpan() as wrapped_class: OpenTelemetrySpan.link("00-2578531519ed94423ceae67588eff2c9-231ebdc614cb9ddd-01") @@ -97,8 +97,8 @@ def test_links(self, tracer): assert len(wrapped_class.span_instance.links) == 1 link = wrapped_class.span_instance.links[0] - assert link.trace_id == int("2578531519ed94423ceae67588eff2c9", 16) - assert link.span_id == int("231ebdc614cb9ddd", 16) + assert link.context.trace_id == int("2578531519ed94423ceae67588eff2c9", 16) + assert link.context.span_id == int("231ebdc614cb9ddd", 16) def test_add_attribute(self, tracer): From 3026eb20dd54030ec19da95642c23724087cd271 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Mon, 23 Dec 2019 13:11:55 -0800 Subject: [PATCH 20/28] Fix setup.py --- sdk/core/azure-core-tracing-opentelemetry/setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/core/azure-core-tracing-opentelemetry/setup.py b/sdk/core/azure-core-tracing-opentelemetry/setup.py index abbafc03b88e..b2a49e596821 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/setup.py +++ b/sdk/core/azure-core-tracing-opentelemetry/setup.py @@ -18,8 +18,8 @@ package_folder_path = "azure/core/tracing/ext/opentelemetry_span" # Version extraction inspired from 'requests' -with open(os.path.join(package_folder_path, '__init__.py'), 'r') as fd: - version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', # type: ignore +with open(os.path.join(package_folder_path, '_version.py'), 'r') as fd: + version = re.search(r'^VERSION\s*=\s*[\'"]([^\'"]*)[\'"]', # type: ignore fd.read(), re.MULTILINE).group(1) if not version: From deeaea0e376cd79d6f37ab8c7583f6051eb51ee7 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Mon, 23 Dec 2019 16:53:37 -0800 Subject: [PATCH 21/28] pylint --- sdk/core/azure-core/azure/core/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/core/azure-core/azure/core/settings.py b/sdk/core/azure-core/azure/core/settings.py index 2e500682e2bd..14b6329ac27a 100644 --- a/sdk/core/azure-core/azure/core/settings.py +++ b/sdk/core/azure-core/azure/core/settings.py @@ -30,7 +30,6 @@ from enum import Enum import logging import os -import sys import six from azure.core.tracing import AbstractSpan From 8672bcc8be808c9164e991f6d9fc29601906c654 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Mon, 23 Dec 2019 16:56:53 -0800 Subject: [PATCH 22/28] Dependency work --- sdk/core/azure-core-tracing-opentelemetry/README.md | 2 ++ sdk/core/azure-core-tracing-opentelemetry/setup.py | 1 - shared_requirements.txt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/sdk/core/azure-core-tracing-opentelemetry/README.md b/sdk/core/azure-core-tracing-opentelemetry/README.md index 3db2d863b5f5..8b23c43afdbf 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/README.md +++ b/sdk/core/azure-core-tracing-opentelemetry/README.md @@ -52,6 +52,8 @@ with tracer.start_as_current_span(name="MyApplication"): client.delete_container('mycontainer') # Call will be traced ``` +Azure Exporter can be found in the package `opentelemetry-azure-monitor-exporter` + ## Troubleshooting diff --git a/sdk/core/azure-core-tracing-opentelemetry/setup.py b/sdk/core/azure-core-tracing-opentelemetry/setup.py index b2a49e596821..3b519fdcce56 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/setup.py +++ b/sdk/core/azure-core-tracing-opentelemetry/setup.py @@ -58,7 +58,6 @@ ], install_requires=[ 'opentelemetry-api>=0.3a0', - 'opentelemetry-azure-monitor-exporter>=0.1a0', 'azure-core<2.0.0,>=1.0.0', ], extras_require={ diff --git a/shared_requirements.txt b/shared_requirements.txt index 934ed680a9e8..fc5610a5aeb7 100644 --- a/shared_requirements.txt +++ b/shared_requirements.txt @@ -120,6 +120,7 @@ six>=1.6 opencensus>=0.6.0 opencensus-ext-threading opencensus-ext-azure>=0.3.1 +opentelemetry-api>=0.3a0 #override azure-eventhub-checkpointstoreblob-aio azure-storage-blob<13.0.0,>=12.0.0 #override azure-eventhub-checkpointstoreblob azure-storage-blob<13.0.0,>=12.0.0 #override azure-eventhub-checkpointstoreblob-aio aiohttp<4.0,>=3.0 From 83bd6e58b484bda19a5704930010b5442c069f64 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Mon, 23 Dec 2019 17:13:35 -0800 Subject: [PATCH 23/28] pylint --- .../azure/core/tracing/ext/opentelemetry_span/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py index 434911f2fa84..3cd8e0fea824 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py +++ b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py @@ -225,10 +225,12 @@ def set_current_span(cls, span): # type: (Span) -> None """Not supported by OpenTelemetry. """ - raise NotImplementedError("set_current_span is not supported by OpenTelemetry plugin. Use change_context instead.") + raise NotImplementedError( + "set_current_span is not supported by OpenTelemetry plugin. Use change_context instead." + ) @classmethod - def set_current_tracer(cls, tracer): + def set_current_tracer(cls, _): # type: (Tracer) -> None """ Set the given tracer as the current tracer in the execution context. @@ -236,7 +238,6 @@ def set_current_tracer(cls, tracer): :type tracer: :class: OpenTelemetry.trace.Tracer """ # Do nothing, if you're able to get two tracer with OpenTelemetry that's a surprise! - pass @classmethod def with_current_context(cls, func): From 9fe77f7af694e07950f2e7fea97944f7da41d5e4 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Thu, 26 Dec 2019 10:17:09 -0800 Subject: [PATCH 24/28] Need Python 3.x at least --- sdk/core/azure-core-tracing-opentelemetry/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/core/azure-core-tracing-opentelemetry/setup.py b/sdk/core/azure-core-tracing-opentelemetry/setup.py index 3b519fdcce56..11e2d41247e8 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/setup.py +++ b/sdk/core/azure-core-tracing-opentelemetry/setup.py @@ -56,6 +56,7 @@ packages=[ 'azure.core.tracing.ext.opentelemetry_span', ], + python_requires=">=3.5.0", install_requires=[ 'opentelemetry-api>=0.3a0', 'azure-core<2.0.0,>=1.0.0', From 00e94c6e5bf3c4fb84480a00de82aac70ccdcd18 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Thu, 9 Jan 2020 13:34:10 -0800 Subject: [PATCH 25/28] Remove diff with master --- sdk/core/azure-core/tests/test_tracing_policy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/core/azure-core/tests/test_tracing_policy.py b/sdk/core/azure-core/tests/test_tracing_policy.py index bfeac7bb0e22..c52572d40357 100644 --- a/sdk/core/azure-core/tests/test_tracing_policy.py +++ b/sdk/core/azure-core/tests/test_tracing_policy.py @@ -116,6 +116,7 @@ def test_distributed_tracing_policy_badurl(caplog): policy.on_response(pipeline_request, PipelineResponse(request, response, PipelineContext(None))) time.sleep(0.001) + policy.on_request(pipeline_request) try: raise ValueError("Transport trouble") From afd12fa70f7890ca04377e8f186294ce0ad6b25f Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Fri, 10 Jan 2020 17:11:19 -0800 Subject: [PATCH 26/28] Remove core changes --- sdk/core/azure-core/azure/core/settings.py | 21 ++++++----- sdk/core/azure-core/tests/test_settings.py | 41 ---------------------- 2 files changed, 10 insertions(+), 52 deletions(-) diff --git a/sdk/core/azure-core/azure/core/settings.py b/sdk/core/azure-core/azure/core/settings.py index 14b6329ac27a..ab542fba11d2 100644 --- a/sdk/core/azure-core/azure/core/settings.py +++ b/sdk/core/azure-core/azure/core/settings.py @@ -30,6 +30,7 @@ from enum import Enum import logging import os +import sys import six from azure.core.tracing import AbstractSpan @@ -120,27 +121,26 @@ def convert_logging(value): return level -def _get_opencensus_span(): +def get_opencensus_span(): # type: () -> Optional[Type[AbstractSpan]] """Returns the OpenCensusSpan if opencensus is installed else returns None""" try: from azure.core.tracing.ext.opencensus_span import OpenCensusSpan # pylint:disable=redefined-outer-name + return OpenCensusSpan # type: ignore except ImportError: return None -def _get_opentelemetry_span(): + +def get_opencensus_span_if_opencensus_is_imported(): # type: () -> Optional[Type[AbstractSpan]] - """Returns the OpenTelemetrySpan if opentelemetry is installed else returns None""" - try: - from azure.core.tracing.ext.opentelemetry_span import OpenTelemetrySpan # pylint:disable=redefined-outer-name - return OpenTelemetrySpan # type: ignore - except ImportError: + if "opencensus" not in sys.modules: return None + return get_opencensus_span() + _tracing_implementation_dict = { - "opencensus": _get_opencensus_span, - "opentelemetry": _get_opentelemetry_span, + "opencensus": get_opencensus_span } # type: Dict[str, Callable[[], Optional[Type[AbstractSpan]]]] @@ -152,7 +152,6 @@ def convert_tracing_impl(value): understands the following strings, ignoring case: * "opencensus" - * "opentelemetry" :param value: the value to convert :type value: string @@ -161,7 +160,7 @@ def convert_tracing_impl(value): """ if value is None: - value = 'opencensus' + return get_opencensus_span_if_opencensus_is_imported() if not isinstance(value, six.string_types): return value diff --git a/sdk/core/azure-core/tests/test_settings.py b/sdk/core/azure-core/tests/test_settings.py index 4fcec1139dc5..a4abc5308d55 100644 --- a/sdk/core/azure-core/tests/test_settings.py +++ b/sdk/core/azure-core/tests/test_settings.py @@ -23,17 +23,11 @@ # THE SOFTWARE. # # -------------------------------------------------------------------------- -import collections import logging import os import sys import pytest -try: - from unittest import mock -except ImportError: - import mock - # module under test import azure.core.settings as m @@ -173,41 +167,6 @@ def test_convert_logging_bad(self): with pytest.raises(ValueError): m.convert_logging("junk") - def test_convert_implementation(self): - # Mostly it's here to be sure if a new plugin is added, someone check the tests - assert len(m._tracing_implementation_dict) == 2 - - with mock.patch.dict('azure.core.settings._tracing_implementation_dict', opencensus=lambda: None): - assert m.convert_tracing_impl(None) is None - assert m.convert_tracing_impl("opencensus") is None - - opencensus_span = mock.Mock() - with mock.patch.dict('azure.core.settings._tracing_implementation_dict', opencensus=lambda: opencensus_span): - assert m.convert_tracing_impl(None) is opencensus_span - assert m.convert_tracing_impl("opencensus") is opencensus_span - - opentelemetry_span = mock.Mock() - with mock.patch.dict('azure.core.settings._tracing_implementation_dict', opentelemetry= lambda: opentelemetry_span): - assert m.convert_tracing_impl("opentelemetry") is opentelemetry_span - # Take this opportunity to test case insensitive - assert m.convert_tracing_impl("OPENTELEMETRY") is opentelemetry_span - # 2.7 and unicode string should work - assert m.convert_tracing_impl(u"opentelemetry") is opentelemetry_span - - with pytest.raises(ValueError): - assert m.convert_tracing_impl("does not exist!!") - - def test_tracing_impl_loader(self): - mod = collections.namedtuple('mod', ['OpenCensusSpan']) - opencensus_span = mock.Mock() - with mock.patch.dict('sys.modules', {'azure.core.tracing.ext.opencensus_span': mod(opencensus_span)}): - assert m._get_opencensus_span() is opencensus_span - - mod = collections.namedtuple('mod', ['OpenTelemetrySpan']) - opentelemetry_span = mock.Mock() - with mock.patch.dict('sys.modules', {'azure.core.tracing.ext.opentelemetry_span': mod(opentelemetry_span)}): - assert m._get_opentelemetry_span() is opentelemetry_span - _standard_settings = ["log_level", "tracing_enabled"] From dde3859c0b5bddc8e888eaef3c1f042595477143 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Mon, 13 Jan 2020 10:14:38 -0800 Subject: [PATCH 27/28] Feedback --- .../HISTORY.md | 2 +- .../README.md | 30 +++++++++++++------ .../ext/opentelemetry_span/__init__.py | 23 +++++++------- .../ext/opentelemetry_span/_version.py | 2 +- 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/sdk/core/azure-core-tracing-opentelemetry/HISTORY.md b/sdk/core/azure-core-tracing-opentelemetry/HISTORY.md index 13b9c1776416..409a4e17d92e 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/HISTORY.md +++ b/sdk/core/azure-core-tracing-opentelemetry/HISTORY.md @@ -3,7 +3,7 @@ ------------------- -## 1.0.0 Unreleased +## 1.0.0b1 Unreleased ### Features diff --git a/sdk/core/azure-core-tracing-opentelemetry/README.md b/sdk/core/azure-core-tracing-opentelemetry/README.md index 8b23c43afdbf..528cc8c34687 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/README.md +++ b/sdk/core/azure-core-tracing-opentelemetry/README.md @@ -10,12 +10,19 @@ Install the opentelemetry python for Python with [pip](https://pypi.org/project/ pip install azure-core-tracing-opentelemetry --pre ``` -Now you can use opentelemetry for Python as usual with any SDKs that is compatible +Now you can use opentelemetry for Python as usual with any SDKs that are compatible with azure-core tracing. This includes (not exhaustive list), azure-storage-blob, azure-keyvault-secrets, azure-eventhub, etc. ## Key concepts * You don't need to pass any context, SDK will get it for you +* Those lines are the only ones you need to enable tracing + + ``` python + from azure.core.settings import settings + from azure.core.tracing.ext.opentelemetry_span import OpenTelemetrySpan + settings.tracing_implementation = OpenTelemetrySpan + ``` ## Examples @@ -24,32 +31,37 @@ call any SDK code that is compatible with azure-core tracing. This is an example using Azure Monitor exporter, but you can use any exporter (Zipkin, etc.). ```python -from opentelemetry.ext.azure_monitor import AzureMonitorSpanExporter - -from opentelemetry import trace -from opentelemetry.sdk.trace import Tracer +# Declare OpenTelemetry as enabled tracing plugin for Azure SDKs from azure.core.settings import settings from azure.core.tracing.ext.opentelemetry_span import OpenTelemetrySpan -from azure.storage.blob import BlobServiceClient - -# Declare that you want to trace Azure SDK with OpenTelemetry settings.tracing_implementation = OpenTelemetrySpan +# Example of Azure Monitor exporter, but you can use anything OpenTelemetry supports +from opentelemetry.ext.azure_monitor import AzureMonitorSpanExporter exporter = AzureMonitorSpanExporter( instrumentation_key="uuid of the instrumentation key (see your Azure Monitor account)" ) +# Regular open telemetry usage from here, see https://github.com/open-telemetry/opentelemetry-python +# for details +from opentelemetry import trace +from opentelemetry.sdk.trace import Tracer + trace.set_preferred_tracer_implementation(lambda T: Tracer()) tracer = trace.tracer() tracer.add_span_processor( SimpleExportSpanProcessor(exporter) ) +# Example with Storage SDKs + +from azure.storage.blob import BlobServiceClient + with tracer.start_as_current_span(name="MyApplication"): client = BlobServiceClient.from_connection_string('connectionstring') - client.delete_container('mycontainer') # Call will be traced + client.create_container('mycontainer') # Call will be traced ``` Azure Exporter can be found in the package `opentelemetry-azure-monitor-exporter` diff --git a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py index 3cd8e0fea824..851c33b3f62d 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py +++ b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/__init__.py @@ -44,19 +44,16 @@ def _set_headers_from_http_request_headers(headers: "Mapping[str, Any]", key: st class OpenTelemetrySpan(HttpSpanMixin, object): - """Wraps a given OpenTelemetry Span so that it implements azure.core.tracing.AbstractSpan""" + """OpenTelemetry plugin for Azure client libraries. + + :param span: The OpenTelemetry span to wrap, or nothing to create a new one. + :type span: ~OpenTelemetry.trace.Span + :param name: The name of the OpenTelemetry span to create if a new span is needed + :type name: str + """ def __init__(self, span=None, name="span"): # type: (Optional[Span], Optional[str]) -> None - """ - If a span is not passed in, creates a new tracer. If the instrumentation key for Azure Exporter is given, will - configure the azure exporter else will just create a new tracer. - - :param span: The OpenTelemetry span to wrap - :type span: :class: OpenTelemetry.trace.Span - :param name: The name of the OpenTelemetry span to create if a new span is needed - :type name: str - """ current_tracer = self.get_current_tracer() self._span_instance = span or current_tracer.start_span(name=name) self._current_ctxt_manager = None @@ -120,9 +117,9 @@ def __enter__(self): def __exit__(self, exception_type, exception_value, traceback): """Finish a span.""" - if not self._current_ctxt_manager: - raise ValueError("Trying to manually exit a ctxt manager that didn't start") - self._current_ctxt_manager.__exit__(exception_type, exception_value, traceback) + if self._current_ctxt_manager: + self._current_ctxt_manager.__exit__(exception_type, exception_value, traceback) + self._current_ctxt_manager = None def start(self): # type: () -> None diff --git a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/_version.py b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/_version.py index 8eedef9ba349..ac9f392f513e 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/_version.py +++ b/sdk/core/azure-core-tracing-opentelemetry/azure/core/tracing/ext/opentelemetry_span/_version.py @@ -3,4 +3,4 @@ # Licensed under the MIT License. # ------------------------------------ -VERSION = "1.0.0" +VERSION = "1.0.0b1" From ac63f1859c62325dd415ca779132ce00d85e5db3 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Mon, 13 Jan 2020 10:40:25 -0800 Subject: [PATCH 28/28] Clarify tests --- .../tests/test_tracing_implementations.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/core/azure-core-tracing-opentelemetry/tests/test_tracing_implementations.py b/sdk/core/azure-core-tracing-opentelemetry/tests/test_tracing_implementations.py index 9cda91b01faa..b7a1297aec34 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/tests/test_tracing_implementations.py +++ b/sdk/core/azure-core-tracing-opentelemetry/tests/test_tracing_implementations.py @@ -55,8 +55,11 @@ def test_span(self, tracer): def test_start_finish(self, tracer): with tracer.start_as_current_span("Root") as parent: wrapped_class = OpenTelemetrySpan() + assert wrapped_class.span_instance.start_time is not None assert wrapped_class.span_instance.end_time is None wrapped_class.start() + assert wrapped_class.span_instance.start_time is not None + assert wrapped_class.span_instance.end_time is None wrapped_class.finish() assert wrapped_class.span_instance.start_time is not None assert wrapped_class.span_instance.end_time is not None