使用分布式链路追踪查看 sql 的执行情况

今天我们来讲一讲如何在 go 语言中使用 OpenTelemetry 链路追踪追踪 sql的执行情况(执行时间、语句等)。

初始化

我们这里需要有一个采用了数据库的项目,为了使用方便,我们这里采用了 ent 来进行数据库的操作。

初始化数据库实体

为了演示方便,我们这里简单定义一个 user 实体, user 有2个成员字段 id 和 name。

$ ent init User
package schema

import (
	"entgo.io/ent"
	"entgo.io/ent/schema/field"
)

// User holds the schema definition for the User entity.
type User struct {
	ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
	return []ent.Field{
		field.Int("id"),
		field.String("name"),
	}
}

// Edges of the User.
func (User) Edges() []ent.Edge {
	return nil
}

执行 go run -mod=mod entgo.io/ent/cmd/ent generate ./schema,就会生成一系列的文件。

初始化数据库连接

接下来我们初始化 mysql 的连接并简单的写了个创建 user 对象的函数。

package main

import (
	"context"
	"log"

	_ "github.com/go-sql-driver/mysql"
	"github.com/overstarry/ent-trace/ent"
)

func main() {
	client, err := ent.Open("mysql", "root:a12345@tcp(127.0.0.1:3306)/trace?parseTime=True")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	// Run the auto migration tool.
	if err := client.Schema.Create(context.Background()); err != nil {
		log.Fatalf("failed creating schema resources: %v", err)
	}
	createUser(context.Background(), client)
}

func createUser(ctx context.Context, client *ent.Client) {
	u, err := client.User.Create().
		SetName("Overstarry").
		SetID(1).Save(ctx)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("Created: %v", u)
}

初始化 OpenTelemetry

这里我们采用的 OpenTelemetry 后端 为 jaeger。 初始化的代码如下:

package main

import (
	"context"
	_ "github.com/go-sql-driver/mysql"
	"github.com/overstarry/ent-trace/ent"
	"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"
	tracesdk "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
	"log"
)

func tracerProvider(url string) (*tracesdk.TracerProvider, error) {
	// Create the Jaeger exporter
	exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		return nil, err
	}
	tp := tracesdk.NewTracerProvider(
		tracesdk.WithSampler(tracesdk.AlwaysSample()),
		tracesdk.WithBatcher(exp),
		tracesdk.WithResource(resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceNameKey.String("opentelemetry-ent-trace"), // 服务名
			semconv.ServiceVersionKey.String("0.0.1"),
			attribute.String("environment", "test"),
		)),
	)
	otel.SetTracerProvider(tp)
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return tp, nil
}

func main() {
	tp, err := tracerProvider("http://localhost:14268/api/traces")
	if err != nil {
		log.Fatal(err)
	}
	client, err := ent.Open("mysql", "root:a12345@tcp(127.0.0.1:3306)/trace?parseTime=True")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	// Run the auto migration tool.
	if err := client.Schema.Create(context.Background()); err != nil {
		log.Fatalf("failed creating schema resources: %v", err)
	}
	createUser(context.Background(), client)
}

func createUser(ctx context.Context, client *ent.Client) {
	u, err := client.User.Create().
		SetName("Overstarry").
		SetID(1).Save(ctx)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("Created: %v", u)
}

截至这里我们就完成了数据库的初始化和 trace 的初始化, 接下来我们将数据库接入 trace。

数据库接入 tracing

接下来我们将为数据库接入 tracing ,由于 opentelemetry 数据库 的相关规范不够完善,我寻找了很久,查看了相关的 issues 、 PR,终于找到了合适的模块 otelsql 来进行 sql 的链路追踪。

otelsql 的使用很简单,只要使用 otelsql 提供的函数注册相应的数据库驱动,再使用 sql 包连接相应的数据库即可。

但由于我们使用的 sql 包不是标准库 database/sql, 而是采用了 ent, ent 缺乏了对 database/sql 的集成,导致使用上有点困难,在 ent 的 issue(#1232) 有讨论过这个事情,ent 提供了 sql.DB 的支持,使我们的工作能够顺利进行。

这时完整的代码:

package main

import (
	"context"
	"database/sql"
	"log"
	"net/http"

	entsql "entgo.io/ent/dialect/sql"
	"github.com/XSAM/otelsql"
	_ "github.com/go-sql-driver/mysql"
	"github.com/overstarry/ent-trace/ent"
	"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"
	tracesdk "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)

func tracerProvider(url string) (*tracesdk.TracerProvider, error) {
	// Create the Jaeger exporter
	exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		return nil, err
	}
	tp := tracesdk.NewTracerProvider(
		tracesdk.WithSampler(tracesdk.AlwaysSample()),
		tracesdk.WithBatcher(exp),
		tracesdk.WithResource(resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceNameKey.String("opentelemetry-ent-trace"), // 服务名
			semconv.ServiceVersionKey.String("0.0.1"),
			attribute.String("environment", "test"),
		)),
	)
	otel.SetTracerProvider(tp)
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return tp, nil
}

func main() {
	_, err := tracerProvider("http://localhost:14268/api/traces")
	if err != nil {
		log.Fatal(err)
	}
	driverName, err := otelsql.Register("mysql", semconv.DBSystemMySQL.Value.AsString())
	if err != nil {
		panic(err)
	}

	// Connect to database
	db, err := sql.Open(driverName, "root:a12345@tcp(127.0.0.1:3306)/trace?parseTime=True")
	if err != nil {
		panic(err)
	}
	defer db.Close()
	drv := entsql.OpenDB("mysql", db)
	client := ent.NewClient(ent.Driver(drv))
	// Run the auto migration tool.
	if err := client.Schema.Create(context.Background()); err != nil {
		log.Fatalf("failed creating schema resources: %v", err)
	}
	createUser(client)
	http.ListenAndServe(":8080", nil)
}

func createUser(client *ent.Client) {
	tracer := otel.GetTracerProvider()
	ctx, span := tracer.Tracer("github.com/overstarry/ent-trace/example").Start(context.Background(), "example")
	defer span.End()

	u, err := client.User.Create().
		SetName("Overstarry").Save(ctx)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("Created: %v", u)
}

运行代码,我们打开 jaeger UI, 可以看到已经有了 sql 的 trace 情况。

img.png

总结

本篇文章讲述了在 sql 中接入tracing。

本文的代码在: https://github.com/overstarry/ent-trace-demo

参考