OpenTelemetry实现go应用的可观测性 - 入门

 提示:转载请注明原文链接

 本文链接:https://360us.net/article/86.html

本文定位是快速入门,理解OpenTelemetry在go里面的基本使用

参考文档:https://opentelemetry.io/docs/instrumentation/go/getting-started

Go 版本需要是1.16以上。

创建一个名字是fib的go项目,就一个功能计算斐波那契数列。

创建三个文件:

fib.go测试项目的核心,功能是计算斐波那契数列。

package main

import "fmt"

// Fibonacci 计算斐波那契数
func Fibonacci(n uint) (uint64, error) {
	if n <= 1 {
		return uint64(n), nil
	}

	var n2, n1 uint64 = 0, 1
	for i := uint(2); i < n; i++ {
		n2, n1 = n1, n1+n2
	}

	return n2 + n1, nil
}

app.go,应用逻辑,接受用户用户输入和输出结果。

package main

import (
	"context"
	"fmt"
	"io"
	"log"
)

// App是一个斐波那契计算应用
type App struct {
	r io.Reader
	l *log.Logger
}

// NewApp 创建App对象
func NewApp(r io.Reader, l *log.Logger) *App {
	return &App{r: r, l: l}
}

// Run 启动轮训,等待用户计算请求和返回计算就结果
func (a *App) Run(ctx context.Context) error {
	for {
		n, err := a.Poll(newCtx)
		if err != nil {
			span.End()
			return err
		}

		a.Write(ctx, n)
	}
}

// Poll 询问用户输入,然后返回请求计算数
func (a *App) Poll(ctx context.Context) (uint, error) {
	a.l.Print("What Fibonacci number would you like to know: ")

	var n uint
	_, err := fmt.Fscanf(a.r, "%d\n", &n)
	return n, nil
}

// Write 输出斐波那契数计算结果给用户
func (a *App) Write(ctx context.Context, n uint) {
	f, err := Fibonacci(n)
	if err != nil {
		a.l.Printf("Fibonacci(%d): %v\n", n, err)
	} else {
		a.l.Printf("Fibonacci(%d) = %d\n", n, f)
	}
}

main.go,启动应用。

package main

import (
	"context"
	"log"
	"os"
	"os/signal"
)

func main() {
    l := log.New(os.Stdout, "", 0)

	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, os.Interrupt)

	errCh := make(chan error)
	app := NewApp(os.Stdin, l)
	go func() {
		errCh <- app.Run(context.Background())
	}()

	select {
	case <-sigCh:
		l.Println("\ngoodbye")
		return
	case err := <-errCh:
		if err != nil {
			l.Fatal(err)
		}
	}
}

追踪检测

OpenTelemetry分为两部分:检测代码API,和实现API的SDK。

检测代码API用来在我们的应用里面生成追踪遥测数据。

安装下面两个包:

go get go.opentelemetry.io/otel go.opentelemetry.io/otel/trace

app.go文件里面引入:

import (
	"context"
	"fmt"
	"io"
	"log"
	"strconv"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/trace"
)

OpenTelemetry的追踪API提供了Tracer来创建追踪。

定义一个唯一的标识用来在Tracer里标识应用。

const name = "fib"

标准方法是用完全限定的包名,这里是示例就简单一点。

先检测Run方法:

func (a *App) Run(ctx context.Context) error {
	for {
		// 检测Run方法,每次循环都创建新的root span和contentx
		newCtx, span := otel.Tracer(name).Start(ctx, "Run")

		n, err := a.Poll(newCtx)
		if err != nil {
			span.End()
			return err
		}

		a.Write(newCtx, n)
		span.End()
	}
}

每次循环都创建新的root span和contentx,因为一次循环就相当于一次请求,每个请求都是一条独立的追踪线。

span代表一组操作或工作,比如http服务,一次请求从开始到结束就是一个span,里面所有的追踪调用关系都会串联在一起,span可以嵌套,一次请求是由一个根span和无数个小span组成的。

然后是Poll方法:

func (a *App) Poll(ctx context.Context) (uint, error) {
	_, span := otel.Tracer(name).Start(ctx, "Poll")
	defer span.End()

	a.l.Print("What Fibonacci number would you like to know: ")

	var n uint
	_, err := fmt.Fscanf(a.r, "%d\n", &n)

	// 防止溢出转成string
	nStr := strconv.FormatUint(uint64(n), 10)
	span.SetAttributes(attribute.String("request.n", nStr))

	return n, err
}

创建了一个子span,用SetAttributes添加了一个属性来注释span

注释可以当需要在遥测里面看到应用的状态或者详细信息的时候添加进去。

最后是Write方法:

func (a *App) Write(ctx context.Context, n uint) {
	var span trace.Span
	ctx, span = otel.Tracer(name).Start(ctx, "Write")
	defer span.End()

	f, err := func(ctx context.Context) (uint64, error) {
		_, span := otel.Tracer(name).Start(ctx, "Fibonacci")
		defer span.End()
		return Fibonacci(n)
	}(ctx)
	if err != nil {
		a.l.Printf("Fibonacci(%d): %v\n", n, err)
	} else {
		a.l.Printf("Fibonacci(%d) = %d\n", n, f)
	}
}

这个方法使用了两个span来检测,一个是write本身,另一个是调用核心逻辑Fibonacci

span的关系是用context.Context来定义的。

比如创建一个span A时会返回一个Context,这个Context包含了对A span的引用,如果创建另一个span B使用了AContext,那么这两个span就是相关的,A span就成为了后一个Bspan的父spanB span被称为A sapn的子span

从上面span的关系可以看出,期望的跟踪关系看起来是下面这样的:

Run
├── Poll
└── Write
    └── Fibonacci

安装SDK

上一步是在应用里面写上生成追踪的检测代码,也就是生成遥测数据。

这一步SDK的作用是把上面生成的追踪数据输出到指定的存储后端,方便查看。

安装包:

go get go.opentelemetry.io/otel/sdk \
       go.opentelemetry.io/otel/exporters/stdout/stdouttrace

main.go里面引入:

import (
	"context"
	"io"
	"log"
	"os"
	"os/signal"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)

创建控制台输出者(Exporter)

exporter的作用是把收集到的遥测数据发送到某个地方,可以是控制台或者远程的其他系统等,比如Jaeger, Zipkin,和 Prometheus。

初始化控制台exporter,在main.go文件添加如下代码:

// newExporter 返回exporter
func newExporter(w io.Writer) (trace.SpanExporter, error) {
	return stdouttrace.New(
		stdouttrace.WithWriter(w),
		// 使用人类友好的输出
		stdouttrace.WithPrettyPrint(),
		// 不打印是时间戳
		stdouttrace.WithoutTimestamps(),
	)
}

创建资源(Resource)

resource代表生成遥测数据的实体,标识一个服务,才知道遥测数据是哪个服务实例的。

mian.go:

// newResource 返回一个描述应用的resource
func newResource() *resource.Resource {
	r, _ := resource.Merge(
		resource.Default(),
		resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceName("fib"),
			semconv.ServiceVersion("v0.1.0"),
			attribute.String("environment", "demo"),
		),
	)
	return r
}

安装追踪器提供者(TracerProvider)

上面做了生成遥测数据和遥测数据exporter,TracerProvider的作用就是把两者连接起来。

它是一个中心点,获得Tracer,并将Tracer的遥测数据输出到管道。

管道就是接收和发送数据到exporter,叫做SpanProcessor

一个TracerProvider可以配置多个处理器(processor),这里的例子只配置了一个。

修改main方法:

func main() {
	l := log.New(os.Stdout, "", 0)

	// 遥测数据写到文件
	f, err := os.Create("traces.txt")
	if err != nil {
		l.Fatal(err)
	}
	defer f.Close()

    // 创建exporter
	exp, err := newExporter(f)
	if err != nil {
		l.Fatal(err)
	}

    // 创建TracerProvider
	tp := trace.NewTracerProvider(
		trace.WithBatcher(exp), //使用了BatchSpanProcessor
		trace.WithResource(newResource()),
	)
	defer func() {
        // 刷出(flush)和停止TracerProvider
		if err := tp.Shutdown(context.Background()); err != nil {
			l.Fatal(err)
		}
	}()
    // 注册全局TracerProvider
	otel.SetTracerProvider(tp)

    /* … */
}

上面用otel.Tracer(name)获取了Tracer,这里注册全局TracerProvider otel.SetTracerProvider(tp)使两者关联了起来。

这里使用的是全局TracerProvider,方便但不一定总是合适,可以显示的传递,也可以从span上下文推断。

比如保存在一个变量里面:

package main

import (
    "log"
)

var tp *sdktrace.TracerProvider

func initTracer() error {
    // ........
    tp = sdktrace.NewTracerProvider(
		sdktrace.WithSampler(sdktrace.AlwaysSample()),
		sdktrace.WithSpanProcessor(bsp),
	)
	otel.SetTracerProvider(tp)
	return nil
}

func main() {
    if err := initTracer(); err != nil {
		log.Panic(err)
	}

    tracer := tp.Tracer("demo")
    // ........
}

运行

现在可以运行demo了:

$ go run .
What Fibonacci number would you like to know:
42
Fibonacci(42) = 267914296
What Fibonacci number would you like to know:
^C
goodbye

查看traces.txt文件可以看到遥测数据。

错误

本例中第100个斐波那契数的结果已经溢出了,需要处理一下错误:

func Fibonacci(n uint) (uint64, error) {
	if n <= 1 {
		return uint64(n), nil
	}

    // 不能计算的数
	if n > 93 {
		return 0, fmt.Errorf("unsupported fibonacci number %d: too large", n)
	}

	var n2, n1 uint64 = 0, 1
	for i := uint(2); i < n; i++ {
		n2, n1 = n1, n1+n2
	}

	return n2 + n1, nil
}

Write函数也需要改下,记录下这个错误信息:

func (a *App) Write(ctx context.Context, n uint) {
	var span trace.Span
	ctx, span = otel.Tracer(name).Start(ctx, "Write")
	defer span.End()

	f, err := func(ctx context.Context) (uint64, error) {
		_, span := otel.Tracer(name).Start(ctx, "Fibonacci")
		defer span.End()
		f, err := Fibonacci(n)
		if err != nil {
			span.RecordError(err)
			span.SetStatus(codes.Error, err.Error())
		}
		return f, err
	}(ctx)
    /* … */
}

RecordError SetStatus分别记录错误和设置状态。

需要引入包go.opentelemetry.io/otel/codes

再次运行,可以在追踪记录里面看到错误信息。

"Events": [
	{
		"Name": "exception",
		"Attributes": [
			{
				"Key": "exception.type",
				"Value": {
					"Type": "STRING",
					"Value": "*errors.errorString"
				}
			},
			{
				"Key": "exception.message",
				"Value": {
					"Type": "STRING",
					"Value": "unsupported fibonacci number 100: too large"
				}
			}
		],
        ...
    }
]

总结

至此文章就结束了,本文给应用添加了追踪检测代码来生成数据,并且通过控制台输出者把生成的数据发送到文件保存了起来,目的是对OpenTelemetry能够快速有个了解。

由于Go不支持自动检测,所以都需要添加代码,对现有项目会有比较大的侵入性。

另一方面可以使用特定的遥测库来生成特定库的遥测数据,可以在这里查找:https://opentelemetry.io/ecosystem/registry/?language=go&component=instrumentation

比如对于net/http,可以使用go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp来包装一下net/http,简单配置一下就可以自动生成跟踪入站和出战的span

完整代码参考:https://github.com/ilaziness/gopkg/tree/main/opentelemetry/getting_start


本文链接:https://360us.net/article/86.html