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

8.9 KiB
Raw Blame History

name, overview, todos
name overview todos
OpenTelemetry三信号完整接入 实现 OpenTelemetry Traces/Metrics/Logs 三大信号的完整初始化与 OTLP gRPC 导出,补齐当前 main.go 引用的未实现桩(NewOTelProvider/OTelShutdown),保持本地日志不变的同时新增 OTel 导出,并加优雅关闭以刷新数据。
id content status
deps-and-globals 补齐 otel sdk/exporter/otelslog/runtime 依赖(go mod tidy),在 global/otel.go 新增 OTelShutdown 与 Meter 变量,并对齐 config.yaml 与 config.example.yaml 的 SamplerRatio 键名 completed
id content status dependencies
provider 实现 pkg/tracing/provider.go 的 NewOTelProvider:构建 Resource 与 Tracer/Meter/Logger 三 Provider、OTLP gRPC 导出、runtime 指标、采样与 Enabled/Insecure 开关、no-op 降级、聚合 Shutdown completed
deps-and-globals
id content status dependencies
log-fanout 实现 pkg/tracing/fanout.go 扇出 slog.Handler,将日志同时输出到本地 JSON(保留 OtelContextHandler)与 otelslog bridge completed
provider
id content status dependencies
wire-main 修改 main.go:修正 setupOTel 符号与错误降级、调整 init 顺序使 logger 桥接 LoggerProvider、setupSlogLogger 接入扇出 handler、main() 增加 SIGINT/SIGTERM 监听与 server.Shutdown + OTelShutdown 优雅关闭 completed
provider
log-fanout
id content status dependencies
verify 执行 go mod tidy 与 go build/go vet 全量验证,smoke 验证 Enabled=false 及采集器不可用时不 panic 并正常启动,清理临时验证文件 completed
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),不重复造轮子。

架构与数据流

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)