BUG-HUNT: [resource] configure_structlog() calls logging.basicConfig(force=True) — silently destroys all pre-existing logging handlers including any externally configured file or syslog handlers #6569

Open
opened 2026-04-09 21:29:13 +00:00 by HAL9000 · 1 comment
Owner

Bug Report: [resource] — configure_structlog() Destroys Pre-Existing Logging Handlers

Severity Assessment

  • Impact: Any Python logging handlers configured before configure_structlog() is called — including FileHandler, RotatingFileHandler, SysLogHandler, or custom handlers set up by deployment infrastructure (e.g., logging.config.fileConfig()) — are silently removed and closed by logging.basicConfig(force=True). After the call, only the StreamHandler added by basicConfig exists. Log records that should go to persistent files or syslog are silently dropped.
  • Likelihood: Medium — occurs in any deployment where Python logging is configured at process start (e.g., via PYTHONLOGGING env, logging.config.fileConfig, or any library that adds handlers early) before configure_structlog() is called.
  • Priority: Medium

Location

  • File: src/cleveragents/config/logging.py
  • Function/Class: configure_structlog()
  • Line: 49

Description

configure_structlog() uses force=True in its logging.basicConfig() call:

# src/cleveragents/config/logging.py line 49
logging.basicConfig(format="%(message)s", level=numeric_level, force=True)

From the Python docs: "If force is set to true, any existing handlers attached to the root logger are removed and closed, before carrying out the configuration as specified by the other arguments."

This means every call to configure_structlog() is destructive: it unconditionally wipes the entire root logger handler configuration and replaces it with a single StreamHandler(sys.stderr). The ProcessorFormatter is then only applied to that one new handler:

# Lines 80-81
root_logger = logging.getLogger()
for handler in root_logger.handlers:
    handler.setFormatter(formatter)

This is a two-sided problem:

  1. Calling configure_structlog() after custom handlers are set up destroys them.
  2. Calling configure_structlog() multiple times (e.g., once at startup with WARNING, then again with DEBUG after reading user config) resets and destroys the formatter state from the first call.

The second scenario is real: cli/main.py calls configure_structlog(log_level="WARNING") in multiple code paths (lines 326–328, 372–374, 403–405, 684–686). If a user or framework reconfigures logging between calls, the second configure_structlog() call destroys that configuration.

Evidence

# src/cleveragents/config/logging.py lines 47-81
def configure_structlog(
    *,
    env: str = "development",
    log_level: str = "INFO",
) -> None:
    numeric_level = getattr(logging, log_level.upper(), None)
    if not isinstance(numeric_level, int):
        raise ValueError(f"Invalid log level: {log_level!r}")

    logging.basicConfig(format="%(message)s", level=numeric_level, force=True)  # ← DESTRUCTIVE
    
    # ... configures structlog ...
    
    formatter = structlog.stdlib.ProcessorFormatter(
        processors=[
            structlog.stdlib.ProcessorFormatter.remove_processors_meta,
            renderer,
        ],
    )

    root_logger = logging.getLogger()
    for handler in root_logger.handlers:   # ← Only applies to the ONE handler created by basicConfig
        handler.setFormatter(formatter)

Expected Behavior

configure_structlog() should preserve or integrate with existing logging handlers rather than destroying them. If it must replace the handler configuration, it should document this destructive behavior clearly and/or save/restore existing handlers. Alternatively, the force=True should be removed and replaced with a check: only call basicConfig if there are no existing handlers.

Actual Behavior

Any pre-existing FileHandler, SysLogHandler, or custom handler on the root logger is silently removed and closed by logging.basicConfig(force=True). Subsequent log records are only emitted to the single StreamHandler created by configure_structlog().

Suggested Fix

Option 1 — Only call basicConfig if no handlers exist (non-destructive):

root_logger = logging.getLogger()
if not root_logger.handlers:
    logging.basicConfig(format="%(message)s", level=numeric_level)
else:
    root_logger.setLevel(numeric_level)

# Apply ProcessorFormatter to ALL existing handlers
for handler in root_logger.handlers:
    handler.setFormatter(formatter)

Option 2 — Document the destructive behavior in the docstring and add an overwrite_handlers: bool = True parameter so callers can opt out.

Category

resource

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: bug-hunter

## Bug Report: [resource] — `configure_structlog()` Destroys Pre-Existing Logging Handlers ### Severity Assessment - **Impact**: Any Python `logging` handlers configured before `configure_structlog()` is called — including `FileHandler`, `RotatingFileHandler`, `SysLogHandler`, or custom handlers set up by deployment infrastructure (e.g., `logging.config.fileConfig()`) — are **silently removed and closed** by `logging.basicConfig(force=True)`. After the call, only the `StreamHandler` added by `basicConfig` exists. Log records that should go to persistent files or syslog are silently dropped. - **Likelihood**: Medium — occurs in any deployment where Python logging is configured at process start (e.g., via `PYTHONLOGGING` env, `logging.config.fileConfig`, or any library that adds handlers early) before `configure_structlog()` is called. - **Priority**: Medium ### Location - **File**: `src/cleveragents/config/logging.py` - **Function/Class**: `configure_structlog()` - **Line**: 49 ### Description `configure_structlog()` uses `force=True` in its `logging.basicConfig()` call: ```python # src/cleveragents/config/logging.py line 49 logging.basicConfig(format="%(message)s", level=numeric_level, force=True) ``` From the Python docs: *"If force is set to true, any existing handlers attached to the root logger are removed and closed, before carrying out the configuration as specified by the other arguments."* This means every call to `configure_structlog()` is **destructive**: it unconditionally wipes the entire root logger handler configuration and replaces it with a single `StreamHandler(sys.stderr)`. The ProcessorFormatter is then only applied to that one new handler: ```python # Lines 80-81 root_logger = logging.getLogger() for handler in root_logger.handlers: handler.setFormatter(formatter) ``` This is a two-sided problem: 1. **Calling `configure_structlog()` after custom handlers are set up** destroys them. 2. **Calling `configure_structlog()` multiple times** (e.g., once at startup with `WARNING`, then again with `DEBUG` after reading user config) resets and destroys the formatter state from the first call. The second scenario is real: `cli/main.py` calls `configure_structlog(log_level="WARNING")` in multiple code paths (lines 326–328, 372–374, 403–405, 684–686). If a user or framework reconfigures logging between calls, the second `configure_structlog()` call destroys that configuration. ### Evidence ```python # src/cleveragents/config/logging.py lines 47-81 def configure_structlog( *, env: str = "development", log_level: str = "INFO", ) -> None: numeric_level = getattr(logging, log_level.upper(), None) if not isinstance(numeric_level, int): raise ValueError(f"Invalid log level: {log_level!r}") logging.basicConfig(format="%(message)s", level=numeric_level, force=True) # ← DESTRUCTIVE # ... configures structlog ... formatter = structlog.stdlib.ProcessorFormatter( processors=[ structlog.stdlib.ProcessorFormatter.remove_processors_meta, renderer, ], ) root_logger = logging.getLogger() for handler in root_logger.handlers: # ← Only applies to the ONE handler created by basicConfig handler.setFormatter(formatter) ``` ### Expected Behavior `configure_structlog()` should preserve or integrate with existing logging handlers rather than destroying them. If it must replace the handler configuration, it should document this destructive behavior clearly and/or save/restore existing handlers. Alternatively, the `force=True` should be removed and replaced with a check: only call `basicConfig` if there are no existing handlers. ### Actual Behavior Any pre-existing `FileHandler`, `SysLogHandler`, or custom handler on the root logger is silently removed and closed by `logging.basicConfig(force=True)`. Subsequent log records are only emitted to the single `StreamHandler` created by `configure_structlog()`. ### Suggested Fix Option 1 — Only call `basicConfig` if no handlers exist (non-destructive): ```python root_logger = logging.getLogger() if not root_logger.handlers: logging.basicConfig(format="%(message)s", level=numeric_level) else: root_logger.setLevel(numeric_level) # Apply ProcessorFormatter to ALL existing handlers for handler in root_logger.handlers: handler.setFormatter(formatter) ``` Option 2 — Document the destructive behavior in the docstring and add an `overwrite_handlers: bool = True` parameter so callers can opt out. ### Category resource ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: `@tdd_issue`, `@tdd_issue_<this-issue-number>`, and `@tdd_expected_fail` to prove the bug exists before fixing it. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
HAL9000 added this to the v3.2.0 milestone 2026-04-09 21:31:43 +00:00
Author
Owner

Verified — Valid resource bug. Silently destroys all pre-existing logging handlers including file/syslog handlers. This is a destructive side-effect that can break production logging setups. MoSCoW: Should Have — logging infrastructure integrity is important.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Valid resource bug. Silently destroys all pre-existing logging handlers including file/syslog handlers. This is a destructive side-effect that can break production logging setups. **MoSCoW: Should Have** — logging infrastructure integrity is important. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#6569
No description provided.