Skip to content

About

Report which existing loggers logging.config.dictConfig() will silently disable, and warn or refuse before it does.

Topics

Resources

Stars

69 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

keeploggers

Finds which loggers logging.config.dictConfig() (or fileConfig()) is about to silently disable, before the missing log lines get noticed.

import keeploggers

names = keeploggers.would_disable(config)
if names:
    print(f"this config would disable: {names}")

Why

logging.config.dictConfig() defaults disable_existing_loggers to True. Any logger created before the config call — which, for a third-party library, means essentially always, since libraries call logging.getLogger(__name__) at import time, long before an application gets around to configuring logging — comes back with .disabled = True. Its .warning() calls still work syntactically. They emit nothing. Nothing in the standard library reports that this happened.

This is current stdlib behavior, checked against Python 3.14.7's logging/config.py, _handle_existing_loggers, and its two call sites in DictConfigurator.configure and _install_loggers: a minimal config that only touches root disables every other logger that already exists, because disable_existing_loggers was never mentioned and silently defaults to True.

The usual workarounds do not hold up:

  • Setting disable_existing_loggers=False explicitly works, but it is not the default, nothing warns that it matters, and most people find out by noticing a library has gone quiet.
  • Configuring logging before importing anything else does not hold up in a real application. Frameworks (Flask, Django, Celery) configure logging during app startup, after most of the dependency tree is already imported and has already called getLogger.
  • Calling logging.getLogger(name) again after configuring returns the same Logger object. Only a placeholder ancestor gets replaced by a fresh instance on the next getLogger call; an already-existing real logger keeps whatever disable_existing_loggers did to it.

disable_existing_loggers=True is not wrong on its own — reconfiguring logging and clearing out stale loggers on purpose is a real use case. The problem is that it is also the default for a call that most applications make exactly once, and it fires whether or not the caller meant to disable anything.

Measured: python examples/demo.py

--- 1. plain logging.config.dictConfig(), application-only config ---
before configuring: somelib_logger.disabled = False
after configuring:  somelib_logger.disabled = True
calling somelib_logger.warning(...) now:
(nothing printed above this line -- the warning call was silently dropped)

--- 2. keeploggers.would_disable() -- checked before applying anything ---
would_disable(config) -> ['somelib.http']

--- 3. keeploggers.dict_config() -- applies the config, warns about it ---
DisabledLoggersWarning: dictConfig() disabled 1 existing logger(s) because 'disable_existing_loggers' was not set (it defaults to True): somelib.http. Set it explicitly, or pass on_disable='ignore', to silence this.
somelib_logger.warning(...) still emits nothing (the config did disable it --
dict_config warns about the default, it doesn't change what dictConfig does):

--- 4. keeploggers.disabled_loggers() -- the runtime diagnostic ---
disabled_loggers() -> ['somelib.http']

No existing package checks for this against the standard library's logging.config before or after applying a config; the alternatives above are the manual workarounds people use instead.

Installation

pip install keeploggers

Python >= 3.11. Zero runtime dependencies — this only calls into logging and logging.config from the standard library.

Usage

Check before applying a config

import keeploggers

names = keeploggers.would_disable(config)
if names:
    print(f"this config would disable: {names}")

Pure and read-only: it inspects the current logger manager and mirrors the stdlib's own decision (including which loggers are spared as children of an explicitly-configured parent, and the fact that incremental: True configs never disable anything), without calling dictConfig itself.

would_disable_file(fname) does the same for a fileConfig-style .ini file, reading it with configparser the same way fileConfig does.

Apply a config, guarded

import keeploggers

keeploggers.dict_config(config)  # on_disable="warn" by default

If config does not set disable_existing_loggers itself, and applying it would disable an existing logger, this warns (naming the loggers) after applying the config — the config still behaves exactly like logging.config.dictConfig(config) would; the difference is finding out about it. If config does set the key, dict_config applies it and says nothing: that was a deliberate choice, not the default catching the caller.

keeploggers.dict_config(config, on_disable="raise")   # refuse to apply it at all
keeploggers.dict_config(config, on_disable="ignore")  # apply it silently, like dictConfig

Check what's disabled right now

keeploggers.disabled_loggers()
# ['somelib.http']

The "where did my logs go" diagnostic — no config dict needed, just inspects live state. Drop it in a shell, a health-check endpoint, or a breakpoint.

Reference

Name Type Default Description
would_disable(config) function — Names of existing loggers dictConfig(config) would disable. Nothing applied.
would_disable_file(fname, defaults=None, disable_existing_loggers=True, encoding=None) function disable_existing_loggers=True Same, for a fileConfig-style .ini file.
dict_config(config, *, on_disable="warn") function on_disable="warn" dictConfig(config), warning/raising/ignoring per on_disable when the default silently disables loggers. Accepts "warn", "raise", or "ignore".
disabled_loggers() function — Names of loggers that are disabled right now, in the running process.
DisabledLoggersWarning exception (UserWarning) — Raised as a warning by dict_config when applying a config silently disabled loggers. Carries the affected names as .names.
DisabledLoggersError exception (RuntimeError) — Raised by dict_config with on_disable="raise" instead of applying a config that would silently disable existing loggers. Carries the affected names as .names.

How it works

_compute_would_disable ports the existing/child-loggers walk from logging.config._handle_existing_loggers: everything currently in the logger manager gets marked as "would disable" except loggers named directly in the incoming config and their children, which are reset rather than disabled. would_disable and would_disable_file run this walk read-only, without calling dictConfig/fileConfig. dict_config runs the same check, then calls the real dictConfig and reports on what it did per on_disable. Placeholder entries (logging.PlaceHolder, created by the manager for ancestors of a logger that was never itself created) are never reported — nothing can log through one, and the stdlib's own disabling of it has no observable effect.

Limitations

  • It cannot see loggers created after configuration. That is fine — disable_existing_loggers only ever touches loggers that already existed at the moment dictConfig/fileConfig ran, so a logger created afterward was never a candidate for this in the first place.
  • No fileConfig support for exotic ConfigParser input (a live file-like object mid-stream, custom RawConfigParser subclasses handed in pre-parsed) — would_disable_file reads a path, an os.PathLike, or a plain text file object the same way fileConfig does; anything more unusual is not covered.
  • Does not cover third-party config systems. Django's LOGGING setting, for example, is handed to logging.config.dictConfig() internally, so the same defaults and the same would_disable()/dict_config() apply to it — but this package does not know how to reach into Django's settings; pass the LOGGING dict to would_disable() directly.
  • No logging.config.listen() / socket-based config support. Config arriving over the network is not something this looks at.
  • Does not touch disable_existing_loggers itself. dict_config reports and can refuse, but never silently rewrites the config to change what gets disabled — that decision stays with the caller.

Compatibility

Tested locally on Python 3.14.7. pyproject.toml requires Python >= 3.11 and classifies 3.11–3.14; CI (.github/workflows/test.yml) runs the full check suite on Python 3.11, 3.12, 3.13, and 3.14.

Development

python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/python -m pytest -q
.venv/bin/python -m mypy src --strict

Tests exercise the real logging.config.dictConfig/fileConfig calls alongside keeploggers' own predictions, not just the internal model in isolation, and an autouse fixture snapshots and restores all the global logging state (Logger.manager.loggerDict, each logger's disabled/level/handlers/propagate, root's handlers, and the private logging._handlers registry) around every test.

License

MIT — see LICENSE.

About

Report which existing loggers logging.config.dictConfig() will silently disable, and warn or refuse before it does.

Topics

Resources

Stars

69 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages