From 583dfdd9419f65f071096fa68c63d48264de3824 Mon Sep 17 00:00:00 2001 From: Gergely Polonkai Date: Thu, 2 Feb 2017 16:33:14 +0100 Subject: [PATCH] Add Blueprint logging support --- README.rst | 59 ++++++++++++++++++++++++- flask_logging_extras/__init__.py | 74 +++++++++++++++++++++++++++++--- setup.py | 2 +- tests/test_logger_keywords.py | 58 ++++++++++++++++++++++++- 4 files changed, 183 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 4d2b7d5..b86c288 100644 --- a/README.rst +++ b/README.rst @@ -15,8 +15,16 @@ Flask-Logging-Extras Flask-Logging-Extras adds additional logging features for Flask applications. -The only feature implemented now is adding extra arguments to the format -string, like this (this example adds the category keyword to the logs: +Extra keywords in the log formatters +------------------------------------ + +Adding extra keywords to the log format message is a bit tedious, as these +must be supplied to the logging methods in the `extra` argument as a +dictionary. + +Flask-Logging-Extras makes this easier, so you can add such keywords to the +logging methods directly. The example adds the category keyword to the logs, +and shows how to do it with and without Flask-Logging-Extras: .. code-block:: python @@ -26,9 +34,56 @@ string, like this (this example adds the category keyword to the logs: app.config['FLASK_LOGGING_EXTRAS_KEYWORDS'] = {'category': ''} app.logger.init_app(app) + # Without Flask-Logging-Extras + current_app.logger.info('this is a the message, as usual', + extra={'category': 'fancy-category'}) + # With Flask-Logging-Extras current_app.logger.info('this is the message, as usual', category='fancy-category') +Logging the blueprint name +-------------------------- + +Although you can always access the blueprint name using `request.blueprint`, +adding it to the logs as a new keyword is not so easy. + +With Flask-Logging-Extras you can specify a keyword that will hold the +blueprint name in the logs, and specify what value to put there if the log +doesn’t originate in a request, or it is not from a blueprint route, but +from an app route. + +.. code-block:: python + + fmt = '[%(blueprint)s] %(message)s' + # Initialize log handlers as usual, like creating a FileHandler, and + # assign fmt to it as a format string + app.config['FLASK_LOGGING_EXTRAS_BLUEPRINT'] = ( + 'blueprint', + '', + '', + ) + + bp = Blueprint('bpname', __name__) + app.register_blueprint(bp) + + @app.route('/route/1/') + def route_1(): + # This will produce the log message: "[] Message" + current_app.logger.info('Message') + + return 'response 1' + + @bp.route('/route/2/') + def route_2(): + # This will produce the log message: "[bpname] Message" + current_app.logger.info('Message') + + return 'response 2' + + def random_function_outside_of_a_request(): + # This will produce the log message: "[] Message" + current_app.logger.info('Message') + Installation ------------ diff --git a/flask_logging_extras/__init__.py b/flask_logging_extras/__init__.py index 74fbbe3..44a3d45 100644 --- a/flask_logging_extras/__init__.py +++ b/flask_logging_extras/__init__.py @@ -32,6 +32,8 @@ no value is present in the message record. import logging +from flask import has_request_context, request + __version_info__ = ('0', '0', '1') __version__ = '.'.join(__version_info__) __author__ = 'Gergely Polonkai' @@ -40,7 +42,8 @@ __copyright__ = '(c) 2015 GT2' class FlaskExtraLogger(logging.getLoggerClass()): """ - A logger class that is capable of adding extra keywords to log formatters + A logger class that is capable of adding extra keywords to log + formatters and logging the blueprint name Usage: @@ -55,8 +58,14 @@ class FlaskExtraLogger(logging.getLoggerClass()): app = Flask(__name__) app.config['FLASK_LOGGING_EXTRAS_KEYWORDS'] = {'category': ''} + app.config['FLASK_LOGGING_EXTRAS_BLUEPRINT'] = ('blueprint', + '', + '') app.logger.init_app() + bp = Blueprint('my_blueprint', __name__) + app.register_blueprint(bp) + formatter = logging.Formatter( '[%(asctime)s] [%(levelname)s] [%(category)s] %(message)s') handler = logging.FileHandler('app.log', mode='a') @@ -68,9 +77,38 @@ class FlaskExtraLogger(logging.getLoggerClass()): app.logger.info('The message', category='my category') # This will produce something like this in app.log: - # [TIMESTAMP2017-01-16 08:44:48.944] [INFO] [my category] The message + # [2017-01-16 08:44:48.944] [INFO] [my category] The message + + formatter = logging.Formatter('[%(blueprint)s] %(message)s') + handler = logging.FileHandler('other.log', mode='a') + handler.setFormatter(formatter) + handler.setLevel(logging.INFO) + + app.logger.addHandler(handler) + + @app.route('/1/') + def route_1(): + # This will produce this log message: + # [] Message + current_app.logger.info('Message') + + return '' + + @bp.route('/2') + def route_2(): + # This will produce this log message: + # [my blueprint] Message + current_app.logger.info('Message') + + return '' + + # This will produce this log message: + [] Message + app.logger.info('Message') """ + _RESERVED_KEYWORDS = ('exc_info', 'extra', 'stack_info') + def __init__(self, *args, **kwargs): if 'app' in kwargs: if kwargs['app'] is not None: @@ -84,6 +122,9 @@ class FlaskExtraLogger(logging.getLoggerClass()): self.app = None self._valid_keywords = [] + self._blueprint_var = None + self._blueprint_app = None + self._blueprint_norequest = None super(FlaskExtraLogger, self).__init__(*args, **kwargs) @@ -91,6 +132,13 @@ class FlaskExtraLogger(logging.getLoggerClass()): if 'extra' not in kwargs: kwargs['extra'] = {} + # If we were asked to log the blueprint name, add it to the extra list + if self._blueprint_var is not None: + if has_request_context(): + kwargs['extra'][self._blueprint_var] = request.blueprint or self._blueprint_app + else: + kwargs['extra'][self._blueprint_var] = self._blueprint_norequest + for kw in self._valid_keywords: if kw in kwargs: kwargs['extra'][kw] = kwargs[kw] @@ -100,6 +148,13 @@ class FlaskExtraLogger(logging.getLoggerClass()): super(FlaskExtraLogger, self)._log(*args, **kwargs) + def _check_reserved_word(self, word): + if word in self._RESERVED_KEYWORDS: + raise ValueError( + '"{keyword}" cannot be used as an extra keyword, as it is ' + 'reserved for internal use.' + .format(keyword=word)) + def init_app(self, app): """ Intialize the logger class with a Flask application @@ -118,14 +173,21 @@ class FlaskExtraLogger(logging.getLoggerClass()): """ app.config.setdefault('FLASK_LOGGING_EXTRAS_KEYWORDS', {}) + app.config.setdefault('FLASK_LOGGING_EXTRAS_BLUEPRINT', + (None, '', '')) for kw in app.config['FLASK_LOGGING_EXTRAS_KEYWORDS']: - if kw in ['exc_info', 'extra', 'stack_info']: - raise ValueError( - '"{keyword}" member of FLASK_LOGGING_EXTRAS_KEYWORDS is ' - 'reserved for internal use.') + self._check_reserved_word(kw) + + self._check_reserved_word( + app.config['FLASK_LOGGING_EXTRAS_BLUEPRINT'][0]) self._valid_keywords = app.config['FLASK_LOGGING_EXTRAS_KEYWORDS'] + ( + self._blueprint_var, + self._blueprint_app, + self._blueprint_norequest, + ) = app.config['FLASK_LOGGING_EXTRAS_BLUEPRINT'] def register_logger_class(cls=FlaskExtraLogger): diff --git a/setup.py b/setup.py index a6d1ca3..2c07db5 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ Flask-Logging-Extras provides extra logging functionality for Flask apps. from setuptools import setup setup(name='Flask-Logging-Extras', - version='0.0.1', + version='0.1.0', url='https://github.com/gergelypolonkai/flask-logging-extras', license='MIT', author='Gergely Polonkai', diff --git a/tests/test_logger_keywords.py b/tests/test_logger_keywords.py index 74265b8..4f10070 100644 --- a/tests/test_logger_keywords.py +++ b/tests/test_logger_keywords.py @@ -7,7 +7,7 @@ import logging import sys from unittest import TestCase -from flask import Flask +from flask import Flask, Blueprint, current_app import flask_logging_extras @@ -173,3 +173,59 @@ class LoggerKeywordsTestCase(TestCase): # string "" will be assigned. app.logger.info('message') self.assertEqual('message []\n', log_stream.lines[-1]) + + +class LoggerBlueprintTestCase(TestCase): + def setUp(self): + # Register our logger class + self.original_logger_class = logging.getLoggerClass() + flask_logging_extras.register_logger_class() + + app = Flask('test_app') + self.app = app + app.config['FLASK_LOGGING_EXTRAS_BLUEPRINT'] = ( + 'bp', '', '') + app.logger.init_app(app) + + fmt = '%(bp)s %(message)s' + self.stream = ListStream() + + formatter = logging.Formatter(fmt) + handler = TestingStreamHandler(stream=self.stream) + handler.setLevel(logging.DEBUG) + handler.setFormatter(formatter) + app.logger.addHandler(handler) + app.logger.setLevel(logging.DEBUG) + + bp = Blueprint('test_blueprint', 'test_bpg13') + + @app.route('/app') + def route_1(): + current_app.logger.info('Message') + + return '' + + @bp.route('/blueprint') + def route_2(): + current_app.logger.info('Message') + + return '' + + app.register_blueprint(bp) + + self.client = app.test_client() + + def tearDown(self): + logging.setLoggerClass(self.original_logger_class) + + def test_request_log(self): + self.client.get('/app') + self.assertEqual(' Message\n', self.stream.lines[-1]) + + page = self.client.get('/blueprint') + self.assertEqual('test_blueprint Message\n', self.stream.lines[-1]) + + with self.app.app_context(): + current_app.logger.info('Message') + + self.assertEqual(' Message\n', self.stream.lines[-1])