使用 OpenTelemetry 实现 Golang 服务的可观测系统
共 20669字,需浏览 42分钟
·
2024-05-14 20:52
这篇文章中我们会讨论可观测性概念,并了解了有关 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]
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/