Skip to content

Commit b2e45f7

Browse files
authored
Merge branch 'master' into majorgreys/instrumentor-celery
2 parents 2d559b9 + 91f656f commit b2e45f7

File tree

2 files changed

+123
-22
lines changed

2 files changed

+123
-22
lines changed

ext/opentelemetry-ext-requests/src/opentelemetry/ext/requests/__init__.py

+45-14
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,14 @@
4545
import types
4646
from urllib.parse import urlparse
4747

48+
from requests import Timeout, URLRequired
49+
from requests.exceptions import InvalidSchema, InvalidURL, MissingSchema
4850
from requests.sessions import Session
4951

5052
from opentelemetry import context, propagators, trace
5153
from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor
5254
from opentelemetry.ext.requests.version import __version__
53-
from opentelemetry.trace import SpanKind, get_tracer
55+
from opentelemetry.trace import SpanKind
5456
from opentelemetry.trace.status import Status, StatusCanonicalCode
5557

5658

@@ -80,31 +82,49 @@ def instrumented_request(self, method, url, *args, **kwargs):
8082
# https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#http-client
8183
try:
8284
parsed_url = urlparse(url)
85+
span_name = parsed_url.path
8386
except ValueError as exc: # Invalid URL
84-
path = "<Unparsable URL: {}>".format(exc)
85-
else:
86-
if parsed_url is None:
87-
path = "<URL parses to None>"
88-
path = parsed_url.path
87+
span_name = "<Unparsable URL: {}>".format(exc)
8988

90-
with tracer.start_as_current_span(path, kind=SpanKind.CLIENT) as span:
89+
exception = None
90+
91+
with tracer.start_as_current_span(
92+
span_name, kind=SpanKind.CLIENT
93+
) as span:
9194
span.set_attribute("component", "http")
9295
span.set_attribute("http.method", method.upper())
9396
span.set_attribute("http.url", url)
9497

9598
headers = kwargs.setdefault("headers", {})
9699
propagators.inject(type(headers).__setitem__, headers)
97-
result = wrapped(self, method, url, *args, **kwargs) # *** PROCEED
98100

99-
span.set_attribute("http.status_code", result.status_code)
100-
span.set_attribute("http.status_text", result.reason)
101-
span.set_status(
102-
Status(_http_status_to_canonical_code(result.status_code))
103-
)
101+
try:
102+
result = wrapped(
103+
self, method, url, *args, **kwargs
104+
) # *** PROCEED
105+
except Exception as exc: # pylint: disable=W0703
106+
exception = exc
107+
result = getattr(exc, "response", None)
108+
109+
if exception is not None:
110+
span.set_status(
111+
Status(_exception_to_canonical_code(exception))
112+
)
113+
114+
if result is not None:
115+
span.set_attribute("http.status_code", result.status_code)
116+
span.set_attribute("http.status_text", result.reason)
117+
span.set_status(
118+
Status(_http_status_to_canonical_code(result.status_code))
119+
)
120+
104121
if span_callback is not None:
105122
span_callback(span, result)
106123

107-
return result
124+
if exception is not None:
125+
raise exception.with_traceback(exception.__traceback__)
126+
127+
return result
108128

109129
instrumented_request.opentelemetry_ext_requests_applied = True
110130

@@ -157,6 +177,17 @@ def _http_status_to_canonical_code(code: int, allow_redirect: bool = True):
157177
return StatusCanonicalCode.UNKNOWN
158178

159179

180+
def _exception_to_canonical_code(exc: Exception) -> StatusCanonicalCode:
181+
if isinstance(
182+
exc,
183+
(InvalidURL, InvalidSchema, MissingSchema, URLRequired, ValueError),
184+
):
185+
return StatusCanonicalCode.INVALID_ARGUMENT
186+
if isinstance(exc, Timeout):
187+
return StatusCanonicalCode.DEADLINE_EXCEEDED
188+
return StatusCanonicalCode.UNKNOWN
189+
190+
160191
class RequestsInstrumentor(BaseInstrumentor):
161192
"""An instrumentor for requests
162193
See `BaseInstrumentor`

ext/opentelemetry-ext-requests/tests/test_requests_integration.py

+78-8
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import sys
15+
from unittest import mock
1616

1717
import httpretty
1818
import requests
19-
import urllib3
2019

2120
import opentelemetry.ext.requests
2221
from opentelemetry import context, propagators, trace
2322
from opentelemetry.ext.requests import RequestsInstrumentor
2423
from opentelemetry.sdk import resources
2524
from opentelemetry.test.mock_httptextformat import MockHTTPTextFormat
2625
from opentelemetry.test.test_base import TestBase
26+
from opentelemetry.trace.status import StatusCanonicalCode
2727

2828

2929
class TestRequestsIntegration(TestBase):
@@ -92,13 +92,8 @@ def test_not_foundbasic(self):
9292

9393
def test_invalid_url(self):
9494
url = "http://[::1/nope"
95-
exception_type = requests.exceptions.InvalidURL
96-
if sys.version_info[:2] < (3, 5) and tuple(
97-
map(int, urllib3.__version__.split(".")[:2])
98-
) < (1, 25):
99-
exception_type = ValueError
10095

101-
with self.assertRaises(exception_type):
96+
with self.assertRaises(ValueError):
10297
requests.post(url)
10398

10499
span_list = self.memory_exporter.get_finished_spans()
@@ -110,6 +105,9 @@ def test_invalid_url(self):
110105
span.attributes,
111106
{"component": "http", "http.method": "POST", "http.url": url},
112107
)
108+
self.assertEqual(
109+
span.status.canonical_code, StatusCanonicalCode.INVALID_ARGUMENT
110+
)
113111

114112
def test_uninstrument(self):
115113
RequestsInstrumentor().uninstrument()
@@ -229,3 +227,75 @@ def test_custom_tracer_provider(self):
229227
span = span_list[0]
230228

231229
self.assertIs(span.resource, resource)
230+
231+
@mock.patch("requests.Session.send", side_effect=requests.RequestException)
232+
def test_requests_exception_without_response(self, *_, **__):
233+
234+
with self.assertRaises(requests.RequestException):
235+
requests.get(self.URL)
236+
237+
span_list = self.memory_exporter.get_finished_spans()
238+
self.assertEqual(len(span_list), 1)
239+
span = span_list[0]
240+
self.assertEqual(
241+
span.attributes,
242+
{"component": "http", "http.method": "GET", "http.url": self.URL},
243+
)
244+
self.assertEqual(
245+
span.status.canonical_code, StatusCanonicalCode.UNKNOWN
246+
)
247+
248+
mocked_response = requests.Response()
249+
mocked_response.status_code = 500
250+
mocked_response.reason = "Internal Server Error"
251+
252+
@mock.patch(
253+
"requests.Session.send",
254+
side_effect=requests.RequestException(response=mocked_response),
255+
)
256+
def test_requests_exception_with_response(self, *_, **__):
257+
258+
with self.assertRaises(requests.RequestException):
259+
requests.get(self.URL)
260+
261+
span_list = self.memory_exporter.get_finished_spans()
262+
self.assertEqual(len(span_list), 1)
263+
span = span_list[0]
264+
self.assertEqual(
265+
span.attributes,
266+
{
267+
"component": "http",
268+
"http.method": "GET",
269+
"http.url": self.URL,
270+
"http.status_code": 500,
271+
"http.status_text": "Internal Server Error",
272+
},
273+
)
274+
self.assertEqual(
275+
span.status.canonical_code, StatusCanonicalCode.INTERNAL
276+
)
277+
278+
@mock.patch("requests.Session.send", side_effect=Exception)
279+
def test_requests_basic_exception(self, *_, **__):
280+
281+
with self.assertRaises(Exception):
282+
requests.get(self.URL)
283+
284+
span_list = self.memory_exporter.get_finished_spans()
285+
self.assertEqual(len(span_list), 1)
286+
self.assertEqual(
287+
span_list[0].status.canonical_code, StatusCanonicalCode.UNKNOWN
288+
)
289+
290+
@mock.patch("requests.Session.send", side_effect=requests.Timeout)
291+
def test_requests_timeout_exception(self, *_, **__):
292+
293+
with self.assertRaises(Exception):
294+
requests.get(self.URL)
295+
296+
span_list = self.memory_exporter.get_finished_spans()
297+
self.assertEqual(len(span_list), 1)
298+
self.assertEqual(
299+
span_list[0].status.canonical_code,
300+
StatusCanonicalCode.DEADLINE_EXCEEDED,
301+
)

0 commit comments

Comments
 (0)