构建接收器

OpenTelemetry 将 分布式追踪 定义为:

追踪请求在构成应用程序的服务之间传递的流程。请求可以由用户或应用程序发起。分布式追踪是一种跨越进程、网络和安全边界的追踪形式。

虽然分布式追踪是以以应用程序为中心的方式定义的,但您可以将其视为系统中的*任何*请求的时间线。每个分布式追踪都显示了请求从开始到结束所花费的时间,并分解了完成请求所采取的步骤。

如果您的系统生成了追踪遥测数据,您可以配置您的 OpenTelemetry Collector,使其包含一个专用于接收和转换该遥测数据的追踪接收器。接收器会将您的数据从其原始格式转换为 OpenTelemetry 追踪模型,以便 Collector 可以对其进行处理。

要实现追踪接收器,您需要以下内容:

  • 一个 Config 实现,以便追踪接收器能够收集和验证 Collector 配置 (config.yaml) 中的配置。

  • 一个 receiver.Factory 实现,以便 Collector 能够正确地实例化追踪接收器组件。

  • 一个 receiver.Traces 实现,该实现收集遥测数据,将其转换为内部追踪表示,并将遥测数据传递给管道中的下一个消费者。

本教程将向您展示如何创建一个名为 tailtracer 的追踪接收器,该接收器模拟一个拉取操作,并在此操作完成后生成追踪。

设置接收器开发和测试环境

首先,使用 构建自定义 Collector 教程创建一个名为 otelcol-dev 的 Collector 实例;您只需要复制 步骤 2 中描述的 builder-config.yaml 文件并运行构建器。完成后,您应该会看到类似以下的文件夹结构:

.
├── builder-config.yaml
├── ocb
└── otelcol-dev
    ├── components.go
    ├── components_test.go
    ├── go.mod
    ├── go.sum
    ├── main.go
    ├── main_others.go
    ├── main_windows.go
    └── otelcol-dev

为了正确测试您的追踪接收器,您可能需要一个分布式追踪后端,以便 Collector 可以将遥测数据发送到该后端。我们将使用 Jaeger。如果您没有运行 Jaeger 实例,可以使用 Docker 轻松启动一个,命令如下:

docker run -d --name jaeger \
  -e COLLECTOR_OTLP_ENABLED=true \
  -p 16686:16686 \
  -p 14317:4317 \
  -p 14318:4318 \
  jaegertracing/all-in-one:1.41

容器启动并运行后,您可以通过以下 URL 访问 Jaeger UI:https://:16686/

现在,创建一个名为 config.yaml 的 Collector 配置文件,以设置 Collector 组件和管道。

touch config.yaml

目前,您只需要一个包含 otlp 接收器以及 otlpdebug 导出的基本追踪管道。您的 config.yaml 文件应如下所示:

config.yaml

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

exporters:
  debug:
    verbosity: detailed
  otlp/jaeger:
    endpoint: localhost:14317
    tls:
      insecure: true
    sending_queue:
      batch:

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlp/jaeger, debug]
  telemetry:
    logs:
      level: debug

注意:在此,我们为了简化起见,在 otlp 导出器配置中使用了 insecure 标志;在生产环境中运行 Collector 时,您应该使用 TLS 证书进行安全通信,或使用 mTLS 进行双向身份验证,方法是遵循此 指南

要验证 Collector 是否已正确设置,请运行以下命令:

./otelcol-dev/otelcol-dev --config config.yaml

输出可能如下所示:

2023-11-08T18:38:37.183+0800	info	service@v0.88.0/telemetry.go:84	Setting up own telemetry...
2023-11-08T18:38:37.185+0800	info	service@v0.88.0/telemetry.go:201	Serving Prometheus metrics	{"address": ":8888", "level": "Basic"}
2023-11-08T18:38:37.185+0800	debug	exporter@v0.88.0/exporter.go:273	Stable component.	{"kind": "exporter", "data_type": "traces", "name": "otlp/jaeger"}
2023-11-08T18:38:37.186+0800	info	exporter@v0.88.0/exporter.go:275	Development component. May change in the future.	{"kind": "exporter", "data_type": "traces", "name": "debug"}
2023-11-08T18:38:37.186+0800	debug	receiver@v0.88.0/receiver.go:294	Stable component.	{"kind": "receiver", "name": "otlp", "data_type": "traces"}
2023-11-08T18:38:37.186+0800	info	service@v0.88.0/service.go:143	Starting otelcol-dev...	{"Version": "1.0.0", "NumCPU": 10}

<OMITTED>

2023-11-08T18:38:37.189+0800	info	service@v0.88.0/service.go:169	Everything is ready. Begin running and processing data.
2023-11-08T18:38:37.189+0800	info	zapgrpc/zapgrpc.go:178	[core] [Server #3 ListenSocket #4] ListenSocket created	{"grpc_log": true}
2023-11-08T18:38:37.195+0800	info	zapgrpc/zapgrpc.go:178	[core] [Channel #1 SubChannel #2] Subchannel Connectivity change to READY	{"grpc_log": true}
2023-11-08T18:38:37.195+0800	info	zapgrpc/zapgrpc.go:178	[core] [pick-first-lb 0x140005efdd0] Received SubConn state update: 0x140005eff80, {ConnectivityState:READY ConnectionError:<nil>}	{"grpc_log": true}
2023-11-08T18:38:37.195+0800	info	zapgrpc/zapgrpc.go:178	[core] [Channel #1] Channel Connectivity change to READY	{"grpc_log": true}

如果一切顺利,Collector 实例应该已启动并正在运行。

您可以使用 telemetrygen 进一步验证设置。例如,打开另一个控制台并运行以下命令:

go install github.com/open-telemetry/opentelemetry-collector-contrib/cmd/telemetrygen@latest

telemetrygen traces --otlp-insecure --traces 1

您应该能够在控制台中看到详细日志,并在 Jaeger UI 中通过以下 URL 查看追踪:https://:16686/

Ctrl + C 停止 Collector 控制台中的 Collector 实例。

设置 Go 模块

每个 Collector 组件都应创建为一个 Go 模块。让我们创建一个 tailtracer 文件夹来托管我们的接收器项目,并将其初始化为一个 Go 模块。

mkdir tailtracer
cd tailtracer
go mod init github.com/open-telemetry/opentelemetry-tutorials/trace-receiver/tailtracer

注意

  1. 上面的模块路径是一个模拟路径,可以是您想要的私有或公共路径。
  2. 请参阅 初始 trace-receiver 代码

由于我们将管理多个 Go 模块(otelcol-devtailtracer,以及未来可能更多的组件),因此建议启用 Go 工作区

cd ..
go work init
go work use otelcol-dev
go work use tailtracer

设计和验证接收器设置

接收器可能有一些可配置的设置,这些设置可以通过 Collector 配置文件进行设置。

tailtracer 接收器将具有以下设置:

  • interval:一个字符串,表示两次遥测拉取操作之间的时间间隔(以分钟为单位)。
  • number_of_traces:每次间隔生成的模拟追踪数量。

tailtracer 接收器的设置将如下所示:

receivers:
  tailtracer: # this line represents the ID of your receiver
    interval: 1m
    number_of_traces: 1

创建一个名为 config.go 的文件,放在 tailtracer 文件夹下,您将在其中编写所有代码来支持您的接收器设置。

touch tailtracer/config.go

要实现接收器的配置方面,您需要创建一个 Config 结构体。将以下代码添加到您的 config.go 文件中:

package tailtracer

type Config struct{

}

为了能够让您的接收器访问其设置,Config 结构体必须为接收器的每个设置都有一个字段。

实施上述要求后,config.go 文件应如下所示:

tailtracer/config.go

package tailtracer

// Config represents the receiver config settings in the Collector config.yaml
type Config struct {
   Interval    string `mapstructure:"interval"`
   NumberOfTraces int `mapstructure:"number_of_traces"`
}

现在您已经可以访问这些设置了,您可以通过实现可选的 ConfigValidator 接口来提供所需的任何值验证。

在这种情况下,interval 值将是可选的(稍后我们将讨论生成默认值)。但是,如果定义了,它应该至少是 1 分钟 (1m),而 number_of_traces 将是必需的值。实施 Validate 方法后,config.go 文件应如下所示:

tailtracer/config.go

package tailtracer

import (
	"fmt"
	"time"
)

// Config represents the receiver config settings in the Collector config.yaml
type Config struct {
	Interval       string `mapstructure:"interval"`
	NumberOfTraces int    `mapstructure:"number_of_traces"`
}

// Validate checks if the receiver configuration is valid
func (cfg *Config) Validate() error {
	interval, _ := time.ParseDuration(cfg.Interval)
	if interval.Minutes() < 1 {
		return fmt.Errorf("when defined, the interval has to be set to at least 1 minute (1m)")
	}

	if cfg.NumberOfTraces < 1 {
		return fmt.Errorf("number_of_traces must be greater or equal to 1")
	}
	return nil
}

如果您想更详细地了解组件配置方面所涉及的结构体和接口,请参阅 Collector GitHub 项目中的 component/config.go 文件。

实现 receiver.Factory 接口

tailtracer 接收器必须提供一个 receiver.Factory 实现。尽管 receiver.Factory 接口定义在 Collector 项目的 receiver/receiver.go 文件中,但正确的实现方式是使用 go.opentelemetry.io/collector/receiver 包中提供的函数。

创建一个名为 factory.go 的文件

touch tailtracer/factory.go

现在,我们遵循约定,添加一个名为 NewFactory() 的函数,该函数将负责实例化 tailtracer 工厂。请继续将以下代码添加到您的 factory.go 文件中:

package tailtracer

import (
	"go.opentelemetry.io/collector/receiver"
)

// NewFactory creates a factory for tailtracer receiver.
func NewFactory() receiver.Factory {
	return nil
}

要实例化您的 tailtracer 接收器工厂,您将使用 receiver 包中的以下函数:

func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefaultConfigFunc, options ...FactoryOption) Factory

receiver.NewFactory() 实例化并返回一个 receiver.Factory,它需要以下参数:

  • component.Type:您的接收器在所有 Collector 组件中唯一的字符串标识符。

  • component.CreateDefaultConfigFunc:一个指向函数的引用,该函数返回您接收器的 component.Config 实例。

  • ...FactoryOptionreceiver.FactoryOption 的切片,它将确定您的接收器能够处理的信号类型。

现在让我们来实现代码来支持 receiver.NewFactory() 所需的所有参数。

识别并提供默认设置

之前,我们提到 tailtracer 接收器的 interval 设置将是可选的。您需要为其提供一个默认值,以便它可以作为默认设置的一部分使用。

请继续将以下代码添加到您的 factory.go 文件中:

var (
	typeStr         = component.MustNewType("tailtracer")
)

const (
	defaultInterval = 1 * time.Minute
)

至于默认设置,您只需要添加一个函数,该函数返回一个 component.Config,其中包含 tailtracer 接收器的默认配置。

为此,请继续将以下代码添加到您的 factory.go 文件中:

func createDefaultConfig() component.Config {
	return &Config{
		Interval: string(defaultInterval),
	}
}

在进行这两项更改后,您会注意到缺少一些导入,因此,带有正确导入的 factory.go 文件应如下所示:

tailtracer/factory.go

package tailtracer

import (
	"time"

	"go.opentelemetry.io/collector/component"
	"go.opentelemetry.io/collector/receiver"
)

var (
	typeStr         = component.MustNewType("tailtracer")
)

const (
	defaultInterval = 1 * time.Minute
)

func createDefaultConfig() component.Config {
	return &Config{
		Interval: string(defaultInterval),
	}
}

// NewFactory creates a factory for tailtracer receiver.
func NewFactory() receiver.Factory {
	return nil
}

指定接收器的能力

接收器组件可以处理追踪、指标和日志。接收器工厂负责指定接收器将提供的能力。

鉴于追踪是本教程的主题,我们将使 tailtracer 接收器仅支持追踪。receiver 包提供了以下函数和类型来帮助工厂描述追踪处理能力:

func WithTraces(createTracesReceiver CreateTracesFunc, sl component.StabilityLevel) FactoryOption

receiver.WithTraces() 实例化并返回一个 receiver.FactoryOption,它需要以下参数:

  • createTracesReceiver:指向一个函数(匹配 receiver.CreateTracesFunc 类型)的引用。receiver.CreateTracesFunc 类型是指向一个函数的指针,该函数负责实例化并返回一个 receiver.Traces 实例,它需要以下参数:
    • context.Context:Collector context.Context 的引用,以便您的追踪接收器可以正确管理其执行上下文。
    • receiver.Settings:Collector 在其下创建接收器的某些设置的引用。
    • component.Config:Collector 传递给工厂的接收器配置设置的引用,以便它可以从 Collector 配置中正确读取其设置。
    • consumer.Traces:管道中下一个 consumer.Traces 的引用,接收到的追踪将被发送到这里。这要么是一个处理器,要么是一个导出器。

首先添加引导代码以正确实现 receiver.CreateTracesFunc 函数指针。请继续将以下代码添加到您的 factory.go 文件中:

func createTracesReceiver(_ context.Context, params receiver.Settings, baseCfg component.Config, consumer consumer.Traces) (receiver.Traces, error) {
	return nil, nil
}

现在您拥有成功实例化接收器工厂所需的所有组件,可以使用 receiver.NewFactory 函数。请继续更新 factory.go 文件中的 NewFactory() 函数,如下所示:

// NewFactory creates a factory for tailtracer receiver.
func NewFactory() receiver.Factory {
	return receiver.NewFactory(
		typeStr,
		createDefaultConfig,
		receiver.WithTraces(createTracesReceiver, component.StabilityLevelAlpha))
}

进行这些更改后,您会注意到缺少一些导入,因此,带有正确导入的 factory.go 文件应如下所示:

tailtracer/factory.go

package tailtracer

import (
	"context"
	"time"

	"go.opentelemetry.io/collector/component"
	"go.opentelemetry.io/collector/consumer"
	"go.opentelemetry.io/collector/receiver"
)

var (
	typeStr         = component.MustNewType("tailtracer")
)

const (
	defaultInterval = 1 * time.Minute
)

func createDefaultConfig() component.Config {
	return &Config{
		Interval: string(defaultInterval),
	}
}

func createTracesReceiver(_ context.Context, params receiver.Settings, baseCfg component.Config, consumer consumer.Traces) (receiver.Traces, error) {
	return nil, nil
}

// NewFactory creates a factory for tailtracer receiver.
func NewFactory() receiver.Factory {
	return receiver.NewFactory(
		typeStr,
		createDefaultConfig,
		receiver.WithTraces(createTracesReceiver, component.StabilityLevelAlpha))
}

实现接收器组件

所有接收器 API 目前都声明在 Collector 项目的 receiver/receiver.go 文件中。打开该文件,花点时间浏览所有接口。

请注意,此时 receiver.Traces(及其同级 receiver.Metricsreceiver.Logs)除了它“继承”自 component.Component 的方法之外,并没有描述任何特定的方法。

这可能感觉有些奇怪,但请记住,Collector API 最初是为了可扩展性而设计的。组件及其信号可以以不同的方式演变,因此这些接口的作用是支持这一点。

要创建 receiver.Traces,您需要实现 component.Component 接口描述的以下方法:

Start(ctx context.Context, host Host) error
Shutdown(ctx context.Context) error

这两种方法都充当事件处理程序,Collector 在其生命周期的各个阶段使用它们与组件进行通信。

Start() 方法表示 Collector 告知组件开始处理的信号。作为事件的一部分,Collector 将传递以下信息:

  • context.Context:大多数情况下,接收器将处理一个长时间运行的操作,因此建议忽略此上下文,而是创建一个新的上下文(例如,从 context.Background())。
  • Host:Host 用于使接收器在 Collector 主机启动并运行时能够与之通信。

Shutdown() 方法表示 Collector 告知组件服务即将关闭的信号,因此组件应停止处理并执行所有必要的清理工作。

  • context.Context:Collector 在关闭操作中作为一部分传递的上下文。

您将通过在 tailtracer 文件夹中创建一个名为 trace-receiver.go 的新文件来开始实现。

touch tailtracer/trace-receiver.go

然后,像这样为名为 tailtracerReceiver 的类型添加声明:

type tailtracerReceiver struct{

}

现在您已经有了 tailtracerReceiver 类型,可以实现 Start()Shutdown() 方法,以便该接收器类型符合 receiver.Traces 接口。

tailtracer/trace-receiver.go

package tailtracer

import (
	"context"
	"go.opentelemetry.io/collector/component"
)

type tailtracerReceiver struct {
}

func (tailtracerRcvr *tailtracerReceiver) Start(ctx context.Context, host component.Host) error {
	return nil
}

func (tailtracerRcvr *tailtracerReceiver) Shutdown(ctx context.Context) error {
	return nil
}

Start() 方法传递了 2 个引用(context.Contextcomponent.Host),您的接收器可能需要保留这些引用,以便在处理操作中使用它们。

context.Context 引用应仅用于创建新上下文以支持接收器处理操作。您需要决定如何处理上下文取消,以便在 Shutdown() 方法中作为组件关闭的一部分正确完成。

component.Host 在接收器的整个生命周期中都可能很有用,因此请将此引用保留在 tailtracerReceiver 类型中。

在包含上述建议的引用的字段后,tailtracerReceiver 类型声明将如下所示:

type tailtracerReceiver struct {
	host   component.Host
	cancel context.CancelFunc
}

现在您需要更新 Start() 方法,以便接收器可以正确初始化自己的处理上下文,将取消函数保存在 cancel 字段中,并初始化其 host 字段值。您还将更新 Stop() 方法以通过调用 cancel 函数来完成上下文。

进行更改后,trace-receiver.go 文件将如下所示:

tailtracer/trace-receiver.go

package tailtracer

import (
	"context"
	"go.opentelemetry.io/collector/component"
)

type tailtracerReceiver struct {
	host   component.Host
	cancel context.CancelFunc
}

func (tailtracerRcvr *tailtracerReceiver) Start(ctx context.Context, host component.Host) error {
	tailtracerRcvr.host = host
	ctx = context.Background()
	ctx, tailtracerRcvr.cancel = context.WithCancel(ctx)

	return nil
}

func (tailtracerRcvr *tailtracerReceiver) Shutdown(ctx context.Context) error {
	if tailtracerRcvr.cancel != nil {
		tailtracerRcvr.cancel()
	}
	return nil
}

保留接收器工厂传递的信息

现在您已经实现了 receiver.Traces 接口方法,您的 tailtracer 接收器组件已准备好由其工厂实例化和返回。

打开 tailtracer/factory.go 文件并导航到 createTracesReceiver() 函数。请注意,工厂将在 createTracesReceiver() 函数参数中传递您的接收器正常工作所需的引用。这些引用包括其配置设置 (component.Config)、将消耗生成的追踪的下一个 Consumer (consumer.Traces) 以及 Collector 日志记录器。这样,tailtracer 接收器就可以向其添加有意义的事件(receiver.Settings)。

鉴于所有这些信息只有在工厂实例化接收器时才会提供,因此 tailtracerReceiver 类型将需要字段来保留这些信息并在其生命周期的其他阶段使用它们。

更新后的 tailtracerReceiver 类型声明将使 trace-receiver.go 文件如下所示:

tailtracer/trace-receiver.go

package tailtracer

import (
	"context"
	"time"
	"go.opentelemetry.io/collector/component"
	"go.opentelemetry.io/collector/consumer"
	"go.uber.org/zap"
)

type tailtracerReceiver struct {
	host         component.Host
	cancel       context.CancelFunc
	logger       *zap.Logger
	nextConsumer consumer.Traces
	config       *Config
}

func (tailtracerRcvr *tailtracerReceiver) Start(ctx context.Context, host component.Host) error {
	tailtracerRcvr.host = host
	ctx = context.Background()
	ctx, tailtracerRcvr.cancel = context.WithCancel(ctx)

	interval, _ := time.ParseDuration(tailtracerRcvr.config.Interval)
	go func() {
		ticker := time.NewTicker(interval)
		defer ticker.Stop()

		for {
			select {
				case <-ticker.C:
					tailtracerRcvr.logger.Info("I should start processing traces now!")
				case <-ctx.Done():
					return
			}
		}
	}()

	return nil
}

func (tailtracerRcvr *tailtracerReceiver) Shutdown(ctx context.Context) error {
	if tailtracerRcvr.cancel != nil {
		tailtracerRcvr.cancel()
	}
	return nil
}

tailtracerReceiver 类型已准备好进行实例化,并将保留其工厂传递的所有有意义的信息。

打开 tailtracer/factory.go 文件并导航到 createTracesReceiver() 函数。

仅当接收器在管道中声明为组件时,才会实例化接收器,工厂负责确保管道中的下一个消费者(处理器或导出器)有效。否则,它应该生成一个错误。

createTracesReceiver() 函数需要一个保护子句来执行此验证。

您还需要变量来正确初始化 tailtracerReceiver 实例的 configlogger 字段。

更新后的 createTracesReceiver() 函数将使 factory.go 文件如下所示:

tailtracer/factory.go

package tailtracer

import (
	"context"
	"time"

	"go.opentelemetry.io/collector/component"
	"go.opentelemetry.io/collector/consumer"
	"go.opentelemetry.io/collector/receiver"
)

var (
	typeStr         = component.MustNewType("tailtracer")
)

const (
	defaultInterval = 1 * time.Minute
)

func createDefaultConfig() component.Config {
	return &Config{
		Interval: string(defaultInterval),
	}
}

func createTracesReceiver(_ context.Context, params receiver.Settings, baseCfg component.Config, consumer consumer.Traces) (receiver.Traces, error) {

	logger := params.Logger
	tailtracerCfg := baseCfg.(*Config)

	traceRcvr := &tailtracerReceiver{
		logger:       logger,
		nextConsumer: consumer,
		config:       tailtracerCfg,
	}

	return traceRcvr, nil
}

// NewFactory creates a factory for tailtracer receiver.
func NewFactory() receiver.Factory {
	return receiver.NewFactory(
		typeStr,
		createDefaultConfig,
		receiver.WithTraces(createTracesReceiver, component.StabilityLevelAlpha))
}

到目前为止,接收器的框架已完全实现。

使用接收器更新 Collector 初始化流程

为了让接收器参与 Collector 管道,我们需要对生成的 otelcol-dev/components.go 文件进行一些更新,所有 Collector 组件都在该文件中注册和实例化。

必须将 tailtracer 接收器工厂实例添加到 factories 映射中,以便 Collector 可以在其初始化过程中将其正确加载。

支持这些更改后,components.go 文件将如下所示:

otelcol-dev/components.go

// Code generated by "go.opentelemetry.io/collector/cmd/builder". DO NOT EDIT.

package main

import (
	"go.opentelemetry.io/collector/exporter"
	"go.opentelemetry.io/collector/extension"
	"go.opentelemetry.io/collector/otelcol"
	"go.opentelemetry.io/collector/processor"
	"go.opentelemetry.io/collector/receiver"
	debugexporter "go.opentelemetry.io/collector/exporter/debugexporter"
	otlpexporter "go.opentelemetry.io/collector/exporter/otlpexporter"
	otlpreceiver "go.opentelemetry.io/collector/receiver/otlpreceiver"
	tailtracer "github.com/open-telemetry/opentelemetry-tutorials/trace-receiver/tailtracer" // newly added line
)

func components() (otelcol.Factories, error) {
	var err error
	factories := otelcol.Factories{}

	factories.Extensions, err = otelcol.MakeFactoryMap[extension.Factory](
	)
	if err != nil {
		return otelcol.Factories{}, err
	}

	factories.Receivers, err = otelcol.MakeFactoryMap[receiver.Factory](
		otlpreceiver.NewFactory(),
		tailtracer.NewFactory(), // newly added line
	)
	if err != nil {
		return otelcol.Factories{}, err
	}

	factories.Exporters, err = otelcol.MakeFactoryMap[exporter.Factory](
		debugexporter.NewFactory(),
		otlpexporter.NewFactory(),
	)
	if err != nil {
		return otelcol.Factories{}, err
	}

	factories.Processors, err = otelcol.MakeFactoryMap[processor.Factory](
	)
	if err != nil {
		return otelcol.Factories{}, err
	}

	return factories, nil
}

运行和调试接收器

确保 Collector config.yaml 已使用 tailtracer 接收器(配置为管道中的接收器之一)正确更新。

config.yaml

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
  tailtracer: # this line represents the ID of your receiver
    interval: 1m
    number_of_traces: 1

exporters:
  debug:
    verbosity: detailed
  otlp/jaeger:
    endpoint: localhost:14317
    tls:
      insecure: true
    sending_queue:
      batch:

service:
  pipelines:
    traces:
      receivers: [otlp, tailtracer]
      exporters: [otlp/jaeger, debug]
  telemetry:
    logs:
      level: debug

由于 otelcol-dev/components.go 文件中的代码发生了更改,我们将使用 go run 命令而不是之前生成的 ./otelcol-dev/otelcol-dev 二进制文件来启动更新后的 Collector。

go run ./otelcol-dev --config config.yaml

输出应如下所示:

2023-11-08T21:38:36.621+0800	info	service@v0.88.0/telemetry.go:84	Setting up own telemetry...
2023-11-08T21:38:36.621+0800	info	service@v0.88.0/telemetry.go:201	Serving Prometheus metrics	{"address": ":8888", "level": "Basic"}
2023-11-08T21:38:36.621+0800	info	exporter@v0.88.0/exporter.go:275	Development component. May change in the future.	{"kind": "exporter", "data_type": "traces", "name": "debug"}
2023-11-08T21:38:36.621+0800	debug	exporter@v0.88.0/exporter.go:273	Stable component.	{"kind": "exporter", "data_type": "traces", "name": "otlp/jaeger"}
2023-11-08T21:38:36.621+0800	debug	receiver@v0.88.0/receiver.go:294	Stable component.	{"kind": "receiver", "name": "otlp", "data_type": "traces"}
2023-11-08T21:38:36.621+0800	debug	receiver@v0.88.0/receiver.go:294	Alpha component. May change in the future.	{"kind": "receiver", "name": "tailtracer", "data_type": "traces"}
2023-11-08T21:38:36.622+0800	info	service@v0.88.0/service.go:143	Starting otelcol-dev...	{"Version": "1.0.0", "NumCPU": 10}
2023-11-08T21:38:36.622+0800	info	extensions/extensions.go:33	Starting extensions...

<OMITTED>

2023-11-08T21:38:36.636+0800	info	zapgrpc/zapgrpc.go:178	[core] [Channel #1] Channel Connectivity change to READY	{"grpc_log": true}
2023-11-08T21:39:36.626+0800	info	tailtracer/trace-receiver.go:33	I should start processing traces now!	{"kind": "receiver", "name": "tailtracer", "data_type": "traces"}
2023-11-08T21:40:36.626+0800	info	tailtracer/trace-receiver.go:33	I should start processing traces now!	{"kind": "receiver", "name": "tailtracer", "data_type": "traces"}
...

从日志中可以看到,tailtracer 已成功初始化。每分钟,都会出现一条消息:“I should start processing traces now!”,该消息由 tailtracer/trace-receiver.go 中的虚拟计时器触发。

注意:您可以通过在 Collector 终端中按 Ctrl + C 来随时停止进程。

此外,您可以使用您选择的 IDE 来调试接收器,就像您正常调试 Go 项目一样。以下是 Visual Studio Code 的一个简单 launch.json 文件供您参考:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch otelcol-dev",
      "type": "go",
      "request": "launch",
      "mode": "auto",
      "program": "${workspaceFolder}/otelcol-dev",
      "args": ["--config", "${workspaceFolder}/config.yaml"]
    }
  ]
}

作为重要的里程碑,让我们看一下现在的文件夹结构:

.
├── builder-config.yaml
├── config.yaml
├── go.work
├── go.work.sum
├── ocb
├── otelcol-dev
│   ├── components.go
│   ├── components_test.go
│   ├── go.mod
│   ├── go.sum
│   ├── main.go
│   ├── main_others.go
│   ├── main_windows.go
│   └── otelcol-dev
└── tailtracer
    ├── config.go
    ├── factory.go
    ├── go.mod
    └── trace-receiver.go

在下一节中,您将了解更多关于 OpenTelemetry Trace 数据模型的信息,以便 tailtracer 接收器最终能够生成有意义的追踪!

Collector Trace 数据模型

您可能熟悉 OpenTelemetry 追踪,您可以通过使用 SDK 并对应用程序进行插桩,以便在 Jaeger 等分布式追踪后端中观察和评估您的追踪。

Jaeger 中的追踪如下所示:

Jaeger trace

虽然这是一个 Jaeger 追踪,但它是由 Collector 中的追踪管道生成的。这可以帮助您了解 OTel 追踪数据模型的一些内容:

  • 一个追踪包含一个或多个以层级结构组织的 Span,以表示依赖关系。
  • Span 可以表示服务内部和/或跨服务的操作。

在追踪接收器中创建追踪的方式将与使用 SDK 创建的方式略有不同,因此让我们从回顾高级概念开始。

处理资源

在 OTel 世界中,所有遥测数据都由一个 Resource 生成。根据 OTel 规范,定义如下:

Resource 是生成遥测数据的实体的不可变表示,以属性的形式呈现。例如,一个在 Kubernetes 容器中运行并生成遥测数据的进程具有 Pod 名称,运行在命名空间中,并且可能是一个属于具有自己名称的 Deployment 的一部分。这三个属性都可以包含在 Resource 中。

追踪最常用于表示服务请求(Jaeger 模型中描述的服务实体),该请求通常实现为在计算单元中运行的进程。然而,OTel 通过属性描述 Resource 的 API 方法足够灵活,可以表示您需要的任何实体,例如 ATM、IoT 传感器等等。

因此,可以安全地说,为了让追踪存在,Resource 必须先启动它。

在本教程中,我们将模拟一个带有遥测数据的系统,该系统演示了位于两个不同州(例如,伊利诺伊州和加利福尼亚州)的 ATM 访问账户的后端系统以执行余额、存款和取款操作。为了实现这一点,我们将实现代码来创建表示 ATM 和后端系统的 Resource 类型。

请继续在 tailtracer 文件夹中创建一个名为 model.go 的文件。

touch tailtracer/model.go

现在,在 model.go 文件中,添加 AtmBackendSystem 类型的定义如下:

tailtracer/model.go

package tailtracer

type Atm struct{
	ID           int64
	Version      string
	Name         string
	StateID      string
	SerialNumber string
	ISPNetwork   string
}

type BackendSystem struct{
	Version       string
	ProcessName   string
	OSType        string
	OSVersion     string
	CloudProvider string
	CloudRegion   string
	Endpoint      string
}

这些类型旨在表示系统中正在被观测的实体。它们包含可以作为 Resource 定义一部分添加到追踪中的有意义信息。您将添加一些辅助函数来生成这些类型的实例。

包含添加的辅助函数的 model.go 文件将如下所示:

tailtracer/model.go

package tailtracer

import (
	"math/rand"
	"time"
)

type Atm struct{
	ID           int64
	Version      string
	Name         string
	StateID      string
	SerialNumber string
	ISPNetwork   string
}

type BackendSystem struct{
	Version       string
	ProcessName   string
	OSType        string
	OSVersion     string
	CloudProvider string
	CloudRegion   string
	Endpoint      string
}

func generateAtm() Atm{
	i := getRandomNumber(1, 2)
	var newAtm Atm

	switch i {
		case 1:
			newAtm = Atm{
				ID: 111,
				Name: "ATM-111-IL",
				SerialNumber: "atmxph-2022-111",
				Version: "v1.0",
				ISPNetwork: "comcast-chicago",
				StateID: "IL",

			}

		case 2:
			newAtm = Atm{
				ID: 222,
				Name: "ATM-222-CA",
				SerialNumber: "atmxph-2022-222",
				Version: "v1.0",
				ISPNetwork: "comcast-sanfrancisco",
				StateID: "CA",
			}
	}

	return newAtm
}

func generateBackendSystem() BackendSystem{
	i := getRandomNumber(1, 3)

	newBackend := BackendSystem{
		ProcessName: "accounts",
		Version: "v2.5",
		OSType: "lnx",
		OSVersion: "4.16.10-300.fc28.x86_64",
		CloudProvider: "amzn",
		CloudRegion: "us-east-2",
	}

	switch i {
		case 1:
		 	newBackend.Endpoint = "api/v2.5/balance"
		case 2:
		  	newBackend.Endpoint = "api/v2.5/deposit"
		case 3:
			newBackend.Endpoint = "api/v2.5/withdrawn"

	}

	return newBackend
}

func getRandomNumber(min int, max int) int {
	rand.Seed(time.Now().UnixNano())
	i := (rand.Intn(max - min + 1) + min)
	return i
}

现在您已经有了生成表示遥测源的对象的实例的函数,您就可以在 OTel Collector 世界中表示这些实体了。

Collector API 提供了一个名为 ptrace 的包,它嵌套在 pdata 包下。它包含了在 Collector 管道组件中使用追踪所需的所有类型、接口和辅助函数。

打开 tailtracer/model.go 文件,并将 go.opentelemetry.io/collector/pdata/ptrace 添加到 import 子句中,以便您可以访问 ptrace 包的功能。

在定义 Resource 之前,您需要创建一个 ptrace.Traces,它将负责通过 Collector 管道传播追踪。您可以使用辅助函数 ptrace.NewTraces() 来实例化它。您还需要创建 AtmBackendSystem 类型的实例,以便您拥有表示追踪中涉及的遥测源的数据。

打开 tailtracer/model.go 文件并向其中添加以下函数:

func generateTraces(numberOfTraces int) ptrace.Traces{
	traces := ptrace.NewTraces()

	for i := 0; i <= numberOfTraces; i++{
		newAtm := generateAtm()
		newBackendSystem := generateBackendSystem()
	}

	return traces
}

到目前为止,您已经听过和读过足够多的关于跟踪(trace)是如何由跨度(span)组成的。您甚至可能已经编写了一些使用 SDK 函数和类型来创建它们的检测代码。然而,您可能不知道的是,在 Collector API 中创建跟踪还涉及其他类型的“跨度”。

您将从一个名为 ptrace.ResourceSpans 的类型开始,它代表资源以及该资源在跟踪中产生或接收的所有操作。您可以在 /pdata/ptrace/generated_resourcespans.go 文件中找到它的定义。

ptrace.Traces 有一个名为 ResourceSpans() 的方法,它返回一个名为 ptrace.ResourceSpansSlice 的辅助类型的实例。ptrace.ResourceSpansSlice 类型的方法可以帮助您处理 ptrace.ResourceSpans 的数组。该数组将包含参与跟踪所代表的请求的 Resource 实体的数量。

ptrace.ResourceSpansSlice 有一个名为 AppendEmpty() 的方法,它向数组添加一个新的 ptrace.ResourceSpan 并返回其引用。

一旦您拥有了 ptrace.ResourceSpan 的实例,您将使用一个名为 Resource() 的方法,该方法将返回与 ResourceSpan 关联的 pcommon.Resource 的实例。

使用以下更改更新 generateTrace() 函数

  • 添加一个名为 resourceSpan 的变量来表示 ResourceSpan
  • 添加一个名为 atmResource 的变量来表示与 ResourceSpan 关联的 pcommon.Resource
  • 分别使用上述方法来初始化这两个变量。

以下是实现更改后该函数的外观

func generateTraces(numberOfTraces int) ptrace.Traces{
	traces := ptrace.NewTraces()

	for i := 0; i <= numberOfTraces; i++{
		newAtm := generateAtm()
		newBackendSystem := generateBackendSystem()

		resourceSpan := traces.ResourceSpans().AppendEmpty()
		atmResource := resourceSpan.Resource()
	}

	return traces
}

通过属性描述资源

Collector API 提供了一个名为 pcommon 的包,它嵌套在 pdata 包下。它包含了描述 Resource 所需的所有类型和辅助函数。

在 Collector 的上下文中,Resource 由键/值对格式的属性来描述,这些属性由 pcommon.Map 类型表示。

您可以在 Collector GitHub 项目的 /pdata/pcommon/map.go 文件中,参考 pcommon.Map 类型及其相关辅助函数来创建使用支持格式的属性值。

键/值对提供了很大的灵活性,可以帮助您建模 Resource 数据。OTel 规范有一些指导方针,可以帮助组织和最小化它可能需要表示的所有不同类型的遥测生成实体之间的冲突。

这些指导方针被称为 Resource Semantic Conventions(资源语义约定),并在 OTel 规范中有记载。

在创建自己的属性来表示您自己的遥测生成实体时,您应该遵循规范提供的指导方针。

属性根据它们描述的概念类型进行逻辑分组。同一组中的属性具有一个以点结尾的公共前缀。例如,所有描述 Kubernetes 属性的属性都以 k8s. 开头。

让我们从打开 tailtracer/model.go 文件开始,并将 go.opentelemetry.io/collector/pdata/pcommon 添加到 import 子句中,以便您可以访问 pcommon 包的功能。

现在,继续添加一个函数,用于从 Atm 实例读取字段值,并将它们作为属性(按“atm.”前缀分组)写入 pcommon.Resource 实例。以下是该函数的外观

func fillResourceWithAtm(resource *pcommon.Resource, atm Atm){
   atmAttrs := resource.Attributes()
   atmAttrs.PutInt("atm.id", atm.ID)
   atmAttrs.PutStr("atm.stateid", atm.StateID)
   atmAttrs.PutStr("atm.ispnetwork", atm.ISPNetwork)
   atmAttrs.PutStr("atm.serialnumber", atm.SerialNumber)
}

资源语义约定还为表示常见的、适用于不同领域的遥测生成实体的属性名称和众所周知的(well-known)值,例如 计算单元环境 等。

对于 BackendSystem 实体,它具有代表 操作系统 的信息字段。我们将使用资源语义约定指定的属性名称和值来表示其 Resource 上的这些信息。

所有资源语义约定属性名称和众所周知的(well-known)值都保存在 Collector GitHub 项目的 /semconv/v1.9.0/generated_resource.go 文件中。

让我们创建一个函数来读取 BackendSystem 实例的字段值,并将它们作为属性写入 pcommon.Resource 实例。打开 tailtracer/model.go 文件并添加以下函数。

func fillResourceWithBackendSystem(resource *pcommon.Resource, backend BackendSystem){
	backendAttrs := resource.Attributes()
	var osType, cloudProvider string

	switch {
		case backend.CloudProvider == "amzn":
			cloudProvider = conventions.AttributeCloudProviderAWS
		case backend.OSType == "mcrsft":
			cloudProvider = conventions.AttributeCloudProviderAzure
		case backend.OSType == "gogl":
			cloudProvider = conventions.AttributeCloudProviderGCP
	}

	backendAttrs.PutStr(conventions.AttributeCloudProvider, cloudProvider)
	backendAttrs.PutStr(conventions.AttributeCloudRegion, backend.CloudRegion)

	switch {
		case backend.OSType == "lnx":
			osType = conventions.AttributeOSTypeLinux
		case backend.OSType == "wndws":
			osType = conventions.AttributeOSTypeWindows
		case backend.OSType == "slrs":
			osType = conventions.AttributeOSTypeSolaris
	}

	backendAttrs.PutStr(conventions.AttributeOSType, osType)
	backendAttrs.PutStr(conventions.AttributeOSVersion, backend.OSVersion)
 }

请注意,我们没有向代表 AtmBackendSystem 实体名称的 pcommon.Resource 添加名为“atm.name”或“backendsystem.name”的属性。这是因为大多数(如果不是全部)与 OTel 跟踪规范兼容的分布式跟踪后端系统会将跟踪中描述的 pcommon.Resource 解释为 Service。因此,它们期望 pcommon.Resource 具有一个名为 service.name 的必需属性,正如资源语义约定所规定的一样。

我们还将使用一个非必需属性 service.version 来表示 AtmBackendSystem 实体的版本信息。

以下是添加代码以正确分配“service.”组属性后 tailtracer/model.go 文件的外观。

tailtracer/model.go

package tailtracer

import (
	"math/rand"
	"time"

	"go.opentelemetry.io/collector/pdata/pcommon"
	"go.opentelemetry.io/collector/pdata/ptrace"
	conventions "go.opentelemetry.io/collector/semconv/v1.9.0"
)

type Atm struct {
	ID           int64
	Version      string
	Name         string
	StateID      string
	SerialNumber string
	ISPNetwork   string
}

type BackendSystem struct {
	Version       string
	ProcessName   string
	OSType        string
	OSVersion     string
	CloudProvider string
	CloudRegion   string
	Endpoint      string
}

func generateAtm() Atm {
	i := getRandomNumber(1, 2)
	var newAtm Atm

	switch i {
	case 1:
		newAtm = Atm{
			ID:           111,
			Name:         "ATM-111-IL",
			SerialNumber: "atmxph-2022-111",
			Version:      "v1.0",
			ISPNetwork:   "comcast-chicago",
			StateID:      "IL",
		}

	case 2:
		newAtm = Atm{
			ID:           222,
			Name:         "ATM-222-CA",
			SerialNumber: "atmxph-2022-222",
			Version:      "v1.0",
			ISPNetwork:   "comcast-sanfrancisco",
			StateID:      "CA",
		}
	}

	return newAtm
}

func generateBackendSystem() BackendSystem {
	i := getRandomNumber(1, 3)

	newBackend := BackendSystem{
		ProcessName:   "accounts",
		Version:       "v2.5",
		OSType:        "lnx",
		OSVersion:     "4.16.10-300.fc28.x86_64",
		CloudProvider: "amzn",
		CloudRegion:   "us-east-2",
	}

	switch i {
	case 1:
		newBackend.Endpoint = "api/v2.5/balance"
	case 2:
		newBackend.Endpoint = "api/v2.5/deposit"
	case 3:
		newBackend.Endpoint = "api/v2.5/withdrawn"
	}

	return newBackend
}

func getRandomNumber(min int, max int) int {
	rand.Seed(time.Now().UnixNano())
	i := (rand.Intn(max-min+1) + min)
	return i
}

func generateTraces(numberOfTraces int) ptrace.Traces {
	traces := ptrace.NewTraces()

	for i := 0; i <= numberOfTraces; i++ {
		newAtm := generateAtm()
		newBackendSystem := generateBackendSystem()

		resourceSpan := traces.ResourceSpans().AppendEmpty()
		atmResource := resourceSpan.Resource()
		fillResourceWithAtm(&atmResource, newAtm)

		resourceSpan = traces.ResourceSpans().AppendEmpty()
		backendResource := resourceSpan.Resource()
		fillResourceWithBackendSystem(&backendResource, newBackendSystem)
	}

	return traces
}

func fillResourceWithAtm(resource *pcommon.Resource, atm Atm) {
	atmAttrs := resource.Attributes()
	atmAttrs.PutInt("atm.id", atm.ID)
	atmAttrs.PutStr("atm.stateid", atm.StateID)
	atmAttrs.PutStr("atm.ispnetwork", atm.ISPNetwork)
	atmAttrs.PutStr("atm.serialnumber", atm.SerialNumber)
	atmAttrs.PutStr(conventions.AttributeServiceName, atm.Name)
	atmAttrs.PutStr(conventions.AttributeServiceVersion, atm.Version)

}

func fillResourceWithBackendSystem(resource *pcommon.Resource, backend BackendSystem) {
	backendAttrs := resource.Attributes()
	var osType, cloudProvider string

	switch {
	case backend.CloudProvider == "amzn":
		cloudProvider = conventions.AttributeCloudProviderAWS
	case backend.OSType == "mcrsft":
		cloudProvider = conventions.AttributeCloudProviderAzure
	case backend.OSType == "gogl":
		cloudProvider = conventions.AttributeCloudProviderGCP
	}

	backendAttrs.PutStr(conventions.AttributeCloudProvider, cloudProvider)
	backendAttrs.PutStr(conventions.AttributeCloudRegion, backend.CloudRegion)

	switch {
	case backend.OSType == "lnx":
		osType = conventions.AttributeOSTypeLinux
	case backend.OSType == "wndws":
		osType = conventions.AttributeOSTypeWindows
	case backend.OSType == "slrs":
		osType = conventions.AttributeOSTypeSolaris
	}

	backendAttrs.PutStr(conventions.AttributeOSType, osType)
	backendAttrs.PutStr(conventions.AttributeOSVersion, backend.OSVersion)

	backendAttrs.PutStr(conventions.AttributeServiceName, backend.ProcessName)
	backendAttrs.PutStr(conventions.AttributeServiceVersion, backend.Version)
}

用跨度表示操作

您现在拥有一个 ResourceSpan 实例,其中相应的 Resource 已正确填充了属性以表示 AtmBackendSystem 实体。您现在已准备好在 ResourceSpan 中表示每个 Resource 作为跟踪一部分执行的操作。

在 OTel 世界中,系统要生成遥测数据,需要手动或通过检测库自动进行检测。

检测库负责设置发生操作的范围(也称为检测范围),并在跟踪的上下文中将这些操作描述为跨度。

pdata.ResourceSpans 有一个名为 ScopeSpans() 的方法,它返回一个名为 ptrace.ScopeSpansSlice 的辅助类型的实例。ptrace.ScopeSpansSlice 类型的方法可以帮助您处理 ptrace.ScopeSpans 的数组。该数组将包含表示不同检测范围及其在跟踪上下文中生成的跨度的 ptrace.ScopeSpan 的数量。

ptrace.ScopeSpansSlice 有一个名为 AppendEmpty() 的方法,它向数组添加一个新的 ptrace.ScopeSpans 并返回其引用。

让我们创建一个函数来实例化一个 ptrace.ScopeSpans,它代表 ATM 系统的检测范围及其跨度。打开 tailtracer/model.go 文件并添加以下函数。

func appendAtmSystemInstrScopeSpans(resourceSpans *ptrace.ResourceSpans) ptrace.ScopeSpans {
	scopeSpans := resourceSpans.ScopeSpans().AppendEmpty()

	return scopeSpans
}

ptrace.ScopeSpans 有一个名为 Scope() 的方法,它返回一个指向 pcommon.InstrumentationScope 实例的引用,该实例代表生成跨度的检测范围。

pcommon.InstrumentationScope 具有以下方法来描述检测范围:

  • SetName(v string) 设置检测库的名称。

  • SetVersion(v string) 设置检测库的版本。

  • Name() string 返回与检测库关联的名称。

  • Version() string 返回与检测库关联的版本。

让我们更新 appendAtmSystemInstrScopeSpans 函数,以便我们可以为新的 ptrace.ScopeSpans 设置检测范围的名称和版本。以下是更新后的 appendAtmSystemInstrScopeSpans 的外观。

func appendAtmSystemInstrScopeSpans(resourceSpans *ptrace.ResourceSpans) ptrace.ScopeSpans {
	scopeSpans := resourceSpans.ScopeSpans().AppendEmpty()
	scopeSpans.Scope().SetName("atm-system")
	scopeSpans.Scope().SetVersion("v1.0")
	return scopeSpans
}

您现在可以更新 generateTraces 函数,并通过调用 appendAtmSystemInstrScopeSpans() 来添加变量来表示 AtmBackendSystem 实体使用的检测范围。以下是更新后的 generateTraces() 的外观。

func generateTraces(numberOfTraces int) ptrace.Traces{
	traces := ptrace.NewTraces()

	for i := 0; i <= numberOfTraces; i++{
		newAtm := generateAtm()
		newBackendSystem := generateBackendSystem()

		resourceSpan := traces.ResourceSpans().AppendEmpty()
		atmResource := resourceSpan.Resource()
		fillResourceWithAtm(&atmResource, newAtm)

		atmInstScope := appendAtmSystemInstrScopeSpans(&resourceSpan)

		resourceSpan = traces.ResourceSpans().AppendEmpty()
		backendResource := resourceSpan.Resource()
		fillResourceWithBackendSystem(&backendResource, newBackendSystem)

		backendInstScope := appendAtmSystemInstrScopeSpans(&resourceSpan)
	}

	return traces
}

至此,您已拥有表示系统中遥测生成实体以及负责标识操作和为系统生成跟踪的检测范围所需的一切。下一步是创建代表给定检测范围作为跟踪一部分生成的跟踪的操作的跨度。

ptrace.ScopeSpans 有一个名为 Spans() 的方法,它返回一个名为 ptrace.SpanSlice 的辅助类型的实例。ptrace.SpanSlice 类型的方法可以帮助您处理 ptrace.Span 的数组。该数组将包含检测范围在跟踪中识别和描述的操作的 ptrace.Span 的数量。

ptrace.SpanSlice 有一个名为 AppendEmpty() 的方法,它向数组添加一个新的 ptrace.Span 并返回其引用。

ptrace.Span 具有以下方法来描述操作:

  • SetTraceID(v pcommon.TraceID) 设置唯一标识此跨度所属跟踪的 pcommon.TraceID

  • SetSpanID(v pcommon.SpanID) 设置唯一标识此跨度在所属跟踪中的 pcommon.SpanID

  • SetParentSpanID(v pcommon.SpanID) 设置父跨度/操作的 pcommon.SpanID,以防此跨度代表的操作作为父操作(嵌套)的一部分执行。

  • SetName(v string) 设置跨度的操作名称。

  • SetKind(v ptrace.SpanKind) 设置 ptrace.SpanKind,定义跨度代表的操作类型。

  • SetStartTimestamp(v pcommon.Timestamp) 设置表示与跨度相关联的操作开始日期和时间的 pcommon.Timestamp

  • SetEndTimestamp(v pcommon.Timestamp) 设置表示与跨度相关联的操作结束日期和时间的 pcommon.Timestamp

从上面的方法可以看出,ptrace.Span 由 2 个必需的 ID 唯一标识;它们自己的唯一 ID 由 pcommon.SpanID 类型表示,以及它们所属的跟踪的 ID,由 pcommon.TraceID 类型表示。

pcommon.TraceID 必须携带一个全局唯一的 ID,表示为 16 字节数组,并应遵循 W3C Trace Context 规范pcommon.SpanID 是在它们所属的跟踪的上下文中唯一的 ID,并表示为 8 字节数组。

pcommon 包提供以下类型来生成跨度 ID:

  • type TraceID [16]byte

  • type SpanID [8]byte

在本教程中,您将使用 github.com/google/uuid 包中的函数来创建 pcommon.TraceID,并使用 crypto/rand 包中的函数来随机生成 pcommon.SpanID。首先,打开 tailtracer/model.go 文件并将这两个包添加到 import 语句中。之后,添加以下函数来帮助生成这两个 ID。

import (
	crand "crypto/rand"
	"math/rand"
  	...
)

func NewTraceID() pcommon.TraceID {
	return pcommon.TraceID(uuid.New())
}

func NewSpanID() pcommon.SpanID {
	var rngSeed int64
	_ = binary.Read(crand.Reader, binary.LittleEndian, &rngSeed)
	randSource := rand.New(rand.NewSource(rngSeed))

	var sid [8]byte
	randSource.Read(sid[:])
	spanID := pcommon.SpanID(sid)

	return spanID
}

现在您有了一种正确标识跨度的方法,可以开始创建它们来表示系统中以及实体之间操作。

作为 generateBackendSystem() 函数的一部分,我们随机分配了 BackEndSystem 实体可以提供给系统的服务操作。接下来,我们将打开 tailtracer/model.go 文件并查看名为 appendTraceSpans() 的函数,该函数将负责创建跟踪并追加代表 BackendSystem 操作的跨度。以下是 appendTraceSpans() 函数的初始实现外观。

func appendTraceSpans(backend *BackendSystem, backendScopeSpans *ptrace.ScopeSpans, atmScopeSpans *ptrace.ScopeSpans) {
	traceId := NewTraceID()
	backendSpanId := NewSpanID()

	backendDuration, _ := time.ParseDuration("1s")
	backendSpanStartTime := time.Now()
	backendSpanFinishTime := backendSpanStartTime.Add(backendDuration)

	backendSpan := backendScopeSpans.Spans().AppendEmpty()
	backendSpan.SetTraceID(traceId)
	backendSpan.SetSpanID(backendSpanId)
	backendSpan.SetName(backend.Endpoint)
	backendSpan.SetKind(ptrace.SpanKindServer)
	backendSpan.SetStartTimestamp(pcommon.NewTimestampFromTime(backendSpanStartTime))
	backendSpan.SetEndTimestamp(pcommon.NewTimestampFromTime(backendSpanFinishTime))
}

您可能注意到 appendTraceSpans() 函数的参数中有 2 个 ptrace.ScopeSpans 的引用,但我们只使用了其中一个。暂时不用担心,我们稍后会回来处理。

接下来,您将更新 generateTraces() 函数,以便通过调用 appendTraceSpans() 函数来生成跟踪。以下是更新后的 generateTraces() 函数的外观。

func generateTraces(numberOfTraces int) ptrace.Traces {
	traces := ptrace.NewTraces()

	for i := 0; i <= numberOfTraces; i++ {
		newAtm := generateAtm()
		newBackendSystem := generateBackendSystem()

		resourceSpan := traces.ResourceSpans().AppendEmpty()
		atmResource := resourceSpan.Resource()
		fillResourceWithAtm(&atmResource, newAtm)

		atmInstScope := appendAtmSystemInstrScopeSpans(&resourceSpan)

		resourceSpan = traces.ResourceSpans().AppendEmpty()
		backendResource := resourceSpan.Resource()
		fillResourceWithBackendSystem(&backendResource, newBackendSystem)

		backendInstScope := appendAtmSystemInstrScopeSpans(&resourceSpan)

		appendTraceSpans(&newBackendSystem, &backendInstScope, &atmInstScope)
	}

	return traces
}

您现在拥有了 BackendSystem 实体及其在正确跟踪上下文中跨度中的表示!接下来,您需要将生成的跟踪推送到管道中,以便下一个消费者(处理器或导出器)可以接收并处理它。

tailtracer/model.go 文件的外观如下。

tailtracer/model.go

package tailtracer

import (
	crand "crypto/rand"
	"encoding/binary"
	"math/rand"
	"time"

	"github.com/google/uuid"
	"go.opentelemetry.io/collector/pdata/pcommon"
	"go.opentelemetry.io/collector/pdata/ptrace"
	conventions "go.opentelemetry.io/collector/semconv/v1.9.0"
)

type Atm struct {
	ID           int64
	Version      string
	Name         string
	StateID      string
	SerialNumber string
	ISPNetwork   string
}

type BackendSystem struct {
	Version       string
	ProcessName   string
	OSType        string
	OSVersion     string
	CloudProvider string
	CloudRegion   string
	Endpoint      string
}

func generateAtm() Atm {
	i := getRandomNumber(1, 2)
	var newAtm Atm

	switch i {
	case 1:
		newAtm = Atm{
			ID:           111,
			Name:         "ATM-111-IL",
			SerialNumber: "atmxph-2022-111",
			Version:      "v1.0",
			ISPNetwork:   "comcast-chicago",
			StateID:      "IL",
		}

	case 2:
		newAtm = Atm{
			ID:           222,
			Name:         "ATM-222-CA",
			SerialNumber: "atmxph-2022-222",
			Version:      "v1.0",
			ISPNetwork:   "comcast-sanfrancisco",
			StateID:      "CA",
		}
	}

	return newAtm
}

func generateBackendSystem() BackendSystem {
	i := getRandomNumber(1, 3)

	newBackend := BackendSystem{
		ProcessName:   "accounts",
		Version:       "v2.5",
		OSType:        "lnx",
		OSVersion:     "4.16.10-300.fc28.x86_64",
		CloudProvider: "amzn",
		CloudRegion:   "us-east-2",
	}

	switch i {
	case 1:
		newBackend.Endpoint = "api/v2.5/balance"
	case 2:
		newBackend.Endpoint = "api/v2.5/deposit"
	case 3:
		newBackend.Endpoint = "api/v2.5/withdrawn"
	}

	return newBackend
}

func getRandomNumber(min int, max int) int {
	rand.Seed(time.Now().UnixNano())
	i := (rand.Intn(max-min+1) + min)
	return i
}

func generateTraces(numberOfTraces int) ptrace.Traces {
	traces := ptrace.NewTraces()

	for i := 0; i <= numberOfTraces; i++ {
		newAtm := generateAtm()
		newBackendSystem := generateBackendSystem()

		resourceSpan := traces.ResourceSpans().AppendEmpty()
		atmResource := resourceSpan.Resource()
		fillResourceWithAtm(&atmResource, newAtm)

		atmInstScope := appendAtmSystemInstrScopeSpans(&resourceSpan)

		resourceSpan = traces.ResourceSpans().AppendEmpty()
		backendResource := resourceSpan.Resource()
		fillResourceWithBackendSystem(&backendResource, newBackendSystem)

		backendInstScope := appendAtmSystemInstrScopeSpans(&resourceSpan)

		appendTraceSpans(&newBackendSystem, &backendInstScope, &atmInstScope)
	}

	return traces
}

func fillResourceWithAtm(resource *pcommon.Resource, atm Atm) {
	atmAttrs := resource.Attributes()
	atmAttrs.PutInt("atm.id", atm.ID)
	atmAttrs.PutStr("atm.stateid", atm.StateID)
	atmAttrs.PutStr("atm.ispnetwork", atm.ISPNetwork)
	atmAttrs.PutStr("atm.serialnumber", atm.SerialNumber)
	atmAttrs.PutStr(conventions.AttributeServiceName, atm.Name)
	atmAttrs.PutStr(conventions.AttributeServiceVersion, atm.Version)

}

func fillResourceWithBackendSystem(resource *pcommon.Resource, backend BackendSystem) {
	backendAttrs := resource.Attributes()
	var osType, cloudProvider string

	switch {
	case backend.CloudProvider == "amzn":
		cloudProvider = conventions.AttributeCloudProviderAWS
	case backend.OSType == "mcrsft":
		cloudProvider = conventions.AttributeCloudProviderAzure
	case backend.OSType == "gogl":
		cloudProvider = conventions.AttributeCloudProviderGCP
	}

	backendAttrs.PutStr(conventions.AttributeCloudProvider, cloudProvider)
	backendAttrs.PutStr(conventions.AttributeCloudRegion, backend.CloudRegion)

	switch {
	case backend.OSType == "lnx":
		osType = conventions.AttributeOSTypeLinux
	case backend.OSType == "wndws":
		osType = conventions.AttributeOSTypeWindows
	case backend.OSType == "slrs":
		osType = conventions.AttributeOSTypeSolaris
	}

	backendAttrs.PutStr(conventions.AttributeOSType, osType)
	backendAttrs.PutStr(conventions.AttributeOSVersion, backend.OSVersion)

	backendAttrs.PutStr(conventions.AttributeServiceName, backend.ProcessName)
	backendAttrs.PutStr(conventions.AttributeServiceVersion, backend.Version)
}

func appendAtmSystemInstrScopeSpans(resourceSpans *ptrace.ResourceSpans) ptrace.ScopeSpans {
	scopeSpans := resourceSpans.ScopeSpans().AppendEmpty()
	scopeSpans.Scope().SetName("atm-system")
	scopeSpans.Scope().SetVersion("v1.0")
	return scopeSpans
}

func NewTraceID() pcommon.TraceID {
	return pcommon.TraceID(uuid.New())
}

func NewSpanID() pcommon.SpanID {
	var rngSeed int64
	_ = binary.Read(crand.Reader, binary.LittleEndian, &rngSeed)
	randSource := rand.New(rand.NewSource(rngSeed))

	var sid [8]byte
	randSource.Read(sid[:])
	spanID := pcommon.SpanID(sid)

	return spanID
}

func appendTraceSpans(backend *BackendSystem, backendScopeSpans *ptrace.ScopeSpans, atmScopeSpans *ptrace.ScopeSpans) {
	traceId := NewTraceID()
	backendSpanId := NewSpanID()

	backendDuration, _ := time.ParseDuration("1s")
	backendSpanStartTime := time.Now()
	backendSpanFinishTime := backendSpanStartTime.Add(backendDuration)

	backendSpan := backendScopeSpans.Spans().AppendEmpty()
	backendSpan.SetTraceID(traceId)
	backendSpan.SetSpanID(backendSpanId)
	backendSpan.SetName(backend.Endpoint)
	backendSpan.SetKind(ptrace.SpanKindServer)
	backendSpan.SetStartTimestamp(pcommon.NewTimestampFromTime(backendSpanStartTime))
	backendSpan.SetEndTimestamp(pcommon.NewTimestampFromTime(backendSpanFinishTime))
}

consumer.Traces 有一个名为 ConsumeTraces() 的方法,该方法负责将生成的跟踪推送到管道中的下一个消费者。您需要更新 tailtracerReceiver 类型中的 Start() 方法,并添加代码来使用它。

打开 tailtracer/trace-receiver.go 文件并将 Start() 方法更新如下。

func (tailtracerRcvr *tailtracerReceiver) Start(ctx context.Context, host component.Host) error {
	tailtracerRcvr.host = host
	ctx = context.Background()
	ctx, tailtracerRcvr.cancel = context.WithCancel(ctx)

	interval, _ := time.ParseDuration(tailtracerRcvr.config.Interval)
	go func() {
		ticker := time.NewTicker(interval)
		defer ticker.Stop()
		for {
			select {
				case <-ticker.C:
					tailtracerRcvr.logger.Info("I should start processing traces now!")
					tailtracerRcvr.nextConsumer.ConsumeTraces(ctx, generateTraces(tailtracerRcvr.config.NumberOfTraces)) // new line added
				case <-ctx.Done():
					return
			}
		}
	}()

	return nil
}

现在让我们再次运行 otelcol-dev

go run ./otelcol-dev --config config.yaml

几分钟后,您应该会看到类似以下的输出。

2023-11-09T11:38:19.890+0800	info	service@v0.88.0/telemetry.go:84	Setting up own telemetry...
2023-11-09T11:38:19.890+0800	info	service@v0.88.0/telemetry.go:201	Serving Prometheus metrics	{"address": ":8888", "level": "Basic"}
2023-11-09T11:38:19.890+0800	debug	exporter@v0.88.0/exporter.go:273	Stable component.	{"kind": "exporter", "data_type": "traces", "name": "otlp/jaeger"}
2023-11-09T11:38:19.890+0800	info	exporter@v0.88.0/exporter.go:275	Development component. May change in the future.	{"kind": "exporter", "data_type": "traces", "name": "debug"}
2023-11-09T11:38:19.891+0800	debug	receiver@v0.88.0/receiver.go:294	Stable component.	{"kind": "receiver", "name": "otlp", "data_type": "traces"}
2023-11-09T11:38:19.891+0800	debug	receiver@v0.88.0/receiver.go:294	Alpha component. May change in the future.	{"kind": "receiver", "name": "tailtracer", "data_type": "traces"}
2023-11-09T11:38:19.891+0800	info	service@v0.88.0/service.go:143	Starting otelcol-dev...	{"Version": "1.0.0", "NumCPU": 10}
2023-11-09T11:38:19.891+0800	info	extensions/extensions.go:33	Starting extensions...

<OMITTED>

2023-11-09T11:38:19.903+0800	info	zapgrpc/zapgrpc.go:178	[core] [Channel #1] Channel Connectivity change to READY	{"grpc_log": true}
2023-11-09T11:39:19.894+0800	info	tailtracer/trace-receiver.go:33	I should start processing traces now!	{"kind": "receiver", "name": "tailtracer", "data_type": "traces"}
2023-11-09T11:39:19.913+0800	info	TracesExporter	{"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 4, "spans": 2}
2023-11-09T11:39:19.913+0800	info	ResourceSpans #0
Resource SchemaURL:
Resource attributes:
     -> atm.id: Int(222)
     -> atm.stateid: Str(CA)
     -> atm.ispnetwork: Str(comcast-sanfrancisco)
     -> atm.serialnumber: Str(atmxph-2022-222)
     -> service.name: Str(ATM-222-CA)
     -> service.version: Str(v1.0)
ScopeSpans #0
ScopeSpans SchemaURL:
InstrumentationScope
ResourceSpans #1
Resource SchemaURL:
Resource attributes:
     -> cloud.provider: Str(aws)
     -> cloud.region: Str(us-east-2)
     -> os.type: Str(linux)
     -> os.version: Str(4.16.10-300.fc28.x86_64)
     -> service.name: Str(accounts)
     -> service.version: Str(v2.5)
ScopeSpans #0
ScopeSpans SchemaURL:
InstrumentationScope
Span #0
    Trace ID       : bbcb00aead044a138cf96c0bf4a4ba83
    Parent ID      :
    ID             : 5056fe4e9adf621c
    Name           : api/v2.5/withdrawn
    Kind           : Server
    Start time     : 2023-11-09 03:39:19.894881 +0000 UTC
    End time       : 2023-11-09 03:39:20.894881 +0000 UTC
    Status code    : Unset
    Status message :
ResourceSpans #2
Resource SchemaURL:
Resource attributes:
     -> atm.id: Int(111)
     -> atm.stateid: Str(IL)
     -> atm.ispnetwork: Str(comcast-chicago)
     -> atm.serialnumber: Str(atmxph-2022-111)
     -> service.name: Str(ATM-111-IL)
     -> service.version: Str(v1.0)
ScopeSpans #0
ScopeSpans SchemaURL:
InstrumentationScope
ResourceSpans #3
Resource SchemaURL:
Resource attributes:
     -> cloud.provider: Str(aws)
     -> cloud.region: Str(us-east-2)
     -> os.type: Str(linux)
     -> os.version: Str(4.16.10-300.fc28.x86_64)
     -> service.name: Str(accounts)
     -> service.version: Str(v2.5)
ScopeSpans #0
ScopeSpans SchemaURL:
InstrumentationScope
Span #0
    Trace ID       : ba013b8223ec4d29806ae493ecd1a5e4
    Parent ID      :
    ID             : 4feb47b55c9c4129
    Name           : api/v2.5/withdrawn
    Kind           : Server
    Start time     : 2023-11-09 03:39:19.894953 +0000 UTC
    End time       : 2023-11-09 03:39:20.894953 +0000 UTC
    Status code    : Unset
    Status message :
	{"kind": "exporter", "data_type": "traces", "name": "debug"}
...

这是在 Jaeger 中生成的跟踪的外观: Jaeger trace

您目前在 Jaeger 中看到的内容代表了一个服务,该服务正在接收来自未被 OTel SDK 检测的外部实体的请求。因此,它无法被识别为跟踪的起源/起点。为了让 ptrace.Span 理解它正在代表一个操作,该操作是作为同一跟踪上下文中 Resource 内或外(嵌套/子)的其他操作的结果而执行的,您需要:

  • 通过调用 SetTraceID() 方法并传递父/调用者 ptrace.Spanpcommon.TraceID 作为参数,设置与调用者操作相同的跟踪上下文。
  • 通过调用 SetParentId() 方法并传递父/调用者 ptrace.Spanpcommon.SpanID 作为参数,在跟踪的上下文中定义调用者操作。

您现在将创建一个 ptrace.Span 来表示 Atm 实体操作,并将其设置为 BackendSystem 跨度的父级。打开 tailtracer/model.go 文件并按如下方式更新 appendTraceSpans() 函数。

func appendTraceSpans(backend *BackendSystem, backendScopeSpans *ptrace.ScopeSpans, atmScopeSpans *ptrace.ScopeSpans) {
	traceId := NewTraceID()

	var atmOperationName string

	switch {
		case strings.Contains(backend.Endpoint, "balance"):
			atmOperationName = "Check Balance"
		case strings.Contains(backend.Endpoint, "deposit"):
			atmOperationName = "Make Deposit"
		case strings.Contains(backend.Endpoint, "withdraw"):
			atmOperationName = "Fast Cash"
		}

	atmSpanId := NewSpanID()
	atmSpanStartTime := time.Now()
	atmDuration, _ := time.ParseDuration("4s")
	atmSpanFinishTime := atmSpanStartTime.Add(atmDuration)

	atmSpan := atmScopeSpans.Spans().AppendEmpty()
	atmSpan.SetTraceID(traceId)
	atmSpan.SetSpanID(atmSpanId)
	atmSpan.SetName(atmOperationName)
	atmSpan.SetKind(ptrace.SpanKindClient)
	atmSpan.Status().SetCode(ptrace.StatusCodeOk)
	atmSpan.SetStartTimestamp(pcommon.NewTimestampFromTime(atmSpanStartTime))
	atmSpan.SetEndTimestamp(pcommon.NewTimestampFromTime(atmSpanFinishTime))

	backendSpanId := NewSpanID()

	backendDuration, _ := time.ParseDuration("2s")
	backendSpanStartTime := atmSpanStartTime.Add(backendDuration)

	backendSpan := backendScopeSpans.Spans().AppendEmpty()
	backendSpan.SetTraceID(atmSpan.TraceID())
	backendSpan.SetSpanID(backendSpanId)
	backendSpan.SetParentSpanID(atmSpan.SpanID())
	backendSpan.SetName(backend.Endpoint)
	backendSpan.SetKind(ptrace.SpanKindServer)
	backendSpan.Status().SetCode(ptrace.StatusCodeOk)
	backendSpan.SetStartTimestamp(pcommon.NewTimestampFromTime(backendSpanStartTime))
	backendSpan.SetEndTimestamp(atmSpan.EndTimestamp())
}

以下是最终的 tailtracer/model.go 文件的外观。

tailtracer/model.go

package tailtracer

import (
	crand "crypto/rand"
	"encoding/binary"
	"math/rand"
	"strings"
	"time"

	"github.com/google/uuid"
	"go.opentelemetry.io/collector/pdata/pcommon"
	"go.opentelemetry.io/collector/pdata/ptrace"
	conventions "go.opentelemetry.io/collector/semconv/v1.9.0"
)

type Atm struct {
	ID           int64
	Version      string
	Name         string
	StateID      string
	SerialNumber string
	ISPNetwork   string
}

type BackendSystem struct {
	Version       string
	ProcessName   string
	OSType        string
	OSVersion     string
	CloudProvider string
	CloudRegion   string
	Endpoint      string
}

func generateAtm() Atm {
	i := getRandomNumber(1, 2)
	var newAtm Atm

	switch i {
	case 1:
		newAtm = Atm{
			ID:           111,
			Name:         "ATM-111-IL",
			SerialNumber: "atmxph-2022-111",
			Version:      "v1.0",
			ISPNetwork:   "comcast-chicago",
			StateID:      "IL",
		}

	case 2:
		newAtm = Atm{
			ID:           222,
			Name:         "ATM-222-CA",
			SerialNumber: "atmxph-2022-222",
			Version:      "v1.0",
			ISPNetwork:   "comcast-sanfrancisco",
			StateID:      "CA",
		}
	}

	return newAtm
}

func generateBackendSystem() BackendSystem {
	i := getRandomNumber(1, 3)

	newBackend := BackendSystem{
		ProcessName:   "accounts",
		Version:       "v2.5",
		OSType:        "lnx",
		OSVersion:     "4.16.10-300.fc28.x86_64",
		CloudProvider: "amzn",
		CloudRegion:   "us-east-2",
	}

	switch i {
	case 1:
		newBackend.Endpoint = "api/v2.5/balance"
	case 2:
		newBackend.Endpoint = "api/v2.5/deposit"
	case 3:
		newBackend.Endpoint = "api/v2.5/withdrawn"
	}

	return newBackend
}

func getRandomNumber(min int, max int) int {
	rand.Seed(time.Now().UnixNano())
	i := (rand.Intn(max-min+1) + min)
	return i
}

func generateTraces(numberOfTraces int) ptrace.Traces {
	traces := ptrace.NewTraces()

	for i := 0; i <= numberOfTraces; i++ {
		newAtm := generateAtm()
		newBackendSystem := generateBackendSystem()

		resourceSpan := traces.ResourceSpans().AppendEmpty()
		atmResource := resourceSpan.Resource()
		fillResourceWithAtm(&atmResource, newAtm)

		atmInstScope := appendAtmSystemInstrScopeSpans(&resourceSpan)

		resourceSpan = traces.ResourceSpans().AppendEmpty()
		backendResource := resourceSpan.Resource()
		fillResourceWithBackendSystem(&backendResource, newBackendSystem)

		backendInstScope := appendAtmSystemInstrScopeSpans(&resourceSpan)

		appendTraceSpans(&newBackendSystem, &backendInstScope, &atmInstScope)
	}

	return traces
}

func fillResourceWithAtm(resource *pcommon.Resource, atm Atm) {
	atmAttrs := resource.Attributes()
	atmAttrs.PutInt("atm.id", atm.ID)
	atmAttrs.PutStr("atm.stateid", atm.StateID)
	atmAttrs.PutStr("atm.ispnetwork", atm.ISPNetwork)
	atmAttrs.PutStr("atm.serialnumber", atm.SerialNumber)
	atmAttrs.PutStr(conventions.AttributeServiceName, atm.Name)
	atmAttrs.PutStr(conventions.AttributeServiceVersion, atm.Version)

}

func fillResourceWithBackendSystem(resource *pcommon.Resource, backend BackendSystem) {
	backendAttrs := resource.Attributes()
	var osType, cloudProvider string

	switch {
	case backend.CloudProvider == "amzn":
		cloudProvider = conventions.AttributeCloudProviderAWS
	case backend.OSType == "mcrsft":
		cloudProvider = conventions.AttributeCloudProviderAzure
	case backend.OSType == "gogl":
		cloudProvider = conventions.AttributeCloudProviderGCP
	}

	backendAttrs.PutStr(conventions.AttributeCloudProvider, cloudProvider)
	backendAttrs.PutStr(conventions.AttributeCloudRegion, backend.CloudRegion)

	switch {
	case backend.OSType == "lnx":
		osType = conventions.AttributeOSTypeLinux
	case backend.OSType == "wndws":
		osType = conventions.AttributeOSTypeWindows
	case backend.OSType == "slrs":
		osType = conventions.AttributeOSTypeSolaris
	}

	backendAttrs.PutStr(conventions.AttributeOSType, osType)
	backendAttrs.PutStr(conventions.AttributeOSVersion, backend.OSVersion)

	backendAttrs.PutStr(conventions.AttributeServiceName, backend.ProcessName)
	backendAttrs.PutStr(conventions.AttributeServiceVersion, backend.Version)
}

func appendAtmSystemInstrScopeSpans(resourceSpans *ptrace.ResourceSpans) ptrace.ScopeSpans {
	scopeSpans := resourceSpans.ScopeSpans().AppendEmpty()
	scopeSpans.Scope().SetName("atm-system")
	scopeSpans.Scope().SetVersion("v1.0")
	return scopeSpans
}

func NewTraceID() pcommon.TraceID {
	return pcommon.TraceID(uuid.New())
}

func NewSpanID() pcommon.SpanID {
	var rngSeed int64
	_ = binary.Read(crand.Reader, binary.LittleEndian, &rngSeed)
	randSource := rand.New(rand.NewSource(rngSeed))

	var sid [8]byte
	randSource.Read(sid[:])
	spanID := pcommon.SpanID(sid)

	return spanID
}

func appendTraceSpans(backend *BackendSystem, backendScopeSpans *ptrace.ScopeSpans, atmScopeSpans *ptrace.ScopeSpans) {
	traceId := NewTraceID()

	var atmOperationName string

	switch {
	case strings.Contains(backend.Endpoint, "balance"):
		atmOperationName = "Check Balance"
	case strings.Contains(backend.Endpoint, "deposit"):
		atmOperationName = "Make Deposit"
	case strings.Contains(backend.Endpoint, "withdraw"):
		atmOperationName = "Fast Cash"
	}

	atmSpanId := NewSpanID()
	atmSpanStartTime := time.Now()
	atmDuration, _ := time.ParseDuration("4s")
	atmSpanFinishTime := atmSpanStartTime.Add(atmDuration)

	atmSpan := atmScopeSpans.Spans().AppendEmpty()
	atmSpan.SetTraceID(traceId)
	atmSpan.SetSpanID(atmSpanId)
	atmSpan.SetName(atmOperationName)
	atmSpan.SetKind(ptrace.SpanKindClient)
	atmSpan.Status().SetCode(ptrace.StatusCodeOk)
	atmSpan.SetStartTimestamp(pcommon.NewTimestampFromTime(atmSpanStartTime))
	atmSpan.SetEndTimestamp(pcommon.NewTimestampFromTime(atmSpanFinishTime))

	backendSpanId := NewSpanID()

	backendDuration, _ := time.ParseDuration("2s")
	backendSpanStartTime := atmSpanStartTime.Add(backendDuration)

	backendSpan := backendScopeSpans.Spans().AppendEmpty()
	backendSpan.SetTraceID(atmSpan.TraceID())
	backendSpan.SetSpanID(backendSpanId)
	backendSpan.SetParentSpanID(atmSpan.SpanID())
	backendSpan.SetName(backend.Endpoint)
	backendSpan.SetKind(ptrace.SpanKindServer)
	backendSpan.Status().SetCode(ptrace.StatusCodeOk)
	backendSpan.SetStartTimestamp(pcommon.NewTimestampFromTime(backendSpanStartTime))
	backendSpan.SetEndTimestamp(atmSpan.EndTimestamp())
}

再次运行 otelcol-dev

go run ./otelcol-dev --config config.yaml

大约 2 分钟后,您应该开始在 Jaeger 中看到类似以下的跟踪: Jaeger trace

我们现在拥有了系统中代表 AtmBackendSystem 遥测生成实体的服务。我们完全理解了这两个实体是如何使用的,以及它们如何影响用户执行的操作的性能。

这是 Jaeger 中其中一个跟踪的详细视图: Jaeger trace

就是这样!您现在已经完成了本教程,并成功实现了一个跟踪接收器,恭喜您!