Include a StatusRequestHandler to make things easy

This commit is contained in:
Gavin M. Roy 2020-08-11 19:40:14 -04:00
parent 507704a6e7
commit 9a49d769df
3 changed files with 39 additions and 17 deletions

View file

@ -9,3 +9,14 @@ functionality for interacting with PostgreSQL.
:undoc-members: :undoc-members:
:private-members: :private-members:
:member-order: bysource :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

View file

@ -717,3 +717,21 @@ class RequestHandlerMixin:
else: else:
LOGGER.debug('Postgres query %s duration: %s', LOGGER.debug('Postgres query %s duration: %s',
metric_name, duration) 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')})

View file

@ -175,15 +175,6 @@ class NoRowRequestHandler(RequestHandler):
'rows': self.cast_data(result.rows)}) '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): class TransactionRequestHandler(RequestHandler):
GET_SQL = """\ GET_SQL = """\
@ -315,7 +306,7 @@ class TestCase(testing.SprocketsHttpTestCase):
web.url('/no-error', NoErrorRequestHandler), web.url('/no-error', NoErrorRequestHandler),
web.url('/no-row', NoRowRequestHandler), web.url('/no-row', NoRowRequestHandler),
web.url('/row-count-no-rows', RowCountNoRowsRequestHandler), web.url('/row-count-no-rows', RowCountNoRowsRequestHandler),
web.url('/status', StatusRequestHandler), web.url('/status', sprockets_postgres.StatusRequestHandler),
web.url('/timeout-error', TimeoutErrorRequestHandler), web.url('/timeout-error', TimeoutErrorRequestHandler),
web.url('/transaction', TransactionRequestHandler), web.url('/transaction', TransactionRequestHandler),
web.url('/transaction/(?P<test_id>.*)', TransactionRequestHandler), web.url('/transaction/(?P<test_id>.*)', TransactionRequestHandler),
@ -329,29 +320,31 @@ class RequestHandlerMixinTestCase(TestCase):
def test_postgres_status(self): def test_postgres_status(self):
response = self.fetch('/status') response = self.fetch('/status')
data = json.loads(response.body) data = json.loads(response.body)
self.assertTrue(data['available']) self.assertEqual(data['status'], 'ok')
self.assertGreaterEqual(data['pool_size'], 1) self.assertGreaterEqual(data['postgres']['pool_size'], 1)
self.assertGreaterEqual(data['pool_free'], 1) self.assertGreaterEqual(data['postgres']['pool_free'], 1)
@mock.patch('aiopg.pool.Pool.acquire') @mock.patch('aiopg.pool.Pool.acquire')
def test_postgres_status_connect_error(self, acquire): def test_postgres_status_connect_error(self, acquire):
acquire.side_effect = asyncio.TimeoutError() acquire.side_effect = asyncio.TimeoutError()
response = self.fetch('/status') response = self.fetch('/status')
self.assertEqual(response.code, 503) 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): def test_postgres_status_not_connected(self):
self.app._postgres_connected.clear() self.app._postgres_connected.clear()
response = self.fetch('/status') response = self.fetch('/status')
self.assertEqual(response.code, 503) 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') @mock.patch('aiopg.cursor.Cursor.execute')
def test_postgres_status_error(self, execute): def test_postgres_status_error(self, execute):
execute.side_effect = asyncio.TimeoutError() execute.side_effect = asyncio.TimeoutError()
response = self.fetch('/status') response = self.fetch('/status')
self.assertEqual(response.code, 503) data = json.loads(response.body)
self.assertFalse(json.loads(response.body)['available']) self.assertEqual(data['status'], 'unavailable')
def test_postgres_callproc(self): def test_postgres_callproc(self):
response = self.fetch('/callproc') response = self.fetch('/callproc')