package tracing import ( "context" "errors" "fmt" "git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/pkg/setting" "go.opentelemetry.io/contrib/instrumentation/runtime" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" logglobal "go.opentelemetry.io/otel/log/global" metricnoop "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel/propagation" sdklog "go.opentelemetry.io/otel/sdk/log" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/noop" ) // defaultServiceName 缺省服务名。 const defaultServiceName = "zt-closet-manage-backend" // noopShutdown 安全的空关闭函数。 func noopShutdown(context.Context) error { return nil } // NewOTelProvider 初始化 OpenTelemetry 三大信号(Traces/Metrics/Logs)并通过 OTLP gRPC 导出。 // // 返回: // - trace.Tracer:全局 Tracer(供 global.Tracer 使用); // - func(ctx) error:聚合的优雅关闭函数,进程退出时调用以 flush 三个 Provider; // - error:初始化错误。 // // 行为约定: // - cfg.Enabled=false:返回 no-op Tracer 与空 Shutdown,不建立任何导出器(业务层 Tracer.Start 仍安全)。 // - cfg.Insecure=true:OTLP gRPC 使用非 TLS(内网采集器)。 // - 导出器采用惰性连接,采集器不可用时不阻断启动,仅后台重试。 func NewOTelProvider(cfg *setting.OtelSettingS) (trace.Tracer, func(context.Context) error, error) { name := defaultServiceName if cfg != nil && cfg.ServiceName != "" { name = cfg.ServiceName } // 未启用:降级为 no-op。 if cfg == nil || !cfg.Enabled { return noop.NewTracerProvider().Tracer(name), noopShutdown, nil } ctx := context.Background() res, err := resource.New(ctx, resource.WithAttributes( attribute.String("service.name", name), attribute.String("service.version", valueOr(cfg.ServiceVersion, "unknown")), attribute.String("deployment.environment", valueOr(cfg.Environment, "unknown")), )) if err != nil { return noop.NewTracerProvider().Tracer(name), noopShutdown, fmt.Errorf("otel build resource: %w", err) } endpoint := cfg.ExporterEndpoint // ===== Traces ===== traceOpts := []otlptracegrpc.Option{otlptracegrpc.WithEndpoint(endpoint)} if cfg.Insecure { traceOpts = append(traceOpts, otlptracegrpc.WithInsecure()) } traceExp, err := otlptracegrpc.New(ctx, traceOpts...) if err != nil { return noop.NewTracerProvider().Tracer(name), noopShutdown, fmt.Errorf("otel trace exporter: %w", err) } ratio := cfg.SamplerRatio if ratio <= 0 { ratio = 1.0 } tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(traceExp), sdktrace.WithResource(res), sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(ratio))), ) otel.SetTracerProvider(tp) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, )) // ===== Metrics ===== metricOpts := []otlpmetricgrpc.Option{otlpmetricgrpc.WithEndpoint(endpoint)} if cfg.Insecure { metricOpts = append(metricOpts, otlpmetricgrpc.WithInsecure()) } metricExp, err := otlpmetricgrpc.New(ctx, metricOpts...) if err != nil { // 已建 trace,尽力关闭并恢复全局默认 provider,避免留下已关闭的 provider。 _ = tp.Shutdown(ctx) otel.SetTracerProvider(noop.NewTracerProvider()) return noop.NewTracerProvider().Tracer(name), noopShutdown, fmt.Errorf("otel metric exporter: %w", err) } mp := sdkmetric.NewMeterProvider( sdkmetric.WithResource(res), sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExp)), ) otel.SetMeterProvider(mp) // Go runtime 指标采集(GC、goroutine、内存等)。 if rerr := runtime.Start(runtime.WithMeterProvider(mp)); rerr != nil { // 非致命,仅记录。 fmt.Printf("otel runtime metrics start failed: %v\n", rerr) } // ===== Logs ===== logOpts := []otlploggrpc.Option{otlploggrpc.WithEndpoint(endpoint)} if cfg.Insecure { logOpts = append(logOpts, otlploggrpc.WithInsecure()) } logExp, err := otlploggrpc.New(ctx, logOpts...) if err != nil { _ = tp.Shutdown(ctx) _ = mp.Shutdown(ctx) otel.SetTracerProvider(noop.NewTracerProvider()) otel.SetMeterProvider(metricnoop.NewMeterProvider()) return noop.NewTracerProvider().Tracer(name), noopShutdown, fmt.Errorf("otel log exporter: %w", err) } lp := sdklog.NewLoggerProvider( sdklog.WithResource(res), sdklog.WithProcessor(sdklog.NewBatchProcessor(logExp)), ) logglobal.SetLoggerProvider(lp) // 聚合优雅关闭:依次 flush/关闭三个 Provider。 shutdown := func(ctx context.Context) error { return errors.Join( tp.Shutdown(ctx), mp.Shutdown(ctx), lp.Shutdown(ctx), ) } return tp.Tracer(name), shutdown, nil } // valueOr 返回 v,v 为空时返回 def。 func valueOr(v, def string) string { if v == "" { return def } return v }