🔭

OpenTelemetry と OpenObserve でトレースとログを可視化してみた

2024/05/22に公開

マイクロサービスや分散システムなど今のシステムは複雑化していて全体の挙動を把握することは難しくなってきています。そこでシステムの内部状態を外部から可視化しシステムのパフォーマンスや健全性を維持するオブザーバビリティは必要不可欠となっています。
オブザーバビリティを高めることで、

  • 障害の迅速な検出:リアルタイムでシステムの状態を監視し、問題が発生したときに通知することでダウンタイムを最小限に抑えられる
  • 信頼性の向上: アプリケーションのレスポンス時間やエラーレートなどを監視して自動リカバリ機能を実装したりユーザー体験の向上に繋げられる
  • パフォーマンスの最適化:システムのボトルネックを特定しパフォーマンス向上の改善点を見つけられる

が期待できます。

オブザーバビリティはシステム運用の中核となる要素で、オブザーバビリティを実現するためにトレース、ログ、メトリクスが重要になります。
それぞれのデータは異なる視点からシステムの状態を可視化するのでデバッグコードを仕込んだアプリケーションをデプロイしなくても包括的なモニタリングとトラブルシューティングが期待できます。

そこで、Rust で Event Sourcing を試してみた ~ AWS のブログを参考に模倣する ~ で構築した Web API サーバに OpenTelemetry を利用してトレースとログを収集し、OpenObserve でそのテレメトリデータを可視化する方法について説明します。

オブザーバビリティ (Observability) とは

オブザーバビリティは、システムの内部状態を外部から可視化するための概念で、システムの健全性やパフォーマンスを監視し、問題の診断やトラブルシューティングを容易にします。

オブザーバビリティにおいて重要なデータ (テレメトリデータ) には以下の3つがあります。

トレース(Trace)

トレースはシステムのリクエスト全体を追跡するためのデータで、リクエストがシステム内のどのサービスを経由しどの部分で遅延やエラーが発生しているのかが把握しやすくなります。

トレースは下記画像 (https://opentelemetry.io/img/waterfall-trace.svg より引用) のように表現されます。

waterfall-trace

画像のようにトレースは複数の要素 (Span) から構成され、各 Span に記録された処理の引数や返り値、処理時間などから問題の特定ができます。

ログ(Log)

ログはシステムの動作に関してテキストで記録したデータで、メッセージとともにログの重要度 (Error や Info など) やタイムスタンプが記録されます。
ログは自由にテキストを書けるので適切に運用されていれば問題の根本原因の特定がしやすくなります。

メトリクス(Metrics)

メトリクスはシステムのパフォーマンスやリソース使用状況を定量的に示すデータで、CPU使用率、メモリ使用量、リクエストのレイテンシなどが含まれます。主にシステムの状態をリアルタイムで監視し、異常を早期に検出するために使用されます。

OpenTelemetry と OpenObserve の役割と特徴について

OpenTelemetry

OpenTelemetryはオープンソースのオブザーバビリティフレームワークで、トレース、メトリクス、ログを収集するための API と SDK を提供します。

特徴としては

  • 言語サポート:多くのプログラミング言語をサポートしているので簡単に各言語同じ仕様で実装が可能
  • 拡張性:プラグインを利用してニーズに合わせた拡張が可能
  • 標準化:オブザーバビリティデータの収集方法が標準化されているのでツール間の互換性ある

があげられます。

また、Datadog や Prometheus などのベンダーにロックされないため OpenTelemetry のデータ形式に対応しているツールで可視化することができます。
これは、例えば Datadog から New Relic への移行時に OpenTelemetry の設定でデータの送信先を変えるだけで移行が可能となり、アプリケーションのコードを変更する必要がないことを意味します。さらに、データの送信先はテレメトリデータごとに複数設定することも可能なのでトレースは Datadog に、メトリクスは Prometheus と Elasticsearch に送信するということが可能です。

OpenObserve

OpenObserveは、収集したテレメトリーデータを可視化するためのプラットフォームです。直感的なダッシュボードや高度なクエリ機能を提供し、システムの状態を視覚的に分析することができます。

特徴

  • リアルタイム分析:リアルタイムでデータを解析し、即座にインサイトを得られる
  • カスタマイズ可能なダッシュボード:ユーザーが自由にダッシュボードを作成可能
  • 統合性:テレメトリデータを含めた多くのデータソースと簡単に統合できる
  • 低価格: 収集したデータを圧縮してストレージに保存するため低コスト

OpenTelemetry の送信先としてトレースは Jaeger、メトリクスは Prometheus などそれぞれのテレメトリデータに合わせて利用するプラットフォームがバラバラになることが多いですが、OpenObserve はトレース、ログ、メトリクスのデータを収集して可視化することが可能なので圧倒的に運用コストが低いです。

他のツールとの比較

下記が OpenTelemetry でよく利用されるプラットフォームとの比較です。

  • Prometheus:主にメトリクスの収集と監視に特化
  • Jaeger:トレースの収集と可視化に特化
  • Grafana:多機能なダッシュボードツールで、Prometheus や OpenTelemetry からのデータを統合して表示できる

上記3つだと OpenObserve の対抗馬としては Garafana が挙げられます。
実際に OpenObserve を使ってみて思ったのは Grafana のほうが洗礼されている感じがしましたが、OpenObserve のほうが設定がシンプルだと感じました。

OpenTelemetry で計装する

Rust で Event Sourcing を試してみた ~ AWS のブログを参考に模倣する ~ で構築した Web API サーバを OpenTelemetry で計装するコード例を紹介します。
今回のコードは GitHub で Pull Request を公開しているので参考にしてみてください。

https://github.com/pyama2000/example-cqrs-event-store/pull/15

計装に必要な依存関係

下記が OpenTelemetry による計装に必要な依存関係です。

Cargo.toml
[dependencies]
opentelemetry = "0.21.0"
opentelemetry-appender-tracing = "0.2.0"
opentelemetry-otlp = { version = "0.14.0", features = ["logs"] }
opentelemetry-semantic-conventions = "0.13.0"
opentelemetry_sdk = { version = "0.21.0", features = ["rt-tokio"] }
tracing = "0.1.40"
tracing-opentelemetry = "0.22.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
  • opentelemetry
    • OpenTelemetry の基本的な機能を提供するクレート
    • トレース、メトリクス、ログを収集・エクスポートするためのインターフェースを含む
  • opentelemetry-appender-tracing
    • tracing クレートのログを OpenTelemetry 形式で出力するためのクレート
    • tracing クレートで生成されたスパンやログを OpenTelemetry のデータフォーマットに変換してトレースと紐づける
  • opentelemetry-otlp
    • OpenTelemetry プロトコル(OTLP)でデータをエクスポートするためのクレート
    • デフォルトではトレースのみサポートするが features を使うことでログやメトリクスもサポートする
  • opentelemetry-semantic-conventions
    • OpenTelemetry の セマンティック規約 を提供するクレート
    • セマンティック規約によってトレースやメトリクスのデータが標準化され、異なるシステム間で一貫性のあるデータを収集・分析することができる
  • opentelemetry_sdk
    • OpenTelemetry SDK のクレート
    • トレースやメトリクスの計測やエクスポート設定、バッチ処理などの機能を提供する
  • tracing
    • スパン生成やロギングに利用するクレート
  • tracing-opentelemetry
    • tracing クレートと OpenTelemetry の統合する
    • tracing のスパンやイベントを OpenTelemetry のトレースとしてエクスポートする
  • tracing-subscriber
    • tracing クレートのフィルタリングやフォーマットなどの設定をする

また、Web API サーバでトレースやログを出力するミドルウェアを追加するために下記依存関係を追加します。

internal/driver/Cargo.toml
bytes = "1.5.0"
http-body-util = "0.1.0"
opentelemetry = { version = "0.22.0", features = ["logs"] }
opentelemetry-semantic-conventions = "0.14.0"
tower-http = { version = "0.5.1", features = ["timeout", "catch-panic", "trace"] }
tracing = "0.1.40"
tracing-opentelemetry = "0.23.0"

byteshttp-body-util クレートは HTTP リクエストのヘッダーからトレースデータを抽出するために必要なため追加しています。
また、tower-http クレートの trace feature を有効にすることで TraceLayer が利用できるので、このミドルウェアを使ってトレースをエクスポートします。

OpenTelemetry と tracing の設定

トレースとログを全てエクスポートする前にプログラムを終了してしまうとデータの欠落が発生してしまいます。そこで、

https://github.com/pyama2000/example-cqrs-event-store/blob/d33f64ece1bb8ded8907ea6ec896d3411b0a70ab/src/main.rs#L20-L27

のように OpenTelemetryGuard 構造体を定義し、その構造体に Drop Trait を実装して構造体が main 関数のスコープから抜けるときにトレースとログのプロバイダをシャットダウンして全てのデータをエクスポートするようにします。

この OpenTelemetryGuard 構造体は start_instrument 関数の返り値にしています。

https://github.com/pyama2000/example-cqrs-event-store/blob/d33f64ece1bb8ded8907ea6ec896d3411b0a70ab/src/main.rs#L29-L90

この start_instrument 関数ではトレースとログを出力する OpenTelemetry のエンドポイントを設定したりトレース ID の生成、バッチ処理を設定したりします。
この関数を詳しく解説すると

https://github.com/pyama2000/example-cqrs-event-store/blob/d33f64ece1bb8ded8907ea6ec896d3411b0a70ab/src/main.rs#L48-L55

でトレースやログに付加するサービス名、バージョン、デプロイ環境などのメタデータを生成しています。

https://github.com/pyama2000/example-cqrs-event-store/blob/d33f64ece1bb8ded8907ea6ec896d3411b0a70ab/src/main.rs#L56-L69

はトレースの設定でトレース ID を RandomIdGenerator でランダムな ID を生成し、デフォルトの設定でバッチ処理するようにしています。

ログの設定もほぼ同じなので割愛します。

そして tracing-subscriber クレートでトレーストログのフォーマットと OpenTelemetry へエクスポートするように設定します。

https://github.com/pyama2000/example-cqrs-event-store/blob/d33f64ece1bb8ded8907ea6ec896d3411b0a70ab/src/main.rs#L79-L88

EnvFilter で環境変数の RUST_LOG から出力するログレベルを決定できるようにしています (デフォルトは INFO)。そして OpenTelemetryLayer と OpenTelemetryTracingBridge をレイヤーに追加することで収集したトレースデータとログデータが OpenTelemetry に送信します。

あとは main 関数で start_instrument を呼び出すとアプリケーション全体で tracing でスパンやロギングしたデータが OpenTelemetry にエクスポートされるようになります。

https://github.com/pyama2000/example-cqrs-event-store/blob/d33f64ece1bb8ded8907ea6ec896d3411b0a70ab/src/main.rs#L8-L9

ミドルウェアでトレースを生成する

tower-http クレートの TraceLayer は、HTTPリクエストの処理をトレースするために使用されるミドルウェアです。このミドルウェアはリクエストが届いてからレスポンスがクライアントに返されるまでの間にリクエストの詳細な情報を収集し、トレースデータとして記録します。また HTTP リクエストヘッダーにトレース情報 ( Trace Context ) がセットされていたら元のトレースを親としたり、エラーが起きたときにスタックトレースを記録するために利用します。

TraceLayer の定義自体は下記のようにシンプルですが、スパンの生成 ( make_span_with ) やリクエストを受け取ったときの処理 ( on_request )、レスポンスを返すときの処理 ( on_response )、エラーが発生したときの処理 ( on_failure ) はそれぞれ別の関数で定義しています。

https://github.com/pyama2000/example-cqrs-event-store/blob/d33f64ece1bb8ded8907ea6ec896d3411b0a70ab/internal/driver/src/lib.rs#L48-L52

TraceLayer::make_span_with に渡している make_span は新しいスパンを作成する関数です。
HTTP リクエストのメソッドやルートからスパン名 (= span_name 変数) を生成して otel.name としてスパン名を記録しているのは、tracing::info_span マクロの第1引数 (スパン名) には変数を指定できなかったからです ( tracing_opentelemetry - Special Fields ) 。
また、後続の処理でスパンに記録するフィールドはあらかじめスパンが初期化されるときに定義する必要があるのでこの関数内で定義しています。

https://github.com/pyama2000/example-cqrs-event-store/blob/d33f64ece1bb8ded8907ea6ec896d3411b0a70ab/internal/driver/src/observability.rs#L44-L64

また、リクエストヘッダーからトレース情報を抽出してトレースを関連付けるようにもしています。
トレースを関連付けることで複数のサービスにまたがるトレースが一意に識別され、システム全体の動作を追跡することができるようになります。
リクエストヘッダーからトレース情報を抽出するために HeaderExtractor 構造体を定義しています。opentelemetry-http クレートの HeaderExtractor を利用することも可能ですが、0.21.0 時点では内部で依存している http クレートのバージョン違いで利用できなかったので独自に構造体を定義しました。

https://github.com/pyama2000/example-cqrs-event-store/blob/d33f64ece1bb8ded8907ea6ec896d3411b0a70ab/internal/driver/src/observability.rs#L21-L31

TraceLayer::on_request に渡している record_request 関数ではリクエストの URL パスや HTTP メソッド、ヘッダー情報などをスパンに記録します。どの情報をスパンに記録するべきかは OpenTelemetry に仕様 ( Semantic Conventions for HTTP Spans ) を参考にしています。

https://github.com/pyama2000/example-cqrs-event-store/blob/d33f64ece1bb8ded8907ea6ec896d3411b0a70ab/internal/driver/src/observability.rs#L67-L83

TraceLayer::on_responseTraceLayer::on_failure に渡す関数も record_request と同様にそれぞれのレイヤーで取得できる情報をスパンに記録しているだけなので説明は割愛します。

関数ごとにスパンを生成する

関数ごとにスパンを生成するために tracing クレートの instrument を利用します。これを利用すると簡単に関数の引数や返り値などが記録されたスパンを生成できます。

今回実装した Web API サーバで例を示すと下記のように #[tracing::instrument(ret, err)] を関数の上で宣言して、返り値またはエラーをスパンに記録するようにしています。

https://github.com/pyama2000/example-cqrs-event-store/blob/d33f64ece1bb8ded8907ea6ec896d3411b0a70ab/internal/app/src/lib.rs#L86-L101

OpenTelemetry の設定

OpenTelemetry の設定について詳しくは Configuration に記載があるのでそちらも参考にしてください。

今回設定した内容は以下の通りです。

https://github.com/pyama2000/example-cqrs-event-store/blob/d33f64ece1bb8ded8907ea6ec896d3411b0a70ab/config/opentelemetry-collector/config.yaml

receivers

OpenTelemetry で受け取るデータに関する設定です。

  • otlp
    • gRPC プロトコルで OTLP フォーマットのデータ (トレース / メトリクス / ログ) を受け取る
  • prometheus/otel-collector
    • Prometheus から定期的にメトリクスデータを収集する

processors

受け取ったデータを処理する設定を定義します。

  • batch
    • データを一定の時間間隔でまとめて送信するプロセッサ
    • 送信頻度を制御することでトレースやメトリクスの送信を効率化し負荷を軽減できる

exporters

処理されたデータをエクスポートする設定です。

  • logging
    • トレース、メトリクス、ログデータをコンソールに出力する
    • deprecated なエクスポーターなので debug exporter に乗り換える必要がある
  • otlp/openobserve
    • OTLP フォーマットで指定したエンドポイント (OpenObserve) に gRPC プロトコルを使ってデータを出力する

service

トレース、メトリクス、ログのパイプラインを設定します。
他にも Health Check のエクステンションを設定したり OpenTelemetry Collector 自身のメトリクス、ログの設定も可能です。

パイプラインはトレース、メトリクス、ログそれぞれの receivers と processors、exporters を指定します。
今回は以下のように設定しました。

テレメトリデータ receivers processors exporters
トレース [otlp] を指定し OTLP でトレースを受信する [batch] を指定しバッチ処理する [logging, otlp/openobserve] を指定しログと OpenObserve にエクスポートする
メトリクス [otlp, prometheus/otel-collector] を指定し OTLP と Prometheus からメトリクスを受信する [batch] を指定しバッチ処理する [logging, otlp/openobserve] を指定しログと OpenObserve にエクスポートする
ログ [otlp] を指定し OTLP でトレースを受信する [batch] を指定しバッチ処理する [logging, otlp/openobserve] を指定しログと OpenObserve にエクスポートする

これらの設定によりトレース、メトリクス、ログデータを効率的に収集して処理し、エクスポートできます。

トレースとログを可視化する

ここまでで Rust の Web API サーバから OpenTelemetry にトレースとログを送信できるようになり、OpenTelemetry からそれぞれのデータを OpenObserve にエクスポートする準備ができました。
続いては実際に OpenTelemetry と OpenObserve を Docker コンテナとして立ち上げてトレースとログを可視化します。

https://github.com/pyama2000/example-cqrs-event-store/blob/d33f64ece1bb8ded8907ea6ec896d3411b0a70ab/compose.yaml#L15-L33

Docker Compose ファイルのコンテナと Web API サーバ を立ち上げるために以下のコマンドを実行します。

# Docker Compose でコンテナを立ち上げる
docker compose up
# データベースにマイグレーションを実行する
cargo run --bin migrate --features migrate
# Web API サーバを立ち上げる
cargo run --release

ブラウザで http://localhost:5080 にアクセスすると OpenObserve のログイン画面が表示されるので User Emailroot@example.com を、PasswordComplexpass#123 を入力するとログインできます。これらの値は compose.yaml の openobserve コンテナの環境変数に設定された ZO_ROOT_USER_EMAILZO_ROOT_USER_PASSWORD の値です。

あとは立ち上げた Web API サーバにリクエストすると http://localhost:5080/web/traces でトレースが表示されているのが確認できます。

OpenObserveでトレースを表示した画像

また OpenObserve には Dashboard 機能があるのでエラーカウントを表示したりすることも可能です。
簡単なダッシュボードの JSON ファイルも用意しているので、 ./config/openobserve/traces.dashboard.json をコピーして http://localhost:5080/web/dashboards/import に JSON の文字列を貼り付けてインポートすると表示されます。

OpenObserveでダッシュボードのJSONをインポートする方法

インポートしたダッシュボードを表示した画像

まとめ

Rust で書いた Web API サーバのトレースとログを OpenTelemetry に集約して OpenObserve で可視化する方法を解説しました。
OpenTelemetry を利用することでテレメトリデータを収集して様々なバックエンド (OpenObserve や New Relic など) にエクスポートして、アプリケーションのパフォーマンスやエラーの原因を迅速に特定できるのはとても魅力的に感じました。

次回はイベントストアに利用していたデータベースを MySQL から DynamoDB に変えて、DynamoDB Streams や Lambda などを利用して Query 用のデータベースにレコードを追加するなどしていこうと思います。

GitHubで編集を提案

Discussion