[Misc] Colorize logs (#29017)

Signed-off-by: Nick Hill <nhill@redhat.com>
This commit is contained in:
Nick Hill
2025-11-19 16:26:04 -08:00
committed by GitHub
parent 537cc635c7
commit 9ccef8e333
6 changed files with 152 additions and 62 deletions

View File

@@ -49,10 +49,13 @@ def test_trace_function_call():
os.remove(path) os.remove(path)
def test_default_vllm_root_logger_configuration(): def test_default_vllm_root_logger_configuration(monkeypatch):
"""This test presumes that VLLM_CONFIGURE_LOGGING (default: True) and """This test presumes that VLLM_CONFIGURE_LOGGING (default: True) and
VLLM_LOGGING_CONFIG_PATH (default: None) are not configured and default VLLM_LOGGING_CONFIG_PATH (default: None) are not configured and default
behavior is activated.""" behavior is activated."""
monkeypatch.setenv("VLLM_LOGGING_COLOR", "0")
_configure_vllm_root_logger()
logger = logging.getLogger("vllm") logger = logging.getLogger("vllm")
assert logger.level == logging.DEBUG assert logger.level == logging.DEBUG
assert not logger.propagate assert not logger.propagate
@@ -70,12 +73,13 @@ def test_default_vllm_root_logger_configuration():
assert formatter.datefmt == _DATE_FORMAT assert formatter.datefmt == _DATE_FORMAT
@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 1) def test_descendent_loggers_depend_on_and_propagate_logs_to_root_logger(monkeypatch):
@patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH", None)
def test_descendent_loggers_depend_on_and_propagate_logs_to_root_logger():
"""This test presumes that VLLM_CONFIGURE_LOGGING (default: True) and """This test presumes that VLLM_CONFIGURE_LOGGING (default: True) and
VLLM_LOGGING_CONFIG_PATH (default: None) are not configured and default VLLM_LOGGING_CONFIG_PATH (default: None) are not configured and default
behavior is activated.""" behavior is activated."""
monkeypatch.setenv("VLLM_CONFIGURE_LOGGING", "1")
monkeypatch.delenv("VLLM_LOGGING_CONFIG_PATH", raising=False)
root_logger = logging.getLogger("vllm") root_logger = logging.getLogger("vllm")
root_handler = root_logger.handlers[0] root_handler = root_logger.handlers[0]
@@ -99,49 +103,50 @@ def test_descendent_loggers_depend_on_and_propagate_logs_to_root_logger():
assert log_record.levelno == logging.INFO assert log_record.levelno == logging.INFO
@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 0) def test_logger_configuring_can_be_disabled(monkeypatch):
@patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH", None)
def test_logger_configuring_can_be_disabled():
"""This test calls _configure_vllm_root_logger again to test custom logging """This test calls _configure_vllm_root_logger again to test custom logging
config behavior, however mocks are used to ensure no changes in behavior or config behavior, however mocks are used to ensure no changes in behavior or
configuration occur.""" configuration occur."""
monkeypatch.setenv("VLLM_CONFIGURE_LOGGING", "0")
monkeypatch.delenv("VLLM_LOGGING_CONFIG_PATH", raising=False)
with patch("vllm.logger.dictConfig") as dict_config_mock: with patch("vllm.logger.dictConfig") as dict_config_mock:
_configure_vllm_root_logger() _configure_vllm_root_logger()
dict_config_mock.assert_not_called() dict_config_mock.assert_not_called()
@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 1) def test_an_error_is_raised_when_custom_logging_config_file_does_not_exist(monkeypatch):
@patch(
"vllm.logger.VLLM_LOGGING_CONFIG_PATH",
"/if/there/is/a/file/here/then/you/did/this/to/yourself.json",
)
def test_an_error_is_raised_when_custom_logging_config_file_does_not_exist():
"""This test calls _configure_vllm_root_logger again to test custom logging """This test calls _configure_vllm_root_logger again to test custom logging
config behavior, however it fails before any change in behavior or config behavior, however it fails before any change in behavior or
configuration occurs.""" configuration occurs."""
monkeypatch.setenv("VLLM_CONFIGURE_LOGGING", "1")
monkeypatch.setenv(
"VLLM_LOGGING_CONFIG_PATH",
"/if/there/is/a/file/here/then/you/did/this/to/yourself.json",
)
with pytest.raises(RuntimeError) as ex_info: with pytest.raises(RuntimeError) as ex_info:
_configure_vllm_root_logger() _configure_vllm_root_logger()
assert ex_info.type == RuntimeError # noqa: E721 assert ex_info.type == RuntimeError # noqa: E721
assert "File does not exist" in str(ex_info) assert "File does not exist" in str(ex_info)
@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 1) def test_an_error_is_raised_when_custom_logging_config_is_invalid_json(monkeypatch):
def test_an_error_is_raised_when_custom_logging_config_is_invalid_json():
"""This test calls _configure_vllm_root_logger again to test custom logging """This test calls _configure_vllm_root_logger again to test custom logging
config behavior, however it fails before any change in behavior or config behavior, however it fails before any change in behavior or
configuration occurs.""" configuration occurs."""
monkeypatch.setenv("VLLM_CONFIGURE_LOGGING", "1")
with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file: with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file:
logging_config_file.write("---\nloggers: []\nversion: 1") logging_config_file.write("---\nloggers: []\nversion: 1")
logging_config_file.flush() logging_config_file.flush()
with patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH", logging_config_file.name): monkeypatch.setenv("VLLM_LOGGING_CONFIG_PATH", logging_config_file.name)
with pytest.raises(JSONDecodeError) as ex_info: with pytest.raises(JSONDecodeError) as ex_info:
_configure_vllm_root_logger() _configure_vllm_root_logger()
assert ex_info.type == JSONDecodeError assert ex_info.type == JSONDecodeError
assert "Expecting value" in str(ex_info) assert "Expecting value" in str(ex_info)
@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 1)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"unexpected_config", "unexpected_config",
( (
@@ -151,26 +156,30 @@ def test_an_error_is_raised_when_custom_logging_config_is_invalid_json():
), ),
) )
def test_an_error_is_raised_when_custom_logging_config_is_unexpected_json( def test_an_error_is_raised_when_custom_logging_config_is_unexpected_json(
monkeypatch,
unexpected_config: Any, unexpected_config: Any,
): ):
"""This test calls _configure_vllm_root_logger again to test custom logging """This test calls _configure_vllm_root_logger again to test custom logging
config behavior, however it fails before any change in behavior or config behavior, however it fails before any change in behavior or
configuration occurs.""" configuration occurs."""
monkeypatch.setenv("VLLM_CONFIGURE_LOGGING", "1")
with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file: with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file:
logging_config_file.write(json.dumps(unexpected_config)) logging_config_file.write(json.dumps(unexpected_config))
logging_config_file.flush() logging_config_file.flush()
with patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH", logging_config_file.name): monkeypatch.setenv("VLLM_LOGGING_CONFIG_PATH", logging_config_file.name)
with pytest.raises(ValueError) as ex_info: with pytest.raises(ValueError) as ex_info:
_configure_vllm_root_logger() _configure_vllm_root_logger()
assert ex_info.type == ValueError # noqa: E721 assert ex_info.type == ValueError # noqa: E721
assert "Invalid logging config. Expected dict, got" in str(ex_info) assert "Invalid logging config. Expected dict, got" in str(ex_info)
@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 1) def test_custom_logging_config_is_parsed_and_used_when_provided(monkeypatch):
def test_custom_logging_config_is_parsed_and_used_when_provided():
"""This test calls _configure_vllm_root_logger again to test custom logging """This test calls _configure_vllm_root_logger again to test custom logging
config behavior, however mocks are used to ensure no changes in behavior or config behavior, however mocks are used to ensure no changes in behavior or
configuration occur.""" configuration occur."""
monkeypatch.setenv("VLLM_CONFIGURE_LOGGING", "1")
valid_logging_config = { valid_logging_config = {
"loggers": { "loggers": {
"vllm.test_logger.logger": { "vllm.test_logger.logger": {
@@ -183,19 +192,18 @@ def test_custom_logging_config_is_parsed_and_used_when_provided():
with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file: with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file:
logging_config_file.write(json.dumps(valid_logging_config)) logging_config_file.write(json.dumps(valid_logging_config))
logging_config_file.flush() logging_config_file.flush()
with ( monkeypatch.setenv("VLLM_LOGGING_CONFIG_PATH", logging_config_file.name)
patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH", logging_config_file.name), with patch("vllm.logger.dictConfig") as dict_config_mock:
patch("vllm.logger.dictConfig") as dict_config_mock,
):
_configure_vllm_root_logger() _configure_vllm_root_logger()
dict_config_mock.assert_called_with(valid_logging_config) dict_config_mock.assert_called_with(valid_logging_config)
@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 0) def test_custom_logging_config_causes_an_error_if_configure_logging_is_off(monkeypatch):
def test_custom_logging_config_causes_an_error_if_configure_logging_is_off():
"""This test calls _configure_vllm_root_logger again to test custom logging """This test calls _configure_vllm_root_logger again to test custom logging
config behavior, however mocks are used to ensure no changes in behavior or config behavior, however mocks are used to ensure no changes in behavior or
configuration occur.""" configuration occur."""
monkeypatch.setenv("VLLM_CONFIGURE_LOGGING", "0")
valid_logging_config = { valid_logging_config = {
"loggers": { "loggers": {
"vllm.test_logger.logger": { "vllm.test_logger.logger": {
@@ -207,7 +215,7 @@ def test_custom_logging_config_causes_an_error_if_configure_logging_is_off():
with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file: with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file:
logging_config_file.write(json.dumps(valid_logging_config)) logging_config_file.write(json.dumps(valid_logging_config))
logging_config_file.flush() logging_config_file.flush()
with patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH", logging_config_file.name): monkeypatch.setenv("VLLM_LOGGING_CONFIG_PATH", logging_config_file.name)
with pytest.raises(RuntimeError) as ex_info: with pytest.raises(RuntimeError) as ex_info:
_configure_vllm_root_logger() _configure_vllm_root_logger()
assert ex_info.type is RuntimeError assert ex_info.type is RuntimeError

View File

@@ -42,6 +42,8 @@ if TYPE_CHECKING:
VLLM_LOGGING_PREFIX: str = "" VLLM_LOGGING_PREFIX: str = ""
VLLM_LOGGING_STREAM: str = "ext://sys.stdout" VLLM_LOGGING_STREAM: str = "ext://sys.stdout"
VLLM_LOGGING_CONFIG_PATH: str | None = None VLLM_LOGGING_CONFIG_PATH: str | None = None
VLLM_LOGGING_COLOR: str = "auto"
NO_COLOR: bool = False
VLLM_LOG_STATS_INTERVAL: float = 10.0 VLLM_LOG_STATS_INTERVAL: float = 10.0
VLLM_TRACE_FUNCTION: int = 0 VLLM_TRACE_FUNCTION: int = 0
VLLM_ATTENTION_BACKEND: str | None = None VLLM_ATTENTION_BACKEND: str | None = None
@@ -616,6 +618,11 @@ environment_variables: dict[str, Callable[[], Any]] = {
"VLLM_LOGGING_STREAM": lambda: os.getenv("VLLM_LOGGING_STREAM", "ext://sys.stdout"), "VLLM_LOGGING_STREAM": lambda: os.getenv("VLLM_LOGGING_STREAM", "ext://sys.stdout"),
# if set, VLLM_LOGGING_PREFIX will be prepended to all log messages # if set, VLLM_LOGGING_PREFIX will be prepended to all log messages
"VLLM_LOGGING_PREFIX": lambda: os.getenv("VLLM_LOGGING_PREFIX", ""), "VLLM_LOGGING_PREFIX": lambda: os.getenv("VLLM_LOGGING_PREFIX", ""),
# Controls colored logging output. Options: "auto" (default, colors when terminal),
# "1" (always use colors), "0" (never use colors)
"VLLM_LOGGING_COLOR": lambda: os.getenv("VLLM_LOGGING_COLOR", "auto"),
# Standard unix flag for disabling ANSI color codes
"NO_COLOR": lambda: os.getenv("NO_COLOR", "0") != "0",
# If set, vllm will log stats at this interval in seconds # If set, vllm will log stats at this interval in seconds
# If not set, vllm will log stats every 10 seconds. # If not set, vllm will log stats every 10 seconds.
"VLLM_LOG_STATS_INTERVAL": lambda: val "VLLM_LOG_STATS_INTERVAL": lambda: val
@@ -1578,6 +1585,7 @@ def compile_factors() -> dict[str, object]:
"VLLM_LOGGING_PREFIX", "VLLM_LOGGING_PREFIX",
"VLLM_LOGGING_STREAM", "VLLM_LOGGING_STREAM",
"VLLM_LOGGING_CONFIG_PATH", "VLLM_LOGGING_CONFIG_PATH",
"VLLM_LOGGING_COLOR",
"VLLM_LOG_STATS_INTERVAL", "VLLM_LOG_STATS_INTERVAL",
"VLLM_DEBUG_LOG_API_SERVER_RESPONSE", "VLLM_DEBUG_LOG_API_SERVER_RESPONSE",
"VLLM_TUNED_CONFIG_FOLDER", "VLLM_TUNED_CONFIG_FOLDER",
@@ -1608,6 +1616,7 @@ def compile_factors() -> dict[str, object]:
"VLLM_TEST_FORCE_LOAD_FORMAT", "VLLM_TEST_FORCE_LOAD_FORMAT",
"LOCAL_RANK", "LOCAL_RANK",
"CUDA_VISIBLE_DEVICES", "CUDA_VISIBLE_DEVICES",
"NO_COLOR",
} }
from vllm.config.utils import normalize_value from vllm.config.utils import normalize_value

View File

@@ -17,18 +17,25 @@ from typing import Any, Literal, cast
import vllm.envs as envs import vllm.envs as envs
VLLM_CONFIGURE_LOGGING = envs.VLLM_CONFIGURE_LOGGING
VLLM_LOGGING_CONFIG_PATH = envs.VLLM_LOGGING_CONFIG_PATH
VLLM_LOGGING_LEVEL = envs.VLLM_LOGGING_LEVEL
VLLM_LOGGING_PREFIX = envs.VLLM_LOGGING_PREFIX
VLLM_LOGGING_STREAM = envs.VLLM_LOGGING_STREAM
_FORMAT = ( _FORMAT = (
f"{VLLM_LOGGING_PREFIX}%(levelname)s %(asctime)s " f"{envs.VLLM_LOGGING_PREFIX}%(levelname)s %(asctime)s "
"[%(fileinfo)s:%(lineno)d] %(message)s" "[%(fileinfo)s:%(lineno)d] %(message)s"
) )
_DATE_FORMAT = "%m-%d %H:%M:%S" _DATE_FORMAT = "%m-%d %H:%M:%S"
def _use_color() -> bool:
if envs.NO_COLOR or envs.VLLM_LOGGING_COLOR == "0":
return False
if envs.VLLM_LOGGING_COLOR == "1":
return True
if envs.VLLM_LOGGING_STREAM == "ext://sys.stdout": # stdout
return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
elif envs.VLLM_LOGGING_STREAM == "ext://sys.stderr": # stderr
return hasattr(sys.stderr, "isatty") and sys.stderr.isatty()
return False
DEFAULT_LOGGING_CONFIG = { DEFAULT_LOGGING_CONFIG = {
"formatters": { "formatters": {
"vllm": { "vllm": {
@@ -36,13 +43,19 @@ DEFAULT_LOGGING_CONFIG = {
"datefmt": _DATE_FORMAT, "datefmt": _DATE_FORMAT,
"format": _FORMAT, "format": _FORMAT,
}, },
"vllm_color": {
"class": "vllm.logging_utils.ColoredFormatter",
"datefmt": _DATE_FORMAT,
"format": _FORMAT,
},
}, },
"handlers": { "handlers": {
"vllm": { "vllm": {
"class": "logging.StreamHandler", "class": "logging.StreamHandler",
"formatter": "vllm", # Choose formatter based on color setting.
"level": VLLM_LOGGING_LEVEL, "formatter": "vllm_color" if _use_color() else "vllm",
"stream": VLLM_LOGGING_STREAM, "level": envs.VLLM_LOGGING_LEVEL,
"stream": envs.VLLM_LOGGING_STREAM,
}, },
}, },
"loggers": { "loggers": {
@@ -144,7 +157,7 @@ _METHODS_TO_PATCH = {
def _configure_vllm_root_logger() -> None: def _configure_vllm_root_logger() -> None:
logging_config = dict[str, Any]() logging_config = dict[str, Any]()
if not VLLM_CONFIGURE_LOGGING and VLLM_LOGGING_CONFIG_PATH: if not envs.VLLM_CONFIGURE_LOGGING and envs.VLLM_LOGGING_CONFIG_PATH:
raise RuntimeError( raise RuntimeError(
"VLLM_CONFIGURE_LOGGING evaluated to false, but " "VLLM_CONFIGURE_LOGGING evaluated to false, but "
"VLLM_LOGGING_CONFIG_PATH was given. VLLM_LOGGING_CONFIG_PATH " "VLLM_LOGGING_CONFIG_PATH was given. VLLM_LOGGING_CONFIG_PATH "
@@ -152,16 +165,22 @@ def _configure_vllm_root_logger() -> None:
"VLLM_CONFIGURE_LOGGING or unset VLLM_LOGGING_CONFIG_PATH." "VLLM_CONFIGURE_LOGGING or unset VLLM_LOGGING_CONFIG_PATH."
) )
if VLLM_CONFIGURE_LOGGING: if envs.VLLM_CONFIGURE_LOGGING:
logging_config = DEFAULT_LOGGING_CONFIG logging_config = DEFAULT_LOGGING_CONFIG
if VLLM_LOGGING_CONFIG_PATH: vllm_handler = logging_config["handlers"]["vllm"]
if not path.exists(VLLM_LOGGING_CONFIG_PATH): # Refresh these values in case env vars have changed.
vllm_handler["level"] = envs.VLLM_LOGGING_LEVEL
vllm_handler["stream"] = envs.VLLM_LOGGING_STREAM
vllm_handler["formatter"] = "vllm_color" if _use_color() else "vllm"
if envs.VLLM_LOGGING_CONFIG_PATH:
if not path.exists(envs.VLLM_LOGGING_CONFIG_PATH):
raise RuntimeError( raise RuntimeError(
"Could not load logging config. File does not exist: %s", "Could not load logging config. File does not exist: %s",
VLLM_LOGGING_CONFIG_PATH, envs.VLLM_LOGGING_CONFIG_PATH,
) )
with open(VLLM_LOGGING_CONFIG_PATH, encoding="utf-8") as file: with open(envs.VLLM_LOGGING_CONFIG_PATH, encoding="utf-8") as file:
custom_config = json.loads(file.read()) custom_config = json.loads(file.read())
if not isinstance(custom_config, dict): if not isinstance(custom_config, dict):

View File

@@ -1,12 +1,13 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from vllm.logging_utils.formatter import NewLineFormatter from vllm.logging_utils.formatter import ColoredFormatter, NewLineFormatter
from vllm.logging_utils.lazy import lazy from vllm.logging_utils.lazy import lazy
from vllm.logging_utils.log_time import logtime from vllm.logging_utils.log_time import logtime
__all__ = [ __all__ = [
"NewLineFormatter", "NewLineFormatter",
"ColoredFormatter",
"lazy", "lazy",
"logtime", "logtime",
] ]

View File

@@ -75,3 +75,53 @@ class NewLineFormatter(logging.Formatter):
parts = msg.split(record.message) parts = msg.split(record.message)
msg = msg.replace("\n", "\r\n" + parts[0]) msg = msg.replace("\n", "\r\n" + parts[0])
return msg return msg
class ColoredFormatter(NewLineFormatter):
"""Adds ANSI color codes to log levels for terminal output.
This formatter adds colors by injecting them into the format string for
static elements (timestamp, filename, line number) and modifying the
levelname attribute for dynamic color selection.
"""
# ANSI color codes
COLORS = {
"DEBUG": "\033[37m", # White
"INFO": "\033[32m", # Green
"WARNING": "\033[33m", # Yellow
"ERROR": "\033[31m", # Red
"CRITICAL": "\033[35m", # Magenta
}
GREY = "\033[90m" # Grey for timestamp and file info
RESET = "\033[0m"
def __init__(self, fmt, datefmt=None, style="%"):
# Inject grey color codes into format string for timestamp and file info
if fmt:
# Wrap %(asctime)s with grey
fmt = fmt.replace("%(asctime)s", f"{self.GREY}%(asctime)s{self.RESET}")
# Wrap [%(fileinfo)s:%(lineno)d] with grey
fmt = fmt.replace(
"[%(fileinfo)s:%(lineno)d]",
f"{self.GREY}[%(fileinfo)s:%(lineno)d]{self.RESET}",
)
# Call parent __init__ with potentially modified format string
super().__init__(fmt, datefmt, style)
def format(self, record):
# Store original levelname to restore later (in case record is reused)
orig_levelname = record.levelname
# Only modify levelname - it needs dynamic color based on severity
if (color_code := self.COLORS.get(record.levelname)) is not None:
record.levelname = f"{color_code}{record.levelname}{self.RESET}"
# Call parent format which will handle everything else
msg = super().format(record)
# Restore original levelname
record.levelname = orig_levelname
return msg

View File

@@ -22,7 +22,7 @@ from .platform_utils import cuda_is_initialized, xpu_is_initialized
logger = init_logger(__name__) logger = init_logger(__name__)
CYAN = "\033[1;36m" CYAN = "\033[0;36m"
RESET = "\033[0;0m" RESET = "\033[0;0m"
@@ -142,6 +142,9 @@ def set_process_title(
def _add_prefix(file: TextIO, worker_name: str, pid: int) -> None: def _add_prefix(file: TextIO, worker_name: str, pid: int) -> None:
"""Add colored prefix to file output for log decoration.""" """Add colored prefix to file output for log decoration."""
if envs.NO_COLOR:
prefix = f"({worker_name} pid={pid}) "
else:
prefix = f"{CYAN}({worker_name} pid={pid}){RESET} " prefix = f"{CYAN}({worker_name} pid={pid}){RESET} "
file_write = file.write file_write = file.write