Skip to content
This repository was archived by the owner on Oct 14, 2024. It is now read-only.

Commit ba1af4d

Browse files
authored
Merge pull request #434 from Strazz1337/fix-adhere-to-otel-span
fix(spans): adhere attribute name to otel semver
2 parents afce579 + dac0eec commit ba1af4d

16 files changed

+199
-157
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.3.4] - 2024-10-11
9+
10+
### Changed
11+
- Updated HTTP span attributes to comply with updated OpenTelemetry semantic conventions. [#409](https://github.com/microsoft/kiota-http-python/issues/409)
12+
813
## [1.3.3] - 2024-08-12
914

1015
### Added

kiota_http/_version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION: str = '1.3.3'
1+
VERSION: str = "1.3.4"

kiota_http/httpx_request_adapter.py

+18-12
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@
2424
)
2525
from kiota_abstractions.store import BackingStoreFactory, BackingStoreFactorySingleton
2626
from opentelemetry import trace
27-
from opentelemetry.semconv.trace import SpanAttributes
27+
from opentelemetry.semconv.attributes.http_attributes import (
28+
HTTP_RESPONSE_STATUS_CODE,
29+
HTTP_REQUEST_METHOD,
30+
)
31+
from opentelemetry.semconv.attributes.network_attributes import NETWORK_PROTOCOL_NAME
32+
from opentelemetry.semconv.attributes.server_attributes import SERVER_ADDRESS
33+
from opentelemetry.semconv.attributes.url_attributes import URL_SCHEME, URL_FULL
2834

2935
from kiota_http._exceptions import (
3036
BackingStoreError,
@@ -529,15 +535,15 @@ async def get_http_response_message(
529535
resp = await self._http_client.send(request)
530536
if not resp:
531537
raise ResponseError("Unable to get response from request")
532-
parent_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, resp.status_code)
538+
parent_span.set_attribute(HTTP_RESPONSE_STATUS_CODE, resp.status_code)
533539
if http_version := resp.http_version:
534-
parent_span.set_attribute(SpanAttributes.HTTP_FLAVOR, http_version)
540+
parent_span.set_attribute(NETWORK_PROTOCOL_NAME, http_version)
535541

536542
if content_length := resp.headers.get("Content-Length", None):
537-
parent_span.set_attribute(SpanAttributes.HTTP_RESPONSE_CONTENT_LENGTH, content_length)
543+
parent_span.set_attribute("http.response.body.size", content_length)
538544

539545
if content_type := resp.headers.get("Content-Type", None):
540-
parent_span.set_attribute("http.response_content_type", content_type)
546+
parent_span.set_attribute("http.response.header.content-type", content_type)
541547
_get_http_resp_span.end()
542548
return await self.retry_cae_response_if_required(resp, request_info, claims)
543549

@@ -586,15 +592,15 @@ def get_request_from_request_information(
586592
)
587593
url = parse.urlparse(request_info.url)
588594
otel_attributes = {
589-
SpanAttributes.HTTP_METHOD: request_info.http_method,
595+
HTTP_REQUEST_METHOD: request_info.http_method,
590596
"http.port": url.port,
591-
SpanAttributes.HTTP_HOST: url.hostname,
592-
SpanAttributes.HTTP_SCHEME: url.scheme,
593-
"http.uri_template": request_info.url_template,
597+
URL_SCHEME: url.hostname,
598+
SERVER_ADDRESS: url.scheme,
599+
"url.uri_template": request_info.url_template,
594600
}
595601

596602
if self.observability_options.include_euii_attributes:
597-
otel_attributes.update({"http.uri": url.geturl()})
603+
otel_attributes.update({URL_FULL: url.geturl()})
598604

599605
request = self._http_client.build_request(
600606
method=request_info.http_method.value,
@@ -610,10 +616,10 @@ def get_request_from_request_information(
610616
setattr(request, "options", request_options)
611617

612618
if content_length := request.headers.get("Content-Length", None):
613-
otel_attributes.update({SpanAttributes.HTTP_REQUEST_CONTENT_LENGTH: content_length})
619+
otel_attributes.update({"http.request.body.size": content_length})
614620

615621
if content_type := request.headers.get("Content-Type", None):
616-
otel_attributes.update({"http.request_content_type": content_type})
622+
otel_attributes.update({"http.request.header.content-type": content_type})
617623
attribute_span.set_attributes(otel_attributes)
618624
_get_request_span.set_attributes(otel_attributes)
619625
_get_request_span.end()

kiota_http/middleware/redirect_handler.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import httpx
44
from kiota_abstractions.request_option import RequestOption
5-
from opentelemetry.semconv.trace import SpanAttributes
5+
from opentelemetry.semconv.attributes.http_attributes import (
6+
HTTP_RESPONSE_STATUS_CODE,
7+
)
68

79
from .._exceptions import RedirectError
810
from .middleware import BaseMiddleware
@@ -75,7 +77,7 @@ async def send(
7577
request, f"RedirectHandler_send - redirect {len(history)}"
7678
)
7779
response = await super().send(request, transport)
78-
_redirect_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code)
80+
_redirect_span.set_attribute(HTTP_RESPONSE_STATUS_CODE, response.status_code)
7981
redirect_location = self.get_redirect_location(response)
8082

8183
if redirect_location and current_options.should_redirect:

kiota_http/middleware/retry_handler.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
import httpx
88
from kiota_abstractions.request_option import RequestOption
9-
from opentelemetry.semconv.trace import SpanAttributes
9+
from opentelemetry.semconv.attributes.http_attributes import (
10+
HTTP_RESPONSE_STATUS_CODE,
11+
)
1012

1113
from .middleware import BaseMiddleware
1214
from .options import RetryHandlerOption
@@ -82,7 +84,7 @@ async def send(self, request: httpx.Request, transport: httpx.AsyncBaseTransport
8284
while retry_valid:
8385
start_time = time.time()
8486
response = await super().send(request, transport)
85-
_retry_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code)
87+
_retry_span.set_attribute(HTTP_RESPONSE_STATUS_CODE, response.status_code)
8688
# check that max retries has not been hit
8789
retry_valid = self.check_retry_valid(retry_count, current_options)
8890

@@ -99,7 +101,7 @@ async def send(self, request: httpx.Request, transport: httpx.AsyncBaseTransport
99101
# increment the count for retries
100102
retry_count += 1
101103
request.headers.update({'retry-attempt': f'{retry_count}'})
102-
_retry_span.set_attribute(SpanAttributes.HTTP_RETRY_COUNT, retry_count)
104+
_retry_span.set_attribute('http.request.resend_count', retry_count)
103105
continue
104106
break
105107
if response is None:

kiota_http/middleware/url_replace_handler.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import httpx
22
from kiota_abstractions.request_option import RequestOption
3-
from opentelemetry.semconv.trace import SpanAttributes
3+
from opentelemetry.semconv.attributes.url_attributes import (URL_FULL)
44

55
from .middleware import BaseMiddleware
66
from .options import UrlReplaceHandlerOption
@@ -40,7 +40,7 @@ async def send(
4040
url_string: str = str(request.url) # type: ignore
4141
url_string = self.replace_url_segment(url_string, current_options)
4242
request.url = httpx.URL(url_string)
43-
_enable_span.set_attribute(SpanAttributes.HTTP_URL, str(request.url))
43+
_enable_span.set_attribute(URL_FULL, str(request.url))
4444
response = await super().send(request, transport)
4545
_enable_span.end()
4646
return response

kiota_http/middleware/user_agent_handler.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from httpx import AsyncBaseTransport, Request, Response
22
from kiota_abstractions.request_option import RequestOption
3-
from opentelemetry.semconv.trace import SpanAttributes
43

54
from .middleware import BaseMiddleware
65
from .options import UserAgentHandlerOption

tests/conftest.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313

1414
from .helpers import MockTransport, MockErrorObject, MockResponseObject, OfficeLocation
1515

16+
1617
@pytest.fixture
1718
def sample_headers():
1819
return {"Content-Type": "application/json"}
1920

21+
2022
@pytest.fixture
2123
def auth_provider():
2224
return AnonymousAuthenticationProvider()
@@ -26,6 +28,7 @@ def auth_provider():
2628
def request_info():
2729
return RequestInformation()
2830

31+
2932
@pytest.fixture
3033
def mock_async_transport():
3134
return MockTransport()
@@ -57,27 +60,32 @@ def mock_error_500_map():
5760
"500": Exception("Internal Server Error"),
5861
}
5962

63+
6064
@pytest.fixture
6165
def mock_apierror_map(sample_headers):
6266
return {
6367
"400": APIError("Resource not found", 400, sample_headers),
6468
"500": APIError("Custom Internal Server Error", 500, sample_headers)
6569
}
6670

71+
6772
@pytest.fixture
6873
def mock_apierror_XXX_map(sample_headers):
69-
return {"XXX": APIError("OdataError",400, sample_headers)}
70-
74+
return {"XXX": APIError("OdataError", 400, sample_headers)}
75+
76+
7177
@pytest.fixture
7278
def mock_request_adapter(sample_headers):
7379
resp = httpx.Response(json={'error': 'not found'}, status_code=404, headers=sample_headers)
7480
mock_request_adapter = AsyncMock
7581
mock_request_adapter.get_http_response_message = AsyncMock(return_value=resp)
7682

83+
7784
@pytest.fixture
7885
def simple_error_response(sample_headers):
7986
return httpx.Response(json={'error': 'not found'}, status_code=404, headers=sample_headers)
8087

88+
8189
@pytest.fixture
8290
def simple_success_response(sample_headers):
8391
return httpx.Response(json={'message': 'Success!'}, status_code=200, headers=sample_headers)
@@ -153,9 +161,7 @@ def mock_users_response(mocker):
153161

154162
@pytest.fixture
155163
def mock_primitive_collection_response(sample_headers):
156-
return httpx.Response(
157-
200, json=[12.1, 12.2, 12.3, 12.4, 12.5], headers=sample_headers
158-
)
164+
return httpx.Response(200, json=[12.1, 12.2, 12.3, 12.4, 12.5], headers=sample_headers)
159165

160166

161167
@pytest.fixture

tests/helpers/mock_async_transport.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import httpx
22

3+
34
class MockTransport():
5+
46
async def handle_async_request(self, request):
5-
return httpx.Response(200, request=request, content=b'Hello World', headers={"Content-Type": "application/json", "test": "test_response_header"})
7+
return httpx.Response(
8+
200,
9+
request=request,
10+
content=b'Hello World',
11+
headers={
12+
"Content-Type": "application/json",
13+
"test": "test_response_header"
14+
}
15+
)

tests/middleware_tests/test_base_middleware.py

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ def test_next_is_none():
99
middleware = BaseMiddleware()
1010
assert middleware.next is None
1111

12+
1213
def test_span_created(request_info):
1314
"""Ensures the current span is returned and the parent_span is not set."""
1415
middleware = BaseMiddleware()

tests/middleware_tests/test_headers_inspection_handler.py

+11-16
Original file line numberDiff line numberDiff line change
@@ -26,47 +26,42 @@ def test_custom_config():
2626

2727
options = HeadersInspectionHandlerOption(inspect_request_headers=False)
2828
assert not options.inspect_request_headers
29-
30-
29+
30+
3131
def test_headers_inspection_handler_construction():
3232
"""
3333
Ensures the Header Inspection handler instance is set.
3434
"""
3535
handler = HeadersInspectionHandler()
3636
assert handler
37-
37+
38+
3839
@pytest.mark.asyncio
3940
async def test_headers_inspection_handler_gets_headers():
41+
4042
def request_handler(request: httpx.Request):
4143
return httpx.Response(
42-
200,
43-
json={"text": "Hello, world!"},
44-
headers={'test_response': 'test_response_header'}
44+
200, json={"text": "Hello, world!"}, headers={'test_response': 'test_response_header'}
4545
)
46+
4647
handler = HeadersInspectionHandler()
47-
48+
4849
# First request
4950
request = httpx.Request(
50-
'GET',
51-
'https://localhost',
52-
headers={'test_request': 'test_request_header'}
51+
'GET', 'https://localhost', headers={'test_request': 'test_request_header'}
5352
)
5453
mock_transport = httpx.MockTransport(request_handler)
5554
resp = await handler.send(request, mock_transport)
5655
assert resp.status_code == 200
5756
assert handler.options.request_headers.try_get('test_request') == {'test_request_header'}
5857
assert handler.options.response_headers.try_get('test_response') == {'test_response_header'}
59-
58+
6059
# Second request
6160
request2 = httpx.Request(
62-
'GET',
63-
'https://localhost',
64-
headers={'test_request_2': 'test_request_header_2'}
61+
'GET', 'https://localhost', headers={'test_request_2': 'test_request_header_2'}
6562
)
6663
resp = await handler.send(request2, mock_transport)
6764
assert resp.status_code == 200
6865
assert not handler.options.request_headers.try_get('test_request') == {'test_request_header'}
6966
assert handler.options.request_headers.try_get('test_request_2') == {'test_request_header_2'}
7067
assert handler.options.response_headers.try_get('test_response') == {'test_response_header'}
71-
72-

tests/middleware_tests/test_parameters_name_decoding_handler.py

+34-18
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from kiota_http.middleware.options import ParametersNameDecodingHandlerOption
66

77
OPTION_KEY = "ParametersNameDecodingHandlerOption"
8+
9+
810
def test_no_config():
911
"""
1012
Test that default values are used if no custom confguration is passed
@@ -19,9 +21,7 @@ def test_custom_options():
1921
"""
2022
Test that default configuration is overrriden if custom configuration is provided
2123
"""
22-
options = ParametersNameDecodingHandlerOption(
23-
enable=False, characters_to_decode=[".", "-"]
24-
)
24+
options = ParametersNameDecodingHandlerOption(enable=False, characters_to_decode=[".", "-"])
2525
handler = ParametersNameDecodingHandler(options)
2626

2727
assert handler.options.enabled is not True
@@ -35,24 +35,40 @@ async def test_decodes_query_parameter_names_only():
3535
Test that only query parameter names are decoded
3636
"""
3737
encoded_decoded = [
38-
("http://localhost?%24select=diplayName&api%2Dversion=2", "http://localhost?$select=diplayName&api-version=2"),
39-
("http://localhost?%24select=diplayName&api%7Eversion=2", "http://localhost?$select=diplayName&api~version=2"),
40-
("http://localhost?%24select=diplayName&api%2Eversion=2", "http://localhost?$select=diplayName&api.version=2"),
41-
("http://localhost:888?%24select=diplayName&api%2Dversion=2", "http://localhost:888?$select=diplayName&api-version=2"),
42-
("http://localhost", "http://localhost"),
43-
("https://google.com/?q=1%2b2", "https://google.com/?q=1%2b2"),
44-
("https://google.com/?q=M%26A", "https://google.com/?q=M%26A"),
45-
("https://google.com/?q=1%2B2", "https://google.com/?q=1%2B2"), # Values are not decoded
46-
("https://google.com/?q=M%26A", "https://google.com/?q=M%26A"), # Values are not decoded
47-
("https://google.com/?q%2D1=M%26A", "https://google.com/?q-1=M%26A"), # Values are not decoded but params are
48-
("https://google.com/?q%2D1&q=M%26A=M%26A", "https://google.com/?q-1&q=M%26A=M%26A"), # Values are not decoded but params are
49-
("https://graph.microsoft.com?%24count=true&query=%24top&created%2Din=2022-10-05&q=1%2b2&q2=M%26A&subject%2Ename=%7eWelcome&%24empty",
50-
"https://graph.microsoft.com?$count=true&query=%24top&created-in=2022-10-05&q=1%2b2&q2=M%26A&subject.name=%7eWelcome&$empty")
38+
(
39+
"http://localhost?%24select=diplayName&api%2Dversion=2",
40+
"http://localhost?$select=diplayName&api-version=2"
41+
),
42+
(
43+
"http://localhost?%24select=diplayName&api%7Eversion=2",
44+
"http://localhost?$select=diplayName&api~version=2"
45+
),
46+
(
47+
"http://localhost?%24select=diplayName&api%2Eversion=2",
48+
"http://localhost?$select=diplayName&api.version=2"
49+
),
50+
(
51+
"http://localhost:888?%24select=diplayName&api%2Dversion=2",
52+
"http://localhost:888?$select=diplayName&api-version=2"
53+
),
54+
("http://localhost", "http://localhost"),
55+
("https://google.com/?q=1%2b2", "https://google.com/?q=1%2b2"),
56+
("https://google.com/?q=M%26A", "https://google.com/?q=M%26A"),
57+
("https://google.com/?q=1%2B2", "https://google.com/?q=1%2B2"), # Values are not decoded
58+
("https://google.com/?q=M%26A", "https://google.com/?q=M%26A"), # Values are not decoded
59+
("https://google.com/?q%2D1=M%26A",
60+
"https://google.com/?q-1=M%26A"), # Values are not decoded but params are
61+
("https://google.com/?q%2D1&q=M%26A=M%26A",
62+
"https://google.com/?q-1&q=M%26A=M%26A"), # Values are not decoded but params are
63+
(
64+
"https://graph.microsoft.com?%24count=true&query=%24top&created%2Din=2022-10-05&q=1%2b2&q2=M%26A&subject%2Ename=%7eWelcome&%24empty",
65+
"https://graph.microsoft.com?$count=true&query=%24top&created-in=2022-10-05&q=1%2b2&q2=M%26A&subject.name=%7eWelcome&$empty"
66+
)
5167
]
52-
68+
5369
def request_handler(request: httpx.Request):
5470
return httpx.Response(200, json={"text": "Hello, world!"})
55-
71+
5672
handler = ParametersNameDecodingHandler()
5773
for encoded, decoded in encoded_decoded:
5874
request = httpx.Request('GET', encoded)

0 commit comments

Comments
 (0)