diff --git a/fastapi/applications.py b/fastapi/applications.py index 4af1146b0d..8aa51a5cd3 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -13,6 +13,7 @@ from fastapi.exception_handlers import ( from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError from fastapi.logger import logger from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware +from fastapi.middleware.slow_request import SlowRequestMiddleware from fastapi.openapi.docs import ( get_redoc_html, get_swagger_ui_html, @@ -1029,7 +1030,8 @@ class FastAPI(Starlette): exception_handlers[key] = value middleware = ( - [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)] # ty: ignore[invalid-argument-type] + [Middleware(SlowRequestMiddleware)] + + [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)] # ty: ignore[invalid-argument-type] + self.user_middleware + [ Middleware( diff --git a/fastapi/middleware/__init__.py b/fastapi/middleware/__init__.py index 620296d5ad..1ea0da8850 100644 --- a/fastapi/middleware/__init__.py +++ b/fastapi/middleware/__init__.py @@ -1 +1,3 @@ from starlette.middleware import Middleware as Middleware + +from fastapi.middleware.slow_request import SlowRequestMiddleware diff --git a/fastapi/middleware/slow_request.py b/fastapi/middleware/slow_request.py new file mode 100644 index 0000000000..1c9617d1cc --- /dev/null +++ b/fastapi/middleware/slow_request.py @@ -0,0 +1,51 @@ +import logging +import time + +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response +from starlette.types import ASGIApp + +logger = logging.getLogger("fastapi") + + +class SlowRequestMiddleware(BaseHTTPMiddleware): + """ + Middleware to monitor slow requests and log warnings when they exceed the threshold. + """ + + def __init__(self, app: ASGIApp, threshold: float = 0.5) -> None: + """ + Initialize the SlowRequestMiddleware. + + Args: + app: The ASGI application. + threshold: The threshold in seconds for considering a request as slow. + Defaults to 0.5 seconds (500ms). + """ + super().__init__(app) + self.threshold = threshold + + async def dispatch(self, request: Request, call_next) -> Response: + """ + Dispatch the request, measuring the time taken. + + Args: + request: The incoming request. + call_next: The next middleware or endpoint handler. + + Returns: + The response from the next middleware or endpoint handler. + """ + start_time = time.perf_counter() + try: + response = await call_next(request) + return response + finally: + duration = time.perf_counter() - start_time + if duration > self.threshold: + method = request.method + url = str(request.url) + logger.warning( + f"Slow request: {method} {url} took {duration:.2f}s (threshold: {self.threshold:.2f}s)" + ) diff --git a/tests/test_slow_request_middleware.py b/tests/test_slow_request_middleware.py new file mode 100644 index 0000000000..14476e2b05 --- /dev/null +++ b/tests/test_slow_request_middleware.py @@ -0,0 +1,53 @@ +import asyncio +import logging +import time + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +@app.get("/fast") +async def fast_endpoint(): + return {"message": "Fast response"} + + +@app.get("/slow") +async def slow_endpoint(): + await asyncio.sleep(0.6) + return {"message": "Slow response"} + + +def test_slow_request_logging(caplog): + caplog.set_level(logging.WARNING) + client = TestClient(app) + + # 测试快速请求,不应记录警告 + response = client.get("/fast") + assert response.status_code == 200 + assert "Slow request" not in caplog.text + + # 测试慢速请求,应记录警告 + response = client.get("/slow") + assert response.status_code == 200 + + # 检查日志是否包含预期的警告信息 + slow_request_logs = [record for record in caplog.records if "Slow request" in record.message] + assert len(slow_request_logs) == 1 + + log_message = slow_request_logs[0].message + assert "GET" in log_message + assert "/slow" in log_message + # 检查是否包含两位小数的时间 + import re + match = re.search(r"took (\d+\.\d{2})s", log_message) + assert match is not None, f"Log message should contain time with two decimal places: {log_message}" + + print("✅ 慢请求监控中间件测试通过!") + print(f" 日志消息: {log_message}") + + +if __name__ == "__main__": + import pytest + pytest.main([__file__, "-v", "-s"])