Skip to content
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

Implement Api/Namespace Loggers #708

Merged
merged 5 commits into from
Oct 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Flask-RESTPlus with Flask.
errors
mask
swagger
logging
postman
scaling
example
Expand Down
103 changes: 103 additions & 0 deletions doc/logging.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
Logging
===============

Flask-RESTPlus extends `Flask's logging <https://flask.palletsprojects.com/en/1.1.x/logging/>`_
by providing each ``API`` and ``Namespace`` it's own standard Python :class:`logging.Logger` instance.
This allows separation of logging on a per namespace basis to allow more fine-grained detail and configuration.

By default, these loggers inherit configuration from the Flask application object logger.

.. code-block:: python

import logging

import flask

from flask_restplus import Api, Resource

# configure root logger
logging.basicConfig(level=logging.INFO)

app = flask.Flask(__name__)

api = Api(app)


# each of these loggers uses configuration from app.logger
ns1 = api.namespace('api/v1', description='test')
ns2 = api.namespace('api/v2', description='test')


@ns1.route('/my-resource')
class MyResource(Resource):
def get(self):
# will log
ns1.logger.info("hello from ns1")
return {"message": "hello"}


@ns2.route('/my-resource')
class MyNewResource(Resource):
def get(self):
# won't log due to INFO log level from app.logger
ns2.logger.debug("hello from ns2")
return {"message": "hello"}


Loggers can be configured individually to override the configuration from the Flask
application object logger. In the above example, ``ns2`` log level can be set to
``DEBUG`` individually:

.. code-block:: python

# ns1 will have log level INFO from app.logger
ns1 = api.namespace('api/v1', description='test')

# ns2 will have log level DEBUG
ns2 = api.namespace('api/v2', description='test')
ns2.logger.setLevel(logging.DEBUG)


@ns1.route('/my-resource')
class MyResource(Resource):
def get(self):
# will log
ns1.logger.info("hello from ns1")
return {"message": "hello"}


@ns2.route('/my-resource')
class MyNewResource(Resource):
def get(self):
# will log
ns2.logger.debug("hello from ns2")
return {"message": "hello"}


Adding additional handlers:


.. code-block:: python

# configure a file handler for ns1 only
ns1 = api.namespace('api/v1')
fh = logging.FileHandler("v1.log")
ns1.logger.addHandler(fh)

ns2 = api.namespace('api/v2')


@ns1.route('/my-resource')
class MyResource(Resource):
def get(self):
# will log to *both* v1.log file and app.logger handlers
ns1.logger.info("hello from ns1")
return {"message": "hello"}


@ns2.route('/my-resource')
class MyNewResource(Resource):
def get(self):
# will log to *only* app.logger handlers
ns2.logger.info("hello from ns2")
return {"message": "hello"}
26 changes: 19 additions & 7 deletions flask_restplus/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,7 @@ def __init__(self, app=None, version='1.0', title=None, description=None,
self._refresolver = None
self.format_checker = format_checker
self.namespaces = []
self.default_namespace = self.namespace(default, default_label,
endpoint='{0}-declaration'.format(default),
validate=validate,
api=self,
path='/',
)

self.ns_paths = dict()

self.representations = OrderedDict(DEFAULT_REPRESENTATIONS)
Expand All @@ -146,7 +141,14 @@ def __init__(self, app=None, version='1.0', title=None, description=None,
self.resources = []
self.app = None
self.blueprint = None

# must come after self.app initialisation to prevent __getattr__ recursion
# in self._configure_namespace_logger
self.default_namespace = self.namespace(default, default_label,
endpoint='{0}-declaration'.format(default),
validate=validate,
api=self,
path='/',
)
if app is not None:
self.app = app
self.init_app(app)
Expand Down Expand Up @@ -205,6 +207,9 @@ def _init_app(self, app):
for resource, namespace, urls, kwargs in self.resources:
self._register_view(app, resource, namespace, *urls, **kwargs)

for ns in self.namespaces:
self._configure_namespace_logger(app, ns)

self._register_apidoc(app)
self._validate = self._validate if self._validate is not None else app.config.get('RESTPLUS_VALIDATE', False)
app.config.setdefault('RESTPLUS_MASK_HEADER', 'X-Fields')
Expand Down Expand Up @@ -266,6 +271,11 @@ def register_resource(self, namespace, resource, *urls, **kwargs):
self.resources.append((resource, namespace, urls, kwargs))
return endpoint

def _configure_namespace_logger(self, app, namespace):
for handler in app.logger.handlers:
namespace.logger.addHandler(handler)
namespace.logger.setLevel(app.logger.level)

def _register_view(self, app, resource, namespace, *urls, **kwargs):
endpoint = kwargs.pop('endpoint', None) or camel_to_dash(resource.__name__)
resource_class_args = kwargs.pop('resource_class_args', ())
Expand Down Expand Up @@ -427,6 +437,8 @@ def add_namespace(self, ns, path=None):
# Register models
for name, definition in six.iteritems(ns.models):
self.models[name] = definition
if not self.blueprint and self.app is not None:
self._configure_namespace_logger(self.app, ns)

def namespace(self, *args, **kwargs):
'''
Expand Down
2 changes: 2 additions & 0 deletions flask_restplus/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import inspect
import warnings
import logging
from collections import namedtuple

import six
Expand Down Expand Up @@ -53,6 +54,7 @@ def __init__(self, name, description=None, path=None, decorators=None, validate=
self.apis = []
if 'api' in kwargs:
self.apis.append(kwargs['api'])
self.logger = logging.getLogger(__name__ + "." + self.name)

@property
def path(self):
Expand Down
12 changes: 12 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ def api(request, app):
yield api


@pytest.fixture
def mock_app(mocker):
app = mocker.Mock(Flask)
# mock Flask app object doesn't have any real loggers -> mock logging
# set up on Api object
mocker.patch.object(restplus.Api, '_configure_namespace_logger')
app.view_functions = {}
app.extensions = {}
app.config = {}
return app


@pytest.fixture(autouse=True)
def _push_custom_request_context(request):
app = request.getfixturevalue('app')
Expand Down
30 changes: 9 additions & 21 deletions tests/legacy/test_api_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,21 +126,17 @@ def test_media_types_q(self, app):
}):
assert api.mediatypes() == ['application/json', 'application/xml']

def test_decorator(self, mocker):
def test_decorator(self, mocker, mock_app):
def return_zero(func):
return 0

app = mocker.Mock(flask.Flask)
app.view_functions = {}
app.extensions = {}
app.config = {}
view = mocker.Mock()
api = restplus.Api(app)
api = restplus.Api(mock_app)
api.decorators.append(return_zero)
api.output = mocker.Mock()
api.add_resource(view, '/foo', endpoint='bar')

app.add_url_rule.assert_called_with('/foo', view_func=0)
mock_app.add_url_rule.assert_called_with('/foo', view_func=0)

def test_add_resource_endpoint(self, app, mocker):
view = mocker.Mock(**{'as_view.return_value.__name__': str('test_view')})
Expand Down Expand Up @@ -181,28 +177,20 @@ def get(self):
foo2 = client.get('/foo/toto')
assert foo2.data == b'"foo1"\n'

def test_add_resource(self, mocker):
app = mocker.Mock(flask.Flask)
app.view_functions = {}
app.extensions = {}
app.config = {}
api = restplus.Api(app)
def test_add_resource(self, mocker, mock_app):
api = restplus.Api(mock_app)
api.output = mocker.Mock()
api.add_resource(views.MethodView, '/foo')

app.add_url_rule.assert_called_with('/foo',
mock_app.add_url_rule.assert_called_with('/foo',
view_func=api.output())

def test_add_resource_kwargs(self, mocker):
app = mocker.Mock(flask.Flask)
app.view_functions = {}
app.extensions = {}
app.config = {}
api = restplus.Api(app)
def test_add_resource_kwargs(self, mocker, mock_app):
api = restplus.Api(mock_app)
api.output = mocker.Mock()
api.add_resource(views.MethodView, '/foo', defaults={"bar": "baz"})

app.add_url_rule.assert_called_with('/foo',
mock_app.add_url_rule.assert_called_with('/foo',
view_func=api.output(),
defaults={"bar": "baz"})

Expand Down
Loading