X Tutup
The Wayback Machine - https://web.archive.org/web/20241209162955/https://github.com/python/cpython/issues/120785
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

logging.config: configuring root through dictConfig won't update already existing loggers #120785

Closed
bgartzi opened this issue Jun 20, 2024 · 5 comments
Labels

Comments

@bgartzi
Copy link

bgartzi commented Jun 20, 2024

Bug report

Bug description:

I found an issue when the root logger is configured after some loggers were initialized (not necessarily configured).

Experiment

I wrote a small reproducer of the issue. There are 4 functions in the file. All of them initialize some loggers, configure some other loggers and finally log some messages. What it is different from each other is:

  • The order in which loggers are initialized and configured.
  • Which is the logger being configured.

The reproducer:

import sys
import logging
from logging.config import dictConfig


LOGCFG = {
    'version': 1,
    'formatters': {
        'std': {
            'format': '[%(levelname)s  - %(asctime)s - %(name)s::] %(message)s'
        },
    },
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'formatter': 'std',
            'class': 'logging.StreamHandler',
            'stream': 'ext://sys.stdout'
        },
    },
    'root': {
        'level':'DEBUG',
        'handlers': ['console'],
    },
}


def cfg_logger(logger):
    return {
        'version': 1,
        'formatters': {
            'std': {
                'format': f"{logger.upper()}[%(levelname)s  - %(asctime)s - %(name)s::] %(message)s"
            },
        },
        'handlers': {
            'console': {
                'level': 'DEBUG',
                'formatter': 'std',
                'class': 'logging.StreamHandler',
                'stream': 'ext://sys.stdout'
            },
        },
        'loggers': {
            logger: {
                'level':'DEBUG',
                'handlers': ['console'],
            },
        },
    }


def configure_root_then_init_loggers():
    dictConfig(LOGCFG)
    foo = logging.getLogger('foo')
    foobar = logging.getLogger('foo.bar')
    foo.error('whot')
    foobar.error('foobar')

def configure_foo_then_init_loggers():
    dictConfig(cfg_logger('foo'))
    foo = logging.getLogger('foo')
    foobar = logging.getLogger('foo.bar')
    foo.error('whot')
    foobar.error('foobar')

def init_loggers_then_config_foo():
    foo = logging.getLogger('foo')
    foobar = logging.getLogger('foo.bar')
    dictConfig(cfg_logger('foo'))
    foo.error('whot')
    foobar.error('foobar')

def init_loggers_then_config_root():
    foo = logging.getLogger('foo')
    foobar = logging.getLogger('foo.bar')
    dictConfig(LOGCFG)
    foo.error('whot')
    foobar.error('foobar')

def init_loggers_then_config_foobar_then_foo():
    foo = logging.getLogger('foo')
    foobar = logging.getLogger('foo.bar')
    dictConfig(cfg_logger('foo.bar'))
    foo.error('whot')
    foobar.error('foobar')
    dictConfig(cfg_logger('foo'))
    foo.error('whot')
    foobar.error('foobar')


def main():
    funcs = [
        configure_foo_then_init_loggers,
        configure_root_then_init_loggers,
        init_loggers_then_config_foo,
        init_loggers_then_config_root,
        init_loggers_then_config_foobar_then_foo,
    ]
    func = funcs[int(sys.argv[1])]
    print(f"Running: {func.__name__}\n{'.'*60}")
    func()
    print(f"\n\n{'_'*60}")


if __name__ == "__main__":
    main()

Results

And the results for each of the analysed cases:

$ python3.12 reproducer.py 0
Running: configure_foo_then_init_loggers
............................................................
FOO[ERROR  - 2024-06-19 18:39:48,426 - foo::] whot
FOO[ERROR  - 2024-06-19 18:39:48,426 - foo.bar::] foobar


____________________________________________________________
$ python3.12 reproducer.py 1
Running: configure_root_then_init_loggers
............................................................
[ERROR  - 2024-06-19 18:39:49,757 - foo::] whot
[ERROR  - 2024-06-19 18:39:49,758 - foo.bar::] foobar


____________________________________________________________
$ python3.12 reproducer.py 2
Running: init_loggers_then_config_foo
............................................................
FOO[ERROR  - 2024-06-19 18:39:52,000 - foo::] whot
FOO[ERROR  - 2024-06-19 18:39:52,000 - foo.bar::] foobar


____________________________________________________________
$ python3.12 reproducer.py 3
Running: init_loggers_then_config_root
............................................................


____________________________________________________________
$ python3.12 reproducer.py 4
Running: init_loggers_then_config_foobar_then_foo
............................................................
FOO.BAR[ERROR  - 2024-06-20 08:29:57,727 - foo.bar::] foobar
FOO[ERROR  - 2024-06-20 08:29:57,728 - foo::] whot
FOO[ERROR  - 2024-06-20 08:29:57,728 - foo.bar::] foobar


____________________________________________________________

Case 0 configure_foo_then_init_loggers

First the foo logger is configured using logging.config.dictConfig. Then, instances for foo and its child logger foo.bar are created via getLogger.

In simple terms, as foo was configured, IIUC, foo.bar inherits its parent configuration. Then, both logs are put into the stdout.

Case 1 configure_root_then_init_loggers

First the root logger is configured using logging.config.dictConfig. Then, instances for foo and its child logger foo.bar are created via getLogger.

In simple terms, as the root logger was configured, both foo and foo.bar inherit the configuration from its parent configuration, which this time is root.

Case 2 init_loggers_then_config_foo

Now, the logger instances are first initialized for both foo and foo.bar. Then, only the parent foo logger is configured by using logging.config.dictConfig.

During foo's configuration its already existing children (foo.bar) will be also treated. IIUC, that will happen approximately in https://github.com/python/cpython/blob/v3.12.3/Lib/logging/config.py#L653-L654, as foo.bar is in the existing loggers list as well as in the child_loggers list.

The output (written into the stdout), will be the same as case 0.

Case 3 init_loggers_then_config_root

In this case, foo and foo.bar loggers are initialized first, but not configured, as in case 2. Then, the root logger is configured by means of logging.config.dictConfig.

In this specific case, the already existing loggers (foo and foo.bar) won't be processed in _handle_existing_loggers, as they exist but are not in the child_loggers list.

There won't be any log written to the stdout.

Case 4 init_loggers_then_config_foobar_then_foo

Last case: foo and foo.bar are initialized. Then, foo.bar is configured. It's also checked that it works as expected. Right after that, foo is configured, which also affects its already existing and configured child foo.bar.

After that, logs from foo.bar will follow the configuration of foo.

Note about python3.9 and case 4

Case 4 doesn't report the same result for python3.9. From python3.10 on, the result is the same as the one reported above. For python3.9, though, will be the following:

$ python3.9 report.py 4
Running: init_loggers_then_config_foobar_then_foo
............................................................
FOO.BAR[ERROR  - 2024-06-20 10:28:19,770 - foo.bar::] foobar
FOO[ERROR  - 2024-06-20 10:28:19,770 - foo.bar::] foobar


____________________________________________________________

But that's another topic.

Expected results

All the cases seem to follow the same behavior except for case 3. When a logger is configured, all of its children will follow the last applied configuration.

However, configuring the root logger by dictConfig won't apply that configuration over the rest of loggers. This is IMHO, a behavior conflict, compared to the expected one from configuring any other logger. Isn't root the parent of all the rest of loggers after all?

CPython versions tested on:

3.9, 3.10, 3.11, 3.12

Operating systems tested on:

Linux

@bgartzi bgartzi added the type-bug An unexpected behavior, bug, or error label Jun 20, 2024
@bgartzi
Copy link
Author

bgartzi commented Jun 20, 2024

After some further research it seems this behavior is caused by the way the child_loggers and existing loggers list are generated before calling _handle_existing_loggers in [0].

Far from being a proper fix, a small workaround like the following one helps changing such behavior (applicable on v3.12.2):

diff --git a/Lib/logging/config.py b/Lib/logging/config.py
index 33417b75d5..349fd6ded4 100644
--- a/Lib/logging/config.py
+++ b/Lib/logging/config.py
@@ -650,6 +650,8 @@ def configure(self):
                 #        logger.propagate = True
                 #    elif disable_existing:
                 #        logger.disabled = True
+                if 'root' in config:
+                    child_loggers = existing
                 _handle_existing_loggers(existing, child_loggers,
                                          disable_existing)

That way, loggers present in existing and not in child_loggers are not marked as disabled by _handle_existing_loggers.

Again, I'm not sure if this behavior was intended or not. It's just that the behavior I found on logger configuration didn't feel quite standard across the different cases to me.

[0] https://github.com/python/cpython/blob/v3.12.3/Lib/logging/config.py#L653-L654

@vsajip vsajip added the pending The issue will be closed if no feedback is provided label Jun 28, 2024
@vsajip
Copy link
Member

vsajip commented Jun 28, 2024

You are normally supposed to set disable_existing_loggers: False in configuration dictionaries. Adding that line to your two configuration dicts, I get this:

$ python gh_120785.py 3
Running: init_loggers_then_config_root
............................................................
[ERROR  - 2024-06-28 19:01:47,444 - foo::] whot
[ERROR  - 2024-06-28 19:01:47,444 - foo.bar::] foobar

This behaviour is documented here (see the warning section):

The dictionary passed to dictConfig() can also specify a Boolean value with key disable_existing_loggers, which if not specified explicitly in the dictionary also defaults to being interpreted as True. This leads to the logger-disabling behaviour described above, which may not be what you want - in which case, provide the key explicitly with a value of False.

The default value of True is for backward compatibility.

@vsajip vsajip added invalid and removed type-bug An unexpected behavior, bug, or error pending The issue will be closed if no feedback is provided labels Jul 6, 2024
@vsajip
Copy link
Member

vsajip commented Jul 6, 2024

Closing, as no further feedback received.

@bgartzi
Copy link
Author

bgartzi commented Jul 12, 2024

Hi @vsajip,

Sorry it took me so long you finally had to close the issue.

Also sorry for opening an issue on an already documented behavior. I didn't notice that part before.

I needed some time to understand the real meaning of the statement, though. It didn't seem quite straightforward at first sight. I assumed that root would be the ancestor of all loggers, as it's the root of the hierarchy. I think that the key statement then it's that ...any non-root loggers existing before the call to be disabled unless they....

I guess that what led me to misunderstanding the statement above was that the root logger wasn't considered an ancestor of the rest of logger in this case. Is there any documentation that could help me understand in which cases is the root logger different to other loggers?

Thanks a lot!

@vsajip
Copy link
Member

vsajip commented Jul 12, 2024

The root logger is the ancestor of all loggers, so there's nothing wrong with that assumption. All other loggers are non-root loggers (clearly, I hope) so any that exist before the call are disabled unless the disable_existing_loggers flag is set to False or

unless they or their ancestors are explicitly named in the logging configuration.

as per the documentation.

Is there any documentation that could help me understand

No other documentation I can particularly point you to - as far as I can see, the documentation paragraph from which the above snippet is taken (and anywhere else in the official docs that disable_existing_loggers is mentioned) is reasonably clear. The root logger is different to other loggers in implementation details, but that shouldn't normally affect how it's used in practice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Development

No branches or pull requests

2 participants
X Tutup