文章目录
- 一、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横空出世。
两个产品合并,首先要考虑的是什么?有过经验的同学都知道:如何让两边的用户能够继续使用。因此新项目首要核心目标就是兼容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 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()
}
效果:
OpenTelemetry系统架构
这其实是一个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()
}
效果
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)
}
}
效果:
「可无视这一章节」
自用框架集成(无视即可)
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
效果:
七、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
}
}
效果:
八、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
}
}
效果:
九、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
}
}
效果: