Observability with OpenTelemetry and Go

这篇文章中我们会讨论可观测性概念,并了解了有关 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 和一个包含新创建的 spancontext.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


-------------The End-------------

本文标题:Observability with OpenTelemetry and Go

文章作者:cloud sjhan

发布时间:2024年05月13日 - 11:05

最后更新:2024年05月13日 - 11:05

原始链接:https://cloudsjhan.github.io/2024/05/13/Observability-with-OpenTelemetry-and-Go/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

cloud sjhan wechat
subscribe to my blog by scanning my public wechat account
坚持原创技术分享,您的支持将鼓励我继续创作!
0%
;