Skip to content

Commit 9767428

Browse files
committed
refactor(logging): extract resolve_log_level and create_log_handler into log.py
Add DEFAULT_LOG_FORMAT constant to constants.py and extract two public functions into log.py: resolve_log_level() for env-var-based level resolution and create_log_handler() for TTY-aware handler selection. Simplify get_logger() to delegate to these new functions while keeping its signature unchanged. Signed-off-by: Major Hayden <major@redhat.com>
1 parent 62ca58e commit 9767428

2 files changed

Lines changed: 72 additions & 32 deletions

File tree

src/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,5 @@
183183
LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR = "LIGHTSPEED_STACK_LOG_LEVEL"
184184
# Default log level when environment variable is not set
185185
DEFAULT_LOG_LEVEL = "INFO"
186+
# Default log format for plain-text logging in non-TTY environments
187+
DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)-8s %(name)s:%(lineno)d %(message)s"

src/log.py

Lines changed: 70 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,72 @@
66

77
from rich.logging import RichHandler
88

9-
from constants import LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL
9+
from constants import (
10+
LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR,
11+
DEFAULT_LOG_LEVEL,
12+
DEFAULT_LOG_FORMAT,
13+
)
14+
15+
16+
def resolve_log_level() -> int:
17+
"""
18+
Resolve and validate the log level from environment variable.
19+
20+
Reads the LIGHTSPEED_STACK_LOG_LEVEL environment variable and validates
21+
it against Python's logging module. If the environment variable is not set,
22+
defaults to DEFAULT_LOG_LEVEL. If the value is invalid, logs a warning and
23+
falls back to DEFAULT_LOG_LEVEL.
24+
25+
Parameters:
26+
None
27+
28+
Returns:
29+
int: A valid logging level constant (e.g., logging.INFO, logging.DEBUG).
30+
"""
31+
level_str = os.environ.get(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL)
32+
33+
# Validate the level string and convert to logging level constant
34+
validated_level = getattr(logging, level_str.upper(), None)
35+
if not isinstance(validated_level, int):
36+
# Write directly to stderr instead of using a logger. This function is
37+
# called at module-import time (before logging is configured), so routing
38+
# through a logger produces inconsistent output depending on root-logger
39+
# state.
40+
print(
41+
f"WARNING: Invalid log level '{level_str}', "
42+
f"falling back to {DEFAULT_LOG_LEVEL}",
43+
file=sys.stderr,
44+
)
45+
validated_level = getattr(logging, DEFAULT_LOG_LEVEL)
46+
47+
return validated_level
48+
49+
50+
def create_log_handler() -> logging.Handler:
51+
"""
52+
Create and return a configured log handler based on TTY availability.
53+
54+
If stderr is connected to a terminal (TTY), returns a RichHandler for
55+
rich-formatted console output. Otherwise, returns a StreamHandler with
56+
plain-text formatting suitable for non-TTY environments (e.g., containers).
57+
58+
Parameters:
59+
None
60+
61+
Returns:
62+
logging.Handler: A configured handler instance (RichHandler or StreamHandler).
63+
"""
64+
if sys.stderr.isatty():
65+
# RichHandler's columnar layout assumes a real terminal.
66+
# RichHandler handles its own formatting, so no formatter is set.
67+
return RichHandler()
68+
69+
# In containers without a TTY, Rich falls back to 80 columns and
70+
# the columns consume most of that width, leaving ~40 chars for the actual message.
71+
# Tracebacks become nearly unreadable. Use a plain StreamHandler instead.
72+
handler = logging.StreamHandler()
73+
handler.setFormatter(logging.Formatter(DEFAULT_LOG_FORMAT))
74+
return handler
1075

1176

1277
def get_logger(name: str) -> logging.Logger:
@@ -15,8 +80,8 @@ def get_logger(name: str) -> logging.Logger:
1580
1681
The returned logger has its level set based on the LIGHTSPEED_STACK_LOG_LEVEL
1782
environment variable (defaults to INFO), its handlers replaced with a single
18-
RichHandler for rich-formatted console output, and propagation to ancestor
19-
loggers disabled.
83+
handler (RichHandler for TTY or StreamHandler for non-TTY), and propagation
84+
to ancestor loggers disabled.
2085
2186
Parameters:
2287
name (str): Name of the logger to retrieve or create.
@@ -30,34 +95,7 @@ def get_logger(name: str) -> logging.Logger:
3095
if logger.handlers:
3196
return logger
3297

33-
# RichHandler's columnar layout (timestamp, level, right-aligned filename) assumes
34-
# a real terminal. In containers without a TTY, Rich falls back to 80 columns and
35-
# the columns consume most of that width, leaving ~40 chars for the actual message.
36-
# Tracebacks become nearly unreadable. Use a plain StreamHandler when there's no TTY.
37-
if sys.stderr.isatty():
38-
logger.handlers = [RichHandler()]
39-
else:
40-
handler = logging.StreamHandler()
41-
handler.setFormatter(
42-
logging.Formatter(
43-
"%(asctime)s %(levelname)-8s %(name)s:%(lineno)d %(message)s"
44-
)
45-
)
46-
logger.handlers = [handler]
98+
logger.handlers = [create_log_handler()]
4799
logger.propagate = False
48-
49-
# Read log level from environment variable with default fallback
50-
level_str = os.environ.get(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL)
51-
52-
# Validate the level string and convert to logging level constant
53-
validated_level = getattr(logging, level_str.upper(), None)
54-
if not isinstance(validated_level, int):
55-
logger.warning(
56-
"Invalid log level '%s', falling back to %s",
57-
level_str,
58-
DEFAULT_LOG_LEVEL,
59-
)
60-
validated_level = getattr(logging, DEFAULT_LOG_LEVEL)
61-
62-
logger.setLevel(validated_level)
100+
logger.setLevel(resolve_log_level())
63101
return logger

0 commit comments

Comments
 (0)