Summary
zenml.logger.init_logging() is currently additive rather than idempotent. If it runs more than once in the same process, it appends a fresh console handler and a fresh ZenMLLoggingHandler to the root logger each time.
That causes duplicate terminal output and duplicate log-routing behavior.
Why this matters
We hit this while investigating duplicated lifecycle logs in a downstream project (kitaru), but the ZenML-side bug is reproducible directly in ZenML without any downstream code changes:
- first
init_logging() call installs the normal root handlers
- a later
init_logging() call adds another console handler and another ZenMLLoggingHandler
- the same log record then gets processed multiple times
In our downstream case, that reintroduced raw ZenML console output after a custom terminal handler had already replaced the original console handler.
Minimal reproduction
import logging
import zenml
from zenml.logger import init_logging
root = logging.getLogger()
print("after import zenml:", [type(h).__name__ for h in root.handlers])
logging.getLogger("zenml.test").info("Step `demo` has started.")
init_logging()
print("after second init_logging():", [type(h).__name__ for h in root.handlers])
logging.getLogger("zenml.test").info("Step `demo` has started.")
Observed behavior
After the second init_logging() call, the root logger contains extra handlers.
In our local investigation, the handler state looked like this:
Before second call:
- one console-equivalent terminal output path
- one
ZenMLLoggingHandler
After second call:
- existing terminal output handler
- existing
ZenMLLoggingHandler
- new
logging.StreamHandler with ConsoleFormatter
- new
ZenMLLoggingHandler
This comes from zenml/src/zenml/logger.py:init_logging() unconditionally doing:
root_logger.addHandler(get_console_handler())
root_logger.addHandler(get_zenml_handler())
Expected behavior
init_logging() should be safe to call multiple times in the same process.
At minimum, it should not keep appending duplicate root handlers on re-entry.
Likely fix
Make zenml.logger.init_logging() idempotent, e.g. by:
- guarding initialization with a module-level flag, or
- deduplicating/removing existing ZenML console + storage handlers before re-adding them
I think the cleanest ownership boundary is to fix this in zenml/src/zenml/logger.py rather than expecting downstream projects to repair root logger state after ZenML re-initializes.
Extra note
During the same investigation we also found a separate downstream Kitaru bug around reload-sensitive handler deduplication, but that is distinct from this ZenML issue. This issue is specifically about ZenML re-adding root handlers on repeated init_logging() calls.
Summary
zenml.logger.init_logging()is currently additive rather than idempotent. If it runs more than once in the same process, it appends a fresh console handler and a freshZenMLLoggingHandlerto the root logger each time.That causes duplicate terminal output and duplicate log-routing behavior.
Why this matters
We hit this while investigating duplicated lifecycle logs in a downstream project (
kitaru), but the ZenML-side bug is reproducible directly in ZenML without any downstream code changes:init_logging()call installs the normal root handlersinit_logging()call adds another console handler and anotherZenMLLoggingHandlerIn our downstream case, that reintroduced raw ZenML console output after a custom terminal handler had already replaced the original console handler.
Minimal reproduction
Observed behavior
After the second
init_logging()call, the root logger contains extra handlers.In our local investigation, the handler state looked like this:
Before second call:
ZenMLLoggingHandlerAfter second call:
ZenMLLoggingHandlerlogging.StreamHandlerwithConsoleFormatterZenMLLoggingHandlerThis comes from
zenml/src/zenml/logger.py:init_logging()unconditionally doing:Expected behavior
init_logging()should be safe to call multiple times in the same process.At minimum, it should not keep appending duplicate root handlers on re-entry.
Likely fix
Make
zenml.logger.init_logging()idempotent, e.g. by:I think the cleanest ownership boundary is to fix this in
zenml/src/zenml/logger.pyrather than expecting downstream projects to repair root logger state after ZenML re-initializes.Extra note
During the same investigation we also found a separate downstream Kitaru bug around reload-sensitive handler deduplication, but that is distinct from this ZenML issue. This issue is specifically about ZenML re-adding root handlers on repeated
init_logging()calls.