Skip to content

Commit 858389b

Browse files
authored
[Corehttp] Implement tracing capabilities (Azure#39172)
Added native tracing capabilities with OpenTelemetry. Signed-off-by: Paul Van Eck <[email protected]>
1 parent f189c8b commit 858389b

26 files changed

+2011
-2
lines changed

sdk/core/corehttp/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,23 @@
44

55
### Features Added
66

7+
- Native tracing support was added. [#39172](https://github.com/Azure/azure-sdk-for-python/pull/39172)
8+
- The `OpenTelemetryTracer` class was added to the `corehttp.instrumentation.tracing.opentelemetry` module. This is a wrapper around the OpenTelemetry tracer that is used to create spans for SDK operations.
9+
- Added a `get_tracer` method to the new `corehttp.instrumentation` module. This method returns an instance of the `OpenTelemetryTracer` class if OpenTelemetry is available.
10+
- A `TracingOptions` TypedDict class was added to define the options that SDK users can use to configure tracing per-operation. These options include the ability to enable or disable tracing and set additional attributes on spans.
11+
- Example usage: `client.method(tracing_options={"enabled": True, "attributes": {"foo": "bar"}})`
12+
- `DistributedHttpTracingPolicy` and `distributed_trace`/`distributed_trace_async` decorators were added to support OpenTelemetry tracing for SDK operations.
13+
- SDK clients can define an `_instrumentation_config` class variable to configure the OpenTelemetry tracer used in method span creation. Possible configuration options are `library_name`, `library_version`, `schema_url`, and `attributes`.
14+
- Added a global settings object, `corehttp.settings`, to the `corehttp` package. This object can be used to set global settings for the `corehttp` package. Currently the only setting is `tracing_enabled` for enabling/disabling tracing. [#39172](https://github.com/Azure/azure-sdk-for-python/pull/39172)
15+
716
### Breaking Changes
817

918
### Bugs Fixed
1019

1120
### Other Changes
1221

22+
- Added `opentelemetry-api` as an optional dependency for tracing. [#39172](https://github.com/Azure/azure-sdk-for-python/pull/39172)
23+
1324
## 1.0.0b6 (2025-03-27)
1425

1526
### Features Added
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
from .tracing._tracer import get_tracer
6+
7+
__all__ = [
8+
"get_tracer",
9+
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
from ._models import SpanKind, Link, TracingOptions
6+
from ._decorator import distributed_trace, distributed_trace_async
7+
8+
__all__ = [
9+
"Link",
10+
"SpanKind",
11+
"TracingOptions",
12+
"distributed_trace",
13+
"distributed_trace_async",
14+
]
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
"""The decorator to apply if you want the given method traced."""
6+
from contextvars import ContextVar
7+
import functools
8+
from typing import Awaitable, Any, TypeVar, overload, Optional, Callable, TYPE_CHECKING
9+
from typing_extensions import ParamSpec
10+
11+
from ._models import SpanKind
12+
from ._tracer import get_tracer
13+
from ...settings import settings
14+
15+
if TYPE_CHECKING:
16+
from ._models import TracingOptions
17+
18+
19+
P = ParamSpec("P")
20+
T = TypeVar("T")
21+
22+
23+
# This context variable is used to determine if we are already in the span context of a decorated function.
24+
_in_span_context = ContextVar("in_span_context", default=False)
25+
26+
27+
@overload
28+
def distributed_trace(__func: Callable[P, T]) -> Callable[P, T]:
29+
pass
30+
31+
32+
@overload
33+
def distributed_trace(**kwargs: Any) -> Callable[[Callable[P, T]], Callable[P, T]]:
34+
pass
35+
36+
37+
def distributed_trace(__func: Optional[Callable[P, T]] = None, **kwargs: Any) -> Any: # pylint: disable=unused-argument
38+
"""Decorator to apply to an SDK method to have it traced automatically.
39+
40+
Span will use the method's qualified name.
41+
42+
Note: This decorator SHOULD NOT be used by application developers. It's intended to be called by client
43+
libraries only. Application developers should use OpenTelemetry directly to instrument their applications.
44+
45+
:param callable __func: A function to decorate
46+
47+
:return: The decorated function
48+
:rtype: Any
49+
"""
50+
51+
def decorator(func: Callable[P, T]) -> Callable[P, T]:
52+
53+
@functools.wraps(func)
54+
def wrapper_use_tracer(*args: Any, **kwargs: Any) -> T:
55+
# If we are already in the span context of a decorated function, don't trace.
56+
if _in_span_context.get():
57+
return func(*args, **kwargs)
58+
59+
# This will be popped in the pipeline or transport runner.
60+
tracing_options: TracingOptions = kwargs.get("tracing_options", {})
61+
62+
# User can explicitly disable tracing for this call
63+
user_enabled = tracing_options.get("enabled")
64+
if user_enabled is False:
65+
return func(*args, **kwargs)
66+
67+
# If tracing is disabled globally and user didn't explicitly enable it, don't trace.
68+
if not settings.tracing_enabled and user_enabled is None:
69+
return func(*args, **kwargs)
70+
71+
config = {}
72+
if args and hasattr(args[0], "_instrumentation_config"):
73+
config = args[0]._instrumentation_config # pylint: disable=protected-access
74+
75+
method_tracer = get_tracer(
76+
library_name=config.get("library_name"),
77+
library_version=config.get("library_version"),
78+
schema_url=config.get("schema_url"),
79+
attributes=config.get("attributes"),
80+
)
81+
if not method_tracer:
82+
return func(*args, **kwargs)
83+
84+
name = func.__qualname__
85+
span_suppression_token = _in_span_context.set(True)
86+
try:
87+
with method_tracer.start_as_current_span(
88+
name=name,
89+
kind=SpanKind.INTERNAL,
90+
attributes=tracing_options.get("attributes"),
91+
) as span:
92+
try:
93+
return func(*args, **kwargs)
94+
except Exception as err: # pylint: disable=broad-except
95+
ex_type = type(err)
96+
module = ex_type.__module__ if ex_type.__module__ != "builtins" else ""
97+
error_type = f"{module}.{ex_type.__qualname__}" if module else ex_type.__qualname__
98+
span.set_attribute("error.type", error_type)
99+
raise
100+
finally:
101+
_in_span_context.reset(span_suppression_token)
102+
103+
return wrapper_use_tracer
104+
105+
return decorator if __func is None else decorator(__func)
106+
107+
108+
@overload
109+
def distributed_trace_async(__func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
110+
pass
111+
112+
113+
@overload
114+
def distributed_trace_async(**kwargs: Any) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]:
115+
pass
116+
117+
118+
def distributed_trace_async( # pylint: disable=unused-argument
119+
__func: Optional[Callable[P, Awaitable[T]]] = None,
120+
**kwargs: Any,
121+
) -> Any:
122+
"""Decorator to apply to an SDK method to have it traced automatically.
123+
124+
Span will use the method's qualified name.
125+
126+
Note: This decorator SHOULD NOT be used by application developers. It's intended to be called by client
127+
libraries only. Application developers should use OpenTelemetry directly to instrument their applications.
128+
129+
:param callable __func: A function to decorate
130+
131+
:return: The decorated function
132+
:rtype: Any
133+
"""
134+
135+
def decorator(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
136+
137+
@functools.wraps(func)
138+
async def wrapper_use_tracer(*args: Any, **kwargs: Any) -> T:
139+
# If we are already in the span context of a decorated function, don't trace.
140+
if _in_span_context.get():
141+
return await func(*args, **kwargs)
142+
143+
# This will be popped in the pipeline or transport runner.
144+
tracing_options: TracingOptions = kwargs.get("tracing_options", {})
145+
146+
# User can explicitly disable tracing for this call
147+
user_enabled = tracing_options.get("enabled")
148+
if user_enabled is False:
149+
return await func(*args, **kwargs)
150+
151+
# If tracing is disabled globally and user didn't explicitly enable it, don't trace.
152+
if not settings.tracing_enabled and user_enabled is None:
153+
return await func(*args, **kwargs)
154+
155+
config = {}
156+
if args and hasattr(args[0], "_instrumentation_config"):
157+
config = args[0]._instrumentation_config # pylint: disable=protected-access
158+
159+
method_tracer = get_tracer(
160+
library_name=config.get("library_name"),
161+
library_version=config.get("library_version"),
162+
schema_url=config.get("schema_url"),
163+
attributes=config.get("attributes"),
164+
)
165+
if not method_tracer:
166+
return await func(*args, **kwargs)
167+
168+
name = func.__qualname__
169+
span_suppression_token = _in_span_context.set(True)
170+
try:
171+
with method_tracer.start_as_current_span(
172+
name=name,
173+
kind=SpanKind.INTERNAL,
174+
attributes=tracing_options.get("attributes"),
175+
) as span:
176+
try:
177+
return await func(*args, **kwargs)
178+
except Exception as err: # pylint: disable=broad-except
179+
ex_type = type(err)
180+
module = ex_type.__module__ if ex_type.__module__ != "builtins" else ""
181+
error_type = f"{module}.{ex_type.__qualname__}" if module else ex_type.__qualname__
182+
span.set_attribute("error.type", error_type)
183+
raise
184+
finally:
185+
_in_span_context.reset(span_suppression_token)
186+
187+
return wrapper_use_tracer
188+
189+
return decorator if __func is None else decorator(__func)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
from __future__ import annotations
6+
from enum import Enum
7+
from typing import Dict, Mapping, Optional, Union, Sequence, TypedDict
8+
9+
from ...utils import CaseInsensitiveEnumMeta
10+
11+
12+
AttributeValue = Union[
13+
str,
14+
bool,
15+
int,
16+
float,
17+
Sequence[str],
18+
Sequence[bool],
19+
Sequence[int],
20+
Sequence[float],
21+
]
22+
Attributes = Mapping[str, AttributeValue]
23+
24+
25+
class SpanKind(Enum, metaclass=CaseInsensitiveEnumMeta):
26+
"""Describes the role or kind of a span within a distributed trace.
27+
28+
This helps to categorize spans based on their relationship to other spans and the type
29+
of operation they represent.
30+
"""
31+
32+
UNSPECIFIED = 1
33+
"""Unspecified span kind."""
34+
35+
SERVER = 2
36+
"""Indicates that the span describes an operation that handles a remote request."""
37+
38+
CLIENT = 3
39+
"""Indicates that the span describes a request to some remote service."""
40+
41+
PRODUCER = 4
42+
"""Indicates that the span describes the initiation or scheduling of a local or remote operation."""
43+
44+
CONSUMER = 5
45+
"""Indicates that the span represents the processing of an operation initiated by a producer."""
46+
47+
INTERNAL = 6
48+
"""Indicates that the span is used internally in the application."""
49+
50+
51+
class Link:
52+
"""Represents a reference from one span to another span.
53+
54+
:param headers: A dictionary of the request header as key value pairs.
55+
:type headers: dict
56+
:param attributes: Any additional attributes that should be added to link
57+
:type attributes: dict
58+
"""
59+
60+
def __init__(self, headers: Dict[str, str], attributes: Optional[Attributes] = None) -> None:
61+
self.headers = headers
62+
self.attributes = attributes
63+
64+
65+
class TracingOptions(TypedDict, total=False):
66+
"""Options to configure tracing behavior for operations."""
67+
68+
enabled: bool
69+
"""Whether tracing is enabled for the operation. By default, if the global setting is enabled, tracing is
70+
enabled for all operations. This option can be used to override the global setting for a specific operation."""
71+
attributes: Attributes
72+
"""Attributes to include in the spans emitted for the operation."""
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
from typing import Optional, Union, Mapping, TYPE_CHECKING
6+
from functools import lru_cache
7+
8+
if TYPE_CHECKING:
9+
try:
10+
from .opentelemetry import OpenTelemetryTracer
11+
except ImportError:
12+
pass
13+
14+
15+
def _get_tracer_impl():
16+
# Check if OpenTelemetry is available/installed.
17+
try:
18+
from .opentelemetry import OpenTelemetryTracer
19+
20+
return OpenTelemetryTracer
21+
except ImportError:
22+
return None
23+
24+
25+
@lru_cache
26+
def _get_tracer_cached(
27+
library_name: Optional[str],
28+
library_version: Optional[str],
29+
schema_url: Optional[str],
30+
attributes_key: Optional[frozenset],
31+
) -> Optional["OpenTelemetryTracer"]:
32+
tracer_impl = _get_tracer_impl()
33+
if tracer_impl:
34+
# Convert attributes_key back to dict if needed
35+
attributes = dict(attributes_key) if attributes_key else None
36+
return tracer_impl(
37+
library_name=library_name,
38+
library_version=library_version,
39+
schema_url=schema_url,
40+
attributes=attributes,
41+
)
42+
return None
43+
44+
45+
def get_tracer(
46+
*,
47+
library_name: Optional[str] = None,
48+
library_version: Optional[str] = None,
49+
schema_url: Optional[str] = None,
50+
attributes: Optional[Mapping[str, Union[str, bool, int, float]]] = None,
51+
) -> Optional["OpenTelemetryTracer"]:
52+
"""Get the OpenTelemetry tracer instance if available.
53+
54+
If OpenTelemetry is not available, this method will return None. This method caches
55+
the tracer instance for each unique set of parameters.
56+
57+
:keyword library_name: The name of the library to use in the tracer.
58+
:paramtype library_name: str
59+
:keyword library_version: The version of the library to use in the tracer.
60+
:paramtype library_version: str
61+
:keyword schema_url: Specifies the Schema URL of the emitted spans.
62+
:paramtype schema_url: str
63+
:keyword attributes: Attributes to add to the emitted spans.
64+
:paramtype attributes: Mapping[str, Union[str, bool, int, float]]
65+
:return: The OpenTelemetry tracer instance if available.
66+
:rtype: Optional[~corehttp.instrumentation.tracing.opentelemetry.OpenTelemetryTracer]
67+
"""
68+
attributes_key = frozenset(attributes.items()) if attributes else None
69+
return _get_tracer_cached(library_name, library_version, schema_url, attributes_key)

0 commit comments

Comments
 (0)