Skip to content

Commit 34f4992

Browse files
authored
Merge pull request #74 from microsoftgraph/feat/telemetry-handler
Feat/telemetry handler
2 parents 368daff + 774f058 commit 34f4992

File tree

10 files changed

+318
-17
lines changed

10 files changed

+318
-17
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ print(result.json())
6161

6262
For more information on how to use the package, refer to the [samples](https://github.com/microsoftgraph/msgraph-sdk-python-core/tree/dev/samples).
6363

64+
65+
## Telemetry Metadata
66+
67+
This library captures metadata by default that provides insights into its usage and helps to improve the developer experience. This metadata includes the `SdkVersion`, `RuntimeEnvironment` and `HostOs` on which the client is running.
68+
6469
## Issues
6570

6671
View or log issues on the [Issues](https://github.com/microsoftgraph/msgraph-sdk-python-core/issues) tab in the repo.

msgraph/core/_client_factory.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .middleware.authorization import AuthorizationHandler
1313
from .middleware.middleware import BaseMiddleware, MiddlewarePipeline
1414
from .middleware.retry import RetryHandler
15+
from .middleware.telemetry import TelemetryHandler
1516

1617

1718
class HTTPClientFactory:
@@ -53,6 +54,7 @@ def create_with_default_middleware(self, credential: TokenCredential, **kwargs)
5354
middleware = [
5455
AuthorizationHandler(credential, **kwargs),
5556
RetryHandler(**kwargs),
57+
TelemetryHandler(),
5658
]
5759
self._register(middleware)
5860
return self.session

msgraph/core/middleware/middleware.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,22 @@
1212

1313
class MiddlewarePipeline(HTTPAdapter):
1414
"""MiddlewarePipeline, entry point of middleware
15-
1615
The pipeline is implemented as a linked-list, read more about
1716
it here https://buffered.dev/middleware-python-requests/
1817
"""
1918
def __init__(self):
2019
super().__init__()
21-
self._middleware = None
20+
self._current_middleware = None
21+
self._first_middleware = None
2222
self.poolmanager = PoolManager(ssl_version=ssl.PROTOCOL_TLSv1_2)
2323

2424
def add_middleware(self, middleware):
2525
if self._middleware_present():
26-
self._middleware.next = middleware
26+
self._current_middleware.next = middleware
27+
self._current_middleware = middleware
2728
else:
28-
self._middleware = middleware
29+
self._first_middleware = middleware
30+
self._current_middleware = self._first_middleware
2931

3032
def send(self, request, **kwargs):
3133

@@ -34,12 +36,12 @@ def send(self, request, **kwargs):
3436
request.context = RequestContext(dict(), headers)
3537

3638
if self._middleware_present():
37-
return self._middleware.send(request, **kwargs)
39+
return self._first_middleware.send(request, **kwargs)
3840
# No middleware in pipeline, call superclass' send
3941
return super().send(request, **kwargs)
4042

4143
def _middleware_present(self):
42-
return self._middleware
44+
return self._current_middleware
4345

4446

4547
class BaseMiddleware(HTTPAdapter):

msgraph/core/middleware/options/retry_middleware_options.py

Lines changed: 0 additions & 7 deletions
This file was deleted.

msgraph/core/middleware/retry.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import datetime
22
import random
3-
import sys
43
import time
54
from email.utils import parsedate_to_datetime
65

7-
from msgraph.core.middleware.middleware import BaseMiddleware
6+
from .._enums import FeatureUsageFlag
7+
from .middleware import BaseMiddleware
88

99

1010
class RetryHandler(BaseMiddleware):

msgraph/core/middleware/telemetry.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import platform
2+
3+
from urllib3.util import parse_url
4+
5+
from .._constants import SDK_VERSION
6+
from .._enums import NationalClouds
7+
from .middleware import BaseMiddleware
8+
9+
10+
class TelemetryHandler(BaseMiddleware):
11+
"""Middleware component that attaches metadata to a Graph request in order to help
12+
the SDK team improve the developer experience.
13+
"""
14+
def send(self, request, **kwargs):
15+
16+
if self.is_graph_url(request.url):
17+
self._add_client_request_id_header(request)
18+
self._append_sdk_version_header(request)
19+
self._add_host_os_header(request)
20+
self._add_runtime_environment_header(request)
21+
22+
response = super().send(request, **kwargs)
23+
return response
24+
25+
def is_graph_url(self, url):
26+
"""Check if the request is made to a graph endpoint. We do not add telemetry headers to
27+
non-graph endpoints"""
28+
endpoints = set(item.value for item in NationalClouds)
29+
30+
base_url = parse_url(url)
31+
endpoint = "{}://{}".format(
32+
base_url.scheme,
33+
base_url.netloc,
34+
)
35+
return endpoint in endpoints
36+
37+
def _add_client_request_id_header(self, request) -> None:
38+
"""Add a client-request-id header with GUID value to request"""
39+
request.headers.update(
40+
{'client-request-id': '{}'.format(request.context.client_request_id)}
41+
)
42+
43+
def _append_sdk_version_header(self, request) -> None:
44+
"""Add SdkVersion request header to each request to identify the language and
45+
version of the client SDK library(s).
46+
Also adds the featureUsage value.
47+
"""
48+
if 'sdkVersion' in request.headers:
49+
sdk_version = request.headers.get('sdkVersion')
50+
if not sdk_version == f'graph-python-core/{SDK_VERSION} '\
51+
f'(featureUsage={request.context.feature_usage})':
52+
request.headers.update(
53+
{
54+
'sdkVersion':
55+
f'graph-python-core/{SDK_VERSION},{ sdk_version} '\
56+
f'(featureUsage={request.context.feature_usage})'
57+
}
58+
)
59+
else:
60+
request.headers.update(
61+
{
62+
'sdkVersion':
63+
f'graph-python-core/{SDK_VERSION} '\
64+
f'(featureUsage={request.context.feature_usage})'
65+
}
66+
)
67+
68+
def _add_host_os_header(self, request) -> None:
69+
"""
70+
Add HostOS request header to each request to help identify the OS
71+
on which our client SDK is running on
72+
"""
73+
system = platform.system()
74+
version = platform.version()
75+
host_os = f'{system} {version}'
76+
request.headers.update({'HostOs': host_os})
77+
78+
def _add_runtime_environment_header(self, request) -> None:
79+
"""
80+
Add RuntimeEnvironment request header to capture the runtime framework
81+
on which the client SDK is running on.
82+
"""
83+
python_version = platform.python_version()
84+
runtime_environment = f'Python/{python_version}'
85+
request.headers.update({'RuntimeEnvironment': runtime_environment})

tests/integration/test_telemetry.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import platform
2+
import re
3+
import uuid
4+
5+
import pytest
6+
7+
from msgraph.core import SDK_VERSION, APIVersion, GraphClient, NationalClouds
8+
9+
BASE_URL = NationalClouds.Global + '/' + APIVersion.v1
10+
11+
12+
@pytest.fixture
13+
def graph_client():
14+
scopes = ['user.read']
15+
credential = _CustomTokenCredential()
16+
client = GraphClient(credential=credential, scopes=scopes)
17+
return client
18+
19+
20+
class _CustomTokenCredential:
21+
def get_token(self, scopes):
22+
return ['{token:https://graph.microsoft.com/}']
23+
24+
25+
def test_telemetry_handler(graph_client):
26+
"""
27+
Test telemetry handler updates the graph request with the requisite headers
28+
"""
29+
response = graph_client.get('https://graph.microsoft.com/v1.0/me')
30+
system = platform.system()
31+
version = platform.version()
32+
host_os = f'{system} {version}'
33+
python_version = platform.python_version()
34+
runtime_environment = f'Python/{python_version}'
35+
36+
assert response.status_code == 401
37+
assert response.request.headers["client-request-id"]
38+
assert response.request.headers["sdkVersion"].startswith('graph-python-core/' + SDK_VERSION)
39+
assert response.request.headers["HostOs"] == host_os
40+
assert response.request.headers["RuntimeEnvironment"] == runtime_environment
41+
42+
43+
def test_telemetry_handler_non_graph_url(graph_client):
44+
"""
45+
Test telemetry handler does not updates the request headers for non-graph requests
46+
"""
47+
response = graph_client.get('https://httpbin.org/status/200')
48+
49+
assert response.status_code == 200
50+
with pytest.raises(KeyError):
51+
response.request.headers["client-request-id"]
52+
response.request.headers["sdkVersion"]
53+
response.request.headers["HostOs"]
54+
response.request.headers["RuntimeEnvironment"]
55+
56+
57+
def test_custom_client_request_id(graph_client):
58+
"""
59+
Test customer provided client request id overrides default value
60+
"""
61+
custom_id = str(uuid.uuid4())
62+
response = graph_client.get(
63+
'https://httpbin.org/status/200', headers={"client-request-id": custom_id}
64+
)
65+
66+
assert response.status_code == 200
67+
assert response.request.context.client_request_id == custom_id
68+
with pytest.raises(KeyError):
69+
response.request.headers["client-request-id"]
70+
response.request.headers["sdkVersion"]
71+
response.request.headers["HostOs"]
72+
response.request.headers["RuntimeEnvironment"]

tests/unit/test_middleware_pipeline.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ def test_adds_middlewares_in_order(self):
1414
middleware_pipeline.add_middleware(MockRequestMiddleware1())
1515
middleware_pipeline.add_middleware(MockRequestMiddleware2())
1616

17-
first_middleware = middleware_pipeline._middleware
18-
second_middleware = middleware_pipeline._middleware.next
17+
first_middleware = middleware_pipeline._first_middleware
18+
second_middleware = middleware_pipeline._first_middleware.next
1919

2020
self.assertIsInstance(first_middleware, MockRequestMiddleware1)
2121
self.assertIsInstance(second_middleware, MockRequestMiddleware2)

0 commit comments

Comments
 (0)