Skip to content

Commit 44b592c

Browse files
requests: Improvements for requests integration (#573)
Adding a TestBase class which wraps a tracer provider that is configured with a memory span exporter. This class inherits from unitest.TestCase, hence other test classes can inherit from it to get access to the underlying memory span exporter and tracer provider. Adding a mock propagator that could be used for testing propagation in different packages. It was implemented in the opentracing-shim and this commit moves it to a generic place. Adding disable_session(), which can be used to disable the instrumentation on a single requests' session object.
1 parent 7d11cf1 commit 44b592c

File tree

10 files changed

+236
-141
lines changed

10 files changed

+236
-141
lines changed

dev-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ sphinx-autodoc-typehints~=1.10.2
99
pytest!=5.2.3
1010
pytest-cov>=2.8
1111
readme-renderer~=24.0
12+
httpretty~=1.0

ext/opentelemetry-ext-http-requests/README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ OpenTelemetry requests Integration
77
:target: https://pypi.org/project/opentelemetry-ext-http-requests/
88

99
This library allows tracing HTTP requests made by the
10-
`requests <https://requests.kennethreitz.org/en/master/>`_ library.
10+
`requests <https://requests.readthedocs.io/en/master/>`_ library.
1111

1212
Installation
1313
------------

ext/opentelemetry-ext-http-requests/setup.cfg

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,10 @@ install_requires =
4343
opentelemetry-api == 0.7.dev0
4444
requests ~= 2.0
4545

46+
[options.extras_require]
47+
test =
48+
opentelemetry-test == 0.7.dev0
49+
httpretty ~= 1.0
50+
4651
[options.packages.find]
4752
where = src

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"""
4343

4444
import functools
45+
import types
4546
from urllib.parse import urlparse
4647

4748
from requests.sessions import Session
@@ -96,9 +97,6 @@ def instrumented_request(self, method, url, *args, **kwargs):
9697
span.set_attribute("http.method", method.upper())
9798
span.set_attribute("http.url", url)
9899

99-
# TODO: Propagate the trace context via headers once we have a way
100-
# to access propagators.
101-
102100
headers = kwargs.setdefault("headers", {})
103101
propagators.inject(type(headers).__setitem__, headers)
104102
result = wrapped(self, method, url, *args, **kwargs) # *** PROCEED
@@ -129,3 +127,10 @@ def disable():
129127
if getattr(Session.request, "opentelemetry_ext_requests_applied", False):
130128
original = Session.request.__wrapped__ # pylint:disable=no-member
131129
Session.request = original
130+
131+
132+
def disable_session(session):
133+
"""Disables instrumentation on the session object."""
134+
if getattr(session.request, "opentelemetry_ext_requests_applied", False):
135+
original = session.request.__wrapped__ # pylint:disable=no-member
136+
session.request = types.MethodType(original, session)

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

Lines changed: 101 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -13,95 +13,50 @@
1313
# limitations under the License.
1414

1515
import sys
16-
import unittest
17-
from unittest import mock
1816

19-
import pkg_resources
17+
import httpretty
2018
import requests
2119
import urllib3
2220

2321
import opentelemetry.ext.http_requests
24-
from opentelemetry import trace
22+
from opentelemetry import context, propagators, trace
23+
from opentelemetry.test.mock_httptextformat import MockHTTPTextFormat
24+
from opentelemetry.test.test_base import TestBase
2525

2626

27-
class TestRequestsIntegration(unittest.TestCase):
27+
class TestRequestsIntegration(TestBase):
28+
URL = "http://httpbin.org/status/200"
2829

29-
# TODO: Copy & paste from test_wsgi_middleware
3030
def setUp(self):
31-
self.span_attrs = {}
32-
self.tracer_provider = trace.DefaultTracerProvider()
33-
self.tracer = trace.DefaultTracer()
34-
self.get_tracer_patcher = mock.patch.object(
35-
self.tracer_provider,
36-
"get_tracer",
37-
autospec=True,
38-
spec_set=True,
39-
return_value=self.tracer,
40-
)
41-
self.get_tracer = self.get_tracer_patcher.start()
42-
self.span_context_manager = mock.MagicMock()
43-
self.span = mock.create_autospec(trace.Span, spec_set=True)
44-
self.span.get_context.return_value = trace.INVALID_SPAN_CONTEXT
45-
self.span_context_manager.__enter__.return_value = self.span
46-
47-
def setspanattr(key, value):
48-
self.assertIsInstance(key, str)
49-
self.span_attrs[key] = value
50-
51-
self.span.set_attribute = setspanattr
52-
self.start_span_patcher = mock.patch.object(
53-
self.tracer,
54-
"start_as_current_span",
55-
autospec=True,
56-
spec_set=True,
57-
return_value=self.span_context_manager,
58-
)
59-
60-
mocked_response = requests.models.Response()
61-
mocked_response.status_code = 200
62-
mocked_response.reason = "Roger that!"
63-
self.send_patcher = mock.patch.object(
64-
requests.Session,
65-
"send",
66-
autospec=True,
67-
spec_set=True,
68-
return_value=mocked_response,
69-
)
70-
71-
self.start_as_current_span = self.start_span_patcher.start()
72-
self.send = self.send_patcher.start()
73-
31+
super().setUp()
7432
opentelemetry.ext.http_requests.enable(self.tracer_provider)
75-
distver = pkg_resources.get_distribution(
76-
"opentelemetry-ext-http-requests"
77-
).version
78-
self.get_tracer.assert_called_with(
79-
opentelemetry.ext.http_requests.__name__, distver
33+
httpretty.enable()
34+
httpretty.register_uri(
35+
httpretty.GET, self.URL, body="Hello!",
8036
)
8137

8238
def tearDown(self):
39+
super().tearDown()
8340
opentelemetry.ext.http_requests.disable()
84-
self.get_tracer_patcher.stop()
85-
self.send_patcher.stop()
86-
self.start_span_patcher.stop()
41+
httpretty.disable()
8742

8843
def test_basic(self):
89-
url = "https://www.example.org/foo/bar?x=y#top"
90-
requests.get(url=url)
91-
self.assertEqual(1, len(self.send.call_args_list))
92-
self.tracer.start_as_current_span.assert_called_with( # pylint:disable=no-member
93-
"/foo/bar", kind=trace.SpanKind.CLIENT
94-
)
95-
self.span_context_manager.__enter__.assert_called_with()
96-
self.span_context_manager.__exit__.assert_called_with(None, None, None)
44+
result = requests.get(self.URL)
45+
self.assertEqual(result.text, "Hello!")
46+
span_list = self.memory_exporter.get_finished_spans()
47+
self.assertEqual(len(span_list), 1)
48+
span = span_list[0]
49+
self.assertIs(span.kind, trace.SpanKind.CLIENT)
50+
self.assertEqual(span.name, "/status/200")
51+
9752
self.assertEqual(
98-
self.span_attrs,
53+
span.attributes,
9954
{
10055
"component": "http",
10156
"http.method": "GET",
102-
"http.url": url,
57+
"http.url": self.URL,
10358
"http.status_code": 200,
104-
"http.status_text": "Roger that!",
59+
"http.status_text": "OK",
10560
},
10661
)
10762

@@ -114,20 +69,84 @@ def test_invalid_url(self):
11469
exception_type = ValueError
11570

11671
with self.assertRaises(exception_type):
117-
requests.post(url=url)
118-
call_args = (
119-
self.tracer.start_as_current_span.call_args # pylint:disable=no-member
120-
)
121-
self.assertTrue(
122-
call_args[0][0].startswith("<Unparsable URL"),
123-
msg=self.tracer.start_as_current_span.call_args, # pylint:disable=no-member
124-
)
125-
self.span_context_manager.__enter__.assert_called_with()
126-
exitspan = self.span_context_manager.__exit__
127-
self.assertEqual(1, len(exitspan.call_args_list))
128-
self.assertIs(exception_type, exitspan.call_args[0][0])
129-
self.assertIsInstance(exitspan.call_args[0][1], exception_type)
72+
requests.post(url)
73+
74+
span_list = self.memory_exporter.get_finished_spans()
75+
self.assertEqual(len(span_list), 1)
76+
span = span_list[0]
77+
78+
self.assertTrue(span.name.startswith("<Unparsable URL"))
13079
self.assertEqual(
131-
self.span_attrs,
80+
span.attributes,
13281
{"component": "http", "http.method": "POST", "http.url": url},
13382
)
83+
84+
def test_disable(self):
85+
opentelemetry.ext.http_requests.disable()
86+
result = requests.get(self.URL)
87+
self.assertEqual(result.text, "Hello!")
88+
span_list = self.memory_exporter.get_finished_spans()
89+
self.assertEqual(len(span_list), 0)
90+
91+
def test_disable_session(self):
92+
session1 = requests.Session()
93+
opentelemetry.ext.http_requests.disable_session(session1)
94+
95+
result = session1.get(self.URL)
96+
self.assertEqual(result.text, "Hello!")
97+
span_list = self.memory_exporter.get_finished_spans()
98+
self.assertEqual(len(span_list), 0)
99+
100+
# Test that other sessions as well as global requests is still
101+
# instrumented
102+
session2 = requests.Session()
103+
result = session2.get(self.URL)
104+
self.assertEqual(result.text, "Hello!")
105+
span_list = self.memory_exporter.get_finished_spans()
106+
self.assertEqual(len(span_list), 1)
107+
108+
self.memory_exporter.clear()
109+
110+
result = requests.get(self.URL)
111+
self.assertEqual(result.text, "Hello!")
112+
span_list = self.memory_exporter.get_finished_spans()
113+
self.assertEqual(len(span_list), 1)
114+
115+
def test_suppress_instrumentation(self):
116+
token = context.attach(
117+
context.set_value("suppress_instrumentation", True)
118+
)
119+
try:
120+
result = requests.get(self.URL)
121+
self.assertEqual(result.text, "Hello!")
122+
finally:
123+
context.detach(token)
124+
125+
span_list = self.memory_exporter.get_finished_spans()
126+
self.assertEqual(len(span_list), 0)
127+
128+
def test_distributed_context(self):
129+
previous_propagator = propagators.get_global_httptextformat()
130+
try:
131+
propagators.set_global_httptextformat(MockHTTPTextFormat())
132+
result = requests.get(self.URL)
133+
self.assertEqual(result.text, "Hello!")
134+
135+
span_list = self.memory_exporter.get_finished_spans()
136+
self.assertEqual(len(span_list), 1)
137+
span = span_list[0]
138+
139+
headers = dict(httpretty.last_request().headers)
140+
self.assertIn(MockHTTPTextFormat.TRACE_ID_KEY, headers)
141+
self.assertEqual(
142+
str(span.get_context().trace_id),
143+
headers[MockHTTPTextFormat.TRACE_ID_KEY],
144+
)
145+
self.assertIn(MockHTTPTextFormat.SPAN_ID_KEY, headers)
146+
self.assertEqual(
147+
str(span.get_context().span_id),
148+
headers[MockHTTPTextFormat.SPAN_ID_KEY],
149+
)
150+
151+
finally:
152+
propagators.set_global_httptextformat(previous_propagator)

ext/opentelemetry-ext-opentracing-shim/setup.cfg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,9 @@ install_requires =
4444
opentracing ~= 2.0
4545
opentelemetry-api == 0.7.dev0
4646

47+
[options.extras_require]
48+
test =
49+
opentelemetry-test == 0.7.dev0
50+
4751
[options.packages.find]
4852
where = src

ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,15 @@
1616
# pylint:disable=no-member
1717

1818
import time
19-
import typing
2019
from unittest import TestCase
2120

2221
import opentracing
2322

2423
import opentelemetry.ext.opentracing_shim as opentracingshim
2524
from opentelemetry import propagators, trace
26-
from opentelemetry.context import Context
2725
from opentelemetry.ext.opentracing_shim import util
2826
from opentelemetry.sdk.trace import TracerProvider
29-
from opentelemetry.trace.propagation import (
30-
get_span_from_context,
31-
set_span_in_context,
32-
)
33-
from opentelemetry.trace.propagation.httptextformat import (
34-
Getter,
35-
HTTPTextFormat,
36-
HTTPTextFormatT,
37-
Setter,
38-
)
27+
from opentelemetry.test.mock_httptextformat import MockHTTPTextFormat
3928

4029

4130
class TestShim(TestCase):
@@ -542,46 +531,3 @@ def test_extract_binary(self):
542531
# Verify exception for non supported binary format.
543532
with self.assertRaises(opentracing.UnsupportedFormatException):
544533
self.shim.extract(opentracing.Format.BINARY, bytearray())
545-
546-
547-
class MockHTTPTextFormat(HTTPTextFormat):
548-
"""Mock propagator for testing purposes."""
549-
550-
TRACE_ID_KEY = "mock-traceid"
551-
SPAN_ID_KEY = "mock-spanid"
552-
553-
def extract(
554-
self,
555-
get_from_carrier: Getter[HTTPTextFormatT],
556-
carrier: HTTPTextFormatT,
557-
context: typing.Optional[Context] = None,
558-
) -> Context:
559-
trace_id_list = get_from_carrier(carrier, self.TRACE_ID_KEY)
560-
span_id_list = get_from_carrier(carrier, self.SPAN_ID_KEY)
561-
562-
if not trace_id_list or not span_id_list:
563-
return set_span_in_context(trace.INVALID_SPAN)
564-
565-
return set_span_in_context(
566-
trace.DefaultSpan(
567-
trace.SpanContext(
568-
trace_id=int(trace_id_list[0]),
569-
span_id=int(span_id_list[0]),
570-
is_remote=True,
571-
)
572-
)
573-
)
574-
575-
def inject(
576-
self,
577-
set_in_carrier: Setter[HTTPTextFormatT],
578-
carrier: HTTPTextFormatT,
579-
context: typing.Optional[Context] = None,
580-
) -> None:
581-
span = get_span_from_context(context)
582-
set_in_carrier(
583-
carrier, self.TRACE_ID_KEY, str(span.get_context().trace_id)
584-
)
585-
set_in_carrier(
586-
carrier, self.SPAN_ID_KEY, str(span.get_context().span_id)
587-
)

0 commit comments

Comments
 (0)