@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 9ccef8e333
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 12, 22:15 UTC

[Misc] Colorize logs (#29017)

Signed-off-by: Nick Hill <nhill@redhat.com>

Nick Hill committed Nov 19, 2025 at 16:26 UTC 9ccef8e333ccd988a587990740405503e76c8c20
6 files changed +152 -62
tests/test_logger.py
+51 -43
@@ -49,10 +49,13 @@ def test_trace_function_call():
49 os.remove(path)
50
51
52 -def test_default_vllm_root_logger_configuration():
52 +def test_default_vllm_root_logger_configuration(monkeypatch):
53 """This test presumes that VLLM_CONFIGURE_LOGGING (default: True) and
54 VLLM_LOGGING_CONFIG_PATH (default: None) are not configured and default
55 behavior is activated."""
56 + monkeypatch.setenv("VLLM_LOGGING_COLOR", "0")
57 + _configure_vllm_root_logger()
58 +
59 logger = logging.getLogger("vllm")
60 assert logger.level == logging.DEBUG
61 assert not logger.propagate
@@ -70,12 +73,13 @@ def test_default_vllm_root_logger_configuration():
73 assert formatter.datefmt == _DATE_FORMAT
74
75
73 -@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 1)
74 -@patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH", None)
75 -def test_descendent_loggers_depend_on_and_propagate_logs_to_root_logger():
76 +def test_descendent_loggers_depend_on_and_propagate_logs_to_root_logger(monkeypatch):
77 """This test presumes that VLLM_CONFIGURE_LOGGING (default: True) and
78 VLLM_LOGGING_CONFIG_PATH (default: None) are not configured and default
79 behavior is activated."""
80 + monkeypatch.setenv("VLLM_CONFIGURE_LOGGING", "1")
81 + monkeypatch.delenv("VLLM_LOGGING_CONFIG_PATH", raising=False)
82 +
83 root_logger = logging.getLogger("vllm")
84 root_handler = root_logger.handlers[0]
85
@@ -99,49 +103,50 @@ def test_descendent_loggers_depend_on_and_propagate_logs_to_root_logger():
103 assert log_record.levelno == logging.INFO
104
105
102 -@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 0)
103 -@patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH", None)
104 -def test_logger_configuring_can_be_disabled():
106 +def test_logger_configuring_can_be_disabled(monkeypatch):
107 """This test calls _configure_vllm_root_logger again to test custom logging
108 config behavior, however mocks are used to ensure no changes in behavior or
109 configuration occur."""
110 + monkeypatch.setenv("VLLM_CONFIGURE_LOGGING", "0")
111 + monkeypatch.delenv("VLLM_LOGGING_CONFIG_PATH", raising=False)
112
113 with patch("vllm.logger.dictConfig") as dict_config_mock:
114 _configure_vllm_root_logger()
115 dict_config_mock.assert_not_called()
116
117
114 -@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 1)
115 -@patch(
116 - "vllm.logger.VLLM_LOGGING_CONFIG_PATH",
117 - "/if/there/is/a/file/here/then/you/did/this/to/yourself.json",
118 -)
119 -def test_an_error_is_raised_when_custom_logging_config_file_does_not_exist():
118 +def test_an_error_is_raised_when_custom_logging_config_file_does_not_exist(monkeypatch):
119 """This test calls _configure_vllm_root_logger again to test custom logging
120 config behavior, however it fails before any change in behavior or
121 configuration occurs."""
122 + monkeypatch.setenv("VLLM_CONFIGURE_LOGGING", "1")
123 + monkeypatch.setenv(
124 + "VLLM_LOGGING_CONFIG_PATH",
125 + "/if/there/is/a/file/here/then/you/did/this/to/yourself.json",
126 + )
127 +
128 with pytest.raises(RuntimeError) as ex_info:
129 _configure_vllm_root_logger()
130 assert ex_info.type == RuntimeError # noqa: E721
131 assert "File does not exist" in str(ex_info)
132
133
129 -@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 1)
130 -def test_an_error_is_raised_when_custom_logging_config_is_invalid_json():
134 +def test_an_error_is_raised_when_custom_logging_config_is_invalid_json(monkeypatch):
135 """This test calls _configure_vllm_root_logger again to test custom logging
136 config behavior, however it fails before any change in behavior or
137 configuration occurs."""
138 + monkeypatch.setenv("VLLM_CONFIGURE_LOGGING", "1")
139 +
140 with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file:
141 logging_config_file.write("---\nloggers: []\nversion: 1")
142 logging_config_file.flush()
137 - with patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH", logging_config_file.name):
138 - with pytest.raises(JSONDecodeError) as ex_info:
139 - _configure_vllm_root_logger()
140 - assert ex_info.type == JSONDecodeError
141 - assert "Expecting value" in str(ex_info)
143 + monkeypatch.setenv("VLLM_LOGGING_CONFIG_PATH", logging_config_file.name)
144 + with pytest.raises(JSONDecodeError) as ex_info:
145 + _configure_vllm_root_logger()
146 + assert ex_info.type == JSONDecodeError
147 + assert "Expecting value" in str(ex_info)
148
149
144 -@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 1)
150 @pytest.mark.parametrize(
151 "unexpected_config",
152 (
@@ -151,26 +156,30 @@ def test_an_error_is_raised_when_custom_logging_config_is_invalid_json():
156 ),
157 )
158 def test_an_error_is_raised_when_custom_logging_config_is_unexpected_json(
159 + monkeypatch,
160 unexpected_config: Any,
161 ):
162 """This test calls _configure_vllm_root_logger again to test custom logging
163 config behavior, however it fails before any change in behavior or
164 configuration occurs."""
165 + monkeypatch.setenv("VLLM_CONFIGURE_LOGGING", "1")
166 +
167 with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file:
168 logging_config_file.write(json.dumps(unexpected_config))
169 logging_config_file.flush()
162 - with patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH", logging_config_file.name):
163 - with pytest.raises(ValueError) as ex_info:
164 - _configure_vllm_root_logger()
165 - assert ex_info.type == ValueError # noqa: E721
166 - assert "Invalid logging config. Expected dict, got" in str(ex_info)
170 + monkeypatch.setenv("VLLM_LOGGING_CONFIG_PATH", logging_config_file.name)
171 + with pytest.raises(ValueError) as ex_info:
172 + _configure_vllm_root_logger()
173 + assert ex_info.type == ValueError # noqa: E721
174 + assert "Invalid logging config. Expected dict, got" in str(ex_info)
175
176
169 -@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 1)
170 -def test_custom_logging_config_is_parsed_and_used_when_provided():
177 +def test_custom_logging_config_is_parsed_and_used_when_provided(monkeypatch):
178 """This test calls _configure_vllm_root_logger again to test custom logging
179 config behavior, however mocks are used to ensure no changes in behavior or
180 configuration occur."""
181 + monkeypatch.setenv("VLLM_CONFIGURE_LOGGING", "1")
182 +
183 valid_logging_config = {
184 "loggers": {
185 "vllm.test_logger.logger": {
@@ -183,19 +192,18 @@ def test_custom_logging_config_is_parsed_and_used_when_provided():
192 with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file:
193 logging_config_file.write(json.dumps(valid_logging_config))
194 logging_config_file.flush()
186 - with (
187 - patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH", logging_config_file.name),
188 - patch("vllm.logger.dictConfig") as dict_config_mock,
189 - ):
195 + monkeypatch.setenv("VLLM_LOGGING_CONFIG_PATH", logging_config_file.name)
196 + with patch("vllm.logger.dictConfig") as dict_config_mock:
197 _configure_vllm_root_logger()
198 dict_config_mock.assert_called_with(valid_logging_config)
199
200
194 -@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 0)
195 -def test_custom_logging_config_causes_an_error_if_configure_logging_is_off():
201 +def test_custom_logging_config_causes_an_error_if_configure_logging_is_off(monkeypatch):
202 """This test calls _configure_vllm_root_logger again to test custom logging
203 config behavior, however mocks are used to ensure no changes in behavior or
204 configuration occur."""
205 + monkeypatch.setenv("VLLM_CONFIGURE_LOGGING", "0")
206 +
207 valid_logging_config = {
208 "loggers": {
209 "vllm.test_logger.logger": {
@@ -207,15 +215,15 @@ def test_custom_logging_config_causes_an_error_if_configure_logging_is_off():
215 with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file:
216 logging_config_file.write(json.dumps(valid_logging_config))
217 logging_config_file.flush()
210 - with patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH", logging_config_file.name):
211 - with pytest.raises(RuntimeError) as ex_info:
212 - _configure_vllm_root_logger()
213 - assert ex_info.type is RuntimeError
214 - expected_message_snippet = (
215 - "VLLM_CONFIGURE_LOGGING evaluated to false, but "
216 - "VLLM_LOGGING_CONFIG_PATH was given."
217 - )
218 - assert expected_message_snippet in str(ex_info)
218 + monkeypatch.setenv("VLLM_LOGGING_CONFIG_PATH", logging_config_file.name)
219 + with pytest.raises(RuntimeError) as ex_info:
220 + _configure_vllm_root_logger()
221 + assert ex_info.type is RuntimeError
222 + expected_message_snippet = (
223 + "VLLM_CONFIGURE_LOGGING evaluated to false, but "
224 + "VLLM_LOGGING_CONFIG_PATH was given."
225 + )
226 + assert expected_message_snippet in str(ex_info)
227
228 # Remember! The root logger is assumed to have been configured as
229 # though VLLM_CONFIGURE_LOGGING=1 and VLLM_LOGGING_CONFIG_PATH=None.
vllm/envs.py
+9
@@ -42,6 +42,8 @@ if TYPE_CHECKING:
42 VLLM_LOGGING_PREFIX: str = ""
43 VLLM_LOGGING_STREAM: str = "ext://sys.stdout"
44 VLLM_LOGGING_CONFIG_PATH: str | None = None
45 + VLLM_LOGGING_COLOR: str = "auto"
46 + NO_COLOR: bool = False
47 VLLM_LOG_STATS_INTERVAL: float = 10.0
48 VLLM_TRACE_FUNCTION: int = 0
49 VLLM_ATTENTION_BACKEND: str | None = None
@@ -616,6 +618,11 @@ environment_variables: dict[str, Callable[[], Any]] = {
618 "VLLM_LOGGING_STREAM": lambda: os.getenv("VLLM_LOGGING_STREAM", "ext://sys.stdout"),
619 # if set, VLLM_LOGGING_PREFIX will be prepended to all log messages
620 "VLLM_LOGGING_PREFIX": lambda: os.getenv("VLLM_LOGGING_PREFIX", ""),
621 + # Controls colored logging output. Options: "auto" (default, colors when terminal),
622 + # "1" (always use colors), "0" (never use colors)
623 + "VLLM_LOGGING_COLOR": lambda: os.getenv("VLLM_LOGGING_COLOR", "auto"),
624 + # Standard unix flag for disabling ANSI color codes
625 + "NO_COLOR": lambda: os.getenv("NO_COLOR", "0") != "0",
626 # If set, vllm will log stats at this interval in seconds
627 # If not set, vllm will log stats every 10 seconds.
628 "VLLM_LOG_STATS_INTERVAL": lambda: val
@@ -1578,6 +1585,7 @@ def compile_factors() -> dict[str, object]:
1585 "VLLM_LOGGING_PREFIX",
1586 "VLLM_LOGGING_STREAM",
1587 "VLLM_LOGGING_CONFIG_PATH",
1588 + "VLLM_LOGGING_COLOR",
1589 "VLLM_LOG_STATS_INTERVAL",
1590 "VLLM_DEBUG_LOG_API_SERVER_RESPONSE",
1591 "VLLM_TUNED_CONFIG_FOLDER",
@@ -1608,6 +1616,7 @@ def compile_factors() -> dict[str, object]:
1616 "VLLM_TEST_FORCE_LOAD_FORMAT",
1617 "LOCAL_RANK",
1618 "CUDA_VISIBLE_DEVICES",
1619 + "NO_COLOR",
1620 }
1621
1622 from vllm.config.utils import normalize_value
vllm/logger.py
+35 -16
@@ -17,18 +17,25 @@ from typing import Any, Literal, cast
17
18 import vllm.envs as envs
19
20 -VLLM_CONFIGURE_LOGGING = envs.VLLM_CONFIGURE_LOGGING
21 -VLLM_LOGGING_CONFIG_PATH = envs.VLLM_LOGGING_CONFIG_PATH
22 -VLLM_LOGGING_LEVEL = envs.VLLM_LOGGING_LEVEL
23 -VLLM_LOGGING_PREFIX = envs.VLLM_LOGGING_PREFIX
24 -VLLM_LOGGING_STREAM = envs.VLLM_LOGGING_STREAM
25 -
20 _FORMAT = (
27 - f"{VLLM_LOGGING_PREFIX}%(levelname)s %(asctime)s "
21 + f"{envs.VLLM_LOGGING_PREFIX}%(levelname)s %(asctime)s "
22 "[%(fileinfo)s:%(lineno)d] %(message)s"
23 )
24 _DATE_FORMAT = "%m-%d %H:%M:%S"
25
26 +
27 +def _use_color() -> bool:
28 + if envs.NO_COLOR or envs.VLLM_LOGGING_COLOR == "0":
29 + return False
30 + if envs.VLLM_LOGGING_COLOR == "1":
31 + return True
32 + if envs.VLLM_LOGGING_STREAM == "ext://sys.stdout": # stdout
33 + return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
34 + elif envs.VLLM_LOGGING_STREAM == "ext://sys.stderr": # stderr
35 + return hasattr(sys.stderr, "isatty") and sys.stderr.isatty()
36 + return False
37 +
38 +
39 DEFAULT_LOGGING_CONFIG = {
40 "formatters": {
41 "vllm": {
@@ -36,13 +43,19 @@ DEFAULT_LOGGING_CONFIG = {
43 "datefmt": _DATE_FORMAT,
44 "format": _FORMAT,
45 },
46 + "vllm_color": {
47 + "class": "vllm.logging_utils.ColoredFormatter",
48 + "datefmt": _DATE_FORMAT,
49 + "format": _FORMAT,
50 + },
51 },
52 "handlers": {
53 "vllm": {
54 "class": "logging.StreamHandler",
43 - "formatter": "vllm",
44 - "level": VLLM_LOGGING_LEVEL,
45 - "stream": VLLM_LOGGING_STREAM,
55 + # Choose formatter based on color setting.
56 + "formatter": "vllm_color" if _use_color() else "vllm",
57 + "level": envs.VLLM_LOGGING_LEVEL,
58 + "stream": envs.VLLM_LOGGING_STREAM,
59 },
60 },
61 "loggers": {
@@ -144,7 +157,7 @@ _METHODS_TO_PATCH = {
157 def _configure_vllm_root_logger() -> None:
158 logging_config = dict[str, Any]()
159
147 - if not VLLM_CONFIGURE_LOGGING and VLLM_LOGGING_CONFIG_PATH:
160 + if not envs.VLLM_CONFIGURE_LOGGING and envs.VLLM_LOGGING_CONFIG_PATH:
161 raise RuntimeError(
162 "VLLM_CONFIGURE_LOGGING evaluated to false, but "
163 "VLLM_LOGGING_CONFIG_PATH was given. VLLM_LOGGING_CONFIG_PATH "
@@ -152,16 +165,22 @@ def _configure_vllm_root_logger() -> None:
165 "VLLM_CONFIGURE_LOGGING or unset VLLM_LOGGING_CONFIG_PATH."
166 )
167
155 - if VLLM_CONFIGURE_LOGGING:
168 + if envs.VLLM_CONFIGURE_LOGGING:
169 logging_config = DEFAULT_LOGGING_CONFIG
170
158 - if VLLM_LOGGING_CONFIG_PATH:
159 - if not path.exists(VLLM_LOGGING_CONFIG_PATH):
171 + vllm_handler = logging_config["handlers"]["vllm"]
172 + # Refresh these values in case env vars have changed.
173 + vllm_handler["level"] = envs.VLLM_LOGGING_LEVEL
174 + vllm_handler["stream"] = envs.VLLM_LOGGING_STREAM
175 + vllm_handler["formatter"] = "vllm_color" if _use_color() else "vllm"
176 +
177 + if envs.VLLM_LOGGING_CONFIG_PATH:
178 + if not path.exists(envs.VLLM_LOGGING_CONFIG_PATH):
179 raise RuntimeError(
180 "Could not load logging config. File does not exist: %s",
162 - VLLM_LOGGING_CONFIG_PATH,
181 + envs.VLLM_LOGGING_CONFIG_PATH,
182 )
164 - with open(VLLM_LOGGING_CONFIG_PATH, encoding="utf-8") as file:
183 + with open(envs.VLLM_LOGGING_CONFIG_PATH, encoding="utf-8") as file:
184 custom_config = json.loads(file.read())
185
186 if not isinstance(custom_config, dict):
vllm/logging_utils/__init__.py
+2 -1
@@ -1,12 +1,13 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 -from vllm.logging_utils.formatter import NewLineFormatter
4 +from vllm.logging_utils.formatter import ColoredFormatter, NewLineFormatter
5 from vllm.logging_utils.lazy import lazy
6 from vllm.logging_utils.log_time import logtime
7
8 __all__ = [
9 "NewLineFormatter",
10 + "ColoredFormatter",
11 "lazy",
12 "logtime",
13 ]
vllm/logging_utils/formatter.py
+50
@@ -75,3 +75,53 @@ class NewLineFormatter(logging.Formatter):
75 parts = msg.split(record.message)
76 msg = msg.replace("\n", "\r\n" + parts[0])
77 return msg
78 +
79 +
80 +class ColoredFormatter(NewLineFormatter):
81 + """Adds ANSI color codes to log levels for terminal output.
82 +
83 + This formatter adds colors by injecting them into the format string for
84 + static elements (timestamp, filename, line number) and modifying the
85 + levelname attribute for dynamic color selection.
86 + """
87 +
88 + # ANSI color codes
89 + COLORS = {
90 + "DEBUG": "\033[37m", # White
91 + "INFO": "\033[32m", # Green
92 + "WARNING": "\033[33m", # Yellow
93 + "ERROR": "\033[31m", # Red
94 + "CRITICAL": "\033[35m", # Magenta
95 + }
96 + GREY = "\033[90m" # Grey for timestamp and file info
97 + RESET = "\033[0m"
98 +
99 + def __init__(self, fmt, datefmt=None, style="%"):
100 + # Inject grey color codes into format string for timestamp and file info
101 + if fmt:
102 + # Wrap %(asctime)s with grey
103 + fmt = fmt.replace("%(asctime)s", f"{self.GREY}%(asctime)s{self.RESET}")
104 + # Wrap [%(fileinfo)s:%(lineno)d] with grey
105 + fmt = fmt.replace(
106 + "[%(fileinfo)s:%(lineno)d]",
107 + f"{self.GREY}[%(fileinfo)s:%(lineno)d]{self.RESET}",
108 + )
109 +
110 + # Call parent __init__ with potentially modified format string
111 + super().__init__(fmt, datefmt, style)
112 +
113 + def format(self, record):
114 + # Store original levelname to restore later (in case record is reused)
115 + orig_levelname = record.levelname
116 +
117 + # Only modify levelname - it needs dynamic color based on severity
118 + if (color_code := self.COLORS.get(record.levelname)) is not None:
119 + record.levelname = f"{color_code}{record.levelname}{self.RESET}"
120 +
121 + # Call parent format which will handle everything else
122 + msg = super().format(record)
123 +
124 + # Restore original levelname
125 + record.levelname = orig_levelname
126 +
127 + return msg
vllm/utils/system_utils.py
+5 -2
@@ -22,7 +22,7 @@ from .platform_utils import cuda_is_initialized, xpu_is_initialized
22
23 logger = init_logger(__name__)
24
25 -CYAN = "\033[1;36m"
25 +CYAN = "\033[0;36m"
26 RESET = "\033[0;0m"
27
28
@@ -142,7 +142,10 @@ def set_process_title(
142
143 def _add_prefix(file: TextIO, worker_name: str, pid: int) -> None:
144 """Add colored prefix to file output for log decoration."""
145 - prefix = f"{CYAN}({worker_name} pid={pid}){RESET} "
145 + if envs.NO_COLOR:
146 + prefix = f"({worker_name} pid={pid}) "
147 + else:
148 + prefix = f"{CYAN}({worker_name} pid={pid}){RESET} "
149 file_write = file.write
150
151 def write_with_prefix(s: str):