dcfd24b624
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>
276 lines
7.0 KiB
Go
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)))
|
|
}
|
|
} |