深入 OpenTelemetry 源码与实战(下篇)

0 阅读15分钟

引言

通过上篇的源码拆解,我们已经掌握了 OpenTelemetry 的核心初始化逻辑,接下来本节将进行本地链路的快速实战,验证我们的初始化代码是否有效,同时掌握最基础的 Span 创建、链路记录方法。 为了避免代码冗余,本节将直接复用上篇实现的 InitTracerProvider 初始化函数(大家可以将上篇的 trace 包代码复制到当前项目中,或直接引用对应包)。我们会构建一个简单的本地调用链路,创建父子/平级 Span,添加自定义属性和事件,最后通过 Jaeger UI 查看追踪结果,快速上手 OpenTelemetry 的基础使用。

本地链路快速落地

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"golang.org/x/sync/errgroup"
	"time"
)
// 记录链路
func funcA(ctx context.Context) {
	tr := otel.Tracer("basic_name")
	_, span := tr.Start(ctx, "func-a")
    // 这里就是为这个 span 本身添加属性 也就是 只有 funcA 能看到 main 和 funcB 看不到 可以看一下运行结果
    // 等大家看完 Start 源码 返回的时候 内部会加上 自己设置的 Attributes 和 Event 
	span.SetAttributes(attribute.String("name", "funA"))
    span.AddEvent("完成 funcA")
	time.Sleep(time.Second)
	span.End()
}
// 记录链路
func funcB(ctx context.Context) {
	tr := otel.Tracer("basic_name")
	_, span := tr.Start(ctx, "func-b")
	fmt.Println("trace:", span.SpanContext().TraceID(), span.SpanContext().SpanID())
	time.Sleep(time.Second)
	span.End()
}

func main() {
	tp, _ := InitTracerProvider(common.Options{
		Name:     "basic_name",   //唯一的服务号
		Endpoint: "http://127.0.0.1:14268/api/traces",  // 换成自己服务器的ip地址
		Sampler:  1,   //这里就全采样用于学习测试
		Batcher:  "jaeger",
	})
	ctx := context.Background()  //定义 主 context
    // 最后必须要关闭 大家看过上面源码就知道 这个关闭类型是 sync.Once 无论调用多少次 都只执行一次
	defer func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_ = tp.Shutdown(ctx)
	}()
	// 重点逻辑 在这里 第一次的话 会生成 TracerID 这条请求中核心参数,会一直传递下去 下面有源码解读
	tr := otel.Tracer("basic_name")
	spanCtx, span := tr.Start(ctx, "func-main")
    /*
     errgroup 不懂的话 我之前文章介绍过 这个非常好用 以后就可以把 sync.WaitGroup 替换成 errgroup 
     不需要去写 add 和 done,因为内部都做了  下面代码其实就是开了两个协程并行执行而已
     */
	gw, gctx := errgroup.WithContext(spanCtx)
	gw.Go(func() error {
		funcA(gctx)
		return nil
	})
	gw.Go(func() error {
		funcB(gctx)
		return nil
	})
	
	_ = gw.Wait()
    // 一定是等协程执行完了 才把 主的 span 关闭
	span.End()

}

运行效果

在这里插入图片描述

核心逻辑讲解

otel.Tracer("basic_name") 核心函数

在有些中间件,或者大型项目在用的时候可能没有用 otel.Tracer() 函数,而是先 GetTracerProvider() 获得全局的 tp

然后在执行 tp.Tracer("basic_name"),大家知道我们用 SetTracerProvider(...) 去放进去,这个时候在去取

otel.Tracer("basic_name") 这个是等价的,如下代码其实就是帮我们在这么做

func Tracer(name string, opts ...trace.TracerOption) trace.Tracer {
	return GetTracerProvider().Tracer(name, opts...)
}

// 详细解读 内部逻辑 这就是我们放上去的 tp 
func (p *TracerProvider) Tracer(name string, opts ...trace.TracerOption) trace.Tracer {
	if p.isShutdown.Load() {  // 是否关闭
		return noop.NewTracerProvider().Tracer(name, opts...)
	}
    // 这个 opt... 解包的 TracerOption 这个类型 我们没有传  就是 nil 这条语句就是初始化一下 就全是空
	c := trace.NewTracerConfig(opts...)
    
	if name == "" {   //防止没有name 这里我们有
		name = defaultTracerName
	}
    // 这里是 核心 用于唯一标识一个 Tracer 示例,以我们例子来看 除了name 全空
	is := instrumentation.Scope{
		Name:       name,
		Version:    c.InstrumentationVersion(),
		SchemaURL:  c.SchemaURL(),
		Attributes: c.InstrumentationAttributes(),
	}

	t, ok := func() (trace.Tracer, bool) {
        // 再次检查 是因为 如果在生成 is 的时候 关了这里再执行就会有问题
		p.mu.Lock()
		defer p.mu.Unlock()
        
		if p.isShutdown.Load() {
			return noop.NewTracerProvider().Tracer(name, opts...), true
		}
        // 判断一下 我们这个 is 在不在我们这个map中。如果在我们就复用,如果不在,我们就新建一个实例存入map
        // 所以说 is 是 唯一标识一个实例的
		t, ok := p.namedTracer[is]
		if !ok {
           	// 第一次没有 
			t = &tracer{
				provider:             p,  //父引用 p 就是 TracerProvider
				instrumentationScope: is,  // 自己的唯一标识
			}

			var err error
            // 创建一个指标采集的自定义 Tracer 实例  如果全局可观测性开关未开启,则直接返回空实例,不执行任何指标创建逻辑
            // 这是关于监控相关的事情,可后续学习
			t.inst, err = observ.NewTracer()
			if err != nil {
				otel.Handle(err)
			}
			// 存入
			p.namedTracer[is] = t
		}
		return t, ok
	}()
	if !ok {
		// 这个的意思 第一次或者没找到,新建了 打个日志
		global.Info(
			"Tracer created",
			"name",
			name,
			"version",
			is.Version,
			"schemaURL",
			is.SchemaURL,
			"attributes",
			is.Attributes,
		)
	}
	return t
}

tr.Start(ctx, "func-main") 函数

这个时候才是真正去生成 TracerIDSpanID

func (tr *tracer) Start(ctx context.Context, name string, options ...trace.SpanStartOption,
) (context.Context, trace.Span) {
    // 这里正常也是 空 我们都没放东西,该配置主要预留给框架/中间件使用,用于在 Span 创建阶段注入
    // 比如中间件会在此阶段注入 SpanKind、Attributes、Links 等,后续大家会明白
	config := trace.NewSpanStartConfig(options...)
	
    // 不走 这是兜底判断
	if ctx == nil {
		ctx = context.Background()
	}
	// 这个是重点 后续有源码 作用就是查找 ctx 里有没有父 span
	if p := trace.SpanFromContext(ctx); p != nil {
        // 这里就是巧妙的用断言去判断是否有 父 span
		if sdkSpan, ok := p.(*recordingSpan); ok {
            // 有 TODO
			sdkSpan.addChild()
		}
	}
	
    // 核心 无论有没有都会走这个 简单来讲就是 TracerID 复用 SpanID 要新建
	s := tr.newSpan(ctx, name, &config)
    // 这里逻辑是最重要的 用于 保存父 Span 使得链路可以延续   代码我放到下面 补充
    // 而且大家有个误区 就是去拿 父 Span 是拿根的么? 大家可以思考一下  给出结果
    /*
    由于每次 start 都会执行这个函数 trace.ContextWithSpan(ctx, s) 新建一个ctx1继承了之前的 ctx 的所有其他键值对
    但覆盖了 currentSpanKey 对应的值  如果是main 调用 funA 然后 funA 再调用 funB 的话 调用链如下
    main 调用 Start:创建根 span(s0),返回ctx1
	funA 调用 Start(传入 ctx1):创建子 span(s1,父为 s0),返回ctx2
	funB 调用 Start(传入 ctx2):创建子 span(s2,父为 s1),返回ctx3
	在我们的例子中:
	main 调用 Start:创建根 span(s0),返回ctx1
	funA 调用 Start(传入 ctx1):创建子 span(s1,父为 s0),返回ctx2
	funB 调用 Start(也是传入 ctx1):创建子 span(s2,父为 s0),返回ctx3
	funA 和 funB 是平级的
	希望大家可以从我讲解的代码上,
    */
	newCtx := trace.ContextWithSpan(ctx, s)
    // 同理 用于统计和监控
	if tr.inst.Enabled() {
		if o, ok := s.(interface{ setOrigCtx(context.Context) }); ok {
			o.setOrigCtx(newCtx)
		}
		psc := trace.SpanContextFromContext(ctx)
		tr.inst.SpanStarted(newCtx, psc, s)
	}
    
	// 大家应该清楚 真正 实现链路追踪的是 内部的 SpanProcessor 也就是我们注册的 Jaeger 或者 Zipkin
    // 初始化时讲解过可以有多个SpanProcessor,存在一个列表中 这个时候真正开始让底层开始初始化
	if rw, ok := s.(ReadWriteSpan); ok && s.IsRecording() {
		sps := tr.provider.getSpanProcessors()
		for _, sp := range sps {
            // 这里就是 Jaeger.OnStart(ctx, rw) 或者 Zipkin.OnStart(ctx, rw)
			sp.sp.OnStart(ctx, rw)
		}
	}
    // 不重要
	if rtt, ok := s.(runtimeTracer); ok {
		newCtx = rtt.runtimeTrace(newCtx)
	}

    // 返回
	return newCtx, s
}


// 补充
type traceContextKeyType int
const currentSpanKey traceContextKeyType = iota

func ContextWithSpan(parent context.Context, span Span) context.Context {
    // 下个再来找就可以找到
	return context.WithValue(parent, currentSpanKey, span)
}

SpanFromContext(ctx context.Context) 函数

这里返回的 noopSpanInstance 是官方刻意设计用于程序正常执行的,可以把它理解成 nil ,如果官方用nil 的话,会在很多地方去判断是否是nil,代码量激增,核心代码被埋没别人也没心情去看,再用断言的形式判断,从而到底返回的是什么

func SpanFromContext(ctx context.Context) Span {
	if ctx == nil {
		return noopSpanInstance
	}
    // 这里如果大家看过我之前文章或者 context 源码,大家会清楚的知道 这就是递归的去找有没有这个 key,value
    // 从自己开始一直找到最开始的根,这里如果找到了说明链路的头已经被创建,也就是已经存在 TracerID,要复用这个 要不断链
	if span, ok := ctx.Value(currentSpanKey).(Span); ok {
		return span
	}
    // 这里就是没找到 符合我们第一次 start
	return noopSpanInstance
}

tr.newSpan(ctx, name, &config) 函数

func (tr *tracer) newSpan(ctx context.Context, name string, config *trace.SpanConfig) trace.Span {
    
	var psc trace.SpanContext
    // 判断是否选择断链  正常情况都不断,默认也是不断  有些情况 比如 消息补偿	不信任上游 可以设置
	if config.NewRoot() {
		ctx = trace.ContextWithSpanContext(ctx, psc)
	} else {
        // 大部分走这一步  后续有代码
		psc = trace.SpanContextFromContext(ctx)
	}


	var tid trace.TraceID
	var sid trace.SpanID
    // 拿到之后 判断一下 TraceID有么 其实就是 上一步返回空说明没有根Span,就生成 TraceID 和 SpanID 如果有了根 走else
    // 这里的生成器在之前的源码讲解中,初始化过,也可以自己传,一般都是用默认
	if !psc.TraceID().IsValid() {
        // 生成 TraceID 和 SpanID
		tid, sid = tr.provider.idGenerator.NewIDs(ctx)
	} else {
        // 这里 复用 根的 TraceID 确保不断链 生成自己的 SpanID
		tid = psc.TraceID()
		sid = tr.provider.idGenerator.NewSpanID(ctx, tid)
	}
	
    // 这是采样决策相关的 
	samplingResult := tr.provider.sampler.ShouldSample(SamplingParameters{
		ParentContext: ctx,
		TraceID:       tid,
		Name:          name,
		Kind:          config.SpanKind(),  		 //中间件相关
		Attributes:    config.Attributes(),		//中间件相关
		Links:         config.Links(),		//中间件相关
	})
	
    // 重要!!! 构造 SpanContext 的配置 第一次的话 肯定要让下一个知道这个是根 防止断链
	scc := trace.SpanContextConfig{
		TraceID:    tid,
		SpanID:     sid,
		TraceState: samplingResult.Tracestate, 
	}
    
    // 之前花了时间在如何采样上 这里就是用讲过的算法判断是否采样
	if isSampled(samplingResult) {
		scc.TraceFlags = psc.TraceFlags() | trace.FlagsSampled
	} else {
		scc.TraceFlags = psc.TraceFlags() &^ trace.FlagsSampled
	}
    // 这里是通过上面的配置生成 SpanContext 重点是在 return 的函数去生成 span(接口) 结构体是:RecordingSpan
    // 这个RecordingSpan 里面有字段 SpanContext 重点 就是从这创建的 以后拿的时候不再是空 而是有东西的
	sc := trace.NewSpanContext(scc)
	
    // 这里就是不采样 那就走这个 return 因为都不选择采样了 就返回一个 轻量级的 Span 只有 traceID 和 SpanID 就可以
    // 节省空间 几乎零开销,在高并发的场景 大部分都是10%的采样 如果没有轻量级记录 内存会被压垮
	if !isRecording(samplingResult) {
		return tr.newNonRecordingSpan(sc)
	}
    // 顾名思义 创建一个活着的或者说可以记录的 Span   可以存储事件,有着各种属性
	return tr.newRecordingSpan(ctx, psc, sc, name, samplingResult, config)
}

trace.SpanContextFromContext(ctx) 函数

内部就是SpanFromContext(ctx).SpanContext() 调用了上面讲过的函数,拿到之后用到它的 SpanContext 字段

如果是第一次也就是返回 noopSpanInstance拿到的 SpanContext 也是空结构体

这里主要讲一下第二次也就是 funcA(gctx)函数中 调用后,main 函数已经创建好主 Span 那它执行这个是怎么样的

func SpanContextFromContext(ctx context.Context) SpanContext {
    // 这里结合上面 对 trace.NewSpanContext(scc) 的注释 再来看这个代码 如果是第一次 那就是空没啥意义
    // 第二次的话 就能拿到这个 span 也就是 SpanFromContext(ctx) 返回的 这个span 真实是 RecordingSpan
    // RecordingSpan 他的一个方法 SpanContext() 就是直接返回 他的字段 RecordingSpan.SpanContext 就是上面创建的
	return SpanFromContext(ctx).SpanContext()
}

// 返回的字段 在 trace.NewSpanContext(scc) 函数 中可以看到

trace.NewSpanContext(scc) 函数

主要了解一下它的字段,TODO

func NewSpanContext(config SpanContextConfig) SpanContext {
	return SpanContext{
		traceID:    config.TraceID,    // 整条调用链的唯一 ID(一条 trace 只有一个)
		spanID:     config.SpanID,     // 当前这个 span 自己的 ID(每个 span 都不一样)
		traceFlags: config.TraceFlags, // 一些“状态位”,最重要的是:采不采样
		traceState: config.TraceState, // 跨厂商透传的额外信息
        // 这个 span 是不是从“远端”来的 如果是 RPC通讯 并不是在我这个主机上创建的,那就为 True
        // 如果是本机创建的,我们基础的例子,都是我们这个进程自己创建的,并没有跨主机进行通讯 那就为 False
        // 当上游 Http 创建 Span 请求我们的后端 我们后端拿到的就是 远程的 Span
		remote:     config.Remote,     
	}
}

tr.newRecordingSpan(ctx, psc, sc, name, samplingResult, config) 函数

这里主要是创建可记录的 Span 这边返回后,就回到了主逻辑 tr.Start() 函数,返回阅读

func (tr *tracer) newRecordingSpan(ctx context.Context, psc, sc trace.SpanContext, name string,
	sr SamplingResult,
	config *trace.SpanConfig,
) *recordingSpan {  // 返回类型   *recordingSpan
    // 可以传递时间 这样也可以把时间算进来
	startTime := config.Timestamp()
	if startTime.IsZero() {
		startTime = time.Now()
	}
	// 要返回的 NewSpan
	s := &recordingSpan{
		parent:      psc,   // 父 span
		spanContext: sc,   // 当前 Span
		spanKind:    trace.ValidateSpanKind(config.SpanKind()),  // 中间件可能用到
		name:        name,   // span 名字  比如 main funA 自己传的
		startTime:   startTime,
        // 这个很有用 自己在业务中存一些 事件 后续进阶例子我们会用到,记住在这存储了就好 
        // 而且为了防止有人恶意存上千个,这里有限制,默认最多 128 个 在初始化 tp 的时候设置的
		events:      newEvictedQueueEvent(tr.provider.spanLimits.EventCountLimit), 
        // // 中间件可能用到
		links:       newEvictedQueueLink(tr.provider.spanLimits.LinkCountLimit),
        // 反向保存 tracer,后面 End() 要用
		tracer:      tr,
	}
	// 了解
	for _, l := range config.Links() {
		s.AddLink(l)
	}
	// 这里是第三方的 一些 key value 对
	s.SetAttributes(sr.Attributes...)
    // 这里是添加用户或者中间件传来的 一些 key value 对 (后续进阶例子也会用到)
	s.SetAttributes(config.Attributes()...)
	
    // 这个是 可观测性开关 默认不开启 这个逻辑不走 直接 return
	if tr.inst.Enabled() {
		ctx = trace.ContextWithSpan(ctx, s)
		tr.inst.SpanLive(ctx, s)
	}

	return s
}

跨服务全链路整合(Gin / Gorm / Fasthttp 生态对接)

客户端部分

方便测试这里使用 fasthttp只是为了模拟跨服务或者跨网段的链路追踪,由于我们学到了初始化的源码和链路追踪的源码,这时候再看一些中间件或者大型项目的链路追踪源码,就明白是如何连接起来了

重点介绍了我们注册的 Propagator (传播器),最重要的 InjectExtract 方法,这是跨服务进行链路追踪和核心

并使用了 baggage 传播我们自己想传的逻辑,补充一下,我们注册的是: SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))

第一个主要就是传送 TraceIDSpanID ,一般不会用它传递其他的,防止污染

Inject 的时候 它是可以支持 p.Inject(Newctx, propagation.MapCarrier(headers)) 这个 headers 前提是 http.headers

但由于我们用的是fasthttp 所以我们需要转换一下,这里告诉大家,如果是正常写项目,直接放入即可

package main

import (
	"context"
	"go.opentelemetry.io/otel/baggage"
	"golang.org/x/sync/errgroup"
	"time"

	"github.com/valyala/fasthttp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/propagation"
)

// 这里 简单协程 证明 链路 funcC 和 funcD 链路是同级
func funcC(ctx context.Context) {
	tr := otel.Tracer("basic_name")
	_, span := tr.Start(ctx, "func-C")
	span.SetAttributes(attribute.String("FuncC的测试key", "内部key,不可传递"))
	time.Sleep(time.Second)
	span.End()
}

// 重点 这里面 是 用 fasthttp 发送请求 携带链路数据以及自己想发的业务数据  比如 userid (可选)
func funcD(ctx context.Context) {
	tr := otel.Tracer("basic_name")
    // start 才是真正 拿到 TraceID 找到链路 所以必须传 ctx 不懂可以回去看源码
	spanCtx, span := tr.Start(ctx, "func-D")
    // 这里给大家展示了 如果创建 baggage 数据 如何塞入 如果你想传的很多,比如是结构体 
    // 那完全可以 json 序列化成 []byte 然后放入 在业务端 在反序列化
	userID, _ := baggage.NewMember("user.id", "10001")
	userRole, _ := baggage.NewMember("user.role", "1")
    // 可以一下 塞多个
	bag, _ := baggage.New(userID, userRole)
    // 这就是 放入 ctx 和 我们生成的数据 返回一个新的 ctx 这个 ctx 包含之前的也有生成的数据
	Newctx := baggage.ContextWithBaggage(spanCtx, bag)
	time.Sleep(time.Second)
    // 新建请求
	req := fasthttp.AcquireRequest()
	defer fasthttp.ReleaseRequest(req)  //防止泄露 关闭
	req.SetRequestURI("http://127.0.0.1:8090/server")
	req.Header.SetMethod("GET")
	//重点 获取 我们传入的 Propagator
	p := otel.GetTextMapPropagator()
    
	// 这个要注意 !!!  由于类型不匹配 只能新建一个 map 然后注入值之后 再把值给到 req.Header
	headers := make(map[string]string)
	p.Inject(Newctx, propagation.MapCarrier(headers))
	for key, value := range headers {
		req.Header.Set(key, value)
	}

	fclient := fasthttp.Client{}
	fres := fasthttp.Response{}
	// 隐藏错误
	_ = fclient.Do(req, &fres)

	span.End()
}

func main() {

	tp, _ := common.InitTracerProvider(common.Options{
		Name:     "basic_name",
		Endpoint: "http://192.168.163.132:14268/api/traces",
		Sampler:  1,
		Batcher:  "jaeger",
	})

	ctx, cancel := context.WithCancel(context.Background())
    // 一样需要关闭 然后 tp 就会把攒的那些 trace 发送出去
	defer func(ctx context.Context) {
		ctx, cancel = context.WithTimeout(ctx, time.Second*5)
		defer cancel()
		if err := tp.Shutdown(ctx); err != nil {
			panic(err)
		}
	}(ctx)

	tr := otel.Tracer("basic_name")
    // 新建根 span
	spanCtx, span := tr.Start(ctx, "func-main")

	gw, gctx := errgroup.WithContext(spanCtx)
	gw.Go(func() error {
		funcC(gctx)
		return nil
	})
	gw.Go(func() error {
		funcD(gctx)
		return nil
	})

	_ = gw.Wait()
	span.End()
}

Server 端部分

这里给大家讲解 Server 端读取链路拿到 baggage 数据,并且使用 gin 官方支持的 Opentelemetry 中间件 还有 grom 支持的

并附带部分源码,如果在清楚上面讲过的源码基础上,下面源码理解起来非常简单

package main

import (
	"Advanced_Shop/test/telemetry/ch03/server/model"
	"Advanced_Shop/test/telemetry/ch04/common"
	"fmt"
	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/baggage"
	"go.opentelemetry.io/otel/propagation"

	"go.opentelemetry.io/otel/sdk/trace"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/schema"
	"time"

	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
	"gorm.io/plugin/opentelemetry/tracing"
)
// 大家这里应该明白一件事情,由于main函数初始化的,想要其他函数使用,必须定义成全局变量,而且如果你的请求处理分布在不同包中,直接使用全局变量会增加耦合度。对于他的并发安全使用也是麻烦,所以大家最好使用 option 选项模式,可以看一下之前的文章
var tp *trace.TracerProvider

func Server(c *gin.Context) {
	// 拿到 context 是client端注入东西的 Context
	ctx := c.Request.Context()
	propagator := otel.GetTextMapPropagator()
	tr := tp.Tracer("basic_name")
    // 提取出来 就知道链路 TraceID 和 baggage 数据 
	sctx := propagator.Extract(ctx, propagation.HeaderCarrier(c.Request.Header))
    // 创建子链路
	spanCtx, span := tr.Start(sctx, "server")
	defer span.End()
    //从中拿到 baggage 数据 并 打印
	bag := baggage.FromContext(sctx)
	userID := bag.Member("user.id").Value()
	fmt.Println(userID)   // 10001
	
    // 这里给大家介绍 gorm 中的 链路追踪 所以 这里建了一个新数据库和新表 只是用于 到时看结果
	dsn := "root:root@tcp(127.0.0.1:3306)/user-srv?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
		},
	})
	if err != nil {
		panic(err)
	}
    
	// 这是重点 使用这个插件  用法与 gin的中间件差不多 tracing.NewPlugin() 后续有源码解读
	if err := db.Use(tracing.NewPlugin()); err != nil {
		panic(err)
	}
	// 这个时候使用数据库的时候 WithContext(spanCtx) 是最关键的 
	if err := db.WithContext(spanCtx).Model(model.UserModels{}).Where("id = ?", 1).First(&model.UserModels{}).Error; err != nil {
		panic(err)
	}
	time.Sleep(500 * time.Millisecond)
	c.JSON(200, gin.H{})
}


func main() {
	tp, _ = common.InitTracerProvider(common.Options{
		Name:     "basic_name",
		Endpoint: "http://192.168.163.132:14268/api/traces",
		Sampler:  1,
		Batcher:  "jaeger",
	})
	r := gin.Default()
    // 先介绍一下 这个源码 然后介绍一下  gorm 对这个 opentelemetry 的 支持的中间件
	r.Use(otelgin.Middleware("gin_Middleware"))
	r.GET("/", func(c *gin.Context) {

	})
	r.GET("/server", Server)
	r.Run(":8090")
}

执行结果

第二张是最后一个 gin 中间件生成的 在这里插入图片描述 在这里插入图片描述

核心逻辑讲解

otelgin.Middleware("gin_Middleware") 函数

func Middleware(service string, opts ...Option) gin.HandlerFunc {
	cfg := config{}
	// 依旧是可以设置一些配置
	for _, opt := range opts {
		opt.apply(&cfg)
	}
    // 这里就是没传就自己去拿 调用的函数也是之前讲过的
	if cfg.TracerProvider == nil {
		cfg.TracerProvider = otel.GetTracerProvider()
	}
    // 这里生成 Tracer
	tracer := cfg.TracerProvider.Tracer(
		ScopeName,
		oteltrace.WithInstrumentationVersion(Version()),
	)
    // 这里 拿  Propagators
	if cfg.Propagators == nil {
		cfg.Propagators = otel.GetTextMapPropagator()
	}
    // 监控系统相关
	if cfg.MeterProvider == nil {
		cfg.MeterProvider = otel.GetMeterProvider()
	}
	if cfg.SpanNameFormatter == nil {
		cfg.SpanNameFormatter = defaultSpanNameFormatter
	}
	meter := cfg.MeterProvider.Meter(
		ScopeName,
		metric.WithInstrumentationVersion(Version()),
	)

	sc := semconv.NewHTTPServer(meter)

	return func(c *gin.Context) {
		requestStartTime := time.Now()
        // 这里就是 中间件需要记录的东西
        // 执行前 记录一下  执行后记录一下 已经拿到 span 关键的链路已经解决剩下就是业务问题·
        
}

tracing.NewPlugin() 函数

Gorm 在设计之初就预留了很多接口,以便未来会用到,这也体现了go语言面向接口编程

这里两个重要函数 beforeafter 返回的是钩子函数,绑定到 Gorm , Gorm 会在 SQL 执行时自动触发这些钩子

因为链路传递需要 context ,通过db.WithContext(spanCtx) 将上层业务的追踪上下文注入 Gorm ,供后续钩子使用。

func NewPlugin(opts ...Option) gorm.Plugin {
   
	p := &otelPlugin{}
	for _, opt := range opts {
		opt(p)
	}
	
	if p.provider == nil {
		p.provider = otel.GetTracerProvider()
	}
	p.tracer = p.provider.Tracer("gorm.io/plugin/opentelemetry")

	return p
}

func (p *otelPlugin) before(spanName string) gormHookFunc {...}

func (p *otelPlugin) after() gormHookFunc {...}

总结

本篇从本地单进程链路验证,到跨服务追踪上下文的Inject/Extract传递,再到 GinGorm 主流生态的自动埋点集成,完整落地了 OpenTelemetry 分布式追踪的实战全流程;既承接了上篇的初始化底层逻辑,又掌握了微服务场景下链路保活与框架适配的核心方法,相关方案可直接迁移至生产环境,实现从接口调用到数据库操作的全链路可观测闭环。