请求日志与错误识别¶
系统将每个业务请求记录到 PostgreSQL 的 request_log 表,用于请求级别的分析与错误自动识别。
与 audit_event(安全审计,只记录敏感操作)不同,request_log 是访问日志,覆盖每一个业务请求。
目标¶
- 直接查看每一个业务请求,主要用于分析。
- 自动识别各类错误(4xx / 5xx),并按错误类型细分。
采集范围¶
- 记录业务请求:
/api/*接口与页面路由,跳过静态资源(复用is_frontend_resource_path)。 - 默认排除
/api/scanner/heartbeat(高频心跳,避免污染与放大数据量),其余扫描器接口保留。 - 采集开关可配置(见「配置」)。
数据模型¶
CREATE TABLE IF NOT EXISTS request_log (
id BIGSERIAL PRIMARY KEY,
request_id UUID NOT NULL,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
http_method TEXT NOT NULL,
request_path TEXT NOT NULL,
status_code SMALLINT NOT NULL,
duration_ms INTEGER,
client_ip INET,
user_agent TEXT,
actor_id INT, -- 已认证用户 id;匿名/扫描器请求为 NULL
actor_name TEXT, -- 已认证用户名;匿名/扫描器请求为 NULL
error_level TEXT NOT NULL DEFAULT 'ok'
CHECK (error_level IN ('ok', 'client_error', 'server_error')),
error_type TEXT -- bad_request / unauthorized / not_found / ...
);
CREATE INDEX IF NOT EXISTS idx_request_log_time
ON request_log (occurred_at DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_request_log_error
ON request_log (error_level, occurred_at DESC);
索引只保留 2 个:
idx_request_log_time:分页、趋势、保留清理。idx_request_log_error:错误查询与错误率趋势。
不建 request_path、status_code、request_id 单列索引:request_path 仅偶尔排查、列表查询自带时间范围;
status_code 与 error_level/error_type 冗余;request_log 每请求一行,request_id 近乎唯一、索引无收益。
采集与错误分类¶
采集点在 main.rs 最外层 wrap_fn:
service.call(req)之前抓取http_method、request_path、client_ip、user_agent。- 响应后抓取
status_code与duration_ms(Instant::elapsed)。 - 按状态码派生
error_level与error_type:
| status | error_level | error_type |
|---|---|---|
| 400 | client_error | bad_request |
| 401 | client_error | unauthorized |
| 403 | client_error | forbidden |
| 404 | client_error | not_found |
| 408 | client_error | timeout |
| 409 | client_error | conflict |
| 422 | client_error | validation |
| 429 | client_error | rate_limited |
| 其他 4xx | client_error | client_error |
| 5xx | server_error | server_error |
| 其余 | ok | NULL |
操作者归属¶
wrap_fn 在最外层,鉴权通过的 UserBaseI 由内层 AuthMiddleware 注入 extensions,
而 req 已被 service.call(req) move 走,外层拿不到。因此不通过 audit_event 关联操作者
(audit_event 只记录敏感操作,多数请求没有对应事件),而是用共享槽位直接采集:
pub struct RequestActor(pub Arc<std::sync::Mutex<Option<(i32, String)>>>); // (user_id, username)
wrap_fn在service.call前req.extensions_mut().insert(RequestActor(actor.clone())),自己保留克隆。AuthMiddleware鉴权成功后(现有Ok((true, uid))分支,拿到tmp_info之后)写槽位:*slot.0.lock().unwrap() = Some((tmp_info.id, tmp_info.username.clone()))。wrap_fn响应后读槽位,写入actor_id/actor_name。
锁都在 await 完成后持有,无跨 await 持锁,使用 std::sync::Mutex 即可。
匿名请求、被 401/403 拒绝的请求、扫描器请求(is_scanner_api_path 直通、不注入 UserBaseI)均为 NULL。
写入策略与失败处理¶
不逐请求同步写库,使用「有界 channel + 后台 writer」:
- 入口(
wrap_fn):有界mpscchannel(容量 8192),try_send满则同步丢弃该条,请求响应零阻塞。 - 后台 writer(
main中tokio::spawn,永不退出): - 攒批,每批 ≤500 行,单事务多行
INSERT提交;或每 500ms 定时 flush。 - flush 失败 → 重试 2 次(间隔 50ms / 200ms),重试期间保留本批。
- 仍失败 → 整批丢弃 +
dropped计数自增 +error!记一条日志,继续下一轮循环。 - 绝不无限重入队、绝不阻塞、绝不退出。
dropped 计数通过日志暴露,当前未并入 summary。
查询接口¶
仅 role = 1 的管理员可访问:
GET /api/requests:分页查询,支持method、path(contains)、status、error_level、error_type、request_id、start、end、page、page_size。GET /api/requests/summary:聚合统计。
start / end 使用 RFC 3339 时间。
聚合与分位数¶
summary 拆两层语义,避免大表实时 percentile_cont 全表扫描:
| 聚合类型 | 支持范围 | 实现 |
|---|---|---|
计数聚合 + 趋势(总请求、error_type 分布、date_trunc 时间分桶) |
任意时间范围 + 过滤 | COUNT / date_trunc,走 time/error 索引 |
| 延迟 P95 / P99 | 仅预设窗口:1h / 24h / 7d | percentile_cont,窗口内走 time 索引 |
不提供「任意范围 × 任意过滤 × 实时 P95/P99」。
前端¶
/requests 页面仅管理员可访问:
- 过滤栏:method、path、status、error_type、仅看错误、时间范围、request_id。
- 汇总卡片:总请求数、错误率、P95 耗时、5xx 数。
- 趋势图:请求量 + 错误数随时间(复用 ApexCharts,对齐
analysis.vue)。 - 可过滤分页表格:时间、方法、路径、状态徽章、耗时、IP、request_id、错误类型徽章。
保留与清理¶
- 配置
retention_days(默认 30 天)。 - 当前使用单表和分批
DELETE清理,避免长事务锁。 - 当前未使用按天分区;应根据实际保留量和清理耗时评估容量。
配置¶
在“设置 → 运行时配置 → 请求日志”发布配置。对应 requests 数据库记录缺失时,以下 TOML 用于初始化;已有记录不会被修改文件或重启覆盖。发布接口与版本冲突规则见服务端运行时配置。
[requests]
enabled = true # 是否采集请求日志
retention_days = 30 # 在线保留天数
隐私与安全¶
request_log不记录请求体、原始查询字符串、Session、CSRF Token、Authorization 请求头、 扫描器注册/运行 Token 及其哈希。只记录 method、path(不含 query)、状态码、耗时、IP、UA 与操作者。- 查询接口仅管理员可见,复用审计接口的鉴权方式。
- 生产环境建议沿用
audit_event的角色权限模型:应用角色只持有request_log的INSERT/SELECT。