Skip to content

Commit 98c31f0

Browse files
authored
[Exporter] Support redirect response in exporter (#20489)
1 parent 7330ea5 commit 98c31f0

File tree

3 files changed

+74
-21
lines changed

3 files changed

+74
-21
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Release History
22

3+
**Features**
4+
- Support stamp specific redirect in exporters
5+
([#20489](https://github.com/Azure/azure-sdk-for-python/pull/20489))
6+
37
**Breaking Changes**
48
- Change exporter OT to AI mapping fields following common schema
59
([#20445](https://github.com/Azure/azure-sdk-for-python/pull/20445))

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/_base.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
import tempfile
66
from enum import Enum
77
from typing import List, Any
8+
from urllib.parse import urlparse
89

910
from opentelemetry.sdk.trace.export import SpanExportResult
1011

1112
from azure.core.exceptions import HttpResponseError, ServiceRequestError
12-
from azure.core.pipeline.policies import ContentDecodePolicy, HttpLoggingPolicy, RequestIdPolicy
13+
from azure.core.pipeline.policies import ContentDecodePolicy, HttpLoggingPolicy, RedirectPolicy, RequestIdPolicy
1314
from azure.monitor.opentelemetry.exporter._generated import AzureMonitorClient
1415
from azure.monitor.opentelemetry.exporter._generated._configuration import AzureMonitorClientConfiguration
1516
from azure.monitor.opentelemetry.exporter._generated.models import TelemetryItem
@@ -43,6 +44,7 @@ def __init__(self, **kwargs: Any) -> None:
4344
self._instrumentation_key = parsed_connection_string.instrumentation_key
4445
self._timeout = 10.0 # networking timeout in seconds
4546
self._api_version = kwargs.get('api_version') or _SERVICE_API_LATEST
47+
self._consecutive_redirects = 0 # To prevent circular redirects
4648

4749
temp_suffix = self._instrumentation_key or ""
4850
default_storage_path = os.path.join(
@@ -56,7 +58,8 @@ def __init__(self, **kwargs: Any) -> None:
5658
config.user_agent_policy,
5759
config.proxy_policy,
5860
ContentDecodePolicy(**kwargs),
59-
config.redirect_policy,
61+
# Handle redirects in exporter, set new endpoint if redirected
62+
RedirectPolicy(permit_redirects=False),
6063
config.retry_policy,
6164
config.authentication_policy,
6265
config.custom_hook_policy,
@@ -100,6 +103,7 @@ def _transmit(self, envelopes: List[TelemetryItem]) -> ExportResult:
100103
try:
101104
track_response = self.client.track(envelopes)
102105
if not track_response.errors:
106+
self._consecutive_redirects = 0
103107
logger.info("Transmission succeeded: Item received: %s. Items accepted: %s",
104108
track_response.items_received, track_response.items_accepted)
105109
return ExportResult.SUCCESS
@@ -120,11 +124,33 @@ def _transmit(self, envelopes: List[TelemetryItem]) -> ExportResult:
120124
envelopes_to_store = [x.as_dict()
121125
for x in resend_envelopes]
122126
self.storage.put(envelopes_to_store)
127+
self._consecutive_redirects = 0
123128
return ExportResult.FAILED_RETRYABLE
124129

125130
except HttpResponseError as response_error:
126131
if _is_retryable_code(response_error.status_code):
127132
return ExportResult.FAILED_RETRYABLE
133+
if _is_redirect_code(response_error.status_code):
134+
self._consecutive_redirects = self._consecutive_redirects + 1
135+
if self._consecutive_redirects < self.client._config.redirect_policy.max_redirects: # pylint: disable=W0212
136+
if response_error.response and response_error.response.headers:
137+
location = response_error.response.headers.get("location")
138+
if location:
139+
url = urlparse(location)
140+
if url.scheme and url.netloc:
141+
# Change the host to the new redirected host
142+
self.client._config.host = "{}://{}".format(url.scheme, url.netloc) # pylint: disable=W0212
143+
# Attempt to export again
144+
return self._transmit(envelopes)
145+
logger.error(
146+
"Error parsing redirect information."
147+
)
148+
return ExportResult.FAILED_NOT_RETRYABLE
149+
logger.error(
150+
"Error sending telemetry because of circular redirects." \
151+
"Please check the integrity of your connection string."
152+
)
153+
return ExportResult.FAILED_NOT_RETRYABLE
128154
return ExportResult.FAILED_NOT_RETRYABLE
129155
except ServiceRequestError as request_error:
130156
# Errors when we're fairly sure that the server did not receive the
@@ -140,9 +166,20 @@ def _transmit(self, envelopes: List[TelemetryItem]) -> ExportResult:
140166
return ExportResult.FAILED_NOT_RETRYABLE
141167
return ExportResult.FAILED_NOT_RETRYABLE
142168
# No spans to export
169+
self._consecutive_redirects = 0
143170
return ExportResult.SUCCESS
144171

145172

173+
def _is_redirect_code(response_code: int) -> bool:
174+
"""
175+
Determine if response is a redirect response.
176+
"""
177+
return bool(response_code in(
178+
307, # Temporary redirect
179+
308, # Permanent redirect
180+
))
181+
182+
146183
def _is_retryable_code(response_code: int) -> bool:
147184
"""
148185
Determine if response is retryable

sdk/monitor/azure-monitor-opentelemetry-exporter/tests/test_base_exporter.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
from opentelemetry.sdk.trace.export import SpanExportResult
1313

1414
from azure.core.exceptions import HttpResponseError, ServiceRequestError
15+
from azure.core.pipeline.transport import HttpResponse
1516
from azure.monitor.opentelemetry.exporter.export._base import (
1617
BaseExporter,
1718
ExportResult,
1819
get_trace_export_result,
1920
)
20-
from azure.monitor.opentelemetry.exporter._generated.models import TelemetryItem
21+
from azure.monitor.opentelemetry.exporter._generated import AzureMonitorClient
22+
from azure.monitor.opentelemetry.exporter._generated.models import TelemetryItem, TrackResponse
2123

2224

2325
def throw(exc_type, *args, **kwargs):
@@ -115,44 +117,54 @@ def test_transmit_from_storage_lease_failure(self, requests_mock):
115117
self._base._transmit_from_storage()
116118
self.assertTrue(self._base.storage.get())
117119

118-
def test_transmit_request_timeout(self):
119-
with mock.patch("requests.Session.request", throw(requests.Timeout)):
120-
result = self._base._transmit(self._envelopes_to_export)
121-
self.assertEqual(result, ExportResult.FAILED_RETRYABLE)
122-
123120
def test_transmit_http_error_retryable(self):
124121
with mock.patch("azure.monitor.opentelemetry.exporter.export._base._is_retryable_code") as m:
125122
m.return_value = True
126-
with mock.patch("requests.Session.request", throw(HttpResponseError)):
123+
with mock.patch.object(AzureMonitorClient, 'track', throw(HttpResponseError)):
127124
result = self._base._transmit(self._envelopes_to_export)
128125
self.assertEqual(result, ExportResult.FAILED_RETRYABLE)
129126

130-
def test_transmit_http_error_retryable(self):
127+
def test_transmit_http_error_not_retryable(self):
131128
with mock.patch("azure.monitor.opentelemetry.exporter.export._base._is_retryable_code") as m:
132129
m.return_value = False
133-
with mock.patch("requests.Session.request", throw(HttpResponseError)):
130+
with mock.patch.object(AzureMonitorClient, 'track', throw(HttpResponseError)):
134131
result = self._base._transmit(self._envelopes_to_export)
135132
self.assertEqual(result, ExportResult.FAILED_NOT_RETRYABLE)
136133

134+
def test_transmit_http_error_redirect(self):
135+
response = HttpResponse(None, None)
136+
response.status_code = 307
137+
response.headers = {"location":"https://example.com"}
138+
prev_redirects = self._base.client._config.redirect_policy.max_redirects
139+
self._base.client._config.redirect_policy.max_redirects = 2
140+
prev_host = self._base.client._config.host
141+
error = HttpResponseError(response=response)
142+
with mock.patch.object(AzureMonitorClient, 'track') as post:
143+
post.side_effect = error
144+
result = self._base._transmit(self._envelopes_to_export)
145+
self.assertEqual(result, ExportResult.FAILED_NOT_RETRYABLE)
146+
self.assertEqual(post.call_count, 2)
147+
self.assertEqual(self._base.client._config.host, "https://example.com")
148+
self._base.client._config.redirect_policy.max_redirects = prev_redirects
149+
self._base.client._config.host = prev_host
150+
137151
def test_transmit_request_error(self):
138-
with mock.patch("requests.Session.request", throw(ServiceRequestError, message="error")):
152+
with mock.patch.object(AzureMonitorClient, 'track', throw(ServiceRequestError, message="error")):
139153
result = self._base._transmit(self._envelopes_to_export)
140154
self.assertEqual(result, ExportResult.FAILED_RETRYABLE)
141155

142156
def test_transmit_request_exception(self):
143-
with mock.patch("requests.Session.request", throw(Exception)):
157+
with mock.patch.object(AzureMonitorClient, 'track', throw(Exception)):
144158
result = self._base._transmit(self._envelopes_to_export)
145159
self.assertEqual(result, ExportResult.FAILED_NOT_RETRYABLE)
146160

147161
def test_transmission_200(self):
148-
with mock.patch("requests.Session.request") as post:
149-
post.return_value = MockResponse(200, json.dumps(
150-
{
151-
"itemsReceived": 1,
152-
"itemsAccepted": 1,
153-
"errors": [],
154-
}
155-
), reason="OK", content="")
162+
with mock.patch.object(AzureMonitorClient, 'track') as post:
163+
post.return_value = TrackResponse(
164+
items_received=1,
165+
items_accepted=1,
166+
errors=[],
167+
)
156168
result = self._base._transmit(self._envelopes_to_export)
157169
self.assertEqual(result, ExportResult.SUCCESS)
158170

0 commit comments

Comments
 (0)