Skip to content
This repository was archived by the owner on Sep 17, 2025. It is now read-only.

Commit 7738266

Browse files
authored
Add httplib integration (#95)
1 parent 40af615 commit 7738266

4 files changed

Lines changed: 284 additions & 1 deletion

File tree

opencensus/trace/config_integration.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717

1818
log = logging.getLogger(__name__)
1919

20-
SUPPORTED_INTEGRATIONS = ['mysql', 'postgresql', 'sqlalchemy', 'requests']
20+
SUPPORTED_INTEGRATIONS = ['httplib', 'mysql', 'postgresql', 'pymysql',
21+
'requests', 'sqlalchemy']
2122

2223
PATH_PREFIX = 'opencensus.trace.ext'
2324

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2017, OpenCensus Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Copyright 2017, OpenCensus Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import logging
16+
import sys
17+
18+
from opencensus.trace import attributes_helper
19+
from opencensus.trace import execution_context
20+
21+
PYTHON2 = sys.version_info.major == 2
22+
23+
if PYTHON2:
24+
import httplib
25+
else:
26+
import http.client as httplib
27+
28+
log = logging.getLogger(__name__)
29+
30+
MODULE_NAME = 'httplib'
31+
HTTPLIB_REQUEST_FUNC = 'request'
32+
HTTPLIB_RESPONSE_FUNC = 'getresponse'
33+
34+
HTTP_METHOD = attributes_helper.COMMON_ATTRIBUTES['HTTP_METHOD']
35+
HTTP_URL = attributes_helper.COMMON_ATTRIBUTES['HTTP_URL']
36+
HTTP_STATUS_CODE = attributes_helper.COMMON_ATTRIBUTES['HTTP_STATUS_CODE']
37+
38+
39+
def trace_integration():
40+
"""Wrap the httplib to trace."""
41+
log.info('Integrated module: {}'.format(MODULE_NAME))
42+
43+
# Wrap the httplib request function
44+
request_func = getattr(
45+
httplib.HTTPConnection, HTTPLIB_REQUEST_FUNC)
46+
wrapped_request = wrap_httplib_request(request_func)
47+
setattr(httplib.HTTPConnection, request_func.__name__, wrapped_request)
48+
49+
# Wrap the httplib response function
50+
response_func = getattr(
51+
httplib.HTTPConnection, HTTPLIB_RESPONSE_FUNC)
52+
wrapped_response = wrap_httplib_response(response_func)
53+
setattr(httplib.HTTPConnection, response_func.__name__, wrapped_response)
54+
55+
56+
def wrap_httplib_request(request_func):
57+
"""Wrap the httplib request function to trace. Create a new span and update
58+
and close the span in the response later.
59+
"""
60+
def call(self, method, url, *args, **kwargs):
61+
_tracer = execution_context.get_opencensus_tracer()
62+
_span = _tracer.start_span()
63+
_span.name = '[httplib]{}'.format(request_func.__name__)
64+
65+
# Add the request url to attributes
66+
_tracer.add_attribute_to_current_span(HTTP_URL, url)
67+
68+
# Add the request method to attributes
69+
_tracer.add_attribute_to_current_span(HTTP_METHOD, method)
70+
71+
# Store the current span id to thread local.
72+
execution_context.set_opencensus_attr(
73+
'httplib/current_span_id', _span.span_id)
74+
75+
return request_func(self, method, url, *args, **kwargs)
76+
77+
return call
78+
79+
80+
def wrap_httplib_response(response_func):
81+
"""Wrap the httplib response function to trace.
82+
83+
If there is a corresponding httplib request span, update and close it.
84+
If not, return the response.
85+
"""
86+
def call(self, *args, **kwargs):
87+
_tracer = execution_context.get_opencensus_tracer()
88+
current_span_id = execution_context.get_opencensus_attr(
89+
'httplib/current_span_id')
90+
91+
span = _tracer.current_span()
92+
93+
# No corresponding request span is found, request not traced.
94+
if span.span_id != current_span_id:
95+
return response_func(self, *args, **kwargs)
96+
97+
result = response_func(self, *args, **kwargs)
98+
99+
# Add the status code to attributes
100+
_tracer.add_attribute_to_current_span(
101+
HTTP_STATUS_CODE, str(result.status))
102+
103+
_tracer.end_span()
104+
return result
105+
106+
return call
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Copyright 2017, OpenCensus Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import unittest
16+
17+
import mock
18+
19+
from opencensus.trace.ext.httplib import trace
20+
21+
22+
class Test_httplib_trace(unittest.TestCase):
23+
24+
def test_trace_integration(self):
25+
mock_wrap_request = mock.Mock()
26+
mock_wrap_response = mock.Mock()
27+
mock_httplib = mock.Mock()
28+
29+
wrap_request_result = 'wrap request result'
30+
wrap_response_result = 'wrap response result'
31+
mock_wrap_request.return_value = wrap_request_result
32+
mock_wrap_response.return_value = wrap_response_result
33+
34+
mock_request_func = mock.Mock()
35+
mock_response_func = mock.Mock()
36+
mock_request_func.__name__ = 'request'
37+
mock_response_func.__name__ = 'getresponse'
38+
setattr(mock_httplib.HTTPConnection, 'request', mock_request_func)
39+
setattr(mock_httplib.HTTPConnection, 'getresponse', mock_response_func)
40+
41+
patch_wrap_request = mock.patch(
42+
'opencensus.trace.ext.httplib.trace.wrap_httplib_request',
43+
mock_wrap_request)
44+
patch_wrap_response = mock.patch(
45+
'opencensus.trace.ext.httplib.trace.wrap_httplib_response',
46+
mock_wrap_response)
47+
patch_httplib = mock.patch(
48+
'opencensus.trace.ext.httplib.trace.httplib', mock_httplib)
49+
50+
with patch_wrap_request, patch_wrap_response, patch_httplib:
51+
trace.trace_integration()
52+
53+
self.assertEqual(getattr(mock_httplib.HTTPConnection, 'request'),
54+
wrap_request_result)
55+
self.assertEqual(getattr(mock_httplib.HTTPConnection, 'getresponse'),
56+
wrap_response_result)
57+
58+
def test_wrap_httplib_request(self):
59+
mock_tracer = MockTracer()
60+
mock_request_func = mock.Mock()
61+
mock_request_func.__name__ = 'request'
62+
63+
patch = mock.patch(
64+
'opencensus.trace.ext.requests.trace.execution_context.'
65+
'get_opencensus_tracer',
66+
return_value=mock_tracer)
67+
68+
wrapped = trace.wrap_httplib_request(mock_request_func)
69+
70+
url = 'http://localhost:8080'
71+
method = 'GET'
72+
73+
with patch:
74+
wrapped(mock.Mock(), method, url)
75+
76+
expected_attributes = {
77+
'/http/url': url,
78+
'/http/method': method}
79+
expected_name = '[httplib]request'
80+
81+
self.assertEqual(expected_attributes,
82+
mock_tracer.span.attributes)
83+
self.assertEqual(expected_name, mock_tracer.span.name)
84+
85+
def test_wrap_httplib_response(self):
86+
mock_span = mock.Mock()
87+
span_id = '1234'
88+
mock_span.span_id = span_id
89+
mock_span.attributes = {}
90+
mock_tracer = MockTracer(mock_span)
91+
mock_response_func = mock.Mock()
92+
mock_result = mock.Mock()
93+
mock_result.status = '200'
94+
mock_response_func.return_value = mock_result
95+
96+
patch_tracer = mock.patch(
97+
'opencensus.trace.ext.requests.trace.execution_context.'
98+
'get_opencensus_tracer',
99+
return_value=mock_tracer)
100+
patch_attr = mock.patch('opencensus.trace.ext.httplib.trace.'
101+
'execution_context.get_opencensus_attr',
102+
return_value=span_id)
103+
104+
wrapped = trace.wrap_httplib_response(mock_response_func)
105+
106+
with patch_tracer, patch_attr:
107+
wrapped(mock.Mock())
108+
109+
expected_attributes = {
110+
'/http/status_code': '200'}
111+
112+
self.assertEqual(expected_attributes,
113+
mock_tracer.span.attributes)
114+
115+
def test_wrap_httplib_response_no_open_span(self):
116+
mock_span = mock.Mock()
117+
span_id = '1234'
118+
mock_span.span_id = span_id
119+
mock_span.attributes = {}
120+
mock_tracer = MockTracer(mock_span)
121+
mock_response_func = mock.Mock()
122+
mock_result = mock.Mock()
123+
mock_result.status = '200'
124+
mock_response_func.return_value = mock_result
125+
126+
patch_tracer = mock.patch(
127+
'opencensus.trace.ext.requests.trace.execution_context.'
128+
'get_opencensus_tracer',
129+
return_value=mock_tracer)
130+
patch_attr = mock.patch('opencensus.trace.ext.httplib.trace.'
131+
'execution_context.get_opencensus_attr',
132+
return_value='1111')
133+
134+
wrapped = trace.wrap_httplib_response(mock_response_func)
135+
136+
with patch_tracer, patch_attr:
137+
wrapped(mock.Mock())
138+
139+
# Attribute should be empty as there is no matching span
140+
expected_attributes = {}
141+
142+
self.assertEqual(expected_attributes,
143+
mock_tracer.span.attributes)
144+
145+
146+
class MockTracer(object):
147+
def __init__(self, span=None):
148+
self.span = span
149+
150+
def current_span(self):
151+
return self.span
152+
153+
def start_span(self):
154+
span = mock.Mock()
155+
span.attributes = {}
156+
self.span = span
157+
return span
158+
159+
def end_span(self):
160+
pass
161+
162+
def add_attribute_to_current_span(self, key, value):
163+
self.span.attributes[key] = value

0 commit comments

Comments
 (0)