Skip to content

Commit eba304c

Browse files
author
Emanuele Palazzetti
authored
[requests] add unpatch and double-patch protection (#404)
1 parent 652629a commit eba304c

File tree

4 files changed

+105
-87
lines changed

4 files changed

+105
-87
lines changed

ddtrace/contrib/requests/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@
3333

3434
with require_modules(required_modules) as missing_modules:
3535
if not missing_modules:
36-
from .patch import TracedSession, patch
37-
__all__ = ['TracedSession', 'patch']
36+
from .patch import TracedSession, patch, unpatch
37+
__all__ = ['TracedSession', 'patch', 'unpatch']

ddtrace/contrib/requests/patch.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,43 @@
1-
"""
2-
Tracing for the requests library.
3-
4-
https://github.com/kennethreitz/requests
5-
"""
6-
7-
# stdlib
81
import logging
92

10-
# 3p
11-
import requests
123
import wrapt
4+
import requests
135

14-
# project
156
import ddtrace
16-
from ddtrace.ext import http
7+
8+
from ...ext import http
179
from ...propagation.http import HTTPPropagator
10+
from ...util import unwrap as _u
1811

1912

2013
log = logging.getLogger(__name__)
2114

2215

2316
def patch():
24-
""" Monkeypatch the requests library to trace http calls. """
17+
"""Activate http calls tracing"""
18+
if getattr(requests, '__datadog_patch', False):
19+
return
20+
setattr(requests, '__datadog_patch', True)
21+
22+
wrapt.wrap_function_wrapper('requests', 'Session.__init__', _session_initializer)
2523
wrapt.wrap_function_wrapper('requests', 'Session.request', _traced_request_func)
2624

2725

26+
def unpatch():
27+
"""Disable traced sessions"""
28+
if not getattr(requests, '__datadog_patch', False):
29+
return
30+
setattr(requests, '__datadog_patch', False)
31+
32+
_u(requests.Session, '__init__')
33+
_u(requests.Session, 'request')
34+
35+
36+
def _session_initializer(func, instance, args, kwargs):
37+
"""Define settings when requests client is initialized"""
38+
func(*args, **kwargs)
39+
40+
2841
def _traced_request_func(func, instance, args, kwargs):
2942
""" traced_request is a tracing wrapper for requests' Session.request
3043
instance method.

tests/contrib/requests/test_requests.py

Lines changed: 64 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,125 @@
1+
import unittest
12

2-
# 3p
3-
from nose.tools import eq_, assert_raises
43
from requests import Session
4+
from nose.tools import eq_, assert_raises
55

6-
# project
7-
from ddtrace.contrib.requests import TracedSession
86
from ddtrace.ext import http, errors
9-
from tests.test_tracer import get_dummy_tracer
7+
from ddtrace.contrib.requests import patch, unpatch
8+
9+
from ...test_tracer import get_dummy_tracer
1010

1111
# socket name comes from https://english.stackexchange.com/a/44048
1212
SOCKET = 'httpbin.org'
1313
URL_200 = 'http://{}/status/200'.format(SOCKET)
1414
URL_500 = 'http://{}/status/500'.format(SOCKET)
1515

16-
class TestRequests(object):
1716

18-
@staticmethod
19-
def test_resource_path():
20-
tracer, session = get_traced_session()
21-
out = session.get(URL_200)
17+
class BaseRequestTestCase(unittest.TestCase):
18+
"""Create a traced Session, patching during the setUp and
19+
unpatching after the tearDown
20+
"""
21+
def setUp(self):
22+
patch()
23+
self.tracer = get_dummy_tracer()
24+
self.session = Session()
25+
setattr(self.session, 'datadog_tracer', self.tracer)
26+
27+
def tearDown(self):
28+
unpatch()
29+
30+
31+
class TestRequests(BaseRequestTestCase):
32+
def test_resource_path(self):
33+
out = self.session.get(URL_200)
2234
eq_(out.status_code, 200)
23-
spans = tracer.writer.pop()
35+
spans = self.tracer.writer.pop()
2436
eq_(len(spans), 1)
2537
s = spans[0]
2638
eq_(s.get_tag("http.url"), URL_200)
2739

28-
@staticmethod
29-
def test_tracer_disabled():
40+
def test_tracer_disabled(self):
3041
# ensure all valid combinations of args / kwargs work
31-
tracer, session = get_traced_session()
32-
tracer.enabled = False
33-
out = session.get(URL_200)
42+
self.tracer.enabled = False
43+
out = self.session.get(URL_200)
3444
eq_(out.status_code, 200)
35-
spans = tracer.writer.pop()
45+
spans = self.tracer.writer.pop()
3646
eq_(len(spans), 0)
3747

38-
@staticmethod
39-
def test_args_kwargs():
48+
def test_args_kwargs(self):
4049
# ensure all valid combinations of args / kwargs work
41-
tracer, session = get_traced_session()
4250
url = URL_200
4351
method = 'GET'
4452
inputs = [
4553
([], {'method': method, 'url': url}),
4654
([method], {'url': url}),
4755
([method, url], {}),
4856
]
49-
untraced = Session()
57+
5058
for args, kwargs in inputs:
51-
# ensure an untraced request works with these args
52-
out = untraced.request(*args, **kwargs)
53-
eq_(out.status_code, 200)
54-
out = session.request(*args, **kwargs)
59+
# ensure a traced request works with these args
60+
out = self.session.request(*args, **kwargs)
5561
eq_(out.status_code, 200)
5662
# validation
57-
spans = tracer.writer.pop()
63+
spans = self.tracer.writer.pop()
5864
eq_(len(spans), 1)
5965
s = spans[0]
6066
eq_(s.get_tag(http.METHOD), 'GET')
6167
eq_(s.get_tag(http.STATUS_CODE), '200')
6268

69+
def test_untraced_request(self):
70+
# ensure the unpatch removes tracing
71+
unpatch()
72+
untraced = Session()
73+
74+
out = untraced.get(URL_200)
75+
eq_(out.status_code, 200)
76+
# validation
77+
spans = self.tracer.writer.pop()
78+
eq_(len(spans), 0)
79+
80+
def test_double_patch(self):
81+
# ensure that double patch doesn't duplicate instrumentation
82+
patch()
83+
session = Session()
84+
setattr(session, 'datadog_tracer', self.tracer)
6385

64-
@staticmethod
65-
def test_200():
66-
tracer, session = get_traced_session()
6786
out = session.get(URL_200)
6887
eq_(out.status_code, 200)
88+
spans = self.tracer.writer.pop()
89+
eq_(len(spans), 1)
90+
91+
def test_200(self):
92+
out = self.session.get(URL_200)
93+
eq_(out.status_code, 200)
6994
# validation
70-
spans = tracer.writer.pop()
95+
spans = self.tracer.writer.pop()
7196
eq_(len(spans), 1)
7297
s = spans[0]
7398
eq_(s.get_tag(http.METHOD), 'GET')
7499
eq_(s.get_tag(http.STATUS_CODE), '200')
75100
eq_(s.error, 0)
76101
eq_(s.span_type, http.TYPE)
77102

78-
@staticmethod
79-
def test_post_500():
80-
tracer, session = get_traced_session()
81-
out = session.post(URL_500)
103+
def test_post_500(self):
104+
out = self.session.post(URL_500)
82105
# validation
83106
eq_(out.status_code, 500)
84-
spans = tracer.writer.pop()
107+
spans = self.tracer.writer.pop()
85108
eq_(len(spans), 1)
86109
s = spans[0]
87110
eq_(s.get_tag(http.METHOD), 'POST')
88111
eq_(s.get_tag(http.STATUS_CODE), '500')
89112
eq_(s.error, 1)
90113

91-
@staticmethod
92-
def test_non_existant_url():
93-
tracer, session = get_traced_session()
94-
114+
def test_non_existant_url(self):
95115
try:
96-
session.get('http://doesnotexist.google.com')
116+
self.session.get('http://doesnotexist.google.com')
97117
except Exception:
98118
pass
99119
else:
100120
assert 0, "expected error"
101121

102-
spans = tracer.writer.pop()
122+
spans = self.tracer.writer.pop()
103123
eq_(len(spans), 1)
104124
s = spans[0]
105125
eq_(s.get_tag(http.METHOD), 'GET')
@@ -109,23 +129,13 @@ def test_non_existant_url():
109129
assert "Traceback (most recent call last)" in s.get_tag(errors.STACK)
110130
assert "requests.exception" in s.get_tag(errors.TYPE)
111131

112-
113-
@staticmethod
114-
def test_500():
115-
tracer, session = get_traced_session()
116-
out = session.get(URL_500)
132+
def test_500(self):
133+
out = self.session.get(URL_500)
117134
eq_(out.status_code, 500)
118135

119-
spans = tracer.writer.pop()
136+
spans = self.tracer.writer.pop()
120137
eq_(len(spans), 1)
121138
s = spans[0]
122139
eq_(s.get_tag(http.METHOD), 'GET')
123140
eq_(s.get_tag(http.STATUS_CODE), '500')
124141
eq_(s.error, 1)
125-
126-
127-
def get_traced_session():
128-
tracer = get_dummy_tracer()
129-
session = TracedSession()
130-
setattr(session, 'datadog_tracer', tracer)
131-
return tracer, session
Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
2-
# 3p
3-
from nose.tools import eq_, assert_in, assert_not_in
41
from requests_mock import Adapter
2+
from nose.tools import eq_, assert_in, assert_not_in
53

6-
# project
7-
from .test_requests import get_traced_session
4+
from .test_requests import BaseRequestTestCase
85

9-
class TestRequestsDistributed(object):
106

7+
class TestRequestsDistributed(BaseRequestTestCase):
118
def headers_here(self, tracer, request, root_span):
129
# Use an additional matcher to query the request headers.
1310
# This is because the parent_id can only been known within such a callback,
@@ -29,19 +26,18 @@ def headers_not_here(self, tracer, request):
2926

3027
def test_propagation_true(self):
3128
adapter = Adapter()
32-
tracer, session = get_traced_session()
33-
session.mount('mock', adapter)
34-
session.distributed_tracing = True
29+
self.session.mount('mock', adapter)
30+
self.session.distributed_tracing = True
3531

36-
with tracer.trace('root') as root:
32+
with self.tracer.trace('root') as root:
3733
def matcher(request):
38-
return self.headers_here(tracer, request, root)
34+
return self.headers_here(self.tracer, request, root)
3935
adapter.register_uri('GET', 'mock://datadog/foo', additional_matcher=matcher, text='bar')
40-
resp = session.get('mock://datadog/foo')
36+
resp = self.session.get('mock://datadog/foo')
4137
eq_(200, resp.status_code)
4238
eq_('bar', resp.text)
4339

44-
spans = tracer.writer.spans
40+
spans = self.tracer.writer.spans
4541
root, req = spans
4642
eq_('root', root.name)
4743
eq_('requests.request', req.name)
@@ -50,14 +46,13 @@ def matcher(request):
5046

5147
def test_propagation_false(self):
5248
adapter = Adapter()
53-
tracer, session = get_traced_session()
54-
session.mount('mock', adapter)
55-
session.distributed_tracing = False
49+
self.session.mount('mock', adapter)
50+
self.session.distributed_tracing = False
5651

57-
with tracer.trace('root'):
52+
with self.tracer.trace('root'):
5853
def matcher(request):
59-
return self.headers_not_here(tracer, request)
54+
return self.headers_not_here(self.tracer, request)
6055
adapter.register_uri('GET', 'mock://datadog/foo', additional_matcher=matcher, text='bar')
61-
resp = session.get('mock://datadog/foo')
56+
resp = self.session.get('mock://datadog/foo')
6257
eq_(200, resp.status_code)
6358
eq_('bar', resp.text)

0 commit comments

Comments
 (0)