question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Using logging's root logger before app.run() breaks Flask logging

See original GitHub issue

On latest PyPI’s release (1.0.2), using logging’s root logger before running Flask breaks its logging.

For example, with such a run.py file:

import logging
import flask

logging.info("it breaks")
flask.Flask(__name__).run()

python run.py outputs this:

 * Serving Flask app "run" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off

and then stops any kind of logging.

The application is running and answering, but you don’t get the Running on http://... line, nor any request logging.

It’s worth noting that using the root logger inside a route doesn’t break Flask.

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:1
  • Comments:8 (4 by maintainers)

github_iconTop GitHub Comments

2reactions
ghostcommented, Nov 29, 2018

TL;DR: This behavior is caused by flask.logging.has_level_handler() and logging.basicConfig(). Specifically, using the root logger (i.e. calling logging.<function()>) before calling app.logger will add a handler to the root logger and cause flask.logging.has_level_handler() to return True when it should return False.

When you use any function from the logging module such as logging.info('message') or logging.basicConfig(filename='some_log_file'), you work with the root logger. The root logger is a Logger object that sits inside the logging module with a default level of logging.WARNING. The root logger isn’t created with any handlers attached, but when you use a module-level function like those I offered previously (specifically any function that feeds into basicConfig()), they will attach a file handler or a stream handler to the root logger for you.

From the logging module (3.7):

def info(msg, *args, **kwargs):
    ...
    if len(root.handlers) == 0:
        basicConfig()  # logging.debug(), etc... all feed into basicConfig()
   ...

def basicConfig(**kwargs):
    ...
        if len(root.handlers) == 0:
            ...  # left out for brevity
            if handlers is None:
                filename = kwargs.pop("filename", None)  # if you use basicConfig() directly
                mode = kwargs.pop("filemode", 'a')
                if filename:
                    h = FileHandler(filename, mode)
                else:
                    stream = kwargs.pop("stream", None)  # if you use message-emitting functions like info()
                    h = StreamHandler(stream)
            ...  # Some formatting stuff
                root.addHandler(h)  # Adds the stream or file handler to the root logger
            ...

In the scenario laid out by @ramnes, the root logger is used first, will have a default level of warning, and will have a stream handler attached to it. Now if we go to flask.logging.create_logger() (which is where all of the calls to the application-level logger will feed into some time after the call to logging.info()), we see the following in the flask.logging module:

def create_logger(app)
    ...
   logger = logging.getLogger('flask.app')
   
    # This prevents the problem if FLASK_DEBUG is set, but it won't trigger in this example.
    if app.debug and logger.level == logging.NOTSET:
        logger.setLevel(logging.DEBUG)

    if not has_level_handler(logger):                                
        logger.addHandler(default_handler)

    return logger

The primary issue lies with has_level_handler():

def has_level_handler(logger):
    ...
    level = logger.getEffectiveLevel()  # This is the first culprit, it will return 30 (the root logger's level)
    current = logger

    while current:
        if any(handler.level <= level for handler in current.handlers):  # This is the second culprit
            return True

        if not current.propagate:
            break

        current = current.parent  # This is the third culprit

    return False

has_level_handler() has three lines that cause the problem if used after a root logger is configured.

First, in our example, getEffectiveLevel() will return a value of 30 (warning). The application logger begins its life with a level of 0, so getEffectiveLevel() will defer to the nearest level in the logging tree. In our case, that’s the root logger with a level of 30 (the 30 is returned). Next, we enter the loop and check the application logger for any handlers with a level of 30 or below. We don’t find any, so we get the logger’s parent (the root logger) and loop again. This time, we do find a handler: the handler logging attached to the root logger for us. All handlers begin their life with a level of 0, so this incidental handler fits the the conditional if any(handler.level <= level for handler in current.handlers) and has has_level_handler() return True when it should return False. Hopping back to create_logger(), this means that the application logger will never be given the default handler.

Now, whenever the application logger is asked to emit a message, it will have to defer to the nearest handler in the logging tree, which just so happens to be the incidental handler that we attached the root logger when we used it at the very beginning. In our example, this is a stream handler, which happens to default to stderr. Recall that the root logger defaults to a level of warning. This means that any messages with a debug- or info-level messages that the application logger attempts to emit will be suppressed. In our example, warning messages and above will still be output to stderr. If you call basicConfig(filename='file.log'), then warning messages and above will be appended to file.log. Notice that the werkzeug logger will still emit some warning messages if you haven’t touched with it.

There are a couple of potential quick fixes (these won’t solve the problem, only mask it)

  1. Call the application logger (app.logger.<something>) before the root logger so that it uses its own default handler (recommended).
  2. Give the application logger the level and handler (also with a level) you want before using it
  3. Set the root logger’s level to something different
  4. Set the root logger’s file / stream handler to something different
  5. Add more handlers (with levels) to the root logger

For a longer-fix, I would look at patching flask.logging.has_level_handler(), although I’m not sure how because I don’t understand its intentions.

1reaction
ramnescommented, Nov 29, 2018

Whether this behavior is the resultant of an intentional implementation or not is not the point here. I do agree that #2436 make things much better than just removing any existing handler as Flask used to do, but please understand that the current behavior is very developer-hostile in that particular case. No one, beside someone that perfectly knows Flask internals, would consider logging.info("it breaks") as a way of configuring (or just altering) Flask’s logging. And yet, the root logger is mostly used by beginners, and developers that just want to throw some logging somewhere quickly for debugging purposes. I don’t know any library other than Flask which has its logging altered by a simple use of the root logger.

Read more comments on GitHub >

github_iconTop Results From Across the Web

How can I log outside of main Flask module? - Stack Overflow
So if your main module is called 'my_tool', you would want to do logger = logging.getLogger('my_tool') in the Service module. To add onto...
Read more >
Logging — Flask Documentation (2.2.x)
The simplest way to do this is to add handlers to the root logger instead of only the app logger. Depending on your...
Read more >
Getting Started Quickly With Flask Logging - SentinelOne
Rerun the application and make a request. Your log message went to the console. If you don't configure logging yourself, Flask adds a...
Read more >
How to Get Started with Logging in Flask - Better Stack
Learn how to start logging with Flask and go from basics to best practices in no time. ... If the app.logger is accessed...
Read more >
Learn the Working of Flask logging with Examples - eduCBA
1. Logger: This is the entry point of any logging system which provides an interface to the Flask application for logging events for...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found