使用 OpenTelemetry 實現 Golang 服務的可觀測系統

這篇文章中我們會討論可觀測性概念,並瞭解了有關 OpenTelemetry 的一些細節,然後會在 Golang 服務中對接 OpenTelemetry 實現分佈式系統可觀測性。

Test Project

我們將使用 Go 1.22 開發我們的測試服務。我們將構建一個 API,返回服務的名稱及其版本。

我們將把我們的項目分成兩個簡單的文件(main.go 和 info.go)。

// file: main.go

package main

import (
   "log"
   "net/http"
)

const portNum string = ":8080"

func main() {
   log.Println("Starting http server.")

   mux := http.NewServeMux()
   mux.HandleFunc("/info", info)

   srv := &http.Server{
      Addr:    portNum,
      Handler: mux,
   }

   log.Println("Started on port", portNum)
   err := srv.ListenAndServe()
   if err != nil {
      log.Println("Fail start http server.")
   }

}
// file: info.go

package main

import (
   "encoding/json"
   "net/http"
)

type InfoResponse struct {
   Version     string `json:"version"`
   ServiceName string `json:"service-name"`
}

func info(w http.ResponseWriter, r *http.Request) {
   w.Header().Set("Content-Type""application/json")
   response := InfoResponse{Version: "0.1.0", ServiceName: "otlp-sample"}
   json.NewEncoder(w).Encode(response)
}

使用 go run . 運行後,應該在 console 中輸出:

Starting http server.
Started on port :8080

訪問 localhost:8080 會顯示:

// http://localhost:8080/info
{
  "version""0.1.0",
  "service-name""otlp-sample"
}

現在我們的服務已經可以運行了,現在要以對其進行監控(或者配置我們的流水線)。在這裏,我們將執行手動監控以理解一些觀測細節。

First Steps

第一步是安裝 Open Telemetry 的依賴。

go get "go.opentelemetry.io/otel" \
       "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" \
       "go.opentelemetry.io/otel/metric" \
       "go.opentelemetry.io/otel/sdk" \
       "go.opentelemetry.io/otel/trace" \
       "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"

目前,我們只會安裝項目的初始依賴。這裏我們將 OpenTelemetry 配置 otel.go 文件。

在我們開始之前,先看下配置的流水線:

定義 Exporter

爲了演示簡單,我們將在這裏使用 console Exporter 。

// file: otel.go

package main

import (
   "context"
   "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
   "go.opentelemetry.io/otel/sdk/trace"
)

func newTraceExporter() (trace.SpanExporter, error) {
   return stdouttrace.New(stdouttrace.WithPrettyPrint())
}

main.go 的代碼如下:

// file: main.go

package main


import (
   "context"
   "log"
   "net/http"
)

const portNum string = ":8080"

func main() {
   log.Println("Starting http server.")

   mux := http.NewServeMux()

   _, err := newTraceExporter()
   if err != nil {
      log.Println("Failed to get console exporter.")
   }

   mux.HandleFunc("/info", info)

   srv := &http.Server{
      Addr:    portNum,
      Handler: mux,
   }

   log.Println("Started on port", portNum)
   err := srv.ListenAndServe()
   if err != nil {
      log.Println("Fail start http server.")
   }

}

Trace

我們的首個信號將是 Trace。爲了與這個信號互動,我們必須創建一個 provider,如下所示。作爲一個參數,我們將擁有一個 Exporter,它將接收收集到的信息。

// file: otel.go

package main

import (
   "context"
   "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
   "go.opentelemetry.io/otel/sdk/trace"
   "time"
)

func newTraceExporter() (trace.SpanExporter, error) {
   return stdouttrace.New(stdouttrace.WithPrettyPrint())
}

func newTraceProvider(traceExporter trace.SpanExporter) *trace.TracerProvider {
   traceProvider := trace.NewTracerProvider(
      trace.WithBatcher(traceExporter,
         trace.WithBatchTimeout(time.Second)),
   )
   return traceProvider
}

在 main.go 文件中,我們將使用創建跟蹤提供程序的函數。

// file: main.go

package main


import (
   "context"
   "go.opentelemetry.io/otel"
   "log"
   "net/http"
)

const portNum string = ":8080"

func main() {
   log.Println("Starting http server.")

   mux := http.NewServeMux()
   ctx := context.Background()

   consoleTraceExporter, err := newTraceExporter()
   if err != nil {
      log.Println("Failed get console exporter.")
   }

   tracerProvider := newTraceProvider(consoleTraceExporter)

   defer tracerProvider.Shutdown(ctx)
   otel.SetTracerProvider(tracerProvider)

   mux.HandleFunc("/info", info)

   srv := &http.Server{
      Addr:    portNum,
      Handler: mux,
   }

   log.Println("Started on port", portNum)
   err = srv.ListenAndServe()
   if err != nil {
      log.Println("Fail start http server.")
   }

}

請注意,在實例化一個 provider 時,我們必須保證它會 “關閉”。這樣可以避免內存泄露。

現在我們的服務已經配置了一個 trace provider,我們準備好收集數據了。讓我們調用 “/info” 接口來產生數據。

// file: info.go

package main

import (
   "encoding/json"
   "go.opentelemetry.io/otel"
   "net/http"
)

type InfoResponse struct {
   Version     string `json:"version"`
   ServiceName string `json:"service-name"`
}

var (
   tracer = otel.Tracer("info-service")
)

func info(w http.ResponseWriter, r *http.Request) {
   _, span := tracer.Start(r.Context()"info")
   defer span.End()

   w.Header().Set("Content-Type""application/json")
   response := InfoResponse{Version: "0.1.0", ServiceName: "otlp-sample"}
   json.NewEncoder(w).Encode(response)
}

tracer = otel.Tracer(“info-service”) 將在我們已經在 main.go 中註冊的全局 trace provider 中創建一個命名的跟蹤器。如果未提供名稱,則將使用默認名稱。

tracer.Start(r.Context(), “info”) 創建一個 Span 和一個包含新創建的 spancontext.Context。如果 "ctx" 中提供的 context.Context 包含一個 Span,那麼新創建的 Span 將是該 Span 的子 Span,否則它將是根 Span

Span 對我們來說是一個新的概念。Span 代表一個工作單元或操作。Span 是跟蹤(Traces)的構建塊。

同樣地,正如提供程序一樣,我們必須始終關閉 Spans 以避免 “內存泄漏”。

現在,我們的端點已經被監控,我們可以在控制檯中查看我們的觀測數據。

{
 "Name":"info",
 "SpanContext":{
   "TraceID":"6216cbe99bfd1165974dc2bda24e0d5c",
   "SpanID":"728454ee6b9a72e3",
   "TraceFlags":"01",
   "TraceState":"",
   "Remote":false
 },
 "Parent":{
   "TraceID":"00000000000000000000000000000000",
   "SpanID":"0000000000000000",
   "TraceFlags":"00",
   "TraceState":"",
   "Remote":false
 },
 "SpanKind":1,
 "StartTime":"2024-03-02T23:39:51.791979-03:00",
 "EndTime":"2024-03-02T23:39:51.792140908-03:00",
 "Attributes":null,
 "Events":null,
 "Links":null,
 "Status":{
   "Code":"Unset",
   "Description":""
 },
 "DroppedAttributes":0,
 "DroppedEvents":0,
 "DroppedLinks":0,
 "ChildSpanCount":0,
 "Resource":[
   {
     "Key":"service.name",
     "Value":{
       "Type":"STRING",
       "Value":"unknown_service:otlp-golang"
     }
   },
   {
     "Key":"telemetry.sdk.language",
     "Value":{
       "Type":"STRING",
       "Value":"go"
     }
   },
   {
     "Key":"telemetry.sdk.name",
     "Value":{
       "Type":"STRING",
       "Value":"opentelemetry"
     }
   },
   {
     "Key":"telemetry.sdk.version",
     "Value":{
       "Type":"STRING",
       "Value":"1.24.0"
     }
   }
 ],
 "InstrumentationLibrary":{
   "Name":"info-service",
   "Version":"",
   "SchemaURL":""
 }
}

添加 Metrics

我們已經有了我們的 tracing 配置。現在來添加我們的第一個指標。

首先,安裝並配置一個專門用於指標的導出器。

go get "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"

通過修改我們的 otel.go 文件,我們將有兩個導出器:一個專門用於 tracing,另一個用於 metrics。

// file: otel.go

func newTraceExporter() (trace.SpanExporter, error) {
   return stdouttrace.New(stdouttrace.WithPrettyPrint())
}

func newMetricExporter() (metric.Exporter, error) {
   return stdoutmetric.New()
}

現在添加我們的 metrics Provider 實例化:

// file: otel.go

func newMeterProvider(meterExporter metric.Exporter) *metric.MeterProvider {
   meterProvider := metric.NewMeterProvider(
      metric.WithReader(metric.NewPeriodicReader(meterExporter,
         metric.WithInterval(10*time.Second))),
   )
   return meterProvider
}

我將提供商的行爲更改爲每 10 秒進行一次定期讀取(默認爲 1 分鐘)。

在實例化一個 MeterProvide r 時,我們將創建一個 Meter。Meters 允許您創建您可以使用的儀器,以創建不同類型的指標(計數器、異步計數器、直方圖、異步儀表、增減計數器、異步增減計數器……)。

現在我們可以在 main.go 中配置我們的新 exporter 和 provider。

// file: main.go

func main() {
   log.Println("Starting http server.")

   mux := http.NewServeMux()
   ctx := context.Background()

   consoleTraceExporter, err := newTraceExporter()
   if err != nil {
      log.Println("Failed get console exporter (trace).")
   }

   consoleMetricExporter, err := newMetricExporter()
   if err != nil {
      log.Println("Failed get console exporter (metric).")
   }

   tracerProvider := newTraceProvider(consoleTraceExporter)

   defer tracerProvider.Shutdown(ctx)
   otel.SetTracerProvider(tracerProvider)

   meterProvider := newMeterProvider(consoleMetricExporter)

   defer meterProvider.Shutdown(ctx)
   otel.SetMeterProvider(meterProvider)

   mux.HandleFunc("/info", info)

   srv := &http.Server{
      Addr:    portNum,
      Handler: mux,
   }

   log.Println("Started on port", portNum)
   err = srv.ListenAndServe()
   if err != nil {
      log.Println("Fail start http server.")
   }
}

最後,讓我們測量我們想要的數據。我們將在 info.go 中做這件事,這與我們之前在 trace 中所做的非常相似。

我們將使用 otel.Meter("info-service") 在已經註冊的全局提供者上創建一個命名的計量器。我們還將通過 metric.Int64Counter 定義我們的測量工具。Int64Counter 是一種記錄遞增的 int64 值的工具。

然而,與 trace 不同,我們需要初始化我們的測量工具。我們將爲我們的度量配置名稱、描述和單位。

// file: info.go

var (
   tracer      = otel.Tracer("info-service")
   meter       = otel.Meter("info-service")
   viewCounter metric.Int64Counter
)

func init() {
   var err error
   viewCounter, err = meter.Int64Counter("user.views",
      metric.WithDescription("The number of views"),
      metric.WithUnit("{views}"))
   if err != nil {
      panic(err)
   }
}

一旦完成這個步驟,我們就可以開始測量了。最終代碼看起來會像這樣:

// file: info.go

package main

import (
   "encoding/json"
   "go.opentelemetry.io/otel"
   "go.opentelemetry.io/otel/metric"
   "net/http"
)

type InfoResponse struct {
   Version     string `json:"version"`
   ServiceName string `json:"service-name"`
}

var (
   tracer      = otel.Tracer("info-service")
   meter       = otel.Meter("info-service")
   viewCounter metric.Int64Counter
)

func init() {
   var err error
   viewCounter, err = meter.Int64Counter("user.views",
      metric.WithDescription("The number of views"),
      metric.WithUnit("{views}"))
   if err != nil {
      panic(err)
   }
}

func info(w http.ResponseWriter, r *http.Request) {
   ctx, span := tracer.Start(r.Context()"info")
   defer span.End()

   viewCounter.Add(ctx, 1)

   w.Header().Set("Content-Type""application/json")
   response := InfoResponse{Version: "0.1.0", ServiceName: "otlp-sample"}
   json.NewEncoder(w).Encode(response)
}

運行我們的服務時,每 10 秒系統將在控制檯顯示我們的數據:

{ 
  "Resource":[
   {
     "Key":"service.name",
     "Value":{
       "Type":"STRING",
       "Value":"unknown_service:otlp-golang"
     }
   },
   {
     "Key":"telemetry.sdk.language",
     "Value":{
       "Type":"STRING",
       "Value":"go"
     }
   },
   {
     "Key":"telemetry.sdk.name",
     "Value":{
       "Type":"STRING",
       "Value":"opentelemetry"
     }
   },
   {
     "Key":"telemetry.sdk.version",
     "Value":{
       "Type":"STRING",
       "Value":"1.24.0"
     }
   }
 ],
 "ScopeMetrics":[
   {
     "Scope":{
       "Name":"info-service",
       "Version":"",
       "SchemaURL":""
     },
     "Metrics":[
       {
         "Name":"user.views",
         "Description":"The number of views",
         "Unit":"{views}",
         "Data":{
           "DataPoints":[
             {
               "Attributes":[


               ],
               "StartTime":"2024-03-03T08:50:39.07383-03:00",
               "Time":"2024-03-03T08:51:45.075332-03:00",
               "Value":1
             }
           ],
           "Temporality":"CumulativeTemporality",
           "IsMonotonic":true
         }
       }
     ]
   }
 ]
}

Context

爲了將追蹤信息發送出去,我們需要傳播上下文。爲了做到這一點,我們必須註冊一個傳播器。我們將在 otel.go 和 main.go 中實現,跟追 Tracing 和 metric 的實現差不多。

// file: otel.go

func newPropagator() propagation.TextMapPropagator {
   return propagation.NewCompositeTextMapPropagator(
      propagation.TraceContext{},
   )
}
// file: main.go 

prop := newPropagator()
otel.SetTextMapPropagator(prop)

HTTP Server

我們將通過觀測數據來豐富我們的 HTTP 服務器以完成我們的監控。爲此我們將使用帶有 OTel 的 http handler 。

// main.go


handleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) {
   handler := otelhttp.WithRouteTag(pattern, http.HandlerFunc(handlerFunc))
   mux.Handle(pattern, handler)
}


handleFunc("/info", info)
newHandler := otelhttp.NewHandler(mux, "/")


srv := &http.Server{
   Addr:    portNum,
   Handler: newHandler,
}

因此,我們將在我們的收集到的數據中獲得來自 HTTP 服務器的額外信息(用戶代理、HTTP 方法、協議、路由等)。

Conclusion

這篇文章我們詳細展示瞭如何使用 Go 來對接 OpenTelemetry 以實現完整的可觀測系統,這裏使用 console Exporter 僅作演示使用 ,在實際的開發中我們可能需要使用更加強大的 Exporter 將數據可視化,比如可以使用 Google Cloud Trace[1] 來將數據直接導出到 Goole Cloud Monitoring 。

References

OpenTelemetry[2]The Future of Observability with OpenTelemetry[3]Cloud-Native Observability with OpenTelemetry[4]Learning OpenTelemetry[5]

參考資料

[1]

google cloud opentelementry: github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace

[2]

OpenTelementry: https://opentelemetry.io/

[3]

The furure of observability: https://learning.oreilly.com/library/view/the-future-of/9781098118433/

[4]

Cloud-Native Observisability with Opentelementry: https://learning.oreilly.com/library/view/cloud-native-observability-with/9781801077705/

[5]

Learning OpenTelementry: https://learning.oreilly.com/library/view/learning-opentelemetry/9781098147174/

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