金天牛

导航

selenium并发请求的实现-并发日志篇

前言  

    前面我们使用了线程、进程、协程实现了并发。在实践操作中,报错时发现定位问题很麻烦:不清楚报错日志属于哪个并发请求的操作。

     那么有没有什么办法好区分日志属于哪个的请求呢:答案是在日志格式中加上请求ID,这样每个请求就能根据ID来分辨是哪个的请求了。

实现:

     首先用到的是contextvars库,contextvars模块提供了管理、存储和访问上下文局部状态的API,它在Python 3.7版本中引入。

     其次用到的是UUID,它是一个通用唯一标识符,用来标明当前。

     接着需要用的是 FastAPI 框架中的 HTTP 中间件(middleware),中间件可以用作日志记录、CORS处理,认证授权、性能监控、异常处理、请求/响应修改等

     下面我们使用fastapi框架结合loguru日志模块来演示下如何实现并发日志。

示例代码:

 1.测试日志是否正常,代码如下:

 1 from uuid import uuid4
 2 from contextvars import ContextVar
 3 from fastapi import FastAPI, Request
 4 from loguru import logger
 5 
 6 
 7 # 初始化FastAPI应用
 8 app = FastAPI()
 9 
10 # 创建线程安全的上下文变量
11 request_id_ctx = ContextVar("request_id", default="system")
12 
13 # 配置loguru日志格式(使用extra字段前需要先绑定)
14 logger.configure(extra={"request_id": ""})  # 初始化extra字段
15 logger.add("app.log",
16            format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {extra[request_id]} | {message}",
17            enqueue=True)  # enqueue确保多进程安全
18 
19 
20 @app.middleware("http")               #中间件
21 async def request_middleware(request: Request, call_next):
22     # 生成唯一请求ID
23     rid = f"REQ-{uuid4()}"
24 
25     # 设置到上下文变量(需要保存token防止提前回收)
26     token = request_id_ctx.set(rid)
27 
28     # 将request_id存入请求对象(可选,方便在路由中获取)
29     request.state.request_id = rid
30 
31     try:
32         # 处理请求前日志(通过上下文获取)
33         with logger.contextualize(request_id=rid):
34             logger.info("Request started")
35         response = await call_next(request)
36 
37     finally:
38         # 确保清理上下文(关键修正:避免内存泄漏)
39         request_id_ctx.reset(token)
40 
41     # 添加请求ID到响应头(可选)
42     response.headers["X-Request-ID"] = rid
43     return response
44 
45 
46 # 获取当前请求日志器(通过contextvars获取)
47 def get_logger():
48     return logger.bind(request_id=request_id_ctx.get())
49 
50 
51 # 测试接口
52 @app.get("/test")
53 async def test_endpoint(request: Request):
54     current_logger = get_logger()
55     current_logger.info("logger进入处理流程")
56     # 模拟业务处理
57     import time
58     time.sleep(0.1)
59     current_logger.info("logger处理完成")
60 
61     return {
62         "request_id": request.state.request_id,
63         "status": "success"
64     }
65 
66 if __name__ == "__main__":
67     import uvicorn
68     uvicorn.run(app, host="0.0.0.0", port=8000)

 

2. 修改第一篇的百度请求,使用并发实现,并加入日志,代码如下:

 

 1 import time
 2 from selenium import webdriver
 3 from selenium.webdriver.common.by import By
 4 from selenium.webdriver.chrome.options import Options
 5 from concurrent.futures import ThreadPoolExecutor
 6 import asyncio
 7 from uuid import uuid4
 8 from contextvars import ContextVar
 9 from loguru import logger
10 from fastapi import FastAPI, Request
11 import sys
12 
13 app = FastAPI()
14 executor = ThreadPoolExecutor(max_workers=10)   # 创建一个线程池,最大线程数设置为10
15 request_id_ctx = ContextVar("request_id", default="system")
16 logger.configure(handlers=[], extra={"request_id": ""})  # 初始化extra字段.handlers清空默认日志输出
17 logger.add("app.log",format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {extra[request_id]} | {file.name}:{line} | {message}",colorize=True,enqueue=True)  # enqueue确保多进程安全
18 logger.add(sys.stderr,format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level}</level> | <cyan>{extra[request_id]}</cyan> | <blue>{file.name}:{line}</blue> | <level>{message}</level>",colorize=True,enqueue=True)  # enqueue确保多进程安全
19 
20 
21 @app.middleware("http")               #中间件
22 async def request_middleware(request: Request, call_next):
23     # 生成唯一请求ID
24     rid = f"REQ-{uuid4()}"
25     # 设置到上下文变量(需要保存token防止提前回收)
26     token = request_id_ctx.set(rid)
27     # 将request_id存入请求对象(可选,方便在路由中获取)
28     request.state.request_id = rid
29     try:
30         # 处理请求前日志(通过上下文获取)
31         with logger.contextualize(request_id=rid):
32             logger.info("Request started")
33             response = await call_next(request)
34     finally:
35         # 确保清理上下文,避免内存泄漏
36         request_id_ctx.reset(token)
37     # 添加请求ID到响应头(可选)
38     response.headers["X-Request-ID"] = rid
39     return response
40 
41 
42 def sync_search_baidu(key_word: str, request_id: str):
43     """实际的搜索逻辑"""
44     # 在子线程中设置上下文变量
45     logger_ctx = logger.bind(request_id=request_id)
46     options = Options()
47     # options.add_argument('--headless') # 启用无头模式
48     options.add_argument('--disable-gpu')  # 禁用GPU加速
49     driver = webdriver.Chrome(options=options)
50     driver.maximize_window()  # 最大化窗口
51     search_result = []
52     try:
53         driver.get("https://www.baidu.com")
54         logger_ctx.info(f"browser is opened,Searching for '{key_word}'...")
55         driver.find_element(By.ID, "kw").send_keys(key_word)
56         driver.find_element(By.ID, "su").click()
57         time.sleep(10)
58         search_results = driver.find_elements(By.XPATH, "//div[@id='content_left']//h3//p/span/span")
59         search_result = [result.text for result in search_results]
60         logger_ctx.info(f"Search completed for '{key_word}'. Results: {search_result}")
61         return search_result
62     except Exception as e:
63         return search_result.append(f"Error occurred while searching for '{key_word}': {e}")
64     finally:
65         driver.quit()
66 
67 
68 @app.get("/search/{key_word}")
69 async def search_baidu(key_word: str, request: Request):
70     """执行百度搜索的异步封装"""
71     loop = asyncio.get_event_loop()  # 获取当前事件循环
72     request_id = request.state.request_id
73     try:
74         # 在线程池中执行同步的Selenium操作
75         return await loop.run_in_executor(
76             executor,
77             lambda: sync_search_baidu(key_word, request_id)
78         )
79     except Exception as e:
80         return {"error": str(e)}
81 
82 
83 if __name__ == '__main__':
84     import uvicorn
85     uvicorn.run(app, host="0.0.0.0", port=8000)

 

 

posted on 2025-06-27 14:23  金天牛  阅读(41)  评论(0)    收藏  举报