448 lines
16 KiB
Markdown
448 lines
16 KiB
Markdown
# 日志基础设施整理方案
|
||
|
||
## 目标
|
||
|
||
当前服务已经有基础日志能力: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` 关联。
|