这篇文章中我们会讨论可观测性概念,并了解了有关 OpenTelemetry 的一些细节,然后会在 Golang 服务中对接 OpenTelemetry 实现分布式系统可观测性。
Test Project
我们将使用 Go 1.22 开发我们的测试服务。我们将构建一个 API,返回服务的名称及其版本。
我们将把我们的项目分成两个简单的文件(main.go 和 info.go)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| // 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.") }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| // 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 中输出:
1 2
| Starting http server. Started on port :8080
|
访问 localhost:8080
会显示:
1 2 3 4 5
| // http://localhost:8080/info { "version": "0.1.0", "service-name": "otlp-sample" }
|
现在我们的服务已经可以运行了,现在要以对其进行监控(或者配置我们的流水线)。在这里,我们将执行手动监控以理解一些观测细节。
First Steps
第一步是安装 Open Telemetry 的依赖。
1 2 3 4 5 6
| 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 。
1 2 3 4 5 6 7 8 9 10 11 12 13
| // 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 的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| // 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,它将接收收集到的信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| // 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 文件中,我们将使用创建跟踪提供程序的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| // 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” 接口来产生数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| // 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 以避免“内存泄漏”。
现在,我们的端点已经被监控,我们可以在控制台中查看我们的观测数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| { "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 配置。现在来添加我们的第一个指标。
首先,安装并配置一个专门用于指标的导出器。
1
| go get "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
|
通过修改我们的 otel.go 文件,我们将有两个导出器:一个专门用于 tracing,另一个用于 metrics。
1 2 3 4 5 6 7 8 9
| // file: otel.go
func newTraceExporter() (trace.SpanExporter, error) { return stdouttrace.New(stdouttrace.WithPrettyPrint()) }
func newMetricExporter() (metric.Exporter, error) { return stdoutmetric.New() }
|
现在添加我们的 metrics Provider 实例化:
1 2 3 4 5 6 7 8 9
| // 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| // 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不同,我们需要初始化我们的测量工具。我们将为我们的度量配置名称、描述和单位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| // 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) } }
|
一旦完成这个步骤,我们就可以开始测量了。最终代码看起来会像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| // 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秒系统将在控制台显示我们的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| { "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 的实现差不多。
1 2 3 4 5 6 7
| // file: otel.go
func newPropagator() propagation.TextMapPropagator { return propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, ) }
|
1 2 3 4
| // file: main.go
prop := newPropagator() otel.SetTextMapPropagator(prop)
|
HTTP Server
我们将通过观测数据来丰富我们的 HTTP 服务器以完成我们的监控。为此我们将使用带有 OTel 的 http handler 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| // 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 来将数据直接导出到 Goole Cloud Monitoring 。
References
OpenTelemetry
The Future of Observability with OpenTelemetry
Cloud-Native Observability with OpenTelemetry
Learning OpenTelemetry