构建接收器
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 接收器以及 otlp 和 debug 导出的基本追踪管道。您的 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
注意
- 上面的模块路径是一个模拟路径,可以是您想要的私有或公共路径。
- 请参阅 初始 trace-receiver 代码。
由于我们将管理多个 Go 模块(otelcol-dev 和 tailtracer,以及未来可能更多的组件),因此建议启用 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"`
}
- 已将
Interval和NumberOfTraces字段添加到config.yaml中,以便能够正确访问它们的值。
现在您已经可以访问这些设置了,您可以通过实现可选的 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
}
- 导入了
fmt包,以便正确格式化打印错误消息。 - 向 Config 结构体添加了
Validate方法,以检查interval设置值是否至少为 1 分钟 (1m),以及number_of_traces设置值是否大于或等于 1。如果不满足这些条件,Collector 将在启动过程中生成一个错误并显示相应消息。
如果您想更详细地了解组件配置方面所涉及的结构体和接口,请参阅 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实例。...FactoryOption:receiver.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
}
- 导入了
time包,以支持默认defaultInterval的 time.Duration 类型。 - 导入了
go.opentelemetry.io/collector/component包,该包声明了component.Config。 - 导入了
go.opentelemetry.io/collector/receiver包,该包声明了receiver.Factory。 - 添加了一个名为
defaultInterval的time.Duration常量,以表示我们接收器的Interval设置的默认值。我们将默认值设置为 1 分钟,因此赋值为1 * time.Minute。 - 添加了一个名为
createDefaultConfig的函数,该函数负责返回一个component.Config实现,在这种情况下,它将是我们的tailtracer.Config结构体的实例。 tailtracer.Config.Interval字段已使用defaultInterval常量初始化。
指定接收器的能力
接收器组件可以处理追踪、指标和日志。接收器工厂负责指定接收器将提供的能力。
鉴于追踪是本教程的主题,我们将使 tailtracer 接收器仅支持追踪。receiver 包提供了以下函数和类型来帮助工厂描述追踪处理能力:
func WithTraces(createTracesReceiver CreateTracesFunc, sl component.StabilityLevel) FactoryOption
receiver.WithTraces() 实例化并返回一个 receiver.FactoryOption,它需要以下参数:
createTracesReceiver:指向一个函数(匹配receiver.CreateTracesFunc类型)的引用。receiver.CreateTracesFunc类型是指向一个函数的指针,该函数负责实例化并返回一个receiver.Traces实例,它需要以下参数:context.Context:Collectorcontext.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))
}
- 导入了
context包,以支持createTracesReceiver函数中引用的context.Context类型。 - 导入了
go.opentelemetry.io/collector/consumer包,以支持createTracesReceiver函数中引用的consumer.Traces类型。 - 更新了
NewFactory()函数,以便它返回由receiver.NewFactory()调用生成的receiver.Factory,并带有必需的参数。生成的接收器工厂将通过调用receiver.WithTraces(createTracesReceiver, component.StabilityLevelAlpha)来处理追踪。
实现接收器组件
所有接收器 API 目前都声明在 Collector 项目的 receiver/receiver.go 文件中。打开该文件,花点时间浏览所有接口。
请注意,此时 receiver.Traces(及其同级 receiver.Metrics 和 receiver.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
}
- 导入了
context包,其中声明了Context类型和函数。 - 导入了
go.opentelemetry.io/collector/component包,其中声明了Host类型。 - 添加了
Start(ctx context.Context, host component.Host)方法的引导实现,以符合receiver.Traces接口。 - 添加了
Shutdown(ctx context.Context)方法的引导实现,以符合receiver.Traces接口。
Start() 方法传递了 2 个引用(context.Context 和 component.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
}
通过添加对 Collector 传递的 component.Host 引用的初始化,更新了 Start() 方法,以初始化 host 字段。
- 使用基于使用
context.Background()创建的新上下文的取消功能,将cancel函数字段设置为(根据 Collector API 文档建议)。 - 通过添加对
cancel()上下文取消函数的调用,更新了Shutdown()方法。
保留接收器工厂传递的信息
现在您已经实现了 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
}
- 导入了
go.opentelemetry.io/collector/consumer,其中声明了管道的消费者类型和接口。 - 导入了
go.uber.org/zap包,Collector 使用它来进行调试。 - 添加了一个名为
logger的zap.Logger字段,以便我们可以在接收器内部访问 Collector 日志记录器引用。 - 添加了一个名为
nextConsumer的consumer.Traces字段,以便我们可以将tailtracer接收器生成的追踪推送到 Collector 管道中声明的下一个消费者。 - 添加了一个名为
config的Config字段,以便我们可以访问 Collector 配置中定义的接收器配置设置。 - 添加了一个名为
interval的变量,该变量根据 Collector 配置中定义的tailtracer接收器的interval设置值初始化为time.Duration。 - 添加了一个
go func()来实现ticker机制,这样接收器就可以在ticker达到interval变量指定的持续时间时生成追踪。 - 使用
tailtracerRcvr.logger字段在接收器应该生成追踪的每次时生成一个信息消息。
tailtracerReceiver 类型已准备好进行实例化,并将保留其工厂传递的所有有意义的信息。
打开 tailtracer/factory.go 文件并导航到 createTracesReceiver() 函数。
仅当接收器在管道中声明为组件时,才会实例化接收器,工厂负责确保管道中的下一个消费者(处理器或导出器)有效。否则,它应该生成一个错误。
createTracesReceiver() 函数需要一个保护子句来执行此验证。
您还需要变量来正确初始化 tailtracerReceiver 实例的 config 和 logger 字段。
更新后的 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))
}
- 添加了一个名为
logger的变量,并使用 Collector 日志记录器将其初始化,该日志记录器作为receiver.Settings引用中的Logger字段可用。 - 添加了一个名为
tailtracerCfg的变量,并通过将component.Config引用转换为tailtracer接收器Config来初始化它。 - 添加了一个名为
traceRcvr的变量,并使用存储在变量中的工厂信息,使用tailtracerReceiver实例对其进行初始化。 - 更新了 return 语句以包含
traceRcvr实例。
到目前为止,接收器的框架已完全实现。
使用接收器更新 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
}
- 导入了接收器模块
github.com/open-telemetry/opentelemetry-tutorials/trace-receiver/tailtracer,其中包含接收器类型和函数。 - 在
otelcol.MakeFactoryMap()调用的参数中添加了一个对tailtracer.NewFactory()的调用,以便您的tailtracer接收器工厂已正确添加到factories映射中。
运行和调试接收器
确保 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 追踪,但它是由 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 文件中,添加 Atm 和 BackendSystem 类型的定义如下:
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
}
- 导入了
math/rand和time包,以支持generateRandomNumber函数的实现。 - 添加了
generateAtm函数,该函数实例化一个Atm类型,并随机分配伊利诺伊州或加利福尼亚州作为StateID的值,以及相应的ISPNetwork值。 - 添加了
generateBackendSystem函数,该函数创建一个BackendSystem类型实例,并随机分配Endpoint字段的服务端点值。 - 添加了
generateRandomNumber函数,用于在指定范围内生成随机数。
现在您已经有了生成表示遥测源的对象的实例的函数,您就可以在 OTel Collector 世界中表示这些实体了。
Collector API 提供了一个名为 ptrace 的包,它嵌套在 pdata 包下。它包含了在 Collector 管道组件中使用追踪所需的所有类型、接口和辅助函数。
打开 tailtracer/model.go 文件,并将 go.opentelemetry.io/collector/pdata/ptrace 添加到 import 子句中,以便您可以访问 ptrace 包的功能。
在定义 Resource 之前,您需要创建一个 ptrace.Traces,它将负责通过 Collector 管道传播追踪。您可以使用辅助函数 ptrace.NewTraces() 来实例化它。您还需要创建 Atm 和 BackendSystem 类型的实例,以便您拥有表示追踪中涉及的遥测源的数据。
打开 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
}
- 添加了
resourceSpan变量,并通过traces.ResourceSpans().AppendEmpty()调用返回的ResourceSpan引用进行了初始化。 - 添加了
atmResource变量,并通过resourceSpan.Resource()调用返回的pcommon.Resource引用进行了初始化。
通过属性描述资源
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)
}
- 声明了一个名为
atmAttrs的变量,并使用resource.Attributes()调用返回的pcommon.Map引用对其进行了初始化。 - 使用
pcommon.Map的PutInt()和PutStr()方法,基于等效的Atm字段类型添加 int 和 string 属性。请注意,由于这些属性是特定的,并且仅代表Atm实体,因此它们都包含在atm.前缀内。
资源语义约定还为表示常见的、适用于不同领域的遥测生成实体的属性名称和众所周知的(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)
}
请注意,我们没有向代表 Atm 和 BackendSystem 实体名称的 pcommon.Resource 添加名为“atm.name”或“backendsystem.name”的属性。这是因为大多数(如果不是全部)与 OTel 跟踪规范兼容的分布式跟踪后端系统会将跟踪中描述的 pcommon.Resource 解释为 Service。因此,它们期望 pcommon.Resource 具有一个名为 service.name 的必需属性,正如资源语义约定所规定的一样。
我们还将使用一个非必需属性 service.version 来表示 Atm 和 BackendSystem 实体的版本信息。
以下是添加代码以正确分配“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)
}
- 将
go.opentelemetry.io/collector/semconv/v1.9.0包导入为conventions,以便访问所有资源语义约定属性名称和值。 - 更新了
fillResourceWithAtm()函数,添加了正确地将“service.name”和“service.version”属性分配给代表Atm实体的pcommon.Resource的行。 - 更新了
fillResourceWithBackendSystem()函数,添加了正确地将“service.name”和“service.version”属性分配给代表BackendSystem实体的pcommon.Resource的行。 - 更新了
generateTraces函数,添加了正确地实例化pcommon.Resource并使用fillResourceWithAtm()和fillResourceWithBackendSystem()函数填充Atm和BackendSystem实体的属性信息的行。
用跨度表示操作
您现在拥有一个 ResourceSpan 实例,其中相应的 Resource 已正确填充了属性以表示 Atm 和 BackendSystem 实体。您现在已准备好在 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() 来添加变量来表示 Atm 和 BackendSystem 实体使用的检测范围。以下是更新后的 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]bytetype 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
}
- 将
crypto/rand导入为crand,以避免与math/rand冲突。 - 添加了新的函数
NewTraceID()和NewSpanID(),分别用于生成跟踪 ID 和跨度 ID。
现在您有了一种正确标识跨度的方法,可以开始创建它们来表示系统中以及实体之间操作。
作为 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))
}
- 添加了
traceId和backendSpanId变量来分别表示跟踪和跨度 ID,并使用之前创建的辅助函数对其进行了初始化。 - 添加了
backendSpanStartTime和backendSpanFinishTime来表示操作的开始和结束时间。在本教程中,任何BackendSystem操作将持续 1 秒。 - 添加了一个名为
backendSpan的变量,它将保存表示该操作的ptrace.Span实例。 - 使用
BackendSystem实例的Endpoint字段值设置跨度的Name。 - 将跨度的
Kind设置为ptrace.SpanKindServer。请参阅跟踪规范中的 SpanKind 部分,了解如何正确定义 SpanKind。 - 使用上述所有方法填充
ptrace.Span,以正确表示BackendSystem操作。
您可能注意到 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
}
- 在
case <=ticker.C条件下添加了一行,调用tailtracerRcvr.nextConsumer.ConsumeTraces()方法,传入在Start()方法中创建的新上下文 (ctx),并调用generateTraces()函数,以便生成的跟踪可以推送到管道中的下一个消费者。
现在让我们再次运行 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 中看到的内容代表了一个服务,该服务正在接收来自未被 OTel SDK 检测的外部实体的请求。因此,它无法被识别为跟踪的起源/起点。为了让 ptrace.Span 理解它正在代表一个操作,该操作是作为同一跟踪上下文中 Resource 内或外(嵌套/子)的其他操作的结果而执行的,您需要:
- 通过调用
SetTraceID()方法并传递父/调用者ptrace.Span的pcommon.TraceID作为参数,设置与调用者操作相同的跟踪上下文。 - 通过调用
SetParentId()方法并传递父/调用者ptrace.Span的pcommon.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 中看到类似以下的跟踪: 
我们现在拥有了系统中代表 Atm 和 BackendSystem 遥测生成实体的服务。我们完全理解了这两个实体是如何使用的,以及它们如何影响用户执行的操作的性能。
这是 Jaeger 中其中一个跟踪的详细视图: 
就是这样!您现在已经完成了本教程,并成功实现了一个跟踪接收器,恭喜您!