【链路追踪】「Go语言」OpenTelemetry实现[gin, gRPC, log, gorm, redis]的集成

文章目录

  • 一、OpenTelemetry的前世今生
    • OpenTracing
    • OpenCensus
    • 大一统
  • 二、OpenTelemetry快速体验
    • go快速体验
    • OpenTelemetry系统架构
    • 尾部采样
  • 三、通过http完成span传输
    • 函数中传递span的context
  • 四、自定义inject和extract源码
  • 五、gRPC集成
  • 自用框架集成(无视即可)
  • 六、log集成
  • 七、gorm集成
  • 八、gin集成
  • 九、redis集成


一、OpenTelemetry的前世今生

OpenTelemetry中文文档:https://github.com/open-telemetry/docs-cn/blob/main/OT.md

之前用的是jaeger实现链路追踪,但是想要换成Zipkin等框架或集成指标监控或集成日志会换框架很麻烦。

OpenTracing

OpenTracing制定了一套平台无关、厂商无关的协议标准,使得开发人员能够方便的添加或更换底层APM的实现。

在2016年11月的时候发生了一件里程碑事件,CNCF.io接受OpenTracing,同时这也是CNCF的第三个项目,前两个都已经鼎鼎大名了:Kubernetes和Prometheus,由此可见开源世界对APM的重视,对统一标准的重视和渴望。

遵循OpenTracing协议的产品有Jaeger、Zipkin等等。

OpenCensus

中国有句老话,既生瑜何生亮,OpenTracing本身出现的更早且更流行,为什么要有OpenCensus这个项目?

这里先补充一下背景知识,前面提到了分布式追踪,其实在APM领域,还有一个极其重要的监控子类:Metrics指标监控,例如cpu、内存、硬盘、网络等机器指标,grpc的请求延迟、错误率等网络协议指标,用户数、访问数、订单数等业务指标,都可以涵盖在内。

首先,该项目有个非常牛逼的亲爹:Google,要知道就连分布式跟踪的基础论文就是谷歌提出的,可以说谷歌就是亲爹无疑了。

其次,OpenCensus的最初目标并不是抢OpenTracing的饭碗,而是为了把Go语言的Metrics采集、链路跟踪与Go语言自带的profile工具打通,统一用户的使用方式。随着项目的进展,野心也膨胀了,这个时候开始幻想为什么不把其它各种语言的相关采集都统一呢?然后项目组发现了OpenTracing,突然发现,我K,作为谷歌,我们都没玩标准,你们竟然敢玩标准敢想着统一全世界?(此处乃作者的疯人疯语) 于是乎,OpenCensus的场景进一步扩大了,不仅做了Metrics基础指标监控,还做了OpenTracing的老本行:分布式跟踪。

有个谷歌做亲爹已经够牛了,那再加入一个微软做干爹呢?是不是要起飞了?所以,对于OpenCensus的发展而言,微软的直接加入可以说是打破了之前的竞争平衡,间接导致了后面OpenTelemetry项目的诞生。

正所谓是:天下合久必分、分久必合,在此之时,必有枭雄出现:OpenTelemetry横空出世。
两个产品合并,首先要考虑的是什么?有过经验的同学都知道:如何让两边的用户能够继续使用。因此新项目首要核心目标就是兼容OpenTracingOpenCensus

OpenTelemetry的核心工作目前主要集中在3个部分:

  1. 规范的制定和协议的统一,规范包含数据传输、API的规范,协议的统一包含:HTTP W3C的标准支持及GRPC等框架的协议标准
  2. 多语言SDK的实现和集成,用户可以使用SDK进行代码自动注入和手动埋点,同时对其他三方库(Log4j、LogBack等)进行集成支持;
  3. 数据收集系统的实现,当前是基于OpenCensus Service的收集系统,包括Agent和Collector。

由此可见,OpenTelemetry的自身定位很明确:数据采集和标准规范的统一,对于数据如何去使用、存储、展示、告警,官方是不涉及的,我们目前推荐使用Prometheus + GrafanaMetrics存储、展示,使用Jaeger做分布式跟踪的存储和展示。

【链路追踪】「Go语言」OpenTelemetry实现[gin, gRPC, log, gorm, redis]的集成_第1张图片

大一统

有了以上的背景知识,我们就可以顶一下OpenTelemetry的终极目标了:实现Metrics、Tracing、Logging的融合及大一统,作为APM的数据采集终极解决方案。

  • Tracing:提供了一个请求从接收到处理完成整个生命周期的跟踪路径,一次请求通常过经过N个系统,因此也被称为分布式链路追踪
  • Metrics:例如cpu、请求延迟、用户访问数等Counter、Gauge、Histogram指标
  • Logging:传统的日志,提供精确的系统记录

这三者的组合可以形成大一统的APM解决方案:

  • 基于Metrics告警发现异常
  • 通过Tracing定位到具体的系统和方法
  • 根据模块的日志最终定位到错误详情和根源
  • 调整Metrics等设置,更精确的告警/发现问题

二、OpenTelemetry快速体验

jaeger自行安装

go快速体验

package main

import (
	"context"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	"time"
)

func main() {
	url := "http://127.0.0.1:14268/api/traces"
	//专门生成Exporter
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}
	//后续使用telemetry,和jaeger无关 ↓初始化
	tp := trace.NewTracerProvider(
		//上报器 批量上报处理链路
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)

	//自己创建context
	ctx, cancel := context.WithCancel(context.Background())
	//优雅退出
	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)

	//生成tracer
	tr := otel.Tracer("mxshop-otel")
	//生成span
	_, span := tr.Start(ctx, "func-main")
	//业务逻辑
	time.Sleep(time.Second)
	var attrs []attribute.KeyValue
	attrs = append(attrs, attribute.String("key1", "value1"))
	attrs = append(attrs, attribute.Bool("key2", false))
	attrs = append(attrs, attribute.Int("key2", 123))
	attrs = append(attrs, attribute.StringSlice("key2", []string{"value1", "value2"}))

	//设置span里的Tags键值对
	span.SetAttributes(attrs...)
	//设置logs
	span.AddEvent("this is an event")
	//业务逻辑
	time.Sleep(time.Second)
	//结束此span
	span.End()
}

效果:
【链路追踪】「Go语言」OpenTelemetry实现[gin, gRPC, log, gorm, redis]的集成_第2张图片

OpenTelemetry系统架构

【链路追踪】「Go语言」OpenTelemetry实现[gin, gRPC, log, gorm, redis]的集成_第3张图片

这其实是一个OpenTelemetry-collector的架构
Receiver可以适配不同的协议,其他厂商(jaeger)实现这个接口即可
processor默认的让他自动处理 剔除不需要的数据。也可以自己加入一些指标 pprof 健康检测等等
exporter是把这些数据打到什么地方,比如jaeger

官方提供的collector:https://github.com/open-telemetry/opentelemetry-collector
官方很少做适配,官方开放了一个仓库 其他云厂商会适配他的接口:
https://github.com/open-telemetry/opentelemetry-collector-contrib

尾部采样

可以到processor看看
高并发应该才会用到

全量采样

  • 优点:几乎可以分析出所有bug
  • 缺点:量大

尾部采样

  • 优点:尾部采样可以降低采样的开销,只对一部分请求进行采样,减少了数据的收集和传输成本。
  • 缺点:由于只对部分请求进行采样,采样结果可能不够全面和准确,可能会丢失某些有价值的数据。

1W请求:10个以内 10/100000000

采样率:100个采1个。优点 :量小。缺点:有可能某天你采集不到这个bug

采样:某个链路有错误必采,某个链路超时1S必采,就要在最后一个链路的时候才决定是否入库

100个span,99个span如果你就直接入库了,最后的一个入不入库已经不重要,如果一个processor可以在最后一个span产生的时候才决定是否将上个链路上报


三、通过http完成span传输

函数中传递span的context

go:

package main

import (
	"NewGo/log"
	"context"
	"encoding/json"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	"sync"
	"time"
)

const (
	traceName = "mxshop-otel"
)

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}
	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//设置传播提取器
	return nil
}
func funcA(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	tr := otel.Tracer(traceName)
	spCtx, span := tr.Start(ctx, "func-a")
	span.SetAttributes(attribute.String("name", "funA"))
	type _LogStruct struct {
		CurrentTime time.Time `json:"currentTime"`
		PassWho     string    `json:"passWho"`
		Name        string    `json:"name"`
	}
	logTest := _LogStruct{
		CurrentTime: time.Time{},
		PassWho:     "jzin",
		Name:        "func-a",
	}
	log.InfofC(spCtx, "is logs")
	b, _ := json.Marshal(logTest)
	log.InfofC(spCtx, string(b))
	span.SetAttributes(attribute.Key("测试key").String(string(b)))
	time.Sleep(time.Second)
	span.End()
}
func funcB(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	tr := otel.Tracer(traceName)
	spCtx, span := tr.Start(ctx, "func-b")
	span.SetAttributes(attribute.String("name", "funB"))
	type _LogStruct struct {
		CurrentTime time.Time `json:"currentTime"`
		PassWho     string    `json:"passWho"`
		Name        string    `json:"name"`
	}
	logTest := _LogStruct{
		CurrentTime: time.Time{},
		PassWho:     "bjzin",
		Name:        "func-b",
	}
	log.InfofC(spCtx, "is logs b")
	b, _ := json.Marshal(logTest)
	log.InfofC(spCtx, string(b))
	span.SetAttributes(attribute.Key("测试key").String(string(b)))
	time.Sleep(time.Second)
	span.End()
}
func main() {
	_ = tracerProvider()
	ctx, cancel := context.WithCancel(context.Background())
	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(traceName)
	spanCtx, span := tr.Start(ctx, "func-main")
	wg := &sync.WaitGroup{}
	wg.Add(2)
	go funcA(spanCtx, wg)
	go funcB(spanCtx, wg)
	//设置logs
	span.AddEvent("this is an event")
	time.Sleep(time.Second)
	wg.Wait()
	span.End()
}

效果
【链路追踪】「Go语言」OpenTelemetry实现[gin, gRPC, log, gorm, redis]的集成_第4张图片
http传输 gin接收
client:

package main

import (
	"NewGo/log"
	"context"
	"encoding/json"
	"fmt"
	"github.com/valyala/fasthttp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	"sync"
	"time"
)

const (
	traceName = "mxshop-otel"
)

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}
	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//全局设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}
func funcA(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	tr := otel.Tracer(traceName)
	spCtx, span := tr.Start(ctx, "func-a")
	span.SetAttributes(attribute.String("name", "funA"))
	type _LogStruct struct {
		CurrentTime time.Time `json:"currentTime"`
		PassWho     string    `json:"passWho"`
		Name        string    `json:"name"`
	}
	logTest := _LogStruct{
		CurrentTime: time.Time{},
		PassWho:     "jzin",
		Name:        "func-a",
	}
	log.InfofC(spCtx, "is logs")
	b, _ := json.Marshal(logTest)
	log.InfofC(spCtx, string(b))
	span.SetAttributes(attribute.Key("测试key").String(string(b)))
	time.Sleep(time.Second)
	span.End()
}
func funcB(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	tr := otel.Tracer(traceName)
	spanCtx, span := tr.Start(ctx, "func-b")
	span.SetAttributes(attribute.String("name", "funB"))
	fmt.Println("trace", span.SpanContext().TraceID(), span.SpanContext().SpanID())
	time.Sleep(time.Second)

	req := fasthttp.AcquireRequest()
	req.SetRequestURI("http://127.0.0.1:8090/server")
	req.Header.SetMethod("GET")

	//拿到传播器
	p := otel.GetTextMapPropagator()
	headers := make(map[string]string)
	//包裹   context信息注入到包裹里面 把trace的id span的id注入到包裹
	p.Inject(spanCtx, propagation.MapCarrier(headers))

	for key, value := range headers {
		req.Header.Set(key, value)
	}

	fclient := fasthttp.Client{}
	fres := fasthttp.Response{}
	err := fclient.Do(req, &fres)
	if err != nil {
		panic(err)
	}

	span.End()
}
func main() {
	_ = tracerProvider()
	ctx, cancel := context.WithCancel(context.Background())
	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(traceName)
	spanCtx, span := tr.Start(ctx, "func-main")
	wg := &sync.WaitGroup{}
	wg.Add(2)
	go funcA(spanCtx, wg)
	go funcB(spanCtx, wg)
	//设置logs
	span.AddEvent("this is an event")
	time.Sleep(time.Second)
	wg.Wait()
	span.End()
}

server(gin实现):

package main

import (
	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	"time"
)

const (
	traceName = "mxshop-otel"
)

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}
	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//全局设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}
func Server(c *gin.Context) {
	//负责span的抽取和生成
	ctx := c.Request.Context()
	p := otel.GetTextMapPropagator()
	//生成新的context
	sCtx := p.Extract(ctx, propagation.HeaderCarrier(c.Request.Header))
	//拿到tracer
	tr := tp.Tracer(traceName)
	_, span := tr.Start(sCtx, "server")
	time.Sleep(time.Millisecond * 500)
	span.End()
	c.JSON(200, gin.H{})
}
func main() {
	_ = tracerProvider()
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{})
	})
	r.GET("/server", Server)
	if err := r.Run(":8090"); err != nil {
		panic(err)
	}
}

四、自定义inject和extract源码

自己实现http中traceID和spanID的传递

根据上述代码改进:

p.Inject(spanCtx, propagation.MapCarrier(headers))中headers的值为:

key="traceparent"
value="00-6be6f033385a7f6e8a56a3bbc71935c3-dc0c6e540abc7a95-01"

value中00和01之间是traceID和spanID
我们是可以自己设置key的。不用他这个propagation

自己设置前必须客户端和服务端达成一致
比如:我的key是tarceID 服务端就得解析tarceID 不能是其他的
直接设置header:

req.Header.Set("trace-id", span.SpanContext().TraceID().String())
req.Header.Set("span-id", span.SpanContext().SpanID().String())

server端解析:

func Server(c *gin.Context) {
	//负责span的抽取和生成
	ctx := c.Request.Context()
	tr := tp.Tracer(traceName)
	traceID := c.Request.Header.Get("trace-id")
	spanID := c.Request.Header.Get("span-id")
	traceid, _ := otelTrace.TraceIDFromHex(traceID)
	spanid, _ := otelTrace.SpanIDFromHex(spanID)
	spanCtx := otelTrace.NewSpanContext(otelTrace.SpanContextConfig{
		TraceID:    otelTrace.TraceID(traceid),
		SpanID:     otelTrace.SpanID(spanid),
		TraceFlags: otelTrace.FlagsSampled, //这个不设置的话是不会保存的
		Remote:     true,
	})

	//手动构造ctx
	carrier := propagation.HeaderCarrier{}
	carrier.Set("trace-id", traceID)
	propagattor := otel.GetTextMapPropagator()
	pctx := propagattor.Extract(ctx, carrier)
	sct := otelTrace.ContextWithRemoteSpanContext(pctx, spanCtx)
	//p := otel.GetTextMapPropagator()
	//生成新的context
	//sCtx := p.Extract(ctx, propagation.HeaderCarrier(c.Request.Header))
	//拿到tracer

	_, span := tr.Start(sct, "server")
	time.Sleep(time.Millisecond * 500)
	span.End()
	c.JSON(200, gin.H{})
}

五、gRPC集成

grpc官方维护的源码:https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/instrumentation/google.golang.org/grpc/otelgrpc

直接使用grpc维护的拦截器:
proto文件自行生成
client:

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/status"

	pb "NewGo/telemetry/rpc"
)

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}

	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}

func main() {
	_ = tracerProvider()
	// Set up a connection to the server.
	conn, err := grpc.Dial("localhost:50052",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
	)
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	// Contact the server and print out its response.
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "world"})
	if err != nil {
		s, ok := status.FromError(err)
		if !ok {
			log.Fatalf("err is not standard grpc error: %v", err)
		}
		fmt.Println(s.Code())
		//log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
}

server端:

这里返回的时候故意返回一个error看看效果

package main

import (
	"context"
	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"log"
	"net"

	pb "NewGo/error/rpc"
	"google.golang.org/grpc"
)

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))

	if err != nil {
		panic(err)
	}

	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}

// server is used to implement helloworld.GreeterServer.
type server struct {
	pb.UnimplementedGreeterServer
}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.GetName())
	return nil, status.Error(codes.NotFound, "user not found")
	//return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
	_ = tracerProvider()
	lis, err := net.Listen("tcp", ":50052")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer(
		grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
	)
	pb.RegisterGreeterServer(s, &server{})
	log.Printf("server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

效果:
【链路追踪】「Go语言」OpenTelemetry实现[gin, gRPC, log, gorm, redis]的集成_第5张图片


「可无视这一章节」

自用框架集成(无视即可)

gmicro中trace的option是为了和业务option隔离开
虽然代码会重复 但是以后想要改trace的代码就不会影响业务代码

Sampler: 1.0 # 采样率 如果在源码中有其他设置 则会使用最后一次设置的

使用_, ok := agents[o.Endpoint]确定唯一性 因为一般是不会换地址的

//log中可以不加 是否允许把TraceID放进来,这样就可以看清楚是属于哪一个链路的
//这个逻辑可以适用于 如果没有这个traceid 表示不上报到jaeger中 那我就打印到本地文件,自行改代码
if l.withTraceID {
traceID := span.SpanContext().TraceID().String()
fields = append(fields, zap.String(“trace_id”, traceID))
}


六、log集成

把自定义的log附着在span上:

自用的自定义的log包(源码在末尾):https://blog.csdn.net/the_shy_faker/article/details/129420308

只需要把spCtx, span := tr.Start(ctx, "func-a")中的spCtx传递到我用的log包中即可:

package main

import (
	"NewGo/log"
	"context"
	"encoding/json"
	"fmt"
	"github.com/valyala/fasthttp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	"sync"
	"time"
)

const (
	traceName = "mxshop-otel"
)

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}
	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//全局设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}
func funcA(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	tr := otel.Tracer(traceName)
	spCtx, span := tr.Start(ctx, "func-a")
	span.SetAttributes(attribute.String("name", "funA"))
	type _LogStruct struct {
		CurrentTime time.Time `json:"currentTime"`
		PassWho     string    `json:"passWho"`
		Name        string    `json:"name"`
	}
	logTest := _LogStruct{
		CurrentTime: time.Time{},
		PassWho:     "jzin",
		Name:        "func-a",
	}
	log.InfofC(spCtx, "is logs")
	b, _ := json.Marshal(logTest)
	log.InfofC(spCtx, string(b))
	span.SetAttributes(attribute.Key("测试key").String(string(b)))
	time.Sleep(time.Second)
	span.End()
}
func funcB(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	tr := otel.Tracer(traceName)
	spanCtx, span := tr.Start(ctx, "func-b")
	span.SetAttributes(attribute.String("name", "funB"))
	fmt.Println("trace", span.SpanContext().TraceID(), span.SpanContext().SpanID())
	time.Sleep(time.Second)

	req := fasthttp.AcquireRequest()
	req.SetRequestURI("http://127.0.0.1:8090/server")
	req.Header.SetMethod("GET")

	//拿到传播器
	p := otel.GetTextMapPropagator()
	//包裹   context信息注入到包裹里面 把trace的id span的id注入到包裹
	p.Inject(spanCtx, propagation.HeaderCarrier{})
	headers := make(map[string]string)
	//包裹   context信息注入到包裹里面 把trace的id span的id注入到包裹
	p.Inject(spanCtx, propagation.MapCarrier(headers))

	for key, value := range headers {
		req.Header.Set(key, value)
	}

	//req.Header.Set("trace-id", span.SpanContext().TraceID().String())
	//req.Header.Set("span-id", span.SpanContext().SpanID().String())

	fclient := fasthttp.Client{}
	fres := fasthttp.Response{}
	_ = fclient.Do(req, &fres)

	span.End()
}
func main() {
	_ = tracerProvider()
	ctx, cancel := context.WithCancel(context.Background())
	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(traceName)
	spanCtx, span := tr.Start(ctx, "func-main")
	wg := &sync.WaitGroup{}
	wg.Add(1)
	go funcA(spanCtx, wg)
	//go funcB(spanCtx, wg)
	//设置logs
	span.AddEvent("this is an event")
	time.Sleep(time.Second)
	wg.Wait()
	span.End()
}

由于gin的context和普通的context不一样
gin中真正的context是放在ctx.(*gin.Context).Request.Context()中的
如果传递的是gin中的context log包里会提取出context

效果:
【链路追踪】「Go语言」OpenTelemetry实现[gin, gRPC, log, gorm, redis]的集成_第6张图片


七、gorm集成

官方维护的集成telemetry源码:https://github.com/go-gorm/opentelemetry/tree/master/examples/demo

注意提前建好库
测试示例:

package main

import (
	"log"
	"os"
	"time"

	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	otelTrace "go.opentelemetry.io/otel/trace"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"
	"gorm.io/plugin/opentelemetry/tracing"
)

const (
	traceName = "mxshop-otel"
)

type BaseModel struct {
	ID        int32          `gorm:"primary_key;comment:ID"`
	CreatedAt time.Time      `gorm:"column:add_time;comment:创建时间"`
	UpdatedAt time.Time      `gorm:"column:update_time;comment:更新时间"`
	DeletedAt gorm.DeletedAt `gorm:"comment:删除时间"`
	IsDeleted bool           `gorm:"comment:是否删除"`
}
type User struct {
	BaseModel
	Mobile   string     `gorm:"index:idx_mobile;unique;type:varchar(11);not null;comment:手机号"`
	Password string     `gorm:"type:varchar(100);not null;comment:密码"`
	NickName string     `gorm:"type:varchar(20);comment:账号名称"`
	Birthday *time.Time `gorm:"type:datetime;comment:出生日期"`
	Gender   string     `gorm:"column:gender;default:male;type:varchar(6);comment:femail表示女,male表示男"`
	Role     int        `gorm:"column:role;default:1;type:int;comment:1表示普通用户,2表示管理员"`
}

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}

	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}

func Server2(c *gin.Context) {
	dsn := "root:123456@tcp(localhost:3306)/mxshop_user_srv2?charset=utf8mb4&parseTime=True&loc=Local"
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags),
		logger.Config{
			LogLevel: logger.Info,
			Colorful: true,
		},
	)
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
		},
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}
	//之前初始化好了*trace.TracerProvider这里就不用再初始化了
	if err := db.Use(tracing.NewPlugin()); err != nil {
		panic(err)
	}

	//负责span的抽取和生成
	//如果使用中间件otelgin.Middleware 它会把自己trace span,把context放到c.Request.Context()中
	//ctx := c.Request.Context()
	//p := otel.GetTextMapPropagator()
	//tr := tp.Tracer(traceName)
	//sCtx := p.Extract(ctx, propagation.HeaderCarrier(c.Request.Header))
	//spanCtx, span := tr.Start(sCtx, "server")

	if err := db.WithContext(c.Request.Context()).Model(User{BaseModel: BaseModel{ID: 12}}).First(&User{}).Error; err != nil {
		panic(err)
	}

	time.Sleep(500 * time.Millisecond)
	//span.End()
	c.JSON(200, gin.H{})

}

func main() {
	_ = tracerProvider()
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{})
	})
	r.GET("/server", Server2)
	err := r.Run(":8090")
	if err != nil {
		return
	}
}

效果:
【链路追踪】「Go语言」OpenTelemetry实现[gin, gRPC, log, gorm, redis]的集成_第7张图片


八、gin集成

官方维护的源码:https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/instrumentation/github.com

加一个官方维护的中间件即可:

package main

import (
	"log"
	"os"
	"time"

	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	otelTrace "go.opentelemetry.io/otel/trace"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"
	"gorm.io/plugin/opentelemetry/tracing"
)

const (
	traceName = "mxshop-otel"
)

type BaseModel struct {
	ID        int32          `gorm:"primary_key;comment:ID"`
	CreatedAt time.Time      `gorm:"column:add_time;comment:创建时间"`
	UpdatedAt time.Time      `gorm:"column:update_time;comment:更新时间"`
	DeletedAt gorm.DeletedAt `gorm:"comment:删除时间"`
	IsDeleted bool           `gorm:"comment:是否删除"`
}
type User struct {
	BaseModel
	Mobile   string     `gorm:"index:idx_mobile;unique;type:varchar(11);not null;comment:手机号"`
	Password string     `gorm:"type:varchar(100);not null;comment:密码"`
	NickName string     `gorm:"type:varchar(20);comment:账号名称"`
	Birthday *time.Time `gorm:"type:datetime;comment:出生日期"`
	Gender   string     `gorm:"column:gender;default:male;type:varchar(6);comment:femail表示女,male表示男"`
	Role     int        `gorm:"column:role;default:1;type:int;comment:1表示普通用户,2表示管理员"`
}

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}

	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}

func Server2(c *gin.Context) {
	dsn := "root:123456@tcp(localhost:3306)/mxshop_user_srv2?charset=utf8mb4&parseTime=True&loc=Local"
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags),
		logger.Config{
			LogLevel: logger.Info,
			Colorful: true,
		},
	)
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
		},
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}
	//之前初始化好了*trace.TracerProvider这里就不用再初始化了
	if err = db.Use(tracing.NewPlugin()); err != nil {
		panic(err)
	}

	//负责span的抽取和生成
	//如果使用中间件otelgin.Middleware 它会把自己trace span,把context放到c.Request.Context()中
	//ctx := c.Request.Context()
	//p := otel.GetTextMapPropagator()
	//tr := tp.Tracer(traceName)
	//sCtx := p.Extract(ctx, propagation.HeaderCarrier(c.Request.Header))
	//spanCtx, span := tr.Start(sCtx, "server")

	if err = db.WithContext(c.Request.Context()).Model(User{BaseModel: BaseModel{ID: 12}}).First(&User{}).Error; err != nil {
		panic(err)
	}

	time.Sleep(500 * time.Millisecond)
	//span.End()
	c.JSON(200, gin.H{})

}
func main() {
	_ = tracerProvider()
	r := gin.Default()
	//添加trace中间件
	r.Use(otelgin.Middleware("my-server"))
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{})
	})
	r.GET("/server", Server2)
	err := r.Run(":8090")
	if err != nil {
		return
	}
}

效果:
【链路追踪】「Go语言」OpenTelemetry实现[gin, gRPC, log, gorm, redis]的集成_第8张图片


九、redis集成

官方维护的redis源码:https://github.com/redis/go-redis/tree/master/extra/redisotel

官方文档里有说明 这里直接用:

package main

import (
	"context"
	"github.com/redis/go-redis/extra/redisotel/v9"
	"github.com/redis/go-redis/v9"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
)

const (
	traceName = "mxshop-otel"
)

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}

	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}

func main() {
	_ = tracerProvider()
	cli := redis.NewClient(&redis.Options{
		Addr: "127.0.0.1:端口",
	})
	// Enable tracing instrumentation.
	if err := redisotel.InstrumentTracing(cli); err != nil {
		panic(err)
	}
	tr := otel.Tracer(traceName)
	spanCtx, span := tr.Start(context.Background(), "redis")
	cli.Set(spanCtx, "name", "jzin", 0)
	span.End()
	err := tp.Shutdown(context.Background())
	if err != nil {
		return
	}
}

效果:

【链路追踪】「Go语言」OpenTelemetry实现[gin, gRPC, log, gorm, redis]的集成_第9张图片

你可能感兴趣的:(golang,OpenTelemetry,链路追踪,jaeger)