Skip to content

Commit

Permalink
Merge pull request #708 from noirbizarre/namespace-loggers
Browse files Browse the repository at this point in the history
Implement Api/Namespace Loggers
  • Loading branch information
ziirish committed Oct 27, 2019
2 parents a10445e + 1a87e3b commit f21c261
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 28 deletions.
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 @@ -130,12 +130,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 @@ -150,7 +145,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 @@ -209,6 +211,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 @@ -270,6 +275,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 @@ -431,6 +441,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 @@ -65,6 +65,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

0 comments on commit f21c261

Please sign in to comment.