项目文件夹

文件
2026-07-08 11:16:09 +08:00

419 行
14 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
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 全局 TracerProvideratomic,C13a)。
// Init 之前/Close 之后均为 Noop/NeverSample,保证任何时刻 Load 非 nil,请求期不 panic。
var tracerProviderPtr atomic.Pointer[sdktrace.TracerProvider]
// tracerPtr 全局 Traceratomic,C13a)。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 并
// Shutdown,Store 回兜底 NeverSample providerC13aClose 后再用已关闭 provider)。
// 幂等:重复 Close 时 Swap 得到的是无 exporter 的兜底 provider,Shutdown 无害。
// 并发安全: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)))
}
}