使用 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