From 8ec160ed46f0ccd7ad5526670fcde9144df8c6fd Mon Sep 17 00:00:00 2001 From: shuwenn <47200617+alphabetc1@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:00:06 +0800 Subject: [PATCH] feature: support uvicorn access log filter(disable logging /metrics) (#15513) --- docs/advanced_features/server_arguments.md | 1 + python/sglang/srt/entrypoints/http_server.py | 2 +- python/sglang/srt/server_args.py | 13 ++ python/sglang/srt/utils/common.py | 121 ++++++++++++++++++- 4 files changed, 135 insertions(+), 2 deletions(-) diff --git a/docs/advanced_features/server_arguments.md b/docs/advanced_features/server_arguments.md index 37c80ec32..efd73a4e4 100644 --- a/docs/advanced_features/server_arguments.md +++ b/docs/advanced_features/server_arguments.md @@ -182,6 +182,7 @@ Please consult the documentation below and [server_args.py](https://github.com/s | `--log-requests-level` | 0: Log metadata (no sampling parameters). 1: Log metadata and sampling parameters. 2: Log metadata, sampling parameters and partial input/output. 3: Log every input/output. | `2` | `0`, `1`, `2`, `3` | | `--log-requests-format` | Format for request logging: 'text' (human-readable) or 'json' (structured) | `text` | `text`, `json` | | `--log-requests-target` | Target(s) for request logging: 'stdout' and/or directory path(s) for file output. Can specify multiple targets, e.g., '--log-requests-target stdout /my/path'. | `None` | List[str] | +| `--uvicorn-access-log-exclude-prefixes` | Exclude uvicorn access logs whose request path starts with any of these prefixes. Defaults to empty (disabled). | `[]` | List[str] | | `--crash-dump-folder` | Folder path to dump requests from the last 5 min before a crash (if any). If not specified, crash dumping is disabled. | `None` | Type: str | | `--show-time-cost` | Show time cost of custom marks. | `False` | bool flag (set to enable) | | `--enable-metrics` | Enable log prometheus metrics. | `False` | bool flag (set to enable) | diff --git a/python/sglang/srt/entrypoints/http_server.py b/python/sglang/srt/entrypoints/http_server.py index 51c090379..d2526f09f 100644 --- a/python/sglang/srt/entrypoints/http_server.py +++ b/python/sglang/srt/entrypoints/http_server.py @@ -1817,7 +1817,7 @@ def launch_server( try: # Update logging configs - set_uvicorn_logging_configs() + set_uvicorn_logging_configs(server_args) # Listen for HTTP requests if server_args.tokenizer_worker_num == 1: diff --git a/python/sglang/srt/server_args.py b/python/sglang/srt/server_args.py index 0a923b72f..78ec3fc71 100644 --- a/python/sglang/srt/server_args.py +++ b/python/sglang/srt/server_args.py @@ -69,6 +69,7 @@ from sglang.utils import is_in_ci logger = logging.getLogger(__name__) # Define constants +DEFAULT_UVICORN_ACCESS_LOG_EXCLUDE_PREFIXES = () SAMPLING_BACKEND_CHOICES = {"flashinfer", "pytorch", "ascend"} LOAD_FORMAT_CHOICES = [ "auto", @@ -346,6 +347,9 @@ class ServerArgs: log_requests_level: int = 2 log_requests_format: str = "text" log_requests_target: Optional[List[str]] = None + uvicorn_access_log_exclude_prefixes: List[str] = dataclasses.field( + default_factory=lambda: list(DEFAULT_UVICORN_ACCESS_LOG_EXCLUDE_PREFIXES) + ) crash_dump_folder: Optional[str] = None show_time_cost: bool = False enable_metrics: bool = False @@ -3112,6 +3116,15 @@ class ServerArgs: help="Target(s) for request logging: 'stdout' and/or directory path(s) for file output. " "Can specify multiple targets, e.g., '--log-requests-target stdout /my/path'. ", ) + parser.add_argument( + "--uvicorn-access-log-exclude-prefixes", + type=str, + nargs="*", + default=list(DEFAULT_UVICORN_ACCESS_LOG_EXCLUDE_PREFIXES), + help="Exclude uvicorn access logs whose request path starts with any of these prefixes. " + "Defaults to empty (disabled). " + "Example: --uvicorn-access-log-exclude-prefixes /metrics /health", + ) parser.add_argument( "--crash-dump-folder", type=str, diff --git a/python/sglang/srt/utils/common.py b/python/sglang/srt/utils/common.py index 0673d9484..31e1074e1 100644 --- a/python/sglang/srt/utils/common.py +++ b/python/sglang/srt/utils/common.py @@ -2314,7 +2314,64 @@ def kill_itself_when_parent_died(): logger.warning("kill_itself_when_parent_died is only supported in linux.") -def set_uvicorn_logging_configs(): +class UvicornAccessLogFilter(logging.Filter): + """Filter uvicorn access logs by request path. + + Notes: + - Uvicorn access records usually provide `request_line` like: "GET /metrics HTTP/1.1". + - We defensively fall back to parsing `record.getMessage()` if needed. + """ + + def __init__(self, excluded_path_prefixes=None): + super().__init__() + excluded_path_prefixes = excluded_path_prefixes or [] + # Normalize once: drop empty prefixes, stringify, keep as tuple (fast iteration, immutable). + self.excluded_path_prefixes = tuple(str(p) for p in excluded_path_prefixes if p) + + def filter(self, record: logging.LogRecord) -> bool: + path = None + + request_line = getattr(record, "request_line", None) + if request_line: + parts = str(request_line).split() + if len(parts) >= 2: + path = parts[1] + + if not path: + # Fallback for non-standard formatters/records + try: + msg = record.getMessage() + except Exception: + msg = None + if msg: + q1 = msg.find('"') + q2 = msg.find('"', q1 + 1) if q1 != -1 else -1 + if q1 != -1 and q2 != -1: + rl = msg[q1 + 1 : q2] + parts = rl.split() + if len(parts) >= 2: + path = parts[1] + + if not path: + return True + + # Strip query string for matching + path = str(path) + # Some proxies/clients may emit absolute-form request-target in logs: + # e.g. "GET https://example.com/metrics HTTP/1.1" -> extract "/metrics". + if "://" in path: + try: + path = urlparse(path).path or path + except Exception: + # If parsing fails, fall back to the raw value. + pass + path = path.split("?", 1)[0] + return not any( + path.startswith(prefix) for prefix in self.excluded_path_prefixes + ) + + +def set_uvicorn_logging_configs(server_args=None): from uvicorn.config import LOGGING_CONFIG LOGGING_CONFIG["formatters"]["default"][ @@ -2326,6 +2383,68 @@ def set_uvicorn_logging_configs(): ] = '[%(asctime)s] %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s' LOGGING_CONFIG["formatters"]["access"]["datefmt"] = "%Y-%m-%d %H:%M:%S" + _configure_uvicorn_access_log_filter(LOGGING_CONFIG, server_args) + + +def _configure_uvicorn_access_log_filter( + uvicorn_logging_config: dict, server_args=None +): + """Configure uvicorn access log path filter into uvicorn LOGGING_CONFIG. + + This optionally filters uvicorn access logs (e.g., suppress noisy /metrics polling). + + Args: + uvicorn_logging_config: The dict-like LOGGING_CONFIG from uvicorn. + server_args: Parsed server args object that may contain: + - uvicorn_access_log_exclude_prefixes (list[str] | tuple[str] | None) + """ + # Optionally filter uvicorn access logs (e.g., suppress noisy /metrics polling). + if server_args is None: + return + + filter_name = "sglang_uvicorn_access_path_filter" + + excluded_prefixes = getattr( + server_args, "uvicorn_access_log_exclude_prefixes", None + ) + if not excluded_prefixes: + return + + # Normalize: accept list/tuple; treat a single string as one prefix (not an iterable of chars). + if isinstance(excluded_prefixes, str): + excluded_prefixes = [excluded_prefixes] + + # De-duplicate while keeping order; drop empty prefixes. + excluded_prefixes = [p for p in excluded_prefixes if p] + excluded_prefixes = list(dict.fromkeys(excluded_prefixes)) + if not excluded_prefixes: + return + + uvicorn_logging_config.setdefault("filters", {}) + uvicorn_logging_config["filters"][filter_name] = { + "()": "sglang.srt.utils.common.UvicornAccessLogFilter", + "excluded_path_prefixes": excluded_prefixes, + } + + # Attach filter to access handler and/or uvicorn.access logger (best-effort across uvicorn versions). + handlers = uvicorn_logging_config.get("handlers", {}) + if "access" in handlers: + filters_list = handlers["access"].setdefault("filters", []) + if not isinstance(filters_list, list): + filters_list = list(filters_list) + handlers["access"]["filters"] = filters_list + if filter_name not in filters_list: + filters_list.append(filter_name) + + loggers_cfg = uvicorn_logging_config.get("loggers", {}) + if "uvicorn.access" in loggers_cfg: + filters_list = loggers_cfg["uvicorn.access"].setdefault("filters", []) + if not isinstance(filters_list, list): + filters_list = list(filters_list) + loggers_cfg["uvicorn.access"]["filters"] = filters_list + if filter_name not in filters_list: + filters_list.append(filter_name) + def get_open_port() -> int: port = os.getenv("SGLANG_PORT")