# 日志基础设施整理方案 ## 目标 当前服务已经有基础日志能力:HTTP/gRPC 边界日志在 `pkg/logx`,服务启动和 worker 使用标准库 `log` 输出。但生产环境需要把日志整理成稳定、可检索、可告警、可控成本的基础设施。 目标: - 所有服务统一输出 JSON 结构化日志到 stdout/stderr。 - 业务代码不直接依赖腾讯云 CLS SDK。 - 部署层采集容器 stdout/stderr 到腾讯云 CLS。 - 日志字段统一,支持按 `request_id`、`command_id`、`user_id`、`room_id`、`transaction_id` 追踪。 - 敏感字段默认脱敏,避免 token、密码、支付凭证、UserSig、SecretKey 进入日志。 - 保留后台审计日志和业务账务流水,不用运行日志替代审计和账务事实。 非目标: - 首版不做完整链路追踪系统。 - 首版不引入重型日志框架。 - 首版不在代码里直连 CLS API 写日志。 - 首版不采集客户端 SDK 日志;客户端日志和崩溃分析后续单独设计。 ## 当前现状 已有能力: - `gateway-service` HTTP 入口接入 `logx.HTTPMiddleware`。 - `room-service`、`wallet-service`、`user-service`、`activity-service` gRPC server 接入 `logx.UnaryServerInterceptor`。 - `logx` 会记录请求、响应、状态码、错误 reason、耗时和 `request_id`。 - `logx` 已经对 `password`、`token`、`secret`、`credential`、`user_sig`、`authorization` 等字段做脱敏。 - 健康检查不会进入高频 access log。 主要问题: - App 后端服务已接入 `logx.Init`,默认输出 stdout JSON;`log.Printf` 会被兜底包成 `stdlog` JSON。 - 核心 worker、RTC callback、room outbox 已改为结构化字段日志。 - admin-server 是独立 Go module,按项目边界在 `server/admin/internal/platform/logging` 接入同一 stdout JSON 口径,不 import `hyapp/pkg/logx`。 - access log 默认不记录 request/response body;本地可通过配置临时打开,生产需要继续按接口和环境控制体积。 - 没有部署层日志采集配置,也没有腾讯云 CLS logset/topic 规划。 ## 总体架构 ```mermaid flowchart LR S1["gateway-service"] --> STDOUT["stdout/stderr JSON logs"] S2["room-service"] --> STDOUT S3["wallet-service"] --> STDOUT S4["user-service"] --> STDOUT S5["activity-service"] --> STDOUT S6["hyapp-admin-server"] --> STDOUT STDOUT --> Runtime["Container Runtime"] Runtime --> Collector["TKE Log Collector / CLS LogListener"] Collector --> CLS["Tencent Cloud CLS"] CLS --> Search["检索与排障"] CLS --> Dashboard["仪表盘"] CLS --> Alarm["告警策略"] ``` 核心决策: - 服务只负责输出标准 JSON 日志。 - 日志采集由部署层负责。 - 本地开发继续用 stdout,`docker compose logs` 可查看。 - 生产环境通过腾讯云容器日志采集或 CLS LogListener 采集 stdout。 - 后台审计继续写 `admin_operation_logs`,钱包账务继续写 `wallet_transactions`、`wallet_entries`,不能用运行日志替代。 ## 日志等级 统一使用: | Level | 使用场景 | | --- | --- | | `debug` | 本地调试,生产默认关闭 | | `info` | 正常业务事件、access log、worker 成功摘要 | | `warn` | 可恢复异常、降级、重试、外部依赖短暂失败 | | `error` | 请求失败、事务失败、外部依赖不可用、worker 批次失败 | 配置项: ```yaml log: level: info format: json include_request_body: false include_response_body: false max_payload_bytes: 2048 ``` 本地可以打开 body: ```yaml log: level: debug format: text include_request_body: true include_response_body: true max_payload_bytes: 8192 ``` 生产默认不记录完整 request/response body,只记录关键字段和错误摘要。 ## 统一字段规范 每条日志都应该尽量包含基础字段: | 字段 | 说明 | | --- | --- | | `time` | 日志时间 | | `level` | `debug/info/warn/error` | | `msg` | 短事件名,例如 `http_access`、`grpc_access`、`wallet_transaction_failed` | | `env` | `local/dev/staging/prod` | | `app_code` | 多 App 租户标识 | | `service` | 服务名 | | `version` | 镜像版本或 git sha | | `node_id` | 当前服务节点 | | `request_id` | 请求追踪 ID | 业务字段按场景补充: | 场景 | 推荐字段 | | --- | --- | | HTTP | `method`、`path`、`status`、`duration_ms`、`client_ip` | | gRPC | `grpc_method`、`grpc_code`、`reason`、`duration_ms` | | 房间 | `room_id`、`room_version`、`command_id`、`user_id` | | 钱包 | `transaction_id`、`command_id`、`user_id`、`asset_type`、`biz_type` | | 礼物 | `room_id`、`sender_user_id`、`target_user_id`、`gift_id`、`gift_count` | | 币商 | `seller_user_id`、`target_user_id`、`amount`、`transaction_id` | | worker | `worker`、`worker_id`、`batch_size`、`retry_count` | | 外部依赖 | `provider`、`endpoint`、`external_code`、`duration_ms` | 字段命名固定使用 snake_case,和 protobuf/mysql 字段风格一致,方便 CLS 查询。 ## `pkg/logx` 改造 `pkg/logx` 应该从“access log middleware”升级成“统一日志入口”。 建议新增: ```go type Config struct { Service string Env string Version string NodeID string Level string Format string IncludeRequestBody bool IncludeResponseBody bool MaxPayloadBytes int } func Init(cfg Config) error func Logger(ctx context.Context) *slog.Logger func With(ctx context.Context, attrs ...slog.Attr) context.Context func Info(ctx context.Context, msg string, attrs ...slog.Attr) func Warn(ctx context.Context, msg string, attrs ...slog.Attr) func Error(ctx context.Context, msg string, err error, attrs ...slog.Attr) ``` 改造原则: - 服务启动时调用 `logx.Init` 设置全局 logger。 - `log.Printf` 逐步替换成 `logx.Info/Warn/Error` 或 `slog` 结构化字段。 - HTTP/gRPC middleware 自动把 `request_id`、`service`、`app_code` 写入日志上下文。 - `max_payload_bytes` 从配置读取,不写死。 - 生产默认不记录完整 body;只有错误或 debug 环境才允许记录脱敏 body。 ## Access Log 策略 HTTP access log: - 成功请求记录一条 `info`。 - 4xx 记录 `warn` 或 `info`,取决于业务错误类型。 - 5xx 记录 `error`。 - healthcheck 不记录。 - request/response body 默认关闭。 gRPC access log: - `codes.OK` 记录 `info`。 - 业务前置失败,例如 `InvalidArgument`、`FailedPrecondition`,记录 `warn` 或 `info`。 - `Internal`、`Unavailable`、`DeadlineExceeded` 记录 `error`。 - `grpc.health.v1.Health` 不记录。 建议生产日志示例: ```json { "time": "2026-05-09T12:00:00.000Z", "level": "INFO", "msg": "grpc_access", "env": "prod", "service": "wallet-service", "request_id": "req-123", "app_code": "lalu", "grpc_method": "/hyapp.wallet.v1.WalletService/TransferCoinFromSeller", "grpc_code": "OK", "reason": "", "duration_ms": 18, "command_id": "cmd-456", "seller_user_id": 1001, "target_user_id": 2002, "transaction_id": "wtx-789" } ``` ## 敏感字段规则 禁止进入日志: - 明文密码 - Firebase ID token - Google/Apple provider credential - refresh token / access token - 腾讯云 SecretId / SecretKey - Tencent IM UserSig - RTC token - 支付 provider receipt 原文 - USDT 完整链上凭证如包含敏感内部备注时,最多记录摘要 - 身份证件、银行卡、提现收款账号全文 可以记录: - `request_id` - `command_id` - `transaction_id` - `room_id` - `user_id` - `provider` - provider 错误码 - token/receipt 的 hash 摘要,前提是确实需要排障 `logx` 的脱敏 key 列表需要继续集中维护,禁止业务代码自己拼接敏感原文。 ## 腾讯云 CLS 接入方案 推荐方案:stdout 采集到 CLS。 ### 方案 A:TKE 容器标准输出采集 如果服务部署在腾讯云 TKE: 1. 每个服务输出 JSON 到 stdout/stderr。 2. TKE 开启日志采集。 3. 采集目标选择容器标准输出。 4. 解析方式选择 JSON。 5. 不同环境写入不同 CLS topic。 推荐 topic: | 环境 | Logset | Topic | | --- | --- | --- | | dev | `hyapp-dev` | `service-logs` | | staging | `hyapp-staging` | `service-logs` | | prod | `hyapp-prod` | `service-logs` | | prod | `hyapp-prod` | `admin-audit-runtime` | | prod | `hyapp-prod` | `wallet-security` | 首版可以所有服务进同一个 `service-logs` topic,通过 `service` 字段过滤。等日志量变大,再把 `gateway-service`、`wallet-service`、`room-service` 拆 topic。 ### 方案 B:CVM / Docker 部署使用 CLS LogListener 如果服务部署在 CVM Docker: 1. 服务仍输出 JSON 到 stdout。 2. Docker 日志落到宿主机默认 json log 文件。 3. 安装 CLS LogListener。 4. LogListener 采集容器日志文件或指定目录。 5. CLS 端按 JSON 解析。 不建议在每个 Go 服务里集成 CLS SDK。直连 SDK 会把业务进程和日志平台绑定,CLS 抖动时还可能影响业务路径。 ## CLS 索引字段 CLS topic 需要开启键值索引。 建议索引字段: - `level` - `env` - `service` - `app_code` - `request_id` - `command_id` - `user_id` - `room_id` - `transaction_id` - `biz_type` - `grpc_code` - `reason` - `status` - `path` - `worker` - `event_type` 全文索引可以保留,但排障优先用字段检索,避免只靠模糊搜索。 ## 告警策略 首版告警不要太多,先覆盖最关键的错误和积压。 建议: | 告警 | 条件 | | --- | --- | | 服务 5xx 增多 | `service=gateway-service AND status>=500` 5 分钟超过阈值 | | gRPC Internal | `grpc_code=Internal` 5 分钟超过阈值 | | 钱包失败 | `service=wallet-service AND level=ERROR` 1 分钟超过阈值 | | room outbox 积压 | worker 日志出现 `status=batch_failed` 或 retryable 数量持续增长 | | 腾讯云 IM/RTC 回调异常 | `provider=tencent AND level>=WARN` 超过阈值 | | 用户登录失败突增 | `msg=auth_failed` 或 `reason=AUTH_FAILED` 超过阈值 | 告警通知: - 开发阶段:企业微信/飞书群。 - 生产阶段:按服务 owner 分组。 - 钱包和支付相关 error 单独告警,不能淹没在普通业务 error 里。 ## 保留周期和成本控制 建议: | 环境 | 保留时间 | | --- | --- | | local | 不上传 CLS | | dev | 3-7 天 | | staging | 7-14 天 | | prod 普通服务日志 | 30 天 | | prod 钱包/支付安全日志 | 90-180 天,按合规要求调整 | 成本控制: - 生产关闭 request/response body。 - 高频成功日志只保留摘要。 - healthcheck 不采集或不输出。 - 大 payload 截断。 - 对重复 worker 成功日志做批次汇总,不逐条刷屏。 - 对外部 callback 原始 body 只在 debug 或白名单场景短期开启。 ## 和审计日志的关系 运行日志和审计日志是两套东西。 运行日志: - 用于排障、告警、性能分析。 - 存在 CLS。 - 可以按保留周期清理。 审计日志: - 用于追责、后台操作记录、财务追踪。 - 存在业务数据库,例如 `admin_operation_logs`。 - 必须有操作者、动作、资源、结果、IP、原因。 - 不能因为 CLS 采集失败而丢失。 钱包账务流水: - 存在 `wallet_transactions`、`wallet_entries`、`wallet_recharge_records` 等表。 - 是账务事实来源。 - 运行日志只辅助定位,不能替代账务表。 ## 阶段划分 第一阶段是服务内日志标准化,目标是让所有进程稳定输出结构化 JSON 到 stdout/stderr。当前第一阶段已经完成到可进入联调的状态: - App 后端服务接入 `logx.Init`。 - `admin-server` 因为是独立 Go module,使用 `server/admin/internal/platform/logging` 接入。 - HTTP/gRPC access log 支持统一字段、脱敏、body 开关、payload 截断和错误等级。 - `log.Printf` 类 worker 日志已改成结构化日志或被 stdout JSON 兜底。 - `logx` 已补充 gRPC 常用追踪字段提取;即使生产关闭 request body,也能在 access log 中检索 `command_id`、`user_id`、`room_id`、`transaction_id` 等字段。 - 启动完成后的进程级失败会按 `ERROR` 结构化输出,不再落成普通 INFO `stdlog`。 第二阶段是腾讯云 CLS 部署接入,目标是把第一阶段的 stdout JSON 真正采集、索引、查询和告警: 1. 在腾讯云 CLS 创建 logset/topic,按环境隔离 `dev`、`staging`、`prod`。 2. 配置 TKE 容器标准输出采集,或在 CVM/Docker 部署 CLS LogListener。 3. CLS topic 使用 JSON 解析,开启字段索引。 4. 建立基础查询模板:按 `request_id` 查链路,按 `service` 看错误,按 `transaction_id` 查钱包,按 `room_id` 查房间。 5. 建立第一批 dashboard:服务错误、接口耗时、gRPC 错误、wallet 失败、room outbox 重试、登录失败。 6. 建立第一批告警:gateway 5xx、gRPC Internal/Unavailable、wallet ERROR、room outbox 重试异常、腾讯云回调异常。 7. 验证保留周期和采集成本,生产默认不上传 request/response body。 第三阶段再做观测增强,包括 OpenTelemetry trace、Prometheus 指标、慢请求日志、安全日志 topic、客户端日志关联。 ## 实施顺序 1. 已增加统一日志配置结构:`log.level`、`log.format`、`log.include_request_body`、`log.include_response_body`、`log.max_payload_bytes`。 2. 已扩展 `pkg/logx.Init`,服务启动时统一设置 JSON/text handler、level、基础字段。 3. 已给 gateway、room、user、wallet、activity 接入 `logx.Init`;admin-server 通过独立 module 内的 `platform/logging` 接入。 4. 已改造 `logx.HTTPMiddleware` 和 `logx.UnaryServerInterceptor`,按配置控制 body、截断大小和错误等级。 5. 已替换核心路径的 `log.Printf`: - room outbox worker - user region rebuild worker - user mic time worker - invite recharge worker - Tencent RTC callback 6. 本地验证 JSON 日志格式:`docker compose logs gateway-service` 能看到结构化 JSON。 7. 生产部署层接入腾讯云 CLS stdout 采集。 8. 在 CLS 配置 JSON 解析和字段索引。 9. 建立基础 dashboard:错误率、接口耗时、钱包失败、room outbox 重试、登录失败。 10. 建立首批告警策略。 ## 测试和验收 代码侧: - `logx` 单测覆盖脱敏字段。 - `logx` 单测覆盖 payload 截断。 - HTTP middleware 单测覆盖 success、4xx、5xx 日志级别。 - gRPC interceptor 单测覆盖 OK、业务错误、Internal。 - 配置解析单测覆盖默认值和非法 log level。 本地验收: ```bash docker compose up -d docker compose logs gateway-service docker compose logs wallet-service ``` 验收标准: - 日志是 JSON 或本地配置指定的 text。 - 每条 access log 有 `service`、`request_id`、`duration_ms`。 - 敏感字段显示为 `***REDACTED***`。 - healthcheck 不刷屏。 - 生产配置下 request/response body 不输出。 CLS 验收: - 能按 `request_id` 查到 gateway 和下游服务日志。 - 能按 `service=wallet-service AND transaction_id=...` 查到钱包交易日志。 - 能按 `room_id` 查房间命令相关日志。 - 能按 `level=ERROR` 聚合最近 5 分钟错误。 - 告警能正确触发并发送到通知渠道。 ## 后续演进 日志整理完成后,再考虑: - OpenTelemetry trace,把 `trace_id/span_id` 加入日志字段。 - Prometheus 指标和日志告警分工,错误率和延迟优先走 metrics。 - 慢请求日志,按 `duration_ms` 阈值记录额外字段。 - 安全日志 topic,把支付、提现、币商、管理员高危操作单独留存。 - 客户端日志和崩溃分析接入,和服务端 `request_id` 或 `session_id` 关联。