使用 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 和一個包含新創建的 span 的 context.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