使用 OpenTelemetry 实现 Golang 服务的可观测系统

Go Official Blog

共 20669字,需浏览 42分钟

 · 2024-05-14


这篇文章中我们会讨论可观测性概念,并了解了有关 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/

浏览 12
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报