-
Notifications
You must be signed in to change notification settings - Fork 11
Add support for Falcon web framework #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import logging | ||
| from datetime import datetime | ||
|
|
||
| import falcon | ||
|
|
||
| from sap import cf_logging | ||
| from sap.cf_logging import defaults | ||
| from sap.cf_logging.core.constants import REQUEST_KEY, RESPONSE_KEY | ||
| from sap.cf_logging.core.framework import Framework | ||
| from sap.cf_logging.falcon_logging.context import FalconContext | ||
| from sap.cf_logging.falcon_logging.request_reader import FalconRequestReader | ||
| from sap.cf_logging.falcon_logging.response_reader import FalconResponseReader | ||
|
|
||
| FALCON_FRAMEWORK_NAME = 'falcon.framework' | ||
|
|
||
|
|
||
| class LoggingMiddleware: | ||
|
|
||
| def __init__(self, logger='cf.falcon.logger'): | ||
| self.logger = logger | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name |
||
|
|
||
| def process_request(self, req, resp): | ||
| """Process the request before routing it. | ||
| :param req: - Falcon Request object | ||
| :param resp : - Falcon Response object | ||
| """ | ||
| framework = cf_logging.framework | ||
| cid = framework.request_reader.get_correlation_id(req) | ||
| framework.context.set('correlation_id', cid, req) | ||
| framework.context.set('request_started_at', datetime.utcnow(), req) | ||
| req.log = lambda msg, lvl=logging.INFO, extra={}: logging.getLogger( | ||
| self.logger).log(lvl, msg, extra=extra.update({REQUEST_KEY: req}) or extra) | ||
|
|
||
| def process_response(self, req, resp, resource, req_succeeded): | ||
| """Post-processing of the response (after routing). | ||
| :param req: - Falcon Request object | ||
| :param resp : - Falcon Response object | ||
| :param resource : - Falcon Resource object to which the request was | ||
| routed | ||
| :param req_succeeded : - True if no exceptions were raised while | ||
| the framework processed and routed the request | ||
| """ | ||
| cf_logging.framework.context.set( | ||
| 'response_sent_at', datetime.utcnow(), req) | ||
| extra = {REQUEST_KEY: req, RESPONSE_KEY: resp} | ||
| logging.getLogger(self.logger).info('', extra=extra) | ||
|
|
||
|
|
||
| def init(app, level=defaults.DEFAULT_LOGGING_LEVEL): | ||
| """ Initializes logging in JSON format. | ||
| :param app: - Falcon application object | ||
| :param level: - valid log level from standard logging package (optional) | ||
| """ | ||
| if not isinstance(app, falcon.API): | ||
| raise TypeError('application should be instance of Falcon API') | ||
|
|
||
| framework = Framework(FALCON_FRAMEWORK_NAME, FalconContext(), | ||
| FalconRequestReader(), FalconResponseReader()) | ||
| cf_logging.init(framework, level) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| """ Falcon logging context - used by the logging package to keep | ||
| request specific data, needed for logging purposes. | ||
| For example correlation_id needs to be stored during request processing, | ||
| so all log entries contain it. | ||
| """ | ||
|
|
||
| from sap.cf_logging.core.context import Context | ||
|
|
||
|
|
||
| class FalconContext(Context): | ||
| """ Stores logging context in Falcon's request object""" | ||
|
|
||
| def set(self, key, value, request): | ||
| request.context[key] = value | ||
|
|
||
| def get(self, key, request): | ||
| return request.context.get(key) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| """ Falcon request reader """ | ||
|
|
||
| import base64 | ||
| import binascii | ||
| import logging | ||
|
|
||
| from sap.cf_logging import defaults | ||
| from sap.cf_logging.core.request_reader import RequestReader | ||
|
|
||
|
|
||
| class FalconRequestReader(RequestReader): | ||
| def get_remote_user(self, request): | ||
| remote_user = defaults.UNKNOWN | ||
| http_auth = request.get_header('Authorization') | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer not to parse auth headers into the logging lib, rather to support the most common authentication frameworks and the way they provide user info. I would suggest to read the user from the request context as expected to be set by falcon-auth authentication layer. |
||
| if http_auth and http_auth.startswith('Basic'): | ||
| auth_parts = http_auth.split(' ', 1) | ||
| if len(auth_parts) == 2: | ||
| try: | ||
| tokens = base64.b64decode( | ||
| auth_parts[1].strip().encode('utf-8')).decode('utf-8') | ||
| tokens = tokens.split(":", 1) | ||
| except (TypeError, binascii.Error, UnicodeDecodeError) as exc: | ||
| logging.debug("Couldn't get username: %s", exc) | ||
| return remote_user | ||
| if len(tokens) == 2: | ||
| remote_user = tokens[0] | ||
| return remote_user | ||
|
|
||
| def get_protocol(self, request): | ||
| return request.scheme | ||
|
|
||
| def get_content_length(self, request): | ||
| return request.content_length | ||
|
|
||
| def get_remote_ip(self, request): | ||
| return request.remote_addr | ||
|
|
||
| def get_remote_port(self, request): | ||
| return defaults.UNKNOWN | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| """ Falcon response reader """ | ||
| from sap.cf_logging.core.response_reader import ResponseReader | ||
|
|
||
| CONTENT_LENGTH = 'Content-Length' | ||
|
|
||
|
|
||
| class FalconResponseReader(ResponseReader): | ||
| def get_status_code(self, response): | ||
| return response.status.split(' ', 1)[0] | ||
|
|
||
| def get_response_size(self, response): | ||
| return response.get_header('Content-Length') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| asyncio | ||
| falcon | ||
| Flask | ||
| sanic; python_version >= '3.5' | ||
| aiohttp; python_version >= '3.5' | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| """ Module that tests the integration of cf_logging with Falcon """ | ||
| import logging | ||
| import pytest | ||
| import falcon | ||
| from falcon import testing | ||
| from sap import cf_logging | ||
| from sap.cf_logging import falcon_logging | ||
| from tests.log_schemas import WEB_LOG_SCHEMA, CLI_LOG_SCHEMA | ||
| from tests.common_test_params import ( | ||
| v_str, v_num, auth_basic, get_web_record_header_fixtures | ||
| ) | ||
| from tests.util import check_log_record, config_root_logger | ||
|
|
||
|
|
||
| # pylint: disable=protected-access | ||
|
|
||
| @pytest.mark.xfail(raises=TypeError, strict=True) | ||
| def test_falcon_requires_valid_app(): | ||
| """ Test the init api expects a valid app """ | ||
| falcon_logging.init({}) | ||
|
|
||
|
|
||
| FIXTURE = get_web_record_header_fixtures() | ||
| FIXTURE.append(({'Authorization': str(auth_basic('user', 'pass'))}, | ||
| {'remote_user': v_str('user')})) | ||
|
|
||
|
|
||
| class TestResource: | ||
| def on_get(self, req, resp): | ||
| resp.set_header('Content-Type', 'text/plain') | ||
| resp.status = falcon.HTTP_200 | ||
| resp.body = 'ok' | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("headers, expected", FIXTURE) | ||
| def test_falcon_request_log(headers, expected): | ||
| """ That the expected records are logged by the logging library """ | ||
| app = falcon.API(middleware=[ | ||
| falcon_logging.LoggingMiddleware() | ||
| ]) | ||
| app.add_route('/test/path', TestResource()) | ||
|
|
||
| _set_up_falcon_logging(app) | ||
| _, stream = config_root_logger('cf.falcon.logger') | ||
|
|
||
| client = testing.TestClient(app) | ||
| _check_expected_response( | ||
| client.simulate_get('/test/path', headers=headers)) | ||
| assert check_log_record(stream, WEB_LOG_SCHEMA, expected) == {} | ||
|
|
||
|
|
||
| def test_web_log(): | ||
| """ That the custom properties are logged """ | ||
| _user_logging({}, {'myprop': 'myval'}, {'myprop': v_str('myval')}) | ||
|
|
||
|
|
||
| def test_correlation_id(): | ||
| """ Test the correlation id is logged when coming from the headers """ | ||
| _user_logging( | ||
| {'X-CorrelationID': '298ebf9d-be1d-11e7-88ff-2c44fd152860'}, | ||
| {}, | ||
| {'correlation_id': v_str('298ebf9d-be1d-11e7-88ff-2c44fd152860')} | ||
| ) | ||
|
|
||
|
|
||
| # Helper functions | ||
| def _set_up_falcon_logging(app, level=logging.DEBUG): | ||
| cf_logging._setup_done = False | ||
| falcon_logging.init(app, level) | ||
|
|
||
|
|
||
| class TestUserResource: | ||
|
|
||
| def __init__(self, extra, expected): | ||
| self.extra = extra | ||
| self.expected = expected | ||
|
|
||
| def on_get(self, req, resp): | ||
| _, stream = config_root_logger('user.logging') | ||
| req.log('in route headers', extra=self.extra) | ||
| assert check_log_record(stream, CLI_LOG_SCHEMA, self.expected) == {} | ||
|
|
||
| resp.set_header('Content-Type', 'text/plain') | ||
| resp.status = falcon.HTTP_200 | ||
| resp.body = 'ok' | ||
|
|
||
|
|
||
| def _user_logging(headers, extra, expected): | ||
| app = falcon.API(middleware=[ | ||
| falcon_logging.LoggingMiddleware() | ||
| ]) | ||
| app.add_route('/test/user/logging', TestUserResource(extra, expected)) | ||
| _set_up_falcon_logging(app) | ||
| client = testing.TestClient(app) | ||
| _check_expected_response(client.simulate_get('/test/user/logging', | ||
| headers=headers)) | ||
|
|
||
|
|
||
| def _check_expected_response(response, status_code=200, body='ok'): | ||
| assert response.status_code == status_code | ||
| if body is not None: | ||
| assert response.text == body |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add to the Features section the support for Falcon, I would suggest creating a bulleted list containing the supported frameworks.