From 68fa9e6914eedbef5461ccfc87c5bed11cf0bac8 Mon Sep 17 00:00:00 2001 From: Major Hayden Date: Mon, 23 Feb 2026 13:45:56 -0600 Subject: [PATCH] RSPEED-2471: Use plain log handler in non-TTY environments RichHandler's columnar layout falls back to 80 columns without a TTY, leaving only ~40 chars for log messages. Tracebacks become unreadable. Fall back to a plain StreamHandler when stderr is not a terminal. Signed-off-by: Major Hayden --- src/lightspeed_stack.py | 22 ++++++++++++++++++---- src/log.py | 22 ++++++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/lightspeed_stack.py b/src/lightspeed_stack.py index 297a91caf..b11d46d06 100644 --- a/src/lightspeed_stack.py +++ b/src/lightspeed_stack.py @@ -19,7 +19,6 @@ from utils import schema_dumper from constants import LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL -FORMAT = "%(message)s" # Read log level from environment variable with validation log_level_str = os.environ.get(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL) log_level = getattr(logging, log_level_str.upper(), None) @@ -29,9 +28,24 @@ file=sys.stderr, ) log_level = getattr(logging, DEFAULT_LOG_LEVEL) -logging.basicConfig( - level=log_level, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()], force=True -) + +# RichHandler's columnar layout produces very narrow log output in containers +# without a TTY (Rich falls back to 80 columns, columns consume ~40 of those). +# Use a plain format when there's no terminal attached. +if sys.stderr.isatty(): + logging.basicConfig( + level=log_level, + format="%(message)s", + datefmt="[%X]", + handlers=[RichHandler()], + force=True, + ) +else: + logging.basicConfig( + level=log_level, + format="%(asctime)s %(levelname)-8s %(name)s:%(lineno)d %(message)s", + force=True, + ) logger = get_logger(__name__) diff --git a/src/log.py b/src/log.py index 0911073aa..a1f74a98c 100644 --- a/src/log.py +++ b/src/log.py @@ -2,6 +2,8 @@ import logging import os +import sys + from rich.logging import RichHandler from constants import LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL @@ -24,12 +26,24 @@ def get_logger(name: str) -> logging.Logger: """ 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): + # Skip reconfiguration if logger already has handlers from a prior call + if logger.handlers: return logger - # Attach RichHandler before any log calls so warnings use consistent formatting - logger.handlers = [RichHandler()] + # RichHandler's columnar layout (timestamp, level, right-aligned filename) assumes + # a real terminal. In containers without a TTY, Rich falls back to 80 columns and + # the columns consume most of that width, leaving ~40 chars for the actual message. + # Tracebacks become nearly unreadable. Use a plain StreamHandler when there's no TTY. + if sys.stderr.isatty(): + logger.handlers = [RichHandler()] + else: + handler = logging.StreamHandler() + handler.setFormatter( + logging.Formatter( + "%(asctime)s %(levelname)-8s %(name)s:%(lineno)d %(message)s" + ) + ) + logger.handlers = [handler] logger.propagate = False # Read log level from environment variable with default fallback