aiohttp中间件实现异步请求日志与重试

在异步 HTTP 请求场景中,aiohttp 是 Python 生态下的主流选择。实际开发中,请求的日志记录(排查问题)和失败重试(提升稳定性)是必备能力,而 aiohttp 的中间件机制能优雅地实现这两个功能,无需侵入业务代码。本文将详细讲解如何基于 aiohttp 中间件,打造通用的异步请求日志与重试组件。

一、核心概念:aiohttp 中间件

aiohttp 中间件是一个异步函数 ,它介于ClientSession和实际 HTTP 请求之间,能拦截请求的发起、响应的返回以及异常的抛出。其核心作用是对请求 / 响应生命周期进行统一处理,格式如下:

python

运行

复制代码
async def middleware(app, handler):
    async def wrapper(request):
        # 请求发送前的处理(如日志、参数修改)
        response = await handler(request)  # 执行实际请求
        # 响应返回后的处理(如日志、响应解析)
        return response
    return wrapper

中间件的优势在于:一次定义,全局生效,所有通过ClientSession发起的请求都会经过中间件处理。

二、实现思路分析

我们需要实现两个核心功能,且需保证逻辑解耦:

  1. 日志中间件:记录请求的 URL、方法、状态码、耗时、请求体(可选)、响应体(可选)等关键信息;
  2. 重试中间件:捕获指定类型的异常(如网络超时、5xx 服务器错误),按照配置的次数和间隔重试请求;
  3. 执行顺序:重试中间件应包裹日志中间件(先重试,再记录最终的请求结果)。

三、完整代码实现

3.1 依赖安装

首先确保安装 aiohttp 和异步重试依赖(tenacity支持异步重试,比手动写循环更优雅):

bash

运行

复制代码
pip install aiohttp tenacity python-dotenv

3.2 核心代码

python

运行

复制代码
import asyncio
import logging
import time
from typing import Dict, Any, Optional

import aiohttp
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential,
    retry_if_exception_type,
    AsyncRetrying,
)

# 配置日志格式
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    handlers=[logging.StreamHandler()]
)
logger = logging.getLogger("aiohttp-requests")

# -------------------------- 日志中间件 --------------------------
async def logging_middleware(app: aiohttp.web.Application, handler):
    """
    记录请求的关键信息:URL、方法、状态码、耗时、异常等
    """
    async def wrapper(request: aiohttp.ClientRequest):
        start_time = time.time()
        request_id = f"req-{int(start_time * 1000)}"  # 生成唯一请求ID
        try:
            # 执行实际请求
            response = await handler(request)
            # 计算耗时
            elapsed = round((time.time() - start_time) * 1000, 2)
            # 记录成功日志
            logger.info(
                f"[{request_id}] SUCCESS | {request.method} {request.url} | "
                f"Status: {response.status} | Elapsed: {elapsed}ms"
            )
            return response
        except Exception as e:
            # 记录失败日志
            elapsed = round((time.time() - start_time) * 1000, 2)
            logger.error(
                f"[{request_id}] FAILED | {request.method} {request.url} | "
                f"Error: {str(e)} | Elapsed: {elapsed}ms",
                exc_info=True  # 打印异常堆栈,便于排查
            )
            raise  # 抛出异常,让重试中间件处理
    return wrapper

# -------------------------- 重试中间件 --------------------------
async def retry_middleware(app: aiohttp.web.Application, handler):
    """
    对指定异常的请求进行重试:
    - 重试次数:3次
    - 等待策略:指数退避(1s, 2s, 4s)
    - 重试条件:网络超时、5xx错误、连接错误
    """
    async def wrapper(request: aiohttp.ClientRequest):
        # 定义重试策略
        retryer = AsyncRetrying(
            stop=stop_after_attempt(3),  # 最多重试3次
            wait=wait_exponential(multiplier=1, min=1, max=4),  # 指数等待
            retry=retry_if_exception_type(
                (
                    aiohttp.ClientTimeoutError,  # 超时异常
                    aiohttp.ClientConnectionError,  # 连接异常
                    aiohttp.ServerConnectionError,  # 服务器连接异常
                )
            ),
            before_sleep=lambda retry_state: logger.warning(
                f"Retry {retry_state.attempt_number} for {request.method} {request.url} "
                f"due to {retry_state.outcome.exception()}"
            ),  # 重试前打印日志
        )

        try:
            # 执行带重试的请求
            response = await retryer.__aenter__()
            try:
                return await handler(request)
            finally:
                await retryer.__aexit__(None, None, None)
        except Exception as e:
            logger.error(f"All retries failed for {request.method} {request.url}: {str(e)}")
            raise
    return wrapper

# -------------------------- 扩展ClientSession --------------------------
class CustomClientSession(aiohttp.ClientSession):
    """
    封装带日志和重试的ClientSession,简化使用
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 注册中间件(注意顺序:重试在外,日志在内)
        self._middlewares = [retry_middleware, logging_middleware]

# -------------------------- 测试代码 --------------------------
async def test_requests():
    """测试异步请求日志与重试功能"""
    # 使用自定义的Session
    async with CustomClientSession(timeout=aiohttp.ClientTimeout(total=5)) as session:
        # 测试正常请求
        try:
            async with session.get("https://httpbin.org/get") as resp:
                print(f"Normal request: {resp.status}")
        except Exception as e:
            print(f"Normal request failed: {e}")

        # 测试会触发重试的请求(模拟超时/5xx)
        try:
            async with session.get("https://httpbin.org/delay/10") as resp:  # 超时请求
                print(f"Retry request: {resp.status}")
        except Exception as e:
            print(f"Retry request failed: {e}")

if __name__ == "__main__":
    asyncio.run(test_requests())

四、代码关键说明

4.1 日志中间件

  • 生成唯一请求 ID:便于关联同一次请求的日志,排查问题时可快速定位;
  • 记录核心指标:请求方法、URL、状态码、耗时,失败时打印异常堆栈;
  • 不吞异常:记录日志后重新抛出异常,保证重试中间件能捕获并处理。

4.2 重试中间件

  • 基于tenacity实现异步重试:相比手动写循环,tenacity支持更灵活的重试策略(指数退避、最大次数、自定义条件);
  • 精准重试:仅对网络超时、连接错误等临时异常重试,避免对业务异常(如 400 参数错误)无效重试;
  • 重试日志:每次重试前打印日志,便于观察重试过程。

4.3 自定义 ClientSession

  • 封装中间件注册逻辑:业务代码只需使用CustomClientSession,无需重复注册中间件;
  • 中间件顺序:重试中间件在外,日志中间件在内,保证每次重试的请求都能被日志记录。

五、进阶优化

5.1 可配置化

将重试次数、等待时间、日志级别等配置抽离到配置文件(如.env),便于不同环境调整:

python

运行

复制代码
# 从环境变量读取配置
import os
from dotenv import load_dotenv

load_dotenv()
RETRY_MAX_ATTEMPTS = int(os.getenv("RETRY_MAX_ATTEMPTS", 3))
RETRY_WAIT_MULTIPLIER = float(os.getenv("RETRY_WAIT_MULTIPLIER", 1))

5.2 支持 5xx 状态码重试

默认重试只捕获异常,可扩展为对 5xx 响应码重试:

python

运行

复制代码
# 在重试中间件中新增判断逻辑
async def wrapper(request: aiohttp.ClientRequest):
    async def _handle_request():
        response = await handler(request)
        if response.status >= 500:
            raise aiohttp.ServerErrorResponse(
                f"Server error: {response.status}", status=response.status
            )
        return response

    # 将原handler替换为_handle_request
    retryer = AsyncRetrying(...)
    try:
        response = await retryer.__aenter__()
        try:
            return await _handle_request()
        finally:
            await retryer.__aexit__(None, None, None)
    # ...

5.3 忽略指定 URL 重试

对不需要重试的 URL(如写操作接口)添加白名单:

python

运行

复制代码
# 重试中间件中添加过滤逻辑
NO_RETRY_URLS = ["/api/write", "/api/submit"]
if any(url in str(request.url) for url in NO_RETRY_URLS):
    return await handler(request)  # 直接执行,不重试

六、使用注意事项

  1. 重试幂等性 :仅对幂等请求(如 GET 查询)重试,POST/PUT 等写操作需确保接口幂等,避免重复提交;
  2. 超时设置:单个请求的超时时间不宜过长,结合重试次数,避免整体请求耗时过久;
  3. 日志脱敏:若请求包含敏感信息(如 token、密码),需在日志中脱敏,避免泄露;
  4. 中间件顺序:重试中间件需在日志中间件外层,否则重试的请求不会被日志记录。

总结

  1. aiohttp 中间件能无侵入地实现请求日志和重试,核心是通过拦截handler执行过程,添加通用逻辑;
  2. 日志中间件重点记录请求 ID、耗时、状态码和异常,重试中间件基于tenacity实现精准的异步重试;
  3. 使用时需注意中间件顺序、重试幂等性和超时控制,保证功能可靠且不引入新问题。

通过上述实现,你可以快速为 aiohttp 异步请求添加企业级的日志和重试能力,提升项目的可维护性和稳定性。

相关推荐
Swift社区2 小时前
Docker 构建 Python FastAPI 镜像最佳实践
python·docker·fastapi
MarkHD2 小时前
Python RPA七日实战:用pyautogui打造第一个自动化脚本
python·自动化·rpa
m0_736919102 小时前
实战:用Python分析某电商销售数据
jvm·数据库·python
乔江seven2 小时前
【python轻量级Web框架 Flask 】1 Flask 初识
开发语言·后端·python·flask
Bruk.Liu2 小时前
(LangChain实战3):LangChain阻塞式invoke与流式stream的调用
人工智能·python·langchain
岱宗夫up2 小时前
Scrapy框架实战教程(上):从入门到实战,搭建你的第一个专业爬虫
爬虫·python·scrapy
Bruk.Liu2 小时前
(LangChain实战4):LangChain消息模版PromptTemplate
人工智能·python·langchain
SunnyRivers2 小时前
Asyncio 提速秘籍:用 run_in_executor 与 to_thread 巧解同步阻塞难题
python·asyncio·to_thread·run_in_executor
亚林瓜子2 小时前
pyspark分组计数
python·spark·pyspark·分组统计