Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenTelemetry plugin #7703

Merged
merged 32 commits into from
Jan 13, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5ce4164
Introducing change_context to replace set_current_span
lmazuel Oct 8, 2019
cf76954
OpenTelemetry plugin:first pass
lmazuel Oct 8, 2019
bc31715
Plug OpenTelemetry in azure-core
lmazuel Oct 8, 2019
1e4457a
Restore proper settings and tracing tests in azure-core
lmazuel Oct 8, 2019
b160216
Create HttpSpanMixin
lmazuel Oct 8, 2019
724daff
Need opentelemetry-sdk for testing
lmazuel Oct 8, 2019
788a339
WIP core test
lmazuel Oct 9, 2019
22b8b93
Merge branch 'master' into opentelemetry
lmazuel Oct 11, 2019
3989a21
Merge remote-tracking branch 'origin/master' into opentelemetry
lmazuel Nov 13, 2019
94b5dd1
Use stable azure-core
lmazuel Nov 13, 2019
88b499f
Support Python 3.8
lmazuel Nov 13, 2019
de574d1
OT Readme
lmazuel Nov 13, 2019
16cce5d
Do not include YAML
lmazuel Nov 13, 2019
151354d
Dependency update
lmazuel Nov 13, 2019
84e4a67
Merge remote-tracking branch 'origin/master' into opentelemetry
lmazuel Dec 20, 2019
9639646
Update dep
lmazuel Dec 20, 2019
d46c47b
Add decorator tests in azure-core
lmazuel Dec 20, 2019
1ce3b56
Remove OpenCensus specific tests
lmazuel Dec 20, 2019
1f5f92a
Remove useless OpenCensus tests
lmazuel Dec 20, 2019
c40e9c5
OT testing
lmazuel Dec 20, 2019
7f65e8c
Versionning
lmazuel Dec 20, 2019
15f3745
Fix Link for OT
lmazuel Dec 23, 2019
3026eb2
Fix setup.py
lmazuel Dec 23, 2019
deeaea0
pylint
lmazuel Dec 24, 2019
8672bcc
Dependency work
lmazuel Dec 24, 2019
83bd6e5
pylint
lmazuel Dec 24, 2019
9fe77f7
Need Python 3.x at least
lmazuel Dec 26, 2019
399cac9
Merge remote-tracking branch 'origin/master' into opentelemetry
lmazuel Jan 9, 2020
00e94c6
Remove diff with master
lmazuel Jan 9, 2020
afd12fa
Remove core changes
lmazuel Jan 11, 2020
dde3859
Feedback
lmazuel Jan 13, 2020
ac63f18
Clarify tests
lmazuel Jan 13, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 0 additions & 49 deletions sdk/core/azure-core-tracing-opencensus/tests/test_settings.py

This file was deleted.

10 changes: 10 additions & 0 deletions sdk/core/azure-core-tracing-opentelemetry/HISTORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

# Release History

-------------------

## 2019-10-07 Version 1.0.0b4

### Features

- Opencensus implementation of azure-core tracing protocol
8 changes: 8 additions & 0 deletions sdk/core/azure-core-tracing-opentelemetry/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
recursive-include tests *.py
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

71 changes: 71 additions & 0 deletions sdk/core/azure-core-tracing-opentelemetry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@


# Azure Core Tracing OpenTelemetry client library for Python

## Getting started

Install the opentelemetry python for Python with [pip](https://pypi.org/project/pip/):

```bash
pip install azure-core-tracing-opentelemetry --pre
```

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

## Examples

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 opentelemetry.ext.azure_monitor import AzureMonitorSpanExporter

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

# 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)"
)

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
```


## 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 OpenTelemetry configuration can be found on the [OpenTelemetry website](https://opentelemetry.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.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
# ------------------------------------
# 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, HttpSpanMixin # 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(HttpSpanMixin, 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._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 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)
Loading