Skip to content

Commit 0a9ce14

Browse files
thehesiodhaotianw465
authored andcommitted
httplib support (#19)
* initial httplib patch * fix unittests * fix warnings * strip url in requests as well * updates based on review * add some docs * refactor based on review * rename to getresponse to be clearer
1 parent fe3dd2f commit 0a9ce14

File tree

12 files changed

+297
-6
lines changed

12 files changed

+297
-6
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ dist
1414
*.egg-info
1515
.tox
1616
.python-version
17+
.pytest_cache
1718

1819
pip-selfcheck.json
1920

2021
.coverage*
21-
htmlcov
22+
htmlcov

aws_xray_sdk/core/patcher.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
'requests',
1111
'sqlite3',
1212
'mysql',
13+
'httplib',
1314
)
1415

1516
_PATCHED_MODULES = set()

aws_xray_sdk/ext/httplib/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .patch import patch
2+
3+
__all__ = ['patch']

aws_xray_sdk/ext/httplib/patch.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
from collections import namedtuple
2+
import sys
3+
import wrapt
4+
5+
from aws_xray_sdk.core import xray_recorder
6+
from aws_xray_sdk.core.models import http
7+
from aws_xray_sdk.ext.util import inject_trace_header, strip_url
8+
9+
import ssl
10+
11+
if sys.version_info >= (3, 0, 0):
12+
httplib_client_module = 'http.client'
13+
import http.client as httplib
14+
else:
15+
httplib_client_module = 'httplib'
16+
import httplib
17+
18+
19+
_XRAY_PROP = '_xray_prop'
20+
_XRay_Data = namedtuple('xray_data', ['method', 'host', 'url'])
21+
22+
23+
def http_response_processor(wrapped, instance, args, kwargs, return_value,
24+
exception, subsegment, stack):
25+
xray_data = getattr(instance, _XRAY_PROP)
26+
27+
subsegment.put_http_meta(http.METHOD, xray_data.method)
28+
subsegment.put_http_meta(http.URL, xray_data.url)
29+
30+
if return_value:
31+
subsegment.put_http_meta(http.STATUS, return_value.code)
32+
33+
# propagate to response object
34+
xray_data = _XRay_Data('READ', xray_data.host, xray_data.url)
35+
setattr(return_value, _XRAY_PROP, xray_data)
36+
37+
if exception:
38+
subsegment.add_exception(exception, stack)
39+
40+
41+
def _xray_traced_http_getresponse(wrapped, instance, args, kwargs):
42+
if kwargs.get('buffering', False):
43+
return wrapped(*args, **kwargs) # ignore py2 calls that fail as 'buffering` only exists in py2.
44+
45+
xray_data = getattr(instance, _XRAY_PROP)
46+
47+
return xray_recorder.record_subsegment(
48+
wrapped, instance, args, kwargs,
49+
name=strip_url(xray_data.url),
50+
namespace='remote',
51+
meta_processor=http_response_processor,
52+
)
53+
54+
55+
def http_request_processor(wrapped, instance, args, kwargs, return_value,
56+
exception, subsegment, stack):
57+
xray_data = getattr(instance, _XRAY_PROP)
58+
59+
# we don't delete the attr as we can have multiple reads
60+
subsegment.put_http_meta(http.METHOD, xray_data.method)
61+
subsegment.put_http_meta(http.URL, xray_data.url)
62+
63+
if exception:
64+
subsegment.add_exception(exception, stack)
65+
66+
67+
def _prep_request(wrapped, instance, args, kwargs):
68+
def decompose_args(method, url, body, headers, encode_chunked):
69+
inject_trace_header(headers, xray_recorder.current_subsegment())
70+
71+
# we have to check against sock because urllib3's HTTPSConnection inherit's from http.client.HTTPConnection
72+
scheme = 'https' if isinstance(instance.sock, ssl.SSLSocket) else 'http'
73+
xray_url = '{}://{}{}'.format(scheme, instance.host, url)
74+
xray_data = _XRay_Data(method, instance.host, xray_url)
75+
setattr(instance, _XRAY_PROP, xray_data)
76+
77+
# we add a segment here in case connect fails
78+
return xray_recorder.record_subsegment(
79+
wrapped, instance, args, kwargs,
80+
name=strip_url(xray_data.url),
81+
namespace='remote',
82+
meta_processor=http_request_processor
83+
)
84+
85+
return decompose_args(*args, **kwargs)
86+
87+
88+
def http_read_processor(wrapped, instance, args, kwargs, return_value,
89+
exception, subsegment, stack):
90+
xray_data = getattr(instance, _XRAY_PROP)
91+
92+
# we don't delete the attr as we can have multiple reads
93+
subsegment.put_http_meta(http.METHOD, xray_data.method)
94+
subsegment.put_http_meta(http.URL, xray_data.url)
95+
subsegment.put_http_meta(http.STATUS, instance.status)
96+
97+
if exception:
98+
subsegment.add_exception(exception, stack)
99+
100+
101+
def _xray_traced_http_client_read(wrapped, instance, args, kwargs):
102+
xray_data = getattr(instance, _XRAY_PROP)
103+
104+
return xray_recorder.record_subsegment(
105+
wrapped, instance, args, kwargs,
106+
name=strip_url(xray_data.url),
107+
namespace='remote',
108+
meta_processor=http_read_processor
109+
)
110+
111+
112+
def patch():
113+
wrapt.wrap_function_wrapper(
114+
httplib_client_module,
115+
'HTTPConnection._send_request',
116+
_prep_request
117+
)
118+
119+
wrapt.wrap_function_wrapper(
120+
httplib_client_module,
121+
'HTTPConnection.getresponse',
122+
_xray_traced_http_getresponse
123+
)
124+
125+
wrapt.wrap_function_wrapper(
126+
httplib_client_module,
127+
'HTTPResponse.read',
128+
_xray_traced_http_client_read
129+
)

aws_xray_sdk/ext/requests/patch.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from aws_xray_sdk.core import xray_recorder
44
from aws_xray_sdk.core.models import http
5-
from aws_xray_sdk.ext.util import inject_trace_header
5+
from aws_xray_sdk.ext.util import inject_trace_header, strip_url
66

77

88
def patch():
@@ -26,7 +26,7 @@ def _xray_traced_requests(wrapped, instance, args, kwargs):
2626

2727
return xray_recorder.record_subsegment(
2828
wrapped, instance, args, kwargs,
29-
name=url,
29+
name=strip_url(url),
3030
namespace='remote',
3131
meta_processor=requests_processor,
3232
)

aws_xray_sdk/ext/util.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,13 @@ def to_snake_case(name):
8989
s1 = first_cap_re.sub(r'\1_\2', name)
9090
# handle acronym words
9191
return all_cap_re.sub(r'\1_\2', s1).lower()
92+
93+
94+
# ? is not a valid entity, and we don't want things after the ? for the segment name
95+
def strip_url(url: str):
96+
"""
97+
Will generate a valid url string for use as a segment name
98+
:param url: url to strip
99+
:return: validated url string
100+
"""
101+
return url.partition('?')[0] if url else url

docs/aws_xray_sdk.ext.httplib.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
aws\_xray\_sdk\.ext\.httplib package
2+
=====================================
3+
4+
Submodules
5+
----------
6+
7+
aws\_xray\_sdk\.ext\.httplib\.patch module
8+
-------------------------------------------
9+
10+
.. automodule:: aws_xray_sdk.ext.httplib.patch
11+
:members:
12+
:undoc-members:
13+
:show-inheritance:
14+
15+
16+
Module contents
17+
---------------
18+
19+
.. automodule:: aws_xray_sdk.ext.httplib
20+
:members:
21+
:undoc-members:
22+
:show-inheritance:

docs/aws_xray_sdk.ext.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Subpackages
1414
aws_xray_sdk.ext.mysql
1515
aws_xray_sdk.ext.requests
1616
aws_xray_sdk.ext.sqlite3
17+
aws_xray_sdk.ext.httplib
1718

1819
Submodules
1920
----------

docs/thirdparty.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Third Party Library Support
66
Patching Supported Libraries
77
----------------------------
88

9-
The SDK supports aioboto3, aiobotocore, boto3, botocore, pynamodb, requests, sqlite3 and
9+
The SDK supports aioboto3, aiobotocore, boto3, botocore, pynamodb, requests, sqlite3, httplib and
1010
mysql-connector.
1111

1212
To patch, use code like the following in the main app::
@@ -35,6 +35,7 @@ The following modules are availble to patch::
3535
'requests',
3636
'sqlite3',
3737
'mysql',
38+
'httplib',
3839
)
3940

4041
Patching boto3 and botocore are equivalent since boto3 depends on botocore.
@@ -73,3 +74,10 @@ up the X-Ray SDK with an Async Context, bear in mind this requires Python 3.5+::
7374

7475
See :ref:`Configure Global Recorder <configurations>` for more information about
7576
configuring the ``xray_recorder``.
77+
78+
Patching httplib
79+
----------------
80+
81+
httplib is a low-level python module which is used by several third party modules, so
82+
by enabling patching to this module you can gain patching of many modules "for free."
83+
Some examples of modules that depend on httplib: requests and httplib2

tests/ext/httplib/__init__.py

Whitespace-only changes.

tests/ext/httplib/test_httplib.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import pytest
2+
import sys
3+
4+
from aws_xray_sdk.core import patch
5+
from aws_xray_sdk.core import xray_recorder
6+
from aws_xray_sdk.core.context import Context
7+
from aws_xray_sdk.ext.util import strip_url
8+
9+
if sys.version_info >= (3, 0, 0):
10+
import http.client as httplib
11+
from urllib.parse import urlparse
12+
else:
13+
import httplib
14+
from urlparse import urlparse
15+
16+
17+
patch(('httplib',))
18+
19+
# httpbin.org is created by the same author of requests to make testing http easy.
20+
BASE_URL = 'httpbin.org'
21+
22+
23+
@pytest.fixture(autouse=True)
24+
def construct_ctx():
25+
"""
26+
Clean up context storage on each test run and begin a segment
27+
so that later subsegment can be attached. After each test run
28+
it cleans up context storage again.
29+
"""
30+
xray_recorder.configure(service='test', sampling=False, context=Context())
31+
xray_recorder.clear_trace_entities()
32+
xray_recorder.begin_segment('name')
33+
yield
34+
xray_recorder.clear_trace_entities()
35+
36+
37+
def _do_req(url, method='GET'):
38+
parts = urlparse(url)
39+
host, _, port = parts.netloc.partition(':')
40+
if port == '':
41+
port = None
42+
conn = httplib.HTTPConnection(parts.netloc, port)
43+
44+
path = '{}?{}'.format(parts.path, parts.query) if parts.query else parts.path
45+
conn.request(method, path)
46+
resp = conn.getresponse()
47+
48+
49+
def test_ok():
50+
status_code = 200
51+
url = 'http://{}/status/{}?foo=bar&baz=foo'.format(BASE_URL, status_code)
52+
_do_req(url)
53+
subsegment = xray_recorder.current_segment().subsegments[1]
54+
assert subsegment.name == strip_url(url)
55+
56+
http_meta = subsegment.http
57+
assert http_meta['request']['url'] == url
58+
assert http_meta['request']['method'].upper() == 'GET'
59+
assert http_meta['response']['status'] == status_code
60+
61+
62+
def test_error():
63+
status_code = 400
64+
url = 'http://{}/status/{}'.format(BASE_URL, status_code)
65+
_do_req(url, 'POST')
66+
subsegment = xray_recorder.current_segment().subsegments[1]
67+
assert subsegment.name == url
68+
assert subsegment.error
69+
70+
http_meta = subsegment.http
71+
assert http_meta['request']['url'] == url
72+
assert http_meta['request']['method'].upper() == 'POST'
73+
assert http_meta['response']['status'] == status_code
74+
75+
76+
def test_throttle():
77+
status_code = 429
78+
url = 'http://{}/status/{}'.format(BASE_URL, status_code)
79+
_do_req(url, 'HEAD')
80+
subsegment = xray_recorder.current_segment().subsegments[1]
81+
assert subsegment.name == url
82+
assert subsegment.error
83+
assert subsegment.throttle
84+
85+
http_meta = subsegment.http
86+
assert http_meta['request']['url'] == url
87+
assert http_meta['request']['method'].upper() == 'HEAD'
88+
assert http_meta['response']['status'] == status_code
89+
90+
91+
def test_fault():
92+
status_code = 500
93+
url = 'http://{}/status/{}'.format(BASE_URL, status_code)
94+
_do_req(url, 'PUT')
95+
subsegment = xray_recorder.current_segment().subsegments[1]
96+
assert subsegment.name == url
97+
assert subsegment.fault
98+
99+
http_meta = subsegment.http
100+
assert http_meta['request']['url'] == url
101+
assert http_meta['request']['method'].upper() == 'PUT'
102+
assert http_meta['response']['status'] == status_code
103+
104+
105+
def test_invalid_url():
106+
try:
107+
_do_req('http://doesnt.exist')
108+
except Exception:
109+
# prevent uncatch exception from breaking test run
110+
pass
111+
subsegment = xray_recorder.current_segment().subsegments[0]
112+
assert subsegment.fault
113+
114+
exception = subsegment.cause['exceptions'][0]
115+
assert exception.type == 'gaierror'

tests/ext/requests/test_requests.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from aws_xray_sdk.core import patch
55
from aws_xray_sdk.core import xray_recorder
66
from aws_xray_sdk.core.context import Context
7+
from aws_xray_sdk.ext.util import strip_url
78

89

910
patch(('requests',))
@@ -28,10 +29,10 @@ def construct_ctx():
2829

2930
def test_ok():
3031
status_code = 200
31-
url = 'http://{}/status/{}'.format(BASE_URL, status_code)
32+
url = 'http://{}/status/{}?foo=bar'.format(BASE_URL, status_code)
3233
requests.get(url)
3334
subsegment = xray_recorder.current_segment().subsegments[0]
34-
assert subsegment.name == url
35+
assert subsegment.name == strip_url(url)
3536

3637
http_meta = subsegment.http
3738
assert http_meta['request']['url'] == url

0 commit comments

Comments
 (0)