opentelemetry 入门

概念

​ OpenTelemetry 是一个可观测性框架和工具包, 旨在创建和管理遥测数据,如链路指标日志。 重要的是,OpenTelemetry 是供应商和工具无关的,这意味着它可以与各种可观测性后端一起使用, 包括 JaegerPrometheus 这类开源工具以及商业化产品。

​ OpenTelemetry 不是像 Jaeger、Prometheus 或其他商业供应商那样的可观测性后端。 OpenTelemetry 专注于遥测数据的生成、采集、管理和导出

​ OpenTelemetry 的一个主要目标是, 无论应用程序或系统采用何种编程语言、基础设施或运行时环境,你都可以轻松地将其仪表化。 重要的是,遥测数据的存储和可视化是有意留给其他工具处理的。

Traces

​ 获取或初始化一个追踪器

 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
package main

import (
	"context"
	"log"

	"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.19.0"
	"go.opentelemetry.io/otel/trace"
)

var tracer trace.Tracer

func newExporter(ctx context.Context) (sdktrace.SpanExporter, error) {
	// Your preferred exporter: console, jaeger, zipkin, OTLP, etc.
	return stdouttrace.New(stdouttrace.WithPrettyPrint())
}

func newTraceProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider {
	// Ensure default SDK resources and the required service name are set.
	r, err := resource.Merge(
		resource.Default(),
		resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceName("ExampleService"),
		),
	)
	if err != nil {
		panic(err)
	}

	return sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exp),
		sdktrace.WithResource(r),
	)
}

func main() {
	ctx := context.Background()

	exp, err := newExporter(ctx)
	if err != nil {
		log.Fatalf("failed to initialize exporter: %v", err)
	}

	// Create a new tracer provider with a batch span processor and the given exporter.
	tp := newTraceProvider(exp)

	// Handle shutdown properly so nothing leaks.
	defer func() { _ = tp.Shutdown(ctx) }()

	otel.SetTracerProvider(tp)

	// Finally, set the tracer that can be used for this package.
	tracer = tp.Tracer("example.io/package/name")
}

Spans

​ span 由 Trace 创建。一旦一个 ( span)完成,它就是不可变的,并且不能再被修改。

 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
func httpHandler(w http.ResponseWriter, r *http.Request) {
	ctx, span := tracer.Start(r.Context(), "hello-span")
	defer span.End()

	// do some work to track with hello-span
}

ctx := context.TODO()
// 获取当前跨度、在某个时间点向当前跨度添加信息
span := trace.SpanFromContext(ctx)

// 嵌套 span
func parentFunction(ctx context.Context) {
	ctx, parentSpan := tracer.Start(ctx, "parent")
	defer parentSpan.End()

	// call the child function and start a nested span in there
	childFunction(ctx)

	// do more work - when this function ends, parentSpan will complete.
}

func childFunction(ctx context.Context) {
	// Create a span to track `childFunction()` - this is a nested span whose parent is `parentSpan`
	ctx, childSpan := tracer.Start(ctx, "child")
	defer childSpan.End()

	// do work here, when this function returns, childSpan will complete.
}

Span Attributes

​ 属性是作为元数据应用于跨度(span)的键和值,可用于对跟踪(trace)进行聚合、过滤和分组。可以在创建跨度时添加属性,也可以在跨度完成之前的生命周期中的任何其他时间添加

1
2
3
4
// setting attributes at creation...
ctx, span = tracer.Start(ctx, "attributesAtCreation", trace.WithAttributes(attribute.String("hello", "world")))
// ... and after creation
span.SetAttributes(attribute.Bool("isTrue", true), attribute.String("stringAttr", "hi!"))

Semantic Attributes

​ 语义属性是由 开放遥测规范 定义的属性,目的是为 HTTP 方法、状态码、用户代理等常见概念在多种语言、框架和运行时之间提供一组共享的属性键。这些属性在 go.opentelemetry.io/otel/semconv/v1.26.0 包中可用。

​ 在 OpenTelemetry 中,可以自由创建跨度(span),由实现者使用特定于所表示操作的属性对其进行注释。跨度表示系统内部和系统之间的特定操作。其中一些操作表示使用诸如 HTTP 之类的知名协议的调用或数据库调用。根据协议和操作类型,需要额外信息才能在监控系统中正确表示和分析跨度。统一不同语言中的这种属性设置方式也很重要。这样,操作员将无需了解某种语言的细节,并且从多语言微服务环境中收集的遥测数据仍然可以轻松关联和交叉分析。

例子

 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
// HTTP 请求的语义属性
import (
	"go.opentelemetry.io/otel/attribute"
	semconv "go.opentelemetry.io/otel/semconv/v1.19.0"
)

attributes := []attribute.KeyValue{
	semconv.HTTPMethodKey.String("GET"),                        // 请求方法
	semconv.HTTPTargetKey.String("/api/resource"),              // 请求的目标路径
	semconv.HTTPURLKey.String("https://example.com/api/resource"), // 完整的URL
	semconv.HTTPStatusCodeKey.Int(200),                         // 响应的状态码
	semconv.HTTPUserAgentKey.String("Mozilla/5.0 ..."),         // 用户代理
	semconv.NetPeerIPKey.String("192.168.0.1"),                 // 客户端IP地址
}

// 数据库查询的语义属性
attributes := []attribute.KeyValue{
	semconv.DBSystemKey.String("mysql"),                       // 数据库类型(如mysql、postgresql)
	semconv.DBNameKey.String("customer_db"),                   // 数据库名称
	semconv.DBStatementKey.String("SELECT * FROM users"),      // 查询语句
	semconv.DBSqlTableKey.String("users"),                     // 表名
	semconv.NetPeerNameKey.String("db.example.com"),           // 数据库主机名
	semconv.NetPeerPortKey.Int(3306),                          // 数据库端口
}

// 异常捕获的语义属性
attributes := []attribute.KeyValue{
	semconv.ExceptionTypeKey.String("java.lang.NullPointerException"), // 异常类型
	semconv.ExceptionMessageKey.String("User ID cannot be null"),      // 异常消息
	semconv.ExceptionStacktraceKey.String("stacktrace here..."),       // 堆栈跟踪
}zuida

// gRPC 请求的语义属性
attributes := []attribute.KeyValue{
	semconv.RPCSystemKey.String("grpc"),                       // RPC 系统
	semconv.RPCServiceKey.String("com.example.MyService"),     // 服务名称
	semconv.RPCMethodKey.String("GetUserData"),                // 调用的方法
	semconv.NetPeerIPKey.String("10.0.0.1"),                   // 客户端 IP
	semconv.NetPeerPortKey.Int(50051),                         // 端口
}

Events

​ 事件是跨度(span)上一种人类可读的消息,它表示在其生命周期内“正在发生的某事”。

1
2
3
4
5
6
span.AddEvent("Acquiring lock")
mutex.Lock()
span.AddEvent("Got lock, doing work...")
// do stuff
span.AddEvent("Unlocking")
mutex.Unlock()

​ 事件的一个有用特性是,其时间戳显示为相对于跨度开始的偏移量,这使您能够轻松查看它们之间经过了多少时间。

​ 事件也可以有自己的属性

1
span.AddEvent("Cancelled wait due to external signal", trace.WithAttributes(attribute.Int("pid", 4328), attribute.String("signal", "SIGHUP")))

span status

​ 可以在一个跨度(Span)上设置状态(Status),这通常用于指定一个跨度未成功完成——错误(Error)。默认情况下,所有跨度都是未设置(Unset),这意味着跨度无错误完成。当您需要明确将一个跨度标记为成功,而不是使用默认的未设置(Unset)(即“无错误”)时,保留正常(Ok)状态。

​ 在跨度(Span)完成之前,可以随时设置状态

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import (
	// ...
	"go.opentelemetry.io/otel/codes"
	// ...
)

// ...

result, err := operationThatCouldFail()
if err != nil {
	span.SetStatus(codes.Error, "operationThatCouldFail failed")
}

record errors

​ 如果您有一个失败的操作,并且希望捕获它产生的错误,则可以记录该错误。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import (
	// ...
	"go.opentelemetry.io/otel/codes"
	// ...
)

// ...

result, err := operationThatCouldFail()
if err != nil {
	span.SetStatus(codes.Error, "operationThatCouldFail failed")
	span.RecordError(err)
}

​ 强烈建议在使用 RecordError 时,也将一个跨度(span)的状态设置为 Error,除非你不希望将跟踪失败操作的跨度视为错误跨度。RecordError 函数在被调用时不会自动设置跨度状态。

Propagators and Context

​ 跟踪可以扩展到单个进程之外。这需要上下文传播,即一种将跟踪的标识符发送到远程进程的机制。为了通过网络传播跟踪上下文,必须向 OpenTelemetry API 注册一个传播器。

 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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package main

import (
	"context"
	"io"
	"log"
	"net/http"

	"github.com/prometheus/client_golang/prometheus/promhttp"
	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/baggage"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	prometheus "go.opentelemetry.io/otel/exporters/prometheus"

	"go.opentelemetry.io/otel/metric"
	"go.opentelemetry.io/otel/propagation"
	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.26.0"

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

func initTracer() (*sdktrace.TracerProvider, error) {
	// Create stdout exporter to be able to retrieve
	// the collected spans.
	exporter, err := otlptracehttp.New(
		context.Background(),
		otlptracehttp.WithInsecure(),
		otlptracehttp.WithEndpointURL("http://localhost:4318/v1/traces"),
	)
	if err != nil {
		return nil, err
	}

	// For the demonstration, use sdktrace.AlwaysSample sampler to sample all traces.
	// In a production application, use sdktrace.ProbabilitySampler with a desired probability.
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithSampler(sdktrace.AlwaysSample()),
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(
			resource.NewWithAttributes(
				semconv.SchemaURL,
				semconv.ServiceNameKey.String("ExampleService"),
			),
		),
	)

	otel.SetTracerProvider(tp)
	otel.SetTextMapPropagator(
		propagation.NewCompositeTextMapPropagator(
			propagation.TraceContext{},
			propagation.Baggage{},
		),
	)
	return tp, err
}

func main() {
	tp, err := initTracer()
	if err != nil {
		log.Fatal(err)
	}
	defer func() {
		if err := tp.Shutdown(context.Background()); err != nil {
			log.Printf("Error shutting down tracer provider: %v", err)
		}
	}()

	uk := attribute.Key("username")

	helloHandler := func(w http.ResponseWriter, req *http.Request) {
		ctx := req.Context()
		span := trace.SpanFromContext(ctx) // span为Hello
		defer span.End()
		bag := baggage.FromContext(ctx)
		span.AddEvent(
			"handling this...",
			trace.WithAttributes(uk.String(bag.Member("username").Value())),
		)

		_, _ = io.WriteString(w, "Hello, world!\n")
	}

	// otelhttp.NewHandler会在处理请求的同时创建一个名为 Hello 的 span
	otelHandler := otelhttp.NewHandler(http.HandlerFunc(helloHandler), "Hello")

	http.Handle("/hello", otelHandler)

	err = http.ListenAndServe(":7777", nil)
	if err != nil {
		log.Fatal(err)
	}
}

Metrics

​ 要开始生成指标,您需要有一个已初始化的计量器提供程序(MeterProvider),它允许您创建一个计量器(Meter)。计量器允许您创建可用于创建不同类型指标的工具。OpenTelemetry Go 目前支持以下工具:

  • Counter 计数器,一种支持非负增量的同步工具
  • Asynchronous Counter 异步计数器,一种支持非负增量的异步仪器
  • Histogram 直方图,一种支持具有统计意义的任意值(如直方图、汇总或百分位数)的同步工具
  • Synchronous Gauge 同步测量仪,一种支持非累加值(如室温)的同步仪器。
  • Asynchronous Gauge 异步仪表,一种支持非累加值(如室温)的异步仪器
  • UpDownCounter 上下计数器,一种支持递增和递减的同步工具,例如活动请求的数量
  • Asynchronous UpDownCounter 异步可逆计数器,一种支持递增和递减操作的异步仪器

​ opentelemetry 推荐直接 push 模式,下面这种暴露http服务指标的方式我找了看了源码,貌似是有意隐藏,提了个QA跟进:In the latest version Prometheus Exporter func (*Exporter) ServeHTTP was removed , how can i to expoter my metric use http server ? · open-telemetry/opentelemetry-go · Discussion #5974

​ 个人感觉 metric api 不如 Prometheus方便简介,指标采集这块可以用其他方案代理,没有银弹。

 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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package main

import (
	"context"
	"io"
	"log"
	"net/http"

	"github.com/prometheus/client_golang/prometheus/promhttp"
	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/baggage"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	prometheus "go.opentelemetry.io/otel/exporters/prometheus"

	"go.opentelemetry.io/otel/metric"
	"go.opentelemetry.io/otel/propagation"
	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.26.0"

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

func initMeter() (*prometheus.Exporter, error) {
	// 创建 Prometheus Exporter
	exporter, err := prometheus.New()
	if err != nil {
		log.Fatalf("无法创建 Prometheus 导出器: %v", err)
	}

	// 创建并设置 MeterProvider
	meterProvider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(exporter))

	otel.SetMeterProvider(meterProvider)

	return exporter, nil
}

func main() {
	if _, err := initMeter(); err != nil {
		log.Fatal(err)
	}

	meter := otel.Meter("example.com/basic")

	counterMeter, _ := meter.Int64Counter("request_count",
		metric.WithDescription("Http request count"),
		metric.WithUnit("1"))

	uk := attribute.Key("username")

	helloHandler := func(w http.ResponseWriter, req *http.Request) {
		counterMeter.Add(req.Context(), 1)
		ctx := req.Context()
		span := trace.SpanFromContext(ctx) // span为Hello
		defer span.End()
		bag := baggage.FromContext(ctx)
		span.AddEvent(
			"handling this...",
			trace.WithAttributes(uk.String(bag.Member("username").Value())),
		)

		_, _ = io.WriteString(w, "Hello, world!\n")
	}

	// otelhttp.NewHandler会在处理请求的同时创建一个名为 Hello 的 span
	otelHandler := otelhttp.NewHandler(http.HandlerFunc(helloHandler), "Hello")

	http.Handle("/hello", otelHandler)
    // 指标暴露为 Prometheus 标准的数据
	http.Handle("/metrics", promhttp.Handler())

	err = http.ListenAndServe(":7777", nil)
	if err != nil {
		log.Fatal(err)
	}
}

Log

截止到目前官方文档关于 log 只是一个 bate 的实验特性 ,日志与指标和跟踪的不同之处在于,没有面向用户的日志 OpenTelemetry 日志 API。相反,有一些工具可以将日志与现有日志桥接 将常用日志包(例如 slog、logrus、zap、logr)迁移到 OpenTelemetry 中生态系统。 有关此设计决策背后的基本原理,请参阅日志记录规范