---
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
global.Tracer.Start 子 span]
C --> D[slog 扇出 Handler]
D --> E[本地 JSON 文件+stdout
OtelContextHandler 注入 trace_id]
D --> F[otelslog bridge]
C --> G[TracerProvider
BatchSpanProcessor]
C --> H[MeterProvider
PeriodicReader + runtime 指标]
F --> I[LoggerProvider
BatchProcessor]
G --> J[(OTLP gRPC Collector
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)
```