Repository navigation
Conversation
get_logger attached a NullHandler, which counts as a handler and stops Python from sending WARNING and above to logging.lastResort. Drop it so an application that has not configured logging still sees RedisVL warnings on stderr. Fixes redis#786
vishal-bala
left a comment
There was a problem hiding this comment.
Hi, please follow the current contribution guidelines and include a "Reviewer Brief" in your description (based on a pre-PR review using the skill included in the repository).
Co-authored-by: Joel Shearon <hippi345@users.noreply.github.com>
Attach a default stderr handler on get_logger loggers so unconfigured apps still see WARNING+ on stderr (redis#786) while exc_info does not dump tracebacks there. CLI exception handlers only log when the host app configured logging.
Stop forcing propagate=False on RedisVL loggers. The default stderr handler now no-ops when the application attached handlers on the root or a parent logger, so INFO/DEBUG and ancestor handlers work again while unconfigured WARNING+ output still reaches stderr without exc tracebacks.
|
good catch, dropped the propagate=False and the default handler now only kicks in when the app hasn't set up its own logging. added tests for basicConfig and a parent logger handler. fixed in da3b4c1. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
Reviewed by Cursor Bugbot for commit da3b4c1. Configure here.
Only the innermost _DefaultStderrHandler for a record emits to lastResort so parent/child get_logger calls do not duplicate warnings. Skip emit when logging.lastResort is None per stdlib semantics.
|
Hi, your reviewer brief doesn't meet the expectations that the repository and the skill set out. If you're using an agent, it should be clear enough for them to run the subagent review panel with the lenses expected. When your PR has gone through the proper preparation, we can have a human reviewer take a look at it 👍 |
|
Hi @vishal-bala — thanks for calling out the earlier brief. I re-ran preparation against head |
| import logging | ||
|
|
||
|
|
||
| class _DefaultStderrHandler(logging.Handler): |
There was a problem hiding this comment.
Could this go back to the fix proposed in #786, which drops the NullHandler and attaches nothing? With no handler on a RedisVL logger, the stdlib's lastResort already does everything this handler re-creates. I measured both on 3.13:
- INFO stays hidden.
- A child warning and a parent warning each print once.
- A root or parent handler takes over once the app configures logging.
- An existing
NullHandlerstill silences the logger.
The plain fix leaves one problem, in the CLI (see the comment on cli/utils.py). Its cause is that each call site both logs and prints the same error, so it's better fixed there. A handler like this one is something every later logging change has to reason about, and it adds costs of its own:
- It can deadlock.
emit()runs while the handler's lock is held, and it callslogging.getLogger(), which takeslogging._lock.dictConfigtakes the two locks in the opposite order: it takeslogging._lockfirst, then acquires every handler's lock in_clearExistingHandlers(). So a RedisVL warning on one thread and adictConfigcall on another can block each other for good. I reproduced it on 3.13.9 by adding a sleep inemit()to widen the window. - Two different unconfigured behaviours. Because the handler strips
exc_info,get_loggerloggers behave differently from the plainlogging.getLoggerloggers inredisvl/mcp/, which keep their tracebacks. - Against stdlib guidance. The Python logging HOWTO tells libraries to add no handler other than
NullHandler, and to leave output to the application.
get_logger would then be:
def get_logger(name, log_level="info", fmt=None):
"""Return a logger instance."""
name = "RedisVL" if log_level == "debug" else name
return logging.getLogger(name)One docs line also needs updating. docs/concepts/queries.md:357 says RedisVL "silently skips" a document whose fields came back empty, while it logs a WARNING. That warning now reaches stderr.
| DEFAULT_REDIS_PORT = 6379 | ||
|
|
||
|
|
||
| def log_cli_exception(exc: BaseException) -> None: |
There was a problem hiding this comment.
log_cli_exception never logs in a real rvl process. Nothing under redisvl/ configures logging, so _has_application_handlers("[RedisVL]") is always False there (measured). It only fires when the CLI classes run inside a process that already has handlers, such as pytest.
Could the three call sites just drop their log call instead?
- In
cli/index.py:86andcli/stats.py:116, deletelogger.error(e, exc_info=True)and keep theprint. - In
cli/migrate.py:77, deletelogger.error(e).
Once the NullHandler is gone, that gives the same single error line this PR prints, without importing a private helper from log.py.
| assert exc_info.value.code == 1 | ||
| assert captured.out == "" | ||
| assert captured.err != "" | ||
| assert "Traceback" not in captured.err |
There was a problem hiding this comment.
This assertion passes on main as well. Under pytest the root logger holds caplog's handlers, so the record never reaches stderr, whatever log_cli_exception does. Could the CLI case be a subprocess test instead? It would run a failing command with logging unconfigured and assert that stderr has one error line and no Traceback.
| ) | ||
|
|
||
|
|
||
| def test_get_logger_attaches_default_stderr_handler(): |
There was a problem hiding this comment.
Once the handler is gone, most of this file can go too. Two subprocess tests would pin #786:
- With logging unconfigured, a WARNING reaches stderr and an INFO doesn't.
- After
basicConfig(), the warning appears exactly once.
The tests that import _DefaultStderrHandler or count handlers check implementation details rather than behaviour.
Remove the custom _DefaultStderrHandler stack and return bare loggers from get_logger so WARNING+ reaches lastResort when the app has not configured logging. Stop duplicating CLI failures by removing logger calls where stderr already prints the error; add subprocess tests for redis#786 and for rvl index stderr shape.
|
@vishal-bala I dropped the custom handler stack and went back to the bare |

Motivation
Fixes #786.
RedisVL warnings never reach an application that has not configured logging, including warnings that signal lost data.
get_loggerattached alogging.NullHandlerwhenever a logger had no handlers. On Python 3.2+, a WARNING record with no handler in the hierarchy normally goes tologging.lastResort(stderr), but aNullHandlercounts as a handler, so the record was dropped. Pytest’scaploginstalls its own handler, which is why tests onmaindid not catch this.Notes
Loggers that already had a
NullHandlerattached earlier in the same process keep it; this change does not remove third-party handlers.Release Notes
RedisVL warnings and errors now show on stderr when the application has not configured logging.
Reviewer Brief
What changed and why
Fixes #786 by removing
NullHandlerfromget_loggerand not attaching any custom handler. With no handler on the logger, Python’slastResortprints WARNING and above to stderr when the application has not configured logging—the behaviour the stdlib documents for libraries.The earlier
_DefaultStderrHandlerstack is gone (per maintainer review): it reimplementedlastResort, risked lock-order deadlocks withdictConfig, and diverged from plainlogging.getLoggercall sites. CLI failure output is fixed at the call site:rvl index,rvl stats, andrvl migrateno longer calllogger.error(..., exc_info=True)where they alreadyprintthe same error, so stderr stays a single line without a traceback.Docs:
docs/concepts/queries.mdno longer says RedisVL “silently skips” empty field payloads; the WARNING is visible on stderr when logging is unconfigured.Files to focus on
redisvl/utils/log.pyredisvl/cli/index.py,stats.py,migrate.pyprintonly.tests/unit/test_log.pybasicConfig).tests/unit/test_cli_index.pytest_index_failure_stderr_subprocess_has_no_tracebackpins CLI stderr shape.docs/concepts/queries.mdHow to test
Manual sanity (Python 3.11+): in a fresh interpreter with no
logging.basicConfig,from redisvl.utils.log import get_logger; get_logger("redisvl.demo").warning("hi")should print one line on stderr;.info(...)should not. AfterbasicConfig(level=logging.WARNING), the same warning should appear once on the configured stream.Risk
NullHandlerin the same process stay silenced (documented in PR Notes).exc_info=Trueon an unconfigured app will show a traceback vialastResort(stdlib); MCP modules already use plainget_logger—consistent now.print). Acceptable forrvl; embedders can wrap the CLI if they need both.Start here:
redisvl/utils/log.py,get_logger. Confirm it only resolves the debug name alias and returnslogging.getLogger(name)with no handler attached.Where it could fail
redisvl/cli/index.py:83(and the matching handlers instats.py/migrate.py): a future change that addslogger.error(..., exc_info=True)back alongsideprintwould restore duplicate tracebacks on unconfigured runs.docs/concepts/queries.md:357: user docs must stay aligned with WARNING reaching stderr when logging is not configured.Not verified: Full
make testin an environment without Docker (sessionredis_containerfixture). Cluster mode and Python versions outside 3.12.3 in the review environment.Beyond CI:
make lintpassed (black/isort unchanged after format, mypy clean on 122 files). Subprocess regression tests:tests/unit/test_log.py— 2 passed;test_index_failure_stderr_subprocess_has_no_traceback— passed; all 7test_index_runtime_errorsparametrized cases — passed (--confcutdir=tests/unitwhere Redis fixtures are unavailable). Onorigin/main, the unconfigured-stderr subprocess reproduction omits the RedisVL warning line; those twotest_log.pycases fail there.Since the last brief:
4264785— replaced_DefaultStderrHandler/log_cli_exceptionwith the #786-style bare logger and CLI print-only errors; trimmed tests and updated queries doc per review.Reviewed
4264785· Cursor agent (composer-2.5) · lenses: correctness, security, design, tests, docs, Redis behaviour