Skip to content

Commit

Permalink
[Core][OTel] Support OTel schema versioning
Browse files Browse the repository at this point in the history
This also adds support for attribute mapping to ensure
that attributes are mapped to the corresponding semantic convention
that we are trying to converge on.

Missing network attributes were also added.

Signed-off-by: Paul Van Eck <paulvaneck@microsoft.com>
  • Loading branch information
pvaneck committed Mar 8, 2023
1 parent b571e19 commit ebba857
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 36 deletions.
Expand Up @@ -3,8 +3,9 @@
# Licensed under the MIT License.
# ------------------------------------
"""Implements azure.core.tracing.AbstractSpan to wrap OpenTelemetry spans."""

from typing import Any, ContextManager, Dict, Optional, Union, Callable, Sequence
import warnings

from opentelemetry import trace
from opentelemetry.trace import Span, Tracer, SpanKind as OpenTelemetrySpanKind, Link as OpenTelemetryLink
from opentelemetry.context import attach, detach, get_current
Expand All @@ -13,29 +14,21 @@

from azure.core.tracing import SpanKind, HttpSpanMixin # pylint: disable=no-name-in-module

from ._schema import OpenTelemetrySchema
from ._version import VERSION

try:
from typing import TYPE_CHECKING, ContextManager
except ImportError:
TYPE_CHECKING = False

if TYPE_CHECKING:
from typing import Any, Mapping, Dict, Optional, Union, Callable, Sequence

from azure.core.pipeline.transport import HttpRequest, HttpResponse

AttributeValue = Union[
str,
bool,
int,
float,
Sequence[str],
Sequence[bool],
Sequence[int],
Sequence[float],
]
Attributes = Optional[Dict[str, AttributeValue]]
AttributeValue = Union[
str,
bool,
int,
float,
Sequence[str],
Sequence[bool],
Sequence[int],
Sequence[float],
]
Attributes = Optional[Dict[str, AttributeValue]]

__version__ = VERSION

Expand All @@ -56,6 +49,10 @@ def __init__(self, span=None, name="span", **kwargs):
# type: (Optional[Span], Optional[str], Any) -> None
current_tracer = self.get_current_tracer()

# TODO: Once we have additional supported versions, we should add a way to specify the version.
self._schema_version = OpenTelemetrySchema.get_latest_version()
self._attribute_mappings = OpenTelemetrySchema.get_attribute_mappings(self._schema_version)

## kind
value = kwargs.pop("kind", None)
kind = (
Expand Down Expand Up @@ -203,6 +200,7 @@ def add_attribute(self, key, value):
:param value: The value of the key value pair
:type value: str
"""
key = self._attribute_mappings.get(key, key)
self.span_instance.set_attribute(key, value)

def get_trace_parent(self):
Expand Down
@@ -0,0 +1,42 @@
# ------------------------------------
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
from enum import Enum
from typing import Dict

from azure.core import CaseInsensitiveEnumMeta


class OpenTelemetrySchemaVersion(
str, Enum, metaclass=CaseInsensitiveEnumMeta
): # pylint: disable=enum-must-inherit-case-insensitive-enum-meta

V1_19_0 = "1.19.0"


class OpenTelemetrySchema:

SUPPORTED_VERSIONS = [
OpenTelemetrySchemaVersion.V1_19_0,
]

# Mappings of attributes potentially reported by Azure SDKs to corresponding ones that follow
# OpenTelemetry semantic conventions.
_ATTRIBUTE_MAPPINGS = {
OpenTelemetrySchemaVersion.V1_19_0: {
"x-ms-client-request-id": "az.client_request_id",
"x-ms-request-id": "az.service_request_id",
"http.user_agent": "user_agent.original",
"messaging_bus.destination": "messaging.destination.name",
"peer.address": "net.peer.name",
}
}

@classmethod
def get_latest_version(cls) -> OpenTelemetrySchemaVersion:
return cls.SUPPORTED_VERSIONS[-1]

@classmethod
def get_attribute_mappings(cls, version: OpenTelemetrySchemaVersion) -> Dict[str, str]:
return cls._ATTRIBUTE_MAPPINGS[version]
31 changes: 31 additions & 0 deletions sdk/core/azure-core-tracing-opentelemetry/tests/test_schema.py
@@ -0,0 +1,31 @@
# ------------------------------------
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
import uuid

from opentelemetry.trace import SpanKind as OpenTelemetrySpanKind

from azure.core.tracing.ext.opentelemetry_span import OpenTelemetrySpan
from azure.core.tracing.ext.opentelemetry_span._schema import OpenTelemetrySchema


class TestOpenTelemetrySchema:
def test_latest_schema_attributes_renamed(self, tracer):
with tracer.start_as_current_span("Root", kind=OpenTelemetrySpanKind.CLIENT) as parent:
wrapped_class = OpenTelemetrySpan(span=parent)
schema_version = OpenTelemetrySchema.get_latest_version()
attribute_mappings = OpenTelemetrySchema.get_attribute_mappings(schema_version)
attribute_values = {}
for key, value in attribute_mappings.items():
attribute_values[value] = uuid.uuid4().hex
# Add attribute using key that is not following OpenTelemetry semantic conventions.
wrapped_class.add_attribute(key, attribute_values[value])

for attribute, expected_value in attribute_values.items():
# Check that expected renamed attribute is present with the correct value.
assert wrapped_class.span_instance.attributes.get(attribute) == expected_value

for key in attribute_mappings:
# Check that original attribute is not present.
assert wrapped_class.span_instance.attributes.get(key) is None
Expand Up @@ -2,23 +2,15 @@
# 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
"""The tests for opentelemetry_span.py"""
from unittest import mock

from opentelemetry import trace
from opentelemetry.trace import SpanKind as OpenTelemetrySpanKind
import pytest

from azure.core.tracing.ext.opentelemetry_span import OpenTelemetrySpan
from azure.core.tracing import SpanKind
import os

import pytest


class TestOpentelemetryWrapper:
Expand Down Expand Up @@ -142,7 +134,7 @@ def test_set_http_attributes(self, tracer):
wrapped_class = OpenTelemetrySpan(span=parent)
request = mock.Mock()
setattr(request, "method", "GET")
setattr(request, "url", "some url")
setattr(request, "url", "https://foo.bar/path")
response = mock.Mock()
setattr(request, "headers", {})
setattr(response, "status_code", 200)
Expand All @@ -152,11 +144,19 @@ def test_set_http_attributes(self, tracer):
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
assert wrapped_class.span_instance.attributes.get("user_agent.original") is None

request.headers["User-Agent"] = "some user agent"
request.url = "http://foo.bar:8080/path"
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")
assert wrapped_class.span_instance.attributes.get("user_agent.original") == request.headers.get(
"User-Agent"
)

if wrapped_class.span_instance.attributes.get("net.peer.name"):
assert wrapped_class.span_instance.attributes.get("net.peer.name") == "foo.bar"
assert wrapped_class.span_instance.attributes.get("net.peer.port") == 8080

def test_span_kind(self, tracer):
with tracer.start_as_current_span("Root") as parent:
Expand Down
14 changes: 13 additions & 1 deletion sdk/core/azure-core/azure/core/tracing/_abstract_span.py
Expand Up @@ -4,6 +4,8 @@
# ------------------------------------
"""Protocol that defines what functions wrappers of tracing libraries should implement."""
from enum import Enum
from urllib.parse import urlparse

from typing import (
TYPE_CHECKING,
Any,
Expand Down Expand Up @@ -202,20 +204,30 @@ class HttpSpanMixin(_MIXIN_BASE):
_HTTP_METHOD = "http.method"
_HTTP_URL = "http.url"
_HTTP_STATUS_CODE = "http.status_code"
_NET_PEER_NAME = "net.peer.name"
_NET_PEER_PORT = "net.peer.port"

def set_http_attributes(self, request: "HttpRequest", response: Optional["HttpResponseType"] = None) -> 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.
:param response: The response received from the server. Is None if no response received.
:type response: ~azure.core.pipeline.transport.HttpResponse or ~azure.core.pipeline.transport.AsyncHttpResponse
"""
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)

host = urlparse(request.url).netloc
if host:
host_parts = host.split(":")
self.add_attribute(self._NET_PEER_NAME, host_parts[0])
if len(host_parts) > 1 and host_parts[1] not in ["80", "443"]:
self.add_attribute(self._NET_PEER_PORT, int(host_parts[1]))

user_agent = request.headers.get("User-Agent")
if user_agent:
self.add_attribute(self._HTTP_USER_AGENT, user_agent)
Expand Down

0 comments on commit ebba857

Please sign in to comment.