Files
wecom_it_smart_desk/docs/03-技术架构/日志体系-系统设计.md
T

30 KiB
Raw Blame History

管理后台日志体系 — 系统设计 + 任务分解

阶段standard SOP 第二阶段(架构设计,输入给工程师 寇豆码) 架构师:高见远(software-architect)|日期2026-07-09 上游输入(已 Read 并核对):

  • 产品+设计文档:docs/04-功能设计/系统日志与审计日志-产品与设计.md(许清楚)
  • PRD 增补:docs/02-产品需求/02-产品需求文档PRD-v1.2-20260704.md §「管理后台日志体系」
  • 架构增补:docs/03-技术架构/00-系统架构设计文档-v1.3.md §7.1.1 日志体系
  • 运行期日志设计:docs/08-安全审计/审计报告-安全审计/健康检查+错误码+日志结构化.md §3
  • 真实代码:frontend-admin/src/{components/Sidebar.vue,router/index.ts,views/SystemLogs.vue,api/admin.ts}backend/app/{api/admin_api.py,api/audit_logs.py,api/router.py,services/admin_service.py,services/audit_log_service.py,models/config_change_log.py,models/audit_log.py,models/automation.py,utils/logging_config.py}docker-compose.yml

设计原则:沿用现有技术栈,不新建表(A/B/D 均复用既有存储),不引入新 npm/PyPI 依赖;D 后端方案必须可部署、可执行。

修订记录2026-07-09 初稿;同日依工程师(寇豆码)实现反馈修订——(1) 双写边界(决策2)经核查实际不存在,改为「待 team-lead 依用户决策确认」并修正 T5;(2) 补充 main.pysetup_logging 的接线(D 文件日志能力此前未启用);(3) team-lead 确认方案甲后双写已落地,文档据实更新为「已生效」,json_format 依 team-lead 指示定为 Truestdout 亦 JSON)。详见 §6.5、§七、§十 #9/#10、§十一 T5、§十二。


一、设计要点摘要

结论
技术栈 前端 Vue3 + TS + Element Plus + Pinia(沿用);后端 FastAPI + SQLAlchemy(async) + PostgreSQL(沿用)
A 配置变更历史 复用 config_change_logs;后端 GET /api/admin/system-logsconfig_key/changed_by/from/to 过滤;前端 SystemLogs.vue 加筛选控件;侧边栏改名「配置变更历史」
B 安全审计日志 后端已存在 GET /api/admin/audit-logs(权限 audit_log:read:all,参数 employee_id/action/resource/from/to/page/page_size);本次仅前端新建页面 + API 函数,后端零改动
C 自动化动作日志 本次不做管理后台可视化(P2,标注后续)
D 运行期结构化日志 新建前端页 + 后端 GET /api/admin/runtime-logs(级别/时间/关键字筛选 + 下载);落地方案:应用层 JSON 文件日志 + docker-compose 宿主机目录 bind mount(不挂 docker.sock
双写边界(决策2 已落地(方案甲)PUT /api/admin/configs/{key}admin_service.update_config同 DB session 同时写 Aconfig_change_logs)与 Baudit_logsconfig_change 事件)。配置变更同时出现在「配置变更历史」(A) 与「安全审计日志」(B),决策2 双写已真实生效(见 §十 #9、T5)
权限 A 沿用 require_admin(建议后续命名为 config_log:read,本迭代不纳入 RBAC 重构);B 沿用 audit_log:read:allD 用 require_admin
新增依赖 无(前端无新 npm 包;后端用标准库 pathlib/os/glob + logging.RotatingFileHandler + fastapi.responses.StreamingResponse

二、实现方案与框架选型

难点与对策

  1. A 增加筛选而不改表/不加索引config_change_logs 已有 idx_ccl_config_keyidx_ccl_changed_at 索引;changed_by 无索引,但配置变更频率低、数据量小,用 WHERE changed_by = :v(或 JOIN agents 按姓名匹配)可接受,短期不新增索引(见「待明确」)。
  2. B 零后端改动:后端 audit_logs.py 完整可用,前端补齐页面与 getAuditLogs 即可。
  3. D 容器内读日志(核心难点):见第六节,采用「应用写文件 + bind mount」而非 docker.sock。
  4. 命名歧义消除:侧边栏/路由将「系统日志」拆为「配置变更历史(A)」与新增「安全审计日志(B)」「运行期日志(D)」。

架构模式:前端 视图组件 + API 函数(axios);后端 路由层(FastAPI Router)→ 服务层(service function)→ 数据层(SQLAlchemy / 文件系统)。A/B 走 DB,D 走文件系统。


三、文件列表(标注 新增 / 修改)

前端(frontend-admin/

文件 类型 说明
src/components/Sidebar.vue 修改 「监控与数据」分组:将原 系统日志 改名为 配置变更历史;新增 安全审计日志(/audit-logs)、运行期日志(/runtime-logs) 两个入口
src/router/index.ts 修改 A 路由 meta.title配置变更历史;新增 audit-logs→AuditLogs.vueruntime-logs→RuntimeLogs.vue
src/views/SystemLogs.vue 修改 A 页:新增「配置键 / 操作人 / 时间」筛选控件,调用 getSystemLogs 新参数
src/views/AuditLogs.vue 新增 B 页:调用 getAuditLogs(级别/操作人/动作/资源/时间筛选 + 分页)
src/views/RuntimeLogs.vue 新增 D 页:级别/时间/关键字筛选 + 分页 + 下载按钮
src/api/admin.ts 修改 getSystemLogs 增参;新增 getAuditLogs / getRuntimeLogs / getRuntimeLogDownload

后端(backend/app/

文件 类型 说明
api/admin_api.py 修改 GET /system-logs 增加 config_key/changed_by/from/to Query 参数并下传
services/admin_service.py 修改 get_system_logs 增加过滤参数与 WHERE 构造(函数位于文件末尾 ~L1689)
api/runtime_logs.py 新增 路由前缀 /admin/runtime-logsGET /runtime-logs(列表 + download=true 下载)
services/runtime_log_service.py 新增 RUNTIME_LOG_DIR*.log,逐行 JSON 解析、按 level/time/keyword 过滤、分页、生成下载流
utils/logging_config.py 修改 setup_logging 支持 log_dir,新增 RotatingFileHandlerJSON 格式)写入 RUNTIME_LOG_DIR,保留 stdout handler
core/config.py 修改 新增配置项 RUNTIME_LOG_DIR(默认 /app/logs
api/router.py 修改 挂载 runtime_logs_router(与 audit_logs_router 一致,~L243-244 后追加)
main.py 修改 接线:启动处调用 setup_logging(level=..., log_dir=settings.RUNTIME_LOG_DIR)(能力已在 logging_config.py 就绪,但此前未被调用,不接线则 /app/logs 文件不生成、D 页空)
docker-compose.yml 修改 backend 服务 volumes 增加 ${RUNTIME_LOG_HOST_DIR:-/var/log/wecom-it-desk}:/app/logsenvironment 增加 RUNTIME_LOG_DIR=/app/logsLOG_FORMAT=json

注:api/audit_logs.pymodels/audit_log.pyservices/audit_log_service.pymodels/config_change_log.pyschemas/admin.py(ConfigHistoryItem) 本次均不修改,直接复用。


四、接口设计(含请求/响应 JSON 示例)

统一响应体:{ "code": 0, "message": "success", "data": {...} };错误 code != 0。 时间参数统一 ISO8601(如 2026-07-09T00:00:00+08:002026-07-09T16:00:00Z)。

① A — 配置变更历史(修改已有)

请求

GET /api/admin/system-logs
Query:
  page         int     = 1
  page_size    int     = 50   (<=200)
  config_key   string?        配置键精确/模糊匹配
  changed_by   string?        操作人 agent_id(前端用坐席下拉取 id 传入)
  from         string?        ISO8601 起始时间(alias from_time
  to           string?        ISO8601 结束时间(alias to_time

响应

{
  "code": 0,
  "message": "success",
  "data": {
    "items": [
      {
        "id": "3f1a...",
        "log_type": "config_change",
        "config_key": "auto_reply_enabled",
        "old_value": "false",
        "new_value": "true",
        "changed_by": "a-1001",
        "changed_by_name": "张明",
        "changed_at": "2026-07-09T14:32:10+08:00"
      }
    ],
    "total": 128,
    "page": 1,
    "page_size": 50
  }
}

② B — 安全审计日志(复用后端,仅前端对接)

请求(后端已支持,无需改动)

GET /api/admin/audit-logs
Query:
  employee_id string?
  action      string?     如 login / config_change / role_change
  resource    string?     如 agent / system_config / conversation
  from        string?     ISO8601alias from_time
  to          string?     ISO8601alias to_time
  page        int = 1
  page_size   int = 50 (<=500)
Header/Permission: audit_log:read:all

响应(节选)

{
  "code": 0,
  "message": "success",
  "data": {
    "items": [
      {
        "id": "9b2c...",
        "employee_id": "a-1001",
        "action": "config_change",
        "resource": "system_config",
        "resource_id": "auto_reply_enabled",
        "details": { "old": "false", "new": "true", "ip": "10.80.0.5" },
        "result": "success",
        "ip_address": "10.80.0.5",
        "user_agent": "Mozilla/5.0 ...",
        "created_at": "2026-07-09T14:32:10+08:00"
      }
    ],
    "total": 5321,
    "page": 1,
    "page_size": 50
  }
}

③ D — 运行期结构化日志(新增)

列表请求

GET /api/admin/runtime-logs
Query:
  level    string?   枚举 DEBUG/INFO/WWARNING/ERROR/CRITICAL(按 >= 阈值语义过滤)
  from     string?    ISO8601 起始时间(按日志 timestamp 字段)
  to       string?    ISO8601 结束时间
  keyword  string?    子串匹配(message 或整行,支持 trace_id / employee_id 检索)
  page     int = 1
  page_size int = 100 (可 20/50/100)
  download bool = false  为 true 时返回文件流(忽略分页)
Permission: require_admin

列表响应

{
  "code": 0,
  "message": "success",
  "data": {
    "items": [
      {
        "timestamp": "2026-07-09T06:32:10Z",
        "level": "ERROR",
        "logger": "app.api.admin_api",
        "message": "request_start: POST /api/admin/configs/...",
        "module": "admin_api",
        "function": "get_system_logs",
        "line": 902,
        "request_id": "req-abc-123",
        "user_id": "a-1001"
      }
    ],
    "total": 342,
    "page": 1,
    "page_size": 100
  }
}

下载响应download=true

HTTP 200
Content-Type: text/plain; charset=utf-8
Content-Disposition: attachment; filename="runtime-logs-20260709T143200Z.log"
Body: 每行一条原始 JSON 日志(已按筛选条件命中)

实现用 StreamingResponse(iter([...]), media_type="text/plain", headers={...}),逐块吐出,避免大文件占内存。


五、数据模型 / 字段确认

落点 字段(关键) 本次是否改
A ConfigChangeLog config_change_logs config_key, old_value, new_value, changed_by, changed_at 不改;复用 ConfigHistoryItem 响应 schema
B AuditLog audit_logs employee_id, action, resource, resource_id, details, result, ip_address, user_agent, created_at 不改
C ActionLog auto_action_logs session_id, event, direction, system, request, response, status, latency_ms, error, created_at 不改(本次不暴露)
D 运行期日志 文件系统(不建表) 每行 JSONtimestamp, level, logger, message, module, function, line, [request_id], [user_id], [extra] logging_config.py 写出

D 不新建数据库表,符合 PM 决策(D 是应用运行 stdout 日志的「查看」,非新业务数据)。


六、D 后端落地方案(重点 · 可部署)

6.1 结论

采用「应用层 JSON 文件日志 + docker-compose 宿主机目录 bind mount」方案。

  • 后端容器内部:扩展 logging_config.setup_logging,在现有 stdout handler 之外,新增一个 RotatingFileHandler,用已有的 JSONFormatter 把结构化日志写入 RUNTIME_LOG_DIR(容器内默认 /app/logs/wecom-it-desk.log,轮转备份 -1/-2...)。
  • docker-compose:把宿主机目录(默认 /var/log/wecom-it-deskbind mount 为容器内的 /app/logs
  • GET /api/admin/runtime-logs:直接读 /app/logs/*.log 文本文件,逐行 json.loads,按 level / from~to(比较 timestamp/ keyword 过滤,时间倒序,分页返回;download=true 时以 StreamingResponse 返回命中行的文本流。

6.2 为什么不用 docker.sock / 不读 /var/lib/docker

备选方案 否决原因
挂载 /var/run/docker.sock 调 Docker API 读日志 安全风险(容器逃逸)、可移植性差(NAS/单机不应依赖 Docker API);本次明确禁止
/var/lib/docker/containers/<id>/*-json.log 路径含随机容器 ID,每次 compose up 变化,无法稳定 bind mount;且 json-file 行格式需二次解析 {"log":...,"stream":...,"time":...}
应用层写文件 + bind mount(选定) 路径确定、格式可控(复用现有 JSONFormatter)、与 stdout 并存(stdout 仍由 docker json-file 捕获供 docker logs)、零新依赖、跨环境可移植

6.3 部署依赖(docker-compose.yml · backend 服务改动)

  backend:
    environment:
      # 已有环境变量保留 ...
      - RUNTIME_LOG_DIR=/app/logs            # 新增:运行期日志写入目录(容器内)
      - LOG_FORMAT=${LOG_FORMAT:-json}       # 新增:确保 JSON 输出(D 端点按 JSON 解析)
      - LOG_LEVEL=${LOG_LEVEL:-INFO}
    volumes:
      - backend-uploads:/app/uploads
      - ${RUNTIME_LOG_HOST_DIR:-/var/log/wecom-it-desk}:/app/logs   # 新增:宿主机日志目录 bind mount
  • 宿主机目录不存在时 compose 自动创建(bind mount 语义)。
  • 轮转:RotatingFileHandler(maxBytes=20*1024*1024, backupCount=5),与现有 docker logging.max-size: "20m" 对齐。
  • 本地开发(非 compose,直接 uvicorn):RUNTIME_LOG_DIR 默认 /app/logs;若该目录不存在,端点需容错——返回 {code:非0, message:"日志目录不可读"} 或空列表,不抛 500(见共享知识)。

6.4 读取逻辑要点(给工程师)

  1. 遍历 RUNTIME_LOG_DIR 下所有 *.log(含轮转备份),按文件 mtime 倒序合并。
  2. 逐行 json.loads;解析失败行跳过(容错)。
  3. level 过滤:大小写不敏感,按「>= 阈值」语义(选 ERROR → 显示 ERROR/CRITICAL;选 INFO → 显示 INFO+WARNING+ERROR+CRITICAL;选 DEBUG → 全部)。
  4. from/to:取每条日志 timestamp 字段(现有 JSONFormatter 输出 UTC Z 格式),解析为 datetime 后比较(兼容 +08:00Z)。
  5. keyword:对整行 JSON 文本(或 message)做子串匹配,支持 trace_id / employee_id 检索。
  6. 分页:收集命中行 → 时间倒序 → 切片(page/page_size)。短期限制:全量读入内存再切片;若单文件超大,后续接 Loki(已在审计报告中规划)。
  7. download=true:将命中行原样(每行一条 JSON)写入 StreamingResponseContent-Disposition 文件名带时间戳。

6.5 接线依赖(关键 · 工程师反馈)

  • logging_config.setup_logging(log_dir=...)RotatingFileHandler 写文件能力已实现,但全项目 grep 显示 setup_logging 此前从未被调用(仅定义),main.py 启动走的是 logging.basicConfig
  • 后果:若不补这一行,/app/logs/wecom-it-desk.log 不会由应用生成,D 页阶段内显示空(已有不抛 500 容错)。
  • 修复(已纳入 T4 文件清单):在 backend/app/main.py 启动初始化处增加一行 setup_logging(level=settings.LOG_LEVEL, json_format=True, log_dir=settings.RUNTIME_LOG_DIR)team-lead 定稿 json_format=Truestdout 亦结构化 JSON)。
    • formatter 行为(已运行时验证 + team-lead 定稿)setup_logging 内 console handler 的 formatter 由 json_format 决定,而 file handler 硬编码 JSONFormatter、与 json_format 无关(源码注释「始终使用 JSONFormatter,保证日志文件为结构化 JSON 行」)。team-lead 最终指示 json_format=True——stdout 亦输出结构化 JSON,对齐审计报告 §3.2 推荐方向;file handler 恒为 JSON 行,RUNTIME_LOG_DIR 下文件 D 端点逐行 json.loads 正常解析,D 页不会空。两种形态对 D 解析均无影响。
  • 该改动为纯接线,能力就绪,风险极低;D 端点的「目录不可读/无文件不 500」容错保留。

七、权限设计

页面/接口 权限 现状 本次
A GET /api/admin/system-logs require_adminadmin 角色) 已有 沿用;建议后续明确命名为 config_log:read(与 B 一并纳入 RBAC 梳理,本迭代不做)
B GET /api/admin/audit-logs audit_log:read:alladmin/auditor 已有(require_permission 复用,零改动
D GET /api/admin/runtime-logs require_adminadmin 角色) 新增 新增端点用 require_admin
双写 PUT /api/admin/configs/{key} 已有 已落地 A+B 双写update_config 同 session 追加 AuditLogaction=config_change 已生效,T5 验收通过

八、依赖包

  • 前端:无新增 npm 包(沿用 Element Plus / axios / Pinia)。
  • 后端:无新增 PyPI 包。仅用标准库 pathlib / os / glob + logging.RotatingFileHandler + fastapi.responses.StreamingResponse

九、共享知识(跨文件约定)

  • 日志级别枚举DEBUG < INFO < WARNING < ERROR < CRITICALD 的 level 参数按此序做 >= 阈值过滤)。
  • 时间参数:统一 ISO8601(前端用 toISOString() 或本地带时区;后端用 datetime 解析,兼容 Z+08:00)。
  • 统一响应体{ code: int, message: string, data: any };成功 code=0
  • D 日志目录:统一读 settings.RUNTIME_LOG_DIR(默认 /app/logs),不在代码里硬编码绝对路径。
  • D 容错:日志目录不存在 / 无可读文件时,列表返回空 items 或友好错误码,禁止 500
  • 命名一致性A 时间参数用 from/toalias 注入),与 B 的 from/to 命名保持一致(见「待明确」)。

十、待明确事项(遗留决策点)

  1. A 时间参数命名PM 文档用 changed_from/changed_to,本设计对齐 B 采用 from/to(alias 注入,对前端透明)。若坚持 PM 原文命名需回退——建议采用 from/to
  2. A changed_by 筛选粒度:按 agent_id 精确匹配(前端坐席下拉取 id);若要支持「按姓名搜」需 JOIN agents 表做 ILIKE。本设计采用 agent_id 精确匹配,实现最简。
  3. A changed_by 缺索引:高数据量下建议后续加 idx_ccl_changed_by;本次不强制。
  4. A 页面权限命名 config_log:read:本迭代不纳入 RBAC 重构,沿用 require_admin;列入后续迭代。
  5. B 待补事件(敏感数据查询/角色变更/系统配置变更/异常登录 P1,API 调用统计 P2):本次不实现,仅保证接口与页面可扩展(后续迭代直接写 audit_logs 即可被 B 页展示)。
  6. C 管理后台可视化:本次不做(P2),标注后续。
  7. E 与 B 衔接:阶段四「会话日志格式规范」落地后,再在 B 补对应审计事件;本次不实现。
  8. D level 语义:采用「>= 阈值」;若产品要求「精确匹配」需调整(影响前端下拉说明)。
  9. 双写边界(决策2)— 已落地(方案甲):实现核查曾显示 update_config 仅写 A;经 team-lead 向用户确认「要补」后,工程师已在 backend/app/services/admin_service.pyupdate_config 中、写 A 之后同 session 追加 AuditLogaction=config_changeresource=system_configresource_id=keydetails={old,new,operator}result=success)。决策2 双写现已真实生效:配置变更同时入 A 与 B,两表均不在此 commit、由调用方统一提交。T5 验收按方案甲通过(见 §十一)。若后续需补 ip_address/user_agent,可在服务层注入 Request 上下文,本期留空。
  10. setup_logging 接线(已落地)logging_config.setup_logging(log_dir=...) 已实现且已在 main.py 启动处调用(T4 已含),RUNTIME_LOG_DIR 下日志文件正常生成;team-lead 最终指示 json_format=Truestdout 亦结构化 JSON,对齐审计报告 §3.2),file handler 恒为 JSOND 端点可解析。详见 §6.5。

十一、任务分解清单(有序 · 含依赖 · 验收要点)

颗粒度按团队主理人指定:T1 侧边栏+路由;T2 A 筛选;T3 B 页;T4 D 页(前后端);T5 双写回归;T6 联调权限。

T1 — 侧边栏重命名 + 路由入口(A 改名、B/D 新增)

  • 涉及文件frontend-admin/src/components/Sidebar.vuefrontend-admin/src/router/index.ts
  • 依赖:无
  • 优先级P1
  • 验收要点:① 侧边栏「监控与数据」出现「配置变更历史(A)」「安全审计日志(B)」「运行期日志(D)」三个独立入口,无「系统日志」歧义统称;② 路由 audit-logsruntime-logs 已注册且 meta.title 正确;③ A 路由 title 改为「配置变更历史」。

T2 — A 页筛选(前端控件 + 后端参数)

  • 涉及文件frontend-admin/src/views/SystemLogs.vuefrontend-admin/src/api/admin.ts(改 getSystemLogs)、backend/app/api/admin_api.py(改 GET /system-logs)、backend/app/services/admin_service.py(改 get_system_logs)
  • 依赖T1
  • 优先级P1
  • 验收要点:① 前端 A 页有「配置键/操作人/时间」三类筛选控件且与分页联动;② 后端 GET /api/admin/system-logs 支持 config_key/changed_by/from/to 且过滤正确;③ 空筛选时等价原行为。

T3 — 新建 B 安全审计日志页(前端,复用后端)

  • 涉及文件frontend-admin/src/views/AuditLogs.vue(新增)、frontend-admin/src/api/admin.ts(新增 getAuditLogs)
  • 依赖T1
  • 优先级P1
  • 验收要点:① 页面可筛选 employee_id/action/resource/from/to 并分页展示;② 调通既有 GET /api/admin/audit-logs;③ 非 admin/auditor 角色被拦截(后端已保证)。

T4 — 新建 D 运行期日志页(前端 + 后端,含部署改动)

  • 涉及文件
    • 前端:frontend-admin/src/views/RuntimeLogs.vue(新增)、frontend-admin/src/api/admin.ts(新增 getRuntimeLogs/getRuntimeLogDownload)
    • 后端:backend/app/api/runtime_logs.py(新增)、backend/app/services/runtime_log_service.py(新增)、backend/app/utils/logging_config.py(改)、backend/app/core/config.py(改)、backend/app/api/router.py(改,挂载 router)、backend/app/main.py(改,启动处调用 setup_logging(log_dir=settings.RUNTIME_LOG_DIR) 完成接线)
    • 部署:docker-compose.yml(backend volumes + env)
  • 依赖T1
  • 优先级P0
  • 验收要点:① admin 可按级别+时间+关键字筛选查看日志;② 可下载 .log/.txt 文件流;③ main.py 已调用 setup_logginglogging_config 写入 RUNTIME_LOG_DIRdocker-compose 挂载生效(容器内 /app/logs 实际生成 .log);④ 非 admin 调接口返回 403;⑤ 目录不可读时不 500。

T5 — 双写边界处理(决策2 · 已落地方案甲)

  • 背景:实现核查曾确认 update_config 仅写 A;team-lead 向用户(宋献)确认「要补」双写,故采用方案甲。
  • 涉及文件backend/app/services/admin_service.pyupdate_config 写 A 后同 session 追加 AuditLog config_change 事件,字段对齐既有 AuditLog 模型与 record_audit_log 约定)
  • 依赖T2
  • 优先级P0
  • 验收要点:① 修改任一配置项后,config_change_logsaudit_logs(config_change 事件) 均新增对应记录;② 两表记录数一致、内容可对应(old/new/operator 一致);③ 不在任一侧删除/合并;④ B 页「安全审计日志」可见该配置变更。
  • 状态 已落地,工程师 IS_PASS 转 QA 全量测试。

T6 — 联调与权限核对

  • 涉及文件:全链路(A/B/D 页面 + 接口)
  • 依赖T2、T3、T4T5 可并行)
  • 优先级P0
  • 验收要点:① A(admin)/B(audit_log:read:all)/D(admin) 权限分别核对通过;② 三个页面在管理后台真实跑通;③ 与 PM 文档 §5 五项决策验收标准逐条对齐。

十二、对 PM 文档遗留 5 问的回应

  1. D 后端聚合方案定稿 → 已定稿:应用层 JSON 文件日志 + docker-compose 宿主机目录 bind mount(见第六节)。不使用 docker.sock,不读 /var/lib/docker。这是短期可执行方案,中期可平滑切 Loki(D 端点抽象出 RuntimeLogService,换实现不影响前端)。
  2. B 待补事件是否纳入本次不建议纳入。敏感数据查询/角色变更/系统配置变更/异常登录(P1)、API 调用统计(P2) 列入后续迭代;本次仅保证 B 接口与页面可扩展(新事件直接写 audit_logs 即被展示)。
  3. C 是否需管理后台可视化查询接口本次不做(标注 P2)。auto_action_logs 已落表,后续如需可补 GET /admin/auto-action-logs(权限待定)。
  4. A 页面权限命名 → 沿用 require_admin;建议后续明确为 config_log:read 并与 B 一并纳入 RBAC 梳理,本迭代不实现。
  5. E 与 B 衔接 → 阶段四「会话日志格式规范」落地后,再在 B 补对应审计事件;本次不实现。

实现核查补充(2026-07-09,工程师反馈)

  • 决策 #2 的「双写」前提经代码核查不成立——update_config 仅写 A,未写 B(详见 §十 #9)。本设计已据实修正,并列为需 team-lead 向用户确认的阻塞项;原「T5 双写回归(无代码改动)」已改为「T5 双写边界处理(依赖决策)」。
  • D 文件日志能力 setup_logging(log_dir=...) 已实现,但 main.py 未调用,需在启动处补一行接线(已纳入 T4 文件清单 + §6.5)。不接线则 D 页空(已有不 500 容错)。

附录 A — 类图(Mermaid

classDiagram
    direction LR

    class ConfigChangeLog {
        +String id
        +String config_key
        +Text old_value
        +Text new_value
        +String changed_by
        +DateTime changed_at
    }
    class AuditLog {
        +String id
        +String employee_id
        +String action
        +String resource
        +String resource_id
        +JSON details
        +String result
        +String ip_address
        +Text user_agent
        +DateTime created_at
    }
    class RuntimeLogEntry {
        +String timestamp
        +String level
        +String logger
        +String message
        +String module
        +String function
        +int line
        +String request_id
        +String user_id
    }
    class ConfigHistoryItem {
        +String id
        +String config_key
        +String old_value
        +String new_value
        +String changed_by
        +String changed_by_name
        +DateTime changed_at
    }

    class AdminApi {
        +GET /system-logs
    }
    class AuditApi {
        +GET /audit-logs
    }
    class RuntimeApi {
        +GET /runtime-logs
    }
    class AdminService {
        +get_system_logs(db, config_key, changed_by, from, to, page, page_size)
    }
    class AuditLogService {
        +list_audit_logs(db, employee_id, action, resource, from, to, page, page_size)
    }
    class RuntimeLogService {
        +read_runtime_logs(dir, level, from, to, keyword, page, page_size)
        +build_runtime_log_stream(dir, level, from, to, keyword)
    }
    class RuntimeLogStore {
        <<filesystem>>
        +RUNTIME_LOG_DIR/*.log
    }

    AdminApi ..> AdminService : 调用
    AuditApi ..> AuditLogService : 调用
    RuntimeApi ..> RuntimeLogService : 调用

    AdminService ..> ConfigChangeLog : SELECT
    AuditLogService ..> AuditLog : SELECT
    RuntimeLogService ..> RuntimeLogStore : 读文件

    ConfigHistoryItem <|.. ConfigChangeLog : 映射
    RuntimeLogEntry <|.. RuntimeLogStore : 逐行解析

附录 B — 时序图(Mermaid

sequenceDiagram
    autonumber
    participant FE as 前端页面
    participant API as admin.ts
    participant RT as FastAPI Router
    participant SVC as Service
    participant DB as PostgreSQL
    participant FS as 日志文件(RUNTIME_LOG_DIR)

    Note over FE,FS: A 配置变更历史(带筛选)
    FE->>API: getSystemLogs({config_key, changed_by, from, to, page, page_size})
    API->>RT: GET /api/admin/system-logs?...
    RT->>RT: require_admin()
    RT->>SVC: get_system_logs(db, filters)
    SVC->>DB: SELECT ConfigChangeLog WHERE ... ORDER changed_at DESC
    SVC->>DB: JOIN agents 取 changed_by_name
    DB-->>SVC: rows
    SVC-->>RT: {items, total, page, page_size}
    RT-->>FE: {code:0, data}

    Note over FE,FS: B 安全审计日志(复用后端)
    FE->>API: getAuditLogs({employee_id, action, resource, from, to, page, page_size})
    API->>RT: GET /api/admin/audit-logs?...
    RT->>RT: require_permission(audit_log:read:all)
    RT->>SVC: list_audit_logs(db, filters)
    SVC->>DB: SELECT AuditLog WHERE ...
    DB-->>SVC: rows
    SVC-->>RT: {items, total, page, page_size}
    RT-->>FE: {code:0, data}

    Note over FE,FS: D 运行期日志(列表)
    FE->>API: getRuntimeLogs({level, from, to, keyword, page, page_size})
    API->>RT: GET /api/admin/runtime-logs?...
    RT->>RT: require_admin()
    RT->>SVC: read_runtime_logs(RUNTIME_LOG_DIR, filters)
    SVC->>FS: 读 *.log 逐行 json.loads
    FS-->>SVC: 命中行
    SVC-->>RT: {items, total, page, page_size}
    RT-->>FE: {code:0, data}

    Note over FE,FS: D 运行期日志(下载)
    FE->>API: getRuntimeLogDownload({level, from, to, keyword})
    API->>RT: GET /api/admin/runtime-logs?download=true
    RT->>RT: require_admin()
    RT->>SVC: build_runtime_log_stream(RUNTIME_LOG_DIR, filters)
    SVC->>FS: 读 *.log 过滤行
    RT-->>FE: StreamingResponse(text/plain, attachment)