Skip to content

操作日志

操作日志中间件自动拦截写请求(POST/PUT/DELETE/PATCH)并记录操作日志,无需手动埋点。日志落库通过 response.background 异步执行,不阻塞响应。源码位于 src/middleware/operation_log.py

工作原理

1. 请求进入 → operation_log_middleware 判断是否需要记录
2. 仅拦截写请求(POST/PUT/DELETE/PATCH),排除登录/验证码等路径
3. 记录请求参数、响应结果、耗时、操作类型
4. 日志落库挂到 response.background,响应返回后异步执行
5. 不阻塞主请求,对性能影响极小

排除路径

以下路径不记录操作日志:

python
_EXCLUDE_PATHS = (
 '/api/v1/login',
 '/api/v1/logout',
 '/api/v1/captcha',
 '/api/v1/upload',
 '/api/v1/loginlog',
 '/api/v1/operationlog',
 '/api/v1/index/menu',
 '/api/v1/index/user',
)

操作类型自动推断

中间件根据 URL 路径末段自动推断操作类型,映射表定义在 _TYPE_MAP 中:

python
_TYPE_MAP = {
    'add': 1,            # 新增
    'update': 2,         # 修改
    'delete': 3,         # 删除
    'batchdelete': 4,    # 批量删除
    'status': 5,         # 状态设置
    'resetpwd': 6,       # 重置密码
    'updatepassword': 7, # 修改密码
    'import': 8,         # 导入
    'export': 9,         # 导出
    'kickout': 10,       # 强退
    'generate': 11,      # 生成代码
    'clear': 12,         # 清空数据
    'auth': 13,          # 授权
    'refresh': 14,       # 缓存
    'openaccount': 15,   # 开账号
}

def _infer_type(request: Request) -> int:
    """从 URL 路径末段推断操作类型,匹配不到返回 0(其它)"""
    path = request.url.path.rstrip('/')
    last_segment = path.split('/')[-1].lower()
    # 跳过纯数字路径参数(如 /delete/123 → 取 delete)
    if last_segment.isdigit():
        parts = path.split('/')
        last_segment = parts[-2].lower() if len(parts) >= 2 else ''
    return _TYPE_MAP.get(last_segment, 0)
路径末段操作类型类型ID
add新增1
update修改2
delete删除3
batchdelete批量删除4
status状态设置5
resetpwd重置密码6
updatepassword修改密码7
import导入8
export导出9
kickout强退10
generate生成代码11
clear清空数据12
auth授权13
refresh缓存14
openaccount开账号15
其它其它0

日志字段

字段说明
title操作标题(从路由 summary 获取)
type操作类型(自动推断)
method路由方法名(如 v1.level.add
url请求路径
param请求参数(截取前2000字符)
result响应结果(截取前2000字符)
consume_time耗时(毫秒)
status状态:0-成功 1-失败
error错误信息
create_user操作人

查询接口

GET /api/v1/oper/log/list?pageNo=1&pageSize=10
GET /api/v1/oper/log/detail/{id}

性能优化

1. 后台任务:日志落库通过 response.background 异步执行,不阻塞响应
2. 前缀解析:仅解析响应体前 8KB(_READ_LIMIT),避免大响应体全量解析
3. 长度截断:参数和结果截取前 2000 字符(_RESULT_MAX_LEN),防止撑爆日志表
4. 路径排除:登录/验证码等高频路径不记录
5. 路径段边界匹配:排除前缀按 path == prefix 或 prefix + '/' 匹配,避免子串误伤

日志落库的后台任务挂载逻辑:

python
def _attach_background(response, task: BackgroundTask) -> None:
    """把日志落库挂到响应后台任务;若响应已有后台任务则链式追加,避免覆盖"""
    prev = getattr(response, 'background', None)
    if prev is None:
        response.background = task
        return

    async def _combined():
        await prev()
        await task()

    response.background = BackgroundTask(_combined)

背景任务

Starlette 在发送响应体后自动运行 response.background 中的任务。如果响应已有后台任务(如其他中间件添加的),操作日志会链式追加,不会覆盖。

总结

操作日志模块具备以下特点:

1. 自动记录:中间件拦截写请求,无需手动埋点
2. 类型推断:根据 URL 路径自动推断操作类型
3. 异步落库:后台任务执行,不阻塞主请求
4. 性能友好:前缀解析 + 长度截断 + 路径排除
5. 完整信息:记录参数、结果、耗时、操作人等

小蚂蚁云团队 · 提供技术支持