Skip to content

Commit f21c261

Browse files
authored
Merge pull request #708 from noirbizarre/namespace-loggers
Implement Api/Namespace Loggers
2 parents a10445e + 1a87e3b commit f21c261

File tree

7 files changed

+279
-28
lines changed

7 files changed

+279
-28
lines changed

doc/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Flask-RESTPlus with Flask.
5151
errors
5252
mask
5353
swagger
54+
logging
5455
postman
5556
scaling
5657
example

doc/logging.rst

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
Logging
2+
===============
3+
4+
Flask-RESTPlus extends `Flask's logging <https://flask.palletsprojects.com/en/1.1.x/logging/>`_
5+
by providing each ``API`` and ``Namespace`` it's own standard Python :class:`logging.Logger` instance.
6+
This allows separation of logging on a per namespace basis to allow more fine-grained detail and configuration.
7+
8+
By default, these loggers inherit configuration from the Flask application object logger.
9+
10+
.. code-block:: python
11+
12+
import logging
13+
14+
import flask
15+
16+
from flask_restplus import Api, Resource
17+
18+
# configure root logger
19+
logging.basicConfig(level=logging.INFO)
20+
21+
app = flask.Flask(__name__)
22+
23+
api = Api(app)
24+
25+
26+
# each of these loggers uses configuration from app.logger
27+
ns1 = api.namespace('api/v1', description='test')
28+
ns2 = api.namespace('api/v2', description='test')
29+
30+
31+
@ns1.route('/my-resource')
32+
class MyResource(Resource):
33+
def get(self):
34+
# will log
35+
ns1.logger.info("hello from ns1")
36+
return {"message": "hello"}
37+
38+
39+
@ns2.route('/my-resource')
40+
class MyNewResource(Resource):
41+
def get(self):
42+
# won't log due to INFO log level from app.logger
43+
ns2.logger.debug("hello from ns2")
44+
return {"message": "hello"}
45+
46+
47+
Loggers can be configured individually to override the configuration from the Flask
48+
application object logger. In the above example, ``ns2`` log level can be set to
49+
``DEBUG`` individually:
50+
51+
.. code-block:: python
52+
53+
# ns1 will have log level INFO from app.logger
54+
ns1 = api.namespace('api/v1', description='test')
55+
56+
# ns2 will have log level DEBUG
57+
ns2 = api.namespace('api/v2', description='test')
58+
ns2.logger.setLevel(logging.DEBUG)
59+
60+
61+
@ns1.route('/my-resource')
62+
class MyResource(Resource):
63+
def get(self):
64+
# will log
65+
ns1.logger.info("hello from ns1")
66+
return {"message": "hello"}
67+
68+
69+
@ns2.route('/my-resource')
70+
class MyNewResource(Resource):
71+
def get(self):
72+
# will log
73+
ns2.logger.debug("hello from ns2")
74+
return {"message": "hello"}
75+
76+
77+
Adding additional handlers:
78+
79+
80+
.. code-block:: python
81+
82+
# configure a file handler for ns1 only
83+
ns1 = api.namespace('api/v1')
84+
fh = logging.FileHandler("v1.log")
85+
ns1.logger.addHandler(fh)
86+
87+
ns2 = api.namespace('api/v2')
88+
89+
90+
@ns1.route('/my-resource')
91+
class MyResource(Resource):
92+
def get(self):
93+
# will log to *both* v1.log file and app.logger handlers
94+
ns1.logger.info("hello from ns1")
95+
return {"message": "hello"}
96+
97+
98+
@ns2.route('/my-resource')
99+
class MyNewResource(Resource):
100+
def get(self):
101+
# will log to *only* app.logger handlers
102+
ns2.logger.info("hello from ns2")
103+
return {"message": "hello"}

flask_restplus/api.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,7 @@ def __init__(self, app=None, version='1.0', title=None, description=None,
130130
self._refresolver = None
131131
self.format_checker = format_checker
132132
self.namespaces = []
133-
self.default_namespace = self.namespace(default, default_label,
134-
endpoint='{0}-declaration'.format(default),
135-
validate=validate,
136-
api=self,
137-
path='/',
138-
)
133+
139134
self.ns_paths = dict()
140135

141136
self.representations = OrderedDict(DEFAULT_REPRESENTATIONS)
@@ -150,7 +145,14 @@ def __init__(self, app=None, version='1.0', title=None, description=None,
150145
self.resources = []
151146
self.app = None
152147
self.blueprint = None
153-
148+
# must come after self.app initialisation to prevent __getattr__ recursion
149+
# in self._configure_namespace_logger
150+
self.default_namespace = self.namespace(default, default_label,
151+
endpoint='{0}-declaration'.format(default),
152+
validate=validate,
153+
api=self,
154+
path='/',
155+
)
154156
if app is not None:
155157
self.app = app
156158
self.init_app(app)
@@ -209,6 +211,9 @@ def _init_app(self, app):
209211
for resource, namespace, urls, kwargs in self.resources:
210212
self._register_view(app, resource, namespace, *urls, **kwargs)
211213

214+
for ns in self.namespaces:
215+
self._configure_namespace_logger(app, ns)
216+
212217
self._register_apidoc(app)
213218
self._validate = self._validate if self._validate is not None else app.config.get('RESTPLUS_VALIDATE', False)
214219
app.config.setdefault('RESTPLUS_MASK_HEADER', 'X-Fields')
@@ -270,6 +275,11 @@ def register_resource(self, namespace, resource, *urls, **kwargs):
270275
self.resources.append((resource, namespace, urls, kwargs))
271276
return endpoint
272277

278+
def _configure_namespace_logger(self, app, namespace):
279+
for handler in app.logger.handlers:
280+
namespace.logger.addHandler(handler)
281+
namespace.logger.setLevel(app.logger.level)
282+
273283
def _register_view(self, app, resource, namespace, *urls, **kwargs):
274284
endpoint = kwargs.pop('endpoint', None) or camel_to_dash(resource.__name__)
275285
resource_class_args = kwargs.pop('resource_class_args', ())
@@ -431,6 +441,8 @@ def add_namespace(self, ns, path=None):
431441
# Register models
432442
for name, definition in six.iteritems(ns.models):
433443
self.models[name] = definition
444+
if not self.blueprint and self.app is not None:
445+
self._configure_namespace_logger(self.app, ns)
434446

435447
def namespace(self, *args, **kwargs):
436448
'''

flask_restplus/namespace.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import inspect
55
import warnings
6+
import logging
67
from collections import namedtuple
78

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

5759
@property
5860
def path(self):

tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ def api(request, app):
6565
yield api
6666

6767

68+
@pytest.fixture
69+
def mock_app(mocker):
70+
app = mocker.Mock(Flask)
71+
# mock Flask app object doesn't have any real loggers -> mock logging
72+
# set up on Api object
73+
mocker.patch.object(restplus.Api, '_configure_namespace_logger')
74+
app.view_functions = {}
75+
app.extensions = {}
76+
app.config = {}
77+
return app
78+
79+
6880
@pytest.fixture(autouse=True)
6981
def _push_custom_request_context(request):
7082
app = request.getfixturevalue('app')

tests/legacy/test_api_legacy.py

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -126,21 +126,17 @@ def test_media_types_q(self, app):
126126
}):
127127
assert api.mediatypes() == ['application/json', 'application/xml']
128128

129-
def test_decorator(self, mocker):
129+
def test_decorator(self, mocker, mock_app):
130130
def return_zero(func):
131131
return 0
132132

133-
app = mocker.Mock(flask.Flask)
134-
app.view_functions = {}
135-
app.extensions = {}
136-
app.config = {}
137133
view = mocker.Mock()
138-
api = restplus.Api(app)
134+
api = restplus.Api(mock_app)
139135
api.decorators.append(return_zero)
140136
api.output = mocker.Mock()
141137
api.add_resource(view, '/foo', endpoint='bar')
142138

143-
app.add_url_rule.assert_called_with('/foo', view_func=0)
139+
mock_app.add_url_rule.assert_called_with('/foo', view_func=0)
144140

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

184-
def test_add_resource(self, mocker):
185-
app = mocker.Mock(flask.Flask)
186-
app.view_functions = {}
187-
app.extensions = {}
188-
app.config = {}
189-
api = restplus.Api(app)
180+
def test_add_resource(self, mocker, mock_app):
181+
api = restplus.Api(mock_app)
190182
api.output = mocker.Mock()
191183
api.add_resource(views.MethodView, '/foo')
192184

193-
app.add_url_rule.assert_called_with('/foo',
185+
mock_app.add_url_rule.assert_called_with('/foo',
194186
view_func=api.output())
195187

196-
def test_add_resource_kwargs(self, mocker):
197-
app = mocker.Mock(flask.Flask)
198-
app.view_functions = {}
199-
app.extensions = {}
200-
app.config = {}
201-
api = restplus.Api(app)
188+
def test_add_resource_kwargs(self, mocker, mock_app):
189+
api = restplus.Api(mock_app)
202190
api.output = mocker.Mock()
203191
api.add_resource(views.MethodView, '/foo', defaults={"bar": "baz"})
204192

205-
app.add_url_rule.assert_called_with('/foo',
193+
mock_app.add_url_rule.assert_called_with('/foo',
206194
view_func=api.output(),
207195
defaults={"bar": "baz"})
208196

0 commit comments

Comments
 (0)