Files
common/jaeger/jaeger.go
2026-06-23 16:51:39 +08:00

198 lines
5.6 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 jaeger
import (
"context"
"encoding/json"
"strconv"
"strings"
"sync"
"github.com/gogf/gf/contrib/trace/otlphttp/v2"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/net/gtrace"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/embedded"
"go.opentelemetry.io/otel/trace/noop"
)
var (
ShutDown func(ctx context.Context)
initOnce sync.Once
)
// Init 初始化 Jaeger 链路追踪(延迟初始化,首次调用时执行)
func Init() {
initOnce.Do(func() {
ctx := context.Background()
jaegerAgent := g.Cfg().MustGet(ctx, "jaeger.addr").String()
serverName := g.Cfg().MustGet(ctx, "server.name").String()
if jaegerAgent == "" {
g.Log().Warning(ctx, "⚠️ Jaeger 配置未找到,跳过初始化")
ShutDown = func(ctx context.Context) {} // 空函数,避免 nil panic
return
}
shutdown, err := otlphttp.Init(serverName, jaegerAgent, "/v1/traces")
if err != nil {
g.Log().Errorf(ctx, "Jaeger 初始化失败: %v", err)
ShutDown = func(ctx context.Context) {}
return
}
ShutDown = shutdown
// 包装 TracerProvider只保留 HTTP Server 追踪,屏蔽 DB/Redis/Client 等内部 span
wrapTracerProvider()
g.Log().Infof(ctx, "✅ Jaeger 初始化成功: %s", jaegerAgent)
})
}
func init() {
// 默认自动初始化(保持向后兼容)
Init()
}
// filterTracerProvider 只放行指定 instrument 的 span其余返回 noop tracer
type filterTracerProvider struct {
embedded.TracerProvider
real trace.TracerProvider
noop trace.TracerProvider
allowed map[string]bool
}
func (f *filterTracerProvider) Tracer(instrumentName string, opts ...trace.TracerOption) trace.Tracer {
if f.allowed[instrumentName] {
return f.real.Tracer(instrumentName, opts...)
}
return f.noop.Tracer(instrumentName, opts...)
}
// wrapTracerProvider 包装全局 TracerProvider只保留 HTTP Server 追踪
func wrapTracerProvider() {
otel.SetTracerProvider(&filterTracerProvider{
real: otel.GetTracerProvider(),
noop: noop.NewTracerProvider(),
allowed: map[string]bool{
"github.com/gogf/gf/v2/net/ghttp.Server": true,
},
})
}
// NewSpan 创建新的链路追踪 Span
// spanName: Span 名称,用于在 Jaeger UI 中标识
// 返回带有 Span 的 context 和 Span 对象,调用方需 defer span.End()
func NewSpan(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, *gtrace.Span) {
return gtrace.NewSpan(ctx, spanName, opts...)
}
// RecordError 统一错误记录方法
// 功能:
// 1. 控制台输出错误(带完整堆栈 %+v
// 2. Jaeger 链路追踪记录错误
// 3. 设置 Span 错误状态
//
// 使用示例:
//
// jaeger.RecordError(ctx, err, "保存数据失败")
//
// 参数:
// - ctx: 包含 trace span 的上下文
// - err: 错误对象(支持 gerror 堆栈)
// - msg: 可选的错误描述(用于日志和 Jaeger 显示)
func RecordError(ctx context.Context, err error, msg ...string) {
if err == nil {
return
}
// 1. 控制台输出(%+v 打印完整堆栈)
if len(msg) > 0 && msg[0] != "" {
g.Log().Errorf(ctx, "%s: %+v", msg[0], err)
} else {
g.Log().Errorf(ctx, "%+v", err)
}
// 2. Jaeger 记录(从 context 获取当前 span
span := trace.SpanFromContext(ctx)
if span == nil || !span.IsRecording() {
return
}
// 3. 记录错误到 span
span.RecordError(err)
span.SetAttributes(
attribute.Bool("error", true),
attribute.String("error.message", err.Error()),
)
// 4. 设置 span 状态为错误
if len(msg) > 0 && msg[0] != "" {
span.SetAttributes(attribute.String("error.msg", msg[0]))
span.SetStatus(codes.Error, msg[0]+": "+err.Error())
return
}
span.SetStatus(codes.Error, err.Error())
}
// NewTracer HTTP 请求链路追踪中间件
// 功能:
// 1. 为每个 HTTP 请求创建 Span
// 2. 记录请求参数和响应内容
// 3. 自动捕获错误并记录到 Jaeger
//
// 使用方式:在路由组中注册为中间件
//
// group.Middleware(jaeger.NewTracer)
func NewTracer(r *ghttp.Request) {
// 创建 Span名称取自 controller 方法的 summary 标签)
ctx, span := gtrace.NewSpan(r.Context(), r.GetServeHandler().GetMetaTag("summary"))
r.SetCtx(ctx)
defer span.End()
// 记录请求参数
span.SetAttributes(attribute.String("request", getParams(r)))
// 执行后续中间件和 handler
r.Middleware.Next()
// 清理响应字符串,确保 UTF-8 有效(处理二进制数据如 ZIP 文件)
response := r.Response.BufferString()
cleanResponse := strings.ToValidUTF8(response, "")
// 如果响应太大(如文件下载),只记录前 1000 字符
if len(cleanResponse) > 1000 {
cleanResponse = cleanResponse[:1000] + "... (truncated)"
}
span.SetAttributes(attribute.String("response", cleanResponse))
span.SetAttributes(attribute.Int("http.status_code", r.Response.Status))
if err := r.GetError(); err != nil {
RecordError(ctx, err)
return
}
if r.Response.Status >= 500 {
span.SetAttributes(attribute.Bool("error", true))
span.SetStatus(codes.Error, "http status "+strconv.Itoa(r.Response.Status))
}
}
// getParams 提取请求参数(用于 Jaeger 记录)
func getParams(r *ghttp.Request) string {
params := map[string]interface{}{}
if r.Method == "POST" {
json.Unmarshal(r.GetBody(), &params) //获取raw传参
}
if r.Method == "GET" {
r.Request.ParseForm()
form := r.Form
for k, v := range form {
if vl, e := strconv.Atoi(v[0]); e == nil {
params[k] = vl
} else {
params[k] = v[0]
}
}
}
rp, _ := json.Marshal(&params)
return string(rp)
}