引言
通过上篇的源码拆解,我们已经掌握了 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") 函数
这个时候才是真正去生成 TracerID 和 SpanID
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 (传播器),最重要的 Inject 和 Extract 方法,这是跨服务进行链路追踪和核心
并使用了 baggage 传播我们自己想传的逻辑,补充一下,我们注册的是:
SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
第一个主要就是传送 TraceID 和 SpanID ,一般不会用它传递其他的,防止污染
在 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语言面向接口编程
这里两个重要函数 before 和 after 返回的是钩子函数,绑定到 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传递,再到 Gin、Gorm 主流生态的自动埋点集成,完整落地了 OpenTelemetry 分布式追踪的实战全流程;既承接了上篇的初始化底层逻辑,又掌握了微服务场景下链路保活与框架适配的核心方法,相关方案可直接迁移至生产环境,实现从接口调用到数据库操作的全链路可观测闭环。