Skip to content

Commit f247fdf

Browse files
committed
feat(logging): add configurable log level via LIGHTSPEED_STACK_LOG_LEVEL 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>
1 parent 4302993 commit f247fdf

4 files changed

Lines changed: 98 additions & 5 deletions

File tree

src/constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,9 @@
176176

177177
# SOLR OKP RAG
178178
MIMIR_DOC_URL = "https://mimir.corp.redhat.com"
179+
180+
# Logging configuration constants
181+
# Environment variable name for configurable log level
182+
LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR = "LIGHTSPEED_STACK_LOG_LEVEL"
183+
# Default log level when environment variable is not set
184+
DEFAULT_LOG_LEVEL = "INFO"

src/lightspeed_stack.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import logging
88
import os
9+
import sys
910
from argparse import ArgumentParser
1011

1112

@@ -16,10 +17,20 @@
1617
from runners.uvicorn import start_uvicorn
1718
from runners.quota_scheduler import start_quota_scheduler
1819
from utils import schema_dumper
20+
from constants import LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL
1921

2022
FORMAT = "%(message)s"
23+
# Read log level from environment variable with validation
24+
log_level_str = os.environ.get(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL)
25+
log_level = getattr(logging, log_level_str.upper(), None)
26+
if not isinstance(log_level, int):
27+
print(
28+
f"WARNING: Invalid log level '{log_level_str}', falling back to {DEFAULT_LOG_LEVEL}",
29+
file=sys.stderr,
30+
)
31+
log_level = getattr(logging, DEFAULT_LOG_LEVEL)
2132
logging.basicConfig(
22-
level="INFO", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
33+
level=log_level, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()], force=True
2334
)
2435

2536
logger = get_logger(__name__)
@@ -83,6 +94,7 @@ def main() -> None:
8394
Start the Lightspeed Core Stack service process based on CLI flags and configuration.
8495
8596
Parses command-line arguments, loads the configured settings, and then:
97+
- If --verbose is provided, sets all loggers to DEBUG level.
8698
- If --dump-configuration is provided, writes the active configuration to
8799
configuration.json and exits (exits with status 1 on failure).
88100
- If --dump-schema is provided, writes the active configuration schema to
@@ -101,6 +113,14 @@ def main() -> None:
101113
parser = create_argument_parser()
102114
args = parser.parse_args()
103115

116+
if args.verbose:
117+
os.environ[LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR] = "DEBUG"
118+
logging.getLogger().setLevel(logging.DEBUG)
119+
for logger_name in logging.Logger.manager.loggerDict:
120+
existing_logger = logging.getLogger(logger_name)
121+
if isinstance(existing_logger, logging.Logger):
122+
existing_logger.setLevel(logging.DEBUG)
123+
104124
configuration.load_configuration(args.config_file)
105125
logger.info("Configuration: %s", configuration.configuration)
106126
logger.info(

src/log.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
"""Log utilities."""
22

33
import logging
4+
import os
45
from rich.logging import RichHandler
56

7+
from constants import LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL
8+
69

710
def get_logger(name: str) -> logging.Logger:
811
"""
912
Get a logger configured for Rich console output.
1013
11-
The returned logger has its level set to DEBUG, its handlers replaced with
12-
a single RichHandler for rich-formatted console output, and propagation to
13-
ancestor loggers disabled.
14+
The returned logger has its level set based on the LIGHTSPEED_STACK_LOG_LEVEL
15+
environment variable (defaults to INFO), its handlers replaced with a single
16+
RichHandler for rich-formatted console output, and propagation to ancestor
17+
loggers disabled.
1418
1519
Parameters:
1620
name (str): Name of the logger to retrieve or create.
@@ -19,7 +23,25 @@ def get_logger(name: str) -> logging.Logger:
1923
logging.Logger: The configured logger instance.
2024
"""
2125
logger = logging.getLogger(name)
22-
logger.setLevel(logging.DEBUG)
26+
27+
# Skip reconfiguration if logger already has a RichHandler from a prior call
28+
if any(isinstance(h, RichHandler) for h in logger.handlers):
29+
return logger
30+
31+
# Read log level from environment variable with default fallback
32+
level_str = os.environ.get(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL)
33+
34+
# Validate the level string and convert to logging level constant
35+
validated_level = getattr(logging, level_str.upper(), None)
36+
if not isinstance(validated_level, int):
37+
logger.warning(
38+
"Invalid log level '%s', falling back to %s",
39+
level_str,
40+
DEFAULT_LOG_LEVEL,
41+
)
42+
validated_level = getattr(logging, DEFAULT_LOG_LEVEL)
43+
44+
logger.setLevel(validated_level)
2345
logger.handlers = [RichHandler()]
2446
logger.propagate = False
2547
return logger

tests/unit/test_log.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
"""Unit tests for functions defined in src/log.py."""
22

3+
import logging
4+
import pytest
5+
36
from log import get_logger
7+
from constants import LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR
48

59

610
def test_get_logger() -> None:
@@ -12,3 +16,44 @@ def test_get_logger() -> None:
1216

1317
# at least one handler need to be set
1418
assert len(logger.handlers) >= 1
19+
20+
21+
def test_get_logger_invalid_env_var_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
22+
"""Test that invalid env var value falls back to INFO level."""
23+
monkeypatch.setenv(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, "FOOBAR")
24+
25+
logger = get_logger("test_invalid")
26+
assert logger.level == logging.INFO
27+
28+
29+
@pytest.mark.parametrize(
30+
"level_name,expected_level",
31+
[
32+
("DEBUG", logging.DEBUG),
33+
("debug", logging.DEBUG),
34+
("INFO", logging.INFO),
35+
("info", logging.INFO),
36+
("WARNING", logging.WARNING),
37+
("warning", logging.WARNING),
38+
("ERROR", logging.ERROR),
39+
("error", logging.ERROR),
40+
("CRITICAL", logging.CRITICAL),
41+
("critical", logging.CRITICAL),
42+
],
43+
)
44+
def test_get_logger_log_level(
45+
monkeypatch: pytest.MonkeyPatch, level_name: str, expected_level: int
46+
) -> None:
47+
"""Test that all valid log levels work correctly, case-insensitively."""
48+
monkeypatch.setenv(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, level_name)
49+
50+
logger = get_logger(f"test_{level_name}")
51+
assert logger.level == expected_level
52+
53+
54+
def test_get_logger_default_log_level(monkeypatch: pytest.MonkeyPatch) -> None:
55+
"""Test that get_logger() uses INFO level by default when env var is not set."""
56+
monkeypatch.delenv(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, raising=False)
57+
58+
logger = get_logger("test_default")
59+
assert logger.level == logging.INFO

0 commit comments

Comments
 (0)