-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add the possibility to add a prefix to all the URLs managed by c2cwsgiutils. Just define `c2c.base_path` in the production.ini or the `C2C_BASE_PATH` environment variable.
- Loading branch information
Patrick Valsecchi
committed
Apr 6, 2017
1 parent
c371262
commit 91cb6c9
Showing
17 changed files
with
215 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,26 @@ | ||
|
||
import c2cwsgiutils.pyramid | ||
from c2cwsgiutils.health_check import HealthCheck | ||
from pyramid.config import Configurator | ||
from pyramid.httpexceptions import HTTPInternalServerError | ||
|
||
from c2cwsgiutils_app import models | ||
|
||
|
||
def _failure(_request): | ||
raise HTTPInternalServerError('failing check') | ||
|
||
|
||
def main(_, **settings): | ||
""" This function returns a Pyramid WSGI application. | ||
""" | ||
config = Configurator(settings=settings, route_prefix='/api') | ||
config.include(c2cwsgiutils.pyramid.includeme) | ||
models.init(config) | ||
config.scan("c2cwsgiutils_app.services") | ||
health_check = HealthCheck(config) | ||
health_check.add_db_session_check(models.DBSession, at_least_one_model=models.Hello) | ||
health_check.add_url_check('http://localhost/api/hello') | ||
health_check.add_custom_check('fail', _failure, 2) | ||
|
||
return config.make_wsgi_app() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import json | ||
|
||
|
||
def test_ok(app_connection): | ||
response = app_connection.get_json("c2c/health_check") | ||
print('response=' + json.dumps(response)) | ||
assert response == { | ||
'status': 200, | ||
'successes': ['db_engine_sqlalchemy', 'db_engine_sqlalchemy_slave', 'http://localhost/api/hello'], | ||
'failures': {} | ||
} | ||
|
||
|
||
def test_failure(app_connection): | ||
response = app_connection.get_json("c2c/health_check", params={'max_level': '2'}, expected_status=500) | ||
print('response=' + json.dumps(response)) | ||
assert response == { | ||
'status': 500, | ||
'successes': ['db_engine_sqlalchemy', 'db_engine_sqlalchemy_slave', 'http://localhost/api/hello'], | ||
'failures': { | ||
'fail': { | ||
'message': 'failing check', | ||
'stacktrace': response['failures']['fail']['stacktrace'] | ||
} | ||
} | ||
} | ||
|
||
|
||
def test_ping(app_connection): | ||
response = app_connection.get_json("c2c/health_check", params={'max_level': '0'}) | ||
print('response=' + json.dumps(response)) | ||
assert response == { | ||
'status': 200, | ||
'successes': [], | ||
'failures': {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,41 +1,41 @@ | ||
def test_api(app_connection): | ||
response = app_connection.get_json('logging/level', | ||
response = app_connection.get_json('c2c/logging/level', | ||
params={'secret': 'changeme', 'name': 'sqlalchemy.engine'}) | ||
assert response == {'status': 200, 'name': 'sqlalchemy.engine', 'level': 'DEBUG', | ||
'effective_level': 'DEBUG'} | ||
|
||
response = app_connection.get_json('logging/level', | ||
response = app_connection.get_json('c2c/logging/level', | ||
params={'secret': 'changeme', 'name': 'sqlalchemy.engine.sub'}) | ||
assert response == {'status': 200, 'name': 'sqlalchemy.engine.sub', 'level': 'NOTSET', | ||
'effective_level': 'DEBUG'} | ||
|
||
response = app_connection.get_json('logging/level', | ||
response = app_connection.get_json('c2c/logging/level', | ||
params={'secret': 'changeme', 'name': 'sqlalchemy.engine', | ||
'level': 'INFO'}) | ||
assert response == {'status': 200, 'name': 'sqlalchemy.engine', 'level': 'INFO', | ||
'effective_level': 'INFO'} | ||
|
||
response = app_connection.get_json('logging/level', | ||
response = app_connection.get_json('c2c/logging/level', | ||
params={'secret': 'changeme', 'name': 'sqlalchemy.engine'}) | ||
assert response == {'status': 200, 'name': 'sqlalchemy.engine', 'level': 'INFO', | ||
'effective_level': 'INFO'} | ||
|
||
response = app_connection.get_json('logging/level', | ||
response = app_connection.get_json('c2c/logging/level', | ||
params={'secret': 'changeme', 'name': 'sqlalchemy.engine.sub'}) | ||
assert response == {'status': 200, 'name': 'sqlalchemy.engine.sub', 'level': 'NOTSET', | ||
'effective_level': 'INFO'} | ||
|
||
response = app_connection.get_json('logging/level', | ||
response = app_connection.get_json('c2c/logging/level', | ||
params={'secret': 'changeme', 'name': 'sqlalchemy.engine', | ||
'level': 'DEBUG'}) | ||
assert response == {'status': 200, 'name': 'sqlalchemy.engine', 'level': 'DEBUG', | ||
'effective_level': 'DEBUG'} | ||
|
||
|
||
def test_api_bad_secret(app_connection): | ||
app_connection.get_json('logging/level', params={'secret': 'wrong', 'name': 'sqlalchemy.engine'}, | ||
app_connection.get_json('c2c/logging/level', params={'secret': 'wrong', 'name': 'sqlalchemy.engine'}, | ||
expected_status=403) | ||
|
||
|
||
def test_api_missing_secret(app_connection): | ||
app_connection.get_json('logging/level', params={'name': 'sqlalchemy.engine'}, expected_status=403) | ||
app_connection.get_json('c2c/logging/level', params={'name': 'sqlalchemy.engine'}, expected_status=403) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
""" | ||
Private utilities. | ||
""" | ||
|
||
import os | ||
|
||
|
||
def get_base_path(config): | ||
return env_or_config(config, 'C2C_BASE_PATH', 'c2c.base_path', '') | ||
|
||
|
||
def env_or_config(config, env_name, config_name, default): | ||
if env_name in os.environ: | ||
return os.environ[env_name] | ||
return config.get_settings().get(config_name, default) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
""" | ||
Setup an health_check API. | ||
To use it, create an instance of this class in your application initialization and do a few calls to its | ||
methods add_db_check() | ||
""" | ||
import logging | ||
import os | ||
import traceback | ||
|
||
import requests | ||
from c2cwsgiutils import stats, _utils | ||
from pyramid.httpexceptions import HTTPNotFound | ||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
|
||
class HealthCheck: | ||
def __init__(self, config): | ||
config.add_route("c2c_health_check", _utils.get_base_path(config) + r"/health_check", | ||
request_method="GET") | ||
config.add_view(self._view, route_name="c2c_health_check", renderer="json", http_cache=0) | ||
self._checks = [] | ||
|
||
def add_db_session_check(self, session, query_cb=None, at_least_one_model=None, level=1): | ||
""" | ||
Check a DB session is working. You can specify either query_cb or at_least_one_model. | ||
:param session: a DB session created by c2cwsgiutils.db.setup_session() | ||
:param query_cb: a callable that take a session as parameter and check it works | ||
:param at_least_one_model: a model that must have at least one entry in the DB | ||
:param level: the level of the health check | ||
""" | ||
if query_cb is None: | ||
query_cb = self._at_least_one(at_least_one_model) | ||
self._checks.append(self._create_db_engine_check(session, session.c2c_rw_bind, query_cb) + (level,)) | ||
if session.c2c_rw_bind != session.c2c_ro_bind: | ||
self._checks.append(self._create_db_engine_check(session, session.c2c_ro_bind, | ||
query_cb) + (level,)) | ||
|
||
def add_url_check(self, url, name=None, check_cb=lambda request, response: None, timeout=3, level=1): | ||
""" | ||
Check that a GET on an URL returns 2xx | ||
:param url: the URL to query | ||
:param name: the name of the check (defaults to url) | ||
:param check_cb: an optional CB to do additional checks on the response (takes the request and the | ||
response as parameters) | ||
:param timeout: the timeout | ||
:param level: the level of the health check | ||
""" | ||
def check(request): | ||
response = requests.get(url, timeout=timeout) | ||
response.raise_for_status() | ||
check_cb(request, response) | ||
if name is None: | ||
name = url | ||
self._checks.append((name, check, level)) | ||
|
||
def add_custom_check(self, name, check_cb, level): | ||
""" | ||
Add a custom check | ||
:param name: the name of the check | ||
:param check_cb: the callback to call (takes the request as parameter) | ||
:param level: the level of the health check | ||
""" | ||
self._checks.append((name, check_cb, level)) | ||
|
||
def _view(self, request): | ||
max_level = int(request.params.get('max_level', '1')) | ||
successes = [] | ||
failures = {} | ||
for name, check, level in self._checks: | ||
if level <= max_level: | ||
try: | ||
check(request) | ||
successes.append(name) | ||
except Exception as e: | ||
trace = traceback.format_exc() | ||
LOG.error(trace) | ||
failure = {'message': str(e)} | ||
if os.environ.get('DEVELOPMENT', '0') != '0': | ||
failure['stacktrace'] = trace | ||
failures[name] = failure | ||
|
||
if failures: | ||
request.response.status = 500 | ||
|
||
return {'status': 500 if failures else 200, 'failures': failures, 'successes': successes} | ||
|
||
def _create_db_engine_check(self, session, bind, query_cb): | ||
def check(request): | ||
prev_bind = session.bind | ||
try: | ||
session.bind = bind | ||
with stats.timer_context(['sql', 'manual', 'health_check', 'db', bind.c2c_name]): | ||
query_cb(session) | ||
finally: | ||
session.bind = prev_bind | ||
return 'db_engine_' + bind.c2c_name, check | ||
|
||
def _at_least_one(self, model): | ||
def query(session): | ||
result = session.query(model).first() | ||
if result is None: | ||
raise HTTPNotFound(model.__name__ + " record not found") | ||
return query |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.