1. 页面概览
1.1 是什么
「审计日志」页(对应前端源码 action/web/src/views/apollo/AuditLogsPage.vue,页面标题「Apollo 审计日志」,TAD-11 批次 2 平台管理组)是 LightApollo 的 HTTP 控制面操作审计查询页:以只读方式分页检索后端记录的每一次 API 调用,并用「汇总卡片 + 多维筛选 + 行点击展开详情」把「谁、在什么时候、调了哪个接口、带了什么参数、成功还是被拒、耗时多少」完整还原出来。数据源是后端每收到一次请求就异步落库的 apollo_audit_logs 表(HTTP 审计),写入者是注册在根路由组、位于 requestIDMiddleware 之后的 httpAuditMiddleware——它覆盖登录/注册/Agent 拉取等公开端点与全部受保护端点,只有探针(/health、/api/health)与 API 文档(/api/v1/docs)属噪声被跳过。所以本页的定位是「Apollo 控制面访问的留痕台账」:从账号登录、创建项目、增删成员,到某次部署发起、渠道改指向、签名者启停,凡经过 HTTP 网关的操作都能在这里查到对应行。
在实际运维中,本页最常用的两个场景分别是「行为留痕取证」与「慢接口/失败归因」:前者在发生越权尝试、误操作删数据、凭据盗用等事件后,用 denied 结果筛选或按时间窗拉出对应记录,还原「发生了什么、是谁干的、带了什么参数」;后者在系统异常时按时间窗把某段时间的全部调用拉出来,用耗时(ms)列定位慢请求、用状态码列粗筛失败端点。需要注意,审计行里**没有保存响应头里的 X-Request-ID(即 envelope 的 request_id)**——该 ID 只在当次请求的响应里返回,用于排障对照,并不随记录落库;跨请求关联只能靠 timestamp + username + method + path 近似匹配(见 8 章)。此外日志表目前没有内置保留期与自动清理任务,演示环境会持续累积(量级通常不大;如需删除需由数据库侧手工处理)。
先分清本页与相邻数据的关系:HTTP 审计(本页数据,表 apollo_audit_logs)记录的是「一次 HTTP 请求」的进出信息——方法、路径、状态码、耗时、客户端 IP、请求体与查询串(脱敏后)、结果归类;而业务生命周期审计是另一套东西——部署推进、漂移检出这类业务事件由各 handler 调 recordAudit → 平台审计服务 LogEventRef(product="apollo")写入按产品名命名的哈希链审计表 apollo_audit_log(单数,见 5.4)。二者互不相见:本页永远查不到 DEPLOYMENT_START 这类业务事件码,页面上 action 输入框的占位示例「如 DEPLOYMENT_START」其实是一处与真实取值口径不一致的提示(存储的 action 恒为「HTTP 方法 + 空格 + 路径」,如 POST /api/v1/deployments/start),详见 7 章与 8 章。
典型使用链路(从操作到留痕的完整闭环):① 平台管理员(super)登录 Apollo——这一次 POST /api/v1/auth/login 本身就会被记一条审计(此时请求尚未经过 access.Authn,行内用户栏为空);② 到「项目管理」页新建项目 core-pay,POST /api/v1/projects 成功返回 201,中间件把它归为 success 落库;③ 切回本页点「查询」——注意查看审计页这个动作本身也会被记录(GET /api/v1/audit-logs),列表里最新一条往往就是你自己;④ 在顶部汇总卡片看日志总数、近 24 小时量与按 action 分布(此时会看到 POST /api/v1/auth/login、POST /api/v1/projects、GET /api/v1/audit-logs 等计数);⑤ 在 action 输入框填入 POST /api/v1/projects 精确过滤,行展开后核对请求体——若创建项目时带了敏感字段会被脱敏成 ***;⑥ 再用结果下拉选 denied,过滤出 401/403 的被拒尝试,判断是否有越权扫描;⑦ 翻页、重置、改 from/to 时间窗,反复核对。整页是纯只读的查询界面,没有任何写按钮,也不触发任何写请求。
从「看得见什么」的角度再强调两条边界,方便日常使用不困惑:其一,审计是异步落库(channel + worker,见 5.4),刚完成的操作要等 worker 消费才可查,存在秒级延迟窗口;其二,匿名流量(未登录访问公开端点、Agent pull/report)也会进表但用户栏为空,denied 大类里既有「登录失败/凭据错」也有「Agent 上报鉴权失败」等非人类操作,统计时需结合 path 列甄别。换句话说:本页证明的是「HTTP 层发生过什么」,而不是「业务层为什么失败」——后者的错误详情要么在展开行的 error_message(当前恒空,见 8 章),要么在各业务页的失败提示里。
1.2 核心价值/能力表
| 能力 | 说明 | 对应页面操作 |
|---|---|---|
| 全量留痕 | 后端每次 API 调用(含公开端点)脱敏后落库,按 id 倒序查询 | 页面首次加载列表 |
| 汇总卡片 | 日志总数 / 近 24 小时 / 按 action 计数一目了然,卡片随 summary 数据渲染 | 顶部汇总区 |
| 多维筛选 | action 精确匹配 + result 三态下拉(success/failure/denied)+ from/to 时间范围 | 筛选栏 + 「查询」 |
| 一键重置 | 清空四个筛选条件并回到第 1 页重新拉取 | 「重置」按钮 |
| 行点击展开 | 展开行展示 action/resource/客户端 IP/user_agent 与三段脱敏详情 | 点击任意日志行 |
| 状态着色 | 状态码 2xx 绿 / 4xx 橙 / 5xx 红;result 徽标三态配色 | 表格状态码与结果列 |
| 结果三态归类 | 2xx → success、401/403 → denied、其余 4xx/5xx → failure,语义在服务端定 | 结果徽标与 result 筛选 |
| 分页浏览 | 每页 20 条、id 倒序,上一页/下一页切换并保留筛选 | 表格下方分页区 |
| 只读零副作用 | 页面无写操作,打开本页只会产生自身列表/汇总的 GET 记录 | 全部操作 |
1.3 一句话总结
本页是 Apollo 控制面访问的「黑匣子查询台」——HTTP 层谁在何时调了哪个接口、成败几何、耗时多少、带了什么脱敏后的参数,全部在此留痕可按动作/结果/时间检索,业务生命周期事件另存别处、不在本页出现。
2. 访问入口
2.1 路由与菜单
| 项 | 值 |
|---|---|
| 路由 path | /apollo/admin/audit-logs |
| 路由 name | ApolloAdminAuditLogs |
| meta.title | Apollo 审计日志 |
| 父路由 | /apollo(组件 ApolloLayout,meta.title: 'Apollo') |
| 侧边栏入口 | ApolloLayout 侧边栏「平台管理」分组下的「审计日志」,位于「项目管理」之后、「签名者白名单」之前 |
| 前端源码 | action/web/src/views/apollo/AuditLogsPage.vue |
| API 客户端 | action/web/src/api/apolloClient.js |
| 路由注册 | action/web/src/router/index.js,/apollo 子路由 children 下(507-511 行),component: ApolloAuditLogsPage,挂 ApolloLayout |
访问方式:登录 Apollo 后从左侧菜单「平台管理 → 审计日志」进入,或直接访问 /apollo/admin/audit-logs。相邻页(侧边栏顺序):项目管理 admin-projects.md(前,成员增删等写操作可到本页核对记录)、签名者白名单 admin-signers.md(后);对照概念:用户管理 admin-users.md(本页用户栏展示的就是用户名/user_id)、部署与漂移 deployments.md(发起部署的 POST /api/v1/deployments/start 会在本页出现,但 DEPLOYMENT_START 业务事件不在本页)。
2.2 认证与权限
- 路由
requiresAuth: true:未登录访问被前端路由守卫拦到/apollo/login(登录态为独立apollo_token,向后兼容回退读取旧aip_token)。 - 两个端点
GET /audit-logs、GET /audit-logs/summary均在 protected 组并标注权限点PermAuditRead(audit:read)——只有被授予含该权限点的平台角色(如platform_admin)的登录用户才能查询审计;403 时拦截器统一alert('无权限执行该操作')。 - 401(apollo_token 失效)由响应拦截器清 token 并跳
/apollo/login;登录页自身 401 不跳,避免死循环。 - 有趣的自指:本页被打开时它自己的
GET /api/v1/audit-logs与GET /api/v1/audit-logs/summary也会被审计中间件记录,因而具备audit:read权限本身也会在日志里留下痕迹。
2.3 端口与 API 前缀
- Apollo 后端端口 18082。Vite 开发代理把
/apollo-api前缀 **rewrite 为/api** 转发(后端路由注册在/api/v1),客户端 baseURL/apollo-api/v1,请求超时 30000ms。 - 统一响应 envelope
{code,message,data,request_id}:拦截器在code===0时把response.data解包为业务数据,页面const { data } = await apiClient.get(...)直接拿业务对象。 - 本页两个端点的数据形态:
/audit-logs为分页对象{items,total,page,page_size};/audit-logs/summary为汇总对象{total,by_result,by_action,last_24h}。
直连调试提示:页面所有调用都走 apolloClient(带 Bearer token),经 Vite/网关的 /apollo-api 前缀分流到 18082 后端;脱离前端直调时用 http://127.0.0.1:18082/api/v1/audit-logs 或 /api/v1/audit-logs/summary(同样需 audit:read 权限的登录 token,见 5.2)。/audit-logs 与 /audit-logs/summary 在 gin 中是两条显式注册的精确路由,summary 不会被当作子路径参数吞掉,但注意路径要写全、不带尾部多余斜杠。
3. 界面布局
┌──────────────────────────────────────────────────────────────┐
│ Apollo 审计日志 │
│ [操作结果提示条(v-if alert.message,含「关闭」)] │
├──────────────────────────────────────────────────────────────┤
│ ① 汇总卡片区(v-if summary): │
│ [日志总数] [近 24 小时] [按 action 计数卡片…(N 张)] │
│ ② 筛选栏(card filter-bar,form): │
│ 动作(action,精确匹配)[输入] 结果(result)[下拉] │
│ 起始时间(from)[datetime-local] 结束时间(to)[同左] │
│ [查询] [重置] │
│ ③ 日志表格(三态:加载中… / 空态 / 表格): │
│ 时间 | 用户 | 方法 | 路径 | 状态码 | 结果 | 耗时(ms) │
│ (行点击展开 → 详情行 colspan=7:action · resource · │
│ 客户端 IP · user_agent · query_params/request_body/ │
│ error_message 三段深色代码区) │
│ ④ 分页:[上一页] 第 X / Y 页(共 N 条) [下一页] │
└──────────────────────────────────────────────────────────────┘
各板块职责:
| 板块 | 职责 |
|---|---|
| 页头 + 提示条 | 标题「Apollo 审计日志」;提示条仅用于列表加载失败等错误提示(单槽,可「关闭」) |
| 汇总卡片区 | 展示 GET /audit-logs/summary 的日志总数 / 近 24 小时 / 按 action 计数 |
| 筛选栏 | 四个筛选条件(action/result/from/to)+ 查询/重置;查询与重置都回到第 1 页 |
| 日志表格卡片 | 加载中/空态/表格三态;行点击展开详情,行带 row-selected 高亮 |
| 详情展开行 | 逐条展示 action、resource、客户端 IP、user_agent 与三段(已脱敏)代码区 |
| 分页区 | 每页 20 条 id 倒序;上一页/下一页切换时保留当前筛选 |
布局要点:loading 只控制表格卡的三态切换,汇总卡与筛选栏不受影响——首次进入时表格卡显示 加载中...,汇总卡若已返回则照常渲染,两者进度独立。汇总数据未返回时整块卡片区不渲染(v-if summary),返回后「日志总数」「近 24 小时」两卡恒渲染,by_action 为空则不渲染任何 action 卡。表格三态互斥:加载中 > 空态 > 表格,只有「表格」态下分页区才出现。翻页/查询失败时 logs 保留旧数据、表格仍在,仅在顶部提示条报错,不把用户打回空态。整页不含弹窗、抽屉与模态框,全部交互收敛在筛选、表格、分页三块,键盘 Enter 在筛选表单内可提交查询(form 的 @submit.prevent)。
4. 交互元素
4.1 页头、操作结果提示条
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 页面标题 | 页头 h2 | 页面身份标识 | 常驻 | Apollo 审计日志 | 无 | 与源码 h2 逐字一致 |
| 操作结果提示条 | 页头下方 | 展示最近一次列表加载失败信息 | 有 alert.message 才渲染;类型 alert-error | 展示 加载审计日志失败:{msg} | 无 | 页面唯一调用 showAlert 的路径是 fetchLogs 的 catch;汇总失败不弹提示(静默置空),见 4.2 |
| 提示条「关闭」 | 提示条右侧 | 清空当前提示 | 提示条可见时可用 | alert.message='' 立即消失 | 无 | 纯本地状态 |
页面状态组:logs(当前页日志数组)、total/page/pageSize(分页三件套,pageSize 固定 20)、loading(列表加载位,控制三态)、busy(按钮/翻页禁用位,见 8 章:声明后从未置 true)、alert(单槽提示)、summary(汇总对象,初始 null 不渲染卡片区)、expandedId(当前展开行 id,单开互斥)、filters(四个筛选条件对象)。totalPages 为计算属性 max(1, ceil(total/pageSize))。
4.2 汇总卡片区
| 控件 | 位置 | 含义 | 渲染条件 | 数据来源 | 边界与细节 |
|---|---|---|---|---|---|
| 「日志总数」卡 | 汇总区第一张 | 过滤口径下的总条数 | summary 非空 | summary.total ?? 0 | 见「全局口径」备注 |
| 「近 24 小时」卡 | 汇总区第二张 | 近 24 小时内记录数 | 同上 | summary.last_24h ?? 0 | 24 小时由后端按 timestamp >= now-24h 统计 |
| action 计数卡 | 其后按序排列 | 每种 action 的记录数 | by_action 非空才渲染 | v-for (cnt, action) in summary.by_action,label 即 action 值 | key 为 'a' + action;action 长文案(如 POST /api/v1/deployments/start)会被整串当卡标题,word-break: break-all 折行 |
| 汇总加载失败 | - | - | - | fetchSummary catch 里 summary.value = null | 静默降级:卡片区整块消失,无错误提示;列表仍正常 |
全局口径(重要):fetchSummary() 调用 /audit-logs/summary 时不携带任何筛选参数——即使你在筛选栏填了 action/result/from/to,汇总卡片的「日志总数/近 24 小时/按 action」仍是对全量日志的统计,与列表的过滤结果并不对应。点「查询/重置」时页面确实会重新调一次 summary 端点,但重新拉到的依旧是全局值,只是「看起来刷新了」。这是页面既有口径(前端缺陷候选见 8 章),理解后即可避免误读:要精确对比过滤口径下的数量,以列表页脚「共 N 条」为准。
4.3 筛选栏:动作与结果
| 控件 | 位置 | 含义 | 默认 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 动作(action,精确匹配)输入框 | 筛选栏 form-group | 按 action 精确过滤 | 空 = 不过滤 | 输入即绑定 filters.action | 「查询」提交时作为 action 参数 | label 与 placeholder 逐字为源码原样;placeholder 如 DEPLOYMENT_START 与真实存储口径(METHOD + path)不一致,见 7 章/8 章;提交前 trim |
| 结果(result)下拉 | 筛选栏 form-group(select) | 按结果分类过滤 | "" = 全部 | 选项 全部/success/failure/denied | 「查询」提交时作为 result 参数 | 与后端三态归类一一对应;留空才发全部 |
| 「查询」 | 筛选栏(btn-primary,type=submit) | 应用筛选 | :disabled="busy" | applyFilter():页码回 1 → fetchLogs() + fetchSummary() | GET /audit-logs + GET /audit-logs/summary | 表单 @submit.prevent 提交 |
| 「重置」 | 筛选栏(btn-outline) | 清空筛选 | 始终可点 | resetFilter():四个条件清空 → 页码回 1 → 重新拉列表与汇总 | 同上 | 与「查询」差异只在先清空 filters |
动作筛选为服务端精确匹配:前端把 filters.action.trim() 作为 action 参数,后端 WHERE action = ? 全等比对——多一个空格、大小写不同都查不到。可参考汇总卡片 by_action 卡上显示的完整 action 值(如 POST /api/v1/projects)再回填输入框。
4.4 筛选栏:时间范围 from/to 的转换
| 控件 | 位置 | 含义 | 默认 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 起始时间(from) | 筛选栏 form-group(datetime-local) | 记录时间下界(含) | 空 = 不限 | 绑定 filters.from | 「查询」时转 RFC3339 后作为 from 参数 | 见下方时区换算 |
| 结束时间(to) | 筛选栏 form-group(datetime-local) | 记录时间上界(含) | 空 = 不限 | 绑定 filters.to | 「查询」时转 RFC3339 后作为 to 参数 | 后端 timestamp <= to 含端点 |
提交时经 toISO(v):new Date(v) 把 datetime-local 值(如 2026-09-07T08:00)按浏览器本地时区解释,再 toISOString() 转成 UTC 的 RFC3339 串(形如 2026-09-07T00:00:00.000Z)传给后端;非法输入解析成 NaN 时返回空串、不发该参数。后端 parseTimeRange 用 time.Parse(time.RFC3339, ...) 解析(解析失败视为未提供),最终落到 WHERE timestamp >= from AND timestamp <= to。日志表的 timestamp 存的是 UTC 时间,与 toISO 产出的 UTC 串同口径,故跨时区使用时:填「本地 8:00」实际过滤的是「UTC 0:00」开始的记录——这与 4.5 表格里时间列的展示口径(UTC 串截断展示、不转时区)是自洽的。
4.5 日志表格行与状态着色
表格卡片三态:loading 为真显示 加载中...;logs.length===0 显示空态 暂无审计日志,请调整筛选条件。;否则渲染表格。列头依次 时间 / 用户 / 方法 / 路径 / 状态码 / 结果 / 耗时(ms)。
| 列 | 内容 | 边界与细节 |
|---|---|---|
| 时间 | formatTime(log.timestamp) | RFC3339 串去 T、去 Z、截前 19 位展示(如 2026-09-07 13:50:00),不转浏览器时区;空值显示 - |
| 用户 | log.username || log.user_id | 受保护端点展示登录用户名;公开端点(未过 Authn)两值皆空则整格为空 |
| 方法 | method-badge 徽标 | 如 POST/GET,灰底黑字 |
| 路径 | <code class="cell-code"> | 后端收到的原始路径(如 /api/v1/projects),等宽小字、可折行 |
| 状态码 | statusClass(log.status_code) | ≥500 红(st-5xx)、≥400 橙(st-4xx)、其余绿(st-2xx) |
| 结果 | status-badge + res-{result} | success 绿、failure 红、denied 橙三色徽标 |
| 耗时(ms) | log.duration_ms | 中间件计量的毫秒数,整数 |
行级交互:任意行 class="clickable-row",@click="toggleDetail(log.id)" 展开/收起该行;expandedId===log.id 时行加 row-selected(#e8f0fe 背景)。单开互斥:点第二行会先把第一行收起,同一时刻只有一个详情行。展开详情时该行用 <template v-for> 渲染额外的 detail-row(colspan=7),收起逻辑与项目页不同——本页不懒加载(数据随列表页一次带全,展开只做展示),翻页后详情自动消失。
4.6 详情展开行
详情行 detail-row 内含一个 .detail-panel,自上而下三部分:
| 区块 | 内容 | 逐字格式与细节 |
|---|---|---|
.detail-meta 元信息行 | 灰字小号 | 逐字:action={{ log.action }} · resource={{ log.resource_type }}/{{ log.resource_id }} · 客户端 IP {{ log.client_ip || '-' }} · user_agent {{ log.user_agent || '-' }} |
| 查询参数块 | 深色代码区 | 子标题 查询参数(query_params,已脱敏);内容为脱敏后的原始 query 串,空则 (无) |
| 请求体块 | 深色代码区 | 子标题 请求体(request_body,已脱敏);JSON/YAML/text 类请求体脱敏后原文,空则 (无) |
| 错误信息块 | 深色代码区 | 子标题 错误信息(error_message);当前恒为空、显示 (无)(后端中间件未填充该列,见 8 章) |
.code-block 为深色等宽代码区(white-space: pre-wrap + word-break: break-all,长 JSON 自动折行)。resource 字段的拆解:resource_type 由后端从路径首段提取(如 /api/v1/projects 的 projects),resource_id 取**路由首个 :id 参数**(如 /api/v1/projects/3/members/9 记录为 projects/3,取的是项目 id 3 而非成员 id 9);无 :id 参数的路由 resource_id 为空串,界面上呈现 projects/ 尾带空段的形态。client_ip 为 c.ClientIP() 值,user_agent 为浏览器 UA;两者空时分别显示 -。
4.7 分页与数据加载细节
| 控件 | 位置 | 可用条件 | 操作效果 | 边界与细节 |
|---|---|---|---|---|
| 「上一页」 | 分页区左(btn-sm btn-outline) | page>1 且 !busy | changePage(page-1) 重新拉列表 | 第 1 页禁用 |
| 页码文本 | 分页区中 | - | 第 {{ page }} / {{ totalPages }} 页(共 {{ total }} 条) | totalPages = max(1, ceil(total/pageSize)),total 为空态时不渲染分页区 |
| 「下一页」 | 分页区右(btn-sm btn-outline) | page<totalPages 且 !busy | changePage(page+1) 重新拉列表 | 末页禁用 |
fetchLogs() 的请求参数组装:{page, page_size:20} 恒带;filters.action.trim() 非空追加 action;filters.result 非空追加 result;toISO(from/to) 非空才追加 from/to。成功后 logs=data.items、total=data.total;失败提示 加载审计日志失败:{msg}(errMsg() 链见 5.1)且 loading 复位。挂载顺序(onMounted):fetchLogs() 与 fetchSummary() 并行发起,互不依赖——列表失败只弹提示、汇总照常渲染;汇总失败静默置空、列表照常。翻页(changePage)只重拉列表不重拉汇总;查询/重置才同时拉两者。busy 本意是防连点(查询/翻页按钮共用),但全页从未把它置 true——按钮实际恒可点,连点会并发发多个同参 GET(幂等无害),见 8 章。
4.8 数据口径与格式化细节
本页几处展示口径易被误读,集中说明如下:
- 时间列与 from/to 口径自洽:时间列
formatTime(t)把 RFC3339/ISO 串做replace('T',' ').replace('Z','').slice(0,19)截断展示(如2026-09-07 13:50:00),不转浏览器本地时区;时间筛选toISO则把本地时间转成 UTC 串后按 UTC 过滤。两者都以 UTC 为基准,所以「筛选出来的行,其时间列读数」在任意时区下口径一致;但对身处 +08:00 的用户,页面上看到的时间比本地时钟少 8 小时属正常。 - 「用户」列的空值不是脏数据:
log.username || log.user_id双值兜底——受保护端点在 Authn 后总有一者有值;公开端点(login/register/agent pull/report)两者皆空,格子留白。想给空用户行归类,看 path 列即可。 - **
client_ip取c.ClientIP()**:后端直连场景等于远端地址;若经反向代理/网关接入且未配置信任代理,读到的可能是网关地址而非真实客户端,属部署层口径问题(见 8 章)。 - 长内容全部可折行:路径
cell-code、详情三段code-block均设word-break: break-all,超长 JSON/UA 不会撑破表格;但折行后不便于肉眼复刻原文,需人工复制到编辑器比对。 - 空值统一展示约定:时间空 →
-;client_ip/user_agent空 →-;query_params/request_body/error_message空 →(无)。其中「时间空」几乎不会出现(timestamp由服务端自动写入),(无)与-的区别即「本就没有该数据」与「字段无值」的展示区分。
5. 后端关联
5.1 API 客户端
action/web/src/api/apolloClient.js:axios 实例 baseURL /apollo-api/v1、timeout 30000ms、请求拦截器对所有请求附加 Authorization: Bearer <token>(token 取 apollo_token,无则回退 aip_token)。响应拦截器:HTTP 2xx 且 code===0 时把 response.data 解包为业务数据(页面 const { data } = ... 直接得到业务对象);HTTP 2xx 但 code!==0 拒绝;4xx/5xx 提取 body.message || body.error 并把 message 别名写入 error.response.data.error 后 reject——**401 清 token 并跳 /apollo/login(登录页自身 401 不跳),403 alert('无权限执行该操作')**。页面 errMsg() 依次取 err.response.data.message → err.response.data.error → err.message → 未知错误。
5.2 端点表
| 方法 | 路径 | 权限点 | 请求参数(Query) | 页面触发点 |
|---|---|---|---|---|
| GET | /audit-logs | audit:read | page/page_size/action/result/from/to(额外支持但页面未用的 user_id/username/resource_type/project_id) | 初次加载、查询/重置、翻页 |
| GET | /audit-logs/summary | audit:read | 无(页面恒不带参,全局口径) | 初次加载、查询/重置 |
注册位置:action/products/apollo/server/server.go protected 组 835-836 行,均标注 access.RequirePerm(..., access.PermAuditRead)。handler 在 handlers.go:handleListAuditLogs(1748-1756 行)经 auditFilterFromQuery(1728 行)构造过滤、parsePage 解析分页后调 accessSvc.ListAuditLogs;handleAuditSummary(1759-1766 行)同参数调 accessSvc.AuditSummary。分页实现:handler 层 parsePage 默认 page=1、page_size=20、上限 100(api.go 166 行);service 层 ListAuditLogs 再兜底一次默认 20、上限 500(access_service.go 443-451 行,防御纵深)。
5.3 响应结构示例
**GET /audit-logs?page=1&page_size=20(envelope 解包后为分页对象,单项即 ApolloAuditLog 行)**:
{
"items": [
{
"id": 42,
"timestamp": "2026-09-07T13:50:00.123456+08:00",
"user_id": "2a3f9b7c-1c6e-4d4a-8f20-3e5c2d1a0b99",
"username": "admin",
"client_ip": "127.0.0.1",
"user_agent": "Mozilla/5.0 ... Chrome/120.0",
"method": "POST",
"path": "/api/v1/projects",
"query_params": "",
"request_body": "{\"name\":\"core-pay\",\"description\":\"核心支付域\"}",
"status_code": 201,
"action": "POST /api/v1/projects",
"resource_type": "projects",
"resource_id": "",
"project_id": 0,
"result": "success",
"error_message": "",
"duration_ms": 38
}
],
"total": 128,
"page": 1,
"page_size": 20
}
字段含义(ApolloAuditLog,access/models.go 72-91 行):id 自增主键(列表按 id DESC 倒序);timestamp 请求开始时间(落库时 autoCreateTime);user_id/username 登录主体(Authn 注入;公开端点为两空串);client_ip/user_agent 客户端信息;method/path HTTP 方法与原始路径;query_params 脱敏后的查询串(无则空串);request_body 脱敏后的请求体(无则空串);status_code 响应状态码;action 方法 + 空格 + 路径;resource_type/resource_id 资源定位(见 4.6);project_id 列存在但 HTTP 中间件不写(恒 0,页面也无此筛选);result 三态归类;error_message 列存在但 HTTP 中间件不写(恒空);duration_ms 请求耗时。表名固定 apollo_audit_logs(models.go 94 行)。
**GET /audit-logs/summary(envelope 解包后,页面恒不带参)**:
{
"total": 128,
"by_result": { "success": 100, "denied": 18, "failure": 10 },
"by_action": {
"POST /api/v1/auth/login": 40,
"GET /api/v1/projects": 22,
"GET /api/v1/audit-logs": 6
},
"last_24h": 12
}
字段含义(AuditSummary,access/access_service.go 52-58 行):total 过滤口径总数;by_result 按结果分组计数;by_action 按 action 分组计数(页面把每一项渲染成一张卡);last_24h 近 24 小时条数(timestamp >= now-24h)。汇总端点的 by_result 前端不直接展示(卡片区只渲染 total/last_24h/by_action 三类),结果分布要人工从列表/徽标观察。
5.4 关键机制
HTTP 审计中间件(TAD-11)。注册在根路由组、位于 requestIDMiddleware 之后(server.go 681-685 行),c.Next() 前记录开始时间并按需采集请求体,之后读取响应状态组装条目调 AuditService.Record 异步落库。三个要点:① 跳过清单为探针与文档路径 /health、/api/health、/api/v1/docs(http_audit.go 66 行),其余全部 API 都记;② 请求体采集只看内容类型——auditableContentTypes 白名单(application/json、application/yaml、text/yaml、text/plain、application/x-yaml 前缀)命中才 io.LimitReader 读入并还原 Body 供后续 handler 解析,multipart 上传/二进制下载不采集,避免记录二进制噪音;读入上限受 maxBodyBytes(1MB,server.go 50 行)约束;③ **用户信息在 c.Next() 后读**:受保护端点由 access.Authn 注入 user_id/username,公开端点取不到则为空串——这就是 4.5「用户」列对登录类请求为空的原因。
结果三态归类(auditResultForStatus)。>=500 → failure;401/403 → denied(认证失败与权限不足单独一类);>=400 → failure(其余 4xx,含 404/409/422 等全部算失败);其余 → success。注意这里的 failure 是宽口径:业务上「正常被拒的 404/冲突 409」在审计里也归 failure,要看语义差异需结合 status_code 列。
脱敏与截断(RedactText/RedactQuery/RedactBody)。落库前对请求体/查询串统一脱敏:超过 2048 字节先截到 2048 再拼 ...[truncated](truncateSuffix);再按正则把 JSON 键值对中 access_token/refresh_token/client_secret/password/authorization/api_key/apikey/signature/secret/token/key(长键名在前避免 token 截断 access_token)的值整体替换为 ***;明文 Bearer <token> 同样替换为 ***。查询串走 RedactQuery:解析出 password/token/access_token/refresh_token/secret/key/api_key/apikey/signature/authorization 参数即把值置 ***(url.Values.Encode 会把 * 编成 %2A,落库前还原为 ***)。因此详情行看到的 body/query 是脱敏后内容,原文不可逆。
异步落库(channel + worker)。AuditService 构造时启动 workerCount 个 worker goroutine 消费容量 1000 的缓冲 channel(audit.go 57 行);Record 用 select 投递,channel 满或 ctx 取消时落入 default 分支降级同步落库(不阻塞请求主路径)。所以「刚操作完立刻刷新本页」存在 worker 消费延迟(秒级),测试里也按此轮询等待记录出现。
筛选与分页。auditFilterFromQuery 从 query 提取 user_id/username/action/result/resource_type/project_id(project_id 需能解析成 uint 否则忽略)与 parseTimeRange 解析的 from/to(RFC3339);applyAuditFilter 逐字段 WHERE 精确比对(action/result/resource_type 全等,timestamp >= from、timestamp <= to)。前端只用了 action/result/from/to 四个,其余为直调 API 才可用的扩展能力。列表 Order("id DESC") 倒序——最新记录永远在列表最前,翻页是往更早的历史翻。
action 的存储口径与业务审计的隔离。中间件写 Action: c.Request.Method + " " + c.Request.URL.Path,即每条记录的 action 恒为「方法 + 空格 + 路径」(如 POST /api/v1/deployments/start、GET /api/v1/projects/3/members)。与此并行的另一条业务审计链是 recordAudit(handlers.go 1780-1789 行):各业务 handler 在生命周期节点调用它,经 s.auditService.LogEventRef(...) 以 product="apollo" 写入平台审计服务的哈希链业务表(AuditTableName 规则 = <product>_audit_log,即 apollo_audit_log 单数表),事件码形如 DEPLOYMENT_START/DEPLOYMENT_SYNC/DRIFT_DETECTED。本页查询的 ApolloAuditLog(apollo_audit_logs 复数表)由 HTTP 中间件独占写入,两表不相交——因此在页面上按业务事件码过滤永远查不到记录,只有把 action 输入框填成真实的方法+路径才能命中。
**resource_type/resource_id 的提取规则**。auditResourceType 把 /api/v1/ 前缀剥离后取路径首段(/api/v1/projects/3/artifacts → projects,无前缀路径 → other);resource_id 读路由参数 :id(首个匹配即命中),嵌套路由只记最外层资源 id。这解释了为何详情行 resource 粒度是「模块级 + 外层 id」,而非精确到被操作的子资源。
打开本页即触发自记录请求链。页面 onMounted 并行发 GET /audit-logs 与 GET /audit-logs/summary,这两笔请求又各自产生一条审计(resource_type 均为 audit-logs);点「查询」再各加一条、翻页再加一条。因此列表头部时常出现自己的访问痕迹,summary 的 by_action 也会统计到 GET /api/v1/audit-logs——这不是泄漏,而是中间件「除探针与文档外全部记录」的必然结果。空库首屏:后端刚启动、尚未有任何 API 调用时,列表为空态、汇总卡全 0;第一个请求(往往是登录)落下后即有数据。慢接口归因提醒:duration_ms 计的是中间件包住的整个请求周期(含 handler 执行与响应序列化),不区分服务端计算与网络传输;对经代理转发到 18082 的调用来说,该值约等于服务端视角耗时,可作慢接口初筛,但不能据此断定瓶颈在后端内部。
6. 权限与安全
- 认证分层:两个端点均位于 protected 组,
access.Authn解析 Bearer JWT / X-API-Key;无效凭据 401,前端清 token 跳登录页。 - 权限点门禁:列表与汇总都要求
audit:read,由内置平台角色按 RBAC 授予;缺权限 403,前端统一alert('无权限执行该操作')。审计页本身就是敏感面——能看它的人应限缩为平台管理员/安全角色。 - 落库脱敏:请求体与查询串在中间件内先脱敏再入库,返回给前端的
query_params/request_body不含明文密码/token/密钥,页面无泄露路径;原文不可逆查看。 - 自指记录:本页自身的查询请求也会被审计,具备 audit:read 的查看行为同样留痕,形成「谁查了审计」的可审计闭环。
- 写入面收敛:
apollo_audit_logs的行只由httpAuditMiddleware经AuditService.Record追加写入,不存在公开的改写/删除端点;业务生命周期事件另走平台审计服务的哈希链业务表(apollo_audit_log),两表均无前端可触达的篡改面。本页及其后端查询端点均为只读,无法对日志做任何变更。 - 越权尝试的可视化:401/403 统一归入
denied,因而「扫接口、猜口令、未授权访问」等尝试会集中成一批 denied 记录;日常巡检可定时按result=denied过滤,配合 client_ip/path 判断是否需要收紧 API Key 与角色授权。
7. 常见问题与排错
以下现象均由 AuditLogsPage.vue 的提示/渲染分支、后端中间件与 service 代码以及既有测试归纳,均可按步骤复现。
1. 现象:刚执行的操作在日志里查不到。 原因:审计为异步落库(channel + worker 消费),存在秒级延迟窗口;且若操作本身是业务生命周期事件(走 apollo_audit_log 单数表)而非 HTTP 调用,本页就永远不会有。处理:稍等 1-2 秒点「查询」重拉;确认你查的是 HTTP 动作(如 POST /api/v1/projects)而非 DEPLOYMENT_START 这类事件码。
2. 现象:action 输入框填「DEPLOYMENT_START」查不到任何记录。 原因:placeholder 示例沿用了业务事件码,但本页存储的 action 恒为「HTTP 方法 + 空格 + 路径」,精确匹配必然落空。处理:改填方法+路径,如 POST /api/v1/deployments/start;不确定时先看汇总卡片 by_action 卡上实际出现的 action 值再回填。
3. 现象:某次被拒操作显示为 denied,但另一类 4xx 显示为 failure。 原因:结果归类是服务端固定规则——401/403 → denied,其余 4xx(404/409/422 等)与 5xx → failure。处理:按此口径理解徽标;要区分具体失败原因以「状态码」列数值为准。
4. 现象:列表过滤出了结果,但顶部「日志总数」卡与实际不符。 原因:fetchSummary() 不携带筛选参数,汇总卡片恒为全量统计(全局口径),与过滤后的列表无对应关系。处理:以列表分页区「共 N 条」作为当前筛选口径下的准确计数;卡片只用于看全貌。
5. **现象:详情行请求体/查询串里的敏感值显示为 ***。** 原因:服务端 RedactBody/RedactQuery 落库前脱敏,属预期行为而非数据缺失。处理:如需原文只能到后端原始请求侧核对,页面无法恢复。
6. 现象:某行「用户」列为空。 原因:该请求落在公开端点(登录/注册/Agent pull/report)——未过 access.Authn,username/user_id 都是空串。处理:以 method/path 列判断是哪个公开调用;对登录失败类排查请结合状态码 401/denied。
7. **现象:展开行 error_message 恒显示 (无)。** 原因:ApolloAuditLog.ErrorMessage 列当前没有任何代码写入(HTTP 中间件不填),页面该区块形同预留。处理:把该区块视为占位;失败原因看状态码列与 path 列即可(见 8 章)。
8. 现象:点「查询/翻页」偶尔感觉没反应或列表闪一下又回旧数据。 原因:busy 全页从未置 true,按钮恒可点,连点会并发发出多个同参 GET——后完成的响应覆盖先完成的,属无副作用的竞态而非丢数据。处理:等列表加载完再点;刷新后如数据不一致以最后一次请求为准(见 8 章)。
9. **现象:本页自身的 GET /api/v1/audit-logs 出现在列表里。** 原因:HTTP 审计覆盖全部 API(探针/文档除外),查看审计页这个动作本身也被记录。处理:正常现象;按 action 过滤该值时能看到每 20 条一页的自我访问痕迹。
10. 现象:想按用户或资源类型筛。 原因:页面筛选栏只暴露 action/result/from/to 四项,后端支持的 user_id/username/resource_type/project_id 参数在页面没有入口。处理:用 curl 直调 GET /api/v1/audit-logs?resource_type=security-gate&result=denied(带 Bearer token 与 audit:read 权限)实现扩展过滤;或等前端补筛选控件。
11. 现象:想拿某条记录的 request_id 去后端日志对照,但审计行里找不到。 原因:审计模型没有持久化 request_id(envelope/响应头里的 X-Request-ID 仅当次请求有效,随记录不落库)。处理:改用「timestamp + username + method + path」四元组在表内近似定位;同一时刻并发同路径请求无法精确区分(见 8 章)。
12. 现象:后端刚启动,本页打开列表为空、汇总卡全 0。 原因:还没有任何 API 请求触发中间件写入(登录页都未访问过)。处理:先在其他页面发起一次带认证的操作(或直接登录一次),再回来查询;POST /api/v1/auth/login 是最容易产生的第一条记录。若操作后仍为空,检查 worker 消费延迟(1-2 秒后重拉)与数据库表是否存在(服务启动 AutoMigrate 会自动建表)。
8. 已知缺陷与边界
| 缺陷/边界 | 说明 |
|---|---|
| action 占位符误导 | 输入框 placeholder「如 DEPLOYMENT_START」是业务事件码示例,与真实存储口径(METHOD + path)不一致,按它填永远查不到。web/src 可修:改 placeholder 为 POST /api/v1/projects 类示例 |
| 汇总卡恒全局口径 | fetchSummary() 不带筛选参数,action/result/from/to 只作用于列表;查询/重置虽重拉 summary 但仍是全量值,卡片数字与过滤结果可能差异显著 |
| busy 形同虚设 | busy=ref(false) 声明后从未被置 true,「查询/翻页」禁用态恒不触发,连点会并发同参 GET(幂等、仅响应覆盖竞态) |
| error_message 恒空 | ApolloAuditLog.ErrorMessage 列无写入方(HTTP 中间件不填),详情行「错误信息」区块恒为 (无),前端预留了展示但没有数据 |
| project_id 恒 0 | 模型含 project_id 列且筛选层支持,但 HTTP 中间件不填充、页面无入口,按项目隔离查询当前不可用 |
| 时间展示 UTC 截断 | 时间列与 detail 无时区换算,跨时区用户看到的时间与本地有偏差(口径与筛选一致:后端按 UTC 存取) |
| result=denied 的语义并集 | denied 同时覆盖 401(登录/凭据/ApiKey 无效)与 403(权限不足)及 Agent 出向鉴权失败,需结合 path 甄别类型,无细分维度 |
| by_result 不展示 | summary 返回的 by_result 分布未渲染为任何卡片/图表,结果构成只能人工从徽标归纳 |
| 无导出/详情页 | 日志只能页内翻看,无 CSV 导出与单条详情独立页;数据量大时按时间窗分段查看 |
| 异步延迟窗口 | 审计异步落库,紧接操作后查询存在秒级空窗;测试/取证场景需等待 worker 消费 |
| 匿名流量混入 | 公开端点(pull/report/login/register)请求也会落库且用户列为空,噪声需要按 path 自行过滤 |
| 无自动刷新 | 页面仅在挂载/查询/重置/翻页时拉数据,无定时轮询——长时间挂页不会自动出现新记录 |
| request_id 不落库 | ApolloAuditLog 无 request_id 列,与响应头 X-Request-ID 无法互查;同刻并发同路径请求难以精确关联 |
| 无保留期/自动清理 | 日志表无内置 TTL 与归档任务,长期运行持续累积,膨胀需数据库侧人工处理 |
| 反代下 IP 失真 | client_ip 用 c.ClientIP(),直连时等于远端地址;经反向代理/网关接入而未配置信任代理时读到的是代理地址 |
附录 A:速查——页面文案与关键常量清单
| 场景 | 源码取值/文案(逐字) |
|---|---|
| 页面标题 | Apollo 审计日志 |
| 汇总卡片标签 | 日志总数 / 近 24 小时(action 卡以 action 值为标签) |
| 筛选 label | 动作(action,精确匹配) / 结果(result) / 起始时间(from) / 结束时间(to) |
| action placeholder | 如 DEPLOYMENT_START |
| result 选项 | 全部 / success / failure / denied |
| 筛选按钮 | 查询 / 重置 |
| 列表列头 | 时间 / 用户 / 方法 / 路径 / 状态码 / 结果 / 耗时(ms) |
| 列表加载态 | 加载中... |
| 列表空态 | 暂无审计日志,请调整筛选条件。 |
| 详情元信息行 | action={{ action }} · resource={{ resource_type }}/{{ resource_id }} · 客户端 IP {{ client_ip || '-' }} · user_agent {{ user_agent || '-' }} |
| 详情子标题 | 查询参数(query_params,已脱敏) / 请求体(request_body,已脱敏) / 错误信息(error_message) |
| 详情空值 | (无) |
| 分页文案 | 第 {{ page }} / {{ totalPages }} 页(共 {{ total }} 条) |
| 分页按钮 | 上一页 / 下一页 |
| 加载失败提示 | 加载审计日志失败:{msg} |
| 提示条关闭 | 关闭 |
| result 取值 | success / failure / denied(徽标 res-success / res-failure / res-denied) |
| 状态码着色 | st-2xx 绿 / st-4xx 橙 / st-5xx 红 |
| 权限点 | audit:read(列表与汇总共用) |
| 存储表 | apollo_audit_logs(HTTP 审计,本页数据源) |
| 业务审计表 | apollo_audit_log(平台哈希链业务审计,本页不查) |