Skip to content

init_logging() re-adds root handlers on re-entry, causing duplicate output #4683

@strickvl

Description

@strickvl

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.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingcore-teamIssues that are being handled by the core teamplannedPlanned for the short term

Type

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions