Skip to content

fix(log): show warnings when logging is not configured - #791

Open
hippi345 wants to merge 6 commits into
redis:mainfrom
hippi345:fix-786
Open

hippi345 wants to merge 6 commits into
redis:mainfrom
hippi345:fix-786

Conversation

@hippi345

@hippi345 hippi345 commented Oct 27, 2026 •

Copy link
Copy Markdown

Motivation

Fixes #786.

RedisVL warnings never reach an application that has not configured logging, including warnings that signal lost data.

get_logger attached a logging.NullHandler whenever a logger had no handlers. On Python 3.2+, a WARNING record with no handler in the hierarchy normally goes to logging.lastResort (stderr), but a NullHandler counts as a handler, so the record was dropped. Pytest’s caplog installs its own handler, which is why tests on main did not catch this.

Notes

Loggers that already had a NullHandler attached 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 NullHandler from get_logger and not attaching any custom handler. With no handler on the logger, Python’s lastResort prints WARNING and above to stderr when the application has not configured logging—the behaviour the stdlib documents for libraries.

The earlier _DefaultStderrHandler stack is gone (per maintainer review): it reimplemented lastResort, risked lock-order deadlocks with dictConfig, and diverged from plain logging.getLogger call sites. CLI failure output is fixed at the call site: rvl index, rvl stats, and rvl migrate no longer call logger.error(..., exc_info=True) where they already print the same error, so stderr stays a single line without a traceback.

Docs: docs/concepts/queries.md no longer says RedisVL “silently skips” empty field payloads; the WARNING is visible on stderr when logging is unconfigured.

Files to focus on

File Why
redisvl/utils/log.py Entire behavioural change; should be ~10 lines.
redisvl/cli/index.py, stats.py, migrate.py Exception handlers: stderr print only.
tests/unit/test_log.py Two subprocess tests pin #786 (unconfigured vs basicConfig).
tests/unit/test_cli_index.py test_index_failure_stderr_subprocess_has_no_traceback pins CLI stderr shape.
docs/concepts/queries.md User-visible wording for WARNING on stderr.

How to test

make lint
make test
# Minimum focused run:
pytest tests/unit/test_log.py \
  tests/unit/test_cli_index.py::test_index_failure_stderr_subprocess_has_no_traceback -v

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. After basicConfig(level=logging.WARNING), the same warning should appear once on the configured stream.

Risk

  • Low: Loggers that already have a third-party NullHandler in the same process stay silenced (documented in PR Notes).
  • Low: Library code that logs exc_info=True on an unconfigured app will show a traceback via lastResort (stdlib); MCP modules already use plain get_logger—consistent now.
  • CLI: No structured log on failure when the host app has configured logging (only stderr print). Acceptable for rvl; 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 returns logging.getLogger(name) with no handler attached.

Where it could fail

  • redisvl/cli/index.py:83 (and the matching handlers in stats.py / migrate.py): a future change that adds logger.error(..., exc_info=True) back alongside print would 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 test in an environment without Docker (session redis_container fixture). Cluster mode and Python versions outside 3.12.3 in the review environment.

Beyond CI: make lint passed (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 7 test_index_runtime_errors parametrized cases — passed (--confcutdir=tests/unit where Redis fixtures are unavailable). On origin/main, the unconfigured-stderr subprocess reproduction omits the RedisVL warning line; those two test_log.py cases fail there.

Since the last brief: 4264785 — replaced _DefaultStderrHandler / log_cli_exception with 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

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

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread redisvl/utils/log.py Resolved

@vishal-bala vishal-bala left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@vishal-bala vishal-bala self-assigned this Oct 30, 2026
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.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread redisvl/utils/log.py Resolved
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.
@hippi345

Copy link
Copy Markdown
Author

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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit da3b4c1. Configure here.

Comment thread redisvl/utils/log.py Resolved
Comment thread redisvl/utils/log.py Resolved
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.
@vishal-bala

Copy link
Copy Markdown
Collaborator

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 👍

@hippi345

Copy link
Copy Markdown
Author

Hi @vishal-bala — thanks for calling out the earlier brief. I re-ran preparation against head ba9f127: full pre-PR review, make lint, and the touched unit tests. Lint and mypy are clean; all 8 tests in tests/unit/test_log.py and all 7 test_index_runtime_errors cases pass locally (--confcutdir=tests/unit here because this environment has no Docker; CI still uses the normal conftest path). I also checked origin/main: the subprocess reproduction omits the RedisVL warning, and the new subprocess tests fail there on the three unconfigured-stderr cases. The PR description now follows the template with an updated Reviewer Brief. Ready for a human pass when you are.

Comment thread redisvl/utils/log.py Outdated
import logging


class _DefaultStderrHandler(logging.Handler):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 NullHandler still 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 calls logging.getLogger(), which takes logging._lock. dictConfig takes the two locks in the opposite order: it takes logging._lock first, then acquires every handler's lock in _clearExistingHandlers(). So a RedisVL warning on one thread and a dictConfig call on another can block each other for good. I reproduced it on 3.13.9 by adding a sleep in emit() to widen the window.
  • Two different unconfigured behaviours. Because the handler strips exc_info, get_logger loggers behave differently from the plain logging.getLogger loggers in redisvl/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.

Comment thread redisvl/cli/utils.py Outdated
DEFAULT_REDIS_PORT = 6379


def log_cli_exception(exc: BaseException) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:86 and cli/stats.py:116, delete logger.error(e, exc_info=True) and keep the print.
  • In cli/migrate.py:77, delete logger.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.

Comment thread tests/unit/test_cli_index.py Outdated
assert exc_info.value.code == 1
assert captured.out == ""
assert captured.err != ""
assert "Traceback" not in captured.err

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/unit/test_log.py Outdated
)


def test_get_logger_attaches_default_stderr_handler():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@hippi345

Copy link
Copy Markdown
Author

@vishal-bala I dropped the custom handler stack and went back to the bare get_logger (no NullHandler), removed the extra logger.error calls in the CLI where we already print to stderr, trimmed test_log.py to the two subprocess cases you suggested, added a subprocess test for rvl index instead of the caplog traceback check, and fixed the queries doc line (4264785). The Reviewer Brief in the description is updated. make lint is clean and the new log/CLI tests pass locally.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RedisVL warnings are never shown when the application has not configured logging

2 participants