使用分布式链路追踪查看 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 情况。
总结
本篇文章讲述了在 sql 中接入tracing。
本文的代码在: https://github.com/overstarry/ent-trace-demo