2021-03-09 20:06:23 +00:00
|
|
|
import asyncio
|
|
|
|
import os
|
2021-03-21 14:22:55 +00:00
|
|
|
import socket
|
2021-03-09 20:06:23 +00:00
|
|
|
import time
|
|
|
|
import typing
|
|
|
|
|
|
|
|
from tornado import testing, web
|
|
|
|
|
2021-03-24 10:48:25 +00:00
|
|
|
import sprockets_statsd.tornado
|
2021-03-09 20:06:23 +00:00
|
|
|
from tests import helpers
|
|
|
|
|
|
|
|
ParsedMetric = typing.Tuple[str, float, str]
|
|
|
|
|
|
|
|
|
2021-03-24 10:48:25 +00:00
|
|
|
class Handler(sprockets_statsd.tornado.RequestHandler, web.RequestHandler):
|
2021-03-09 20:06:23 +00:00
|
|
|
async def get(self):
|
|
|
|
with self.execution_timer('execution-timer'):
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
self.increase_counter('request-count')
|
|
|
|
self.write('true')
|
|
|
|
|
|
|
|
|
2021-03-24 10:48:25 +00:00
|
|
|
class Application(sprockets_statsd.tornado.Application, web.Application):
|
2021-03-09 20:06:23 +00:00
|
|
|
def __init__(self, **settings):
|
|
|
|
super().__init__([web.url('/', Handler)], **settings)
|
|
|
|
|
|
|
|
|
2021-03-21 21:45:23 +00:00
|
|
|
class AsyncTestCaseWithTimeout(testing.AsyncTestCase):
|
|
|
|
def run_coroutine(self, coro):
|
|
|
|
loop: asyncio.AbstractEventLoop = self.io_loop.asyncio_loop
|
|
|
|
try:
|
|
|
|
loop.run_until_complete(
|
|
|
|
asyncio.wait_for(coro,
|
|
|
|
timeout=testing.get_async_test_timeout()))
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
self.fail(f'coroutine {coro} took too long to complete')
|
|
|
|
|
|
|
|
|
|
|
|
class ApplicationTests(AsyncTestCaseWithTimeout):
|
2021-03-09 20:06:23 +00:00
|
|
|
def setUp(self):
|
|
|
|
super().setUp()
|
|
|
|
self._environ = {}
|
|
|
|
|
2021-03-24 10:30:26 +00:00
|
|
|
def tearDown(self):
|
|
|
|
super().tearDown()
|
|
|
|
for name, value in self._environ.items():
|
|
|
|
if value is not None:
|
|
|
|
os.environ[name] = value
|
|
|
|
else:
|
|
|
|
os.environ.pop(name, None)
|
|
|
|
|
2021-03-09 20:06:23 +00:00
|
|
|
def setenv(self, name, value):
|
|
|
|
self._environ.setdefault(name, os.environ.pop(name, None))
|
|
|
|
os.environ[name] = value
|
|
|
|
|
|
|
|
def unsetenv(self, name):
|
|
|
|
self._environ.setdefault(name, os.environ.pop(name, None))
|
|
|
|
|
|
|
|
def test_statsd_setting_defaults(self):
|
|
|
|
self.unsetenv('STATSD_HOST')
|
|
|
|
self.unsetenv('STATSD_PORT')
|
2021-03-24 02:01:02 +00:00
|
|
|
self.unsetenv('STATSD_PREFIX')
|
2021-03-21 21:45:23 +00:00
|
|
|
self.unsetenv('STATSD_PROTOCOL')
|
2021-03-09 20:06:23 +00:00
|
|
|
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application()
|
2021-03-09 20:06:23 +00:00
|
|
|
self.assertIn('statsd', app.settings)
|
|
|
|
self.assertIsNone(app.settings['statsd']['host'],
|
|
|
|
'default host value should be None')
|
|
|
|
self.assertEqual(8125, app.settings['statsd']['port'])
|
2021-03-23 11:08:34 +00:00
|
|
|
self.assertEqual(None, app.settings['statsd']['prefix'])
|
2021-03-21 21:45:23 +00:00
|
|
|
self.assertEqual('tcp', app.settings['statsd']['protocol'])
|
2021-03-09 20:06:23 +00:00
|
|
|
|
|
|
|
def test_that_statsd_settings_read_from_environment(self):
|
|
|
|
self.setenv('STATSD_HOST', 'statsd')
|
|
|
|
self.setenv('STATSD_PORT', '5218')
|
2021-03-24 02:01:02 +00:00
|
|
|
self.setenv('STATSD_PREFIX', 'my-service')
|
2021-03-21 21:45:23 +00:00
|
|
|
self.setenv('STATSD_PROTOCOL', 'udp')
|
2021-03-09 20:06:23 +00:00
|
|
|
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application()
|
2021-03-09 20:06:23 +00:00
|
|
|
self.assertIn('statsd', app.settings)
|
|
|
|
self.assertEqual('statsd', app.settings['statsd']['host'])
|
|
|
|
self.assertEqual(5218, app.settings['statsd']['port'])
|
2021-03-24 02:01:02 +00:00
|
|
|
self.assertEqual('my-service', app.settings['statsd']['prefix'])
|
2021-03-21 21:45:23 +00:00
|
|
|
self.assertEqual('udp', app.settings['statsd']['protocol'])
|
2021-03-09 20:06:23 +00:00
|
|
|
|
2021-03-23 11:08:34 +00:00
|
|
|
def test_prefix_when_only_service_is_set(self):
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application(service='blah')
|
2021-03-09 20:06:23 +00:00
|
|
|
self.assertIn('statsd', app.settings)
|
2021-03-23 11:08:34 +00:00
|
|
|
self.assertEqual(None, app.settings['statsd']['prefix'])
|
2021-03-09 20:06:23 +00:00
|
|
|
|
2021-03-23 11:08:34 +00:00
|
|
|
def test_prefix_when_only_environment_is_set(self):
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application(environment='whatever')
|
2021-03-09 20:06:23 +00:00
|
|
|
self.assertIn('statsd', app.settings)
|
2021-03-23 11:08:34 +00:00
|
|
|
self.assertEqual(None, app.settings['statsd']['prefix'])
|
2021-03-09 20:06:23 +00:00
|
|
|
|
2021-03-23 11:08:34 +00:00
|
|
|
def test_prefix_default_when_service_and_environment_are_set(self):
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application(environment='development',
|
|
|
|
service='my-service')
|
2021-03-09 20:06:23 +00:00
|
|
|
self.assertIn('statsd', app.settings)
|
2021-03-23 11:08:34 +00:00
|
|
|
self.assertEqual('applications.my-service.development',
|
2021-03-09 20:06:23 +00:00
|
|
|
app.settings['statsd']['prefix'])
|
|
|
|
|
|
|
|
def test_overridden_settings(self):
|
|
|
|
self.setenv('STATSD_HOST', 'statsd')
|
|
|
|
self.setenv('STATSD_PORT', '9999')
|
2021-03-24 02:01:02 +00:00
|
|
|
self.setenv('STATSD_PREFIX', 'service')
|
2021-03-21 21:45:23 +00:00
|
|
|
self.setenv('STATSD_PROTOCOL', 'tcp')
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application(
|
2021-03-21 21:45:23 +00:00
|
|
|
statsd={
|
|
|
|
'host': 'statsd.example.com',
|
|
|
|
'port': 5218,
|
|
|
|
'prefix': 'myapp',
|
|
|
|
'protocol': 'udp',
|
|
|
|
})
|
2021-03-09 20:06:23 +00:00
|
|
|
self.assertEqual('statsd.example.com', app.settings['statsd']['host'])
|
|
|
|
self.assertEqual(5218, app.settings['statsd']['port'])
|
|
|
|
self.assertEqual('myapp', app.settings['statsd']['prefix'])
|
2021-03-21 21:45:23 +00:00
|
|
|
self.assertEqual('udp', app.settings['statsd']['protocol'])
|
2021-03-09 20:06:23 +00:00
|
|
|
|
|
|
|
def test_that_starting_without_configuration_fails(self):
|
|
|
|
self.unsetenv('STATSD_HOST')
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application()
|
2021-03-09 20:06:23 +00:00
|
|
|
with self.assertRaises(RuntimeError):
|
2021-03-21 21:45:23 +00:00
|
|
|
self.run_coroutine(app.start_statsd())
|
2021-03-09 20:06:23 +00:00
|
|
|
|
2021-03-24 02:01:27 +00:00
|
|
|
def test_that_starting_without_prefix_fails_by_default(self):
|
|
|
|
self.unsetenv('STATSD_PREFIX')
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application(statsd={
|
2021-03-24 02:01:27 +00:00
|
|
|
'host': 'statsd.example.com',
|
|
|
|
'protocol': 'udp',
|
|
|
|
})
|
|
|
|
with self.assertRaises(RuntimeError) as cm:
|
|
|
|
self.run_coroutine(app.start_statsd())
|
|
|
|
self.assertTrue('prefix is not set' in str(cm.exception),
|
|
|
|
'Expected "prefix is not set" in exception message')
|
|
|
|
|
|
|
|
def test_starting_without_prefix_on_purpose(self):
|
|
|
|
self.unsetenv('STATSD_PREFIX')
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application(
|
2021-03-24 02:01:27 +00:00
|
|
|
statsd={
|
|
|
|
'allow_no_prefix': True,
|
|
|
|
'host': 'statsd.example.com',
|
|
|
|
'protocol': 'udp',
|
|
|
|
})
|
|
|
|
try:
|
|
|
|
self.run_coroutine(app.start_statsd())
|
|
|
|
finally:
|
|
|
|
self.run_coroutine(app.stop_statsd())
|
|
|
|
|
|
|
|
def test_starting_with_calculated_prefix(self):
|
|
|
|
self.unsetenv('STATSD_PREFIX')
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application(
|
2021-03-24 02:01:27 +00:00
|
|
|
environment='development',
|
|
|
|
service='my-service',
|
|
|
|
statsd={
|
|
|
|
'host': 'statsd.example.com',
|
|
|
|
'protocol': 'udp',
|
|
|
|
})
|
|
|
|
try:
|
|
|
|
self.run_coroutine(app.start_statsd())
|
|
|
|
self.assertEqual('applications.my-service.development',
|
|
|
|
app.settings['statsd']['prefix'])
|
|
|
|
finally:
|
|
|
|
self.run_coroutine(app.stop_statsd())
|
|
|
|
|
2021-03-09 20:06:23 +00:00
|
|
|
def test_starting_twice(self):
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application(statsd={
|
2021-03-09 20:06:23 +00:00
|
|
|
'host': 'localhost',
|
|
|
|
'port': '8125',
|
2021-03-24 02:01:27 +00:00
|
|
|
'prefix': 'my-service',
|
2021-03-09 20:06:23 +00:00
|
|
|
})
|
|
|
|
try:
|
2021-03-21 21:45:23 +00:00
|
|
|
self.run_coroutine(app.start_statsd())
|
2021-03-23 11:43:20 +00:00
|
|
|
connector = app.statsd_connector
|
2021-03-09 20:06:23 +00:00
|
|
|
self.assertIsNotNone(connector, 'statsd.Connector not created')
|
|
|
|
|
2021-03-21 21:45:23 +00:00
|
|
|
self.run_coroutine(app.start_statsd())
|
2021-03-23 11:43:20 +00:00
|
|
|
self.assertIs(app.statsd_connector, connector,
|
2021-03-09 20:06:23 +00:00
|
|
|
'statsd.Connector should not be recreated')
|
|
|
|
finally:
|
2021-03-21 21:45:23 +00:00
|
|
|
self.run_coroutine(app.stop_statsd())
|
2021-03-09 20:06:23 +00:00
|
|
|
|
|
|
|
def test_stopping_without_starting(self):
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application(statsd={
|
2021-03-09 20:06:23 +00:00
|
|
|
'host': 'localhost',
|
|
|
|
'port': '8125',
|
|
|
|
})
|
2021-03-21 21:45:23 +00:00
|
|
|
self.run_coroutine(app.stop_statsd())
|
2021-03-09 20:06:23 +00:00
|
|
|
|
2021-03-11 12:31:24 +00:00
|
|
|
def test_optional_parameters(self):
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application(
|
2021-03-11 12:31:24 +00:00
|
|
|
statsd={
|
|
|
|
'host': 'localhost',
|
|
|
|
'port': '8125',
|
2021-03-24 02:01:27 +00:00
|
|
|
'prefix': 'my-service',
|
2021-03-11 12:31:24 +00:00
|
|
|
'reconnect_sleep': 0.5,
|
|
|
|
'wait_timeout': 0.25,
|
|
|
|
})
|
2021-03-21 21:45:23 +00:00
|
|
|
self.run_coroutine(app.start_statsd())
|
2021-03-11 12:31:24 +00:00
|
|
|
|
2021-03-23 11:43:20 +00:00
|
|
|
processor = app.statsd_connector.processor
|
2021-03-11 12:31:24 +00:00
|
|
|
self.assertEqual(0.5, processor._reconnect_sleep)
|
|
|
|
self.assertEqual(0.25, processor._wait_timeout)
|
2021-03-21 21:45:23 +00:00
|
|
|
self.run_coroutine(app.stop_statsd())
|
|
|
|
|
|
|
|
def test_starting_with_invalid_protocol(self):
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application(statsd={
|
2021-03-21 21:45:23 +00:00
|
|
|
'host': 'localhost',
|
2021-03-24 02:01:27 +00:00
|
|
|
'prefix': 'my-service',
|
2021-03-21 21:45:23 +00:00
|
|
|
'protocol': 'unknown'
|
|
|
|
})
|
|
|
|
with self.assertRaises(RuntimeError):
|
|
|
|
self.run_coroutine(app.start_statsd())
|
|
|
|
|
|
|
|
def test_that_protocol_strings_are_translated(self):
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application(statsd={
|
2021-03-21 21:45:23 +00:00
|
|
|
'host': 'localhost',
|
2021-03-24 02:01:27 +00:00
|
|
|
'prefix': 'my-service',
|
2021-03-21 21:45:23 +00:00
|
|
|
'protocol': 'tcp',
|
|
|
|
})
|
|
|
|
self.run_coroutine(app.start_statsd())
|
|
|
|
self.assertEqual(socket.IPPROTO_TCP,
|
|
|
|
app.statsd_connector.processor._ip_protocol)
|
|
|
|
self.run_coroutine(app.stop_statsd())
|
|
|
|
|
2021-03-24 10:48:25 +00:00
|
|
|
app = sprockets_statsd.tornado.Application(statsd={
|
2021-03-21 21:45:23 +00:00
|
|
|
'host': 'localhost',
|
2021-03-24 02:01:27 +00:00
|
|
|
'prefix': 'my-service',
|
2021-03-21 21:45:23 +00:00
|
|
|
'protocol': 'udp',
|
|
|
|
})
|
|
|
|
self.run_coroutine(app.start_statsd())
|
|
|
|
self.assertEqual(socket.IPPROTO_UDP,
|
|
|
|
app.statsd_connector.processor._ip_protocol)
|
|
|
|
self.run_coroutine(app.stop_statsd())
|
2021-03-11 12:31:24 +00:00
|
|
|
|
2021-03-09 20:06:23 +00:00
|
|
|
|
2021-03-21 21:45:23 +00:00
|
|
|
class RequestHandlerTests(AsyncTestCaseWithTimeout, testing.AsyncHTTPTestCase):
|
2021-03-09 20:06:23 +00:00
|
|
|
def setUp(self):
|
|
|
|
super().setUp()
|
2021-03-21 14:22:55 +00:00
|
|
|
self.statsd_server = helpers.StatsdServer(socket.IPPROTO_TCP)
|
2021-03-09 20:06:23 +00:00
|
|
|
self.io_loop.spawn_callback(self.statsd_server.run)
|
2021-03-21 21:45:23 +00:00
|
|
|
self.run_coroutine(self.statsd_server.wait_running())
|
2021-03-09 20:06:23 +00:00
|
|
|
|
2021-03-23 11:08:34 +00:00
|
|
|
self.app.settings['statsd'].update({
|
|
|
|
'host': self.statsd_server.host,
|
|
|
|
'port': self.statsd_server.port,
|
|
|
|
'prefix': 'applications.service',
|
2021-03-21 21:45:23 +00:00
|
|
|
'protocol': 'tcp',
|
2021-03-23 11:08:34 +00:00
|
|
|
})
|
2021-03-21 21:45:23 +00:00
|
|
|
self.run_coroutine(self.app.start_statsd())
|
2021-03-09 20:06:23 +00:00
|
|
|
|
|
|
|
def tearDown(self):
|
2021-03-21 21:45:23 +00:00
|
|
|
self.run_coroutine(self.app.stop_statsd())
|
2021-03-09 20:06:23 +00:00
|
|
|
self.statsd_server.close()
|
2021-03-21 21:45:23 +00:00
|
|
|
self.run_coroutine(self.statsd_server.wait_closed())
|
2021-03-09 20:06:23 +00:00
|
|
|
super().tearDown()
|
|
|
|
|
|
|
|
def get_app(self):
|
|
|
|
self.app = Application()
|
|
|
|
return self.app
|
|
|
|
|
|
|
|
def wait_for_metrics(self, metric_count=3):
|
|
|
|
timeout_remaining = testing.get_async_test_timeout()
|
|
|
|
for _ in range(metric_count):
|
|
|
|
start = time.time()
|
2021-03-19 22:19:03 +00:00
|
|
|
try:
|
|
|
|
self.io_loop.run_sync(
|
|
|
|
self.statsd_server.message_received.acquire,
|
|
|
|
timeout=timeout_remaining)
|
|
|
|
except TimeoutError:
|
|
|
|
self.fail()
|
2021-03-09 20:06:23 +00:00
|
|
|
timeout_remaining -= (time.time() - start)
|
|
|
|
|
|
|
|
def parse_metric(self, metric_line: bytes) -> ParsedMetric:
|
|
|
|
metric_line = metric_line.decode()
|
|
|
|
path, _, rest = metric_line.partition(':')
|
|
|
|
value, _, type_code = rest.partition('|')
|
|
|
|
try:
|
|
|
|
value = float(value)
|
|
|
|
except ValueError:
|
|
|
|
self.fail(f'value of {path} is not a number: value={value!r}')
|
|
|
|
return path, value, type_code
|
|
|
|
|
|
|
|
def find_metric(self, needle: str) -> ParsedMetric:
|
|
|
|
needle = needle.encode()
|
|
|
|
for line in self.statsd_server.metrics:
|
|
|
|
if needle in line:
|
|
|
|
return self.parse_metric(line)
|
|
|
|
self.fail(f'failed to find metric containing {needle!r}')
|
|
|
|
|
|
|
|
def test_the_request_metric_is_sent_last(self):
|
|
|
|
rsp = self.fetch('/')
|
|
|
|
self.assertEqual(200, rsp.code)
|
|
|
|
self.wait_for_metrics()
|
|
|
|
|
|
|
|
path, _, type_code = self.find_metric('Handler.GET.200')
|
2021-03-23 11:08:34 +00:00
|
|
|
self.assertEqual(path, 'applications.service.timers.Handler.GET.200')
|
2021-03-09 20:06:23 +00:00
|
|
|
self.assertEqual('ms', type_code)
|
|
|
|
|
|
|
|
def test_execution_timer(self):
|
|
|
|
rsp = self.fetch('/')
|
|
|
|
self.assertEqual(200, rsp.code)
|
|
|
|
self.wait_for_metrics()
|
|
|
|
|
|
|
|
path, _, type_code = self.find_metric('execution-timer')
|
2021-03-23 11:08:34 +00:00
|
|
|
self.assertEqual('applications.service.timers.execution-timer', path)
|
2021-03-09 20:06:23 +00:00
|
|
|
self.assertEqual('ms', type_code)
|
|
|
|
|
|
|
|
def test_counter(self):
|
|
|
|
rsp = self.fetch('/')
|
|
|
|
self.assertEqual(200, rsp.code)
|
|
|
|
self.wait_for_metrics()
|
|
|
|
|
|
|
|
path, value, type_code = self.find_metric('request-count')
|
2021-03-23 11:08:34 +00:00
|
|
|
self.assertEqual('applications.service.counters.request-count', path)
|
2021-03-09 20:06:23 +00:00
|
|
|
self.assertEqual(1.0, value)
|
|
|
|
self.assertEqual('c', type_code)
|
|
|
|
|
|
|
|
def test_handling_request_without_statsd_configured(self):
|
|
|
|
self.io_loop.run_sync(self.app.stop_statsd)
|
|
|
|
|
|
|
|
rsp = self.fetch('/')
|
|
|
|
self.assertEqual(200, rsp.code)
|
|
|
|
|
|
|
|
def test_handling_request_without_prefix(self):
|
|
|
|
self.app.settings['statsd']['prefix'] = ''
|
|
|
|
|
|
|
|
rsp = self.fetch('/')
|
|
|
|
self.assertEqual(200, rsp.code)
|
|
|
|
self.wait_for_metrics()
|
|
|
|
|
|
|
|
path, _, _ = self.find_metric('Handler.GET.200')
|
|
|
|
self.assertEqual('timers.Handler.GET.200', path)
|
|
|
|
|
|
|
|
path, _, _ = self.find_metric('execution-timer')
|
|
|
|
self.assertEqual('timers.execution-timer', path)
|
|
|
|
|
|
|
|
path, _, _ = self.find_metric('request-count')
|
|
|
|
self.assertEqual('counters.request-count', path)
|