simplify logging configuration

single default handler and formatter
don't remove handlers
configure level once using setLevel
document logging
reorganize logging tests
This commit is contained in:
David Lord 2017-07-28 14:55:52 -07:00
parent 85fa8aabf5
commit 66b1b752da
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
13 changed files with 399 additions and 451 deletions

View file

@ -16,10 +16,9 @@ from functools import update_wrapper
from itertools import chain
from threading import Lock
from werkzeug.datastructures import ImmutableDict, Headers
from werkzeug.exceptions import BadRequest, HTTPException, \
InternalServerError, MethodNotAllowed, default_exceptions, \
BadRequestKeyError
from werkzeug.datastructures import Headers, ImmutableDict
from werkzeug.exceptions import BadRequest, BadRequestKeyError, HTTPException, \
InternalServerError, MethodNotAllowed, default_exceptions
from werkzeug.routing import BuildError, Map, RequestRedirect, Rule
from . import cli, json
@ -30,6 +29,7 @@ from .globals import _request_ctx_stack, g, request, session
from .helpers import _PackageBoundObject, \
_endpoint_from_view_func, find_package, get_debug_flag, \
get_flashed_messages, locked_cached_property, url_for
from .logging import create_logger
from .sessions import SecureCookieSessionInterface
from .signals import appcontext_tearing_down, got_request_exception, \
request_finished, request_started, request_tearing_down
@ -37,9 +37,6 @@ from .templating import DispatchingJinjaLoader, Environment, \
_default_template_ctx_processor
from .wrappers import Request, Response
# a lock used for logger initialization
_logger_lock = Lock()
# a singleton sentinel value for parameter defaults
_sentinel = object()
@ -264,12 +261,6 @@ class Flask(_PackageBoundObject):
#: ``USE_X_SENDFILE`` configuration key. Defaults to ``False``.
use_x_sendfile = ConfigAttribute('USE_X_SENDFILE')
#: The name of the logger to use. By default the logger name is the
#: package name passed to the constructor.
#:
#: .. versionadded:: 0.4
logger_name = ConfigAttribute('LOGGER_NAME')
#: The JSON encoder class to use. Defaults to :class:`~flask.json.JSONEncoder`.
#:
#: .. versionadded:: 0.10
@ -294,8 +285,6 @@ class Flask(_PackageBoundObject):
'SECRET_KEY': None,
'PERMANENT_SESSION_LIFETIME': timedelta(days=31),
'USE_X_SENDFILE': False,
'LOGGER_NAME': None,
'LOGGER_HANDLER_POLICY': 'always',
'SERVER_NAME': None,
'APPLICATION_ROOT': '/',
'SESSION_COOKIE_NAME': 'session',
@ -392,10 +381,6 @@ class Flask(_PackageBoundObject):
#: to load a config from files.
self.config = self.make_config(instance_relative_config)
# Prepare the deferred setup of the logger.
self._logger = None
self.logger_name = self.import_name
#: A dictionary of all view functions registered. The keys will
#: be function names which are also used to generate URLs and
#: the values are the function objects themselves.
@ -613,27 +598,28 @@ class Flask(_PackageBoundObject):
return rv
return self.debug
@property
@locked_cached_property
def logger(self):
"""A :class:`logging.Logger` object for this application. The
default configuration is to log to stderr if the application is
in debug mode. This logger can be used to (surprise) log messages.
Here some examples::
"""The ``'flask.app'`` logger, a standard Python
:class:`~logging.Logger`.
app.logger.debug('A value for debugging')
app.logger.warning('A warning occurred (%d apples)', 42)
app.logger.error('An error occurred')
In debug mode, the logger's :attr:`~logging.Logger.level` will be set
to :data:`~logging.DEBUG`.
If there are no handlers configured, a default handler will be added.
See :ref:`logging` for more information.
.. versionchanged:: 1.0
Behavior was simplified. The logger is always named
``flask.app``. The level is only set during configuration, it
doesn't check ``app.debug`` each time. Only one format is used,
not different ones depending on ``app.debug``. No handlers are
removed, and a handler is only added if no handlers are already
configured.
.. versionadded:: 0.3
"""
if self._logger and self._logger.name == self.logger_name:
return self._logger
with _logger_lock:
if self._logger and self._logger.name == self.logger_name:
return self._logger
from flask.logging import create_logger
self._logger = rv = create_logger(self)
return rv
return create_logger(self)
@locked_cached_property
def jinja_env(self):

View file

@ -1,94 +1,69 @@
# -*- coding: utf-8 -*-
"""
flask.logging
~~~~~~~~~~~~~
Implements the logging support for Flask.
:copyright: (c) 2015 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
from __future__ import absolute_import
import logging
import sys
from werkzeug.local import LocalProxy
from logging import getLogger, StreamHandler, Formatter, getLoggerClass, \
DEBUG, ERROR
from .globals import _request_ctx_stack
PROD_LOG_FORMAT = '[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
DEBUG_LOG_FORMAT = (
'-' * 80 + '\n' +
'%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' +
'%(message)s\n' +
'-' * 80
)
from .globals import request
@LocalProxy
def _proxy_stream():
"""Finds the most appropriate error stream for the application. If a
WSGI request is in flight we log to wsgi.errors, otherwise this resolves
to sys.stderr.
def wsgi_errors_stream():
"""Find the most appropriate error stream for the application. If a request
is active, log to ``wsgi.errors``, otherwise use ``sys.stderr``.
If you configure your own :class:`logging.StreamHandler`, you may want to
use this for the stream. If you are using file or dict configuration and
can't import this directly, you can refer to it as
``ext://flask.logging.wsgi_errors_stream``.
"""
ctx = _request_ctx_stack.top
if ctx is not None:
return ctx.request.environ['wsgi.errors']
return sys.stderr
return request.environ['wsgi.errors'] if request else sys.stderr
def _should_log_for(app, mode):
policy = app.config['LOGGER_HANDLER_POLICY']
if policy == mode or policy == 'always':
return True
def has_level_handler(logger):
"""Check if there is a handler in the logging chain that will handle the
given logger's :meth:`effective level <~logging.Logger.getEffectiveLevel>`.
"""
level = logger.getEffectiveLevel()
current = logger
while current:
if any(handler.level <= level for handler in current.handlers):
return True
if not current.propagate:
break
current = current.parent
return False
#: Log messages to :func:`~flask.logging.wsgi_errors_stream` with the format
#: ``[%(asctime)s] %(levelname)s in %(module)s: %(message)s``.
default_handler = logging.StreamHandler(wsgi_errors_stream)
default_handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
))
def create_logger(app):
"""Creates a logger for the given application. This logger works
similar to a regular Python logger but changes the effective logging
level based on the application's debug flag. Furthermore this
function also removes all attached handlers in case there was a
logger with the log name before.
"""Get the ``'flask.app'`` logger and configure it if needed.
When :attr:`~flask.Flask.debug` is enabled, set the logger level to
:data:`logging.DEBUG` if it is not set.
If there is no handler for the logger's effective level, add a
:class:`~logging.StreamHandler` for
:func:`~flask.logging.wsgi_errors_stream` with a basic format.
"""
Logger = getLoggerClass()
logger = logging.getLogger('flask.app')
class DebugLogger(Logger):
def getEffectiveLevel(self):
if self.level == 0 and app.debug:
return DEBUG
return Logger.getEffectiveLevel(self)
if app.debug and logger.level == logging.NOTSET:
logger.setLevel(logging.DEBUG)
class DebugHandler(StreamHandler):
def emit(self, record):
if app.debug and _should_log_for(app, 'debug'):
StreamHandler.emit(self, record)
class ProductionHandler(StreamHandler):
def emit(self, record):
if not app.debug and _should_log_for(app, 'production'):
StreamHandler.emit(self, record)
debug_handler = DebugHandler()
debug_handler.setLevel(DEBUG)
debug_handler.setFormatter(Formatter(DEBUG_LOG_FORMAT))
prod_handler = ProductionHandler(_proxy_stream)
prod_handler.setLevel(ERROR)
prod_handler.setFormatter(Formatter(PROD_LOG_FORMAT))
logger = getLogger(app.logger_name)
# just in case that was not a new logger, get rid of all the handlers
# already attached to it.
del logger.handlers[:]
logger.__class__ = DebugLogger
logger.addHandler(debug_handler)
logger.addHandler(prod_handler)
# Disable propagation by default
logger.propagate = False
if not has_level_handler(logger):
logger.addHandler(default_handler)
return logger