Compare commits

...

12 Commits

Author SHA1 Message Date
Gergely Polonkai 45b397f74e Update documentation requirements
Upgrade to Flask-Sphinx-Themes 1.0.2
2018-05-03 13:10:05 +02:00
Gergely Polonkai ddb469a8d2
Merge pull request #5 from gergelypolonkai/rewrite-as-formatter
Rewrite the whole thing as a Formatter
2018-05-02 13:00:26 +02:00
Gergely Polonkai 7469083f0b Rewrite the whole thing as a Formatter
Having a complete `Logger` class for this purpose is a bit of an overkill.
2018-05-02 12:47:46 +02:00
Gergely Polonkai 63895ea46e
Merge pull request #4 from gergelypolonkai/config-before-app
Allow configuration of the logger before the app gets created
2018-01-31 14:23:51 +01:00
Gergely Polonkai f1459dac40 Version bump 2018-01-31 14:20:07 +01:00
Gergely Polonkai e2e2937615 Allow the logger to be configured without an active app 2018-01-31 14:20:07 +01:00
Gergely Polonkai 07dc35cec2 [Bugfix] Fix the default value of self._valid_keywords 2018-01-31 14:20:07 +01:00
Gergely Polonkai 8195d11c8c
Merge pull request #3 from gergelypolonkai/tox-fix
Fix tox environment list
2018-01-30 20:17:17 +01:00
Gergely Polonkai a51fbc33ea Fix tox environment list 2018-01-30 20:14:26 +01:00
Gergely Polonkai 41bcdc4b0b
Merge pull request #2 from gergelypolonkai/autoconfigure
Configure automatically with current_app
2018-01-30 14:40:52 +01:00
Gergely Polonkai b9c3128315 Configure automatically with current_app 2018-01-30 14:36:20 +01:00
Gergely Polonkai fbebc6ca26 [Dev] Remove Python 3.3 from the TOX list 2018-01-30 14:36:20 +01:00
7 changed files with 153 additions and 316 deletions

View File

@ -1,12 +1,11 @@
language: python
python:
- "3.5"
- "3.6"
sudo: false
env:
- TOXENV=py27
- TOXENV=py33
- TOXENV=py34
- TOXENV=py35
- TOXENV=py36
install:
- pip install -U pip
- pip install -U Flask tox coverage codecov

View File

@ -1 +1 @@
Flask-Sphinx-Themes==1.0.1
Flask-Sphinx-Themes==1.0.2

View File

@ -60,9 +60,9 @@ author = 'Gergely Polonkai'
# built documents.
#
# The short X.Y version.
version = '0.1'
version = '1.0'
# The full version, including alpha/beta/rc tags.
release = '0.1.1'
release = '1.0.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View File

@ -32,17 +32,17 @@ no value is present in the message record.
import logging
from flask import has_request_context, request
from flask import has_request_context, request, current_app, has_app_context
__version_info__ = ('0', '0', '1')
__version_info__ = ('1', '0', '0')
__version__ = '.'.join(__version_info__)
__author__ = 'Gergely Polonkai'
__license__ = 'MIT'
__copyright__ = '(c) 2015 GT2'
__copyright__ = '(c) 2015-2018 Benchmarked.games'
class FlaskExtraLogger(logging.getLoggerClass()):
"""
A logger class that is capable of adding extra keywords to log
class FlaskExtraLoggerFormatter(logging.Formatter):
"""A log formatter class that is capable of adding extra keywords to log
formatters and logging the blueprint name
Usage:
@ -50,174 +50,92 @@ class FlaskExtraLogger(logging.getLoggerClass()):
.. code-block:: python
import logging
from logging.config import dictConfig
from flask_logging_extras import register_logger_class
# This must be done before the app is initialized!
register_logger_class(cls=FlaskExtraLogger)
dictConfig({
'formatters': {
'extras': {
'format': '[%(asctime)s] [%(levelname)s] [%(category)s] [%(blueprint)s] %(message)s',
},
},
'handlers': {
'extras_handler': {
'class': 'logging.FileHandler',
'args': ('app.log', 'a'),
'formatter': 'extras',
'level': 'INFO',
},
},
'loggers': {
'my_app': {
'handlers': ['extras_handler'],
}
},
})
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')
handler.setFormatter(formatter)
handler.setLevel(logging.INFO)
app.logger.addHandler(handler)
app.logger.info('The message', category='my category')
logger = logging.getLogger('my_app')
# This will produce something like this in app.log:
# [2017-01-16 08:44:48.944] [INFO] [my category] The message
# [2018-05-02 12:44:48.944] [INFO] [my category] [<NOT REQUEST>] The message
logger.info('The message', extra=dict(category='my category'))
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/')
@app.route('/1')
def route_1():
# This will produce this log message:
# [<APP>] Message
current_app.logger.info('Message')
# This will produce a log message like this:
# [2018-05-02 12:44:48.944] [INFO] [<unset>] [<APP>] Message
logger.info('Message')
return ''
@bp.route('/2')
def route_2():
# This will produce this log message:
# [my blueprint] Message
current_app.logger.info('Message')
# This will produce a log message like this:
# [2018-05-02 12:44:48.944] [INFO] [<unset>] [my_blueprint] Message
logger.info('Message')
return ''
# This will produce this log message:
[<NOT REQUEST>] Message
app.logger.info('Message')
# This will produce a log message like this:
# [2018-05-02 12:44:48.944] [INFO] [<unset>] [<NOT REQUEST>] Message
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:
raise TypeError(
"Cannot initialise {classname} with an app. See the"
"documentation of Flask-Logging-Extras for more info."
.format(classname=self.__class__.__name__))
else:
# If app is None, treat it as if it wasnt there
del(kwargs['app'])
self.app = None
self._valid_keywords = []
self._blueprint_var = None
self._blueprint_app = None
self._blueprint_norequest = None
super(FlaskExtraLogger, self).__init__(*args, **kwargs)
def _log(self, *args, **kwargs):
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]
del(kwargs[kw])
else:
kwargs['extra'][kw] = self._valid_keywords[kw]
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
The class reads its necessary configuration from the config of this
application.
If the application doesnt call this, or doesnt have the
`FLASK_LOGGING_EXTRAS_KEYWORDS` in its config, no extra
functionality will be added.
:param app: a Flask application
:type app: Flask
:raises ValueError: if the app tries to register a keyword that is
reserved for internal use
def _collect_keywords(self, record):
"""Collect all valid keywords and add them to the log record if not present
"""
app.config.setdefault('FLASK_LOGGING_EXTRAS_KEYWORDS', {})
app.config.setdefault('FLASK_LOGGING_EXTRAS_BLUEPRINT',
(None, '<app>', '<not a request>'))
# We assume we do have an active app context here
defaults = current_app.config.get('FLASK_LOGGING_EXTRAS_KEYWORDS', {})
for kw in app.config['FLASK_LOGGING_EXTRAS_KEYWORDS']:
self._check_reserved_word(kw)
for keyword, default in defaults.items():
if keyword not in record.__dict__:
setattr(record, keyword, default)
self._check_reserved_word(
app.config['FLASK_LOGGING_EXTRAS_BLUEPRINT'][0])
def format(self, record):
bp_var, bp_app, bp_noreq = ('blueprint', '<app>', '<not a request>')
blueprint = None
self._valid_keywords = app.config['FLASK_LOGGING_EXTRAS_KEYWORDS']
(
self._blueprint_var,
self._blueprint_app,
self._blueprint_norequest,
) = app.config['FLASK_LOGGING_EXTRAS_BLUEPRINT']
if has_app_context():
self._collect_keywords(record)
bp_var, bp_app, bp_noreq = current_app.config.get('FLASK_LOGGING_EXTRAS_BLUEPRINT',
(bp_var, bp_app, bp_noreq))
def register_logger_class(cls=FlaskExtraLogger):
"""
Register a new logger class
if bp_var and has_request_context():
blueprint = request.blueprint or bp_app
It is effectively a wrapper around `logging.setLoggerClass()`, with an
added check to make sure the class can be used as a logger.
if bp_var and bp_var not in record.__dict__:
blueprint = blueprint or bp_noreq
To use the extra features of the logger class in a Flask app, you must
call it before the app is instantiated.
setattr(record, bp_var, blueprint)
This function returns the logger class that was the default before
calling. This might be useful if you only want to use `cls` in the
Flask app object, but not anywhere else in your code. In this case,
simply call `register_logger_class()` again with the return value from
the first invocation.
:param cls: a logger class to register as the default one
:type cls: class(logging.Logger)
:returns: the old default logger class
:rtype: class
:raises TypeError: if the class is not a subclass of `logging.Logger`
"""
if not issubclass(cls, logging.Logger):
raise TypeError(
"The logger class must be a subclass of logging.Logger!")
old_class = logging.getLoggerClass()
logging.setLoggerClass(cls)
return old_class
return super(FlaskExtraLoggerFormatter, self).format(record)

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.1.0',
version='1.0.0',
url='https://github.com/gergelypolonkai/flask-logging-extras',
license='MIT',
author='Gergely Polonkai',

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
"""
Unit tests for Flask-Logging-Extras
"""Unit tests for Flask-Logging-Extras
"""
import logging
from logging.config import dictConfig
import sys
from unittest import TestCase
@ -11,203 +11,124 @@ from flask import Flask, Blueprint, current_app
import flask_logging_extras
class ListStream(object):
"""
Primitive stream that stores its input lines in a list
"""
class ListHandler(logging.StreamHandler):
def __init__(self, *args, **kwargs):
self.lines = []
super(ListHandler, self).__init__(*args, **kwargs)
super(ListStream, self).__init__(*args, **kwargs)
self.logs = []
def write(self, data):
if len(self.lines) == 0 or self.lines[-1].endswith('\n'):
self.lines.append(data)
else:
self.lines[-1] += data
def emit(self, record):
try:
msg = self.format(record)
except (KeyError, IndexError) as exc:
msg = (exc.__class__.__name__, str(exc))
self.logs.append(msg)
class TestingStreamHandler(logging.StreamHandler):
def handleError(self, record):
exc, exc_msg, trace = sys.exc_info()
def configure_loggers(extra_var):
dictConfig({
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'selftest': {
'class': 'flask_logging_extras.FlaskExtraLoggerFormatter',
'format': '%(message)s %({extra_var})s'.format(extra_var=extra_var),
},
},
'handlers': {
'selftest': {
'class': 'test_logger_keywords.ListHandler',
'formatter': 'selftest',
},
},
'loggers': {
'selftest': {
'handlers': ['selftest'],
'level': 'INFO',
},
},
})
super(logging.StreamHandler, self).handleError(record)
formatter = flask_logging_extras.FlaskExtraLoggerFormatter(
fmt='%(message)s %({extra_var})s'.format(extra_var=extra_var))
raise(exc(exc_msg))
logger = logging.getLogger('selftest')
handlers = [handler for handler in logger.handlers if isinstance(handler, ListHandler)]
handler = handlers[0]
# TODO: Why doesnt this happen automatically in Python 2.7?
handler.setFormatter(formatter)
return logger, handler
class LoggerKeywordsTestCase(TestCase):
def setUp(self):
self.original_logger_class = logging.getLoggerClass()
self.logger, self.handler = configure_loggers('extra_keyword')
self.app = Flask('test_app')
self.app.config['FLASK_LOGGING_EXTRAS_KEYWORDS'] = {
'extra_keyword': 'placeholder',
}
def tearDown(self):
logging.setLoggerClass(self.original_logger_class)
# Make sure we dont try to log the current blueprint for this test case
self.app.config['FLASK_LOGGING_EXTRAS_BLUEPRINT'] = (None, '', '')
def test_logger_registration(self):
class NotLoggerClass(object):
pass
def test_keywords_no_app_ctx(self):
"""With the current formatter, the last log line must be a just the message
"""
with self.assertRaises(TypeError):
flask_logging_extras.register_logger_class(cls=NotLoggerClass)
self.logger.info('message')
self.assertIn(('KeyError', "'extra_keyword'"), self.handler.logs)
original_logging_class = logging.getLoggerClass()
old_class = flask_logging_extras.register_logger_class()
# If no class is specified, FlaskExtraLogger should be registered
self.assertEqual(logging.getLoggerClass(),
flask_logging_extras.FlaskExtraLogger)
# The return value of this function should be the old default
self.assertEqual(original_logging_class, old_class)
class MyLogger(logging.Logger):
pass
# Calling register_logger_class() with any subclass of
# logging.Logger should succeed
flask_logging_extras.register_logger_class(cls=MyLogger)
def test_logger_class(self):
# If app is present during __init__, and is not None, we should get
# a TypeError
with self.assertRaises(TypeError):
flask_logging_extras.FlaskExtraLogger('test_logger',
app='test value')
# If app is present, but is None, no exception should be raised
flask_logging_extras.FlaskExtraLogger('test_logger', app=None)
def test_logger_class_init_app(self):
logger = flask_logging_extras.FlaskExtraLogger('test')
app = Flask('test_app')
logger.init_app(app)
self.assertEqual({}, logger._valid_keywords)
app.config['FLASK_LOGGING_EXTRAS_KEYWORDS'] = {'exc_info': None}
with self.assertRaises(ValueError):
logger.init_app(app)
app.config['FLASK_LOGGING_EXTRAS_KEYWORDS'] = {'my_keyword': '<unset>'}
logger.init_app(app)
self.assertEqual({'my_keyword': '<unset>'}, logger._valid_keywords)
# Without registration first, app.logger should not be of class
# FlaskExtraLogger (even though logger.init_app() succeeded without
# it.)
self.assertNotIsInstance(app.logger,
flask_logging_extras.FlaskExtraLogger)
flask_logging_extras.register_logger_class()
# Even after registratiiion, existing apps are (obviously) not
# tampered with
self.assertNotIsInstance(app.logger,
flask_logging_extras.FlaskExtraLogger)
app = Flask('test_app')
# Newly created apps, though, should be made with this class
self.assertIsInstance(app.logger,
flask_logging_extras.FlaskExtraLogger)
def _create_app_with_logger(self, fmt, keywords=[]):
app = Flask('test_app')
self.assertIsInstance(app.logger,
flask_logging_extras.FlaskExtraLogger)
app.config['FLASK_LOGGING_EXTRAS_KEYWORDS'] = keywords
app.logger.init_app(app)
formatter = logging.Formatter(fmt)
stream = ListStream()
handler = TestingStreamHandler(stream=stream)
handler.setLevel(logging.DEBUG)
handler.setFormatter(formatter)
app.logger.addHandler(handler)
app.logger.setLevel(logging.DEBUG)
return app, stream
def test_keywords(self):
# Register our logger class as the default
old_logger_class = flask_logging_extras.register_logger_class()
app, log_stream = self._create_app_with_logger('%(message)s')
app.logger.info('message')
# With the current formatter, the last log line must be a just the
# message
self.assertEqual(log_stream.lines[-1], 'message\n')
app, log_stream = self._create_app_with_logger(
'%(message)s %(extra_keyword)s')
# If we dont register 'extra_keyword' in the app config, we get a
def test_keywords_app_ctx_assign_value(self):
"""If we dont register 'extra_keyword' in the app config, we get a
# KeyError. We set logging.raiseExceptions to False here; the
# reason is that this doesnt mean an actual exception is raised,
# but our TestingStreamHandler does that for us (which we need for
# testing purposes)
old_raiseExceptions = logging.raiseExceptions
logging.raiseExceptions = False
with self.assertRaises(KeyError):
app.logger.info('message')
logging.raiseExceptions = old_raiseExceptions
"""
app, log_stream = self._create_app_with_logger(
'%(message)s [%(extra_keyword)s]',
keywords={'extra_keyword': '<unset>'})
with self.app.app_context():
self.logger.info('message', extra=dict(extra_keyword='test'))
app.logger.info('message', extra_keyword='test')
self.assertEqual('message [test]\n', log_stream.lines[-1])
self.assertIn('message test', self.handler.logs)
# If we dont provide a value for a registered extra keyword, the
def test_keywords_app_ctx_no_value(self):
"""If we dont provide a value for a registered extra keyword, the
# string "<unset>" will be assigned.
app.logger.info('message')
self.assertEqual('message [<unset>]\n', log_stream.lines[-1])
"""
with self.app.app_context():
self.logger.info('message')
self.assertIn('message placeholder', self.handler.logs)
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)
app.config['FLASK_LOGGING_EXTRAS_BLUEPRINT'] = ('bp', '<app>', '<norequest>')
fmt = '%(bp)s %(message)s'
self.stream = ListStream()
configure_loggers('bp')
self.logger = logging.getLogger('selftest')
handlers = [handler for handler in self.logger.handlers if isinstance(handler, ListHandler)]
self.handler = handlers[0]
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')
bp = Blueprint('test_blueprint', 'test_bp')
@app.route('/app')
def route_1():
current_app.logger.info('Message')
self.logger.info('Message')
return ''
@bp.route('/blueprint')
def route_2():
current_app.logger.info('Message')
self.logger.info('Message')
return ''
@ -215,17 +136,16 @@ class LoggerBlueprintTestCase(TestCase):
self.client = app.test_client()
def tearDown(self):
logging.setLoggerClass(self.original_logger_class)
def test_request_log(self):
def test_blueprint_log_no_blueprint(self):
self.client.get('/app')
self.assertEqual('<app> Message\n', self.stream.lines[-1])
self.assertIn('Message <app>', self.handler.logs)
page = self.client.get('/blueprint')
self.assertEqual('test_blueprint Message\n', self.stream.lines[-1])
def test_blueprint_log_blueprint(self):
self.client.get('/blueprint')
self.assertIn('Message test_blueprint', self.handler.logs)
def test_blueprint_log_no_request(self):
with self.app.app_context():
current_app.logger.info('Message')
self.logger.info('Message')
self.assertEqual('<norequest> Message\n', self.stream.lines[-1])
self.assertIn('Message <norequest>', self.handler.logs)

View File

@ -1,5 +1,5 @@
[tox]
envlist = py27, py33, py34, py35, py36
envlist = py27, py34, py36
[testenv]
commands =