Files
zt-closet-backend/.codebuddy/plans/OpenTelemetry三信号完整接入_fe5b200f.md
2026-09-11 01:41:52 +08:00

120 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: OpenTelemetry三信号完整接入
overview: 实现 OpenTelemetry Traces/Metrics/Logs 三大信号的完整初始化与 OTLP gRPC 导出,补齐当前 main.go 引用的未实现桩(NewOTelProvider/OTelShutdown),保持本地日志不变的同时新增 OTel 导出,并加优雅关闭以刷新数据。
todos:
- id: deps-and-globals
content: 补齐 otel sdk/exporter/otelslog/runtime 依赖(go mod tidy),在 global/otel.go 新增 OTelShutdown 与 Meter 变量,并对齐 config.yaml 与 config.example.yaml 的 SamplerRatio 键名
status: completed
- id: provider
content: 实现 pkg/tracing/provider.go 的 NewOTelProvider:构建 Resource 与 Tracer/Meter/Logger 三 Provider、OTLP gRPC 导出、runtime 指标、采样与 Enabled/Insecure 开关、no-op 降级、聚合 Shutdown
status: completed
dependencies:
- deps-and-globals
- id: log-fanout
content: 实现 pkg/tracing/fanout.go 扇出 slog.Handler,将日志同时输出到本地 JSON(保留 OtelContextHandler)与 otelslog bridge
status: completed
dependencies:
- provider
- id: wire-main
content: 修改 main.go:修正 setupOTel 符号与错误降级、调整 init 顺序使 logger 桥接 LoggerProvider、setupSlogLogger 接入扇出 handler、main() 增加 SIGINT/SIGTERM 监听与 server.Shutdown + OTelShutdown 优雅关闭
status: completed
dependencies:
- provider
- log-fanout
- id: verify
content: 执行 go mod tidy 与 go build/go vet 全量验证,smoke 验证 Enabled=false 及采集器不可用时不 panic 并正常启动,清理临时验证文件
status: completed
dependencies:
- wire-main
---
## 产品概述
为「衣柜/穿搭」后端补齐 OpenTelemetry 三大信号(Traces / Metrics / Logs)的完整初始化与 OTLP gRPC 导出,把当前"只埋点、未导出"的半接入状态升级为可真正观测的完整链路,并修复因待实现桩导致的编译失败问题。
## 核心功能
- **Traces(链路追踪)**:注册真实 TracerProvider(Resource + OTLP gRPC 批量导出 + 采样率),使全项目已有的 span 与 gin 请求 span 真正导出。
- **Metrics(指标)**:注册 MeterProvider(OTLP gRPC 周期导出),采集 Go runtime 指标,并预留自定义指标入口。
- **Logs(日志)**:新增 OTel 日志导出(otelslog 桥接),同时保留现有本地 JSON 日志(文件+stdout,含 trace_id/span_id 注入)。
- **优雅关闭**:进程收到退出信号时先关闭 HTTP 服务,再刷新并关闭三个 Provider,避免丢失缓冲数据。
- **开关与降级**:尊重配置 `Enabled` 开关;未启用或采集器不可用时降级为 no-op,服务照常启动、业务不受影响、不 panic。
## 技术栈
- 语言/框架:Go 1.27 + Gin(沿用现有项目),OpenTelemetry-Go SDK。
- 基准版本对齐现有 `go.opentelemetry.io/otel v1.46.0`;SDK trace/metric 用对应 v1.46.x,log 相关为配套 v0.x,由 `go mod tidy` 解析。
- 传输:OTLP over gRPC,导出到配置的 `ExporterEndpoint`(如 `127.0.0.1:4317`)。
## 实现思路
在 `pkg/tracing` 新增统一初始化入口 `NewOTelProvider`,一次性构建 Resource 与三个 SDK Provider(Tracer/Meter/Logger),设置到 otel 全局,并返回聚合的 shutdown 函数;`main.go` 修正现有待实现桩(`setupOTel` 引用的未定义符号)、修正 init 顺序与错误降级、补充信号监听与优雅关闭。业务层 100+ 处 `global.Tracer.Start(...)` 与 `otelgin.Middleware(...)` 无需改动,Provider 注册后立即生效。
### 关键技术决策
- **统一入口 + 聚合 Shutdown**:单一 `NewOTelProvider` 返回 `(trace.Tracer, func(ctx) error, error)`,内部聚合三个 Provider 的 Shutdown,`main` 只持有一个 `global.OTelShutdown`,降低接线复杂度与遗漏 flush 风险。
- **日志扇出(fanout)而非替换**:保留现有本地 JSON handler(含 `OtelContextHandler` 的 trace 注入),叠加 `otelslog` bridge handler,用一个极简自研 `slog.Handler` 扇出到两者,避免引入第三方 slog-multi 依赖,且不破坏现有日志落盘。
- **降级优先,保障可用性**:`Enabled=false` 或 exporter 建立失败时,`global.Tracer` 使用 no-op tracer(业务层 `Tracer.Start` 安全非 nil),`OTelShutdown` 为空实现;`Insecure=true` 用非 TLS gRPC(内网采集器语义,不额外放开外网)。
- **批处理与性能**:Trace 用 BatchSpanProcessor、Metric 用 PeriodicReader、Log 用 BatchProcessor,均为异步批量导出,热路径开销可控;采样由 `SamplerRatio` 控制(`ParentBased(TraceIDRatioBased)`),对 <=0 做默认兜底 1.0。
## 实现要点(防回归)
- **编译阻断修复**:`main.go` 现引用未定义符号 `tracing.NewOTelProvider`、`global.OTelShutdown`、`global.OTelSetting`(真实名为 `global.OtelSetting`)。需实现前两者、并把 setupOTel 内改回 `global.OtelSetting`,使项目恢复可编译。
- **配置键名对齐 bug**:`config.example.yaml` 使用 `SampleRatio`,而 `setting.OtelSettingS` 字段为 `SamplerRatio`(拼写不一致 → viper 读不到 → 采样率 0 → 不采样)。统一为 `SamplerRatio`(改 yaml 键),并在代码对 ratio<=0 兜底 1.0。同时 `configs/config.yaml`(实际运行配置)若存在同样键名需一并对齐。
- **init 顺序**:需保证 setupOTel 在 setupSlogLogger 之前完成 LoggerProvider 构建,slog 扇出才能拿到 bridge;或 setupSlogLogger 在 provider 就绪后重建默认 logger。
- **错误处理**:setupOTel 失败时告警不 panic(保持服务可启动),并降级 no-op。
- **优雅关闭**:`main()` 当前直接 `ListenAndServe`,需改为监听 SIGINT/SIGTERM,先 `http.Server.Shutdown(ctx)` 再 `global.OTelShutdown(ctx)`,设置合理超时(如 5s)避免卡死。
- 复用现有 `internal/middleware/otel.go`(otelgin)与 `pkg/tracing/otel.go`(OtelContextHandler),不重复造轮子。
## 架构与数据流
```mermaid
flowchart TD
A[HTTP 请求] --> B[otelgin 中间件 server span]
B --> C[handler/service/dao<br/>global.Tracer.Start 子 span]
C --> D[slog 扇出 Handler]
D --> E[本地 JSON 文件+stdout<br/>OtelContextHandler 注入 trace_id]
D --> F[otelslog bridge]
C --> G[TracerProvider<br/>BatchSpanProcessor]
C --> H[MeterProvider<br/>PeriodicReader + runtime 指标]
F --> I[LoggerProvider<br/>BatchProcessor]
G --> J[(OTLP gRPC Collector<br/>ExporterEndpoint)]
H --> J
I --> J
```
## 目录结构
```
zt-closet-backend/
├── go.mod / go.sum # [MODIFY] go mod tidy 补 otel sdk / sdk-metric / sdk-log / resource+semconv /
│ # exporters/otlp/{otlptracegrpc, otlpmetricgrpc, otlploggrpc} /
│ # contrib/instrumentation/runtime / contrib/bridges/otelslog / log/global
├── global/
│ └── otel.go # [MODIFY] 保留 Tracer;新增 OTelShutdown func(context.Context) error、
│ # Meter metric.Meter(可选自定义指标入口)、LoggerProvider(可选)
├── pkg/
│ ├── setting/section.go # [MODIFY] 若选择改字段则同步;默认优先改 yaml 对齐字段(本方案改 yaml,section 不动)
│ └── tracing/
│ ├── otel.go # [KEEP] OtelContextHandler 保留
│ ├── provider.go # [NEW] NewOTelProvider:构建 Resource + Tracer/Meter/Logger Provider +
│ │ # OTLP gRPC 导出 + runtime 指标 + 采样 + Enabled/Insecure 开关 +
│ │ # no-op 降级 + 聚合 shutdown
│ └── fanout.go # [NEW] fanoutHandler:极简 slog.Handler,将日志同时发给本地 handler 与 otelslog bridge
├── internal/middleware/
│ └── otel.go # [KEEP] otelgin 中间件,无需改
├── configs/
│ ├── config.example.yaml # [MODIFY] OTel 段键名对齐 SamplerRatio,校正注释
│ └── config.yaml # [MODIFY] 若存在同名键,一并对齐 SamplerRatio(运行时实际读取)
└── main.go # [MODIFY] 修正 setupOTel 符号与错误降级;调整 init 顺序;
# setupSlogLogger 使用扇出 handler;main() 加信号监听 + 优雅关闭
```
## 关键代码结构(接口约定)
```
// pkg/tracing/provider.go —— 统一初始化入口
// 返回:全局 Tracer、聚合 Shutdown(flush 三个 Provider)、错误
// Enabled=false 或初始化失败时返回 no-op Tracer + 安全空 Shutdown
func NewOTelProvider(cfg *setting.OtelSettingS) (trace.Tracer, func(context.Context) error, error)
```