[Core][Logging] Add built-in JSON formatter (#58739)

Add `--logging-config.formatter=json` to enable the python-json-logger formatter without having to use a complete Python logging dictConfig. Signed-off-by: Mark McLoughlin <markmc@redhat.com> Co-authored-by: OpenAI Codex <noreply@openai.com>

Mark McLoughlin committed Oct 1, 2026 at 09:11 UTC 91fcec12350aa01cd151d866fd76a307df17d120
11 files changed +148 -46
.buildkite/test_areas/misc.yaml
+2 -2
@@ -530,7 +530,7 @@ steps:
530 - vllm/v1/
531 - vllm/version.py
532 - cmake/utils.cmake
533 - - tests/test_access_log_filter.py
533 + - tests/test_uvicorn_logging.py
534 - tests/test_attention_backend_registry.py
535 - tests/test_audio_media_size_precheck.py
536 - tests/test_cmake_utils.py
@@ -571,7 +571,7 @@ steps:
571 - pip install cmake # test_cmake_utils.py shells out to cmake; the CPU image has none
572 - python3 standalone_tests/lazy_imports.py
573 - pytest -v -s device_allocator
574 - - pytest -v -s test_access_log_filter.py
574 + - pytest -v -s test_uvicorn_logging.py
575 - pytest -v -s test_attention_backend_registry.py
576 - pytest -v -s test_audio_media_size_precheck.py
577 - pytest -v -s test_cmake_utils.py
examples/features/logging_configuration.md
+53 -17
@@ -6,6 +6,7 @@ robust and flexible configuration of the various loggers used by vLLM.
6 For `vllm serve`, configure logging with CLI arguments:
7
8 - Use the built-in configuration, optionally with `--log-level`.
9 +- Emit structured JSON with `--logging-config.formatter json`.
10 - Use a custom Python logging configuration file with
11 `--logging-config.pylogging_config_file`.
12 - Disable vLLM logging configuration with
@@ -14,7 +15,7 @@ For `vllm serve`, configure logging with CLI arguments:
15 ## CLI logging configuration
16
17 `--logging-config` accepts a JSON object. Its fields are `log_level`,
17 -`configure_logging`, and `pylogging_config_file`. For example:
18 +`formatter`, `configure_logging`, and `pylogging_config_file`. For example:
19
20 ```bash
21 vllm serve mistralai/Mistral-7B-v0.1 \
@@ -28,6 +29,25 @@ vllm serve mistralai/Mistral-7B-v0.1 \
29 --logging-config.log_level DEBUG
30 ```
31
32 +The built-in `text` formatter is selected by default. To emit one JSON object
33 +per vLLM and Uvicorn log record, select the built-in `json` formatter:
34 +
35 +```bash
36 +vllm serve mistralai/Mistral-7B-v0.1 \
37 + --logging-config.formatter json
38 +```
39 +
40 +The built-in JSON output includes `asctime`, `levelname`, `name`,
41 +`processName`, `process`, `message`, and `vllm_process_name`. `processName` is
42 +Python's process name, `vllm_process_name` identifies vLLM's logical process
43 +(including worker ranks where applicable), and `process` is the
44 +operating-system PID.
45 +
46 +The built-in profile formats records emitted through vLLM's Python loggers and
47 +Uvicorn's server and access loggers when using `vllm serve`. It does not convert
48 +direct writes to `stdout` or `stderr`. Use a custom configuration to format
49 +other loggers.
50 +
51 `--log-level` is a shortcut for `--logging-config.log_level` and takes
52 precedence if both are supplied. It sets the level of vLLM's built-in logging
53 configuration. `--log-config-file` is deprecated and will be removed in
@@ -41,11 +61,11 @@ The custom logging configuration file must be JSON following Python's [logging
61 configuration dictionary
62 schema](https://docs.python.org/3/library/logging.config.html#dictionary-schema-details).
63
44 -!!! note "Custom configurations override `--log-level`"
64 +!!! note "Custom configurations override built-in formatter settings"
65 When `pylogging_config_file` is set, vLLM loads that JSON file and replaces
46 - its built-in `dictConfig`; it does not merge the two. Therefore,
47 - `--log-level` applies only when no custom configuration file is provided.
48 - Set logger and handler levels in the custom file itself. vLLM applies the
66 + its built-in `dictConfig`; it does not merge the two. Therefore, `log_level`
67 + and `formatter` apply only when no custom configuration file is provided.
68 + Set levels and formatters in the custom file itself. vLLM applies the
69 resolved configuration in its child processes as well.
70
71 ## Environment variables
@@ -62,10 +82,11 @@ for those settings.
82
83 ### Example 1: Customize vLLM root logger
84
65 -For this example, we will customize the vLLM root logger to use
85 +For more control over fields and handlers, configure the vLLM root logger and
86 +Uvicorn's server and access loggers with
87 [`python-json-logger`](https://github.com/nhairs/python-json-logger)
67 -(which is part of the container image) to log to
68 -STDOUT of the console in JSON format with a log level of `INFO`.
88 +(which is part of the container image) to log to STDOUT in JSON format at
89 +`INFO` level.
90
91 To begin, first, create an appropriate JSON logging configuration file:
92
@@ -92,8 +113,24 @@ To begin, first, create an appropriate JSON logging configuration file:
113 "handlers": ["console"],
114 "level": "INFO",
115 "propagate": false
116 + },
117 + "uvicorn": {
118 + "handlers": ["console"],
119 + "level": "INFO",
120 + "propagate": false
121 + },
122 + "uvicorn.error": {
123 + "handlers": ["console"],
124 + "level": "INFO",
125 + "propagate": false
126 + },
127 + "uvicorn.access": {
128 + "handlers": ["console"],
129 + "level": "INFO",
130 + "propagate": false
131 }
132 },
133 + "disable_existing_loggers": false,
134 "version": 1
135 }
136 ```
@@ -105,21 +142,20 @@ vllm serve mistralai/Mistral-7B-v0.1 --max-model-len 2048 \
142 --logging-config.pylogging_config_file /path/to/logging_config.json
143 ```
144
108 -Each vLLM log record is one JSON object and includes `vllm_process_name`, which
109 -identifies vLLM's logical process (including worker ranks where applicable).
110 -The standard `process` record attribute contains the operating-system PID.
111 -vLLM avoids altering `stdout` or `stderr`, so no text is prepended to JSON
112 -log records.
145 +With this custom configuration, each vLLM and Uvicorn log record is one JSON
146 +object. The example selects `vllm_process_name` and `process` explicitly; both
147 +fields are also present in the built-in JSON output described above. vLLM
148 +avoids altering `stdout` or `stderr`, so no text is prepended to JSON log
149 +records.
150
151 This applies to records emitted through the configured Python loggers. A JSON
152 formatter cannot convert unrelated output into JSON, such as a third-party
153 library writing directly to `stdout` or `stderr`; configure or route such
154 output separately when a consumer requires every collected line to be JSON.
155
119 -When serving, `VLLM_LOGGING_CONFIG_PATH` is also used as Uvicorn's logging
120 -configuration. This example configures only `vllm`; configure `uvicorn`,
121 -`uvicorn.error`, and `uvicorn.access` with a JSON handler when those server
122 -logs must also be structured.
156 +When serving, vLLM also passes this file to Uvicorn as its logging
157 +configuration. The `uvicorn`, `uvicorn.error`, and `uvicorn.access` entries
158 +format its server and access logs as JSON.
159
160 ### Example 2: Silence a particular vLLM logger
161
tests/entrypoints/launchers/test_cli_args.py
+3
@@ -76,6 +76,8 @@ def test_logging_config_cli_args(serve_parser):
76 [
77 "--logging-config",
78 '{"log_level":"WARNING","pylogging_config_file":"/tmp/json.json"}',
79 + "--logging-config.formatter",
80 + "json",
81 "--log-level",
82 "DEBUG",
83 "--log-config-file",
@@ -85,6 +87,7 @@ def test_logging_config_cli_args(serve_parser):
87
88 config = AsyncEngineArgs.from_cli_args(args).create_logging_config()
89 assert config.log_level == "DEBUG"
90 + assert config.formatter == "json"
91 assert config.pylogging_config_file == "/tmp/flat.json"
92
93
tests/test_logger.py
+31
@@ -4,6 +4,7 @@ import enum
4 import io
5 import json
6 import logging
7 +import multiprocessing
8 import os
9 import sys
10 import tempfile
@@ -21,6 +22,7 @@ from vllm.config import LoggingConfig
22 from vllm.logger import (
23 _DATE_FORMAT,
24 _FORMAT,
25 + _JSON_FORMAT,
26 _configure_vllm_root_logger,
27 _use_color,
28 configure_logging,
@@ -204,6 +206,35 @@ def test_configure_logging_preserves_application_record_factory(
206 logging.setLogRecordFactory(original_factory)
207
208
209 +def test_builtin_json_formatter(monkeypatch):
210 + output = io.StringIO()
211 +
212 + try:
213 + with monkeypatch.context() as context:
214 + context.setattr(sys, "stdout", output)
215 + context.setattr(vllm_logger, "_vllm_process_info", None)
216 + _configure_vllm_root_logger(LoggingConfig(formatter="json"))
217 + decorate_logs("Worker_DP0")
218 + init_logger("vllm.structured_log_probe").info("structured log probe")
219 +
220 + log = json.loads(output.getvalue())
221 + formatter = logging.getLogger("vllm").handlers[0].formatter
222 + finally:
223 + _configure_vllm_root_logger(LoggingConfig())
224 +
225 + assert log == {
226 + "asctime": log["asctime"],
227 + "levelname": "INFO",
228 + "name": "vllm.structured_log_probe",
229 + "processName": multiprocessing.current_process().name,
230 + "process": os.getpid(),
231 + "message": "structured log probe",
232 + "vllm_process_name": "Worker_DP0",
233 + }
234 + assert formatter is not None
235 + assert formatter._fmt == _JSON_FORMAT
236 +
237 +
238 def test_use_color_force_color(monkeypatch):
239 """FORCE_COLOR forces colored logs without a TTY, while NO_COLOR and an
240 explicit VLLM_LOGGING_COLOR=0 take precedence over it."""
tests/test_uvicorn_logging.py renamed
+2 -2
@@ -1,10 +1,10 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -"""Tests for the UvicornAccessLogFilter class."""
3 +"""Tests for Uvicorn logging configuration and access log filtering."""
4
5 import logging
6
7 -from vllm.logging_utils.access_log_filter import (
7 +from vllm.logging_utils.uvicorn_logging import (
8 UvicornAccessLogFilter,
9 create_uvicorn_log_config,
10 )
vllm/config/logging.py
+8 -4
@@ -9,6 +9,7 @@ import vllm.envs as envs
9 from vllm.config.utils import config
10
11 LogLevel = Literal["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"]
12 +LogFormatter = Literal["text", "json"]
13
14
15 @config
@@ -17,8 +18,8 @@ class LoggingConfig:
18
19 Supply a JSON object with ``--logging-config`` or individual fields with
20 dotted arguments such as ``--logging-config.log_level DEBUG`` and
20 - ``--logging-config.pylogging_config_file logging.json``. ``--log-level``
21 - and legacy ``--log-config-file`` override their matching fields. Set
21 + ``--logging-config.formatter json``. ``--log-level`` and legacy
22 + ``--log-config-file`` override their matching fields. Set
23 ``configure_logging`` to false to skip applying a ``dictConfig``.
24 """
25
@@ -27,6 +28,9 @@ class LoggingConfig:
28 )
29 """Log level used when no custom logging configuration is provided."""
30
31 + formatter: LogFormatter = "text"
32 + """Formatter used by the built-in logging configuration."""
33 +
34 configure_logging: bool = Field(default_factory=lambda: envs.VLLM_CONFIGURE_LOGGING)
35 """Whether to apply a Python logging configuration.
36
@@ -41,6 +45,6 @@ class LoggingConfig:
45 """Path to a Python logging JSON file using
46 [``dictConfig`` schema](https://docs.python.org/3/library/logging.config.html#configuration-file-format).
47
44 - A custom ``dictConfig`` is authoritative over ``log_level`` for logger,
45 - handler, and formatter settings.
48 + A custom ``dictConfig`` is authoritative over ``log_level`` and
49 + ``formatter`` for logger, handler, and formatter settings.
50 """
vllm/entrypoints/launchers/utils/server_utils.py
+9 -6
@@ -8,6 +8,7 @@ from contextlib import asynccontextmanager
8 from fastapi import FastAPI
9
10 from vllm import envs
11 +from vllm.config.logging import LogFormatter
12 from vllm.engine.protocol import EngineClient
13 from vllm.logger import init_logger
14 from vllm.utils.gc_utils import freeze_gc_heap
@@ -33,8 +34,8 @@ def get_uvicorn_log_config(args: Namespace) -> dict | None:
34
35 Priority:
36 1. If log_config_file is specified, use it
36 - 2. If disable_access_log_for_endpoints is specified, create a config with
37 - the access log filter
37 + 2. If disable_access_log_for_endpoints or JSON formatting is specified,
38 + create a config with the requested filter and format
39 3. Otherwise, return None (use uvicorn defaults)
40 """
41 # First, try to load from file if specified
@@ -48,19 +49,21 @@ def get_uvicorn_log_config(args: Namespace) -> dict | None:
49 if log_config is not None:
50 return log_config
51
51 - # If endpoints to filter are specified, create a config with the filter
52 - if args.disable_access_log_for_endpoints:
52 + formatter: LogFormatter = (
53 + logging_config.formatter if logging_config is not None else "text"
54 + )
55 + if args.disable_access_log_for_endpoints or formatter == "json":
56 from vllm.logging_utils import create_uvicorn_log_config
57
55 - # Parse comma-separated string into list
58 excluded_paths = [
59 p.strip()
58 - for p in args.disable_access_log_for_endpoints.split(",")
60 + for p in (args.disable_access_log_for_endpoints or "").split(",")
61 if p.strip()
62 ]
63 return create_uvicorn_log_config(
64 excluded_paths=excluded_paths,
65 log_level=args.uvicorn_log_level,
66 + formatter=formatter,
67 )
68
69 return None
vllm/logger.py
+16 -4
@@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, Any, Literal, cast
20
21 import vllm.envs as envs
22 from vllm.logging_utils import ColoredFormatter, NewLineFormatter
23 +from vllm.logging_utils.formatter import JSON_FORMAT as _JSON_FORMAT
24
25 if TYPE_CHECKING:
26 from vllm.config.logging import LoggingConfig
@@ -30,7 +31,6 @@ _FORMAT = (
31 "[%(fileinfo)s:%(lineno)d] %(message)s"
32 )
33 _DATE_FORMAT = "%m-%d %H:%M:%S"
33 -
34 _vllm_process_info: tuple[str, int] | None = None
35
36
@@ -211,10 +211,12 @@ def _configure_vllm_root_logger(config: "LoggingConfig | None" = None) -> None:
211 configure_logging = envs.VLLM_CONFIGURE_LOGGING
212 log_level = envs.VLLM_LOGGING_LEVEL
213 log_config_file = envs.VLLM_LOGGING_CONFIG_PATH
214 + log_formatter = "text"
215 else:
216 configure_logging = config.configure_logging
217 log_level = config.log_level
218 log_config_file = config.pylogging_config_file
219 + log_formatter = config.formatter
220
221 if not configure_logging and log_config_file:
222 raise RuntimeError(
@@ -233,12 +235,22 @@ def _configure_vllm_root_logger(config: "LoggingConfig | None" = None) -> None:
235 # Refresh these values in case env vars have changed.
236 vllm_handler["level"] = log_level
237 vllm_handler["stream"] = envs.VLLM_LOGGING_STREAM
236 - vllm_handler["formatter"] = "vllm_color" if _use_color() else "vllm"
238 + for formatter in logging_config["formatters"].values():
239 + formatter["log_level"] = log_level
240 +
241 + if log_formatter == "json":
242 + logging_config["formatters"] = {
243 + "vllm_json": {
244 + "class": "pythonjsonlogger.jsonlogger.JsonFormatter",
245 + "format": _JSON_FORMAT,
246 + }
247 + }
248 + vllm_handler["formatter"] = "vllm_json"
249 + else:
250 + vllm_handler["formatter"] = "vllm_color" if _use_color() else "vllm"
251
252 vllm_loggers = logging_config["loggers"]["vllm"]
253 vllm_loggers["level"] = log_level
240 - for formatter in logging_config["formatters"].values():
241 - formatter["log_level"] = log_level
254
255 if log_config_file:
256 if not path.exists(log_config_file):
vllm/logging_utils/__init__.py
+4 -4
@@ -1,14 +1,14 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 -from vllm.logging_utils.access_log_filter import (
5 - UvicornAccessLogFilter,
6 - create_uvicorn_log_config,
7 -)
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 from vllm.logging_utils.torch_tensor import tensors_str_no_data
8 +from vllm.logging_utils.uvicorn_logging import (
9 + UvicornAccessLogFilter,
10 + create_uvicorn_log_config,
11 +)
12
13 __all__ = [
14 "NewLineFormatter",
vllm/logging_utils/formatter.py
+4
@@ -4,6 +4,10 @@
4 import logging
5 from pathlib import Path
6
7 +JSON_FORMAT = (
8 + "%(asctime)s %(levelname)s %(name)s %(processName)s %(process)d %(message)s"
9 +)
10 +
11
12 class NewLineFormatter(logging.Formatter):
13 """Adds logging prefix to newlines to align multi-line messages."""
vllm/logging_utils/uvicorn_logging.py renamed
+16 -7
@@ -1,15 +1,13 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -"""Access log filter for uvicorn to exclude specific endpoints from logging.
4 -
5 -This module provides a logging filter that can be used to suppress access logs
6 -for specific endpoints (e.g., /health, /metrics) to reduce log noise in
7 -production environments.
8 -"""
3 +"""Uvicorn logging configuration and access log filtering."""
4
5 import logging
6 +from typing import Literal
7 from urllib.parse import urlparse
8
9 +from vllm.logging_utils.formatter import JSON_FORMAT
10 +
11
12 class UvicornAccessLogFilter(logging.Filter):
13 """A logging filter that excludes access logs for specified endpoint paths.
@@ -70,8 +68,9 @@ class UvicornAccessLogFilter(logging.Filter):
68 def create_uvicorn_log_config(
69 excluded_paths: list[str] | None = None,
70 log_level: str = "info",
71 + formatter: Literal["text", "json"] = "text",
72 ) -> dict:
74 - """Create a uvicorn logging configuration with access log filtering.
73 + """Create a uvicorn logging configuration with optional access filtering.
74
75 This function generates a logging configuration dictionary that can be
76 passed to uvicorn's `log_config` parameter. It sets up the access log
@@ -80,6 +79,7 @@ def create_uvicorn_log_config(
79 Args:
80 excluded_paths: List of URL paths to exclude from access logs.
81 log_level: The log level for uvicorn loggers.
82 + formatter: Format used for uvicorn logs.
83
84 Returns:
85 A dictionary containing the logging configuration.
@@ -140,4 +140,13 @@ def create_uvicorn_log_config(
140 },
141 },
142 }
143 + if formatter == "json":
144 + json_formatter = {
145 + "class": "pythonjsonlogger.jsonlogger.JsonFormatter",
146 + "format": JSON_FORMAT,
147 + }
148 + config["formatters"] = {
149 + "default": json_formatter.copy(),
150 + "access": json_formatter.copy(),
151 + }
152 return config