| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 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.""" |
| 14 | |
| 15 | def __init__(self, fmt, datefmt=None, style="%", *, log_level: str | None = None): |
| 16 | super().__init__(fmt, datefmt, style) |
| 17 | |
| 18 | self.use_relpath = log_level == "DEBUG" |
| 19 | if self.use_relpath: |
| 20 | self.root_dir = Path(__file__).resolve().parent.parent.parent |
| 21 | |
| 22 | def format(self, record): |
| 23 | def shrink_path(relpath: Path) -> str: |
| 24 | """Shortens a file path for logging display: |
| 25 | - Removes leading 'vllm' folder if present. |
| 26 | - If path starts with 'v1', |
| 27 | keeps the first two and last two levels, |
| 28 | collapsing the middle as '...'. |
| 29 | - Otherwise, keeps the first and last two levels, |
| 30 | collapsing the middle as '...'. |
| 31 | - If the path is short, returns it as-is. |
| 32 | - Examples: |
| 33 | vllm/model_executor/layers/quantization/utils/fp8_utils.py -> |
| 34 | model_executor/.../quantization/utils/fp8_utils.py |
| 35 | vllm/model_executor/layers/quantization/awq.py -> |
| 36 | model_executor/layers/quantization/awq.py |
| 37 | |
| 38 | Args: |
| 39 | relpath (Path): The relative path to be shortened. |
| 40 | |
| 41 | Returns: |
| 42 | str: The shortened path string for display. |
| 43 | |
| 44 | """ |
| 45 | parts = list(relpath.parts) |
| 46 | new_parts = [] |
| 47 | if parts and parts[0] == "vllm": |
| 48 | parts = parts[1:] |
| 49 | if parts and parts[0] == "v1": |
| 50 | new_parts += parts[:2] |
| 51 | parts = parts[2:] |
| 52 | elif parts: |
| 53 | new_parts += parts[:1] |
| 54 | parts = parts[1:] |
| 55 | if len(parts) > 2: |
| 56 | new_parts += ["..."] + parts[-2:] |
| 57 | else: |
| 58 | new_parts += parts |
| 59 | return "/".join(new_parts) |
| 60 | |
| 61 | if self.use_relpath: |
| 62 | abs_path = getattr(record, "pathname", None) |
| 63 | if abs_path: |
| 64 | try: |
| 65 | relpath = Path(abs_path).resolve().relative_to(self.root_dir) |
| 66 | except Exception: |
| 67 | relpath = Path(record.filename) |
| 68 | else: |
| 69 | relpath = Path(record.filename) |
| 70 | record.fileinfo = shrink_path(relpath) |
| 71 | else: |
| 72 | record.fileinfo = record.filename |
| 73 | |
| 74 | msg = super().format(record) |
| 75 | if record.message != "": |
| 76 | parts = msg.split(record.message) |
| 77 | msg = msg.replace("\n", "\r\n" + parts[0]) |
| 78 | return msg |
| 79 | |
| 80 | |
| 81 | class ColoredFormatter(NewLineFormatter): |
| 82 | """Adds ANSI color codes to log levels for terminal output. |
| 83 | |
| 84 | This formatter adds colors by injecting them into the format string for |
| 85 | static elements (timestamp, filename, line number) and modifying the |
| 86 | levelname attribute for dynamic color selection. |
| 87 | """ |
| 88 | |
| 89 | # ANSI color codes |
| 90 | COLORS = { |
| 91 | "DEBUG": "\033[37m", # White |
| 92 | "INFO": "\033[32m", # Green |
| 93 | "WARNING": "\033[33m", # Yellow |
| 94 | "ERROR": "\033[31m", # Red |
| 95 | "CRITICAL": "\033[35m", # Magenta |
| 96 | } |
| 97 | GREY = "\033[90m" # Grey for timestamp and file info |
| 98 | PROCESS_PREFIX_COLOR = "\033[0;36m" |
| 99 | PROCESS_PREFIX_RESET = "\033[0;0m" |
| 100 | RESET = "\033[0m" |
| 101 | |
| 102 | def __init__(self, fmt, datefmt=None, style="%", *, log_level: str | None = None): |
| 103 | # Inject grey color codes into format string for timestamp and file info |
| 104 | if fmt: |
| 105 | # Wrap process prefix with cyan |
| 106 | fmt = fmt.replace( |
| 107 | "(%(vllm_process_name)s pid=%(process)d)", |
| 108 | f"{self.PROCESS_PREFIX_COLOR}" |
| 109 | "(%(vllm_process_name)s pid=%(process)d)" |
| 110 | f"{self.PROCESS_PREFIX_RESET}", |
| 111 | ) |
| 112 | # Wrap %(asctime)s with grey |
| 113 | fmt = fmt.replace("%(asctime)s", f"{self.GREY}%(asctime)s{self.RESET}") |
| 114 | # Wrap [%(fileinfo)s:%(lineno)d] with grey |
| 115 | fmt = fmt.replace( |
| 116 | "[%(fileinfo)s:%(lineno)d]", |
| 117 | f"{self.GREY}[%(fileinfo)s:%(lineno)d]{self.RESET}", |
| 118 | ) |
| 119 | |
| 120 | # Call parent __init__ with potentially modified format string |
| 121 | super().__init__(fmt, datefmt, style, log_level=log_level) |
| 122 | |
| 123 | def format(self, record): |
| 124 | # Store original levelname to restore later (in case record is reused) |
| 125 | orig_levelname = record.levelname |
| 126 | |
| 127 | # Only modify levelname - it needs dynamic color based on severity |
| 128 | if (color_code := self.COLORS.get(record.levelname)) is not None: |
| 129 | record.levelname = f"{color_code}{record.levelname}{self.RESET}" |
| 130 | |
| 131 | # Call parent format which will handle everything else |
| 132 | msg = super().format(record) |
| 133 | |
| 134 | # Restore original levelname |
| 135 | record.levelname = orig_levelname |
| 136 | |
| 137 | return msg |