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

Commit 7dd2a44

Browse files
geobeauc24t
authored andcommitted
Don't trace if it's a http request made by the exporter (#289)
Disable httplib tracing for http calls made by the exporter, which can cause an infinite loop. Fixes #135
1 parent 32ee2c8 commit 7dd2a44

11 files changed

Lines changed: 316 additions & 12 deletions

File tree

README.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,8 +418,29 @@ method, and status will be collected.
418418

419419
You can enable Requests integration by specifying ``'requests'`` to ``trace_integrations``.
420420

421+
It's possible to configure a list of URL you don't want traced. By default the request to exporter
422+
won't be traced. It's configurable by giving an array of hostname/port to the attribute
423+
``blacklist_hostnames`` in OpenCensus context's attributes:
424+
425+
.. code:: python
426+
427+
execution_context.set_opencensus_attr('blacklist_hostnames',['hostname:port'])
428+
429+
Only the hostname must be specified if only the hostname is specified in the URL request.
430+
421431
.. _Requests package: https://pypi.python.org/pypi/requests
422432

433+
Httplib
434+
~~~~~~~~
435+
436+
Census can trace HTTP requests made with the httplib library.
437+
438+
You can enable Requests integration by specifying ``'httplib'`` to ``trace_integrations``.
439+
440+
It's possible to configure a list of URL you don't want traced. See requests integration
441+
for more information. The only difference is that you need to specify hostname and port
442+
every time.
443+
423444
Google Cloud Client Libraries
424445
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
425446

opencensus/trace/ext/django/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
'ZIPKIN_EXPORTER_PORT': 9411,
3838
'ZIPKIN_EXPORTER_PROTOCOL': 'http',
3939
'OCAGENT_TRACE_EXPORTER_ENDPOINT': None,
40+
'BLACKLIST_HOSTNAMES': None,
4041
'TRANSPORT': 'opencensus.trace.exporters.transports.sync.SyncTransport',
4142
}
4243

opencensus/trace/ext/django/middleware.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
ZIPKIN_EXPORTER_PORT = 'ZIPKIN_EXPORTER_PORT'
4646
ZIPKIN_EXPORTER_PROTOCOL = 'ZIPKIN_EXPORTER_PROTOCOL'
4747
OCAGENT_TRACE_EXPORTER_ENDPOINT = 'OCAGENT_TRACE_EXPORTER_ENDPOINT'
48+
BLACKLIST_HOSTNAMES = 'BLACKLIST_HOSTNAMES'
4849

4950
log = logging.getLogger(__name__)
5051

@@ -161,6 +162,9 @@ def __init__(self, get_response=None):
161162
else:
162163
self.exporter = self._exporter(transport=transport)
163164

165+
self.blacklist_hostnames = settings.params.get(
166+
BLACKLIST_HOSTNAMES, None)
167+
164168
# Initialize the propagator
165169
self.propagator = self._propagator()
166170

@@ -179,6 +183,10 @@ def process_request(self, request):
179183
REQUEST_THREAD_LOCAL_KEY,
180184
request)
181185

186+
execution_context.set_opencensus_attr(
187+
'blacklist_hostnames',
188+
self.blacklist_hostnames)
189+
182190
try:
183191
# Start tracing this request
184192
span_context = self.propagator.from_headers(

opencensus/trace/ext/flask/flask_middleware.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
ZIPKIN_EXPORTER_PORT = 'ZIPKIN_EXPORTER_PORT'
4646
ZIPKIN_EXPORTER_PROTOCOL = 'ZIPKIN_EXPORTER_PROTOCOL'
4747
OCAGENT_TRACE_EXPORTER_ENDPOINT = 'OCAGENT_TRACE_EXPORTER_ENDPOINT'
48+
BLACKLIST_HOSTNAMES = 'BLACKLIST_HOSTNAMES'
4849

4950
log = logging.getLogger(__name__)
5051

@@ -165,6 +166,7 @@ def init_app(self, app):
165166
else:
166167
self.exporter = self.exporter(transport=transport)
167168

169+
self.blacklist_hostnames = params.get(BLACKLIST_HOSTNAMES, None)
168170
# Initialize the propagator
169171
if inspect.isclass(self.propagator):
170172
self.propagator = self.propagator()
@@ -203,6 +205,9 @@ def _before_request(self):
203205
tracer.add_attribute_to_current_span(
204206
HTTP_METHOD, flask.request.method)
205207
tracer.add_attribute_to_current_span(HTTP_URL, flask.request.url)
208+
execution_context.set_opencensus_attr(
209+
'blacklist_hostnames',
210+
self.blacklist_hostnames)
206211
except Exception: # pragma: NO COVER
207212
log.error('Failed to trace request', exc_info=True)
208213

opencensus/trace/ext/httplib/trace.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from opencensus.trace import attributes_helper
1919
from opencensus.trace import execution_context
2020
from opencensus.trace import span as span_module
21+
from opencensus.trace.ext import utils
2122

2223
PYTHON2 = sys.version_info.major == 2
2324

@@ -61,6 +62,12 @@ def wrap_httplib_request(request_func):
6162

6263
def call(self, method, url, body, headers, *args, **kwargs):
6364
_tracer = execution_context.get_opencensus_tracer()
65+
blacklist_hostnames = execution_context.get_opencensus_attr(
66+
'blacklist_hostnames')
67+
dest_url = '{}:{}'.format(self._dns_host, self.port)
68+
if utils.disable_tracing_hostname(dest_url, blacklist_hostnames):
69+
return request_func(self, method, url, body,
70+
headers, *args, **kwargs)
6471
_span = _tracer.start_span()
6572
_span.span_kind = span_module.SpanKind.CLIENT
6673
_span.name = '[httplib]{}'.format(request_func.__name__)
@@ -100,7 +107,7 @@ def call(self, *args, **kwargs):
100107
span = _tracer.current_span()
101108

102109
# No corresponding request span is found, request not traced.
103-
if span.span_id != current_span_id:
110+
if not span or span.span_id != current_span_id:
104111
return response_func(self, *args, **kwargs)
105112

106113
result = response_func(self, *args, **kwargs)

opencensus/trace/ext/requests/trace.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@
1515
import logging
1616
import requests
1717
import wrapt
18+
try:
19+
from urllib.parse import urlparse
20+
except ImportError:
21+
from urlparse import urlparse
1822

1923
from opencensus.trace import attributes_helper
2024
from opencensus.trace import execution_context
2125
from opencensus.trace import span as span_module
26+
from opencensus.trace.ext import utils
2227

2328
log = logging.getLogger(__name__)
2429

@@ -56,6 +61,16 @@ def trace_integration(tracer=None):
5661
def wrap_requests(requests_func):
5762
"""Wrap the requests function to trace it."""
5863
def call(url, *args, **kwargs):
64+
blacklist_hostnames = execution_context.get_opencensus_attr(
65+
'blacklist_hostnames')
66+
parsed_url = urlparse(url)
67+
if parsed_url.port is None:
68+
dest_url = parsed_url.hostname
69+
else:
70+
dest_url = '{}:{}'.format(parsed_url.hostname, parsed_url.port)
71+
if utils.disable_tracing_hostname(dest_url, blacklist_hostnames):
72+
return requests_func(url, *args, **kwargs)
73+
5974
_tracer = execution_context.get_opencensus_tracer()
6075
_span = _tracer.start_span()
6176
_span.name = '[requests]{}'.format(requests_func.__name__)
@@ -80,6 +95,17 @@ def wrap_session_request(wrapped, instance, args, kwargs):
8095
"""Wrap the session function to trace it."""
8196
method = kwargs.get('method') or args[0]
8297
url = kwargs.get('url') or args[1]
98+
99+
blacklist_hostnames = execution_context.get_opencensus_attr(
100+
'blacklist_hostnames')
101+
parsed_url = urlparse(url)
102+
if parsed_url.port is None:
103+
dest_url = parsed_url.hostname
104+
else:
105+
dest_url = '{}:{}'.format(parsed_url.hostname, parsed_url.port)
106+
if utils.disable_tracing_hostname(dest_url, blacklist_hostnames):
107+
return wrapped(*args, **kwargs)
108+
83109
_tracer = execution_context.get_opencensus_tracer()
84110
_span = _tracer.start_span()
85111

opencensus/trace/ext/utils.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import re
1616

17+
from opencensus.trace import execution_context
18+
1719
# By default the blacklist urls are not tracing, currently just include the
1820
# health check url. The paths are literal string matched instead of regular
1921
# expressions. Do not include the '/' at the beginning of the path.
@@ -63,3 +65,31 @@ def disable_tracing_url(url, blacklist_paths=None):
6365
return True
6466

6567
return False
68+
69+
70+
def disable_tracing_hostname(url, blacklist_hostnames=None):
71+
"""Disable tracing for the provided blacklist URLs, by default not tracing
72+
the exporter url.
73+
74+
If the url path starts with the blacklisted path, return True.
75+
76+
:type blacklist_hostnames: list
77+
:param blacklist_hostnames: URL that not tracing.
78+
79+
:rtype: bool
80+
:returns: True if not tracing, False if tracing.
81+
"""
82+
if blacklist_hostnames is None:
83+
# Exporter host_name are not traced by default
84+
_tracer = execution_context.get_opencensus_tracer()
85+
try:
86+
blacklist_hostnames = [
87+
'{}:{}'.format(
88+
_tracer.exporter.host_name,
89+
_tracer.exporter.port
90+
)
91+
]
92+
except(AttributeError):
93+
blacklist_hostnames = []
94+
95+
return url in blacklist_hostnames

tests/unit/trace/ext/django/test_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def test__set_default_configs(self):
8686
'ZIPKIN_EXPORTER_PORT': 9411,
8787
'ZIPKIN_EXPORTER_PROTOCOL': 'http',
8888
'OCAGENT_TRACE_EXPORTER_ENDPOINT': None,
89+
'BLACKLIST_HOSTNAMES': None,
8990
'TRANSPORT':
9091
'opencensus.trace.exporters.transports.sync.SyncTransport',
9192
}

tests/unit/trace/ext/httplib/test_httplib_trace.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,85 @@ def test_wrap_httplib_request(self):
9999
self.assertEqual(expected_attributes,
100100
mock_tracer.span.attributes)
101101
self.assertEqual(expected_name, mock_tracer.span.name)
102-
self.assertEqual(span_module.SpanKind.CLIENT, mock_tracer.span.span_kind)
102+
self.assertEqual(span_module.SpanKind.CLIENT, mock_tracer.span.span_kind)
103+
104+
def test_wrap_httplib_request_blacklist_ok(self):
105+
mock_span = mock.Mock()
106+
span_id = '1234'
107+
mock_span.span_id = span_id
108+
mock_tracer = MockTracer(mock_span)
109+
mock_request_func = mock.Mock()
110+
mock_request_func.__name__ = 'request'
111+
112+
patch_tracer = mock.patch(
113+
'opencensus.trace.ext.requests.trace.execution_context.'
114+
'get_opencensus_tracer',
115+
return_value=mock_tracer)
116+
patch_attr = mock.patch(
117+
'opencensus.trace.ext.requests.trace.execution_context.'
118+
'get_opencensus_attr',
119+
return_value=None)
120+
121+
wrapped = trace.wrap_httplib_request(mock_request_func)
122+
123+
mock_self = mock.Mock()
124+
method = 'GET'
125+
url = 'http://localhost:8080'
126+
body = None
127+
headers = {}
128+
129+
with patch_tracer, patch_attr:
130+
wrapped(mock_self, method, url, body, headers)
131+
132+
expected_attributes = {
133+
'http.url': url,
134+
'http.method': method}
135+
expected_name = '[httplib]request'
136+
137+
mock_request_func.assert_called_with(
138+
mock_self, method, url, body, {
139+
'traceparent': '00-123-456-01',
140+
}
141+
)
142+
143+
def test_wrap_httplib_request_blacklist_nok(self):
144+
mock_span = mock.Mock()
145+
span_id = '1234'
146+
mock_span.span_id = span_id
147+
mock_tracer = MockTracer(mock_span)
148+
mock_request_func = mock.Mock()
149+
mock_request_func.__name__ = 'request'
150+
151+
patch_tracer = mock.patch(
152+
'opencensus.trace.ext.requests.trace.execution_context.'
153+
'get_opencensus_tracer',
154+
return_value=mock_tracer)
155+
patch_attr = mock.patch(
156+
'opencensus.trace.ext.requests.trace.execution_context.'
157+
'get_opencensus_attr',
158+
return_value=['localhost:8080'])
159+
160+
wrapped = trace.wrap_httplib_request(mock_request_func)
161+
162+
mock_self = mock.Mock()
163+
mock_self._dns_host = 'localhost'
164+
mock_self.port = '8080'
165+
method = 'GET'
166+
url = 'http://{}:{}'.format(mock_self._dns_host, mock_self.port)
167+
body = None
168+
headers = {}
169+
170+
with patch_tracer, patch_attr:
171+
wrapped(mock_self, method, url, body, headers)
172+
173+
expected_attributes = {
174+
'http.url': url,
175+
'http.method': method}
176+
expected_name = '[httplib]request'
177+
178+
mock_request_func.assert_called_with(
179+
mock_self, method, url, body, {}
180+
)
103181

104182
def test_wrap_httplib_response(self):
105183
mock_span = mock.Mock()

0 commit comments

Comments
 (0)