一、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横空出世。
两个产品合并,首先要考虑的是什么?有过经验的同学都知道:如何让两边的用户能够继续使用。因此新项目首要核心目标就是兼容OpenTracing和OpenCensus。
OpenTelemetry的核心工作目前主要集中在3个部分:
- 规范的制定和协议的统一,规范包含数据传输、API的规范,协议的统一包含:HTTP W3C的标准支持及GRPC等框架的协议标准
- 多语言SDK的实现和集成,用户可以使用SDK进行代码自动注入和手动埋点,同时对其他三方库(Log4j、LogBack等)进行集成支持;
- 数据收集系统的实现,当前是基于OpenCensus Service的收集系统,包括Agent和Collector。
由此可见,OpenTelemetry的自身定位很明确:数据采集和标准规范的统一,对于数据如何去使用、存储、展示、告警,官方是不涉及的,我们目前推荐使用Prometheus + Grafana做Metrics存储、展示,使用Jaeger做分布式跟踪的存储和展示。
大一统
有了以上的背景知识,我们就可以顶一下OpenTelemetry的终极目标了:实现Metrics、Tracing、Logging的融合及大一统,作为APM的数据采集终极解决方案。
- Tracing:提供了一个请求从接收到处理完成整个生命周期的跟踪路径,一次请求通常过经过N个系统,因此也被称为分布式链路追踪
- Metrics:例如cpu、请求延迟、用户访问数等Counter、Gauge、Histogram指标
- Logging:传统的日志,提供精确的系统记录
这三者的组合可以形成大一统的APM解决方案:
- 基于Metrics告警发现异常
- 通过Tracing定位到具体的系统和方法
- 根据模块的日志最终定位到错误详情和根源
- 调整Metrics等设置,更精确的告警/发现问题
二、OpenTelemetry快速体验
jaeger自行安装
go快速体验
package mainimport ("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"//专门生成Exporterjexp, 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,//设置servicesemconv.ServiceNameKey.String("mxshop-user"),//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的attribute.String("environment", "dev"),attribute.Int("ID", 1),),),)otel.SetTracerProvider(tp)//自己创建contextctx, 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)//生成tracertr := otel.Tracer("mxshop-otel")//生成span_, span := tr.Start(ctx, "func-main")//业务逻辑time.Sleep(time.Second)var attrs []attribute.KeyValueattrs = 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...)//设置logsspan.AddEvent("this is an event")//业务逻辑time.Sleep(time.Second)//结束此spanspan.End()
}
效果:
OpenTelemetry系统架构
OpenTelemetry-collector
官方提供的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 mainimport ("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.TracerProviderfunc 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,//设置servicesemconv.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)//设置logsspan.AddEvent("this is an event")time.Sleep(time.Second)wg.Wait()span.End()
}
效果
http传输 gin接收
client:
package mainimport ("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.TracerProviderfunc 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,//设置servicesemconv.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)//设置logsspan.AddEvent("this is an event")time.Sleep(time.Second)wg.Wait()span.End()
}
server(gin实现):
package mainimport ("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.TracerProviderfunc 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,//设置servicesemconv.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()//生成新的contextsCtx := p.Extract(ctx, propagation.HeaderCarrier(c.Request.Header))//拿到tracertr := 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))
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,})//手动构造ctxcarrier := 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 mainimport ("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.TracerProviderfunc 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,//设置servicesemconv.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 mainimport ("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.TracerProviderfunc 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,//设置servicesemconv.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)}
}
效果:
「可无视这一章节」
自用框架集成(无视即可)
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")
package mainimport ("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.TracerProviderfunc 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,//设置servicesemconv.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)//设置logsspan.AddEvent("this is an event")time.Sleep(time.Second)wg.Wait()span.End()
}
ctx.(*gin.Context).Request.Context()
效果:
七、gorm集成
官方维护的集成telemetry源码:https://github.com/go-gorm/opentelemetry/tree/master/examples/demo
注意提前建好库
package mainimport ("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 {BaseModelMobile 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.TracerProviderfunc 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,//设置servicesemconv.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}
}
效果:
八、gin集成
官方维护的源码:https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/instrumentation/github.com
加一个官方维护的中间件即可:
package mainimport ("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 {BaseModelMobile 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.TracerProviderfunc 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,//设置servicesemconv.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}
}
效果:
九、redis集成
官方维护的redis源码:https://github.com/redis/go-redis/tree/master/extra/redisotel
官方文档里有说明 这里直接用:
package mainimport ("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.TracerProviderfunc 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,//设置servicesemconv.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}
}
效果: