上一章节通过部署Tempo、Grafana Agent和Grafana完成链路跟踪整体服务的搭建。同时实现了一个Go程序Demo集成opentelemetry SDK完成链路的接入和验证,这一节将向大家介绍如何用Go语言集成opentelemetry SDK。
OpenTelemetry-Go
在opentelemetry官网中的接入指导中有Go语言集成一步步介绍如何使用一个基本的net/http应用程序集成openteletry。
代码中需要集成以下库,可以事先通过go get命令拉取。
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/semconv/v1.21.0"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
在OpenTelemetry-Go Githhub仓库有着更多的集成示例,大家也可以进行参考GO集成示例:https://github.com/open-telemetry/opentelemetry-go/tree/main/example
与官网使用基本的net/http应用程序集成openteletry指导不同,接下来讲解的Go Demo将会涉及到Web服务常用的基础库,如Gin、Gorm、go-redis如何集成opentelemetry,而有关可以用于扩展OpenTelemetry的库、插件、集成和其他有用工具大家可以在当前链接查找:https://opentelemetry.io/ecosystem/registry/?component=instrumentation&language=go
大家可以先花3到5分钟阅读opentelemetry官网的Go语言集成示例,可以更好的理解接下来讲解的Go Demo接入过程。
如何构建Trace和Span关系
在开始介绍Demo前大家有必要了解如何构建Trace和Span关系,一个完整的Trace由若干个Span构成,一个Span可以是一条调用链路中的函数的调用、数据库的操作或者微服务之间的调用等等。
创建TracerProvider
TracerProvider用于创建Trace,在TracerProvider中定义Trace数据如何输出,按照怎样的采样策略输出。在理解如何构建Trace和Span的关系时不考虑将Trace数据发送至后端服务,可以先默认使用控制台输出Trace和Span。采样策略也采用最常见的全采样方式
# 输出到控制台的Exporter
function DisplayWindowSize(){
exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
}
#全采样
tp := trace.NewTracerProvider(
trace.WithBatcher(exporter),
trace.WithSampler(trace.AlwaysSample()),
)
otel.SetTracerProvider(tp)
创建Trace和Span
Trace由若干Span构成,同时每个Span也可以拥有自己的子Span,一直嵌套下去。每个pan除了可以设置名称以外,还可以设置一些例如参数调用等标识性属性。
func doWork(ctx context.Context) {
tracer := otel.Tracer("my-package-name")
ctx, span := tracer.Start(ctx, "doWork")
span.SetAttributes(attribute.String("key", "value"))
defer span.End()
doSubWork(ctx)
}
func doSubWork(ctx context.Context, p1, p2 string) {
ctx, span := tracer.Start(ctx, "doSubWork")
span.SetAttributes(attribute.String("p1", p1))
span.SetAttributes(attribute.String("p2", p2 ))
defer span.End()
}
接下来我们将开始Go 程序集成Opentelemetry SDK的完整过程讲解。
初始化Opentelemetry
初始化Opentelemetry与上述与创建TracerProvider一样,只是Demo中会将Trace发送至链路后端,且需要设置应用的资源配置,例如服务名称、运行的Namespace、运行的示例名称(一般服务都会以多实例运行,标识是哪个实例)等信息。
构造Exporter
我们可以构造一个GRPC Exporter用于将指标数据发送至指定的后端服务,在服务的配置文件中需要对后端服务的GRPC地址进行配置。
func newGrpcExporterAndSpanProcessor(ctx context.Context) (*otlptrace.Exporter, sdktrace.SpanProcessor) {
traceExporter, err := otlptrace.New(
ctx,
otlptracegrpc.NewClient(
otlptracegrpc.WithInsecure(),
otlptracegrpc.WithEndpoint(GRPC_ENDPOINT),
otlptracegrpc.WithDialOption(grpc.WithBlock()),
otlptracegrpc.WithCompressor(gzip.Name)),
)
if err != nil {
log.Fatalf("%s: %v", "Failed to create the OpenTelemetry trace exporter", err)
}
batchSpanProcessor := sdktrace.NewBatchSpanProcessor(traceExporter)
return traceExporter, batchSpanProcessor
}
构造TracerProvider
在上面已经介绍了TracerProvider用于定义Trace数据如何输出,按照怎样的采样策略输出,同时也需要设置服务的资源信息,例如服务名称、命名空间等标识信息。
首先我们设置资源信息
// 设置应用资源
func newResource(ctx context.Context, serviceName string) *resource.Resource {
// hostname默认值为本机主机名
hostName, _ := os.Hostname()
namespace := os.Getenv("NAMESPACE")
r, err := resource.New(
ctx,
resource.WithFromEnv(),
resource.WithProcess(), // runtime信息 process.runtime.name: go/gc, process.runtime.version: go1.20.1s
resource.WithTelemetrySDK(),
resource.WithHost(),
resource.WithAttributes(
semconv.ServiceNameKey.String(serviceName),
semconv.HostNameKey.String(hostName),
semconv.K8SNamespaceNameKey.String(namespace),
),
resource.WithContainer(),
)
if err != nil {
log.Fatalf("%s: %v", "Failed to create OpenTelemetry resource", err)
}
return r
}
最后使用上述内容初始化Opentelemetry,同时设置采样策略为全采样方式。
func InitOpenTelemetry(serviceName string) {
ctx := context.Background()
traceExporter, batchSpanProcessor := newGrpcExporterAndSpanProcessor(ctx)
otelResource := newResource(ctx, serviceName)
traceProvider := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.AlwaysSample()),
sdktrace.WithResource(otelResource),
sdktrace.WithSpanProcessor(batchSpanProcessor))
otel.SetTracerProvider(traceProvider)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
}
最后在服务关闭前需要对资源进行释放
func CloseOpenTelemetry() {
cxt, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
if err := traceExporter.Shutdown(cxt); err != nil {
otel.Handle(err)
}
}
Gin扩展otelgin
在初始化Opentelemetry完成后,可以通过Opentelemetry提供的库扩展现有的常见使用库,使其具备产生Trace的功能。
对gin扩展的Opentelemetry库Github地址: go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin
使用otelgin扩展gin相较于原有使用方式只是增加一行中间件的注入:
r := gin.Default()
r.Use(otelgin.Middleware(serverName))
...
r.Run(":8080")
通过otelgin.Middleware的源码:github.com/gin-gonic/gin/otelgin/gintrace.go,我们可以看到先获取已经初始化完成的TracerProvider
cfg.TracerProvider = otel.GetTracerProvider()
然后将TracerProvider存到gin的Context中
c.Set(tracerKey, tracer)
根据一些属性构建span,属性包括请求类型、请求URL等。同时构建span返回的ctx继续会存储到Request的Context中,在完成htpp请求操作后,会关闭当前span,在context传递到接口实现时,可以一层层的传递下去,形成调用的嵌套span
ctx, span := tracer.Start(ctx, spanName, opts...)
defer span.End()
c.Request = c.Request.WithContext(ctx)
构建函数Span
在实现的Demo中,实现了一个/server1接口,该接口会调用server2,然后server2调用server3的调用拓扑。在sever1接口新建了一个以服务名称命名的span:
func server1(ctx *gin.Context) {
tr := otel.Tracer("/server1")
c, span := tr.Start(ctx.Request.Context(), serverName)
defer span.End()
log.WithContext(c).WithField("TraceID", span.SpanContext().TraceID()).Error("sending go-server2")
body, err := utils.SendReuqst(c, "/server2", "go-server2", "GET", "http://server2.go-optl-demo.svc.cluster.local:8080/server2")
if err != nil {
ctx.String(http.StatusInternalServerError, body)
return
}
ctx.String(http.StatusOK, body)
}
在Grafana中可以看到这个span及其详细属性:

可以看到在这个span开头有个红色感叹号标识,这是因为我们在代码中模拟输出了Error日志,点击events按钮查看输出的异常日志信息

Gorm扩展otelgorm
与gin集成类似,只需要增加一行配置插件
import (
_ "github.com/go-sql-driver/mysql"
"github.com/uptrace/opentelemetry-go-extra/otelgorm"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func InitDB() {
var err error
DB, err = gorm.Open(mysql.Open("..."), &gorm.Config{})
if err != nil {
panic(err)
}
if err := DB.Use(otelgorm.NewPlugin()); err != nil {
panic(err)
}
}
在使用Gorm进行数据库操作调用时,无需任何处理,在server3中模拟对数据库的查询操作在Trace中可以看到这个span,以及对应的SQL。

在otelgorm中配置插件实现的是注册不同类型的hook,在调用不同的Gorm库函数时,触发对应的hook构造span信息。
go-redis扩展redisotel
与otelgorm类似的hook实现,在使用go-redis的函数操作时会触发对应的hook完成span的构造。而go-redis扩展redisotel也只需要一行集成代码
var client *rdb.Client
...
if err := redisotel.InstrumentTracing(client ); err != nil {
panic(err)
}
在链路中可以看到redis的span,详情中会有具体的redis操作命令

HTTP Client
在Demo中记录各个服务之间的调用详情的span是手动启动的Span,其中属性添加了请求方法、请求URL等信息
func doSend(ctx context.Context, tr trace.Tracer, funcName, target, method, url string) (string, error) {
ctx, span := tr.Start(ctx, funcName, trace.WithAttributes(semconv.PeerService(target)))
defer span.End()
req, _ := http.NewRequestWithContext(ctx, method, url, nil)
fmt.Printf("Sending request...\n")
client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
res, err := client.Do(req)
if err != nil {
panic(err)
}
body, err := io.ReadAll(res.Body)
_ = res.Body.Close()
log.WithContext(ctx).WithField("TraceID", span.SpanContext().TraceID()).Info(string(body))
return string(body), err
}
详细的http调用span信息如下:

到这里就给大家介绍了使用Go集成OpentelemetrySDK的方法,同时带领大家了解了Gin、Gorm、Go-Redis库集成otel功能属性的示例,更多的代码示例可以参考Go集成OPTL示例:https://github.com/open-telemetry/opentelemetry-go/tree/main/example
往期回顾:
Grafana全家桶(三)使用Grafana Agent与Tempo实现链路追踪完整demo




