Skip to content

Commit 954a7ff

Browse files
authored
Support stable http semconv - request telemetry (#39208)
1 parent 6d96c43 commit 954a7ff

File tree

4 files changed

+205
-42
lines changed

4 files changed

+205
-42
lines changed

sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
### Features Added
66

7+
- Support stable http semantic conventions for breeze exporter - REQUESTS
8+
([#39208](https://github.com/Azure/azure-sdk-for-python/pull/39208))
9+
710
### Breaking Changes
811

912
### Bugs Fixed

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_exporter.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44
import json
55
import logging
66
from time import time_ns
7-
from typing import Any, Dict, List, Sequence
7+
from typing import no_type_check, Any, Dict, List, Sequence
88
from urllib.parse import urlparse
99

10+
from opentelemetry.semconv.attributes.client_attributes import CLIENT_ADDRESS
11+
from opentelemetry.semconv.attributes.http_attributes import (
12+
HTTP_REQUEST_METHOD,
13+
HTTP_RESPONSE_STATUS_CODE,
14+
)
1015
from opentelemetry.semconv.trace import DbSystemValues, SpanAttributes
1116
from opentelemetry.semconv._incubating.attributes import gen_ai_attributes
1217
from opentelemetry.sdk.resources import Resource
@@ -65,6 +70,28 @@
6570
"code.",
6671
]
6772

73+
_STANDARD_OPENTELEMETRY_HTTP_ATTRIBUTES = [
74+
"client.address",
75+
"client.port",
76+
"server.address",
77+
"server.port",
78+
"url.full",
79+
"url.path",
80+
"url.query",
81+
"url.scheme",
82+
"url.template",
83+
"error.type",
84+
"network.local.address",
85+
"network.local.port",
86+
"network.protocol.name",
87+
"network.peer.address",
88+
"network.peer.port",
89+
"network.protocol.version",
90+
"network.transport",
91+
"user_agent.original",
92+
"user_agent.synthetic.type",
93+
]
94+
6895
_STANDARD_AZURE_MONITOR_ATTRIBUTES = [
6996
_SAMPLE_RATE_KEY,
7097
]
@@ -141,7 +168,7 @@ def _get_otel_resource_envelope(self, resource: Resource) -> TelemetryItem:
141168
def _span_to_envelope(self, span: ReadableSpan) -> TelemetryItem:
142169
envelope = _convert_span_to_envelope(span)
143170
envelope.instrumentation_key = self._instrumentation_key
144-
return envelope
171+
return envelope # type: ignore
145172

146173
def _span_events_to_envelopes(self, span: ReadableSpan) -> Sequence[TelemetryItem]:
147174
if not span or len(span.events) == 0:
@@ -179,6 +206,7 @@ def from_connection_string(cls, conn_str: str, **kwargs: Any) -> "AzureMonitorTr
179206
# pylint: disable=too-many-branches
180207
# pylint: disable=protected-access
181208
# mypy: disable-error-code="assignment,attr-defined,index,operator,union-attr"
209+
@no_type_check
182210
def _convert_span_to_envelope(span: ReadableSpan) -> TelemetryItem:
183211
# Update instrumentation bitmap if span was generated from instrumentation
184212
_check_instrumentation_span(span)
@@ -208,8 +236,9 @@ def _convert_span_to_envelope(span: ReadableSpan) -> TelemetryItem:
208236
)
209237
envelope.data = MonitorBase(base_data=data, base_type="RequestData")
210238
envelope.tags[ContextTagKeys.AI_OPERATION_NAME] = span.name
211-
if SpanAttributes.NET_PEER_IP in span.attributes:
212-
envelope.tags[ContextTagKeys.AI_LOCATION_IP] = span.attributes[SpanAttributes.NET_PEER_IP]
239+
location_ip = trace_utils._get_location_ip(span.attributes)
240+
if location_ip:
241+
envelope.tags[ContextTagKeys.AI_LOCATION_IP] = location_ip
213242
if _AZURE_SDK_NAMESPACE_NAME in span.attributes: # Azure specific resources
214243
# Currently only eventhub and servicebus are supported (kind CONSUMER)
215244
data.source = trace_utils._get_azure_sdk_target_source(span.attributes)
@@ -222,21 +251,19 @@ def _convert_span_to_envelope(span: ReadableSpan) -> TelemetryItem:
222251
difference = (start_time / 1000000) - enqueued_time
223252
total += difference
224253
data.measurements["timeSinceEnqueued"] = max(0, total / len(span.links))
225-
elif SpanAttributes.HTTP_METHOD in span.attributes: # HTTP
254+
elif HTTP_REQUEST_METHOD in span.attributes or SpanAttributes.HTTP_METHOD in span.attributes: # HTTP
226255
path = ""
227-
if SpanAttributes.HTTP_USER_AGENT in span.attributes:
256+
user_agent = trace_utils._get_user_agent(span.attributes)
257+
if user_agent:
228258
# TODO: Not exposed in Swagger, need to update def
229-
envelope.tags["ai.user.userAgent"] = span.attributes[SpanAttributes.HTTP_USER_AGENT]
230-
# http specific logic for ai.location.ip
231-
if SpanAttributes.HTTP_CLIENT_IP in span.attributes:
232-
envelope.tags[ContextTagKeys.AI_LOCATION_IP] = span.attributes[SpanAttributes.HTTP_CLIENT_IP]
259+
envelope.tags["ai.user.userAgent"] = user_agent
233260
# url
234261
url = trace_utils._get_url_for_http_request(span.attributes)
235262
data.url = url
236263
# Http specific logic for ai.operation.name
237264
if SpanAttributes.HTTP_ROUTE in span.attributes:
238265
envelope.tags[ContextTagKeys.AI_OPERATION_NAME] = "{} {}".format(
239-
span.attributes[SpanAttributes.HTTP_METHOD],
266+
span.attributes.get(HTTP_REQUEST_METHOD) or span.attributes.get(SpanAttributes.HTTP_METHOD),
240267
span.attributes[SpanAttributes.HTTP_ROUTE],
241268
)
242269
elif url:
@@ -246,12 +273,13 @@ def _convert_span_to_envelope(span: ReadableSpan) -> TelemetryItem:
246273
if not path:
247274
path = "/"
248275
envelope.tags[ContextTagKeys.AI_OPERATION_NAME] = "{} {}".format(
249-
span.attributes[SpanAttributes.HTTP_METHOD],
276+
span.attributes.get(HTTP_REQUEST_METHOD) or span.attributes.get(SpanAttributes.HTTP_METHOD),
250277
path,
251278
)
252279
except Exception: # pylint: disable=broad-except
253280
pass
254-
status_code = span.attributes.get(SpanAttributes.HTTP_STATUS_CODE)
281+
status_code = span.attributes.get(HTTP_RESPONSE_STATUS_CODE) \
282+
or span.attributes.get(SpanAttributes.HTTP_STATUS_CODE)
255283
if status_code:
256284
try:
257285
status_code = int(status_code) # type: ignore
@@ -263,12 +291,10 @@ def _convert_span_to_envelope(span: ReadableSpan) -> TelemetryItem:
263291
# Success criteria for server spans depends on span.success and the actual status code
264292
data.success = span.status.is_ok and status_code and status_code not in range(400, 500)
265293
elif SpanAttributes.MESSAGING_SYSTEM in span.attributes: # Messaging
266-
if SpanAttributes.NET_PEER_IP in span.attributes:
267-
envelope.tags[ContextTagKeys.AI_LOCATION_IP] = span.attributes[SpanAttributes.NET_PEER_IP]
268294
if span.attributes.get(SpanAttributes.MESSAGING_DESTINATION):
269-
if span.attributes.get(SpanAttributes.NET_PEER_NAME):
295+
if span.attributes.get(CLIENT_ADDRESS) or span.attributes.get(SpanAttributes.NET_PEER_NAME):
270296
data.source = "{}/{}".format(
271-
span.attributes.get(SpanAttributes.NET_PEER_NAME),
297+
span.attributes.get(CLIENT_ADDRESS) or span.attributes.get(SpanAttributes.NET_PEER_NAME),
272298
span.attributes.get(SpanAttributes.MESSAGING_DESTINATION),
273299
)
274300
elif span.attributes.get(SpanAttributes.NET_PEER_IP):
@@ -515,7 +541,8 @@ def _is_standard_attribute(key: str) -> bool:
515541
for prefix in _STANDARD_OPENTELEMETRY_ATTRIBUTE_PREFIXES:
516542
if key.startswith(prefix):
517543
return True
518-
return key in _STANDARD_AZURE_MONITOR_ATTRIBUTES
544+
return key in _STANDARD_AZURE_MONITOR_ATTRIBUTES or \
545+
key in _STANDARD_OPENTELEMETRY_HTTP_ATTRIBUTES
519546

520547

521548
def _get_trace_export_result(result: ExportResult) -> SpanExportResult:

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_utils.py

Lines changed: 72 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
from typing import no_type_check, Optional, Tuple
55
from urllib.parse import urlparse
66

7+
from opentelemetry.semconv.attributes import (
8+
client_attributes,
9+
server_attributes,
10+
url_attributes,
11+
user_agent_attributes,
12+
)
713
from opentelemetry.semconv.trace import DbSystemValues, SpanAttributes
814
from opentelemetry.util.types import Attributes
915

@@ -60,8 +66,10 @@ def _is_sql_db(db_system: str) -> bool:
6066
def _get_azure_sdk_target_source(attributes: Attributes) -> Optional[str]:
6167
# Currently logic only works for ServiceBus and EventHub
6268
if attributes:
63-
peer_address = attributes.get("peer.address")
64-
destination = attributes.get("message_bus.destination")
69+
# New semconv attributes: https://github.com/Azure/azure-sdk-for-python/pull/29203
70+
# TODO: Keep track of when azure-sdk supports stable semconv for these fields
71+
peer_address = attributes.get("net.peer.name") or attributes.get("peer.address")
72+
destination = attributes.get("messaging.destination.name") or attributes.get("message_bus.destination")
6573
if peer_address and destination:
6674
return str(peer_address) + "/" + str(destination)
6775
return None
@@ -217,37 +225,77 @@ def _get_target_for_rpc_dependency(target: Optional[str], attributes: Attributes
217225
return target
218226

219227

228+
# Request
229+
230+
@no_type_check
231+
def _get_location_ip(attributes: Attributes) -> Optional[str]:
232+
return attributes.get(client_attributes.CLIENT_ADDRESS) or \
233+
attributes.get(SpanAttributes.HTTP_CLIENT_IP) or \
234+
attributes.get(SpanAttributes.NET_PEER_IP) # We assume non-http spans don't have http related attributes
235+
236+
237+
@no_type_check
238+
def _get_user_agent(attributes: Attributes) -> Optional[str]:
239+
return attributes.get(user_agent_attributes.USER_AGENT_ORIGINAL) or \
240+
attributes.get(SpanAttributes.HTTP_USER_AGENT)
241+
242+
220243
@no_type_check
221244
def _get_url_for_http_request(attributes: Attributes) -> Optional[str]:
222245
url = ""
223246
if attributes:
247+
# Url
248+
if url_attributes.URL_FULL in attributes:
249+
return attributes[url_attributes.URL_FULL]
224250
if SpanAttributes.HTTP_URL in attributes:
225251
return attributes[SpanAttributes.HTTP_URL]
226-
scheme = attributes.get(SpanAttributes.HTTP_SCHEME)
227-
http_target = attributes.get(SpanAttributes.HTTP_TARGET)
252+
# Scheme
253+
scheme = attributes.get(url_attributes.URL_SCHEME) or attributes.get(SpanAttributes.HTTP_SCHEME)
254+
# Target
255+
http_target = ""
256+
if url_attributes.URL_PATH in attributes:
257+
http_target = attributes.get(url_attributes.URL_PATH, "")
258+
if url_attributes.URL_QUERY in attributes:
259+
http_target = "{}?{}".format(
260+
http_target,
261+
attributes.get(url_attributes.URL_QUERY, "")
262+
)
263+
if not http_target:
264+
http_target = attributes.get(SpanAttributes.HTTP_TARGET)
228265
if scheme and http_target:
229-
if SpanAttributes.HTTP_HOST in attributes:
266+
# Host
267+
http_host = ""
268+
if server_attributes.SERVER_ADDRESS in attributes:
269+
http_host = attributes.get(server_attributes.SERVER_ADDRESS, "")
270+
if server_attributes.SERVER_PORT in attributes:
271+
http_host = "{}:{}".format(
272+
http_host,
273+
attributes.get(server_attributes.SERVER_PORT, "")
274+
)
275+
if not http_host:
276+
http_host = attributes.get(SpanAttributes.HTTP_HOST, "")
277+
if http_host:
230278
url = "{}://{}{}".format(
231279
scheme,
232-
attributes[SpanAttributes.HTTP_HOST],
280+
http_host,
281+
http_target,
282+
)
283+
elif SpanAttributes.HTTP_SERVER_NAME in attributes:
284+
server_name = attributes[SpanAttributes.HTTP_SERVER_NAME]
285+
host_port = attributes.get(SpanAttributes.NET_HOST_PORT, "")
286+
url = "{}://{}:{}{}".format(
287+
scheme,
288+
server_name,
289+
host_port,
290+
http_target,
291+
)
292+
elif SpanAttributes.NET_HOST_NAME in attributes:
293+
host_name = attributes[SpanAttributes.NET_HOST_NAME]
294+
host_port = attributes.get(SpanAttributes.NET_HOST_PORT, "")
295+
url = "{}://{}:{}{}".format(
296+
scheme,
297+
host_name,
298+
host_port,
233299
http_target,
234300
)
235-
elif SpanAttributes.NET_HOST_PORT in attributes:
236-
host_port = attributes[SpanAttributes.NET_HOST_PORT]
237-
if SpanAttributes.HTTP_SERVER_NAME in attributes:
238-
server_name = attributes[SpanAttributes.HTTP_SERVER_NAME]
239-
url = "{}://{}:{}{}".format(
240-
scheme,
241-
server_name,
242-
host_port,
243-
http_target,
244-
)
245-
elif SpanAttributes.NET_HOST_NAME in attributes:
246-
host_name = attributes[SpanAttributes.NET_HOST_NAME]
247-
url = "{}://{}:{}{}".format(
248-
scheme,
249-
host_name,
250-
host_port,
251-
http_target,
252-
)
253301
return url

0 commit comments

Comments
 (0)