2026-07-11 23:13:10 +08:00
|
|
|
|
# 管理后台日志体系 — 系统设计 + 任务分解
|
|
|
|
|
|
|
|
|
|
|
|
> **阶段**:standard SOP 第二阶段(架构设计,输入给工程师 寇豆码)
|
|
|
|
|
|
> **架构师**:高见远(software-architect)|**日期**:2026-07-09
|
|
|
|
|
|
> **上游输入**(已 Read 并核对):
|
|
|
|
|
|
> - 产品+设计文档:`docs/04-功能设计/系统日志与审计日志-产品与设计.md`(许清楚)
|
2026-08-03 18:46:55 +08:00
|
|
|
|
> - PRD 增补:`01-产品文档/08-集成生态/PRD-REQ-集成-002-管理后台-v1.2.md` §「管理后台日志体系」
|
2026-07-11 23:13:10 +08:00
|
|
|
|
> - 架构增补:`docs/03-技术架构/00-系统架构设计文档-v1.3.md` §7.1.1 日志体系
|
2026-08-03 18:46:55 +08:00
|
|
|
|
> - 运行期日志设计:`docs/06-安全审计/审计报告-安全审计/健康检查+错误码+日志结构化.md` §3
|
2026-07-11 23:13:10 +08:00
|
|
|
|
> - 真实代码:`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 后端方案必须可部署、可执行。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 一、设计要点摘要
|
|
|
|
|
|
|
|
|
|
|
|
| 项 | 结论 |
|
|
|
|
|
|
|----|------|
|
|
|
|
|
|
| 技术栈 | 前端 Vue3 + TS + Element Plus + Pinia(沿用);后端 FastAPI + SQLAlchemy(async) + PostgreSQL(沿用)|
|
|
|
|
|
|
| A 配置变更历史 | 复用 `config_change_logs`;后端 `GET /api/admin/system-logs` 增 `config_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) | 不改代码,T5 做回归验证:`PUT /api/admin/configs/{key}` 同时写 `config_change_logs` 与 `audit_logs`(`config_change` 事件)|
|
|
|
|
|
|
| 权限 | A 沿用 `require_admin`(建议后续命名为 `config_log:read`,本迭代不纳入 RBAC 重构);B 沿用 `audit_log:read:all`;D 用 `require_admin` |
|
|
|
|
|
|
| 新增依赖 | 无(前端无新 npm 包;后端用标准库 `pathlib/os/glob` + `logging.RotatingFileHandler` + `fastapi.responses.StreamingResponse`)|
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 二、实现方案与框架选型
|
|
|
|
|
|
|
|
|
|
|
|
**难点与对策**
|
|
|
|
|
|
|
|
|
|
|
|
1. **A 增加筛选而不改表/不加索引**:`config_change_logs` 已有 `idx_ccl_config_key`、`idx_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.vue`、`runtime-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-logs`:`GET /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`,新增 `RotatingFileHandler`(JSON 格式)写入 `RUNTIME_LOG_DIR`,保留 stdout handler |
|
|
|
|
|
|
| `core/config.py` | 修改 | 新增配置项 `RUNTIME_LOG_DIR`(默认 `/app/logs`)|
|
|
|
|
|
|
| `api/router.py` | 修改 | 挂载 `runtime_logs_router`(与 `audit_logs_router` 一致,~L243-244 后追加)|
|
|
|
|
|
|
| `docker-compose.yml` | 修改 | backend 服务 `volumes` 增加 `${RUNTIME_LOG_HOST_DIR:-/var/log/wecom-it-desk}:/app/logs`;`environment` 增加 `RUNTIME_LOG_DIR=/app/logs`、`LOG_FORMAT=json` |
|
|
|
|
|
|
|
|
|
|
|
|
> 注:`api/audit_logs.py`、`models/audit_log.py`、`services/audit_log_service.py`、`models/config_change_log.py`、`schemas/admin.py`(ConfigHistoryItem) 本次**均不修改**,直接复用。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 四、接口设计(含请求/响应 JSON 示例)
|
|
|
|
|
|
|
|
|
|
|
|
> 统一响应体:`{ "code": 0, "message": "success", "data": {...} }`;错误 `code != 0`。
|
|
|
|
|
|
> 时间参数统一 **ISO8601**(如 `2026-07-09T00:00:00+08:00` 或 `2026-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)
|
|
|
|
|
|
```
|
|
|
|
|
|
**响应**
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"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? ISO8601(alias from_time)
|
|
|
|
|
|
to string? ISO8601(alias to_time)
|
|
|
|
|
|
page int = 1
|
|
|
|
|
|
page_size int = 50 (<=500)
|
|
|
|
|
|
Header/Permission: audit_log:read:all
|
|
|
|
|
|
```
|
|
|
|
|
|
**响应**(节选)
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"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
|
|
|
|
|
|
```
|
|
|
|
|
|
**列表响应**
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"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** 运行期日志 | **文件系统**(不建表)| 每行 JSON:`timestamp, 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-desk`)bind 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 服务改动)
|
|
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
|
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:00` 与 `Z`)。
|
|
|
|
|
|
5. `keyword`:对整行 JSON 文本(或 `message`)做子串匹配,支持 `trace_id` / `employee_id` 检索。
|
|
|
|
|
|
6. 分页:收集命中行 → 时间倒序 → 切片(`page/page_size`)。**短期限制**:全量读入内存再切片;若单文件超大,后续接 Loki(已在审计报告中规划)。
|
|
|
|
|
|
7. `download=true`:将命中行原样(每行一条 JSON)写入 `StreamingResponse`,`Content-Disposition` 文件名带时间戳。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 七、权限设计
|
|
|
|
|
|
|
|
|
|
|
|
| 页面/接口 | 权限 | 现状 | 本次 |
|
|
|
|
|
|
|-----------|------|------|------|
|
|
|
|
|
|
| A `GET /api/admin/system-logs` | `require_admin`(admin 角色)| 已有 | 沿用;**建议后续**明确命名为 `config_log:read`(与 B 一并纳入 RBAC 梳理,本迭代不做)|
|
|
|
|
|
|
| B `GET /api/admin/audit-logs` | `audit_log:read:all`(admin/auditor)| 已有(`require_permission`)| 复用,零改动 |
|
|
|
|
|
|
| D `GET /api/admin/runtime-logs` | `require_admin`(admin 角色)| 新增 | 新增端点用 `require_admin` |
|
|
|
|
|
|
| 双写 `PUT /api/admin/configs/{key}` | 已有 | 维持 A+B 双写 | **不改**(T5 验证)|
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 八、依赖包
|
|
|
|
|
|
|
|
|
|
|
|
- **前端**:无新增 npm 包(沿用 Element Plus / axios / Pinia)。
|
|
|
|
|
|
- **后端**:无新增 PyPI 包。仅用标准库 `pathlib / os / glob` + `logging.RotatingFileHandler` + `fastapi.responses.StreamingResponse`。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 九、共享知识(跨文件约定)
|
|
|
|
|
|
|
|
|
|
|
|
- **日志级别枚举**:`DEBUG < INFO < WARNING < ERROR < CRITICAL`(D 的 `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/to`(`alias` 注入),与 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` 语义**:采用「>= 阈值」;若产品要求「精确匹配」需调整(影响前端下拉说明)。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 十一、任务分解清单(有序 · 含依赖 · 验收要点)
|
|
|
|
|
|
|
|
|
|
|
|
> 颗粒度按团队主理人指定:T1 侧边栏+路由;T2 A 筛选;T3 B 页;T4 D 页(前后端);T5 双写回归;T6 联调权限。
|
|
|
|
|
|
|
|
|
|
|
|
### T1 — 侧边栏重命名 + 路由入口(A 改名、B/D 新增)
|
|
|
|
|
|
- **涉及文件**:`frontend-admin/src/components/Sidebar.vue`、`frontend-admin/src/router/index.ts`
|
|
|
|
|
|
- **依赖**:无
|
|
|
|
|
|
- **优先级**:P1
|
|
|
|
|
|
- **验收要点**:① 侧边栏「监控与数据」出现「配置变更历史(A)」「安全审计日志(B)」「运行期日志(D)」三个独立入口,无「系统日志」歧义统称;② 路由 `audit-logs`、`runtime-logs` 已注册且 `meta.title` 正确;③ A 路由 title 改为「配置变更历史」。
|
|
|
|
|
|
|
|
|
|
|
|
### T2 — A 页筛选(前端控件 + 后端参数)
|
|
|
|
|
|
- **涉及文件**:`frontend-admin/src/views/SystemLogs.vue`、`frontend-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)
|
|
|
|
|
|
- 部署:`docker-compose.yml`(backend volumes + env)
|
|
|
|
|
|
- **依赖**:T1
|
|
|
|
|
|
- **优先级**:P0
|
|
|
|
|
|
- **验收要点**:① admin 可按级别+时间+关键字筛选查看日志;② 可下载 `.log`/`.txt` 文件流;③ `logging_config` 写入 `RUNTIME_LOG_DIR` 且 docker-compose 挂载生效;④ 非 admin 调接口返回 403;⑤ 目录不可读时不 500。
|
|
|
|
|
|
|
|
|
|
|
|
### T5 — 双写回归验证(决策2,无代码改动)
|
|
|
|
|
|
- **涉及文件**:无(验证 `PUT /api/admin/configs/{key}` 既有行为)
|
|
|
|
|
|
- **依赖**:T2
|
|
|
|
|
|
- **优先级**:P0
|
|
|
|
|
|
- **验收要点**:① 修改任一配置项后,`config_change_logs` 与 `audit_logs`(`config_change` 事件) 均新增对应记录;② 两表记录数一致、内容可对应;③ 不在任一侧删除/合并。
|
|
|
|
|
|
|
|
|
|
|
|
### T6 — 联调与权限核对
|
|
|
|
|
|
- **涉及文件**:全链路(A/B/D 页面 + 接口)
|
|
|
|
|
|
- **依赖**:T2、T3、T4(T5 可并行)
|
|
|
|
|
|
- **优先级**: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 补对应审计事件;本次不实现。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 附录 A — 类图(Mermaid)
|
|
|
|
|
|
|
|
|
|
|
|
```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)
|
|
|
|
|
|
|
|
|
|
|
|
```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)
|
|
|
|
|
|
```
|