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 <[email protected]>
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.

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.Changes
Default stderr logging in
get_loggerget_loggernow attaches a_DefaultStderrHandlerinstead of aNullHandler. When the host application has not configured logging, WARNING and above are forwarded to stderr throughlogging.lastResort; INFO and DEBUG stay quiet. The handler stripsexc_info,exc_text, andstack_infoso default stderr output does not dump tracebacks on top of CLI error lines._has_application_handlersand_default_stderr_ownerdecide when the default handler emits and prevent duplicate lines for parent/child RedisVL loggers. Loggers keep default propagation (propagate=True) so once the app adds real handlers (rootbasicConfig, a parent logger handler, and so on), the default handler stops emitting and records propagate normally.CLI failure logging
log_cli_exception()inredisvl/cli/utils.pycallslogger.error(..., exc_info=True)only when_has_application_handlersis true.rvl index,rvl stats, andrvl migrateuse it so unconfigured runs still print a single error line on stderr without a duplicate traceback.Tests
tests/unit/test_log.pyexercises logging in fresh interpreter subprocesses (pytestcaplogwould mask the bug).tests/unit/test_cli_index.pyassertsTracebackis absent from stderr on runtime failures.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
Start here:
redisvl/utils/log.py,_DefaultStderrHandler.emit. It gates unconfigured-app output: when to forward WARNING+ tolastResort, when to strip tracebacks, and when to defer to the host’s logging config.Where it could fail
redisvl/utils/log.py:49:_has_application_handlersmust match real host setups (rootbasicConfig, parent handlers, and so on); a misclassification leaves WARNING+ on stderr when the app also logs, or skips stderr when the app expectedlastResort.redisvl/utils/log.py:37:_default_stderr_ownermust pick one handler in aget_loggerhierarchy; the wrong owner could suppress warnings or print them twice.redisvl/cli/utils.py:15:log_cli_exceptionuses_has_application_handlerson[RedisVL]; misclassification duplicates tracebacks on stderr or skips structured logging.Not verified: Full
make testwith the defaulttests/conftest.pysession fixture (needs Docker). Python versions other than 3.12.3 in this review environment.Beyond CI: On
ba9f127,make lintpassed (black/isort unchanged, mypy clean on 122 files). With--confcutdir=tests/unit(no Docker on the review host),tests/unit/test_log.py— 8 passed;tests/unit/test_cli_index.pytest_index_runtime_errors— 7 passed (15 total). Onorigin/main(31d2d0a), the same subprocess reproduction prints onlyfrom plain loggeron stderr (redisvl warning missing). With the PR’stest_log.pyonmain(import adjusted to omit_DefaultStderrHandler), the three unconfigured-stderr subprocess tests fail; four configured-logging cases pass.Reviewed
ba9f127· Cursor agent (composer-2.5) · lenses: correctness, security, design, tests, docs, Redis behaviour