レシーバーを構築する
OpenTelemetry は分散トレーシングを次のように定義しています。
トレースは、アプリケーションを構成するサービスによって処理される、単一のリクエストの進行を追跡します。 リクエストはユーザーまたはアプリケーションによって開始されることがあります。 分散トレーシングは、プロセス、ネットワーク、セキュリティの境界を越えるトレースの形態です。
分散トレースはアプリケーション中心の方法で定義されていますが、システムを通過する あらゆる リクエストのタイムラインとして考えることができます。 各分散トレースは、リクエストが最初から最後まで完了するのにかかった時間を示し、完了するために実行されたステップを分解します。
システムがトレーシングテレメトリーを生成する場合、OpenTelemetry Collector にトレースレシーバーを設定して、そのテレメトリーを受信し変換できます。 レシーバーは、元の形式のデータを OpenTelemetry トレースモデルに変換し、Collector が処理できるようにします。
トレースレシーバーを実装するには、以下が必要です。
Config実装。トレースレシーバーが Collector の config.yaml で設定を収集し検証できるようにします。receiver.Factory実装。Collector がトレースレシーバーコンポーネントを適切にインスタンス化できるようにします。receiver.Traces実装。テレメトリーを収集し、内部トレース表現に変換し、パイプラインの次のコンシューマーにテレメトリーを渡します。
このチュートリアルでは、プル操作をシミュレートし、その操作の結果としてトレースを生成する tailtracer というトレースレシーバーの作成方法を説明します。
レシーバーの開発およびテスト環境のセットアップ
まず、カスタム Collector の構築チュートリアルを使用して otelcol-dev という名前の Collector インスタンスを作成します。
OpenTelemetry Collector Builder の設定に記載されている 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 にアクセスできます。 http://localhost:16686/
次に、Collector コンポーネントとパイプラインをセットアップするために config.yaml という名前の 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
コンソールに詳細なログが表示され、次の URL から Jaeger UI でトレースを確認できるはずです。 http://localhost:16686/
Collector コンソールで Ctrl + C を押して Collector インスタンスを停止します。
Go モジュールのセットアップ
すべての Collector コンポーネントは Go モジュールとして作成する必要があります。
レシーバープロジェクトをホストする tailtracer フォルダを作成し、Go モジュールとして初期化しましょう。
mkdir tailtracer
cd tailtracer
go mod init github.com/open-telemetry/opentelemetry-tutorials/trace-receiver/tailtracer
上記のモジュールパスはモックパスであり、任意のプライベートまたはパブリックなパスにできます。 初期 trace-receiver コードを参照してください。
otelcol-dev と tailtracer、そして将来的にはさらに多くのコンポーネントという複数の Go モジュールを管理するため、Go の Workspaces を有効にすることを推奨します。
cd ..
go work init
go work use otelcol-dev
go work use tailtracer
レシーバー設定の設計と検証
レシーバーにはいくつかの設定可能な項目があり、Collector の設定ファイルで設定できます。
tailtracer レシーバーには以下の設定があります。
interval: テレメトリーのプル操作間の時間間隔(分単位)を表す文字列。number_of_traces: 各間隔で生成されるモックトレースの数。
tailtracer レシーバーの設定は次のようになります。
receivers:
tailtracer: # この行はレシーバーの ID を表します
interval: 1m
number_of_traces: 1
レシーバーの設定をサポートするすべてのコードを記述するために、tailtracer フォルダの下に config.go という名前のファイルを作成します。
touch tailtracer/config.go
レシーバーの設定面を実装するには、Config 構造体を作成する必要があります。
config.go ファイルに次のコードを追加してください。
package tailtracer
type Config struct{
}
レシーバーが設定にアクセスできるようにするには、Config 構造体にレシーバーの各設定項目に対応するフィールドが必要です。
上記の要件を実装した後の config.go ファイルは次のようになります。
tailtracer/config.go
package tailtracer
// Config は Collector の config.yaml におけるレシーバーの設定を表します
type Config struct {
Interval string `mapstructure:"interval"`
NumberOfTraces int `mapstructure:"number_of_traces"`
}
- config.yaml の値に適切にアクセスするために、
IntervalとNumberOfTracesフィールドを追加しました。
設定にアクセスできるようになったので、オプションの ConfigValidator インターフェイスに従って Validate メソッドを実装し、それらの値に必要な検証を行えます。
この場合、interval の値はオプションです(デフォルト値の生成については後述します)。
ただし、定義する場合は少なくとも 1 分(1m)以上で、number_of_traces は必須の値です。
Validate メソッドを実装した後の config.go は次のようになります。
tailtracer/config.go
package tailtracer
import (
"fmt"
"time"
)
// Config は Collector の config.yaml におけるレシーバーの設定を表します
type Config struct {
Interval string `mapstructure:"interval"`
NumberOfTraces int `mapstructure:"number_of_traces"`
}
// Validate はレシーバーの設定が有効かどうかを確認します
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パッケージをインポートしました。 interval設定値が少なくとも 1 分(1m)以上であること、number_of_traces設定値が 1 以上であることを確認するValidateメソッドを Config 構造体に追加しました。 これが満たされない場合、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
次に、慣例に従って tailtracer ファクトリのインスタンス化を担当する NewFactory() という名前の関数を追加しましょう。
factory.go ファイルに次のコードを追加してください。
package tailtracer
import (
"go.opentelemetry.io/collector/receiver"
)
// NewFactory は tailtracer レシーバーのファクトリを作成します。
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
)
デフォルト設定については、tailtracer レシーバーのデフォルト設定を保持する component.Config を返す関数を追加するだけです。
それを実現するために、factory.go ファイルに次のコードを追加してください。
func createDefaultConfig() component.Config {
return &Config{
Interval: string(defaultInterval),
}
}
これら 2 つの変更の後、いくつかのインポートが不足していることに気づくでしょう。
適切なインポートを含めた 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 は tailtracer レシーバーのファクトリを作成します。
func NewFactory() receiver.Factory {
return nil
}
- defaultInterval の time.Duration 型をサポートするために
timeパッケージをインポートしました。 component.Configが宣言されているgo.opentelemetry.io/collector/componentパッケージをインポートしました。receiver.Factoryが宣言されているgo.opentelemetry.io/collector/receiverパッケージをインポートしました。- レシーバーの
Interval設定のデフォルト値を表すdefaultIntervalというtime.Duration定数を追加しました。 デフォルト値を 1 分に設定するため、値として1 * time.Minuteを代入しています。 component.Config実装を返すcreateDefaultConfigという関数を追加しました。 この場合、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: 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 は tailtracer レシーバーのファクトリを作成します。
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 は tailtracer レシーバーのファクトリを作成します。
func NewFactory() receiver.Factory {
return receiver.NewFactory(
typeStr,
createDefaultConfig,
receiver.WithTraces(createTracesReceiver, component.StabilityLevelAlpha))
}
createTracesReceiver関数で参照されるcontext.Context型をサポートするためにcontextパッケージをインポートしました。createTracesReceiver関数で参照されるconsumer.Traces型をサポートするためにgo.opentelemetry.io/collector/consumerパッケージをインポートしました。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: ホストは、レシーバーが起動して実行された後に 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パッケージをインポートしました。Host型が宣言されているgo.opentelemetry.io/collector/componentパッケージをインポートしました。receiver.Tracesインターフェースに準拠するためにStart(ctx context.Context, host component.Host)メソッドのブートストラップ実装を追加しました。receiver.Tracesインターフェースに準拠するためにShutdown(ctx context.Context)メソッドのブートストラップ実装を追加しました。
Start() メソッドは、レシーバーが処理操作の一部として保持する必要がある 2 つの参照(context.Context と component.Host)を渡しています。
context.Context の参照は、レシーバーの処理操作をサポートする新しいコンテキストの作成に使用する必要があります。
Shutdown() メソッドの一部としてコンテキストを適切に終了できるよう、コンテキストのキャンセルを処理する最適な方法を決定する必要があります。
component.Host はレシーバーのライフサイクル全体で役立つ可能性があるため、tailtracerReceiver 型にその参照を保持しましょう。
上記で提案された参照を保持するフィールドを含めた後の tailtracerReceiver 型宣言は次のようになります。
type tailtracerReceiver struct {
host component.Host
cancel context.CancelFunc
}
次に、レシーバーが独自の処理コンテキストを適切に初期化し、キャンセル関数を cancel フィールドに保持し、host フィールドの値を初期化できるように Start() メソッドを更新する必要があります。
また、cancel 関数を呼び出してコンテキストを終了するように Stop() メソッドも更新します。
変更を加えた後の 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 参照で host フィールドの初期化を追加して Start() メソッドを更新しました。
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をインポートしました。 - Collector がデバッグ機能に使用する
go.uber.org/zapパッケージをインポートしました。 - レシーバー内から Collector ロガー参照にアクセスできるように、
loggerという名前のzap.Loggerフィールドを追加しました。 tailtracerレシーバーが生成したトレースを Collector パイプラインで宣言された次のコンシューマーにプッシュできるように、nextConsumerという名前のconsumer.Tracesフィールドを追加しました。- Collector の設定で定義されたレシーバーの設定にアクセスできるように、
configという名前のConfigフィールドを追加しました。 tailtracerレシーバーのinterval設定の値に基づいてtime.Durationとして初期化されるintervalという変数を追加しました。Collector の設定で定義されています。tickerメカニズムを実装するgo func()を追加しました。tickerがinterval変数で指定された時間に達するたびにレシーバーがトレースを生成できるようにします。- レシーバーがトレースを生成すべきタイミングごとに info メッセージを生成するために
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 は tailtracer レシーバーのファクトリを作成します。
func NewFactory() receiver.Factory {
return receiver.NewFactory(
typeStr,
createDefaultConfig,
receiver.WithTraces(createTracesReceiver, component.StabilityLevelAlpha))
}
receiver.Settings参照のLoggerという名前のフィールドで利用可能な Collector ロガーで初期化されたloggerという変数を追加しました。component.Config参照をtailtracerレシーバーのConfigにキャストして初期化されたtailtracerCfgという変数を追加しました。- 変数に格納されたファクトリ情報を使用して
tailtracerReceiverインスタンスで初期化されたtraceRcvrという変数を追加しました。 traceRcvrインスタンスを含むように return 文を更新しました。
ここまでで、レシーバーのスケルトンは完全に実装されました。
レシーバーを使用した 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" // 新しく追加した行
)
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(), // 新しく追加した行
)
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レシーバーモジュールをインポートしました。 tailtracerレシーバーファクトリがfactoriesマップに適切に追加されるように、otelcol.MakeFactoryMap()呼び出しのパラメーターとしてtailtracer.NewFactory()への呼び出しを追加しました。
レシーバーの実行とデバッグ
Collector の config.yaml が、パイプラインで使用されるレシーバーの 1 つとして tailtracer レシーバーが設定されて適切に更新されていることを確認します。
config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
tailtracer: # この行はレシーバーの ID を表します
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 ファイルにコード変更があったため、以前生成した ./otelcol-dev/otelcol-dev バイナリファイルのかわりに go run コマンドを使用して更新された 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 は正常に初期化されました。
毎分、tailtracer/trace-receiver.go のダミー ticker によってトリガーされる I should start processing traces now! というメッセージが表示されます。
プロセスを停止するには Collector ターミナルで Ctrl + C を押してください。
さらに、通常の Go プロジェクトをデバッグするのと同様に、お好みの IDE を使用してレシーバーをデバッグできます。
以下は 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
次のセクションでは、tailtracer レシーバーがようやく意味のあるトレースを生成できるように、OpenTelemetry トレースデータモデルについて詳しく学びます!
Collector トレースデータモデル
SDK を使用し、アプリケーションを計装して Jaeger のような分散トレーシングバックエンドでトレースを観察・評価することで、OpenTelemetry トレースに馴染みがあるかもしれません。
以下は Jaeger でのトレースの表示例です。

これは Jaeger のトレースですが、Collector のトレースパイプラインによって生成されたものです。 これにより、OTel トレースデータモデルについていくつかのことが理解できます。
- トレースは、依存関係を表す階層構造で構成された 1 つ以上のスパンで構成されます。
- スパンは、サービス内および/またはサービス間の操作を表すことができます。
トレースレシーバーでのトレース作成は SDK で行う場合とは若干異なりますので、高レベルの概念を確認することから始めましょう。
リソースの操作
OTel の世界では、すべてのテレメトリーは Resource によって生成されます。
以下は OTel 仕様による定義です。
Resourceは、テレメトリーを生成するエンティティの属性としての不変な表現です。 たとえば、Kubernetes 上のコンテナで実行されているテレメトリーを生成するプロセスには Pod 名があり、名前空間で実行され、独自の名前を持つ Deployment の一部である場合があります。 これら 3 つの属性すべてをResourceに含めることができます。
トレースは最も一般的にサービスリクエスト(Jaeger のモデルで記述される Services エンティティ)を表すために使用されます。
これは通常、コンピュートユニットで実行されるプロセスとして実装されます。
しかし、OTel の API による属性を通じた Resource の記述方法は、ATM、IoT センサーなど、必要な任意のエンティティを表現するのに十分な柔軟性があります。
したがって、トレースが存在するためには Resource がそれを開始する必要があるといえます。
このチュートリアルでは、2 つの異なる州(たとえばイリノイ州とカリフォルニア州)にある ATM が Account のバックエンドシステムにアクセスして残高照会、入金、引き出しの操作を実行するテレメトリーを示すシステムをシミュレートします。
これを実現するために、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"
)
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 {
i := (rand.Intn(max - min + 1) + min)
return i
}
generateRandomNumber関数の実装をサポートするためにmath/randパッケージをインポートしました。Atm型をインスタンス化し、StateIDの値としてイリノイ州またはカリフォルニア州をランダムに割り当て、対応するISPNetworkの値も設定するgenerateAtm関数を追加しました。BackendSystem型のインスタンスを作成し、Endpointフィールドにサービスエンドポイント値をランダムに割り当てるgenerateBackendSystem関数を追加しました。- 指定された範囲内でランダムな数値を生成する
generateRandomNumber関数を追加しました。
テレメトリーを生成するエンティティを表すオブジェクトインスタンスを生成する関数ができたので、OTel Collector の世界でこれらのエンティティを表現する準備ができました。
Collector API は pdata パッケージの下にネストされた ptrace というパッケージを提供しています。
これには、Collector パイプラインコンポーネントでトレースを操作するために必要なすべての型、インターフェイス、ヘルパー関数が含まれています。
tailtracer/model.go ファイルを開き、ptrace パッケージの機能にアクセスできるように import 句に go.opentelemetry.io/collector/pdata/ptrace を追加してください。
Resource を定義する前に、Collector パイプラインを介してトレースを伝搬する ptrace.Traces を作成する必要があります。
ヘルパー関数 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
}
ここまでで、トレースがスパンで構成されることについて十分に聞いたり読んだりしてきたはずです。 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という変数を追加する。ResourceSpanに関連付けられたpcommon.Resourceを表すatmResourceという変数を追加する。- 上記のメソッドを使用して、それぞれの変数を初期化する。
変更を実装した後の関数は次のようになります。
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
}
traces.ResourceSpans().AppendEmpty()呼び出しで返されたResourceSpan参照で初期化されたresourceSpan変数を追加しました。resourceSpan.Resource()呼び出しで返されたpcommon.Resource参照で初期化されたatmResource変数を追加しました。
属性によるリソースの記述
Collector API は pdata パッケージの下にネストされた pcommon というパッケージを提供しています。
Resource を記述するために必要なすべての型とヘルパー関数が含まれています。
Collector のコンテキストでは、Resource は pcommon.Map 型で表されるキー/値ペア形式の属性によって記述されます。
pcommon.Map 型の定義とサポートされている形式で属性値を作成するための関連ヘルパー関数については、Collector の GitHub プロジェクト内の /pdata/pcommon/map.go ファイルを参照してください。
キー/値ペアは、Resource データをモデル化するための多くの柔軟性を提供します。
OTel 仕様には、表現する必要があるさまざまな種類のテレメトリー生成エンティティ間の整理と競合の最小化を支援するガイドラインがあります。
これらのガイドラインはリソースセマンティック規約として知られ、OTel 仕様に文書化されています。
独自のテレメトリー生成エンティティを表す独自の属性を作成する場合は、仕様で提供されているガイドラインに従うべきです。
属性は、記述するコンセプトの種類によって論理的にグループ化されます。 同じグループの属性には、ドットで終わる共通のプレフィックスがあります。 たとえば、Kubernetes のプロパティを記述するすべての属性は
k8s.で始まります。
まず tailtracer/model.go ファイルを開き、pcommon パッケージの機能にアクセスできるように import 句に go.opentelemetry.io/collector/pdata/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)
}
resource.Attributes()呼び出しで返されたpcommon.Map参照で初期化されたatmAttrsという変数を宣言しました。- 同等の
Atmフィールド型に基づいて int と string の属性を追加するためにpcommon.MapのPutInt()とPutStr()メソッドを使用しました。 これらの属性はAtmエンティティに固有でそれのみを表すため、すべてatm.プレフィックスでグループ化されています。
リソースセマンティック規約には、コンピュートユニット、環境など、異なるドメインにまたがって一般的で適用可能なテレメトリー生成エンティティを表すための規定的な属性名とよく知られた値もあります。
BackendSystem エンティティには、オペレーティングシステムとクラウドに関連する情報を表すフィールドがあります。
リソースセマンティック規約で指定された属性名と値を使用して、この情報をその Resource に表現します。
リソースセマンティック規約のキーとよく知られた値は、OpenTelemetry セマンティック規約パッケージ go.opentelemetry.io/otel/semconv/v1.38.0 で定義されています。
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 = semconv.CloudProviderAWS.Value.AsString()
case backend.CloudProvider == "mcrsft":
cloudProvider = semconv.CloudProviderAzure.Value.AsString()
case backend.CloudProvider == "gogl":
cloudProvider = semconv.CloudProviderGCP.Value.AsString()
}
backendAttrs.PutStr(string(semconv.CloudProviderKey), cloudProvider)
backendAttrs.PutStr(string(semconv.CloudRegionKey), backend.CloudRegion)
switch {
case backend.OSType == "lnx":
osType = semconv.OSTypeLinux.Value.AsString()
case backend.OSType == "wndws":
osType = semconv.OSTypeWindows.Value.AsString()
case backend.OSType == "slrs":
osType = semconv.OSTypeSolaris.Value.AsString()
}
backendAttrs.PutStr(string(semconv.OSTypeKey), osType)
backendAttrs.PutStr(string(semconv.OSVersionKey), backend.OSVersion)
}
Atm と BackendSystem エンティティ名を表す「atm.name」や「backendsystem.name」という属性を pcommon.Resource に追加していないことに注目してください。
これは、OTel トレース仕様と互換性のあるほとんど(すべてではないにしても)の分散トレーシングバックエンドシステムが、トレースで記述された pcommon.Resource を Service として解釈するためです。
したがって、リソースセマンティック規約で規定されている service.name という必須属性を pcommon.Resource が持つことを期待しています。
Atm と BackendSystem エンティティの両方のバージョン情報を表すために、service.version という非必須属性も使用します。
「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"
"go.opentelemetry.io/otel/semconv/v1.38.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 {
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(string(semconv.ServiceNameKey), atm.Name)
atmAttrs.PutStr(string(semconv.ServiceVersionKey), atm.Version)
}
func fillResourceWithBackendSystem(resource *pcommon.Resource, backend BackendSystem) {
backendAttrs := resource.Attributes()
var osType, cloudProvider string
switch {
case backend.CloudProvider == "amzn":
cloudProvider = semconv.CloudProviderAWS.Value.AsString()
case backend.CloudProvider == "mcrsft":
cloudProvider = semconv.CloudProviderAzure.Value.AsString()
case backend.CloudProvider == "gogl":
cloudProvider = semconv.CloudProviderGCP.Value.AsString()
}
backendAttrs.PutStr(string(semconv.CloudProviderKey), cloudProvider)
backendAttrs.PutStr(string(semconv.CloudRegionKey), backend.CloudRegion)
switch {
case backend.OSType == "lnx":
osType = semconv.OSTypeLinux.Value.AsString()
case backend.OSType == "wndws":
osType = semconv.OSTypeWindows.Value.AsString()
case backend.OSType == "slrs":
osType = semconv.OSTypeSolaris.Value.AsString()
}
backendAttrs.PutStr(string(semconv.OSTypeKey), osType)
backendAttrs.PutStr(string(semconv.OSVersionKey), backend.OSVersion)
backendAttrs.PutStr(string(semconv.ServiceNameKey), backend.ProcessName)
backendAttrs.PutStr(string(semconv.ServiceVersionKey), backend.Version)
}
Atmエンティティを表すpcommon.Resourceに「service.name」と「service.version」属性を適切に割り当てる行を追加してfillResourceWithAtm()関数を更新しました。BackendSystemエンティティを表すpcommon.Resourceに「service.name」と「service.version」属性を適切に割り当てる行を追加してfillResourceWithBackendSystem()関数を更新しました。fillResourceWithAtm()とfillResourceWithBackendSystem()関数を使用して、AtmとBackendSystemエンティティの両方の属性情報でpcommon.Resourceを適切にインスタンス化して埋める行を追加してgenerateTraces関数を更新しました。
スパンによる操作の表現
これで、適切な属性で埋められた各 Resource を持つ ResourceSpan インスタンスにより、Atm と BackendSystem エンティティを表現できるようになりました。
ResourceSpan 内のトレースの一部として各 Resource が実行する操作を表現する準備ができました。
OTel の世界では、システムがテレメトリーを生成するためには、手動または計装ライブラリを通じて自動的に計装される必要があります。
計装ライブラリは、トレースに参加する操作が発生するスコープ(計装スコープとしても知られる)を設定し、これらの操作をトレースのコンテキストでスパンとして記述する責任があります。
pdata.ResourceSpans には ScopeSpans() というメソッドがあり、ptrace.ScopeSpansSlice というヘルパー型のインスタンスを返します。
ptrace.ScopeSpansSlice 型には ptrace.ScopeSpans の配列を扱うためのメソッドがあります。
配列には、トレースのコンテキストで異なる計装スコープとそれが生成したスパンを表す ptrace.ScopeSpan の数と同じ数のアイテムが含まれます。
ptrace.ScopeSpansSlice には AppendEmpty() というメソッドがあり、新しい ptrace.ScopeSpans を配列に追加してその参照を返します。
ATM システムの計装スコープとそのスパンを表す ptrace.ScopeSpans をインスタンス化する関数を作成しましょう。
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は計装ライブラリに関連付けられたバージョンを返します。
新しい ptrace.ScopeSpans の計装スコープの名前とバージョンを設定できるように appendAtmSystemInstrScopeSpans 関数を更新しましょう。
更新後の 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 関数を更新して、Atm と BackendSystem エンティティの両方が使用する計装スコープを表す変数を appendAtmSystemInstrScopeSpans() で初期化して追加できます。
更新後の 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.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 によって一意に識別されます。
pcommon.SpanID 型で表される独自の一意の ID と、pcommon.TraceID 型で表される関連付けられたトレースの ID です。
pcommon.TraceID は 16 バイト配列として表されるグローバルに一意の ID を保持する必要があり、W3C Trace Context 仕様に従うべきです。
pcommon.SpanID は関連付けられたトレースのコンテキストで一意の ID であり、8 バイト配列として表されます。
pcommon パッケージはスパン ID を生成するための以下の型を提供しています。
type TraceID [16]bytetype SpanID [8]byte
このチュートリアルでは、pcommon.TraceID には github.com/google/uuid パッケージの関数を使用し、pcommon.SpanID のランダム生成には crypto/rand パッケージの関数を使用して ID を作成します。
まず、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
}
math/randとの競合を避けるためにcrypto/randをcrandとしてインポートしました。- トレース ID とスパン ID をそれぞれ生成するための新しい関数
NewTraceID()とNewSpanID()を追加しました。
スパンを適切に識別する方法ができたので、システム内のエンティティの内部および間の操作を表すスパンの作成を開始できます。
generateBackendSystem() 関数の一部として、BackEndSystem エンティティがシステムにサービスとして提供できる操作をランダムに割り当てました。
次に、tailtracer/model.go ファイルを開き、トレースを作成し BackendSystem の操作を表すスパンを追加する appendTraceSpans() という関数を見ます。
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))
}
- トレースとスパンの ID を表す
traceIdとbackendSpanId変数を追加し、前に作成したヘルパー関数で初期化しました。 - 操作の開始時間と終了時間を表す
backendSpanStartTimeとbackendSpanFinishTimeを追加しました。 チュートリアルでは、すべてのBackendSystem操作は 1 秒かかります。 - この操作を表す
ptrace.Spanのインスタンスを保持するbackendSpanという変数を追加しました。 BackendSystemインスタンスのEndpointフィールド値でスパンのNameを設定しました。- スパンの
Kindをptrace.SpanKindServerに設定しました。 SpanKind を適切に定義する方法については、トレース仕様の SpanKind セクションを参照してください。 - 上記のすべてのメソッドを使用して、
BackendSystem操作を表す適切な値でptrace.Spanを埋めました。
appendTraceSpans() 関数のパラメーターに ptrace.ScopeSpans への参照が 2 つありますが、そのうち 1 つしか使用していないことに気づいたかもしれません。
今は心配しないでください。後で戻ります。
次に、appendTraceSpans() 関数を呼び出してトレースを生成できるように generateTraces() 関数を更新します。
更新後の 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"
"go.opentelemetry.io/otel/semconv/v1.38.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 {
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(string(semconv.ServiceNameKey), atm.Name)
atmAttrs.PutStr(string(semconv.ServiceVersionKey), atm.Version)
}
func fillResourceWithBackendSystem(resource *pcommon.Resource, backend BackendSystem) {
backendAttrs := resource.Attributes()
var osType, cloudProvider string
switch {
case backend.CloudProvider == "amzn":
cloudProvider = semconv.CloudProviderAWS.Value.AsString()
case backend.CloudProvider == "mcrsft":
cloudProvider = semconv.CloudProviderAzure.Value.AsString()
case backend.CloudProvider == "gogl":
cloudProvider = semconv.CloudProviderGCP.Value.AsString()
}
backendAttrs.PutStr(string(semconv.CloudProviderKey), cloudProvider)
backendAttrs.PutStr(string(semconv.CloudRegionKey), backend.CloudRegion)
switch {
case backend.OSType == "lnx":
osType = semconv.OSTypeLinux.Value.AsString()
case backend.OSType == "wndws":
osType = semconv.OSTypeWindows.Value.AsString()
case backend.OSType == "slrs":
osType = semconv.OSTypeSolaris.Value.AsString()
}
backendAttrs.PutStr(string(semconv.OSTypeKey), osType)
backendAttrs.PutStr(string(semconv.OSVersionKey), backend.OSVersion)
backendAttrs.PutStr(string(semconv.ServiceNameKey), backend.ProcessName)
backendAttrs.PutStr(string(semconv.ServiceVersionKey), 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)) // 新しく追加した行
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 の内部または外部で発生した別の操作の結果として実行された操作を表していることを理解するには、以下を行う必要があります。
- 親/呼び出し元の
ptrace.Spanのpcommon.TraceIDをパラメーターとして渡してSetTraceID()メソッドを呼び出し、呼び出し元の操作と同じトレースコンテキストを設定する。 - 親/呼び出し元の
ptrace.Spanのpcommon.SpanIDをパラメーターとして渡してSetParentId()メソッドを呼び出し、トレースのコンテキストで呼び出し元の操作を定義する。
次に、Atm エンティティの操作を表す ptrace.Span を作成し、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"
"go.opentelemetry.io/otel/semconv/v1.38.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 {
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(string(semconv.ServiceNameKey), atm.Name)
atmAttrs.PutStr(string(semconv.ServiceVersionKey), atm.Version)
}
func fillResourceWithBackendSystem(resource *pcommon.Resource, backend BackendSystem) {
backendAttrs := resource.Attributes()
var osType, cloudProvider string
switch {
case backend.CloudProvider == "amzn":
cloudProvider = semconv.CloudProviderAWS.Value.AsString()
case backend.CloudProvider == "mcrsft":
cloudProvider = semconv.CloudProviderAzure.Value.AsString()
case backend.CloudProvider == "gogl":
cloudProvider = semconv.CloudProviderGCP.Value.AsString()
}
backendAttrs.PutStr(string(semconv.CloudProviderKey), cloudProvider)
backendAttrs.PutStr(string(semconv.CloudRegionKey), backend.CloudRegion)
switch {
case backend.OSType == "lnx":
osType = semconv.OSTypeLinux.Value.AsString()
case backend.OSType == "wndws":
osType = semconv.OSTypeWindows.Value.AsString()
case backend.OSType == "slrs":
osType = semconv.OSTypeSolaris.Value.AsString()
}
backendAttrs.PutStr(string(semconv.OSTypeKey), osType)
backendAttrs.PutStr(string(semconv.OSVersionKey), backend.OSVersion)
backendAttrs.PutStr(string(semconv.ServiceNameKey), backend.ProcessName)
backendAttrs.PutStr(string(semconv.ServiceVersionKey), 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 でのそれらのトレースの 1 つの詳細ビューです。

以上です! これでこのチュートリアルの最後に到達し、トレースレシーバーの実装に成功しました。おめでとうございます!
フィードバック
このページは役に立ちましたか?
Thank you. Your feedback is appreciated!
Please let us know how we can improve this page. Your feedback is appreciated!