diff --git a/docs/request_handler.rst b/docs/request_handler.rst index 4d9f19c..93e0f08 100644 --- a/docs/request_handler.rst +++ b/docs/request_handler.rst @@ -9,3 +9,14 @@ functionality for interacting with PostgreSQL. :undoc-members: :private-members: :member-order: bysource + +The :class:`~sprockets_postgres.StatusRequestHandler` is a Tornado +:class:`tornado.web.RequestHandler` that can be used for application health +monitoring. If the Postgres connection is unavailable, it will report the +API as unavailable and return a 503 status code. + +.. autoclass:: sprockets_postgres.StatusRequestHandler + :members: + :undoc-members: + :private-members: + :member-order: bysource diff --git a/sprockets_postgres.py b/sprockets_postgres.py index fb3f460..16560dd 100644 --- a/sprockets_postgres.py +++ b/sprockets_postgres.py @@ -717,3 +717,21 @@ class RequestHandlerMixin: else: LOGGER.debug('Postgres query %s duration: %s', metric_name, duration) + + +class StatusRequestHandler(web.RequestHandler): + """A RequestHandler that can be used to expose API health or status""" + + async def get(self, *_args, **_kwarg): + postgres = await self.application.postgres_status() + if not postgres['available']: + self.set_status(503) + self.write({ + 'application': self.settings.get('service', 'unknown'), + 'environment': self.settings.get('environment', 'unknown'), + 'postgres': { + 'pool_free': postgres['pool_free'], + 'pool_size': postgres['pool_size'] + }, + 'status': 'ok' if postgres['available'] else 'unavailable', + 'version': self.settings.get('version', 'unknown')}) diff --git a/tests.py b/tests.py index 50abf73..dde98b6 100644 --- a/tests.py +++ b/tests.py @@ -175,15 +175,6 @@ class NoRowRequestHandler(RequestHandler): 'rows': self.cast_data(result.rows)}) -class StatusRequestHandler(RequestHandler): - - async def get(self): - status = await self.application.postgres_status() - if not status['available']: - self.set_status(503, 'Database Unavailable') - await self.finish(status) - - class TransactionRequestHandler(RequestHandler): GET_SQL = """\ @@ -315,7 +306,7 @@ class TestCase(testing.SprocketsHttpTestCase): web.url('/no-error', NoErrorRequestHandler), web.url('/no-row', NoRowRequestHandler), web.url('/row-count-no-rows', RowCountNoRowsRequestHandler), - web.url('/status', StatusRequestHandler), + web.url('/status', sprockets_postgres.StatusRequestHandler), web.url('/timeout-error', TimeoutErrorRequestHandler), web.url('/transaction', TransactionRequestHandler), web.url('/transaction/(?P.*)', TransactionRequestHandler), @@ -329,29 +320,31 @@ class RequestHandlerMixinTestCase(TestCase): def test_postgres_status(self): response = self.fetch('/status') data = json.loads(response.body) - self.assertTrue(data['available']) - self.assertGreaterEqual(data['pool_size'], 1) - self.assertGreaterEqual(data['pool_free'], 1) + self.assertEqual(data['status'], 'ok') + self.assertGreaterEqual(data['postgres']['pool_size'], 1) + self.assertGreaterEqual(data['postgres']['pool_free'], 1) @mock.patch('aiopg.pool.Pool.acquire') def test_postgres_status_connect_error(self, acquire): acquire.side_effect = asyncio.TimeoutError() response = self.fetch('/status') self.assertEqual(response.code, 503) - self.assertFalse(json.loads(response.body)['available']) + data = json.loads(response.body) + self.assertEqual(data['status'], 'unavailable') def test_postgres_status_not_connected(self): self.app._postgres_connected.clear() response = self.fetch('/status') self.assertEqual(response.code, 503) - self.assertFalse(json.loads(response.body)['available']) + data = json.loads(response.body) + self.assertEqual(data['status'], 'unavailable') @mock.patch('aiopg.cursor.Cursor.execute') def test_postgres_status_error(self, execute): execute.side_effect = asyncio.TimeoutError() response = self.fetch('/status') - self.assertEqual(response.code, 503) - self.assertFalse(json.loads(response.body)['available']) + data = json.loads(response.body) + self.assertEqual(data['status'], 'unavailable') def test_postgres_callproc(self): response = self.fetch('/callproc')