Skip to content

[Feat] Add --log-format json option for structured logging#849

Merged
ruizhang0101 merged 5 commits intovllm-project:mainfrom
keyuchen21:feat/log-format-json
Feb 26, 2026
Merged

[Feat] Add --log-format json option for structured logging#849
ruizhang0101 merged 5 commits intovllm-project:mainfrom
keyuchen21:feat/log-format-json

Conversation

@keyuchen21
Copy link
Contributor

Summary

  • Adds --log-format json CLI option that outputs structured JSON log lines with timestamp, level, logger, message, filename, and lineno fields
  • Configures uvicorn loggers to use the same JSON formatter when --log-format json is set
  • Adds tests for JsonFormatter, set_log_format, and the --log-format parser argument
  • Updates router README with documentation for --log-level and --log-format options

Closes #416

Test plan

  • pytest src/tests/test_log.py -v — all 21 tests pass (6 new TestJsonFormatter tests)
  • pytest src/tests/test_parser.py -v — all 12 tests pass (2 new --log-format tests)
  • Full test suite shows no regressions from these changes

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @keyuchen21, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant enhancement to the router's logging system by providing an option for structured JSON output. This change allows users to easily switch between human-readable text logs and machine-parseable JSON logs via a new command-line argument. The primary impact is improved log analysis and integration with external logging systems, offering greater flexibility and observability for the application.

Highlights

  • Structured JSON Logging: Introduced a new --log-format json CLI option that enables structured JSON log output, including fields like timestamp, level, logger, message, filename, and lineno.
  • Uvicorn Integration: Configured Uvicorn loggers to seamlessly adopt the new JSON formatter when the --log-format json option is activated, ensuring consistent logging across the application.
  • Comprehensive Testing: Added extensive unit tests for the JsonFormatter, the logic for switching log formats, and the parsing of the new --log-format CLI argument to ensure robustness.
  • Documentation Update: Updated the router's README to document the newly added --log-format option and the existing --log-level option, improving user guidance.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/tests/test_log.py
    • Imported the json module.
    • Added a new TestJsonFormatter class with multiple test methods to verify JSON output, exception handling, and the dynamic switching of log formats.
  • src/tests/test_parser.py
    • Added test_parse_args_log_format_defaults_to_text to confirm the default log format is 'text'.
    • Added test_parse_args_log_format_accepts_json to verify that the parser correctly accepts 'json' as a log format option.
  • src/vllm_router/README.md
    • Updated the 'Logging Options' section to include descriptions for the new --log-format and existing --log-level command-line arguments.
  • src/vllm_router/app.py
    • Imported JsonFormatter and set_log_format from the logging module.
    • Called set_log_format with the parsed command-line argument.
    • Modified the uvicorn.run call to conditionally apply a custom log_config that uses JsonFormatter for Uvicorn's loggers when JSON format is requested, including a mapping for 'trace' log level to 'DEBUG' for compatibility.
  • src/vllm_router/log.py
    • Imported the json module.
    • Added a global _LOG_FORMAT variable, initialized to 'text', to track the current log format.
    • Implemented the set_log_format function to dynamically update the formatters of all registered log handlers to either JsonFormatter or CustomFormatter.
    • Defined a new JsonFormatter class, inheriting from logging.Formatter, to format log records as JSON strings, including exception information when present.
    • Modified the init_logger function to initialize log handlers with either JsonFormatter or CustomFormatter based on the global _LOG_FORMAT.
  • src/vllm_router/parsers/parser.py
    • Added the --log-format command-line argument to the argument parser, allowing choices between 'text' and 'json' with 'text' as the default.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The PR introduces structured JSON logging which is a great addition for observability. The implementation correctly integrates with both the internal router loggers and Uvicorn. I've suggested some improvements to the JsonFormatter to ensure it captures all relevant context (like extra fields and stack traces) and handles non-serializable data gracefully. I also noted a minor inconsistency in how Uvicorn logs are routed compared to the router's own logs.

Comment on lines 40 to 46
formatter = JsonFormatter() if _LOG_FORMAT == "json" else None
for logger in _loggers:
for handler in logger.handlers:
if _LOG_FORMAT == "json":
handler.setFormatter(formatter)
elif isinstance(handler.formatter, JsonFormatter):
handler.setFormatter(CustomFormatter())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When switching back to 'text' format, a new CustomFormatter instance is created for every handler in every logger. It would be more efficient to instantiate the formatter once outside the loops.

Suggested change
formatter = JsonFormatter() if _LOG_FORMAT == "json" else None
for logger in _loggers:
for handler in logger.handlers:
if _LOG_FORMAT == "json":
handler.setFormatter(formatter)
elif isinstance(handler.formatter, JsonFormatter):
handler.setFormatter(CustomFormatter())
if _LOG_FORMAT == "json":
formatter = JsonFormatter()
else:
formatter = CustomFormatter()
for logger in _loggers:
for handler in logger.handlers:
if _LOG_FORMAT == "json":
handler.setFormatter(formatter)
elif isinstance(handler.formatter, JsonFormatter):
handler.setFormatter(formatter)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — applied. Formatter is now instantiated once before the loop to avoid redundant allocations. Fixed in 8f7003a.

Comment on lines 78 to 89
def format(self, record):
log_record = {
"timestamp": self.formatTime(record, self.datefmt),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"filename": record.filename,
"lineno": record.lineno,
}
if record.exc_info and record.exc_info[0] is not None:
log_record["exception"] = self.formatException(record.exc_info)
return json.dumps(log_record)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The JsonFormatter currently omits several important pieces of information for structured logging, such as extra fields passed to the logger and stack_info. Additionally, json.dumps may raise a TypeError if any field (especially in extra) is not JSON-serializable.

Consider updating the formatter to include these fields and using default=str to handle non-serializable objects. You might also want to ensure timestamps are in UTC for consistency in cloud environments.

    def format(self, record):
        log_record = {
            "timestamp": self.formatTime(record, self.datefmt),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
            "filename": record.filename,
            "lineno": record.lineno,
        }
        if record.exc_info and record.exc_info[0] is not None:
            log_record["exception"] = self.formatException(record.exc_info)
        if record.stack_info:
            log_record["stack_info"] = self.formatStack(record.stack_info)

        # Include extra fields
        standard_attrs = {
            "args", "asctime", "created", "exc_info", "exc_text", "filename",
            "funcName", "levelname", "levelno", "lineno", "module", "msecs",
            "message", "msg", "name", "pathname", "process", "processName",
            "relativeCreated", "stack_info", "thread", "threadName"
        }
        for key, value in record.__dict__.items():
            if key not in standard_attrs and not key.startswith("_"):
                log_record[key] = value

        return json.dumps(log_record, default=str)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially addressed in 8f7003a:

  • Added default=str to json.dumps as a safety net for non-serializable objects.
  • Added stack_info support.
  • Skipping the extra fields iteration for now — the codebase doesn't use extra in any logging calls, and blindly dumping record.__dict__ could leak unexpected data into structured logs. Happy to revisit if there's an actual use case.

Comment on lines +372 to +383
"handlers": {
"default": {
"formatter": "json",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
"access": {
"formatter": "json",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the JSON log configuration for Uvicorn, both the default and access handlers are routed to sys.stdout. This is inconsistent with init_logger (used for the router's own logs), which routes logs with level WARNING and above to stderr.

While some users prefer all structured logs on stdout, maintaining consistency across the application is usually better for log collection. Consider routing the default handler (which handles errors) to stderr to match the router's behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentionally keeping both uvicorn handlers on stdout — JSON logging is typically consumed by log aggregators (CloudWatch, Datadog, etc.) where a single stream is standard practice. Splitting stdout/stderr adds complexity for JSON consumers with no real benefit. The router's own split is useful for colored text output but less relevant for machine-parsed JSON.

Add a JsonFormatter that outputs log records as JSON with timestamp,
level, logger, message, filename, and lineno fields. The new
--log-format flag (choices: text, json) controls the output format
for both the router loggers and uvicorn.

Signed-off-by: Keyu Chen <54015474+keyuchen21@users.noreply.github.com>
Add TestJsonFormatter class covering JSON output validation, exception
inclusion/exclusion, format switching via set_log_format, and
init_logger format respect. Add parser tests verifying --log-format
defaults to text and accepts json. Update README logging options
documentation.

Signed-off-by: Keyu Chen <54015474+keyuchen21@users.noreply.github.com>
Instantiate formatter once outside the loop in set_log_format to avoid
redundant allocations. Add stack_info support and default=str fallback
to JsonFormatter for robustness. Add tests for stack_info inclusion and
non-serializable object handling.

Signed-off-by: Keyu Chen <54015474+keyuchen21@users.noreply.github.com>
keyuchen21 and others added 2 commits February 22, 2026 18:05
Signed-off-by: Keyu Chen <54015474+keyuchen21@users.noreply.github.com>
Signed-off-by: Rui Zhang <51696593+ruizhang0101@users.noreply.github.com>
Copy link
Collaborator

@ruizhang0101 ruizhang0101 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM :)))

@ruizhang0101 ruizhang0101 merged commit 2837b16 into vllm-project:main Feb 26, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feature: Add json logging

2 participants