日志与归档系统协同重构计划
- 文档性质:日志写入、检索、归档、存储、运维与恢复实施计划
- 设计真源:日志与归档协同重构方案
- 产品规格:日志检索与归档协同
- 依据:日志归档与 MySQL 日志维护、现行归档命令
- 当前状态:仅完成只读审计和方案编制;未执行归档、未删除历史文件、未变更生产配置。
- 2026-08-12 转入技术债:不在当前迭代排期,保留决策依据;重新评估条件以产品规格与设计文档为准。
背景
现行 db:archive-logs 对配置白名单执行 pt-archiver --file --purge。它有表白名单、路径校验、全局文件锁、每表 PID、执行报告、SHA-256 和 archive_audit_logs,但归档文件直接写入最终路径,源记录可在文件完成持久化前被清除;哈希和文件头只在命令结束后检查。该流程无法把“归档物已可靠落盘”作为物理删除的前置条件。
同步审查发现,operation_logs 与 activity_logs 正在双写,管理端 channel 依赖多张数据库表和 Laravel 文件日志,且管理员清理接口可绕过归档直接删除。automation_logs 是自动化幂等状态,gateway_logs 可能是支付证据,二者不能再被当作默认普通日志处理。本计划先收敛日志真源与分类,再实施 V2 归档。
本地归档根还保留了旧实现产生的 JSON/JSONL 元数据。当前清理器只匹配 .log,因此不会处理这些历史文件。它们不是本计划可直接删除的对象。
本计划的目标是将归档改造成可中断、可恢复、可校验的两阶段流程,同时保持普通日志与财务/支付/审计数据的边界。
目标
- 每一批源记录只有在对应归档物完成校验、持久化和发布后才能被物理删除。
- 中断、磁盘写入失败、重复触发和发布后删除失败均可通过持久化状态恢复,不静默遗漏数据。
- 归档物、审计记录、执行报告和恢复命令之间有一一对应、可验证的关联。
- 使用 Laravel 心跳中的每日归档任务;增加可供外部监控读取的健康检查,并保持
schedule:run为唯一系统调度入口。 - 每个可归档日志流都有唯一在线真源、明确读模型和经过批准的保留策略;归档不再改变管理端结果的完整性语义。
- 移除管理端直接删除与文件重写旁路;
automation_logs、财务/支付/审计数据和未批准流不参与通用归档。
非目标
- 不改变普通日志的业务保留期限;当前默认 30 天源数据、180 天归档文件,任何期限调整须由数据责任人与合规负责人确认。
- 不处理财务流水、账单、支付、支付回调、失败队列和归档审计表。
- 不自动删除、迁移或伪造旧归档根中的 JSON/JSONL 元数据。
- 不把归档文件目录配置为 NAS、外部挂载或后台可编辑路径。
- 不在本计划内执行生产归档、
OPTIMIZE TABLE、生产定时任务变更或历史数据恢复。 - 不在未完成对账前删除
operation_logs,也不保留长期双写或“表为空才回退”的运行时兼容路径。
目标协议与边界
V2 归档状态机
每个表的每个 ID 分块作为一个独立归档物,状态只能按下列方向推进:
text
planned -> staging -> verified -> published -> purging -> purged
\-> failed / needs_recoveryplanned:记录固定的表名、截止时间、ID 下限/上限和预期行数;仅允许白名单中的普通日志表。staging:pt-archiver不带--purge写入 V2 根目录内的.part文件;同一批次绝不追加到既有最终文件。verified:流式校验 CSV 表头、行数、ID 边界、文件大小和 SHA-256;文件句柄 flush/fsync 完成后才可进入下一步。published:.part以同目录原子 rename 成最终 CSV,并以相同方式发布包含版本、表、ID 边界、截止时间、行数、大小和哈希的 manifest。purging:只按已发布 manifest 的固定表、截止时间和 ID 边界删除源记录;删除操作以小事务分块并记录实际删除数。purged:确认该选择范围内无剩余候选记录,写入最终审计、执行报告和事件日志。任一异常留下可诊断状态,不根据“命令退出”推断成功。
新目录固定为 storage/app/private/log-archives/v2/{YYYY-MM}/{batch-id}/。目录及文件名只由服务生成,禁止命令参数或管理端提交任意路径。仓库内未发现当前归档目录的读取调用方;上线前仍须由运维负责人确认没有仓库外消费者依赖旧目录或 .log 文件名。
日志真源与检索切换
activity_logs是 API、认证与业务活动的目标唯一在线真源;新增事件 ID、流名、链路 ID 和发生时间后,用窗口化回填与对账替代当前operation_logs双写。- 管理端保留既有 channel 与在线响应结构,但
AdminLogV2QueryService改为显式 source 映射,禁止以“表存在/有数据”隐式切源。归档历史通过 manifest 目录做受限冷热检索,不能暴露文件路径。 schedule_run_logs的历史与activity_logscron 镜像必须能关联去重;schedule_task_runs仅处理终态历史,queued/running永远留在线。automation_logs永久移出通用归档与清理,gateway_logs在财务/合规批准前移出默认白名单;消息和插件运行日志先完成数据分类再决定是否进入首批。POST /v2/admin/log-cleanups先停止其物理删除及laravel.log重写能力,后续只能创建受审计的保留变更申请;它不再是归档或清理执行入口。
数据与恢复契约
- 仅允许
config/log_archive.php的 8 张表;财务、支付和审计表继续在服务层拒绝。 - 归档前通过
created_at < cutoff与固定 ID 边界选择记录。实施前必须以实库EXPLAIN和索引实况确认每张表按created_at筛选的代价;缺失索引才新增迁移,不能凭计划猜测索引。 - 新增迁移只追加字段或表。建议扩展
archive_audit_logs,为每个归档物记录协议版本、ID 边界、截止时间、预期/验证/删除行数、临时/最终/manifest 相对路径、哈希、大小和各阶段时间;历史行保持不改写。 - 恢复命令只能按审计记录定位 V2 manifest,默认 dry-run,且必须显式确认后执行。它校验根目录白名单、文件大小、哈希、CSV 头和表结构;按主键只插入缺失记录,遇到同 ID 不同内容时失败并输出冲突清单,绝不覆盖现有记录。
- 不为旧目录实现透明运行时兼容。旧产物先只读盘点、备份与分类;如需清理或迁移,另建经过授权的历史数据处置计划。
范围与验收
| 编号 | 验收项 |
|---|---|
| A1 | 8 张白名单表可按 30 天默认值和显式 --retain-days 生成 V2 归档物;受排除表始终在启动外部工具前失败。 |
| A2 | 任何 .part 写入、CSV/哈希校验或 manifest 发布失败时,源表记录数不减少。 |
| A3 | 已发布、未清除的批次重试时只进入清除和收尾,不重复导出、不追加表头、不产生第二份最终归档物。 |
| A4 | 清除过程中中断后能根据审计状态恢复,并准确报告已删、未删和异常记录;不把不完整批次标记为成功。 |
| A5 | 篡改、丢失、大小不符或路径越界的归档物无法恢复或清除,并产生可定位告警状态。 |
| A6 | 恢复 dry-run、成功恢复、重复恢复和 ID 冲突均有自动化测试;恢复不能写入非白名单表。 |
| A7 | 归档文件保留清理仅处理已验证的 V2 终态归档物;旧 JSON/JSONL 和孤立 .part 默认不删除,只告警和列入盘点。 |
| A8 | Linux 演练验证 pt-archiver、凭据权限、并发、Laravel 心跳任务、健康检查和首轮报告;生产切换前不以本地静态测试代替。 |
实施步骤
Phase 0:确认边界与基线
| 编号 | 工作项 | 交付物 | 验收 |
|---|---|---|---|
| P0-0 | 在生产变更单中确认暂停现有 --execute 直删任务或切为 dry-run;未完成预检不得执行物理删除。 | 运维确认与任务状态证据 | V2 未发布期间不存在无 manifest 的生产删除。 |
| P0-1 | 在隔离或获授权的只读环境盘点 8 表的行数、最早时间、EXPLAIN、索引和增长速率。 | 基线报告 | 每张表的选择条件与索引选择有实证。 |
| P0-2 | 运维确认 30/180 天是否满足实际保留要求,并确认无外部程序消费旧目录、旧文件名或旧 manifest。 | 已批准的政策与消费者清单 | 未确认时不切换产物布局。 |
| P0-3 | 盘点旧归档根:按版本、日期、大小、哈希和可读性分类,单独备份清单。 | 只读清单 | 不删除、不移动、不修改旧文件。 |
| P0-4 | 明确日志表归档后不可更新的契约;不满足时改为记录精确主键列表或停止该表进入 V2。 | 选择稳定性决策 | 删除谓词不会误删后来变更的记录。 |
| P0-5 | 为所有管理端 channel 建立日志流目录,标出唯一写入真源、镜像、状态表、数据责任人、可归档资格与读模型。 | 已批准的日志流清单 | 不存在按表名猜测为普通日志的删除行为。 |
| P0-6 | 对 operation_logs/activity_logs 双写、调度镜像和管理员直删路径做行数、事件 ID、时间范围与调用面基线。 | 对账基线与下线清单 | 能量化迁移缺口并证明直删入口已冻结。 |
Phase 1:持久化归档协议
| 编号 | 工作项 | 交付物 | 验收 |
|---|---|---|---|
| P1-0 | 新增日志流目录、ActivityEventWriter 和 activity_logs 追加字段;窗口化回填旧操作日志并做可重复对账。 | 唯一写入器、映射迁移、对账报告 | 迁移窗口内一个逻辑事件只保留一个可查询真源。 |
| P1-1 | 新增 V2 批次/归档物元数据迁移,保留现有 archive_audit_logs 历史;状态、边界、哈希与错误信息可索引查询。 | 新增迁移、模型与状态约束 | 不改写历史审计;状态转移非法时失败。 |
| P1-2 | 将 LogArchiveService 拆分为选择、暂存、校验发布、清除、收尾与恢复协调器;移除无调用方的旧直接 --file --purge 路径。 | 小职责服务与命令编排 | 生产运行时只有 V2 协议可被调用。 |
| P1-3 | 为 CSV 流实现写入后 flush/fsync、哈希/行数/ID 边界校验、原子 rename 和 manifest 原子发布。 | 文件协议实现 | 最终文件与 manifest 永不指向未校验内容。 |
| P1-4 | 将 pt-archiver 改为仅导出暂存文件;实现受 manifest 约束的分块清除和恢复扫描。 | 受控清除器 | 清除前不存在可绕过的发布校验。 |
| P1-5 | 将报告和逐事件日志改为引用 batch ID 与归档物 ID;敏感配置和凭据仍不写入报告。 | 报告契约 | 任一归档物可从报告反查审计和 manifest。 |
Phase 2:恢复、清理与可观测性
| 编号 | 工作项 | 交付物 | 验收 |
|---|---|---|---|
| P2-1 | 增加受确认的恢复命令及 dry-run,封装 manifest 校验、逐行导入、ID 冲突检测和结果审计。 | 恢复服务、命令与测试 | 恢复不覆盖现有数据,篡改文件必被拒绝。 |
| P2-2 | 增加只读健康检查:最近成功批次、滞留状态、候选积压、未处理 .part 与文件保留到期情况。 | JSON 健康检查输出 | 监控无需读取归档内容或凭据即可发现失败。 |
| P2-3 | 重写 V2 清理器:只清理 manifest 校验通过且已过政策期限的终态归档物;失败或未知文件隔离告警。 | 保留清理实现 | 不再仅按扩展名递归删除。 |
| P2-4 | 更新运维真源,补充 V2 目录、状态含义、健康检查、恢复演练与旧文件处置边界。 | 更新后的参考资料 | 不再宣称中断时必然不会遗漏。 |
| P2-5 | 将管理端 channel 查询切到显式真源映射,增加受限冷热检索、manifest 来源标识与跨边界去重。 | 查询服务、接口契约、管理端交互 | 不再因表是否有数据回退,归档不可用可见。 |
| P2-6 | 删除管理端直接物理清理与 Laravel 文件逐行重写;保留只读容量预览和受审计的保留变更申请。 | API/权限调整与回归 | 任意删除均由归档协议或独立文件轮转策略执行。 |
Phase 3:验证、灰度与切换
| 编号 | 工作项 | 交付物 | 验收 |
|---|---|---|---|
| P3-1 | 增加测试替身,模拟外部归档成功、写满磁盘、哈希失败、rename 失败、进程中断、清除中断和 PID/全局锁冲突。 | Feature/Unit 回归测试 | A1 至 A7 自动化覆盖。 |
| P3-2 | 在 Linux 隔离环境对小表执行 V2 dry-run、暂存、发布、清除、恢复和篡改拒绝演练。 | 演练报告 | 文件/审计/源表行数逐项一致。 |
| P3-3 | 生产确认每分钟 schedule:run 与默认队列消费正常,完成预检后仅灰度 gateway_logs;观察完整周期与健康检查,再逐表放开。 | 变更单、首轮报告 | 未出现滞留、重复或缺失,且运维确认。 |
| P3-4 | V2 全量稳定后,保留 log-archive 心跳任务作为唯一归档入口;移除旧的直接归档 Crontab。 | 运维配置与验收记录 | 至少连续 7 天健康检查正常。 |
| P3-5 | 以低风险、已批准日志流灰度冷热检索;确认后再迁移 operation_logs 历史并清除双写代码。 | 灰度报告与下线变更 | 十个 channel 的在线行为和归档边界符合规格。 |
质量门禁与测试矩阵
| 场景 | 最低验证 |
|---|---|
| 选择 | 8 表白名单、排除表、保留天数覆盖、空候选、不同 ID 分块与真实索引 EXPLAIN。 |
| 文件协议 | .part 不可见、fsync 后校验、原子发布、重复运行、重复表头、哈希/大小/头部失败。 |
| 清除 | 发布前零删除、精确 ID/截止时间约束、部分删除中断、重试幂等、全局锁和每表 PID 冲突。 |
| 恢复 | dry-run、成功导入、重复导入、ID 内容冲突、路径越界、哈希篡改、非白名单表。 |
| 运维 | 无 pt-archiver、不可读凭据、不可写目录、过期成功批次、滞留批次和心跳任务未执行。 |
| 回归 | php artisan test tests/Feature/DatabaseArchiveLogsCommandTest.php 及新增恢复/健康检查测试、Pint、PHPStan、pnpm run docs:check。 |
| 日志迁移 | OperationLogService、LogOperation、AdminLogV2QueryService、ScheduleRunLogService 与管理端日志 API 的定向测试;双写回填、去重、冷热边界和直删拒绝。 |
| 状态保护 | AutomationLog 幂等、ScheduleTaskRunRepository::activeRunForTask() 与支付网关排除的定向回归。 |
上线、监控与回滚
- 上线前完成数据库备份可恢复性确认、磁盘容量评估、归档账号最小权限复核和 Linux 隔离演练。
- 新迁移仅新增;旧归档记录和文件不改写。切换前移除旧的直接归档 Crontab,确认不存在
staging、published、purging等非终态 V2 批次。 - 灰度期间,每轮比对候选行数、验证行数、实际删除数、最终文件哈希和 manifest;任一不一致立即停止后续表。
- 回滚只移除
log-archive任务注册并停止后续批次。已经published的归档物不得删除;已进入purging的批次必须用 V2 恢复协调器收敛到终态,不能恢复旧的直接--purge触发方式。 - 健康检查应告警:超过约定周期无成功批次、非终态批次超时、候选积压持续增长、文件/manifest 校验失败和存储空间低于阈值。具体阈值由运维确定,不在代码中硬编码。
进度
- [x] 完成当前实现、调度、配置、文档与本地归档根的只读审计。
- [x] 建立 V2 两阶段协议、恢复与灰度的实施方案。
- [x] 将现行归档命令接入
log-archive心跳任务:每日 02:00 入队、显式执行、不可手动触发,并完成定向回归与静态检查。 - [x] 完成日志写入、管理端检索、调度镜像、直接清理与归档白名单的静态协同审查。
- [x] 固化日志流目录、唯一真源、冷热检索和状态表保护的目标方案。
- [ ] Phase 0:等待数据保留责任人、运维及外部消费者确认。
- [ ] Phase 1:等待方案评审通过后开始实现。
- [ ] Phase 2:等待 V2 归档物协议可用后开始实现。
- [ ] Phase 3:等待隔离 Linux 演练与生产变更授权。
决策日志
| 日期 | 决策 | 原因 |
|---|---|---|
| 2026-08-01 | 采用“暂存、校验发布、再清除”的 V2 协议 | 当前直接 --file --purge 不能把持久化校验作为删除前置条件。 |
| 2026-08-01 | V2 使用独立目录与 manifest,不为旧目录保留运行时兼容层 | 避免新旧产物混写或误判;旧文件先只读盘点。 |
| 2026-08-01 | 将归档接入 Laravel 心跳任务 | 用户要求统一使用定时任务;每分钟 schedule:run 已是生产唯一调度入口,任务运行记录、队列锁与失败状态可复用。 |
| 2026-08-01 | 将归档任务限制为 1 小时、锁定 61 分钟 | 当前数据库队列的 retry_after 为 3900 秒;不得通过增大任务超时破坏现有任务的可见性与防重复契约。 |
| 2026-08-01 | 不在方案中变更 30/180 天保留参数 | 保留期限属于数据政策,应由责任人确认而非由技术重构隐式改变。 |
| 2026-08-01 | activity_logs 作为访问、认证与业务活动的目标唯一真源 | 当前双写与查询回退无法证明完整性;迁移对账后删除旧写入路径。 |
| 2026-08-01 | automation_logs 不进入通用归档,gateway_logs 暂停默认归档 | 前者承担幂等控制,后者关联支付网关证据,均不能按普通日志期限猜测删除。 |
| 2026-08-01 | 管理端直接清理改为受审计保留变更流程 | 当前数据库删除和日志文件重写绕过归档物、manifest、恢复与审计。 |
