otelsql 入门指南:Go SQL 的 OpenTelemetry 监控
博客文章在发布后不会更新。这篇文章已经发布一年多了,其内容可能已过时,部分链接可能无效。在依赖任何信息之前,请务必核实。
otelsql 是 Go 编程语言中 database/sql 库的一个监控库。它在应用程序与数据库交互时生成跟踪(traces)和指标(metrics)。通过这样做,该库可以帮助您识别 SQL 查询中可能影响应用程序性能的错误或延迟。
让我们深入了解如何使用这个库!
入门
otelsql 是 database/sql 接口的一个包装层。当用户使用经过包装的数据库接口时,otelsql 会生成遥测数据,并将操作传递给底层数据库。
在接下来的示例中,您将使用 Docker Compose 来运行 otelsql 存储库中的 otel-collector 示例。此示例使用带有 otelsql 监控的 MySQL 客户端。生成的遥测数据将推送到 OpenTelemetry Collector。然后,它会在 Jaeger 上显示跟踪数据,在 Prometheus 服务器上显示指标数据。
数据流如下
flowchart LR;
A[MySQL client]-->B[OpenTelemetry Collector];
B-->C["Jaeger (trace)"];
B-->D["Prometheus (metrics)"];让我们克隆 otelsql 存储库在这里,运行示例,并查看最重要的几行代码。
git clone https://github.com/XSAM/otelsql.git
在 otelsql 文件夹中,您还可以检查 git 标签到 v0.29.0(撰写此文时最新的标签),以确保示例是可运行的,因为运行示例的步骤将来可能会有所更改。
git checkout tags/v0.29.0
让我们进入 otel-collector 示例文件夹并启动所有服务。
cd example/otel-collector
docker compose up -d
在构建映像并运行服务后,让我们检查服务日志,以确保 SQL 客户端已完成。
docker compose logs client
然后,我们可以访问位于 localhost:16686 的 Jaeger UI 和位于 localhost:9090 的 Prometheus UI 来查看结果。
这里我们在 Jaeger 上查看跟踪图。我们可以看到与数据库的每次操作的持续时间和参数。

这里我们在 Prometheus 上查看指标 db_sql_latency_milliseconds_sum。

更多 otelsql 生成的指标选项可以在 otelsql 文档中找到。
理解示例
让我们先看一下 docker-compose.yaml 文件。
version: '3.9'
services:
mysql:
image: mysql:8.3
environment:
- MYSQL_ROOT_PASSWORD=otel_password
- MYSQL_DATABASE=db
healthcheck:
test:
mysqladmin ping -h 127.0.0.1 -u root --password=$$MYSQL_ROOT_PASSWORD
start_period: 5s
interval: 5s
timeout: 5s
retries: 10
otel-collector:
image: otel/opentelemetry-collector-contrib:0.91.0
command: ['--config=/etc/otel-collector.yaml']
volumes:
- ./otel-collector.yaml:/etc/otel-collector.yaml
depends_on:
- jaeger
prometheus:
image: prom/prometheus:v2.45.2
volumes:
- ./prometheus.yaml:/etc/prometheus/prometheus.yml
ports:
- 9090:9090
depends_on:
- otel-collector
jaeger:
image: jaegertracing/all-in-one:1.52
ports:
- 16686:16686
client:
build:
dockerfile: $PWD/Dockerfile
context: ../..
depends_on:
mysql:
condition: service_healthy
此 Docker Compose 文件包含五个服务。client 服务是由 Dockerfile 构建的 MySQL 客户端,其源代码是示例文件夹中的 main.go。client 服务在 mysql 服务启动后运行。然后,它初始化 OpenTelemetry 客户端和 otelsql 监控,向 mysql 服务发出 SQL 查询,并通过 OpenTelemetry Protocol (OTLP) 将指标和跟踪数据发送到 otel-collector 服务。
接收到数据后,otel-collector 服务会转换数据格式,并将指标数据发送到 prometheus 服务,将跟踪数据发送到 jaeger 服务。
让我们检查 main.go,看看 client 服务中发生了什么。这是 main 函数。
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
conn, err := initConn(ctx)
if err != nil {
log.Fatal(err)
}
shutdownTracerProvider, err := initTracerProvider(ctx, conn)
if err != nil {
log.Fatal(err)
}
defer func() {
if err := shutdownTracerProvider(ctx); err != nil {
log.Fatalf("failed to shutdown TracerProvider: %s", err)
}
}()
shutdownMeterProvider, err := initMeterProvider(ctx, conn)
if err != nil {
log.Fatal(err)
}
defer func() {
if err := shutdownMeterProvider(ctx); err != nil {
log.Fatalf("failed to shutdown MeterProvider: %s", err)
}
}()
db := connectDB()
defer db.Close()
err = runSQLQuery(ctx, db)
if err != nil {
log.Fatal(err)
}
fmt.Println("Example finished")
}
这个 main 函数非常直接。它与 otel-collector 服务初始化连接,该连接由 tracer provider 和 meter provider 使用。然后,它使用 connection 和一个 shutdown 方法来配置 tracer provider 和 meter provider,这确保了遥测数据可以在应用程序退出前正确推送到 otel-collector 服务。完成 OpenTelemetry 客户端设置后,它调用 connectDB 方法来使用 otelsql 库与 MySQL 数据库进行交互。让我们看看这里的细节。
func connectDB() *sql.DB {
// Connect to database
db, err := otelsql.Open("mysql", mysqlDSN, otelsql.WithAttributes(
semconv.DBSystemMySQL,
))
if err != nil {
log.Fatal(err)
}
// Register DB stats to meter
err = otelsql.RegisterDBStatsMetrics(db, otelsql.WithAttributes(
semconv.DBSystemMySQL,
))
if err != nil {
log.Fatal(err)
}
return db
}
我们没有使用 Go 提供的 sql.Open 方法,而是使用 otelsql.Open 来创建一个 sql.DB 实例。otelsql.Open 返回的 sql.DB 实例是一个包装器,它将所有 DB 操作传递并监控给底层 sql.DB 实例(由 sql.Open 创建)。当用户使用此包装器发送 SQL 查询时,otelsql 可以看到查询并使用 OpenTelemetry 客户端生成遥测数据。
除了使用 otelsql.Open,otelsql 还提供了三种额外的初始化监控的方法:otelsql.OpenDB、otelsql.Register 和 otelsql.WrapDriver。这些额外的方法涵盖了不同的用例,因为有些数据库驱动程序或框架不提供直接创建 sql.DB 的方式。有时,您可能需要这些额外的方法来手动创建 sql.DB 并将其推送到那些数据库驱动程序。您可以查看 otelsql 文档中的示例来了解如何使用这些方法。
接着,我们使用 otelsql.RegisterDBStatsMetrics 来注册 sql.DBStats 中的指标数据。指标记录过程在后台运行,并在注册后根据需要更新指标的值,因此我们无需担心为此创建单独的线程。
在拥有一个由 otelsql 包装的 sql.DB 后,我们就可以使用它来执行查询。
func runSQLQuery(ctx context.Context, db *sql.DB) error {
// Create a parent span (Optional)
tracer := otel.GetTracerProvider()
ctx, span := tracer.Tracer(instrumentationName).Start(ctx, "example")
defer span.End()
err := query(ctx, db)
if err != nil {
span.RecordError(err)
return err
}
return nil
}
func query(ctx context.Context, db *sql.DB) error {
// Make a query
rows, err := db.QueryContext(ctx, `SELECT CURRENT_TIMESTAMP`)
if err != nil {
return err
}
defer rows.Close()
var currentTime time.Time
for rows.Next() {
err = rows.Scan(¤tTime)
if err != nil {
return err
}
}
fmt.Println(currentTime)
return nil
}
这个 runSQLQuery 方法首先创建一个父 Span(这是一个可选步骤,它使查询 Span 具有父级,并且在跟踪图中看起来更好),然后从 MySQL 数据库查询当前时间戳。
在此方法之后,client 应用程序完成并退出。这些是理解示例最重要的几行代码。
将示例用作游乐场
在理解了示例之后,我们可以将其用作游乐场,使其变得稍微复杂一些,以了解它在实际场景中的用法。
使用以下代码替换示例中的 runSQLQuery 方法。
func runSQLQuery(ctx context.Context, db *sql.DB) error {
// Create a parent span (Optional)
tracer := otel.GetTracerProvider()
ctx, span := tracer.Tracer(instrumentationName).Start(ctx, "example")
defer span.End()
runSlowSQLQuery(ctx, db)
err := query(ctx, db)
if err != nil {
span.RecordError(err)
return err
}
return nil
}
func runSlowSQLQuery(ctx context.Context, db *sql.DB) {
db.QueryContext(ctx, `SELECT SLEEP(1)`)
}
这次,我们在示例中添加了一个新的查询,这是一个慢查询,需要 1 秒才能返回。让我们看看会发生什么,以及如何识别这个慢查询。
为了使此更改生效,我们需要重新构建 client 服务。
docker compose build client
docker compose up client
在客户端完成后,我们可以查看 Jaeger 上我们刚刚生成的跟踪的跟踪图。

从这张图中,我们知道整个示例需要 1 秒才能完成。导致此延迟的根本原因与与数据库的网络延迟和时间戳查询无关。是 SELECT SLEEP(1) 查询导致了延迟。
您还可以通过指标中的数据库聚合统计数据来了解延迟。这就是 otelsql 可以提供的可观测性,因此您可以了解您的应用程序正在对数据库做什么。
兼容性
您可能担心与其他数据库以及其他第三方数据库框架(如 ORM)的兼容性问题,并想知道此监控的可用性有多广泛。
从实现的角度来看,只要数据库驱动程序或数据库框架通过 database/sql 结合 context 与数据库(任何数据库,不只是 SQL 数据库)进行交互,otelsql 应该都能正常工作。
这是一个 示例,展示了 otelsql 如何与 Facebook 的 Go 实体框架一起使用。
其他炫酷功能
现在您已经体验了主要功能,让我们花点时间探索 otelsql 提供的其他炫酷功能。
Sqlcommenter 支持
otelsql 集成了 Sqlcommenter,这是一个开源的 ORM 自动监控库,通过将注释注入 SQL 语句来与 OpenTelemetry 合并,从而实现数据库的上下文传播。
使用 WithSQLCommenter 选项,otelsql 为它监控的每个 SQL 语句注入一个注释。
例如,发送到数据库的 SQL 查询
SELECT * from FOO
变成
SELECT * from FOO /*traceparent='00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',tracestate='congo%3Dt61rcWkgMzE%2Crojo%3D00f067aa0ba902b7'*/
然后,支持 Sqlcommenter 的数据库可以记录此查询的某个特定跟踪的操作,并将其跟踪 Span 发布到跟踪存储,这样您就可以在一个地方看到您的应用程序跟踪 Span 与来自数据库的查询跟踪 Span 相关联。

图片来自 google cloud 文档。
自定义 Span 名称
如果您不喜欢默认的 Span 名称,可以使用 otelsql.WithSpanNameFormatter 来自定义 Span 名称。
这是示例用法
otelsql.WithSpanNameFormatter(func(ctx context.Context, method otelsql.Method, query string) string {
return string(method) + ": " + query
})
然后,Span 名称可以变成 {method}: {query}。下面是一个 Span 名称的示例
sql.conn.query: select current_timestamp
过滤 Span
您可以使用 otelsql.SpanOptions 中的 otelsql.SpanFilter 来过滤掉您不想生成的 Span。当您想丢弃某些 Span 时,这很有用。
下一步?
您现在应该能够将从这篇博文中学习到的内容应用到您自己的 otelsql 安装中了。
我很想听听您的经验!如果您觉得 otelsql 有帮助,请给它点星!如果您遇到任何问题,请随时 联系我们 或 创建问题。