Files
2026-07-08 11:16:09 +08:00

419 lines
14 KiB
Go
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.
package trace
import (
"context"
"fmt"
"math"
"net/http"
"os"
"sync/atomic"
"time"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/contrib/propagators/b3"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
)
// Config 链路追踪配置
type Config struct {
// ServiceName 服务名称
ServiceName string
// ServiceVersion 服务版本
ServiceVersion string
// Environment 运行环境
Environment string
// ExporterType 导出器类型: "otlp-http", "otlp-grpc", "stdout"
ExporterType string
// Endpoint OTLP 导出器地址
Endpoint string
// Insecure 是否使用明文(无 TLS)连接 collector。
// 默认 falseTLS);对 localhost:4318 等明文 collector 需显式置 trueC13c)。
Insecure bool
// SampleRatio 采样比例 (0.0-1.0)
SampleRatio float64
// Enabled 是否启用
Enabled bool
// Propagator 传播器类型: "w3c", "b3", "jaeger"
Propagator string
}
// DefaultConfig 默认配置
var DefaultConfig = Config{
ServiceName: "xlgo-service",
ServiceVersion: "1.0.0", // 应用自身版本(非框架版本 xlgo.Version);建议业务侧覆盖为实际应用版本
Environment: "development",
ExporterType: "otlp-http",
Endpoint: "localhost:4318",
SampleRatio: 1.0,
Enabled: false,
Propagator: "w3c",
}
// tracerProviderPtr 全局 TracerProvideratomicC13a)。
// Init 之前/Close 之后均为 Noop/NeverSample,保证任何时刻 Load 非 nil,请求期不 panic。
var tracerProviderPtr atomic.Pointer[sdktrace.TracerProvider]
// tracerPtr 全局 TraceratomicC13a)。Init 之前为 Noop,调用安全。
var tracerPtr atomic.Pointer[trace.Tracer]
const defaultOperationTimeout = 5 * time.Second
var operationTimeoutNanos atomic.Int64
func init() {
// 初始化为 Noop,保证包级函数在任何时刻(未 Init / 已 CloseLoad 均非 nilC13a)。
// P1 #19:用 noop.NewTracerProvider()(新 OTel API),替代已弃用的 trace.NewNoopTracerProvider()。
noopProvider := noop.NewTracerProvider()
noopTracer := noopProvider.Tracer("xlgo")
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.NeverSample()))
tracerProviderPtr.Store(tp)
tracerPtr.Store(&noopTracer)
operationTimeoutNanos.Store(int64(defaultOperationTimeout))
}
// getTracer 返回全局 Tracer 的 atomic 快照(永不 nil)。
func getTracer() trace.Tracer {
return *tracerPtr.Load()
}
// TracerProvider 全局 TracerProvider(导出供高级用法;返回当前快照)。
func TracerProvider() *sdktrace.TracerProvider {
return tracerProviderPtr.Load()
}
// Init 初始化链路追踪
func Init(cfg Config) error {
if math.IsNaN(cfg.SampleRatio) || cfg.SampleRatio < 0 || cfg.SampleRatio > 1 {
return fmt.Errorf("trace SampleRatio must be between 0.0 and 1.0: %v", cfg.SampleRatio)
}
if !cfg.Enabled {
// 设置 Noop TracerP1 #19noop 包替代弃用 API
noopProvider := noop.NewTracerProvider()
otel.SetTracerProvider(noopProvider)
noopTracer := noopProvider.Tracer(cfg.ServiceName)
tracerPtr.Store(&noopTracer)
// M-64 修复:Swap 出旧 provider(可能持有 exporter)并 Shutdown
// 避免禁用 trace 后旧 exporter 后台 goroutine/连接持续占用。
fallback := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.NeverSample()))
if old := tracerProviderPtr.Swap(fallback); old != nil {
_ = shutdownProviderWithTimeout(context.Background(), old)
}
return nil
}
// 创建资源
// 注意:不传 semconv.SchemaURL 与 resource.Default() 混用——
// resource.Default() 在不同 OTel 版本可能使用与 semconv v1.24.0 不同的 schema URL
// resource.Merge 对冲突的 SchemaURL 直接报错。这里用空 schema URL 的属性集合并,
// 避免版本漂移导致的初始化失败。
res, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
"",
semconv.ServiceName(cfg.ServiceName),
semconv.ServiceVersion(cfg.ServiceVersion),
attribute.String("environment", cfg.Environment),
),
)
if err != nil {
return err
}
// 创建导出器
exporter, err := createExporter(cfg)
if err != nil {
return err
}
// 创建 TracerProvider
newProvider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.TraceIDRatioBased(cfg.SampleRatio)),
)
// 设置传播器(非法类型返错,C13e 不再静默回落)
prop, err := createPropagator(cfg.Propagator)
if err != nil {
// M-65 修复:传播器非法时回滚已创建的 provider。此时尚未 otel.SetTracerProvider
// otel 全局仍指向旧 provider,无需恢复——仅 Shutdown 新 provider 避免泄漏。
_ = shutdownProviderWithTimeout(context.Background(), newProvider)
return err
}
// 全部成功后再切换全局状态(M-65:避免失败后 otel 指向已 Shutdown 的 provider)。
otel.SetTracerProvider(newProvider)
otel.SetTextMapPropagator(prop)
// 原子替换:先建新 provider,成功后再 Store,并关闭旧 provider(若持有 exporter)。
oldProvider := tracerProviderPtr.Swap(newProvider)
tracer := newProvider.Tracer(cfg.ServiceName)
tracerPtr.Store(&tracer)
if oldProvider != nil {
_ = shutdownProviderWithTimeout(context.Background(), oldProvider)
}
return nil
}
func operationTimeout() time.Duration {
timeout := time.Duration(operationTimeoutNanos.Load())
if timeout <= 0 {
return defaultOperationTimeout
}
return timeout
}
func contextWithOperationTimeout(parent context.Context) (context.Context, context.CancelFunc) {
if parent == nil {
parent = context.Background()
}
return context.WithTimeout(parent, operationTimeout())
}
func shutdownProviderWithTimeout(parent context.Context, provider *sdktrace.TracerProvider) error {
if provider == nil {
return nil
}
ctx, cancel := contextWithOperationTimeout(parent)
defer cancel()
return provider.Shutdown(ctx)
}
// createExporter 创建导出器
func createExporter(cfg Config) (sdktrace.SpanExporter, error) {
switch cfg.ExporterType {
case "otlp-http":
opts := []otlptracehttp.Option{otlptracehttp.WithEndpoint(cfg.Endpoint)}
if cfg.Insecure {
opts = append(opts, otlptracehttp.WithInsecure()) // C13c
}
client := otlptracehttp.NewClient(opts...)
ctx, cancel := contextWithOperationTimeout(context.Background())
defer cancel()
return otlptrace.New(ctx, client)
case "otlp-grpc":
opts := []otlptracegrpc.Option{otlptracegrpc.WithEndpoint(cfg.Endpoint)}
if cfg.Insecure {
opts = append(opts, otlptracegrpc.WithInsecure()) // C13c
}
client := otlptracegrpc.NewClient(opts...)
ctx, cancel := contextWithOperationTimeout(context.Background())
defer cancel()
return otlptrace.New(ctx, client)
case "stdout":
return stdouttrace.New(
stdouttrace.WithWriter(os.Stdout),
stdouttrace.WithPrettyPrint(),
)
default:
// C13b:未知导出器返错,不再返回 nil 喂 WithBatcher(nil)。
return nil, fmt.Errorf("不支持的导出器类型: %s", cfg.ExporterType)
}
}
// createPropagator 创建传播器(C13e:实现 b3,jaeger 映射 W3C,未知返错)。
func createPropagator(propagatorType string) (propagation.TextMapPropagator, error) {
switch propagatorType {
case "w3c", "":
return propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
), nil
case "b3":
// 同时支持多头与单头注入/抽取,兼容旧 B3 客户端。
return b3.New(b3.WithInjectEncoding(b3.B3MultipleHeader | b3.B3SingleHeader)), nil
case "jaeger":
// 现代 Jaeger agent 透传 W3C TraceContext;纯 Jaeger thrift 头协议需下游用 b3。
// 不引入不稳定的 jaegerremix 模块,jaeger 视为 W3C 别名。
return propagation.TraceContext{}, nil
default:
return nil, fmt.Errorf("不支持的传播器类型: %s", propagatorType)
}
}
// Close 关闭链路追踪。
//
// H-14 修复:去掉 sync.Once。原实现 Close→Init→Close 第二次因 once 已消费而 no-op,
// 新 provider 的 exporter 后台 goroutine/连接泄漏。改为每次 Swap 出当前 provider 并
// ShutdownStore 回兜底 NeverSample providerC13aClose 后再用已关闭 provider)。
// 幂等:重复 Close 时 Swap 得到的是无 exporter 的兜底 providerShutdown 无害。
// 并发安全:Swap 原子返回唯一指针,不会 double-Shutdown 同一 provider。
func Close(ctx context.Context) error {
fallback := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.NeverSample()))
old := tracerProviderPtr.Swap(fallback)
noopTracer := noop.NewTracerProvider().Tracer("xlgo") // P1 #19noop 包替代弃用 API
tracerPtr.Store(&noopTracer)
if old != nil {
return shutdownProviderWithTimeout(ctx, old)
}
return nil
}
// Middleware Gin 中间件
func Middleware(serviceName string) gin.HandlerFunc {
return func(c *gin.Context) {
// 从请求中提取 TraceContext
ctx := otel.GetTextMapPropagator().Extract(c.Request.Context(), propagation.HeaderCarrier(c.Request.Header))
// 创建 Span。P1 #19span 名以路由模板(FullPath)为准以保持低基数。
// 原实现 `Method+" "+FullPath()` 后判 `== ""` 永不成立("GET " 非空),
// 未匹配路由的回退分支形同虚设、且用原始 URL.Path(含 ID)会导致 span 名高基数爆炸。
// 现显式判断 FullPath 为空(未匹配路由),用固定低基数名。
route := c.FullPath()
var spanName string
if route == "" {
spanName = c.Request.Method + " [unmatched]"
} else {
spanName = c.Request.Method + " " + route
}
attrs := []attribute.KeyValue{
semconv.HTTPRequestMethodKey.String(c.Request.Method),
semconv.URLPathKey.String(c.Request.URL.Path),
semconv.HTTPRouteKey.String(route),
attribute.String("http.user_agent", c.Request.UserAgent()),
attribute.String("http.host", c.Request.Host),
}
if serviceName != "" {
attrs = append(attrs, attribute.String("service.name", serviceName))
}
ctx, span := getTracer().Start(ctx, spanName,
trace.WithSpanKind(trace.SpanKindServer),
trace.WithAttributes(attrs...),
)
// P1 #19defer 保证下游 handler panic(在上游 recovery 捕获前)时 span 也被结束,
// 不泄漏未结束的 span。
defer span.End()
// C13d:更新 c.Request,使下游 c.Request.Context() 含 span
// 同时保留 c.Set("otel_ctx", ctx) 兼容既有 GetContext 用法。
c.Request = c.Request.WithContext(ctx)
c.Set("otel_ctx", ctx)
// 将 TraceID 添加到响应头
if span.SpanContext().HasTraceID() {
c.Header("X-Trace-ID", span.SpanContext().TraceID().String())
}
// 执行请求
c.Next()
// 设置 Span 状态
status := c.Writer.Status()
span.SetAttributes(semconv.HTTPResponseStatusCodeKey.Int(status))
if status >= 400 {
span.SetStatus(codes.Error, http.StatusText(status))
}
// 成功路径不显式设 codes.OkM18):OTel 规范中 Span 状态默认 UNSET
// 仅在错误时设 Error;显式设 Ok 会掩盖下游子 Span 的真实错误状态。
}
}
// GetContext 从 Gin Context 获取 OpenTelemetry Context
//
// C13:裸断言改 comma-ok,防 "otel_ctx" 被外部置为非 context 值时 panic。
func GetContext(c *gin.Context) context.Context {
if v, exists := c.Get("otel_ctx"); exists {
if ctx, ok := v.(context.Context); ok {
return ctx
}
}
return c.Request.Context()
}
// GetTraceID 获取当前 TraceID
func GetTraceID(c *gin.Context) string {
ctx := GetContext(c)
span := trace.SpanFromContext(ctx)
if span.SpanContext().HasTraceID() {
return span.SpanContext().TraceID().String()
}
return ""
}
// StartSpan 创建子 Span
func StartSpan(c *gin.Context, name string, attrs ...attribute.KeyValue) (context.Context, trace.Span) {
ctx := GetContext(c)
return getTracer().Start(ctx, name,
trace.WithSpanKind(trace.SpanKindInternal),
trace.WithAttributes(attrs...),
)
}
// StartSpanFromContext 从 Context 创建 Span
func StartSpanFromContext(ctx context.Context, name string, attrs ...attribute.KeyValue) (context.Context, trace.Span) {
return getTracer().Start(ctx, name,
trace.WithSpanKind(trace.SpanKindInternal),
trace.WithAttributes(attrs...),
)
}
// RecordError 记录错误
func RecordError(c *gin.Context, err error) {
if c == nil || err == nil {
return
}
ctx := GetContext(c)
span := trace.SpanFromContext(ctx)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
}
// RecordErrorToSpan 记录错误到指定 Span
func RecordErrorToSpan(span trace.Span, err error) {
if span == nil || err == nil {
return
}
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
}
// AddAttributes 添加属性到当前 Span
func AddAttributes(c *gin.Context, attrs ...attribute.KeyValue) {
ctx := GetContext(c)
span := trace.SpanFromContext(ctx)
span.SetAttributes(attrs...)
}
// GetTracer 获取全局 Tracer
func GetTracer() trace.Tracer {
return getTracer()
}
// SetAttribute 设置单个属性
func SetAttribute(c *gin.Context, key string, value any) {
ctx := GetContext(c)
span := trace.SpanFromContext(ctx)
switch v := value.(type) {
case string:
span.SetAttributes(attribute.String(key, v))
case int:
span.SetAttributes(attribute.Int(key, v))
case int64:
span.SetAttributes(attribute.Int64(key, v))
case bool:
span.SetAttributes(attribute.Bool(key, v))
case float64:
span.SetAttributes(attribute.Float64(key, v))
default:
span.SetAttributes(attribute.String(key, fmt.Sprintf("%v", v)))
}
}