Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/vllm_router/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import logging
import threading
from contextlib import asynccontextmanager

Expand Down Expand Up @@ -103,6 +104,28 @@
logger = init_logger(__name__)


class _EndpointAccessLogFilter(logging.Filter):
"""Suppress uvicorn access-log records for a fixed set of request paths.

Handy for silencing health/readiness probes (e.g. ``/health``,
``/metrics``) that would otherwise flood the router logs while keeping
access logs for real traffic. uvicorn emits each access record with
positional ``args`` of ``(client_addr, method, path, http_version,
status_code)``; we match on ``path`` (ignoring any query string).
"""

def __init__(self, endpoints):
super().__init__()
self._endpoints = {e.strip() for e in endpoints if e and e.strip()}

def filter(self, record: logging.LogRecord) -> bool:
if self._endpoints and record.args and len(record.args) >= 3:
path = str(record.args[2]).split("?", 1)[0]
if path in self._endpoints:
return False
return True
Comment on lines +121 to +126

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

If record.args is a dictionary (which can happen if a log message uses named placeholders), len(record.args) returns the number of keys. If the dictionary has 3 or more keys, record.args[2] will raise a KeyError because 2 is not a valid key. To prevent potential logging crashes, we should explicitly verify that record.args is a sequence (like a tuple or list) before indexing it.

Suggested change
def filter(self, record: logging.LogRecord) -> bool:
if self._endpoints and record.args and len(record.args) >= 3:
path = str(record.args[2]).split("?", 1)[0]
if path in self._endpoints:
return False
return True
def filter(self, record: logging.LogRecord) -> bool:
if (
self._endpoints
and isinstance(record.args, (tuple, list))
and len(record.args) >= 3
):
path = str(record.args[2]).split("?", 1)[0]
if path in self._endpoints:
return False
return True



@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.aiohttp_client_wrapper.start()
Expand Down Expand Up @@ -393,6 +416,19 @@ def main():
# many concurrent requests active.
set_ulimit()

# Optionally drop access logs for noisy probe endpoints. The filter is
# attached to the ``uvicorn.access`` logger before uvicorn configures its
# own logging; uvicorn's dictConfig replaces handlers but preserves
# existing logger filters, so the suppression survives both text and json
# log formats.
disabled_endpoints = parse_comma_separated_args(
args.disable_access_log_for_endpoints
)
if disabled_endpoints:
logging.getLogger("uvicorn.access").addFilter(
_EndpointAccessLogFilter(disabled_endpoints)
)
Comment on lines +419 to +430

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

In Python's logging.config.dictConfig (which uvicorn calls during startup), any existing filters on loggers are cleared if they are not explicitly defined in the configuration dictionary (unless incremental=True is used, which is not the case here). As a result, adding the filter to uvicorn.access before calling uvicorn.run() will cause it to be silently removed during startup, making the suppression ineffective.

To resolve this, we should store the parsed endpoints in app.state and apply the filter inside the FastAPI lifespan context manager, which executes after uvicorn has completed its logging configuration.

For example, in lifespan:

@asynccontextmanager
async def lifespan(app: FastAPI):
    # ... existing code ...
    disabled_endpoints = getattr(app.state, "disable_access_log_for_endpoints", None)
    if disabled_endpoints:
        logging.getLogger("uvicorn.access").addFilter(
            _EndpointAccessLogFilter(disabled_endpoints)
        )
    yield
    # ...
Suggested change
# Optionally drop access logs for noisy probe endpoints. The filter is
# attached to the ``uvicorn.access`` logger before uvicorn configures its
# own logging; uvicorn's dictConfig replaces handlers but preserves
# existing logger filters, so the suppression survives both text and json
# log formats.
disabled_endpoints = parse_comma_separated_args(
args.disable_access_log_for_endpoints
)
if disabled_endpoints:
logging.getLogger("uvicorn.access").addFilter(
_EndpointAccessLogFilter(disabled_endpoints)
)
app.state.disable_access_log_for_endpoints = parse_comma_separated_args(
args.disable_access_log_for_endpoints
)


uvicorn_kwargs = {
"host": args.host,
"port": args.port,
Expand Down
9 changes: 9 additions & 0 deletions src/vllm_router/parsers/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,15 @@ def parse_args():
help="Log output format. 'text' for human-readable colored output, "
"'json' for structured JSON logging. Default is 'text'.",
)
parser.add_argument(
"--disable-access-log-for-endpoints",
type=str,
default=None,
help="Comma-separated list of request paths whose uvicorn access "
"logs should be suppressed, e.g. '/health,/metrics'. Useful to keep "
"liveness/readiness probe noise out of the router logs while "
"preserving access logs for real traffic.",
)

parser.add_argument(
"--sentry-dsn",
Expand Down
Loading