From 7287093772078d58ffbba2fc8bca84fd441c6bcf Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 27 Jul 2024 11:00:36 +0800 Subject: [PATCH] Update operation log middleware info reading (#368) * Fix operation log middleware summary reading * update ua info and ip info * update opera middle functions * move dataclasses.py * fix lint --- backend/common/dataclasses.py | 32 +++++++++ backend/middleware/opera_log_middleware.py | 80 +++++++++++----------- backend/utils/request_parse.py | 13 ++-- 3 files changed, 80 insertions(+), 45 deletions(-) create mode 100644 backend/common/dataclasses.py diff --git a/backend/common/dataclasses.py b/backend/common/dataclasses.py new file mode 100644 index 00000000..1643cf31 --- /dev/null +++ b/backend/common/dataclasses.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import dataclasses + +from fastapi import Response + +from backend.common.enums import StatusType + + +@dataclasses.dataclass +class IpInfo: + ip: str + country: str | None + region: str | None + city: str | None + + +@dataclasses.dataclass +class UserAgentInfo: + user_agent: str + os: str | None + browser: str | None + device: str | None + + +@dataclasses.dataclass +class RequestCallNextReturn: + code: str + msg: str + status: StatusType + err: Exception | None + response: Response diff --git a/backend/middleware/opera_log_middleware.py b/backend/middleware/opera_log_middleware.py index 8234e375..f6539dc9 100644 --- a/backend/middleware/opera_log_middleware.py +++ b/backend/middleware/opera_log_middleware.py @@ -10,7 +10,8 @@ from starlette.requests import Request from backend.app.admin.schema.opera_log import CreateOperaLogParam from backend.app.admin.service.opera_log_service import OperaLogService -from backend.common.enums import OperaLogCipherType +from backend.common.dataclasses import RequestCallNextReturn +from backend.common.enums import OperaLogCipherType, StatusType from backend.common.log import log from backend.core.conf import settings from backend.utils.encrypt import AESCipher, ItsDCipher, Md5Cipher @@ -28,88 +29,91 @@ class OperaLogMiddleware(BaseHTTPMiddleware): return await call_next(request) # 请求解析 - user_agent, device, os, browser = await parse_user_agent_info(request) - ip, country, region, city = await parse_ip_info(request) + ip_info = await parse_ip_info(request) + ua_info = await parse_user_agent_info(request) try: # 此信息依赖于 jwt 中间件 username = request.user.username except AttributeError: username = None method = request.method - router = request.scope.get('route') - summary = getattr(router, 'summary', None) or '' args = await self.get_request_args(request) args = await self.desensitization(args) # 设置附加请求信息 - request.state.ip = ip - request.state.country = country - request.state.region = region - request.state.city = city - request.state.user_agent = user_agent - request.state.os = os - request.state.browser = browser - request.state.device = device + request.state.ip = ip_info.ip + request.state.country = ip_info.country + request.state.region = ip_info.region + request.state.city = ip_info.city + request.state.user_agent = ua_info.user_agent + request.state.os = ua_info.os + request.state.browser = ua_info.browser + request.state.device = ua_info.device # 执行请求 start_time = timezone.now() - code, msg, status, err, response = await self.execute_request(request, call_next) + res = await self.execute_request(request, call_next) end_time = timezone.now() cost_time = (end_time - start_time).total_seconds() * 1000.0 + # 此信息只能在请求后获取 + _route = request.scope.get('route') + summary = getattr(_route, 'summary', None) or '' + # 日志创建 opera_log_in = CreateOperaLogParam( username=username, method=method, title=summary, path=path, - ip=ip, - country=country, - region=region, - city=city, - user_agent=user_agent, - os=os, - browser=browser, - device=device, + ip=request.state.ip, + country=request.state.country, + region=request.state.region, + city=request.state.city, + user_agent=request.state.user_agent, + os=request.state.os, + browser=request.state.browser, + device=request.state.device, args=args, - status=status, - code=code, - msg=msg, + status=res.status, + code=res.code, + msg=res.msg, cost_time=cost_time, opera_time=start_time, ) create_task(OperaLogService.create(obj_in=opera_log_in)) # noqa: ignore # 错误抛出 + err = res.err if err: raise err from None - return response + return res.response - async def execute_request(self, request: Request, call_next) -> tuple: + async def execute_request(self, request: Request, call_next) -> RequestCallNextReturn: """执行请求""" + code = 200 + msg = 'Success' + status = StatusType.enable err = None response = None try: response = await call_next(request) - code, msg, status = await self.request_exception_handler(request) except Exception as e: log.exception(e) + code, msg = await self.request_exception_handler(request, code, msg) # code 处理包含 SQLAlchemy 和 Pydantic - code = getattr(e, 'code', None) or 500 - msg = getattr(e, 'msg', None) or 'Internal Server Error' - status = 0 + code = getattr(e, 'code', None) or code + msg = getattr(e, 'msg', None) or msg + status = StatusType.disable err = e - return str(code), msg, status, err, response + return RequestCallNextReturn(code=str(code), msg=msg, status=status, err=err, response=response) @staticmethod @sync_to_async - def request_exception_handler(request: Request) -> tuple: + def request_exception_handler(request: Request, code: int, msg: str) -> tuple[str, str]: """请求异常处理器""" - code = 200 - msg = 'Success' - status = 1 try: http_exception = request.state.__request_http_exception__ except AttributeError: @@ -117,7 +121,6 @@ class OperaLogMiddleware(BaseHTTPMiddleware): else: code = http_exception.get('code', 500) msg = http_exception.get('msg', 'Internal Server Error') - status = 0 try: validation_exception = request.state.__request_validation_exception__ except AttributeError: @@ -125,8 +128,7 @@ class OperaLogMiddleware(BaseHTTPMiddleware): else: code = validation_exception.get('code', 400) msg = validation_exception.get('msg', 'Bad Request') - status = 0 - return code, msg, status + return code, msg @staticmethod async def get_request_args(request: Request) -> dict: diff --git a/backend/utils/request_parse.py b/backend/utils/request_parse.py index 3edc78b0..c73b4e34 100644 --- a/backend/utils/request_parse.py +++ b/backend/utils/request_parse.py @@ -7,6 +7,7 @@ from fastapi import Request from user_agents import parse from XdbSearchIP.xdbSearcher import XdbSearcher +from backend.common.dataclasses import IpInfo, UserAgentInfo from backend.common.log import log from backend.core.conf import settings from backend.core.path_conf import IP2REGION_XDB @@ -75,13 +76,13 @@ def get_location_offline(ip: str) -> dict | None: return None -async def parse_ip_info(request: Request) -> tuple[str, str, str, str]: +async def parse_ip_info(request: Request) -> IpInfo: country, region, city = None, None, None ip = await get_request_ip(request) location = await redis_client.get(f'{settings.IP_LOCATION_REDIS_PREFIX}:{ip}') if location: country, region, city = location.split(' ') - return ip, country, region, city + return IpInfo(ip=ip, country=country, region=region, city=city) if settings.LOCATION_PARSE == 'online': location_info = await get_location_online(ip, request.headers.get('User-Agent')) elif settings.LOCATION_PARSE == 'offline': @@ -97,14 +98,14 @@ async def parse_ip_info(request: Request) -> tuple[str, str, str, str]: f'{country} {region} {city}', ex=settings.IP_LOCATION_EXPIRE_SECONDS, ) - return ip, country, region, city + return IpInfo(ip=ip, country=country, region=region, city=city) @sync_to_async -def parse_user_agent_info(request: Request) -> tuple[str, str, str, str]: +def parse_user_agent_info(request: Request) -> UserAgentInfo: user_agent = request.headers.get('User-Agent') _user_agent = parse(user_agent) - device = _user_agent.get_device() os = _user_agent.get_os() browser = _user_agent.get_browser() - return user_agent, device, os, browser + device = _user_agent.get_device() + return UserAgentInfo(user_agent=user_agent, device=device, os=os, browser=browser)