前言
- 随着微服务架构的流行,系统变得越来越复杂,需要更好的工具来监测和分析应用程序的性能和行为。
- 目前在分布式系统中收集和分析数据的方式多种多样,缺少标准化和统一性。
- 开发人员需要能够深入了解应用程序的内部运行情况,以便更好地解决问题和优化性能。
因此,OpenTelemetry的目标是成为一个可互操作的标准,提供一个通用的框架,用于生成、收集、传输和处理云原生环境中的数据,并提供一致的观测性体验。它可以帮助开发人员更好地了解其应用程序在分布式环境中的运行状况,并支持更好的故障排除和性能优化。
OpenTelemetry
OpenTelemetry 的前身是 OpenCensus 和 OpenTracing
-
OpenTracing 是一个用于分布式追踪的开源标准化API,由Google、LightStep和Uber等公司发起。它提供了一套API和工具,让开发人员能够在应用程序中添加分布式追踪能力。它的目的是为了让开发人员能够更轻松地将分布式跟踪添加到他们的应用程序中,并从中获得有关系统行为的洞察。Opentracing定义了一个标准的数据格式和语义,使得不同供应商的追踪系统可以共同工作,并允许应用程序轻松地在不同的追踪系统之间切换。
-
OpenCensus 是一款用于跟踪和度量分布式应用程序的开源工具,它提供了一个通用的API和库,可在多种语言中使用,包括Java、Go、Python和C++等。OpenCensus通过收集和聚合跨多个服务的分布式跟踪和度量数据,帮助开发人员更好地了解系统的运行情况和性能瓶颈,并支持开发人员对应用程序进行更加细粒度的监控和诊断。与OpenTracing不同,OpenCensus同时支持跟踪和度量。
如何在 go 语言中使用 OpenTelemetry
接下来我们编写一个简单的 app 来演示如何为 go 应用接入 Opentelemetry。(所有文件在同一文件夹下,包名为 main)
- 创建
add.go文件,编写以下函数,功能是计算 n 的阶乘
func Factorial(n int64)(int64,error){
tmp := 1
for i :=2 ;i <= n;i++{
tmp *= i
}
return tmp,nil
}
创建app.go文件,将以下内容写入app.go
type app struct{
r io.Reader
w io.Writer
}
func newApp(r io.Reader,w io.witer)*app{
return &app{r: r,w: w}
}
// Poll asks a user for input and returns the request.
func (a *App) Poll(ctx context.Context) (int64, error) {
a.w.Write([]byte("What Fibonacci number would you like to know: "))
var n int64
_, err := fmt.Fscanf(a.r, "%d\n", &n)
return n, err
}
// Write writes the n-th Fibonacci number back to the user.
func (a *App) Write(ctx context.Context, n int64) {
factorial,err := Factorial(n)
if err != nil {
str := fmt.Sprintf("Fibonacci(%d): %v\n", n, err)
a.w.Write([]byte(str))
} else {
str := fmt.Sprintf("Fibonacci(%d) = %d\n", n, factorial)
a.w.Write([]byte(str))
}
}
// Run starts polling users for Fibonacci number requests and writes results.
func (a *App) Run(ctx context.Context) error {
for {
n, err := a.Poll(ctx)
if err != nil {
return err
}
a.Write(ctx, n)
}
}
创建main.go文件,写入以下内容
func main(){
app := NewApp(os.Stdin, os.Stdout)
ctx := context.Background()
signalCh := make(chan os.Signal, 1)
errCh := make(chan error)
signal.Notify(signalCh, os.Interrupt)
go func() {
errCh <- app.Run(ctx)
}()
select {
case <-signalCh:
log.Println("\nbye")
case err := <-errCh:
log.Println(err)
}
}
至此,这个 app 也算功能完善了,接下来我们为其接入 OpenTelemetry。
- OpenTelemetry 分为了 API 和 SDK 两部分,我们先接入 API 部分
先获取包
go get go.opentelemetry.io/otel \
go.opentelemetry.io/otel/trace
改写app.go文件
const name = "Factorial"
改写Run函数
// Run starts polling users for Fibonacci number requests and writes results.
func (a *App) Run(ctx context.Context) error {
for {
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()
}
}
上面的代码为 for 循环的每次迭代创建一个 span。此跨度是使用来自全局 TracerProvider 的 Tracer 创建的。在后面的部分中,你将了解有关 TracerProvider 的更多信息,并在安装 SDK 时处理设置全局 TracerProvider 的另一方面。
接下来监测Poll
// Poll asks a user for input and returns the request.
func (a *App) Poll(ctx context.Context) (uint, error) {
_, span := otel.Tracer(name).Start(ctx, "Poll")
defer span.End()
a.w.Write([]byte("What Factorial number would you like to know: "))
var n uint
_, err := fmt.Fscanf(a.r, "%d\n", &n)
return n, err
}
在上面的代码中,我们开启了一个新的span,将Run方法传入的context用作创建span的context,在Poll方法中的span就与Run方法中的span形成父子关系,Run方法为父,Poll方法为子。
接下来监测Write方法同理
// Write writes the n-th Fibonacci number back to the user.
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 := Factorial(n)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
}
return f, err
}(ctx)
if err != nil {
str := fmt.Sprintf("Factorial(%d): %v\n", n, err)
a.w.Write([]byte(str))
} else {
str := fmt.Sprintf("Factorial(%d) = %d\n", n, f)
a.w.Write([]byte(str))
}
}
通过上面的介绍,我们应该知道了,Otel 中span是通过context来联系起来的。
- 接入 SDK
SDK 实现了 API,并遵循 Opentelemetygo 规范。 要使用 SDK 先要导包
go get go.opentelemetry.io/otel/sdk \
go.opentelemetry.io/otel/exporters/stdout/stdouttrace
SDK 将来自 OpenTelemetyAPI 的遥测数据连接到 Exporter。导出程序是允许将遥测数据发送到某个地方的包——或者发送到控制台 ,或者发送到远程系统或收集器进行进一步的分析和/或丰富。开放遥测通过其生态系统支持各种各样的 Exporter,包括流行的开源工具,如 Jaeger,Zipkin 和 Prometheus。
要初始化控制台导出程序,请向main.go文件添加以下函数:
// newExporter returns a console exporter.
func newExporter(w io.Writer) (trace.SpanExporter, error) {
return stdouttrace.New(
stdouttrace.WithWriter(w),
// Use human-readable output.
stdouttrace.WithPrettyPrint(),
// Do not print timestamps for the demo.
stdouttrace.WithoutTimestamps(),
)
}
遥测数据对于解决服务问题至关重要。问题是,你需要一种方法来确定数据来自哪个服务,甚至哪个服务实例。Otel 使用Resource来表示生产遥测的实体。将以下函数添加到main.go文件中,以为应用程序创建适当的Resource。
// newResource returns a resource describing this application.
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
}
你可以将与遥感数据有关的任意数据添加到Resource中
你的应用程序已经可以生成遥测数据,并且有一个导出程序将数据发送到控制台,但是它们是如何连接的呢?这里是使用TracerProvider的地方。它是一个集中点,在这里仪器将从这些跟踪器获得一个跟踪器,并将遥测数据从这些跟踪器输出到输出管道。
在main.go中添加以下逻辑
func main() {
l := log.New(os.Stdout, "", 0)
// Write telemetry data to a file.
f, err := os.Create("traces.txt")
if err != nil {
l.Fatal(err)
}
defer f.Close()
exp, err := newExporter(f)
if err != nil {
l.Fatal(err)
}
tp := trace.NewTracerProvider(
trace.WithBatcher(exp),
trace.WithResource(newResource()),
)
defer func() {
if err := tp.Shutdown(context.Background()); err != nil {
l.Fatal(err)
}
}()
otel.SetTracerProvider(tp)
...
}
首先,你创建了一个将导出到文件的控制台导出程序。然后,你将向新的TracerProvider注册Exporter。这是在传递给跟踪时使用BatchSpanProcessor完成的。使用Batcher选项。批处理数据是一个很好的实践,它将有助于避免下游系统过载。最后,在创建了TraceProvider之后,你在defer中注册了一个刷新和停止它的函数,并将其注册为全局OpenTelemetyTracerProvider。
至此,示例已经演示完了 在项目目录下执行以下命令
go run .
项目目录下会生成一个traces.txt文件,使用cat命令查看文件内容
{
"Name": "Poll",
"SpanContext": {
"TraceID": "953b225056a2fafd23b21532f50bcfe6",
"SpanID": "520fdc46b3d4e26f",
"TraceFlags": "01",
"TraceState": "",
"Remote": false
},
"Parent": {
"TraceID": "953b225056a2fafd23b21532f50bcfe6",
"SpanID": "6361207186f494e7",
"TraceFlags": "01",
"TraceState": "",
"Remote": false
},
"SpanKind": 1,
"StartTime": "0001-01-01T00:00:00Z",
"EndTime": "0001-01-01T00:00:00Z",
"Attributes": null,
"Events": [
{
"Name": "exception",
"Attributes": [
{
"Key": "exception.type",
"Value": {
"Type": "STRING",
"Value": "*errors.errorString"
}
},
{
"Key": "exception.message",
"Value": {
"Type": "STRING",
"Value": "expected integer"
}
}
],
"DroppedAttributeCount": 0,
"Time": "0001-01-01T00:00:00Z"
}
],
"Links": null,
"Status": {
"Code": "Error",
"Description": "expected integer"
},
"DroppedAttributes": 0,
"DroppedEvents": 0,
"DroppedLinks": 0,
"ChildSpanCount": 0,
"Resource": [
{
"Key": "environment",
"Value": {
"Type": "STRING",
"Value": "demo"
}
},
{
"Key": "service.name",
"Value": {
"Type": "STRING",
"Value": "fib"
}
},
{
"Key": "service.version",
"Value": {
"Type": "STRING",
"Value": "v0.1.0"
}
},
{
"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.15.1"
}
}
],
"InstrumentationLibrary": {
"Name": "fib",
"Version": "",
"SchemaURL": ""
}
}
...
看到了上述内容,代表我们成功在 go 程序中接入了 Otel。
总结
- Otel 的目的是数据采集和标准规范的统一而不是数据的展示与处理,不包括使用、存储、展示、告警。
- 在 go 语言中使用 Otel 只需使用官方提供的 API 和 SDK ,而 Exproter 可以使用一些开源工具,如 Jaeger,Prometheus 。
更多有关 Otel 的介绍请参考官网。