OpenTelemetry 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.
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 UIjaeger-collectorjaeger-queryjaeger-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。

容器公開以下端口:

jCT0xk

我們這裏使用 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 數據。

Jaeger search

點擊右側的 trace 數據,即可查看詳情。

Jaeger trace

完整代碼請查看 https://github.com/Q1mi/dice

參考資料

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/NE2Rr8bIadKtjXpogrMh-Q