Files
xlgo-core/trace/trace.go
T
杭州明婳科技 dcfd24b624 release: v1.0.3 bug fix release
v1.0.3 定位为 bug fix release,收口 v1.0.2 引入的破坏性清理并修复
4 个轻量 bug + 依赖复查 + 版本号治理 + 文档对齐。同时包含此前未提交
的 v1.0.2 全部工作。

v1.0.3 变更:

Fixed:
- generateJTI 忽略 rand.Read 错误(jwt)— 改为 (string, error) 并传播
- QueryBuilder.Page Count 受残留 Limit 截断(repository)— countDB 加 Limit(-1).Offset(-1)
- OSS/本地存储文件名冲突(storage)— 新增 uniqueFilename 加 8 字节随机后缀
- 数据库重试策略对不可恢复错误无效(database)— 新增 isTransientDBError

Dependencies:
- go mod tidy 补全 postgres 方言传递依赖;安全补丁升级
  x/crypto v0.49→v0.53、golang-jwt/jwt/v5 v5.2.1→v5.3.1、gorilla/websocket v1.5.1→v1.5.3

Removed:
- Breaking: 清理 v1.0.2 兼容别名(InitMySQL* / driverName)
- 死代码 database.DBResolver
- 代码中 292 行"评分/理由"自夸注释(#26,23 个文件)

Changed:
- Breaking: 错误码体系重构 CodeSuccess 1→0、CodeFail 0→1,删除 CodeInvalidParams,加编译期防撞码
- database/mysql.go → manager.go;Logger 拆分三独立 core 修复 Tee 重复写入
- 版本号常量化:app.go 新增 const Version 作为唯一来源,CLI/脚手架模板引用之,不再散落字面量

Security:
- CORS 中间件按 W3C 规范修复 Allow-Credentials / Vary: Origin / 非白名单不回显

Added:
- console 包显式 level 控制(SetLevel/WithLevel/LevelSilent,atomic.Int32 并发安全)
- 新增测试 jwt/repository/storage/database/router/middleware/app
- 文档:新增 CHANGELOG.md、Version_v1.0.2_report.md;更新 README/GUIDE 顶部更新日志
- 文档对齐:删除对不存在的 config.DefaultManager() getter 的描述(保持 API 与文档一致)

详见 CHANGELOG.md 与 README.md 更新日志。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:40:10 +08:00

276 lines
7.0 KiB
Go

package trace
import (
"context"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"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/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"
)
// 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
// 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",
}
// TracerProvider 全局 TracerProvider
var tracerProvider *sdktrace.TracerProvider
// Tracer 全局 Tracer
var tracer trace.Tracer
// Init 初始化链路追踪
func Init(cfg Config) error {
if !cfg.Enabled {
// 设置 Noop Tracer
otel.SetTracerProvider(trace.NewNoopTracerProvider())
tracer = otel.Tracer(cfg.ServiceName)
return nil
}
// 创建资源
res, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
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
tracerProvider = sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.TraceIDRatioBased(cfg.SampleRatio)),
)
// 设置全局 TracerProvider
otel.SetTracerProvider(tracerProvider)
// 设置传播器
propagator := createPropagator(cfg.Propagator)
otel.SetTextMapPropagator(propagator)
// 创建 Tracer
tracer = otel.Tracer(cfg.ServiceName)
return nil
}
// createExporter 创建导出器
func createExporter(cfg Config) (sdktrace.SpanExporter, error) {
switch cfg.ExporterType {
case "otlp-http":
client := otlptracehttp.NewClient(
otlptracehttp.WithEndpoint(cfg.Endpoint),
)
return otlptrace.New(context.Background(), client)
case "otlp-grpc":
client := otlptracegrpc.NewClient(
otlptracegrpc.WithEndpoint(cfg.Endpoint),
)
return otlptrace.New(context.Background(), client)
default:
return nil, nil
}
}
// createPropagator 创建传播器
func createPropagator(propagatorType string) propagation.TextMapPropagator {
switch propagatorType {
case "w3c":
return propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
)
default:
return propagation.TraceContext{}
}
}
// Close 关闭链路追踪
func Close(ctx context.Context) error {
if tracerProvider != nil {
return tracerProvider.Shutdown(ctx)
}
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
spanName := c.Request.Method + " " + c.FullPath()
if spanName == "" {
spanName = c.Request.Method + " " + c.Request.URL.Path
}
ctx, span := tracer.Start(ctx, spanName,
trace.WithSpanKind(trace.SpanKindServer),
trace.WithAttributes(
semconv.HTTPRequestMethodKey.String(c.Request.Method),
semconv.URLPathKey.String(c.Request.URL.Path),
semconv.HTTPRouteKey.String(c.FullPath()),
attribute.String("http.user_agent", c.Request.UserAgent()),
attribute.String("http.host", c.Request.Host),
),
)
// 将 context 存入 Gin Context
c.Set("otel_ctx", ctx)
// 将 TraceID 添加到响应头
traceID := span.SpanContext().TraceID().String()
c.Header("X-Trace-ID", traceID)
// 执行请求
c.Next()
// 设置 Span 状态
status := c.Writer.Status()
span.SetAttributes(semconv.HTTPResponseStatusCodeKey.Int(status))
if status >= 400 {
span.SetStatus(codes.Error, http.StatusText(status))
} else {
span.SetStatus(codes.Ok, "")
}
// 结束 Span
span.End()
}
}
// GetContext 从 Gin Context 获取 OpenTelemetry Context
func GetContext(c *gin.Context) context.Context {
if ctx, exists := c.Get("otel_ctx"); exists {
return ctx.(context.Context)
}
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 tracer.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 tracer.Start(ctx, name,
trace.WithSpanKind(trace.SpanKindInternal),
trace.WithAttributes(attrs...),
)
}
// RecordError 记录错误
func RecordError(c *gin.Context, err error) {
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) {
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 tracer
}
// 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)))
}
}