diff --git a/.agents/skills/clickhouse-batchwriter/SKILL.md b/.agents/skills/clickhouse-batchwriter/SKILL.md index 5ae09a64..9aa5532f 100644 --- a/.agents/skills/clickhouse-batchwriter/SKILL.md +++ b/.agents/skills/clickhouse-batchwriter/SKILL.md @@ -7,7 +7,7 @@ description: "Wavelet 项目专用:当新增或修改 ClickHouse 批量写入 开始前阅读根目录 `AGENTS.md`。ClickHouse 是辅助 OLAP 存储,**厌恶高频单条写入**(过多小 part);写入路径必须优先批量或异步聚合。 -DDL 与表结构变更见 `database-migration` 技能;本技能只覆盖**运行时写入架构**。 +DDL 与表结构变更见 `database-migration` 技能。日志/分析用途表的判定、三库回落与切换见 `logstore` 技能。本技能只覆盖**运行时写入架构**。 ## 分层职责 @@ -17,7 +17,7 @@ DDL 与表结构变更见 `database-migration` 技能;本技能只覆盖**运 | 批量框架 | `internal/infra/persistence/batchwriter/` | 泛型队列 + 按条数/时间 flush + 非阻塞入队 + 优雅停机;**各业务域独立实例** | | Model | `internal/model/analytics/` | 列定义、`TableName()`、`BatchInsertSQL()`(及可选 `InsertColumns()`) | | Repository | `internal/repository/analytics/` | `BatchInsert*` / `BatchInsertNodeAccessLogs` 等;`PrepareBatch` + 多行 `Append` + 一次 `Send` | -| Apps | `internal/apps//` | 采集、入队、背压;`FlushFunc` 只调 repository,不写 SQL、不 `PrepareBatch` | +| Apps | `internal/apps//` | 采集、入队、背压;`FlushFunc` 只调 logstore / repository,不写 SQL、不 `PrepareBatch` | | 装配 | `internal/platform/bootstrap/bootstrap.go` | 进程启动时调用 `Writer.Start`;初始化时需调用 `lifecycle.OnShutdown` 挂载停机钩子 | | 生命周期 | `internal/platform/lifecycle/lifecycle.go` | 统一协调全局并发优雅停机,业务包无需在 `bootstrap.go` 中硬编码 `Stop` 逻辑 | @@ -37,6 +37,7 @@ writer.Stop(stopCtx) // close 队列 + drain + 最终 flush - `QueueSize`: 10_000 - `MaxBatchSize`: 1_000 +- `MinBatchSize`: 50(未达阈值则跳过按时间 flush,除非设了 `MaxFlushWait`) - `FlushInterval`: 1s 各域可独立覆盖;可观测低频指标可用更小 `MaxBatchSize`(如 100)与更长 `FlushInterval`(如 2–5s),但**不要**退化为逐条 `Send`。 @@ -49,7 +50,8 @@ writer.Stop(stopCtx) // close 队列 + drain + 最终 flush ### FlushFunc 规范 - 签名:`func(ctx context.Context, items []T) error` -- 内部调用 `internal/repository/analytics` 的 `BatchInsert*`(传入 `[]analyticsmodel.X`) +- **日志/分析用途表**:`logstore.Active(ctx)` 再调对应 `BatchInsert*`。禁止 apps 直连 `analyticsrepo` 或 `db.ChConn`。 +- 仅 CH、无需主库回落的分析表:才直接调 `repository/analytics` 的 `BatchInsert*`。 - 在 flush 边界记录一次错误日志,不要把 DB 驱动错误直接暴露给 HTTP 客户端 - `Start` 使用 `context.WithoutCancel(parent)`,避免请求 ctx 取消中断后台 flush @@ -57,11 +59,11 @@ writer.Stop(stopCtx) // close 队列 + drain + 最终 flush 每个业务域拥有自己的 `Writer`、配置与 `FlushFunc`: -| 域 | 表 | 现状 | 目标形态 | -| :--- | :--- | :--- | :--- | -| 管理端审计 | `w_user_access_logs` | `risk_control` → `batchwriter` + `analyticsrepo.BatchInsert` | 已接入 | -| 边缘访问日志 | `of_node_access_logs` | `openflare/chwriter` 异步 flush | 已接入 | -| 可观测时序 | `of_node_metric_snapshots` 等 5 表 | `openflare/chwriter` 五表独立 writer + 进程内短 TTL 去重 | 已接入 | +| 域 | 表 | 写入路径 | +| :--- | :--- | :--- | +| 管理端审计 | `w_user_access_logs` | `risk_control` → `batchwriter` → `logstore.Active` | +| 边缘访问日志 | `of_node_access_logs` | `openflare/chwriter` → `logstore.Active` | +| 可观测时序 | `of_node_metric_snapshots` 等 | `openflare/chwriter` 分表 writer + 进程内短 TTL 去重 → `logstore.Active` | **不要**把 audit、access log、observability 并入同一 channel。 @@ -73,8 +75,9 @@ writer.Stop(stopCtx) // close 队列 + drain + 最终 flush - `len(items)==0` 直接返回 - `db.ChConn == nil` 返回明确错误 - 一次 `PrepareBatch` → 循环 `Append` → 一次 `Send` -4. **Writer 胶水**(`internal/apps//` 或 `internal/repository/analytics/_writer.go`): +4. **Writer 胶水**(`internal/apps//`): - `New` + `Start`,并在初始化逻辑内通过 `lifecycle.OnShutdown("your_writer_name", Stop)` 注册停机回调 + - 日志表的 `FlushFunc` 调 `logstore.Active`(见 `logstore` skill) - 业务路径 `TryEnqueue`;HTTP 背压用 `IsFull()` 5. **测试**: - repository:mock `ChConn` 验证 `BatchInsertSQL` 与 append 列数 @@ -123,14 +126,9 @@ var globalChan chan any ```go // internal/platform/bootstrap/bootstrap.go(示意) -var userAccessLogWriter *batchwriter.Writer[*analytics.UserAccessLog] - func RegisterAPI(ctx context.Context) { - // ... - if config.Config.ClickHouse.Enabled { - initUserAccessLogWriter(ctx) // Start writer - risk_control.BindWriter(userAccessLogWriter) // 或逐步替换 InitLogWriter - } + // 日志 writer 不依赖 clickhouse.enabled:flush 时由 logstore 选库 + risk_control.InitLogWriter(ctx) } ``` @@ -149,7 +147,8 @@ make code-check - flush 按 `MaxBatchSize` 与 `FlushInterval` 触发 - `Stop` 能 drain 队列内剩余项 - repository 层无 goroutine、无 channel -- `clickhouse.enabled: false` 时不 `Start` writer、不入队 +- 日志表:`clickhouse.enabled: false` 时 writer 仍 `Start`,flush 走主库 logstore +- 仅 CH 的分析表:未启用 CH 时不要 `Start`、不要入队 ## 相关文件速查 @@ -157,7 +156,8 @@ make code-check - 连接:`internal/infra/persistence/clickhouse.go` - 审计写入:`internal/apps/risk_control/logics.go` - OpenFlare 写入胶水:`internal/apps/openflare/chwriter/writer.go` -- 节点访问日志 repository:`internal/repository/analytics/node_access_log_writer.go` -- 可观测 repository:`internal/repository/analytics/node_observability_writer.go` +- 日志抽象:`internal/repository/logstore` +- 节点访问日志 CH 实现:`internal/repository/analytics/node_access_log_writer.go` +- 可观测 CH 实现:`internal/repository/analytics/node_observability_writer.go` - 生命周期管理器:`internal/platform/lifecycle/lifecycle.go` - Bootstrap:`internal/platform/bootstrap/bootstrap.go` \ No newline at end of file diff --git a/.agents/skills/database-migration/SKILL.md b/.agents/skills/database-migration/SKILL.md index 238aaafd..52e01a7a 100644 --- a/.agents/skills/database-migration/SKILL.md +++ b/.agents/skills/database-migration/SKILL.md @@ -81,7 +81,7 @@ make code-check ClickHouse 是**辅助 OLAP 存储**,与 PostgreSQL/SQLite 主库**完全独立**的迁移与访问管线: - 主库(PG/SQLite):业务事务数据、`goose_db_version`、双方言 SQL。 -- 分析库(ClickHouse):访问日志、统计聚合等分析型数据、`goose_clickhouse_version`、单方言 SQL。 +- 分析库(ClickHouse):分析型数据、`goose_clickhouse_version`、单方言 SQL。日志用途表还必须在主库建回落并走 `logstore`(见该 skill);CH 目录仍只放 CH DDL。 **不要**把 ClickHouse 表结构混入 PG/SQLite 迁移目录,也**不要**在 `support-files/`、`internal/apps/` 或 `internal/repository/` 中手写 DDL。 @@ -118,7 +118,7 @@ ClickHouse 是**辅助 OLAP 存储**,与 PostgreSQL/SQLite 主库**完全独 1. **Model**:在 `internal/model/analytics/` 定义 struct,`gorm:"column:..."` 与 DDL 列名一一对应;实现 `TableName()`,批量写入表可提供 `InsertColumns()` / `BatchInsertSQL()`。 2. **Goose SQL**:在 `internal/infra/persistence/migrator/goose/clickhouse/` 新增递增版本文件(格式同主库,如 `YYYYMMDDNNNN_create_xxx.sql`),编写 `-- +goose Up` / `-- +goose Down`。 3. **Repository**:在 `internal/repository/analytics/` 实现 `BatchInsert*`(`db.ChConn` 一次 `PrepareBatch` + 多行 `Append` + 一次 `Send`)与查询(`db.ChDB`);连接未初始化时返回明确错误,**不要**在 handler 写 SQL,**不要**在 repository 内维护 channel/goroutine。 -4. **Apps**:在 `internal/apps//` 编排采集与入队;高频写入通过 `internal/infra/persistence/batchwriter` 各域独立实例异步 flush(详见 `clickhouse-batchwriter` 技能),`FlushFunc` 只调 repository `BatchInsert*`;管理端统计 API 只读 repository,不触达 DDL。 +4. **Apps**:在 `internal/apps//` 编排采集与入队;高频写入通过 `internal/infra/persistence/batchwriter` 各域独立实例异步 flush(详见 `clickhouse-batchwriter` 技能)。**日志/分析用途表**还要同时建 PG/SQLite 回落并接入 `logstore`(见 `logstore` 技能),`FlushFunc` 调 `logstore.Active` 而不是 `analyticsrepo`;普通业务分析表仍只读 repository。 ### ClickHouse 验证 diff --git a/.agents/skills/logstore/SKILL.md b/.agents/skills/logstore/SKILL.md new file mode 100644 index 00000000..4839c06c --- /dev/null +++ b/.agents/skills/logstore/SKILL.md @@ -0,0 +1,75 @@ +--- +name: "logstore" +description: "OpenFlare / Wavelet:当新增或修改日志/分析用途表(节点访问日志、用户访问日志、可观测时序)、接入 internal/repository/logstore、切换日志主库、实现 PG/SQLite 回落,或判断一张表该走业务主库还是日志库时必须使用。" +--- + +# 日志用途表开发 + +开始前阅读根目录 `AGENTS.md`。DDL 用 `database-migration`;高频写入队列用 `clickhouse-batchwriter`;切换任务用 `new-async-task`。本技能只回答:**这张表是不是日志表,以及如何接入可切换的日志主库。** + +设计背景见 [日志存储解耦](../../../docs/design/logstore.md)。 + +## 先判定 + +日志表同时满足: + +- 追加写入、几乎不更新单行 +- 按时间查询/聚合,允许按保留天数删除 +- 关闭 ClickHouse 后仍要能写、能查 +- 不参与网站/节点/证书等事务一致性 + +**不要**做成日志表:Zone、节点、配置版本、任务执行、上传元数据。这些走主库 `repository`。 + +当前日志域: + +| 域 | 接口 | 表 | +| :--- | :--- | :--- | +| 节点访问日志 | `AccessLogStore` | `of_node_access_logs` | +| 可观测 | `ObservabilityStore` | `of_node_metric_snapshots` / `of_node_edge_health` / `of_node_obs_frps` / `of_node_obs_frpc` | +| 用户访问审计 | `UserAccessLogStore` | `w_user_access_logs` | + +## 分层 + +| 层级 | 路径 | 职责 | +| :--- | :--- | :--- | +| 抽象 | `internal/repository/logstore` | 接口 + `Active`/`BuildForMigration`;apps **只**面向这里或 `repository` 门面 | +| CH 实现 | `logstore/clickhouse_store.go` 委托 `analytics` | 原生批量 + 现有聚合 SQL | +| 主库实现 | `logstore/postgres_store.go` | PG(按月分区)与 SQLite(普通表)共用 GORM | +| Model | `internal/model/analytics` | 实体与批量 SQL,无 IO | +| 入队 | `chwriter` / `risk_control` + `batchwriter` | flush 调 logstore `BatchInsert*`;CH 入队经 hooks | +| 切换 | `of_log_db_switch` | 冻结 → `chwriter.Drain` → 逐表复制 → 翻转 | +| 约束 | `logstore/imports_test.go` | apps 禁止 import `repository/analytics` | + +`log_database` 只能是「随主库」或 `clickhouse`。`log_database` / `log_db_migration` 受保护。 + +## 新增一张日志表 + +1. **Model**(`internal/model/analytics`):`TableName` + `InsertColumns` / `BatchInsertSQL`。 +2. **三套 DDL**:CH `MergeTree` + `toYYYYMM`;PG `PARTITION BY RANGE(时间列)`(主键含分区键);SQLite 普通表。不要在主库建 CH 物化视图,聚合实时算。 +3. **挂到已有域或新接口**:能进 `AccessLogStore` / `ObservabilityStore` / `UserAccessLogStore` 就不要再拆包。新域才新增接口并放进 `Store`。 +4. **方法最少集**:`BatchInsert`(含 `ensureWritable`)、业务查询、`ListForMigration`、`MigrationRange`、`DeleteAll`、`DeleteBefore`、`EnsurePartitions`(仅 PG 预建)。 +5. **双实现**:CH 委托 `analyticsrepo`;GORM 共用一套,方言 SQL 放 `dialect_*.go`。零值 id 用 `idgen.NextUint64ID()`。 +6. **`buildStore`**:CH / GORM 两分支都挂上。 +7. **写入**:独立 `batchwriter`;`FlushFunc` → `logstore.Active`。节点日志/可观测走 `SetAccessLogHooks` / `SetObservabilityHooks`,不要让 apps 碰 `ChConn`。 +8. **切换任务**:`clearTarget` + `copy*` 增加该表;源数据不删,失败不翻转。 +9. **清理**:访问类走 `log_retention_days_*`;性能指标走 `metric_retention_days`。不要擅自共用错误的 TTL。 +10. **import-lint**:apps 新增对 `analytics` 或 `infra/persistence`(`batchwriter`/`idgen` 除外)的 import 必须失败。 + +## 禁止 + +- apps 直连 `analyticsrepo` / `db.ChConn` / `db.ChDB` 做日志读写 +- 只建 CH、不建主库回落 +- Handler 内逐条 `PrepareBatch` +- 业务表塞进 logstore +- 管理端改 `log_database` / `log_db_migration` + +## 验证 + +```bash +go test ./internal/repository/logstore ./internal/repository/analytics +go test ./internal/apps/openflare/... ./internal/apps/admin/logs ./internal/apps/admin/status +make swagger +make code-check +``` + +对照:`of_node_access_logs` 或 `w_user_access_logs` 的 model、三库 goose、`logstore` 双实现、`chwriter`/`risk_control` flush、`LogDBSwitchHandler`。 diff --git a/AGENTS.md b/AGENTS.md index 5f4aa62b..a47a1db1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,6 +72,7 @@ Strong success criteria let you loop independently. Weak criteria ("make it work | `new-async-task` | Asynq 任务、定时任务、TaskHandler、任务元数据 | | `new-setting` | 系统/业务/公开设置、`/admin/system`、`/admin/settings` | | `database-migration` | 表结构、goose 迁移(PG/SQLite/ClickHouse)、seed | +| `logstore` | 日志/分析用途表、`internal/repository/logstore`、切换日志主库、PG/SQLite 回落 | | `clickhouse-batchwriter` | CH 批量写入、batchwriter、分析表 flush/背压 | | `file-upload` | 上传/摄取、`upload.Ingest`、文件访问、`w_uploads` | | `cache-framework` | 业务缓存(RAM/Redis/DB)、失效、多节点同步 | @@ -91,6 +92,7 @@ Strong success criteria let you loop independently. Weak criteria ("make it work - **分层**:`apps → repository → model`,`repository → infra/persistence`;禁止 `model → repository`。 - `model`:实体、表名、配置 key、查询 DTO、无 IO 规则。禁止 `db.DB` / Redis / CH;禁止 `import repository`。GORM hook 仅可 mutate 自身字段,禁止在 hook 内再查 DB/缓存。 - `repository`:唯一持久化入口。apps/logics 禁止为业务 CRUD 直调 `db.DB`(管理端 SQL 控制台、infra 内部等例外保留)。禁止新增 `model.Get/List/Create/...` 类数据访问 API。 +- 日志/分析表(节点访问日志、用户访问日志、可观测时序)走 `internal/repository/logstore`,禁止 apps 直连 `repository/analytics` 或 `db.ChConn`/`db.ChDB`。判定与接入步骤见 `logstore` skill。 - 跨模块集成(任务 Handler、推送事件、域监听、完成钩子)禁止 `init()` 注册;经 `internal/platform/bootstrap` 在 `internal/cmd` 入口显式装配。 - 核心业务(如 `oauth`、`user`)禁止直接 import push/custom_events;经 `internal/listener` 发域事件,push 在 bootstrap 订阅。 - 依赖任务/推送注册的测试须显式 `bootstrap.RegisterTasks()` / `RegisterPushDomainEvents()` 等,不依赖 `init()`。 diff --git a/docs/config.ts b/docs/config.ts index eda32533..b7ab64ff 100644 --- a/docs/config.ts +++ b/docs/config.ts @@ -138,6 +138,7 @@ function sidebarDesign(): DefaultTheme.SidebarItem[] { { text: '边缘可观测与业务流量统计', link: 'observability-design' }, { text: '观测数据传输模型', link: 'observability-transport-model' }, { text: '观测上报协议与表结构', link: 'observability-data-model' }, + { text: '日志存储解耦', link: 'logstore' }, { text: 'Uptime Kuma 监控同步设计', link: 'kuma-design' }, { text: '登录验证码设计', link: 'login-captcha' } ] diff --git a/docs/deployment/deployment.md b/docs/deployment/deployment.md index ea995c7b..d6187a90 100644 --- a/docs/deployment/deployment.md +++ b/docs/deployment/deployment.md @@ -2,7 +2,7 @@ 你会学到:OpenFlare 的推荐部署方式、Server 与 Agent 的运行要求、源码启动方式、联调步骤、升级与卸载入口。 -生产环境建议使用 PostgreSQL 作为 Server 数据库,并通过 `config.yaml` 或环境变量配置 `APP_SESSION_SECRET` 等参数。完整 Docker Compose 部署还需 Redis 与 ClickHouse(见仓库根目录 `docker-compose.yaml`)。Agent 部署方式推荐为 Docker 部署(即直接使用内置 OpenResty 的 Agent 镜像);亦支持通过安装脚本或手动本地运行。 +生产环境建议使用 PostgreSQL 作为 Server 数据库,并通过 `config.yaml` 或环境变量配置 `APP_SESSION_SECRET` 等参数。完整 Docker Compose 部署需要 Redis;ClickHouse 可选,用于海量访问日志与观测时序(见仓库根目录 `docker-compose.yaml`)。Agent 部署方式推荐为 Docker 部署(即直接使用内置 OpenResty 的 Agent 镜像);亦支持通过安装脚本或手动本地运行。日志库判定与切换见 [日志存储解耦](../design/logstore.md)。 ## 部署拓扑 diff --git a/docs/design/architecture.md b/docs/design/architecture.md index 0ffc9021..d8cb502b 100644 --- a/docs/design/architecture.md +++ b/docs/design/architecture.md @@ -145,7 +145,7 @@ OpenResty access.log(业务事实) | | Agent tail 增量明细(不 sum/count/uniq) v -Server 入库 ClickHouse +Server 经 logstore 入库(当前日志主库:PostgreSQL / SQLite / ClickHouse) | +---> 全局聚合 --> 看板「已提供数据 / 请求 / UV」 +---> host∈Zone --> Zone「已提供数据」等(同一套语义) diff --git a/docs/design/index.md b/docs/design/index.md index a2df9e0d..767fe6fb 100644 --- a/docs/design/index.md +++ b/docs/design/index.md @@ -32,6 +32,7 @@ OpenFlare 适合需要统一管理多台 OpenResty 代理节点的团队,具 | **Pages 静态托管** | 支持上传或从 Remote URL、公开 GitHub Release 同步预构建产物;GitHub latest 可定时检查并可选自动发布。不可变部署由边缘节点拉取并由 OpenResty 本地服务,支持回滚、API 反代与 SPA Fallback | [Pages 静态托管设计](./pages-design.md) / [Pages 使用指南](../guide/pages-usage.md) | | **TLS 证书自动续期** | 将证书显式绑定到 Zone 域名,并通过 ACME 协议向 Let's Encrypt 申请/续期证书 | [Zone 与域名资源设计](./zone-design.md) | | **多节点监控与观测** | 访问日志为业务流量唯一真相;Agent 只上报明细与主机读数,Server 统一聚合;与 Zone/看板对账 | [观测数据传输模型](./observability-transport-model.md) / [边缘可观测与业务流量统计](./observability-design.md) / [上报协议与表结构](./observability-data-model.md) / [系统架构](./architecture.md) | +| **日志存储** | 访问日志与可观测时序走可切换日志主库(随业务主库或 ClickHouse);关闭 ClickHouse 后仍可写可查 | [日志存储解耦](./logstore.md) | --- @@ -62,7 +63,7 @@ OpenFlare 适合需要统一管理多台 OpenResty 代理节点的团队,具 ### 5. 系统与版本边界 * **全局单一激活版本**:所有节点拉取并消费同一份全局激活配置。不进行按节点分组的差异化配置发布。 * **单租户架构**:OpenFlare 仅供单团队在受信任的内部网络部署使用。采用单租户设计,不支持细粒度的多用户角色或多租户资源隔离。 -* **外部基础设施依赖性**:Server 虽支持 SQLite 作为本地轻量关系数据库,但**系统必须强制依赖外部 Redis(或 Valkey)及 ClickHouse 实例**。Redis 用于处理分布式协调、后台异步队列(Asynq 框架)及系统级全局缓存;ClickHouse 用于接收海量节点访问日志与基础观测的异步 Flush。系统不支持完全脱离这两个组件运行。 +* **外部基础设施依赖性**:Server **必须依赖**外部 Redis(或 Valkey),用于分布式协调、Asynq 队列与系统缓存。关系库为 PostgreSQL,或关闭 `database.enabled` 时使用 SQLite。ClickHouse **可选**:不启用时,访问日志与可观测时序由当前日志主库(随业务主库)承接;启用后可通过「切换日志数据库」任务迁到 ClickHouse。系统不支持脱离 Redis 运行。详情见 [日志存储解耦](./logstore.md)。 --- @@ -98,7 +99,7 @@ OpenFlare 已收敛为**单 monorepo**(Go 模块 `github.com/Rain-kl/Wavelet` | `internal/apps/openflare/{agent,relay,flared}/` | **Server 侧**边缘协议处理器(鉴权、心跳、WS) | | `internal/model/` | GORM 实体 / DTO / 无 IO 领域规则(`openflare_*.go` + 平台模型);**不含** DB 访问 | | `internal/infra/persistence/migrator/goose/` | goose SQL 迁移(PostgreSQL / SQLite / ClickHouse) | -| `internal/repository/` | 数据访问层(平台 + OpenFlare 业务 CRUD、缓存、ClickHouse 分析读写);**唯一**持久化入口 | +| `internal/repository/` | 数据访问层(平台 + OpenFlare 业务 CRUD、缓存、`logstore` 日志读写);**唯一**持久化入口 | | `internal/infra/task/` | Asynq 异步任务(Worker + Scheduler) | | `internal/infra/config/` | Viper 配置加载 | | `internal/shared/` | 统一 API 响应封装(`response/`) | @@ -191,6 +192,7 @@ OpenFlare 已收敛为**单 monorepo**(Go 模块 `github.com/Rain-kl/Wavelet` ## 文档维护原则 * 产品范围或系统边界变化:更新本文档([产品边界](./index.md))。 +* 日志存储、日志表判定或切换协议变化:更新 [日志存储解耦](./logstore.md)。 * 系统结构、组件分工变化:更新 [系统架构](./architecture.md)。 * 发布、同步、回滚与 Agent 模型变化:更新 [Agent 与发布模型](./agent-design.md)。 * 部署方式变化:更新 [部署说明](../deployment/deployment.md) 与 README。 diff --git a/docs/design/logstore.md b/docs/design/logstore.md new file mode 100644 index 00000000..74fea36e --- /dev/null +++ b/docs/design/logstore.md @@ -0,0 +1,89 @@ +# 日志存储解耦 + +你会学到:哪些表属于日志用途、为什么不能绑死 ClickHouse,以及新增一张日志表时必须走哪条代码路径。逐步落地步骤见 `.agents/skills/logstore/SKILL.md`。 + +观测字段与上报协议仍以 [观测上报协议与表结构](./observability-data-model.md) 为准;本文只约定**存到哪、怎么切库**。 + +--- + +## 1. 目标 + +* **ClickHouse 可选**:不启用时,PostgreSQL(或关闭主库时的 SQLite)完整承接写入、查询、聚合与清理。 +* **上层不碰底层库**:apps 只面向 `internal/repository/logstore`(或 `repository` 门面)。`repository/analytics` 与 `db.ChConn` / `db.ChDB` 仅供 logstore 的 ClickHouse 实现使用。 +* **可切换**:任务管理里的「切换日志数据库」在 PostgreSQL/SQLite 与 ClickHouse 之间复制数据并翻转主库;迁移期间冻结写入,成功才切换,源数据不删。 + +--- + +## 2. 什么算日志表 + +同时满足才进 logstore: + +* 追加写入,几乎不更新单行 +* 按时间查询或聚合,允许按保留天数删除 +* 关闭 ClickHouse 后仍要能写、能查 +* 不参与网站 / 节点 / 证书等事务一致性 + +**不要**做成日志表:Zone、节点、配置版本、任务执行、上传元数据。这些走业务主库 `repository`。 + +当前日志域: + +| 域 | 接口 | 表 | +| --- | --- | --- | +| 节点访问日志 | `AccessLogStore` | `of_node_access_logs` | +| 可观测时序 | `ObservabilityStore` | `of_node_metric_snapshots` / `of_node_edge_health` / `of_node_obs_frps` / `of_node_obs_frpc` | +| 用户访问审计 | `UserAccessLogStore` | `w_user_access_logs` | + +ClickHouse 上的小时级物化视图(如 `of_access_log_hourly`)只服务 CH 查询加速。PostgreSQL / SQLite **不建**同构聚合表,查询时从原始日志实时聚合。 + +--- + +## 3. 分层 + +| 层级 | 路径 | 职责 | +| --- | --- | --- | +| 抽象 | `internal/repository/logstore` | 接口 + `Active` / `BuildForMigration`;按 `log_database` 选实现 | +| CH 实现 | `logstore/clickhouse_store.go` | 委托 `repository/analytics`(原生批量 + 现有聚合 SQL) | +| 主库实现 | `logstore/postgres_store.go` | PostgreSQL(高频表按月分区)与 SQLite(普通表)共用 GORM | +| Model | `internal/model/analytics` | 实体与批量 SQL,无 IO | +| 入队 | `chwriter` / `risk_control` + `batchwriter` | `FlushFunc` 调 `logstore.Active`;节点日志 / 可观测经 hooks 入队 | +| 约束 | `logstore/imports_test.go` | apps 禁止 import `repository/analytics` | + +`log_database` 只有两种合法状态:**随业务主库**(`postgres` 或 `sqlite`)或 **`clickhouse`**。不存在「主库 PostgreSQL + 日志 SQLite」。`log_database` / `log_db_migration` 受保护,管理端不可改。 + +启动时:`log_database=clickhouse` 但 ClickHouse 未启用会拒绝启动,须先重新启用 ClickHouse 并切回主库后再关掉。 + +--- + +## 4. 切换协议 + +任务类型 `of_log_db_switch`(管理端名称「切换日志数据库」),参数 `target`。 + +1. 校验目标合法且不等于当前库。 +2. 写 `log_db_migration=migrating`,排空在途 batchwriter(`Drain`,不要 `Stop` writer)。此后写入返回明确错误(HTTP 503),不排队积压。 +3. 清空目标日志表后按 id 分页复制;复制前对 PostgreSQL 目标 `EnsurePartitions`。 +4. 全部成功才写 `log_database=target` 并清除迁移标记;失败清除标记,写入继续走源库。 +5. 源数据不删;重试前重新清空目标以保证幂等。 + +不要另起切换协议,也不要在任务里直连 `analyticsrepo`。 + +--- + +## 5. 新增一张日志表 + +列名必须在 ClickHouse / PostgreSQL / SQLite 三套 goose 中一致。顺序与禁止项见 `logstore` skill。要点: + +* 高频表:CH 用 `MergeTree` + `toYYYYMM`;PG 用 `PARTITION BY RANGE(时间列)`,主键含分区键;SQLite 普通表 + 索引。 +* ID 用 snowflake `uint64`,迁移时原样保留。 +* 写入走独立 `batchwriter`;flush 调 `logstore.Active`,不要 `analyticsrepo.BatchInsert`。 +* 切换任务的 `copy*` 必须覆盖新表;清理走已有 `log_retention_days_*` 或 `metric_retention_days`,不要用错 TTL。 + +运行时配置见 [配置项参考 · 日志存储](../reference/configuration.md#8-日志存储log-database)。 + +--- + +## 6. 相关文档 + +* 开发步骤:`.agents/skills/logstore/SKILL.md` +* DDL:`.agents/skills/database-migration/SKILL.md` +* 批量写入:`.agents/skills/clickhouse-batchwriter/SKILL.md` +* 实现前设计稿(历史):[日志数据库解耦设计](../superpowers/specs/2026-08-08-log-database-decoupling-design.md)