Skip to content

Commit 34180e4

Browse files
authored
Support redirect in live metrics (#35910)
1 parent a624a22 commit 34180e4

File tree

5 files changed

+138
-4
lines changed

5 files changed

+138
-4
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+
- Implement redirect for live metrics
8+
([#35910](https://github.com/Azure/azure-sdk-for-python/pull/35910))
9+
710
### Breaking Changes
811

912
### Bugs Fixed

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_quickpulse/_constants.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,11 @@ class _DocumentIngressDocumentType(Enum):
5252
Event = "Event"
5353
Trace = "Trace"
5454

55+
# Response Headers
56+
57+
_QUICKPULSE_ETAG_HEADER_NAME = "x-ms-qps-configuration-etag"
58+
_QUICKPULSE_POLLING_HEADER_NAME = "x-ms-qps-service-polling-interval-hint"
59+
_QUICKPULSE_REDIRECT_HEADER_NAME = "x-ms-qps-service-endpoint-redirect-v2"
60+
_QUICKPULSE_SUBSCRIBED_HEADER_NAME = "x-ms-qps-subscribed"
61+
5562
# cSpell:enable

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_quickpulse/_exporter.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Licensed under the MIT License.
33
import logging
44
from typing import Any, Optional
5+
import weakref
56

67
from opentelemetry.context import (
78
_SUPPRESS_INSTRUMENTATION_KEY,
@@ -28,10 +29,12 @@
2829
_LONG_PING_INTERVAL_SECONDS,
2930
_POST_CANCEL_INTERVAL_SECONDS,
3031
_POST_INTERVAL_SECONDS,
32+
_QUICKPULSE_SUBSCRIBED_HEADER_NAME,
3133
)
3234
from azure.monitor.opentelemetry.exporter._quickpulse._generated._configuration import QuickpulseClientConfiguration
3335
from azure.monitor.opentelemetry.exporter._quickpulse._generated._client import QuickpulseClient
3436
from azure.monitor.opentelemetry.exporter._quickpulse._generated.models import MonitoringDataPoint
37+
from azure.monitor.opentelemetry.exporter._quickpulse._policy import _QuickpulseRedirectPolicy
3538
from azure.monitor.opentelemetry.exporter._quickpulse._state import (
3639
_get_global_quickpulse_state,
3740
_is_ping_state,
@@ -85,9 +88,10 @@ def __init__(self, connection_string: Optional[str]) -> None:
8588
# TODO: Support AADaudience (scope)/credentials
8689
# Pass `None` for now until swagger definition is fixed
8790
config = QuickpulseClientConfiguration(credential=None) # type: ignore
91+
qp_redirect_policy = _QuickpulseRedirectPolicy(permit_redirects=False)
8892
policies = [
89-
# TODO: Support redirect
90-
config.redirect_policy,
93+
# Custom redirect policy for QP
94+
qp_redirect_policy,
9195
# Needed for serialization
9296
ContentDecodePolicy(),
9397
# Logging for client calls
@@ -102,6 +106,9 @@ def __init__(self, connection_string: Optional[str]) -> None:
102106
endpoint=self._live_endpoint,
103107
policies=policies
104108
)
109+
# Create a weakref of the client to the redirect policy so the endpoint can be
110+
# dynamically modified if redirect does occur
111+
qp_redirect_policy._qp_client_ref = weakref.ref(self._client)
105112

106113
MetricExporter.__init__(
107114
self,
@@ -146,7 +153,7 @@ def export(
146153
# If no response, assume unsuccessful
147154
result = MetricExportResult.FAILURE
148155
else:
149-
header = post_response._response_headers.get("x-ms-qps-subscribed") # pylint: disable=protected-access
156+
header = post_response._response_headers.get(_QUICKPULSE_SUBSCRIBED_HEADER_NAME) # pylint: disable=protected-access
150157
if header != "true":
151158
# User leaving the live metrics page will be treated as an unsuccessful
152159
result = MetricExportResult.FAILURE
@@ -240,7 +247,7 @@ def _ticker(self) -> None:
240247
self._base_monitoring_data_point,
241248
)
242249
if ping_response:
243-
header = ping_response._response_headers.get("x-ms-qps-subscribed") # pylint: disable=protected-access
250+
header = ping_response._response_headers.get(_QUICKPULSE_SUBSCRIBED_HEADER_NAME) # pylint: disable=protected-access
244251
if header and header == "true":
245252
# Switch state to post if subscribed
246253
_set_global_quickpulse_state(_QuickpulseState.POST_SHORT)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from typing import Any, Optional
5+
from urllib.parse import urlparse
6+
from weakref import ReferenceType
7+
8+
from azure.core.pipeline import PipelineResponse, policies
9+
10+
from azure.monitor.opentelemetry.exporter._quickpulse._constants import _QUICKPULSE_REDIRECT_HEADER_NAME
11+
from azure.monitor.opentelemetry.exporter._quickpulse._generated import QuickpulseClient
12+
13+
14+
# Quickpulse endpoint handles redirects via header instead of status codes
15+
# We use a custom RedirectPolicy to handle this use case
16+
# pylint: disable=protected-access
17+
class _QuickpulseRedirectPolicy(policies.RedirectPolicy):
18+
19+
def __init__(self, **kwargs: Any) -> None:
20+
# Weakref to QuickPulseClient instance
21+
self._qp_client_ref: Optional[ReferenceType[QuickpulseClient]] = None
22+
super().__init__(**kwargs)
23+
24+
# Gets the redirect location from header
25+
def get_redirect_location(self, response: PipelineResponse) -> Optional[str] :
26+
redirect_location = response.http_response.headers.get(_QUICKPULSE_REDIRECT_HEADER_NAME)
27+
qp_client = None
28+
if redirect_location:
29+
redirected_url = urlparse(redirect_location)
30+
if redirected_url.scheme and redirected_url.netloc:
31+
if self._qp_client_ref:
32+
qp_client = self._qp_client_ref()
33+
if qp_client and qp_client._client:
34+
# Set new endpoint to redirect location
35+
qp_client._client._base_url = f"{redirected_url.scheme}://{redirected_url.netloc}"
36+
return redirect_location # type: ignore
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import weakref
4+
import unittest
5+
from unittest import mock
6+
7+
from azure.monitor.opentelemetry.exporter._quickpulse._policy import _QuickpulseRedirectPolicy
8+
9+
10+
class TestQuickpulseRedirectPolicy(unittest.TestCase):
11+
12+
def test_get_redirect_location(self):
13+
policy = _QuickpulseRedirectPolicy()
14+
pipeline_resp_mock = mock.Mock()
15+
http_resp_mock = mock.Mock()
16+
headers = {
17+
"x-ms-qps-service-endpoint-redirect-v2": "https://eastus.livediagnostics.monitor.azure.com/QuickPulseService.svc"
18+
}
19+
http_resp_mock.headers = headers
20+
pipeline_resp_mock.http_response = http_resp_mock
21+
policy = _QuickpulseRedirectPolicy()
22+
qp_client_mock = mock.Mock()
23+
client_mock = mock.Mock()
24+
client_mock._base_url = "previous_url"
25+
qp_client_mock._client = client_mock
26+
qp_client_ref = weakref.ref(qp_client_mock)
27+
policy._qp_client_ref = qp_client_ref
28+
redirect_location = policy.get_redirect_location(pipeline_resp_mock)
29+
self.assertEqual(
30+
redirect_location,
31+
"https://eastus.livediagnostics.monitor.azure.com/QuickPulseService.svc",
32+
)
33+
self.assertEqual(
34+
client_mock._base_url,
35+
"https://eastus.livediagnostics.monitor.azure.com",
36+
)
37+
38+
def test_get_redirect_location_no_header(self):
39+
policy = _QuickpulseRedirectPolicy()
40+
pipeline_resp_mock = mock.Mock()
41+
http_resp_mock = mock.Mock()
42+
headers = {}
43+
http_resp_mock.headers = headers
44+
pipeline_resp_mock.http_response = http_resp_mock
45+
policy = _QuickpulseRedirectPolicy()
46+
self.assertIsNone(policy.get_redirect_location(pipeline_resp_mock))
47+
redirect_location = policy.get_redirect_location(pipeline_resp_mock)
48+
49+
def test_get_redirect_location_invalid_url(self):
50+
policy = _QuickpulseRedirectPolicy()
51+
pipeline_resp_mock = mock.Mock()
52+
http_resp_mock = mock.Mock()
53+
headers = {
54+
"x-ms-qps-service-endpoint-redirect-v2": "invalid_url"
55+
}
56+
http_resp_mock.headers = headers
57+
pipeline_resp_mock.http_response = http_resp_mock
58+
policy = _QuickpulseRedirectPolicy()
59+
qp_client_mock = mock.Mock()
60+
client_mock = mock.Mock()
61+
client_mock._base_url = "previous_url"
62+
qp_client_mock._client = client_mock
63+
qp_client_ref = weakref.ref(qp_client_mock)
64+
policy._qp_client_ref = qp_client_ref
65+
redirect_location = policy.get_redirect_location(pipeline_resp_mock)
66+
self.assertEqual(redirect_location, "invalid_url")
67+
self.assertEqual(client_mock._base_url,"previous_url")
68+
69+
def test_get_redirect_location_no_client(self):
70+
policy = _QuickpulseRedirectPolicy()
71+
pipeline_resp_mock = mock.Mock()
72+
http_resp_mock = mock.Mock()
73+
headers = {
74+
"x-ms-qps-service-endpoint-redirect-v2": "https://eastus.livediagnostics.monitor.azure.com/QuickPulseService.svc"
75+
}
76+
http_resp_mock.headers = headers
77+
pipeline_resp_mock.http_response = http_resp_mock
78+
policy = _QuickpulseRedirectPolicy()
79+
redirect_location = policy.get_redirect_location(pipeline_resp_mock)
80+
self.assertEqual(redirect_location, "https://eastus.livediagnostics.monitor.azure.com/QuickPulseService.svc")
81+
self.assertIsNone(policy._qp_client_ref)

0 commit comments

Comments
 (0)