Skip to content

Commit 74e47e4

Browse files
committed
[Core] Tracing updates
Signed-off-by: Paul Van Eck <[email protected]>
1 parent ebf87af commit 74e47e4

23 files changed

+2251
-493
lines changed

.vscode/cspell.json

+1
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@
353353
"onmicrosoft",
354354
"openai",
355355
"OPENAI",
356+
"otel",
356357
"otlp",
357358
"OTLP",
358359
"owasp",

sdk/core/azure-core/CHANGELOG.md

+21
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,33 @@
44

55
### Features Added
66

7+
- Added native OpenTelemetry tracing to Azure Core which enables users to use OpenTelemetry to trace Azure SDK operations without needing to install a plugin. #39563
8+
- A new `TracerProvider` class was added, allowing SDK developers to pass in various metadata to the OpenTelemetry tracer and spans.
9+
- `OpenTelemetryTracer` and `OpenTelemetrySpan` classes were added to the `azure.core.tracing` namespace.
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, set additional attributes on spans, and disable recording exceptions as events on spans.
11+
- The `tracing_enabled` setting on the global `settings` object is now respected and can be used to enable or disable native tracing for all SDKs.
12+
- If `setting.tracing_implementation` is set, it will take precedence over the native core tracing.
13+
- The `DistributedTracingPolicy` and `distributed_trace`/`distributed_trace_async` decorators now uses the OpenTelemetry tracer if it is available and enabled.
14+
715
### Breaking Changes
816

17+
- Inside `DistributedTracingPolicy`, some tracing modifications have been made to converge with the OpenTelemetry stable HTTP semantic conventions. #39563
18+
- The **span name** for HTTP requests will now be set to just the HTTP method by default (previously the path was included).
19+
- The following span attributes were renamed:
20+
- `http.method` -> `http.request.method`
21+
- `http.url` -> `url.full`
22+
- `http.status_code` -> `http.response.status_code`
23+
- `http.user_agent` -> `user_agent.original`
24+
- `net.peer.name` -> `server.address`
25+
- `net.peer.port` -> `server.port`
26+
- Removed automatic tracing enablement for the OpenTelemetry plugin if `opentelemetry` was imported. #39563
27+
928
### Bugs Fixed
1029

1130
### Other Changes
1231

32+
- Added `opentelemetry-api` as an optional dependency for tracing. #39563
33+
1334
## 1.32.0 (2024-10-31)
1435

1536
### Features Added

sdk/core/azure-core/azure/core/pipeline/policies/_distributed_tracing.py

+123-22
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,14 @@
3939
from azure.core.rest import HttpResponse, HttpRequest
4040
from azure.core.settings import settings
4141
from azure.core.tracing import SpanKind
42+
from ...tracing._tracer import default_tracer_provider, TracerProvider
43+
from ...tracing._models import TracingOptions
4244

4345
if TYPE_CHECKING:
44-
from azure.core.tracing._abstract_span import (
46+
from ...tracing._abstract_span import (
4547
AbstractSpan,
4648
)
49+
from ...tracing.opentelemetry_span import OpenTelemetrySpan
4750

4851
HTTPResponseType = TypeVar("HTTPResponseType", HttpResponse, LegacyHttpResponse)
4952
HTTPRequestType = TypeVar("HTTPRequestType", HttpRequest, LegacyHttpRequest)
@@ -61,10 +64,7 @@ def _default_network_span_namer(http_request: HTTPRequestType) -> str:
6164
:returns: The string to use as network span name
6265
:rtype: str
6366
"""
64-
path = urllib.parse.urlparse(http_request.url).path
65-
if not path:
66-
path = "/"
67-
return path
67+
return http_request.method
6868

6969

7070
class DistributedTracingPolicy(SansIOHTTPPolicy[HTTPRequestType, HTTPResponseType]):
@@ -77,35 +77,91 @@ class DistributedTracingPolicy(SansIOHTTPPolicy[HTTPRequestType, HTTPResponseTyp
7777
"""
7878

7979
TRACING_CONTEXT = "TRACING_CONTEXT"
80+
81+
# Current HTTP semantic conventions
82+
_HTTP_RESEND_COUNT = "http.request.resend_count"
83+
_USER_AGENT_ORIGINAL = "user_agent.original"
84+
_HTTP_REQUEST_METHOD = "http.request.method"
85+
_URL_FULL = "url.full"
86+
_HTTP_RESPONSE_STATUS_CODE = "http.response.status_code"
87+
_SERVER_ADDRESS = "server.address"
88+
_SERVER_PORT = "server.port"
89+
_ERROR_TYPE = "error.type"
90+
91+
# Legacy HTTP semantic conventions
92+
_HTTP_USER_AGENT = "http.user_agent"
93+
_HTTP_METHOD = "http.method"
94+
_HTTP_URL = "http.url"
95+
_HTTP_STATUS_CODE = "http.status_code"
96+
_NET_PEER_NAME = "net.peer.name"
97+
_NET_PEER_PORT = "net.peer.port"
98+
99+
# Azure attributes
80100
_REQUEST_ID = "x-ms-client-request-id"
81101
_RESPONSE_ID = "x-ms-request-id"
82-
_HTTP_RESEND_COUNT = "http.request.resend_count"
102+
_REQUEST_ID_ATTR = "az.client_request_id"
103+
_RESPONSE_ID_ATTR = "az.service_request_id"
83104

84-
def __init__(self, **kwargs: Any):
105+
def __init__(self, *, tracer_provider: Optional[TracerProvider] = None, **kwargs: Any):
85106
self._network_span_namer = kwargs.get("network_span_namer", _default_network_span_namer)
86107
self._tracing_attributes = kwargs.get("tracing_attributes", {})
108+
self._tracer_provider = tracer_provider
87109

88110
def on_request(self, request: PipelineRequest[HTTPRequestType]) -> None:
89111
ctxt = request.context.options
90112
try:
91-
span_impl_type = settings.tracing_implementation()
92-
if span_impl_type is None:
113+
tracing_options: TracingOptions = ctxt.pop("tracing_options", {})
114+
tracing_enabled = settings.tracing_enabled()
115+
116+
# User can explicitly disable tracing for this request.
117+
user_enabled = tracing_options.get("enabled")
118+
if user_enabled is False:
93119
return
94120

121+
# If tracing is disabled globally and user didn't explicitly enable it, don't trace.
122+
if not tracing_enabled and user_enabled is None:
123+
return
124+
125+
span_impl_type = settings.tracing_implementation()
95126
namer = ctxt.pop("network_span_namer", self._network_span_namer)
96127
tracing_attributes = ctxt.pop("tracing_attributes", self._tracing_attributes)
97128
span_name = namer(request.http_request)
98129

99-
span = span_impl_type(name=span_name, kind=SpanKind.CLIENT)
100-
for attr, value in tracing_attributes.items():
101-
span.add_attribute(attr, value)
102-
span.start()
130+
span_attributes = {**tracing_attributes, **tracing_options.get("attributes", {})}
131+
132+
if span_impl_type:
133+
# If the plugin is enabled, prioritize it over the core tracing.
134+
span = span_impl_type(name=span_name, kind=SpanKind.CLIENT)
135+
for attr, value in span_attributes.items():
136+
span.add_attribute(attr, value) # type: ignore
103137

104-
headers = span.to_header()
105-
request.http_request.headers.update(headers)
138+
headers = span.to_header()
139+
request.http_request.headers.update(headers)
140+
141+
request.context[self.TRACING_CONTEXT] = span
142+
else:
143+
# Otherwise, use the core tracing.
144+
tracer = (
145+
self._tracer_provider.get_tracer()
146+
if self._tracer_provider
147+
else default_tracer_provider.get_tracer()
148+
)
149+
if not tracer:
150+
return
151+
152+
core_span = tracer.start_span(
153+
name=span_name,
154+
kind=SpanKind.CLIENT,
155+
attributes=span_attributes,
156+
record_exception=tracing_options.get("record_exception", True),
157+
)
158+
159+
trace_context_headers = tracer.get_trace_context()
160+
request.http_request.headers.update(trace_context_headers)
161+
request.context[self.TRACING_CONTEXT] = core_span
106162

107-
request.context[self.TRACING_CONTEXT] = span
108163
except Exception as err: # pylint: disable=broad-except
164+
print(err)
109165
_LOGGER.warning("Unable to start network span: %s", err)
110166

111167
def end_span(
@@ -126,21 +182,26 @@ def end_span(
126182
if self.TRACING_CONTEXT not in request.context:
127183
return
128184

129-
span: "AbstractSpan" = request.context[self.TRACING_CONTEXT]
185+
span = request.context[self.TRACING_CONTEXT]
130186
http_request: Union[HttpRequest, LegacyHttpRequest] = request.http_request
131187
if span is not None:
132-
span.set_http_attributes(http_request, response=response)
188+
self._set_http_attributes(span, request=http_request, response=response)
189+
set_attribute_func = (
190+
getattr(span, "add_attribute") if hasattr(span, "add_attribute") else getattr(span, "set_attribute")
191+
)
192+
end_func = getattr(span, "finish", None) or getattr(span, "end")
193+
133194
if request.context.get("retry_count"):
134-
span.add_attribute(self._HTTP_RESEND_COUNT, request.context["retry_count"])
195+
set_attribute_func(self._HTTP_RESEND_COUNT, request.context["retry_count"])
135196
request_id = http_request.headers.get(self._REQUEST_ID)
136197
if request_id is not None:
137-
span.add_attribute(self._REQUEST_ID, request_id)
198+
set_attribute_func(self._REQUEST_ID_ATTR, request_id)
138199
if response and self._RESPONSE_ID in response.headers:
139-
span.add_attribute(self._RESPONSE_ID, response.headers[self._RESPONSE_ID])
200+
set_attribute_func(self._RESPONSE_ID_ATTR, response.headers[self._RESPONSE_ID])
140201
if exc_info:
141202
span.__exit__(*exc_info)
142203
else:
143-
span.finish()
204+
end_func()
144205

145206
def on_response(
146207
self,
@@ -151,3 +212,43 @@ def on_response(
151212

152213
def on_exception(self, request: PipelineRequest[HTTPRequestType]) -> None:
153214
self.end_span(request, exc_info=sys.exc_info())
215+
216+
def _set_http_attributes(
217+
self,
218+
span: Union["AbstractSpan", "OpenTelemetrySpan"],
219+
request: Union[HttpRequest, LegacyHttpRequest],
220+
response: Optional[HTTPResponseType] = None,
221+
) -> None:
222+
"""
223+
Add correct attributes for a http client span.
224+
225+
:param span: The span to add attributes to.
226+
:type span: ~azure.core.tracing.AbstractSpan or ~azure.core.tracing.opentelemetry_span.OpenTelemetrySpan
227+
:param request: The request made
228+
:type request: azure.core.rest.HttpRequest
229+
:param response: The response received from the server. Is None if no response received.
230+
:type response: ~azure.core.pipeline.transport.HttpResponse or ~azure.core.pipeline.transport.AsyncHttpResponse
231+
"""
232+
set_attribute_func = (
233+
getattr(span, "add_attribute") if hasattr(span, "add_attribute") else getattr(span, "set_attribute")
234+
)
235+
236+
set_attribute_func(self._HTTP_REQUEST_METHOD, request.method)
237+
set_attribute_func(self._URL_FULL, request.url)
238+
239+
parsed_url = urllib.parse.urlparse(request.url)
240+
if parsed_url.hostname:
241+
set_attribute_func(self._SERVER_ADDRESS, parsed_url.hostname)
242+
if parsed_url.port and parsed_url.port not in [80, 443]:
243+
set_attribute_func(self._SERVER_PORT, parsed_url.port)
244+
245+
user_agent = request.headers.get("User-Agent")
246+
if user_agent:
247+
set_attribute_func(self._USER_AGENT_ORIGINAL, user_agent)
248+
if response and response.status_code:
249+
set_attribute_func(self._HTTP_RESPONSE_STATUS_CODE, response.status_code)
250+
if response.status_code >= 400:
251+
set_attribute_func(self._ERROR_TYPE, str(response.status_code))
252+
else:
253+
set_attribute_func(self._HTTP_RESPONSE_STATUS_CODE, 504)
254+
set_attribute_func(self._ERROR_TYPE, "504")

sdk/core/azure-core/azure/core/settings.py

+22-19
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
from enum import Enum
3131
import logging
3232
import os
33-
import sys
3433
from typing import (
3534
Type,
3635
Optional,
@@ -91,6 +90,24 @@ def convert_bool(value: Union[str, bool]) -> bool:
9190
raise ValueError("Cannot convert {} to boolean value".format(value))
9291

9392

93+
def convert_tracing_enabled(value: Optional[Union[str, bool]]) -> bool:
94+
"""Convert tracing value to bool with regard to tracing implementation.
95+
96+
:param value: the value to convert
97+
:type value: str or bool or None
98+
:returns: A boolean value matching the intent of the input
99+
:rtype: bool
100+
:raises ValueError: If conversion to bool fails
101+
"""
102+
if value is None:
103+
# If tracing_enabled was not explicitly set to a boolean, determine tracing enablement
104+
# based on tracing_implementation being set.
105+
if settings.tracing_implementation():
106+
return True
107+
return False
108+
return convert_bool(value)
109+
110+
94111
_levels = {
95112
"CRITICAL": logging.CRITICAL,
96113
"ERROR": logging.ERROR,
@@ -184,18 +201,6 @@ def _get_opentelemetry_span() -> Optional[Type[AbstractSpan]]:
184201
return None
185202

186203

187-
def _get_opencensus_span_if_opencensus_is_imported() -> Optional[Type[AbstractSpan]]:
188-
if "opencensus" not in sys.modules:
189-
return None
190-
return _get_opencensus_span()
191-
192-
193-
def _get_opentelemetry_span_if_opentelemetry_is_imported() -> Optional[Type[AbstractSpan]]:
194-
if "opentelemetry" not in sys.modules:
195-
return None
196-
return _get_opentelemetry_span()
197-
198-
199204
_tracing_implementation_dict: Dict[str, Callable[[], Optional[Type[AbstractSpan]]]] = {
200205
"opencensus": _get_opencensus_span,
201206
"opentelemetry": _get_opentelemetry_span,
@@ -218,9 +223,7 @@ def convert_tracing_impl(value: Optional[Union[str, Type[AbstractSpan]]]) -> Opt
218223
219224
"""
220225
if value is None:
221-
return (
222-
_get_opentelemetry_span_if_opentelemetry_is_imported() or _get_opencensus_span_if_opencensus_is_imported()
223-
)
226+
return None
224227

225228
if not isinstance(value, str):
226229
return value
@@ -504,11 +507,11 @@ def _config(self, props: Mapping[str, Any]) -> Tuple[Any, ...]:
504507
default=logging.INFO,
505508
)
506509

507-
tracing_enabled: PrioritizedSetting[Union[str, bool], bool] = PrioritizedSetting(
510+
tracing_enabled: PrioritizedSetting[Optional[Union[str, bool]], bool] = PrioritizedSetting(
508511
"tracing_enabled",
509512
env_var="AZURE_TRACING_ENABLED",
510-
convert=convert_bool,
511-
default=False,
513+
convert=convert_tracing_enabled,
514+
default=None,
512515
)
513516

514517
tracing_implementation: PrioritizedSetting[

sdk/core/azure-core/azure/core/tracing/__init__.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
AbstractSpan,
77
SpanKind,
88
HttpSpanMixin,
9-
Link,
109
)
10+
from ._models import Link, SpanKind, StatusCode, TracingOptions
11+
from ._tracer import TracerProvider
1112

12-
__all__ = ["AbstractSpan", "SpanKind", "HttpSpanMixin", "Link"]
13+
__all__ = ["AbstractSpan", "HttpSpanMixin", "Link", "SpanKind", "StatusCode", "TracingOptions", "TracerProvider"]

0 commit comments

Comments
 (0)