Skip to content

RSPEED-2437: Add configurable log level and unify logging#1148

Merged
tisnik merged 5 commits intolightspeed-core:mainfrom
major:mhayden/configurable-log-level
Feb 17, 2026
Merged

RSPEED-2437: Add configurable log level and unify logging#1148
tisnik merged 5 commits intolightspeed-core:mainfrom
major:mhayden/configurable-log-level

Conversation

@major
Copy link
Contributor

@major major commented Feb 13, 2026

Description

Adds configurable log level via LIGHTSPEED_STACK_LOG_LEVEL environment variable and centralizes all logging through a single get_logger() function in src/log.py.

Commits

  1. feat(logging) — Introduces src/log.py with get_logger() that reads log level from the env var (defaults to INFO), validates the value with a warning on invalid input, and configures RichHandler for consistent console output. The --verbose CLI flag overrides the env var. Moves import sys to top-level in lightspeed_stack.py.

  2. refactor(logging) — Mechanical refactor replacing all logging.getLogger() calls with get_logger(__name__) across 52 modules. No logic or behavioral changes — every file gets the same two-line diff. Reviewable via --stat and spot-checking. Uvicorn logging (src/runners/uvicorn.py) is intentionally excluded.

  3. docs: a2a_protocol.md — Updates stale logger name reference from app.endpoints.handlers to app.endpoints.health.

  4. docs: CLAUDE.md — Updates dev guide logging pattern from logging.getLogger(__name__) to get_logger(__name__).

Type of change

  • Refactor
  • New feature

Tools used to create PR

  • Assisted-by: Claude (code review, fixup commits)
  • Generated by: N/A

Related Tickets & Documents

Checklist before requesting a review

  • I have performed a self-review of my code.
  • PR has passed all pre-merge test jobs.
  • If it is a core feature, I have added thorough tests.

Testing

  • Set LIGHTSPEED_STACK_LOG_LEVEL=DEBUG and verify all module loggers output at DEBUG level
  • Set LIGHTSPEED_STACK_LOG_LEVEL=INVALID and verify a warning is printed with fallback to INFO
  • Run with --verbose flag and verify application loggers switch to DEBUG
  • Verify uv run make verify passes (all linters/type checks clean)

Summary by CodeRabbit

  • New Features

    • Environment-driven log level via LIGHTSPEED_STACK_LOG_LEVEL (fallback INFO)
    • Added --verbose to enable DEBUG across the app
    • Centralized logger setup for consistent logger naming and behavior
  • Documentation

    • Updated logging guidance to use the centralized logger
  • Tests

    • Added tests for environment-driven log level mapping and fallback behavior

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 13, 2026

Walkthrough

Replaces per-module use of Python's stdlib logging with a centralized get_logger() factory, adds environment-driven log level constants and validation, introduces verbose/DEBUG propagation at runtime, updates core log configuration in src/log.py, and adds tests for the new logging behavior.

Changes

Cohort / File(s) Summary
Logger import migration
src/a2a_storage/..., src/app/endpoints/..., src/authentication/..., src/authorization/..., src/utils/..., src/client.py, src/configuration.py, src/llama_stack_configuration.py, src/models/config.py, src/observability/splunk.py, src/app/database.py, src/app/main.py
Replaced import logging + logging.getLogger(...) with from log import get_logger and logger = get_logger(__name__). Changes are import/initialization-only; no control-flow or public API edits.
Cache logger name updates
src/cache/cache_factory.py, src/cache/in_memory_cache.py, src/cache/noop_cache.py, src/cache/postgres_cache.py, src/cache/sqlite_cache.py
Switched hard-coded logger names to get_logger(__name__), aligning logger identity to module path; behavior unchanged.
Log level & core logger
src/constants.py, src/log.py
Added LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR and DEFAULT_LOG_LEVEL. get_logger() now reads/validates env var, sets logger level accordingly, avoids reconfiguration if a RichHandler exists, and disables propagation.
Runtime verbose handling
src/lightspeed_stack.py
Integrates env-driven log level into logging.basicConfig (uses force=True), validates level, prints warnings to stderr, and adds --verbose handling to set DEBUG and escalate existing loggers.
Tests
tests/unit/test_log.py, tests/unit/authorization/test_azure_token_manager.py
Added tests for env-driven log level parsing, invalid-value fallback, default level; adjusted Azure token manager test to control logger propagation for assertions.
Docs / misc
CLAUDE.md, docs/a2a_protocol.md
Updated documentation/examples to reference get_logger(__name__) and adjusted troubleshooting logger reference.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

ok-to-test

Suggested reviewers

  • tisnik
  • asimurka
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: adding configurable log level support and unifying logging across the codebase through a centralized get_logger() function.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/endpoints/health.py (1)

18-29: ⚠️ Potential issue | 🟡 Minor

Update documentation reference to new endpoint logger names.

The logger name change from shared "app.endpoints.handlers" to per-module names (e.g., "app.endpoints.health") is reflected in code, but documentation in docs/a2a_protocol.md still references the old logger name. Update this reference to ensure monitoring and log filtering guidance remains accurate for users checking endpoint logs.

No Python code actively references the old logger name, so the change is localized to configuration and documentation updates.

🧹 Nitpick comments (4)
src/utils/token_counter.py (1)

4-6: Coding guideline deviation: get_logger(__name__) replaces the mandated logging.getLogger(__name__) pattern.

The project coding guidelines specify: "Use logger = logging.getLogger(__name__) pattern for module logging". This PR intentionally replaces that pattern across ~45 modules. The new get_logger wrapper internally delegates to logging.getLogger(name), so it's functionally compatible, but the guideline text should be updated to reflect the new convention (e.g., in CLAUDE.md or equivalent) to avoid confusion in future reviews.

This applies to all 8 files in this PR and the ~35 other modules mentioned in the commit description.

As per coding guidelines: Use logger = logging.getLogger(__name__) pattern for module logging.

tests/unit/authorization/test_azure_token_manager.py (1)

139-147: Correct workaround for propagate=False, but consider extracting a reusable fixture.

The try/finally to toggle propagate is necessary because get_logger disables propagation (which prevents caplog from capturing log records). This is correct, but if other tests face the same issue, a shared pytest fixture (or context manager) that temporarily enables propagation would reduce boilerplate and risk of copy-paste errors.

♻️ Optional: reusable context manager
# conftest.py or a test utility module
from contextlib import contextmanager
import logging

`@contextmanager`
def propagate_logger(name: str):
    """Temporarily enable propagation on a logger so caplog can capture records."""
    log = logging.getLogger(name)
    log.propagate = True
    try:
        yield log
    finally:
        log.propagate = False

Usage:

with propagate_logger("authorization.azure_token_manager"):
    with caplog.at_level("WARNING"):
        result = token_manager.refresh_token()
        assert result is False
        assert "Failed to retrieve Azure access token" in caplog.text
src/lightspeed_stack.py (2)

22-34: Duplicate log-level validation logic with src/log.py.

Lines 22–32 replicate the same env-var read → getattr validate → fallback flow that get_logger() in src/log.py (lines 32–42) already performs. If the validation rules ever diverge (e.g., supporting custom level names), you'd need to update both places.

Consider extracting a shared helper (e.g., resolve_log_level() -> int) in log.py that both call sites can use.


26-31: Move import sys to the top of the file.

Placing a stdlib import inside a conditional block at module level is unconventional. Since this code runs unconditionally at import time (module-level if), sys should be with the other imports at the top.

Proposed fix
 import logging
 import os
+import sys
 from argparse import ArgumentParser

Then at line 26–27:

-    import sys
-
     print(

@major major force-pushed the mhayden/configurable-log-level branch 5 times, most recently from 47b87fe to fb4f58e Compare February 16, 2026 15:30
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@docs/a2a_protocol.md`:
- Line 774: The docs incorrectly instruct readers to check the
`app.endpoints.health` logger; update the A2A troubleshooting Debug Logging
section to reference the actual A2A logger created by get_logger(__name__)
(i.e., `app.endpoints.a2a`) instead of `app.endpoints.health`, and ensure any
log examples or grep commands use `app.endpoints.a2a` so they match the logger
name used by the A2A endpoint implementation.
🧹 Nitpick comments (2)
tests/unit/authorization/test_azure_token_manager.py (1)

139-147: Reasonable workaround for propagate=False, minor fragility note.

The hardcoded logger name "authorization.azure_token_manager" will silently break if the module is ever renamed or moved. Consider importing the module and using its __name__ to keep the test in sync:

from authorization import azure_token_manager
azure_logger = logging.getLogger(azure_token_manager.__name__)

That said, the try/finally to restore propagation state is well done.

src/log.py (1)

35-45: Warning for invalid log level is emitted before the RichHandler is attached.

On lines 37-41, logger.warning(...) fires before line 45 sets logger.handlers = [RichHandler()]. On the first call for a given logger name, the logger has no handlers yet, so this warning goes through Python's lastResort handler (plain stderr) rather than the configured RichHandler — producing inconsistent formatting compared to all subsequent log output.

Consider reordering: attach the RichHandler first (with a safe default level), then validate and adjust the level.

Proposed fix
     logger = logging.getLogger(name)
 
     # Skip reconfiguration if logger already has a RichHandler from a prior call
     if any(isinstance(h, RichHandler) for h in logger.handlers):
         return logger
 
+    # Attach RichHandler early so any warnings use consistent formatting
+    logger.handlers = [RichHandler()]
+    logger.propagate = False
+
     # Read log level from environment variable with default fallback
     level_str = os.environ.get(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL)
 
     # Validate the level string and convert to logging level constant
     validated_level = getattr(logging, level_str.upper(), None)
     if not isinstance(validated_level, int):
         logger.warning(
             "Invalid log level '%s', falling back to %s",
             level_str,
             DEFAULT_LOG_LEVEL,
         )
         validated_level = getattr(logging, DEFAULT_LOG_LEVEL)
 
     logger.setLevel(validated_level)
-    logger.handlers = [RichHandler()]
-    logger.propagate = False
     return logger

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/unit/authorization/test_azure_token_manager.py (1)

140-148: Save and restore the original propagate value instead of hardcoding False.

The finally block assumes propagate was originally False. If the centralized logger's default changes, this will silently leave the logger in the wrong state for subsequent tests in the same process.

♻️ Suggested fix
         azure_logger = logging.getLogger(azure_token_manager.__name__)
+        original_propagate = azure_logger.propagate
         azure_logger.propagate = True
         try:
             with caplog.at_level("WARNING"):
                 result = token_manager.refresh_token()
                 assert result is False
                 assert "Failed to retrieve Azure access token" in caplog.text
         finally:
-            azure_logger.propagate = False
+            azure_logger.propagate = original_propagate

Copy link
Contributor

@jrobertboos jrobertboos left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@tisnik tisnik left a comment

Choose a reason for hiding this comment

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

LGTM

…VEL env var

This commit introduces runtime-configurable logging via the LIGHTSPEED_STACK_LOG_LEVEL environment variable, allowing deployment-time control of log verbosity without code changes.

Key changes:
- Added LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR and DEFAULT_LOG_LEVEL constants
- Modified get_logger() to read log level from environment with defensive validation
- Updated lightspeed_stack.py basicConfig() to respect the environment variable
- Added force=True to basicConfig() to override llama_stack_client's early logging setup
- Revived --verbose CLI flag to set DEBUG level and update all existing loggers
- Added comprehensive unit tests covering default, custom, case-insensitive, invalid, and all valid log levels

The --verbose flag now provides a convenient CLI shortcut for enabling debug logging, while the environment variable enables fine-grained control in containerized deployments.

Signed-off-by: Pavel Tišnovský <ptisnovs@redhat.com>
Signed-off-by: Major Hayden <major@redhat.com>
All 43+ modules now use the centralized get_logger() function from log.py, ensuring consistent log level configuration via the LIGHTSPEED_STACK_LOG_LEVEL environment variable. Non-standard logger names have been standardized to __name__ for clarity and consistency across the codebase.

Signed-off-by: Major Hayden <major@redhat.com>
Signed-off-by: Major Hayden <major@redhat.com>
Signed-off-by: Major Hayden <major@redhat.com>
…ragile hardcoded names

Address review feedback: move RichHandler attachment before log level validation so warnings use consistent formatting, use module.__name__ instead of hardcoded logger name string in tests, and fix stale logger reference in A2A docs.

Signed-off-by: Major Hayden <major@redhat.com>
@major major force-pushed the mhayden/configurable-log-level branch from e53353b to 2517935 Compare February 17, 2026 13:23
@tisnik
Copy link
Contributor

tisnik commented Feb 17, 2026

/ok-to-test

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/lightspeed_stack.py (1)

23-34: Duplicate log-level validation logic — consider extracting to src/log.py.

The env-var reading and validation (lines 23–31) duplicates the same logic inside get_logger() in src/log.py (lines 37–47). If validation rules change (e.g., accepting custom level names), both sites must be kept in sync.

Consider exporting a small helper from log.py (e.g., resolve_log_level() -> int) that both basicConfig here and get_logger can call.

♻️ Sketch of shared helper in src/log.py
# In src/log.py
def resolve_log_level() -> int:
    """Read and validate the log level from the environment variable.

    Returns:
        int: A valid logging level constant.
    """
    level_str = os.environ.get(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL)
    validated_level = getattr(logging, level_str.upper(), None)
    if not isinstance(validated_level, int):
        # Warning must go to stderr since loggers may not be configured yet
        print(
            f"WARNING: Invalid log level '{level_str}', falling back to {DEFAULT_LOG_LEVEL}",
            file=sys.stderr,
        )
        validated_level = getattr(logging, DEFAULT_LOG_LEVEL)
    return validated_level

Then in lightspeed_stack.py:

-log_level_str = os.environ.get(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL)
-log_level = getattr(logging, log_level_str.upper(), None)
-if not isinstance(log_level, int):
-    print(
-        f"WARNING: Invalid log level '{log_level_str}', falling back to {DEFAULT_LOG_LEVEL}",
-        file=sys.stderr,
-    )
-    log_level = getattr(logging, DEFAULT_LOG_LEVEL)
+from log import resolve_log_level
+log_level = resolve_log_level()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lightspeed_stack.py` around lines 23 - 34, Extract the duplicate
environment-based log-level parsing/validation into a shared helper (e.g.,
resolve_log_level()) in src/log.py and have both get_logger() and the top-level
basicConfig call in lightspeed_stack.py call it; specifically, move the logic
that reads LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, validates via getattr(logging,
...), prints the stderr warning and falls back to DEFAULT_LOG_LEVEL into
resolve_log_level() and replace the inline validation in lightspeed_stack.py
(the block that computes log_level and calls logging.basicConfig) and the
validation inside get_logger() to call resolve_log_level() and use its returned
int when configuring the logger.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/utils/token_counter.py`:
- Around line 4-6: Remove the unused custom logger: delete the import of
get_logger and the logger = get_logger(__name__) initialization in the module so
the unused symbol logger and its wrapper import are removed; if you later need
logging, replace them with the standard pattern by adding import logging and
logger = logging.getLogger(__name__) in this module (refer to the logger
variable and get_logger import to locate the lines to delete).

---

Nitpick comments:
In `@src/lightspeed_stack.py`:
- Around line 23-34: Extract the duplicate environment-based log-level
parsing/validation into a shared helper (e.g., resolve_log_level()) in
src/log.py and have both get_logger() and the top-level basicConfig call in
lightspeed_stack.py call it; specifically, move the logic that reads
LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, validates via getattr(logging, ...), prints
the stderr warning and falls back to DEFAULT_LOG_LEVEL into resolve_log_level()
and replace the inline validation in lightspeed_stack.py (the block that
computes log_level and calls logging.basicConfig) and the validation inside
get_logger() to call resolve_log_level() and use its returned int when
configuring the logger.

Comment on lines +4 to +6
from log import get_logger

logger = logging.getLogger(__name__)
logger = get_logger(__name__)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check if logger is used in the file
rg -n 'logger\.' src/utils/token_counter.py

Repository: lightspeed-core/lightspeed-stack

Length of output: 58


🏁 Script executed:

# Also check the entire content of the file to understand context
wc -l src/utils/token_counter.py && cat -n src/utils/token_counter.py

Repository: lightspeed-core/lightspeed-stack

Length of output: 1567


🏁 Script executed:

# Check what get_logger is and where it comes from
find . -name "log.py" -o -name "log" -type d | head -20

Repository: lightspeed-core/lightspeed-stack

Length of output: 88


🏁 Script executed:

# Search for how get_logger is defined and used across the codebase
rg -n "def get_logger" --type py

Repository: lightspeed-core/lightspeed-stack

Length of output: 134


🏁 Script executed:

# Check coding guideline compliance - look for logging.getLogger usage vs get_logger
rg -n "logging.getLogger|from log import get_logger" src/ --type py | head -30

Repository: lightspeed-core/lightspeed-stack

Length of output: 1825


Remove unused logger and use standard logging pattern per coding guidelines.

The logger is initialized but never used in this module. Additionally, the import pattern violates the coding guidelines, which specify logger = logging.getLogger(__name__) directly instead of using the custom get_logger wrapper.

Remove lines 4-6 entirely. If logging is needed in the future, use the standard pattern:

import logging

logger = logging.getLogger(__name__)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/token_counter.py` around lines 4 - 6, Remove the unused custom
logger: delete the import of get_logger and the logger = get_logger(__name__)
initialization in the module so the unused symbol logger and its wrapper import
are removed; if you later need logging, replace them with the standard pattern
by adding import logging and logger = logging.getLogger(__name__) in this module
(refer to the logger variable and get_logger import to locate the lines to
delete).

@tisnik tisnik merged commit 227e504 into lightspeed-core:main Feb 17, 2026
21 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants