手动仪表化

用于 OpenTelemetry Go 的手动仪表化

Manual instrumentation is the act of adding observability code to an app yourself.

If you’re instrumenting an app, you need to use the OpenTelemetry SDK for your language. You’ll then use the SDK to initialize OpenTelemetry and the API to instrument your code. This will emit telemetry from your app, and any library you installed that also comes with instrumentation.

If you’re instrumenting a library, only install the OpenTelemetry API package for your language. Your library will not emit telemetry on its own. It will only emit telemetry when it is part of an app that uses the OpenTelemetry SDK. For more on instrumenting libraries, see Libraries.

For more information about the OpenTelemetry API and SDK, see the specification.

设置

跟踪

获取跟踪器

要创建 span,您需要首先获取或初始化跟踪器。

确保已安装正确的包:

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

然后初始化一个 exporter、资源、跟踪器提供程序和最后一个跟踪器。

package app

import (
	"context"
	"fmt"
	"log"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
	"go.opentelemetry.io/otel/trace"
)

var tracer trace.Tracer

func newExporter(ctx context.Context) /* (someExporter.Exporter, error) */ {
	// 你选择的 exporter:console、jaeger、zipkin、OTLP 等等。
}

func newTraceProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider {
	// 确保默认的 SDK 资源和所需的服务名称已设置。
	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)
	}

	// 创建一个跟踪器提供程序,并使用给定的 exporter、批量 span 处理器。
	tp := newTraceProvider(exp)

	// 正确处理关闭操作以避免资源泄漏。
	defer func() { _ = tp.Shutdown(ctx) }()

	otel.SetTracerProvider(tp)

	// 最后,设置可用于该包的跟踪器。
	tracer = tp.Tracer("ExampleService")
}

现在您可以使用 tracer 来手动仪表化您的代码。

创建 Spans

Span 是由跟踪器创建的。如果您尚未初始化跟踪器,您需要先执行该操作。

要使用跟踪器创建一个 Span,您还需要一个 context.Context 的实例。这些通常来自于诸如请求对象之类的东西,可能已经包含来自 [instrumentation library][] 的父 Span。

func httpHandler(w http.ResponseWriter, r *http.Request) {
	ctx, span := tracer.Start(r.Context(), "hello-span")
	defer span.End()

	// 某些工作要追踪这个 hello-span
}

在 Go 中,context 包用于存储活动的 Span。当你启动一个 Span 时,你会获得一个创建的 Span 以及包含它的修改后的 context 的句柄。

一旦 Span 完成,它就是不可变的,不能再修改。

获取当前的 Span

要获取当前的 Span,你需要从你有 handle 的 context.Context 中提取出它:

// 这个 context 需要包含您计划提取出来的活动 Span。
ctx := context.TODO()
span := trace.SpanFromContext(ctx)

// 对当前的 Span 做一些处理,如果你想结束它,也可以选择调用 `span.End()`

如果您希望在某个时刻向当前 Span 中添加信息,这将非常有帮助。

创建嵌套的 Spans

您可以创建一个嵌套的 Span 来跟踪嵌套操作中的工作。

如果您手头的当前 context.Context 已经包含一个 Span,那么创建一个新的 Span 就会使它成为一个嵌套 Span。例如:

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

	// 调用子函数并在其中启动一个嵌套的 Span
	childFunction(ctx)

	// 做更多的工作 - 当此函数结束时,parentSpan 也完成了。
}

func childFunction(ctx context.Context) {
	// 创建一个用于跟踪 `childFunction()` 的 Span - 这是一个其父 Span 是 `parentSpan` 的嵌套 Span
	ctx, childSpan := tracer.Start(ctx, "child")
	defer childSpan.End()

	// 在这里做工作,当此函数返回时,childSpan 完成。
}

一旦 Span 完成,它就是不可变的,不能再修改。

Span 属性

属性是应用于 Span 的元数据的键和值,对于聚合、过滤和分组跟踪非常有用。属性可以在创建 Span 时添加,也可以在 Span 完成之前的任何时间添加。

// 在创建时设置属性…
ctx, span = tracer.Start(ctx, "attributesAtCreation", trace.WithAttributes(attribute.String("hello", "world")))
// …和创建之后
span.SetAttributes(attribute.Bool("isTrue", true), attribute.String("stringAttr", "hi!"))

属性键也可以预先计算:

var myKey = attribute.Key("myCoolAttribute")
span.SetAttributes(myKey.String("a value"))

语义属性

语义属性是由 [OpenTelemetry 规范][] 定义的属性,在多个语言、框架和运行时之间为共同的概念提供一套共享的属性键,例如 HTTP 方法、状态码、用户代理等。这些属性在 go.opentelemetry.io/otel/semconv/v1.21.0 包中可用。

有关详细信息,请参见 [Trace 语义约定][]。

事件

事件是一个可以在 Span 中表示“某事发生”的人类可读消息。例如,想象一个需要对互斥锁下的资源进行独占访问的函数,可以在两个时刻创建一个事件-一次是在尝试获得资源访问权限时,另一次是在获得互斥锁时。

span.AddEvent("正在获得锁")
mutex.Lock()
span.AddEvent("获得了锁,正在进行工作...")
// 做一些事情
span.AddEvent("解锁")
mutex.Unlock()

事件的一个有用的特点是它们的时间戳显示为距离 Span 开始的时间偏移量,这样您可以轻松地看到它们之间经过了多长时间。

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

span.AddEvent("因外部信号取消等待", trace.WithAttributes(attribute.Int("pid", 4328), attribute.String("signal", "SIGHUP")))

设置 Span 状态

Span 的状态可以设置为 .Error,通常用于指定 Span 正在跟踪的操作中发生了错误。

import (
	// ...
	"go.opentelemetry.io/otel/codes"
	// ...
)

// ...

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

默认情况下,所有 Span 的状态都是 Unset。在极少数情况下,您可能还希望将状态设置为 Ok,但通常情况下这是不必要的。

记录错误

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

import (
	// ...
	"go.opentelemetry.io/otel/codes"
	// ...
)

// ...

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

强烈建议您在使用 RecordError 时也将 Span 的状态设置为 Error,除非您不希望将跟踪一个失败操作的 Span 视为错误 Span。当调用时,RecordError 函数 会自动设置一个 Span 状态。

传播器和上下文

跟踪可以超越单个进程。这需要 上下文传播,一种将跟踪的标识符发送到远程进程的机制。

为了通过网络传播跟踪上下文,必须使用注册到 OpenTelemetry API 中的传播器。

import (
  "go.opentelemetry.io/otel"
  "go.opentelemetry.io/otel/propagation"
)
...
otel.SetTextMapPropagator(propagation.TraceContext{})

OpenTelemetry 还支持 B3 标头格式,以与不支持 W3C TraceContext 标准的现有跟踪系统 (go.opentelemetry.io/contrib/propagators/b3) 兼容。

配置上下文传播后,您很可能希望使用自动仪表化来处理实际管理上下文序列化的后台工作。

指标

要开始生成 指标,您需要初始化一个允许您创建 MeterMeterProviderMeter 允许您创建各种类型的指标所需的仪表。OpenTelemetry Go 目前支持以下仪表:

  • 计数器,一种支持非负增量的同步仪表
  • 异步计数器,一种支持非负增量的异步仪表
  • 直方图,一种支持具有统计意义的任意值,如直方图、摘要或百分位数的同步仪表
  • 异步计量仪表,一种支持非可加性值的异步仪表,如室温
  • 上下计数器,一种支持增量和减量的同步仪表,如活动请求数
  • 异步上下计数器,一种支持增量和减量的异步仪表

有关同步和异步仪表的详细信息,以及哪种类型最适合您的场景,请参见 附加指导方针

如果没有由仪表库或手动创建的 MeterProvider,OpenTelemetry 指标 API 将使用无操作实现,并无法生成数据。

在这里,您可以找到有关以下的更详细的包文档:

初始化指标

要在您的应用程序中启用指标,您需要初始化一个已初始化的MeterProvider,它将允许您创建一个Meter

如果没有创建 MeterProvider,OpenTelemetry 指标的 API 将使用无操作实现,并且无法生成数据。因此,您必须修改源代码,使用以下软件包包含 SDK 初始化代码:

确保已安装适当的 Go 模块:

go get go.opentelemetry.io/otel \
  go.opentelemetry.io/otel/exporters/stdout/stdoutmetric \
  go.opentelemetry.io/otel/sdk \
  go.opentelemetry.io/otel/sdk/metric

然后初始化资源、指标输出器和指标提供程序:

package main

import (
	"context"
	"log"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
	"go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/resource"
	semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)

func main() {
	// 创建资源.
	res, err := newResource()
	if err != nil {
		panic(err)
	}

	// 创建一个仪表提供程序.
	// 如果它接受 MeterProvider 实例,您可以直接将该实例传递给您的仪表代码。
	meterProvider, err := newMeterProvider(res)
	if err != nil {
		panic(err)
	}

	// 正确处理关闭操作以避免资源泄漏。
	defer func() {
		if err := meterProvider.Shutdown(context.Background()); err != nil {
			log.Println(err)
		}
	}()

	// 将其注册为全局仪表提供程序,以便可以使用 otel.Meter 并使用 otel.GetMeterProvider 进行访问。
	// 大多数仪表库使用的是全局仪表提供程序的默认值。
	// 如果没有设置全局仪表提供程序,则使用无操作实现,无法生成数据。
	otel.SetMeterProvider(meterProvider)
}

func newResource() (*resource.Resource, error) {
	return resource.Merge(resource.Default(),
		resource.NewWithAttributes(semconv.SchemaURL,
			semconv.ServiceName("my-service"),
			semconv.ServiceVersion("0.1.0"),
		))
}

func newMeterProvider(res *resource.Resource) (*metric.MeterProvider, error) {
	metricExporter, err := stdoutmetric.New()
	if err != nil {
		return nil, err
	}

	meterProvider := metric.NewMeterProvider(
		metric.WithResource(res),
		metric.WithReader(metric.NewPeriodicReader(metricExporter,
			// 默认为 1 分钟, 设置 3 秒以演示目的。
			metric.WithInterval(3*time.Second))),
	)
	return meterProvider, nil
}

现在配置了 MeterProvider,您可以获取一个 Meter

获取一个 Meter

在应用程序的任何地方,您可以调用 otel.Meter 来获取一个 Meter,以手动仪表化您的代码。例如:

import "go.opentelemetry.io/otel"

var meter = otel.Meter("my-service-meter")

同步和异步工具

OpenTelemetry工具可以是同步或异步(可观察的)。

同步工具在被调用时进行测量。测量与程序执行期间的其他函数调用一样,在另一个调用中完成。定期地,这些测量的聚合由配置的导出器导出。由于测量与导出值的解耦,一个导出周期可能包含零个或多个聚合测量。

另一方面,异步工具在SDK请求时提供测量。当SDK导出时,将调用在创建工具时提供的回调函数。此回调函数将测量提供给SDK,并立即导出。所有异步工具上的测量在每个导出周期执行一次。

异步工具在以下几种情况下很有用:

  • 当更新计数器不是计算上便宜且您不希望当前执行的线程等待测量时
  • 需要以与程序执行无关的频率进行观测(即,它们不能与请求生命周期关联时可以准确测量)
  • 没有已知的测量值的时间戳

在这些情况下,直接观察累积值通常比在后处理中聚合一系列增量更好(同步示例)。

使用计数器

计数器可用于测量非负增加的值。

例如,以下是如何报告HTTP处理程序的调用次数:

import (
	"net/http"

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

func init() {
	apiCounter, err := meter.Int64Counter(
		"api.counter",
		metric.WithDescription("API调用的次数"),
		metric.WithUnit("{次}"),
	)
	if err != nil {
		panic(err)
	}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		apiCounter.Add(r.Context(), 1)

		// 在API调用中完成一些工作
	})
}

使用增减计数器

增减计数器可以增加和减少,允许您观察一个累积值,该值可以增加或减少。

例如,以下是如何报告某个集合的项目数量:

import (
	"context"

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

var itemsCounter metric.Int64UpDownCounter

func init() {
	var err error
	itemsCounter, err = meter.Int64UpDownCounter(
		"items.counter",
		metric.WithDescription("项目数量"),
		metric.WithUnit("{项目}"),
	)
	if err != nil {
		panic(err)
	}
}

func addItem() {
	// 添加一个项目的代码

	itemsCounter.Add(context.Background(), 1)
}

func removeItem() {
	// 从集合中删除一个项目的代码

	itemsCounter.Add(context.Background(), -1)
}

使用直方图

直方图用于测量随时间的值分布。

例如,以下是如何报告HTTP处理程序的响应时间分布:

import (
	"net/http"
	"time"

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

func init() {
	histogram, err := meter.Float64Histogram(
		"task.duration",
		metric.WithDescription("任务执行的持续时间"),
		metric.WithUnit("秒"),
	)
	if err != nil {
		panic(err)
	}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()

		// 在API调用中完成一些工作

		duration := time.Since(start)
		histogram.Record(r.Context(), duration.Seconds())
	})
}

使用可观测(异步)计数器

可观测计数器可用于测量累积的非负递增值。

例如,以下是如何报告应用程序启动以来的时间:

import (
	"context"
	"time"

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

func init() {
	start := time.Now()
	if _, err := meter.Float64ObservableCounter(
		"uptime",
		metric.WithDescription("应用程序启动以来的持续时间"),
		metric.WithUnit("秒"),
		metric.WithFloat64Callback(func(_ context.Context, o metric.Float64Observer) error {
			o.Observe(float64(time.Since(start).Seconds()))
			return nil
		}),
	); err != nil {
		panic(err)
	}
}

使用可观测(异步)增减计数器

可观测的增减计数器可以增加和减少,允许您测量递增的累积非负值。

例如,以下是如何报告某个数据库的一些指标:

import (
	"context"
	"database/sql"

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

// 为提供的数据库注册异步的指标,使用完数据库之后需要取消注册 metric.Registration。
func registerDBMetrics(db *sql.DB, meter metric.Meter, poolName string) (metric.Registration, error) {
	max, err := meter.Int64ObservableUpDownCounter(
		"db.client.connections.max",
		metric.WithDescription("允许的最大打开连接数"),
		metric.WithUnit("{连接}"),
	)
	if err != nil {
		return nil, err
	}

	waitTime, err := meter.Int64ObservableUpDownCounter(
		"db.client.connections.wait_time",
		metric.WithDescription("从池中获取一个打开连接所需的时间"),
		metric.WithUnit("毫秒"),
	)
	if err != nil {
		return nil, err
	}

	reg, err := meter.RegisterCallback(
		func(_ context.Context, o metric.Observer) error {
			stats := db.Stats()
			o.ObserveInt64(max, int64(stats.MaxOpenConnections))
			o.ObserveInt64(waitTime, int64(stats.WaitDuration))
			return nil
		},
		max,
		waitTime,
	)
	if err != nil {
		return nil, err
	}
	return reg, nil
}

使用可观测(异步)量规

应使用可观测的量规来测量非累积值。

例如,以下是如何报告应用程序中使用的堆对象的内存使用情况:

import (
	"context"
	"runtime"

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

func init() {
	if _, err := meter.Int64ObservableGauge(
		"memory.heap",
		metric.WithDescription(
			"分配的堆对象的内存使用情况",
		),
		metric.WithUnit("字节"),
		metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
			var m runtime.MemStats
			runtime.ReadMemStats(&m)
			o.Observe(int64(m.HeapAlloc))
			return nil
		}),
	); err != nil {
		panic(err)
	}
}

添加属性

可以使用WithAttributeSetWithAttributes选项添加属性。

import (
	"net/http"

	"go.opentelemetry.io/otel/metric"
	semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)

func init() {
	apiCounter, err := meter.Int64UpDownCounter(
		"api.finished.counter",
		metric.WithDescription("完成的API调用次数"),
		metric.WithUnit("{调用}"),
	)
	if err != nil {
		panic(err)
	}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		// 在API调用中完成一些工作并设置响应的HTTP状态码

		apiCounter.Add(r.Context(), 1,
			metric.WithAttributes(semconv.HTTPStatusCode(statusCode)))
	})
}

注册视图

视图为SDK用户提供了自定义SDK输出的灵活性。您可以自定义要处理或忽略哪些指标工具。您还可以自定义聚合以及要在指标上报告的属性。

每个工具都有一个默认视图,该视图保留原始名称、说明和属性,并具有基于工具类型的默认聚合。当注册视图与工具匹配时,将默认视图替换为已注册视图。与仪表匹配的其他注册视图是可附加的,并导致为该仪表输出多个指标。

您可以使用NewView函数创建一个视图并使用WithView选项注册它。

例如,以下是如何在v0.34.0版本的http工具库中将latency工具重命名为request.latency的视图:

view := metric.NewView(metric.Instrument{
	Name: "latency",
	Scope: instrumentation.Scope{
		Name:    "http",
		Version: "0.34.0",
	},
}, metric.Stream{Name: "request.latency"})

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

例如,以下是如何将http工具库中的latency工具报告为指数直方图的视图:

view := metric.NewView(
	metric.Instrument{
		Name:  "latency",
		Scope: instrumentation.Scope{Name: "http"},
	},
	metric.Stream{
		Aggregation: metric.AggregationBase2ExponentialHistogram{
			MaxSize:  160,
			MaxScale: 20,
		},
	},
)

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

SDK在导出指标之前会过滤指标和属性。例如,您可以使用视图来减少高基数指标的内存使用或删除可能包含敏感数据的属性。

以下是如何从http工具库中删除latency工具的视图:

view := metric.NewView(
  metric.Instrument{
    Name:  "latency",
    Scope: instrumentation.Scope{Name: "http"},
  },
  metric.Stream{Aggregation: metric.AggregationDrop{}},
)

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

以下是如何删除http工具库中latency工具录制的http.request.method属性的视图:

view := metric.NewView(
  metric.Instrument{
    Name:  "latency",
    Scope: instrumentation.Scope{Name: "http"},
  },
  metric.Stream{AttributeFilter: attribute.NewDenyKeysFilter("http.request.method")},
)

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

条件的Name字段支持通配符模式匹配。*通配符表示匹配零个或多个字符,?表示匹配一个字符。例如,*模式匹配所有工具名称。

以下示例显示了如何创建一个视图,该视图将毫秒作为单位设置为任何名称后缀为.ms的工具:

view := metric.NewView(
  metric.Instrument{Name: "*.ms"},
  metric.Stream{Unit: "ms"},
)

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

NewView函数提供了一种方便的创建视图的方法。如果NewView无法提供所需的功能,则可以直接创建自定义的View

例如,以下是如何创建一个视图,该视图使用正则表达式匹配来确保所有数据流名称都有其所使用的单位的后缀:

re := regexp.MustCompile(`[._](ms|byte)$`)
var view metric.View = func(i metric.Instrument) (metric.Stream, bool) {
	// 在自定义的View函数中,您需要显式复制名称、说明和单位。
	s := metric.Stream{Name: i.Name, Description: i.Description, Unit: i.Unit}
	// 对于没有定义单位后缀但定义了维度单位的任何仪表,更新名称以添加单位后缀。
	if re.MatchString(i.Name) {
		return s, false
	}
	switch i.Unit {
	case "ms":
		s.Name += ".ms"
	case "By":
		s.Name += ".byte"
	default:
		return s, false
	}
	return s, true
}

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

日志

日志API目前不稳定,文档待定。

后续步骤

您还需要配置适当的导出器来将您的遥测数据导出到一个或多个遥测后端。

最后修改 December 13, 2023: improve glossary translation (46f8201b)