Browse Source

Add Blueprint logging support

blueprint-logging
Gergely Polonkai 3 years ago
parent
commit
583dfdd941
4 changed files with 183 additions and 10 deletions
  1. +57
    -2
      README.rst
  2. +68
    -6
      flask_logging_extras/__init__.py
  3. +1
    -1
      setup.py
  4. +57
    -1
      tests/test_logger_keywords.py

+ 57
- 2
README.rst View File

@@ -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': '<unset>'}
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',
'<APP>',
'<NO REQUEST>',
)

bp = Blueprint('bpname', __name__)
app.register_blueprint(bp)

@app.route('/route/1/')
def route_1():
# This will produce the log message: "[<APP>] 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: "[<NO REQUEST>] Message"
current_app.logger.info('Message')

Installation
------------



+ 68
- 6
flask_logging_extras/__init__.py View File

@@ -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': '<unset>'}
app.config['FLASK_LOGGING_EXTRAS_BLUEPRINT'] = ('blueprint',
'<APP>',
'<NOT REQUEST>')
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:
# [<APP>] 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:
[<NOT REQUEST>] 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, '<app>', '<not a request>'))

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):


+ 1
- 1
setup.py View File

@@ -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',


+ 57
- 1
tests/test_logger_keywords.py View File

@@ -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 "<unset>" will be assigned.
app.logger.info('message')
self.assertEqual('message [<unset>]\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>', '<norequest>')
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('<app> 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('<norequest> Message\n', self.stream.lines[-1])

Loading…
Cancel
Save