RustにおけるSpans、Events、Tower-HTTPを用いたオブザーバビリティの解明
Takashi Yamamoto
Infrastructure Engineer · Leapcell

はじめに
現代のソフトウェア開発の複雑な世界では、特に分散システムにおいて、アプリケーションの動作を理解することが不可欠です。問題が発生した場合でも、順調に進んでいる場合でも、実行フロー、操作にかかった時間、イベントのコンテキストに関する洞察を得ることは、デバッグ、パフォーマンス最適化、および一般的なシステム健全性監視にとって極めて重要です。そこで、堅牢なオブザーバビリティツールが登場します。Rustのエコシステムには、構造化ロギングと分散トレーシングのための強力で柔軟なフレームワークであるtracingがあります。これは、洗練された診断ツールを構築するための基盤を提供します。
この記事では、tracingを深く掘り下げ、その基本的なコンポーネントであるスパンとイベントを探求し、次に、人気のあるHTTPミドルウェアのコレクションであるtower-httpとシームレスに統合して、Webサービスに実用的なオブザーバビリティをもたらす方法を実証します。
トレーシングのコアの理解
実践的な例に入る前に、tracing内の2つの中心的な概念、つまり**スパン(Spans)とイベント(Events)**をしっかりと理解しましょう。
スパン:実行の封筒
スパンは、アプリケーション内での実行期間を表します。特定の操作をカプセル化する、境界付けられたコンテキスト、つまり論理的な作業単位と考えてください。スパンには開始と終了があり、階層構造を形成します。たとえば、単一のHTTPリクエストハンドラーがスパンになる可能性があります。そのハンドラー内で、データベースクエリや外部API呼び出しがネストされたスパンになる可能性があります。この階層関係は、操作の因果連鎖を理解し、システム全体のリクエストフローを再構築するために不可欠です。スパンは、フィールドと呼ばれる関連データを保持でき、これらは、リクエストID、ユーザーID、または入力パラメータなど、その特定の操作に関連するコンテキスト情報を提供します。
イベント:時間のポイント
スパンとは対照的に、イベントは、アプリケーションの実行内での特定の時点における、離散的で瞬時的な発生を表します。これらはアトミックなログメッセージであり、エラーの検出、ユーザーのログイン、または特定のデータ変換ステップの完了など、必ずしも期間を包含しない重要な発生を報告するためによく使用されます。スパンと同様に、イベントも関連コンテキストを提供するためにフィールドを保持します。
tracingの力は、スパンとイベントが連携する方法にあります。イベントは多くの場合、アクティブなスパンのコンテキスト内で発生し、そのコンテキストフィールドを継承し、その特定の作業単位内で何が起こっているかの物語を強化します。
レイヤーとサブスクライバ
tracingは、出力をコンソールに直接印刷したり、データをトレーシングバックエンドに送信したりしません。代わりに、サブスクライバとレイヤーのシステムを通じて動作します。
サブスクライバは、tracingのSubscriberトレイトを実装する型であり、生成されるトレーシングデータ(スパンとイベント)を処理する責任があります。
レイヤーは、トレースデータソースとサブスクライバの間に配置されるコンポーズ可能なユニットであり、最終的なサブスクライバに到達する前にデータをフィルタリング、フォーマット、およびエンリッチすることを可能にします。このアーキテクチャは、さまざまな宛先にログを記録したり、特定の種類のイベントをフィルタリングしたりするなど、オブザーバビリティソリューションを特定のニーズに合わせて調整できる、計り知れない柔軟性を提供します。
スパンとイベントによる実践的なトレーシング
いくつかのRustコードでこれらの概念を説明しましょう。まず、Cargo.tomlにtracingとtracing-subscriberを追加する必要があります。
[dependencies] tracing = "0.1" tracing-subscriber = "0.3"
次に、いくつかの作業をシミュレートするシンプルな関数を考えてみましょう。
use tracing::{info, span, Level}; #[tracing::instrument] // このマクロは関数のスパンを作成します async fn perform_complex_operation(input_value: u32) -> String { // スパン内のイベント info!("Starting complex operation with input_value={}", input_value); // いくつかの作業をシミュレート tokio::time::sleep(std::time::Duration::from_millis(50)).await; let intermediate_result = input_value * 2; // 別のイベント info!("Calculated intermediate_result={}", intermediate_result); // 新しいネストされたスパンに入る let nested_span = span!(Level::INFO, "nested_processing", step = 1); let _guard = nested_span.enter(); // スパンコンテキストに入る tokio::time::sleep(std::time::Duration::from_millis(30)).await; let final_result = format!("Processed: {}", intermediate_result + 10); info!("Finished nested processing"); drop(_guard); // ネストされたスパンを明示的に終了する info!("Complex operation completed, returning: {}", final_result); final_result } #[tokio::main] async fn main() { // コンソール出力用のシンプルなサブスクライバを初期化する tracing_subscriber::fmt::init(); let result = perform_complex_operation(42).await; info!("Application finished with result: {}", result); }
この例では:
perform_complex_operationの#[tracing::instrument]は、関数全体の実行を囲むスパンを自動的に作成します。また、関数引数をスパンのフィールドとして自動的に含めます。info!マクロ呼び出しはイベントを生成します。info!("Starting complex operation with input_value={}", input_value);が、現在のスパンのコンテキスト(#[tracing::instrument]のため)でinput_valueをフィールドとして自動的に取得していることに注意してください。span!を使用してネストされたスパンnested_processingを手動で作成し、nested_span.enter()でそのコンテキストに入ります。_guardは、スパンがスコープを外れたとき(またはdropで明示的に)にスパンが終了することを保証します。これは、きめ細かな制御のための手動スパン作成を示しています。tracing_subscriber::fmt::init()は、フォーマットされたトレースデータをコンソールに出力する基本的なサブスクライバを設定し、階層構造を確認できるようにします。
これを実行すると、次のような(明確さのために簡略化された)出力を観察できます。
INFO tokio_app: Starting complex operation with input_value=42 span=perform_complex_operation
INFO tokio_app: Calculated intermediate_result=84 span=perform_complex_operation
INFO tokio_app: Finished nested processing span=perform_complex_operation::nested_processing step=1
INFO tokio_app: Complex operation completed, returning: Processed: 94 span=perform_complex_operation
INFO tokio_app: Application finished with result: Processed: 94
span=... が各イベントのアクティブなスパンを示し、span=perform_complex_operation::nested_processing がネストされたスパンを示していることに注意してください。
Tower-HTTPとの統合
次に、tracingとtowerサービス用のHTTPミドルウェアのコレクションであるtower-httpを統合して、オブザーバビリティを高めましょう。
tower-httpは、この目的に特別に設計されたTraceミドルウェアを提供します。
まず、Cargo.tomlに必要な依存関係を追加します。
[dependencies] tracing = "0.1" tracing-subscriber = "0.3" tokio = { version = "1", features = ["full"] } axum = "0.6" # シンプルなWebサーバーにaxumを使用 tower = "0.4" tower-http = { version = "0.4", features = ["trace"] }
次に、簡単なaxumアプリケーション(内部でtowerを使用)を作成し、Traceミドルウェアを適用しましょう。
use axum::{routing::get, Router}; use tower_http::trace::{TraceLayer, DefaultOnRequest, DefaultOnResponse}; use tracing::{info, Level}; use std::time::Duration; // トレーシングされた関数を使用するシンプルなハンドラ async fn hello_handler() -> String { info!("Handler received request"); let result = perform_complex_operation(100).await; format!("Hello, from handler! {}", result) } // 前のトレーニングされた関数 #[tracing::instrument] async fn perform_complex_operation(input_value: u32) -> String { info!("Starting complex operation with input_value={}", input_value); tokio::time::sleep(Duration::from_millis(50)).await; let intermediate_result = input_value * 2; info!("Calculated intermediate_result={}", intermediate_result); // ... (以前と同じ関数の残りの部分) let nested_span = tracing::span!(Level::INFO, "nested_processing", step = 1); let _guard = nested_span.enter(); tokio::time::sleep(Duration::from_millis(30)).await; let final_result = format!("Processed: {}", intermediate_result + 10); info!("Finished nested processing"); drop(_guard); info!("Complex operation completed, returning: {}", final_result); final_result } #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); let app = Router::new() .route("/hello", get(hello_handler)) .layer( TraceLayer::new_for_http() // HTTPサービス用の新しいTraceLayerを作成する .on_request(DefaultOnRequest::new().level(Level::INFO)) // リクエスト詳細をログに記録する .on_response(DefaultOnResponse::new().level(Level::INFO)), // レスポンス詳細をログに記録する ); let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); info!("Listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); }
この強化された例では:
/helloエンドポイントを持つaxumRouterを作成しました。TraceLayer::new_for_http()ミドルウェアをアプリケーションに適用しました。これにより、各受信HTTPリクエストのルートスパンが自動的に作成されます。.on_request(DefaultOnRequest::new().level(Level::INFO))は、ミドルウェアがリクエストの開始時に、メソッドとパスなどの詳細を含むINFOレベルのイベントを発行するように構成します。.on_response(DefaultOnResponse::new().level(Level::INFO))は、ステータスコードと応答時間を含め、応答が送信されるときにINFOレベルのイベントを発行するように構成します。
このアプリケーションを実行し、http://127.0.0.1:3000/hello にリクエストを送信すると(たとえば、curl を使用して)、包括的なトレース出力が表示されます。
INFO tower_http::trace::make_span: started processing request request.method=GET request.uri=/hello request.version=HTTP/1.1 remote_addr=127.0.0.1:49877 request_id=... span=http_request
INFO axum_app: Handler received request span=http_request
INFO axum_app: Starting complex operation with input_value=100 span=http_request::perform_complex_operation
INFO axum_app: Calculated intermediate_result=200 span=http_request::perform_complex_operation
INFO axum_app: Finished nested processing span=http_request::perform_complex_operation::nested_processing step=1
INFO axum_app: Complex operation completed, returning: Processed: 210 span=http_request::perform_complex_operation
INFO tower_http::trace::make_span: finished processing request status=200 response.time=100ms span=http_request
tower-http がトップレベルの http_request スパンを作成し、ハンドラ内のすべての後続の info! イベントおよび手動スパンが自動的にこのリクエストスパンの下にネストされていることに注目してください。これは、HTTPリクエストのライフサイクルの全体像、つまり到着から最終応答まで、内部操作が詳細なコンテキストを提供していることを明確に示しています。この構造化された階層ビューは、ボトルネックの特定、リクエストフローの理解、およびWebサービス内の問題のデバッグに非常に役立ちます。
結論
tracing クレートは、Rustのオブザーバビリティツールキットにおいて不可欠なツールであり、アプリケーションの動作を理解するための堅牢で柔軟なフレームワークを提供します。作業の包含単位としてのスパンと、瞬時的な発生としてのイベントの概念を習得することで、コードの実行を詳細に描写する力を得ることができます。
tracing を tower-http と統合することで、この比類なき可視性がWebサービスに直接もたらされ、不透明なHTTPリクエストが明確で追跡可能な物語に変換されます。
tracing を採用することは、開発者がより回復力があり、パフォーマンスが高く、理解しやすいRustアプリケーションを構築できるようにします。

