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):