使用 OpenTelemetry 實現(xiàn) Golang 服務(wù)的可觀測系統(tǒng)
共 20669字,需瀏覽 42分鐘
·
2024-05-14 20:52
這篇文章中我們會討論可觀測性概念,并了解了有關(guān) OpenTelemetry 的一些細(xì)節(jié),然后會在 Golang 服務(wù)中對接 OpenTelemetry 實現(xiàn)分布式系統(tǒng)可觀測性。
Test Project
我們將使用 Go 1.22 開發(fā)我們的測試服務(wù)。我們將構(gòu)建一個 API,返回服務(wù)的名稱及其版本。
我們將把我們的項目分成兩個簡單的文件(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 . 運行后,應(yīng)該在 console 中輸出:
Starting http server.
Started on port :8080
訪問 localhost:8080 會顯示:
// http://localhost:8080/info
{
"version": "0.1.0",
"service-name": "otlp-sample"
}
現(xiàn)在我們的服務(wù)已經(jīng)可以運行了,現(xiàn)在要以對其進行監(jiān)控(或者配置我們的流水線)。在這里,我們將執(zhí)行手動監(jiān)控以理解一些觀測細(xì)節(jié)。
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。為了與這個信號互動,我們必須創(chuàng)建一個 provider,如下所示。作為一個參數(shù),我們將擁有一個 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 文件中,我們將使用創(chuàng)建跟蹤提供程序的函數(shù)。
// 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 時,我們必須保證它會“關(guān)閉”。這樣可以避免內(nèi)存泄露。
現(xiàn)在我們的服務(wù)已經(jīng)配置了一個 trace provider,我們準(zhǔn)備好收集數(shù)據(jù)了。讓我們調(diào)用 “/info” 接口來產(chǎn)生數(shù)據(jù)。
// 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”) 將在我們已經(jīng)在 main.go 中注冊的全局 trace provider 中創(chuàng)建一個命名的跟蹤器。如果未提供名稱,則將使用默認(rèn)名稱。
tracer.Start(r.Context(), “info”) 創(chuàng)建一個 Span 和一個包含新創(chuàng)建的 span 的 context.Context。如果 "ctx" 中提供的 context.Context 包含一個 Span,那么新創(chuàng)建的 Span 將是該 Span 的子 Span,否則它將是根 Span。
Span 對我們來說是一個新的概念。Span 代表一個工作單元或操作。Span 是跟蹤(Traces)的構(gòu)建塊。
同樣地,正如提供程序一樣,我們必須始終關(guān)閉 Spans 以避免“內(nèi)存泄漏”。
現(xiàn)在,我們的端點已經(jīng)被監(jiān)控,我們可以在控制臺中查看我們的觀測數(shù)據(jù)。
{
"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
我們已經(jīng)有了我們的 tracing 配置。現(xiàn)在來添加我們的第一個指標(biāo)。
首先,安裝并配置一個專門用于指標(biāo)的導(dǎo)出器。
go get "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
通過修改我們的 otel.go 文件,我們將有兩個導(dǎo)出器:一個專門用于 tracing,另一個用于 metrics。
// file: otel.go
func newTraceExporter() (trace.SpanExporter, error) {
return stdouttrace.New(stdouttrace.WithPrettyPrint())
}
func newMetricExporter() (metric.Exporter, error) {
return stdoutmetric.New()
}
現(xiàn)在添加我們的 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秒進行一次定期讀?。J(rèn)為1分鐘)。
在實例化一個 MeterProvide r時,我們將創(chuàng)建一個Meter。Meters 允許您創(chuàng)建您可以使用的儀器,以創(chuàng)建不同類型的指標(biāo)(計數(shù)器、異步計數(shù)器、直方圖、異步儀表、增減計數(shù)器、異步增減計數(shù)器……)。
現(xiàn)在我們可以在 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.")
}
}
最后,讓我們測量我們想要的數(shù)據(jù)。我們將在 info.go 中做這件事,這與我們之前在 trace 中所做的非常相似。
我們將使用 otel.Meter("info-service") 在已經(jīng)注冊的全局提供者上創(chuàng)建一個命名的計量器。我們還將通過 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)
}
運行我們的服務(wù)時,每10秒系統(tǒng)將在控制臺顯示我們的數(shù)據(jù):
{
"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
為了將追蹤信息發(fā)送出去,我們需要傳播上下文。為了做到這一點,我們必須注冊一個傳播器。我們將在 otel.go和main.go 中實現(xiàn),跟追 Tracing 和 metric 的實現(xiàn)差不多。
// file: otel.go
func newPropagator() propagation.TextMapPropagator {
return propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
)
}
// file: main.go
prop := newPropagator()
otel.SetTextMapPropagator(prop)
HTTP Server
我們將通過觀測數(shù)據(jù)來豐富我們的 HTTP 服務(wù)器以完成我們的監(jiān)控。為此我們將使用帶有 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,
}
因此,我們將在我們的收集到的數(shù)據(jù)中獲得來自 HTTP 服務(wù)器的額外信息(用戶代理、HTTP方法、協(xié)議、路由等)。
Conclusion
這篇文章我們詳細(xì)展示了如何使用 Go 來對接 OpenTelemetry 以實現(xiàn)完整的可觀測系統(tǒng),這里使用 console Exporter 僅作演示使用 ,在實際的開發(fā)中我們可能需要使用更加強大的 Exporter 將數(shù)據(jù)可視化,比如可以使用 Google Cloud Trace[1] 來將數(shù)據(jù)直接導(dǎo)出到 Goole Cloud Monitoring 。
References
OpenTelemetry[2]The Future of Observability with OpenTelemetry[3]Cloud-Native Observability with OpenTelemetry[4]Learning OpenTelemetry[5]
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/
