OpenTelemetry Go快速指南
承蒙大家厚爱,我的《Go语言之路》的纸质版图书已经上架京东,有需要的朋友请点击 此链接 购买。
本教程将演示如何在 Go 中使用 OpenTelemetry,我们将手写一个简单的应用程序,并向外发送链路追踪和指标数据。
准备示例应用程序
创建一个扔骰子的程序。
在本地新建一个dice目录,并进入该目录下。
mkdir dice
cd dice
执行 go mod 初始化。
go mod init dice
在同一目录下创建 main.go 文件,并添加以下代码。
package main
import (
"log"
"net/http"
)
func main() {
http.HandleFunc("/roll", roll)
log.Fatal(http.ListenAndServe(":8080", nil))
}
在同目录下另外创建一个名为 roll.go 的文件,并向该文件添加以下代码:
package main
import (
"fmt"
"math/rand"
"net/http"
)
func roll(w http.ResponseWriter, r *http.Request) {
number := 1 + rand.Intn(6)
_, _ = fmt.Fprintln(w, number)
}
使用以下命令构建并运行应用程序:
go run .
使用浏览器打开 http://127.0.0.1:8080/roll 确保程序能够正常运行。
添加 OpenTelemetry 测量仪器
接下来,我们将展示如何在示例应用程序中添加 OpenTelemetry 测量仪器。
引入依赖
在你的Go项目中安装以下依赖包。
go get "go.opentelemetry.io/otel" \
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" \
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace" \
"go.opentelemetry.io/otel/propagation" \
"go.opentelemetry.io/otel/sdk/metric" \
"go.opentelemetry.io/otel/sdk/resource" \
"go.opentelemetry.io/otel/sdk/trace" \
"go.opentelemetry.io/otel/semconv/v1.24.0" \
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
这里安装的是 OpenTelemety SDK 组件和 net/http 测量仪器。如果要对不同的库进行网络请求检测,则需要安装相应的仪器库。
初始化OpenTelemetry SDK
首先,我们将初始化OpenTelemetry SDK。任何想导出追踪数据的应用程序都必需完成这一步初始化。
新建一个otel.go文件,并在其中添加以下代码。
package main
import (
"context"
"errors"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/trace"
)
// setupOTelSDK 引导 OpenTelemetry pipeline。
// 如果没有返回错误,请确保调用 shutdown 进行适当清理。
func setupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, err error) {
var shutdownFuncs []func(context.Context) error
// shutdown 会调用通过 shutdownFuncs 注册的清理函数。
// 调用产生的错误会被合并。
// 每个注册的清理函数将被调用一次。
shutdown = func(ctx context.Context) error {
var err error
for _, fn := range shutdownFuncs {
err = errors.Join(err, fn(ctx))
}
shutdownFuncs = nil
return err
}
// handleErr 调用 shutdown 进行清理,并确保返回所有错误信息。
handleErr := func(inErr error) {
err = errors.Join(inErr, shutdown(ctx))
}
// 设置传播器
prop := newPropagator()
otel.SetTextMapPropagator(prop)
// 设置 trace provider.
tracerProvider, err := newTraceProvider()
if err != nil {
handleErr(err)
return
}
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
otel.SetTracerProvider(tracerProvider)
// 设置 meter provider.
meterProvider, err := newMeterProvider()
if err != nil {
handleErr(err)
return
}
shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
otel.SetMeterProvider(meterProvider)
return
}
func newPropagator() propagation.TextMapPropagator {
return propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
)
}
func newTraceProvider() (*trace.TracerProvider, error) {
traceExporter, err := stdouttrace.New(
stdouttrace.WithPrettyPrint())
if err != nil {
return nil, err
}
traceProvider := trace.NewTracerProvider(
trace.WithBatcher(traceExporter,
// 默认为 5s。为便于演示,设置为 1s。
trace.WithBatchTimeout(time.Second)),
)
return traceProvider, nil
}
func newMeterProvider() (*metric.MeterProvider, error) {
metricExporter, err := stdoutmetric.New()
if err != nil {
return nil, err
}
meterProvider := metric.NewMeterProvider(
metric.WithReader(metric.NewPeriodicReader(metricExporter,
// 默认为 1m。为便于演示,设置为 3s。
metric.WithInterval(3*time.Second))),
)
return meterProvider, nil
}
如果不使用 tracing ,则可以省略相应的 TracerProvider 的初始化代码;
如果不使用 metrics ,则可以省略 MeterProvider 的初始化代码。
测量 HTTP server
现在,我们已经初始化了OpenTelemetry SDK,可以测量HTTP服务器了。
按如下代码修改 main.go,加入设置 OpenTelemetry SDK 的代码,并使用 otelhttp 仪器库测量 HTTP 服务器:
package main
import (
"context"
"errors"
"log"
"net"
"net/http"
"os"
"os/signal"
"time"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func newHTTPHandler() http.Handler {
mux := http.NewServeMux()
// handleFunc 是 mux.HandleFunc 的替代品,。
// 它使用 http.route 模式丰富了 handler 的 HTTP 测量
handleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) {
// 为 HTTP 测量配置 "http.route"。
handler := otelhttp.WithRouteTag(pattern, http.HandlerFunc(handlerFunc))
mux.Handle(pattern, handler)
}
// Register handlers.
handleFunc("/roll", roll)
// 为整个服务器添加 HTTP 测量。
handler := otelhttp.NewHandler(mux, "/")
return handler
}
func main() {
if err := run(); err != nil {
log.Fatalln(err)
}
}
func run() (err error) {
// 平滑处理 SIGINT (CTRL+C) .
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
// 设置 OpenTelemetry.
otelShutdown, err := setupOTelSDK(ctx)
if err != nil {
return
}
// 妥善处理停机,确保无泄漏
defer func() {
err = errors.Join(err, otelShutdown(context.Background()))
}()
// 启动 HTTP server.
srv := &http.Server{
Addr: ":8080",
BaseContext: func(_ net.Listener) context.Context { return ctx },
ReadTimeout: time.Second,
WriteTimeout: 10 * time.Second,
Handler: newHTTPHandler(),
}
srvErr := make(chan error, 1)
go func() {
srvErr <- srv.ListenAndServe()
}()
// 等待中断.
select {
case err = <-srvErr:
// 启动 HTTP 服务器时出错.
return
case <-ctx.Done():
// 等待第一个 CTRL+C.
// 尽快停止接收信号通知.
stop()
}
// 调用 Shutdown 时,ListenAndServe 会立即返回 ErrServerClosed。
err = srv.Shutdown(context.Background())
return
}
添加自定义测量
测量库可以捕捉系统边缘的遥测数据,例如入站和出站 HTTP 请求,但无法捕捉应用程序中的情况。因此需要编写一些自定义的手动仪器。
修改 roll.go,使用 OpenTelemetry API 包含定制的测量仪器:
package main
import (
"fmt"
"math/rand"
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
)
var (
tracer = otel.Tracer("roll")
meter = otel.Meter("roll")
rollCnt metric.Int64Counter
)
func init() {
var err error
rollCnt, err = meter.Int64Counter("dice.rolls",
metric.WithDescription("The number of rolls by roll value"),
metric.WithUnit("{roll}"))
if err != nil {
panic(err)
}
}
func roll(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "roll") // 开始 span
defer span.End() // 结束 span
number := 1 + rand.Intn(6)
rollValueAttr := attribute.Int("roll.value", number)
span.SetAttributes(rollValueAttr) // span 添加属性
// 摇骰子次数的指标 +1
rollCnt.Add(ctx, 1, metric.WithAttributes(rollValueAttr))
_, _ = fmt.Fprintln(w, number)
}
运行应用程序
使用以下命令构建并运行应用程序:
go mod tidy
export OTEL_RESOURCE_ATTRIBUTES="service.name=dice,service.version=0.1.0"
go run .
使用浏览器中打开 http://127.0.0.1:8080/roll。向服务器发送请求时,你会在控制台显示的链路跟踪中看到两个 span。由仪器库生成的 span 跟踪向 /roll 路由发出请求的生命周期。名为 roll 的 span 是手动创建的,它是前面提到的 span 的子 span。
将链路追踪数据发送至 Jaeger
如果觉着控制台看的 span 不够直观,可以选择将链路追踪的数据发送至 Jaeger,通过 Jaeger UI 查看。
启动 Jaeger
Jaeger 官方提供的 all-in-one 是为快速本地测试而设计的可执行文件。它包括 Jaeger UI、jaeger-collector、jaeger-query 和 jaeger-agent,以及一个内存存储组件。
启动 all-in-one 的最简单方法是使用发布到 DockerHub 的预置镜像(只需一条命令行)。
docker run --rm --name jaeger \
-e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
-p 14250:14250 \
-p 14268:14268 \
-p 14269:14269 \
-p 9411:9411 \
jaegertracing/all-in-one:1.55
然后你可以使用浏览器打开 http://localhost:16686 访问Jaeger UI。
容器公开以下端口:
| Port | Protocol | Component | Function |
|---|---|---|---|
| 6831 | UDP | agent | accept jaeger.thrift over Thrift-compact protocol (used by most SDKs) |
| 6832 | UDP | agent | accept jaeger.thrift over Thrift-binary protocol (used by Node.js SDK) |
| 5775 | UDP | agent | (deprecated) accept zipkin.thrift over compact Thrift protocol (used by legacy clients only) |
| 5778 | HTTP | agent | serve configs (sampling, etc.) |
| 16686 | HTTP | query | serve frontend |
| 4317 | HTTP | collector | accept OpenTelemetry Protocol (OTLP) over gRPC |
| 4318 | HTTP | collector | accept OpenTelemetry Protocol (OTLP) over HTTP |
| 14268 | HTTP | collector | accept jaeger.thrift directly from clients |
| 14250 | HTTP | collector | accept model.proto |
| 9411 | HTTP | collector | Zipkin compatible endpoint (optional) |
我们这里使用 HTTP 协议的4318 端口上报链路追踪数据。
上报至 Jaeger
安装 otlptracehttp 依赖包。
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
修改otel.go 代码,新增以下函数。
func newJaegerTraceProvider(ctx context.Context) (*trace.TracerProvider, error) {
// 创建一个使用 HTTP 协议连接本机Jaeger的 Exporter
traceExporter, err := otlptracehttp.New(ctx,
otlptracehttp.WithEndpoint("127.0.0.1:4318"),
otlptracehttp.WithInsecure())
if err != nil {
return nil, err
}
traceProvider := trace.NewTracerProvider(
trace.WithBatcher(traceExporter,
// 默认为 5s。为便于演示,设置为 1s。
trace.WithBatchTimeout(time.Second)),
)
return traceProvider, nil
}
并且按如下代码修改设置 trace provider 部分。
// 设置 trace provider.
//tracerProvider, err := newTraceProvider()
tracerProvider, err := newJaegerTraceProvider(ctx)
if err != nil {
handleErr(err)
return
}
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
otel.SetTracerProvider(tracerProvider)
再次构建并启动程序。
go run .
尝试访问一次 http://127.0.0.1:8080/roll ,确保修改后的服务能够正常运行。
使用 Jaeger UI
使用浏览器打开 http://127.0.0.1:16686 的Jaeger UI界面。 在屏幕左侧的 service 下拉框中选中 dice后查找,即可看到上报的 trace 数据。

点击右侧的 trace 数据,即可查看详情。

完整代码请查看 https://github.com/Q1mi/dice
更多关于 Jaeger 的内容请看 Jaeger快速指南。
参考资料
- https://opentelemetry.io/docs/languages/go/getting-started/
- https://www.jaegertracing.io/docs/1.55/getting-started/
其他常用库的OTel相关内容






