Skip to content

日志与归档协同重构方案

结论

日志系统不能与归档系统分开重构。当前业务和 API 访问会通过 OperationLogService 同步写入 operation_logsactivity_logs,而管理端不同 channel 又读取数据库表、调度表和 Laravel 文件日志。若直接按表归档或由管理端直接删除,会产生重复、缺页、破坏幂等状态和缺失归档证据的风险。

目标不是把全部内容塞入一张表,而是建立一个受版本控制的“日志流目录”:每一类数据都有唯一写入真源、检索投影、保留策略和允许的归档状态。只有被目录明确标为可归档的不可变事件,才能进入 V2 两阶段归档协议。

当前审查结果

区域当前实现与归档的冲突或风险
API 与业务操作全局 LogOperation 中间件记录绝大多数 /api/* 请求;OperationLogService 先写 operation_logs,再容错双写 activity_logs同一事件有两份 ID,双写失败不阻断主流程;管理员查询的切源条件不能证明两表完整一致。
管理端检索/v2/admin/logs/{channel} 通过 AdminLogV2QueryService 提供 activity、api、admin-logins、短信、邮件、网关、runtime、schedule、tasks、system 十个 channel。查询层同时依赖 operation_logsactivity_logsschedule_run_logs、插件运行日志和 laravel.log,不存在统一的冷热边界。
调度记录ScheduleRunLogService 同时写 schedule_run_logsactivity_logs 的 cron 镜像;心跳任务另写 schedule_task_runs同一执行有镜像和独立运行状态,按整表归档会导致页面重复或影响进行中任务的去重。
管理端清理POST /v2/admin/log-cleanups 可直接删除短信、邮件、API、管理员登录、业务审计、调度记录,并会重写 storage/logs/laravel.log该旁路不生成归档物、manifest 或可恢复审计,不能继续与归档任务并存。
当前归档db:archive-logs 的默认白名单含 8 表,使用 pt-archiver --file --purge,默认在线 30 天、文件 180 天。心跳任务 log-archive 每日 02:00 已注册并显式传入 --execute外部工具写入最终文件与物理删除耦合,文件校验在删除后;白名单把幂等状态与支付网关证据当作普通日志处理。
文件运行日志Laravel 使用 daily channel,默认保留 14 天;管理员直接从文件解析 runtime/task 内容。这不是数据库归档物,按行重写活动日志文件会与 Monolog 轮转产生并发和完整性风险。

必须先调整的分类

automation_logsrecordOnce()hasRecord()markExecuted() 被自动化流程用于幂等控制,不能按通用日志期限归档或删除。gateway_logs 关联账单和支付网关交互,虽然当前未列入财务排除清单,但在数据责任人确认前也不能作为普通日志删除。二者从 V2 默认白名单移除。

目标架构

text
请求、业务服务、调度、插件
          |
          v
  日志流目录(类型、真源、保留、归档资格)
          |
   +------+-----------------+------------------+
   |                        |                  |
   v                        v                  v
在线事件真源             业务状态表         Laravel 文件轮转
activity_logs 等          automation_logs    daily / 14 天
   |
   v
V2 归档协调器:暂存 -> 校验 -> 发布 -> 清除
   |                                  |
   v                                  v
归档物 + manifest + 审计元数据      在线检索 / 冷检索目录

1. 日志流目录与数据等级

新增仅由后端读取的 LogStreamRegistry(配置加测试,不开放管理端任意修改)。每个流至少声明:stream、在线真源、是否不可变、是否可归档、选择时间列、热数据期限、归档文件期限、冷检索能力和数据责任人。运行时不允许命令参数绕过该目录指定表名或文件路径。

分类当前来源V2 处置前置条件
API、认证、业务活动activity_logsoperation_logs 为过渡双写来源activity_logs 成为唯一在线事件真源;历史 operation_logs 只做一次迁移与清退。用事件 ID、行数和抽样内容完成回填对账;API 与管理员登录查询改读新真源后才停止旧写入。
短信、邮件投递message_logs候选可归档流,不进入默认删除。确认通知追溯期限、可检索字段及对个人数据的保留要求。
传统 Cron 执行历史schedule_run_logsactivity_logs 镜像保留一个规范检索投影,终态记录才允许归档。明确镜像的事件关联字段,查询层不得把同次运行显示两次。
心跳任务运行状态schedule_task_runsqueuedrunning 永久留在线;仅终态历史可在单独期限下归档。归档选择条件必须排除活跃状态,且不影响 activeRunForTask()
自动化幂等状态automation_logs不归档、不纳入管理员清理。如未来需要压缩,另行设计可证明不破坏幂等性的状态归并。
插件运行诊断integration_plugin_runtime_logs候选可归档流。完成请求/响应元数据字段审查、冷热检索和插件边界回归。
支付网关交互gateway_logs默认排除。财务与合规确认其是否属于支付审计证据及最长保留期。
归档审计、财务、支付、回调、失败队列archive_audit_logs永久排除。只能依照各自业务/合规策略处理。
Laravel 文件日志storage/logs/laravel-*.log继续由 Monolog 日轮转处理;不进入数据库归档命令。删除管理端逐行重写,改为轮转、只读检索和独立运维策略。

2. 单一事件真源迁移

activity_logs 承接访问、认证和业务活动,保留已有 actor_*subject_*moduleactiondescriptioncontextip_addresscreated_at 语义。新迁移仅追加以下稳定检索字段,历史内容不改写:

  • event_id:ULID,唯一标识一个逻辑事件,供回填、归档和冷检索引用。
  • stream:受 LogStreamRegistry 约束的 accessauthbusinessschedule 等流名。
  • trace_id:从现有 context 提升的可索引关联键;没有时保持 NULL
  • occurred_at:事件发生时间;过渡期与 created_at 一致,禁止用归档时间替代。

ActivityEventWriter 负责落库。OperationLogService 在迁移期间只作为调用面适配器,最终删除其对 operation_logs 的写入与 ActivityLog 容错双写实现;它不再保留没有调用方的双表兼容路径。全局中间件继续生成 X-Request-Id,并将成功轮询跳过规则迁入日志流目录,改动必须由路由级测试覆盖。

3. 在线与冷数据检索契约

管理端现有 channel 名称和分页响应在在线查询阶段保持不变。切换完成后,AdminLogV2QueryService 不再按“表是否有数据”选择来源,而按 channel 显式映射到唯一在线真源。

归档完成的历史数据通过归档目录和 manifest 建立只读目录索引。新增的冷检索只接受时间范围、channel/stream、事件 ID、trace ID 和受限关键词;不接受物理路径、文件名或原始 SQL。跨冷热边界采用时间游标而非数据库 offset,返回带来源的不可变 ID:hot:{event_id}archive:{archive_item_id}:{event_id}。当范围跨越热数据截止点时,服务按 occurred_at,event_id 合并去重;若归档物未通过校验,结果必须显式标注不可用而不是静默缺页。

冷检索首期仅开放 manifest 已验证的可归档流。管理员详情页显示数据来源、归档批次、哈希和恢复状态,不允许在线编辑归档内容。

4. V2 归档协议

对每个已批准的表和固定 ID 分块执行如下状态机:

text
planned -> staging -> verified -> published -> purging -> purged
    \-> failed / needs_recovery
  1. 创建审计项,固定表、流、截止时间、ID 边界和预期行数。
  2. pt-archiver 只导出同目录 .part,不带 --purge,不追加已有文件。
  3. 流式校验 CSV 表头、行数、ID 边界、大小和 SHA-256;flush/fsync 成功后原子发布数据文件与 manifest。
  4. 仅按已发布 manifest 的表、截止时间和 ID 边界分块删除源记录;记录实际删除数。
  5. 无候选残留后标记 purged,写入归档审计、心跳任务摘要与可观测事件。

归档根固定为 storage/app/private/log-archives/v2/{YYYY-MM}/{batch-id}/。manifest、审计记录和报告必须能双向关联。已发布但未清除的批次只能继续清除或显式恢复,不能再导出第二份文件。

5. 定时任务与删除入口

生产只保留每分钟 schedule:run 作为系统入口,由 scheduler:heartbeat 在 15 分钟槽位内派发 log-archive 队列任务。每日 02:00 是默认触发时点,不是保留策略本身。任务运行使用现有 schedule_task_runs 互斥、超时和失败记录;V2 上线后还需写入批次 ID、候选数、发布数、删除数和滞留状态。

当前注册的任务仍会调用旧的 --execute 直删协议,因此它不能作为 V2 上线前的生产执行路径。实施前由运维在环境层面确认暂停该任务或仅运行 dry-run;本方案不授权在未完成预检时执行物理删除。

POST /v2/admin/log-cleanups 不得再直接删除数据库日志或重写当前 Laravel 日志文件。切换期将其改为只读预览与引导说明;V2 稳定后可改为受权限保护的“创建归档保留变更申请”,由数据策略和定时任务执行,不能绕过 manifest 与审计记录。

迁移顺序

  1. 盘点实库行数、最早时间、索引、增长率、现有归档文件和外部消费者;确认每个流的数据责任人与期限。
  2. 先冻结直接清理的物理删除能力,并从归档候选中排除 automation_logsgateway_logs 和所有未批准流。
  3. 新增事件字段、日志流目录与唯一写入器;按窗口回填 operation_logsactivity_logs,以事件映射和对账报告验证。
  4. 将管理端 API、管理员登录与业务 activity 查询切到 activity_logs,在完整观察期内验证新旧结果;随后停止并删除双写路径。
  5. 实现 V2 元数据、暂存发布、恢复、健康检查和冷热查询,先在单一低风险流灰度。
  6. 将每日任务切换到 V2;稳定后才允许按获批的流清退旧 operation_logs 与旧归档格式。

验收与不可妥协项

  • 每个可归档流有唯一写入真源、责任人和经批准的在线/归档期限;未分类或状态型数据不能被默认删除。
  • 归档文件或 manifest 未校验发布时,源记录数不得减少。
  • 同一 API/业务事件在切换后只出现一条逻辑记录;管理员查询无“表为空才回退”的隐式切源。
  • automation_logs 删除或归档回归测试必须证明不会改变 recordOnce()hasRecord()markExecuted() 的幂等结果;默认验收是该表完全不参与归档。
  • 心跳重试、进程中断、磁盘满、哈希失败、清除中断和归档物篡改均可定位到批次并恢复或告警。
  • 管理端直删路径、文件逐行重写和绕过归档的 Crontab 在切换完成后不存在。
  • 每次迁移均运行受影响 PHP 测试、Pint、PHPStan 与 pnpm run docs:check;涉及管理端冷检索时追加端到端测试。

开放决策

  1. 普通访问/业务日志、消息日志、调度终态和插件诊断的在线与归档保留期限,需要数据责任人与合规负责人书面确认;当前 30/180 天只作为代码默认值,不作为批准政策。
  2. gateway_logs 是否可归档、可检索和保留多久,必须由财务和合规确认,确认前保持排除。
  3. 外部工具、备份或人工脚本是否依赖现有 .log 文件名及目录,需由运维盘点后才能切换 V2 目录。
  4. 冷检索的关键词范围和权限模型需在管理端设计评审中确定,首期不得提供任意文件下载或路径读取。

基于 AGPL-3.0-or-later 发布