Skip to content

Commit 639decb

Browse files
authored
Release 0.20.2 (#804)
* [tests] limit grpcio version to >=1.8.0,<1.18.0 * [tools] Add confirmation to 'rake pypi:release' task (#791) * [tools] Add confirmation to 'rake pypi:release' task * check if we are on tagged version instead * [core] Call HTTPResponse.read() before HTTPConnection.close() (#800) * [core] Call HTTPResponse.read() before HTTPConnection.close() * engrish * add reason and msg as well * use API.Response in integration tests * limit version of grpc * ensure we have regression test * move API.Response to just Response * result_traces/services => traces/services_response * fix logging error message tests * fix test for python 2.7 * Update ddtrace/writer.py Co-Authored-By: brettlangdon <[email protected]> * Update ddtrace/api.py Co-Authored-By: brettlangdon <[email protected]> * fix integration tests * Bump version to 0.20.2 (#803)
1 parent c509eac commit 639decb

File tree

7 files changed

+155
-59
lines changed

7 files changed

+155
-59
lines changed

Rakefile

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,31 @@ end
117117
namespace :pypi do
118118
RELEASE_DIR = '/tmp/dd-trace-py-release'
119119

120+
def get_version()
121+
return `python setup.py --version`.strip
122+
end
123+
124+
def get_branch()
125+
return `git name-rev --name-only HEAD`.strip
126+
end
127+
128+
task :confirm do
129+
ddtrace_version = get_version
130+
131+
if get_branch.downcase != 'tags/v#{ddtrace_version}'
132+
print "WARNING: Expected current commit to be tagged as 'tags/v#{ddtrace_version}, instead we are on '#{get_branch}', proceed anyways [y|N]? "
133+
$stdout.flush
134+
135+
abort if $stdin.gets.to_s.strip.downcase != 'y'
136+
end
137+
138+
puts "WARNING: This task will build and release a new wheel to https://pypi.org/project/ddtrace/, this action cannot be undone"
139+
print " To proceed please type the version '#{ddtrace_version}': "
140+
$stdout.flush
141+
142+
abort if $stdin.gets.to_s.strip.downcase != ddtrace_version
143+
end
144+
120145
task :clean do
121146
FileUtils.rm_rf(RELEASE_DIR)
122147
end
@@ -130,7 +155,7 @@ namespace :pypi do
130155
sh "python setup.py -q sdist -d #{RELEASE_DIR}"
131156
end
132157

133-
task :release => [:install, :build] do
158+
task :release => [:confirm, :install, :build] do
134159
builds = Dir.entries(RELEASE_DIR).reject {|f| f == '.' || f == '..'}
135160
if builds.length == 0
136161
fail "no build found in #{RELEASE_DIR}"

ddtrace/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from .tracer import Tracer
55
from .settings import config
66

7-
__version__ = '0.20.1'
7+
__version__ = '0.20.2'
88

99
# a global tracer instance with integration settings
1010
tracer = Tracer()

ddtrace/api.py

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,71 @@
2727
'fallback': None}}
2828

2929

30-
def _parse_response_json(response):
30+
class Response(object):
3131
"""
32-
Parse the content of a response object, and return the right type,
33-
can be a string if the output was plain text, or a dictionnary if
34-
the output was a JSON.
32+
Custom API Response object to represent a response from calling the API.
33+
34+
We do this to ensure we know expected properties will exist, and so we
35+
can call `resp.read()` and load the body once into an instance before we
36+
close the HTTPConnection used for the request.
3537
"""
36-
if hasattr(response, 'read'):
37-
body = response.read()
38+
__slots__ = ['status', 'body', 'reason', 'msg']
39+
40+
def __init__(self, status=None, body=None, reason=None, msg=None):
41+
self.status = status
42+
self.body = body
43+
self.reason = reason
44+
self.msg = msg
45+
46+
@classmethod
47+
def from_http_response(cls, resp):
48+
"""
49+
Build a ``Response`` from the provided ``HTTPResponse`` object.
50+
51+
This function will call `.read()` to consume the body of the ``HTTPResponse`` object.
52+
53+
:param resp: ``HTTPResponse`` object to build the ``Response`` from
54+
:type resp: ``HTTPResponse``
55+
:rtype: ``Response``
56+
:returns: A new ``Response``
57+
"""
58+
return cls(
59+
status=resp.status,
60+
body=resp.read(),
61+
reason=getattr(resp, 'reason', None),
62+
msg=getattr(resp, 'msg', None),
63+
)
64+
65+
def get_json(self):
66+
"""Helper to parse the body of this request as JSON"""
3867
try:
68+
body = self.body
69+
if not body:
70+
log.debug('Empty reply from Datadog Agent, %r', self)
71+
return
72+
3973
if not isinstance(body, str) and hasattr(body, 'decode'):
4074
body = body.decode('utf-8')
75+
4176
if hasattr(body, 'startswith') and body.startswith('OK'):
4277
# This typically happens when using a priority-sampling enabled
4378
# library with an outdated agent. It still works, but priority sampling
4479
# will probably send too many traces, so the next step is to upgrade agent.
45-
log.debug("'OK' is not a valid JSON, please make sure trace-agent is up to date")
80+
log.debug('Cannot parse Datadog Agent response, please make sure your Datadog Agent is up to date')
4681
return
47-
content = loads(body)
48-
return content
82+
83+
return loads(body)
4984
except (ValueError, TypeError) as err:
50-
log.debug("unable to load JSON '%s': %s" % (body, err))
85+
log.debug('Unable to parse Datadog Agent JSON response: %s %r', err, body)
86+
87+
def __repr__(self):
88+
return '{0}(status={1!r}, body={2!r}, reason={3!r}, msg={4!r})'.format(
89+
self.__class__.__name__,
90+
self.status,
91+
self.body,
92+
self.reason,
93+
self.msg,
94+
)
5195

5296

5397
class API(object):
@@ -142,6 +186,11 @@ def _put(self, endpoint, data, count=0):
142186
headers[TRACE_COUNT_HEADER] = str(count)
143187

144188
conn.request("PUT", endpoint, data, headers)
145-
return get_connection_response(conn)
189+
190+
# Parse the HTTPResponse into an API.Response
191+
# DEV: This will call `resp.read()` which must happen before the `conn.close()` below,
192+
# if we call `.close()` then all future `.read()` calls will return `b''`
193+
resp = get_connection_response(conn)
194+
return Response.from_http_response(resp)
146195
finally:
147196
conn.close()

ddtrace/writer.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88

99
from ddtrace import api
1010

11-
from .api import _parse_response_json
12-
1311
log = logging.getLogger(__name__)
1412

1513

@@ -126,8 +124,8 @@ def _on_shutdown(self):
126124
time.sleep(0.05)
127125

128126
def _target(self):
129-
result_traces = None
130-
result_services = None
127+
traces_response = None
128+
services_response = None
131129

132130
while True:
133131
traces = self._trace_queue.pop()
@@ -141,43 +139,50 @@ def _target(self):
141139
if traces:
142140
# If we have data, let's try to send it.
143141
try:
144-
result_traces = self.api.send_traces(traces)
142+
traces_response = self.api.send_traces(traces)
145143
except Exception as err:
146144
log.error("cannot send spans to {1}:{2}: {0}".format(err, self.api.hostname, self.api.port))
147145

148146
services = self._service_queue.pop()
149147
if services:
150148
try:
151-
result_services = self.api.send_services(services)
149+
services_response = self.api.send_services(services)
152150
except Exception as err:
153151
log.error("cannot send services to {1}:{2}: {0}".format(err, self.api.hostname, self.api.port))
154152

155153
if self._trace_queue.closed() and self._trace_queue.size() == 0:
156154
# no traces and the queue is closed. our work is done
157155
return
158156

159-
if self._priority_sampler:
160-
result_traces_json = _parse_response_json(result_traces)
157+
if self._priority_sampler and traces_response:
158+
result_traces_json = traces_response.get_json()
161159
if result_traces_json and 'rate_by_service' in result_traces_json:
162160
self._priority_sampler.set_sample_rate_by_service(result_traces_json['rate_by_service'])
163161

164-
self._log_error_status(result_traces, "traces")
165-
result_traces = None
166-
self._log_error_status(result_services, "services")
167-
result_services = None
162+
self._log_error_status(traces_response, "traces")
163+
traces_response = None
164+
self._log_error_status(services_response, "services")
165+
services_response = None
168166

169167
time.sleep(1) # replace with a blocking pop.
170168

171-
def _log_error_status(self, result, result_name):
169+
def _log_error_status(self, response, response_name):
170+
if not isinstance(response, api.Response):
171+
return
172+
172173
log_level = log.debug
173-
if result and getattr(result, "status", None) >= 400:
174+
if response.status >= 400:
174175
now = time.time()
175176
if now > self._last_error_ts + LOG_ERR_INTERVAL:
176177
log_level = log.error
177178
self._last_error_ts = now
178-
log_level("failed_to_send %s to Agent: HTTP error status %s, reason %s, message %s", result_name,
179-
getattr(result, "status", None), getattr(result, "reason", None),
180-
getattr(result, "msg", None))
179+
log_level(
180+
'failed_to_send %s to Datadog Agent: HTTP error status %s, reason %s, message %s',
181+
response_name,
182+
response.status,
183+
response.reason,
184+
response.msg,
185+
)
181186

182187
def _apply_filters(self, traces):
183188
"""

tests/test_api.py

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import mock
2+
import re
23
import warnings
34

45
from unittest import TestCase
56
from nose.tools import eq_, ok_
67

78
from tests.test_tracer import get_dummy_tracer
8-
from ddtrace.api import _parse_response_json, API
9+
from ddtrace.api import API, Response
910
from ddtrace.compat import iteritems, httplib
1011

1112

1213
class ResponseMock:
13-
def __init__(self, content):
14+
def __init__(self, content, status=200):
15+
self.status = status
1416
self.content = content
1517

1618
def read(self):
@@ -34,36 +36,49 @@ def test_parse_response_json(self, log):
3436
tracer.debug_logging = True
3537

3638
test_cases = {
37-
'OK': {'js': None, 'log': "please make sure trace-agent is up to date"},
38-
'OK\n': {'js': None, 'log': "please make sure trace-agent is up to date"},
39-
'error:unsupported-endpoint': {'js': None, 'log': "unable to load JSON 'error:unsupported-endpoint'"},
40-
42: {'js': None, 'log': "unable to load JSON '42'"}, # int as key to trigger TypeError
41-
'{}': {'js': {}},
42-
'[]': {'js': []},
43-
'{"rate_by_service": {"service:,env:":0.5, "service:mcnulty,env:test":0.9, "service:postgres,env:test":0.6}}': { # noqa
44-
'js': {
45-
'rate_by_service': {
39+
'OK': dict(
40+
js=None,
41+
log='Cannot parse Datadog Agent response, please make sure your Datadog Agent is up to date',
42+
),
43+
'OK\n': dict(
44+
js=None,
45+
log='Cannot parse Datadog Agent response, please make sure your Datadog Agent is up to date',
46+
),
47+
'error:unsupported-endpoint': dict(
48+
js=None,
49+
log='Unable to parse Datadog Agent JSON response: .*? \'error:unsupported-endpoint\'',
50+
),
51+
42: dict( # int as key to trigger TypeError
52+
js=None,
53+
log='Unable to parse Datadog Agent JSON response: .*? 42',
54+
),
55+
'{}': dict(js={}),
56+
'[]': dict(js=[]),
57+
58+
# Priority sampling "rate_by_service" response
59+
('{"rate_by_service": '
60+
'{"service:,env:":0.5, "service:mcnulty,env:test":0.9, "service:postgres,env:test":0.6}}'): dict(
61+
js=dict(
62+
rate_by_service={
4663
'service:,env:': 0.5,
4764
'service:mcnulty,env:test': 0.9,
4865
'service:postgres,env:test': 0.6,
4966
},
50-
},
51-
},
52-
' [4,2,1] ': {'js': [4, 2, 1]},
67+
),
68+
),
69+
' [4,2,1] ': dict(js=[4, 2, 1]),
5370
}
5471

5572
for k, v in iteritems(test_cases):
56-
r = ResponseMock(k)
57-
js = _parse_response_json(r)
73+
log.reset_mock()
74+
75+
r = Response.from_http_response(ResponseMock(k))
76+
js = r.get_json()
5877
eq_(v['js'], js)
5978
if 'log' in v:
60-
ok_(
61-
1 <= len(log.call_args_list),
62-
'not enough elements in call_args_list: {}'.format(log.call_args_list),
63-
)
64-
print(log.call_args_list)
65-
args = log.call_args_list[-1][0][0]
66-
ok_(v['log'] in args, 'unable to find {} in {}'.format(v['log'], args))
79+
log.assert_called_once()
80+
msg = log.call_args[0][0] % log.call_args[0][1:]
81+
ok_(re.match(v['log'], msg), msg)
6782

6883
@mock.patch('ddtrace.compat.httplib.HTTPConnection')
6984
def test_put_connection_close(self, HTTPConnection):

tests/test_integration.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from unittest import TestCase, skip, skipUnless
1010
from nose.tools import eq_, ok_
1111

12-
from ddtrace.api import API
12+
from ddtrace.api import API, Response
1313
from ddtrace.ext import http
1414
from ddtrace.filters import FilterRequestsOnUrl
1515
from ddtrace.constants import FILTERS_KEY
@@ -41,7 +41,7 @@ class FlawedAPI(API):
4141
def _put(self, endpoint, data, count=0):
4242
conn = httplib.HTTPConnection(self.hostname, self.port)
4343
conn.request('HEAD', endpoint, data, self._headers)
44-
return conn.getresponse()
44+
return Response.from_http_response(conn.getresponse())
4545

4646

4747
@skipUnless(
@@ -188,7 +188,7 @@ def test_worker_http_error_logging(self):
188188

189189
logged_errors = log_handler.messages['error']
190190
eq_(len(logged_errors), 1)
191-
ok_('failed_to_send traces to Agent: HTTP error status 400, reason Bad Request, message Content-Type:'
191+
ok_('failed_to_send traces to Datadog Agent: HTTP error status 400, reason Bad Request, message Content-Type:'
192192
in logged_errors[0])
193193

194194
def test_worker_filter_request(self):
@@ -488,8 +488,8 @@ def setUp(self):
488488
"""
489489
# create a new API object to test the transport using synchronous calls
490490
self.tracer = get_dummy_tracer()
491-
self.api_json = API('localhost', 8126, encoder=JSONEncoder())
492-
self.api_msgpack = API('localhost', 8126, encoder=MsgpackEncoder())
491+
self.api_json = API('localhost', 8126, encoder=JSONEncoder(), priority_sampling=True)
492+
self.api_msgpack = API('localhost', 8126, encoder=MsgpackEncoder(), priority_sampling=True)
493493

494494
def test_send_single_trace(self):
495495
# register a single trace with a span and send them to the trace agent
@@ -506,11 +506,13 @@ def test_send_single_trace(self):
506506
response = self.api_json.send_traces(traces)
507507
ok_(response)
508508
eq_(response.status, 200)
509+
eq_(response.get_json(), dict(rate_by_service={'service:,env:': 1}))
509510

510511
# test Msgpack encoder
511512
response = self.api_msgpack.send_traces(traces)
512513
ok_(response)
513514
eq_(response.status, 200)
515+
eq_(response.get_json(), dict(rate_by_service={'service:,env:': 1}))
514516

515517

516518
@skipUnless(

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ deps =
202202
gevent11: gevent>=1.1,<1.2
203203
gevent12: gevent>=1.2,<1.3
204204
gevent13: gevent>=1.3,<1.4
205-
grpc: grpcio>=1.8.0
205+
grpc: grpcio>=1.8.0,<1.18.0
206206
grpc: googleapis-common-protos
207207
jinja27: jinja2>=2.7,<2.8
208208
jinja28: jinja2>=2.8,<2.9

0 commit comments

Comments
 (0)