import asyncio import itertools import socket import unittest from unittest import mock from tornado import iostream, testing, web from sprockets.mixins.metrics import statsd from sprockets.mixins.metrics.testing import FakeStatsdServer import examples.statsd class CounterBumper(statsd.StatsdMixin, web.RequestHandler): async def get(self, counter, value): with self.execution_timer(*counter.split('.')): await asyncio.sleep(float(value)) self.set_status(204) self.finish() def post(self, counter, amount): self.increase_counter(*counter.split('.'), amount=int(amount)) self.set_status(204) class DefaultStatusCode(statsd.StatsdMixin, web.RequestHandler): def get(self): pass def assert_between(low, value, high): if not (low <= value < high): raise AssertionError('Expected {} to be between {} and {}'.format( value, low, high)) class MisconfiguredStatsdMetricCollectionTests(testing.AsyncHTTPTestCase): def get_app(self): self.application = web.Application([ web.url('/', examples.statsd.SimpleHandler), web.url('/counters/(.*)/([.0-9]*)', CounterBumper), web.url('/status_code', DefaultStatusCode), ]) def test_bad_protocol_raises_ValueError(self): with self.assertRaises(ValueError): statsd.StatsDCollector(host='127.0.0.1', port=8125, protocol='bad_protocol') class TCPStatsdMetricCollectionTests(testing.AsyncHTTPTestCase): def get_app(self): self.application = web.Application([ web.url('/', examples.statsd.SimpleHandler), web.url('/counters/(.*)/([.0-9]*)', CounterBumper), web.url('/status_code', DefaultStatusCode), ]) return self.application def setUp(self): self.application = None self.namespace = 'testing' super().setUp() self.statsd = FakeStatsdServer(self.io_loop, protocol='tcp') statsd.install(self.application, **{'namespace': self.namespace, 'host': self.statsd.sockaddr[0], 'port': self.statsd.sockaddr[1], 'protocol': 'tcp', 'prepend_metric_type': True}) @mock.patch.object(iostream.IOStream, 'write') def test_write_not_executed_when_connection_is_closed(self, mock_write): self.application.statsd._sock.close() self.application.statsd.send('foo', 500, 'c') mock_write.assert_not_called() @mock.patch.object(iostream.IOStream, 'write') def test_expected_counters_data_written(self, mock_sock): path = ('foo', 'bar') value = 500 metric_type = 'c' expected = "{}:{}|{}\n".format('.'.join( itertools.chain((self.namespace, 'counters'), path)), value, metric_type) self.application.statsd.send(path, value, metric_type) mock_sock.assert_called_once_with(expected.encode()) @mock.patch.object(iostream.IOStream, 'write') def test_expected_timers_data_written(self, mock_sock): path = ('foo', 'bar') value = 500 metric_type = 'ms' expected = "{}:{}|{}\n".format('.'.join( itertools.chain((self.namespace, 'timers'), path)), value, metric_type) self.application.statsd.send(path, value, metric_type) mock_sock.assert_called_once_with(expected.encode()) def test_tcp_message_format(self): expected = '{path}:{value}|{metric_type}\n' self.assertEqual(self.application.statsd._msg_format, expected) def test_that_http_method_call_is_recorded(self): response = self.fetch('/') self.assertEqual(response.code, 204) expected = 'testing.timers.SimpleHandler.GET.204' for path, value, stat_type in self.statsd.find_metrics(expected, 'ms'): assert_between(250.0, float(value), 500.0) def test_that_counter_increment_defaults_to_one(self): response = self.fetch('/', method='POST', body='') self.assertEqual(response.code, 204) prefix = 'testing.counters.request.path' for path, value, stat_type in self.statsd.find_metrics(prefix, 'c'): self.assertEqual(int(value), 1) def test_that_counter_accepts_increment_value(self): response = self.fetch('/counters/path/5', method='POST', body='') self.assertEqual(response.code, 204) prefix = 'testing.counters.path' for path, value, stat_type in self.statsd.find_metrics(prefix, 'c'): self.assertEqual(int(value), 5) def test_that_execution_timer_records_time_spent(self): response = self.fetch('/counters/one.two.three/0.25') self.assertEqual(response.code, 204) prefix = 'testing.timers.one.two.three' for path, value, stat_type in self.statsd.find_metrics(prefix, 'ms'): assert_between(250.0, float(value), 300.0) def test_that_add_metric_tag_is_ignored(self): response = self.fetch('/', headers={'Correlation-ID': 'does not matter'}) self.assertEqual(response.code, 204) def test_that_status_code_is_used_when_not_explicitly_set(self): response = self.fetch('/status_code') self.assertEqual(response.code, 200) expected = 'testing.timers.DefaultStatusCode.GET.200' self.assertEqual(expected, list(self.statsd.find_metrics(expected, 'ms'))[0][0]) class TCPStatsdConfigurationTests(testing.AsyncHTTPTestCase): def get_app(self): self.application = web.Application([ web.url('/', examples.statsd.SimpleHandler), web.url('/counters/(.*)/([.0-9]*)', CounterBumper), ]) return self.application def setUp(self): self.application = None self.namespace = 'testing' super().setUp() self.statsd = FakeStatsdServer(self.io_loop, protocol='tcp') statsd.install(self.application, **{'namespace': self.namespace, 'host': self.statsd.sockaddr[0], 'port': self.statsd.sockaddr[1], 'protocol': 'tcp', 'prepend_metric_type': False}) def test_that_http_method_call_is_recorded(self): response = self.fetch('/') self.assertEqual(response.code, 204) expected = 'testing.SimpleHandler.GET.204' for path, value, stat_type in self.statsd.find_metrics(expected, 'ms'): assert_between(250.0, float(value), 500.0) def test_that_counter_accepts_increment_value(self): response = self.fetch('/counters/path/5', method='POST', body='') self.assertEqual(response.code, 204) prefix = 'testing.path' for path, value, stat_type in self.statsd.find_metrics(prefix, 'c'): self.assertEqual(int(value), 5) class UDPStatsdMetricCollectionTests(testing.AsyncHTTPTestCase): def get_app(self): self.application = web.Application([ web.url('/', examples.statsd.SimpleHandler), web.url('/counters/(.*)/([.0-9]*)', CounterBumper), web.url('/status_code', DefaultStatusCode), ]) return self.application def setUp(self): self.application = None self.namespace = 'testing' super().setUp() self.statsd = FakeStatsdServer(self.io_loop, protocol='udp') statsd.install(self.application, **{'namespace': self.namespace, 'host': self.statsd.sockaddr[0], 'port': self.statsd.sockaddr[1], 'protocol': 'udp', 'prepend_metric_type': True}) def tearDown(self): self.statsd.close() super().tearDown() @mock.patch.object(socket.socket, 'sendto') def test_expected_counters_data_written(self, mock_sock): path = ('foo', 'bar') value = 500 metric_type = 'c' expected = "{}:{}|{}".format('.'.join( itertools.chain((self.namespace, 'counters'), path)), value, metric_type) self.application.statsd.send(path, value, metric_type) mock_sock.assert_called_once_with( expected.encode(), (self.statsd.sockaddr[0], self.statsd.sockaddr[1])) @mock.patch.object(socket.socket, 'sendto') def test_expected_timers_data_written(self, mock_sock): path = ('foo', 'bar') value = 500 metric_type = 'ms' expected = "{}:{}|{}".format('.'.join( itertools.chain((self.namespace, 'timers'), path)), value, metric_type) self.application.statsd.send(path, value, metric_type) mock_sock.assert_called_once_with( expected.encode(), (self.statsd.sockaddr[0], self.statsd.sockaddr[1])) def test_udp_message_format(self): expected = '{path}:{value}|{metric_type}' self.assertEqual(self.application.statsd._msg_format, expected) def test_that_http_method_call_is_recorded(self): response = self.fetch('/') self.assertEqual(response.code, 204) expected = 'testing.timers.SimpleHandler.GET.204' for path, value, stat_type in self.statsd.find_metrics(expected, 'ms'): assert_between(250.0, float(value), 500.0) def test_that_counter_increment_defaults_to_one(self): response = self.fetch('/', method='POST', body='') self.assertEqual(response.code, 204) prefix = 'testing.counters.request.path' for path, value, stat_type in self.statsd.find_metrics(prefix, 'c'): self.assertEqual(int(value), 1) def test_that_counter_accepts_increment_value(self): response = self.fetch('/counters/path/5', method='POST', body='') self.assertEqual(response.code, 204) prefix = 'testing.counters.path' for path, value, stat_type in self.statsd.find_metrics(prefix, 'c'): self.assertEqual(int(value), 5) def test_that_execution_timer_records_time_spent(self): response = self.fetch('/counters/one.two.three/0.25') self.assertEqual(response.code, 204) prefix = 'testing.timers.one.two.three' for path, value, stat_type in self.statsd.find_metrics(prefix, 'ms'): assert_between(250.0, float(value), 300.0) def test_that_add_metric_tag_is_ignored(self): response = self.fetch('/', headers={'Correlation-ID': 'does not matter'}) self.assertEqual(response.code, 204) def test_that_status_code_is_used_when_not_explicitly_set(self): response = self.fetch('/status_code') self.assertEqual(response.code, 200) expected = 'testing.timers.DefaultStatusCode.GET.200' self.assertEqual(expected, list(self.statsd.find_metrics(expected, 'ms'))[0][0]) class UDPStatsdConfigurationTests(testing.AsyncHTTPTestCase): def get_app(self): self.application = web.Application([ web.url('/', examples.statsd.SimpleHandler), web.url('/counters/(.*)/([.0-9]*)', CounterBumper), ]) return self.application def setUp(self): self.application = None self.namespace = 'testing' super().setUp() self.statsd = FakeStatsdServer(self.io_loop, protocol='udp') statsd.install(self.application, **{'namespace': self.namespace, 'host': self.statsd.sockaddr[0], 'port': self.statsd.sockaddr[1], 'protocol': 'udp', 'prepend_metric_type': False}) def tearDown(self): self.statsd.close() super().tearDown() def test_that_http_method_call_is_recorded(self): response = self.fetch('/') self.assertEqual(response.code, 204) expected = 'testing.SimpleHandler.GET.204' for path, value, stat_type in self.statsd.find_metrics(expected, 'ms'): assert_between(250.0, float(value), 500.0) def test_that_counter_accepts_increment_value(self): response = self.fetch('/counters/path/5', method='POST', body='') self.assertEqual(response.code, 204) prefix = 'testing.path' for path, value, stat_type in self.statsd.find_metrics(prefix, 'c'): self.assertEqual(int(value), 5) class StatsdInstallationTests(unittest.TestCase): def setUp(self): self.application = web.Application([ web.url('/', examples.statsd.SimpleHandler), ]) def test_collecter_is_not_reinstalled(self): self.assertTrue(statsd.install(self.application)) self.assertFalse(statsd.install(self.application)) def test_host_is_used(self): statsd.install(self.application, **{'host': 'example.com'}) self.assertEqual(self.application.statsd._host, 'example.com') def test_port_is_used(self): statsd.install(self.application, **{'port': '8888'}) self.assertEqual(self.application.statsd._port, 8888) def test_default_host_and_port_is_used(self): statsd.install(self.application, **{'namespace': 'testing'}) self.assertEqual(self.application.statsd._host, '127.0.0.1') self.assertEqual(self.application.statsd._port, 8125)