🐕

Rustでaxumとreqwestを使用したロギングの実装

2024/08/21に公開

背景

最近はRustの勉強にはまっており、個人アプリをつくりながら学習しています。
Rustは主にWeb API Backendの用途で使っています。

APIをつくっていると、loggingしたいですが自分で設定していく必要があります。
今回はその実装時に調べたことを記事にしました。

はじめに

RustのWebフレームワークaxumと、HTTPクライアントライブラリreqwestを使用する際のロギング実装について説明します。トレーシングとロギングを適切に設定することで、アプリケーションの動作を詳細に把握し、デバッグや性能分析を効果的に行うことができます。

axumでのロギング設定

axumでロギングを実装するには、主にtracingクレートを使用します。

1. 依存関係の追加

Cargo.tomlに以下の依存関係を追加します:

[dependencies]
axum = "0.6.0"
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

2. トレーシングの初期化

main.rsでトレーシングを初期化します:

use axum::{Router, routing::get};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

#[tokio::main]
async fn main() {
    tracing_subscriber::registry()
        .with(tracing_subscriber::EnvFilter::new(
            std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()),
        ))
        .with(tracing_subscriber::fmt::layer())
        .init();

    // ルーターの設定
    let app = Router::new().route("/", get(handler));

    // サーバーの起動
    let addr = "127.0.0.1:3000";
    tracing::info!("Listening on {}", addr);
    axum::Server::bind(&addr.parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn handler() -> &'static str {
    "Hello, World!"
}

3. ミドルウェアを使用したリクエストのロギング

すべてのリクエストをログに記録するミドルウェアを実装します:

use axum::{
    Router, 
    routing::get,
    middleware::{self, Next},
    response::IntoResponse,
    http::Request,
    body::Body,
};
use tracing::instrument;

pub async fn logging_middleware(req: Request<Body>, next: Next) -> impl IntoResponse {
    let path = req.uri().path().to_owned();
    let method = req.method().clone();

    tracing::info!("Received request: {} {}", method, path);

    next.run(req).await
}

#[tokio::main]
async fn main() {
    // トレーシングの初期化(前述のコードと同じ)

    let app = Router::new()
        .route("/", get(handler))
        .layer(middleware::from_fn(logging_middleware));

    // サーバーの起動(前述のコードと同じ)
}

#[instrument]
async fn handler() -> impl IntoResponse {
    tracing::info!("Handling request");
    "Hello, World!"
}

reqwestを使った外部API呼び出しのトレーシング

reqwestを使用した外部API呼び出しをトレーシングするには、reqwest-tracingクレートを使用します。

1. 依存関係の追加

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
reqwest-middleware = "0.2"
reqwest-tracing = "0.4"
tracing = "0.1"

2. トレーシングミドルウェアの設定

use reqwest_middleware::ClientBuilder;
use reqwest_tracing::TracingMiddleware;

let client = ClientBuilder::new(reqwest::Client::new())
    .with(TracingMiddleware::default())
    .build();

3. APIリクエストの実装

#[tracing::instrument]
async fn call_api() -> Result<(), Box<dyn std::error::Error>> {
    let response = client
        .get("https://api.example.com/data")
        .send()
        .await?;
    
    let data = response.json::<serde_json::Value>().await?;
    tracing::info!("Received data: {:?}", data);
    
    Ok(())
}

リクエストボディのロギング

reqwestのリクエストボディをログ出力するには、カスタムミドルウェアを作成します。

1. 依存関係の追加

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
reqwest-middleware = "0.2"
tracing = "0.1"
serde_json = "1.0"

2. カスタムミドルウェアの実装

use reqwest_middleware::{Middleware, Next, Request, Response};
use std::future::Future;
use std::pin::Pin;
use tracing::info;

pub struct LoggingMiddleware;

impl Middleware for LoggingMiddleware {
    fn handle<'life0, 'life1, 'life2, 'async_trait>(
        &'life0 self,
        req: Request,
        extensions: &'life1 mut task_local_extensions::Extensions,
        next: Next<'life2>,
    ) -> Pin<Box<dyn Future<Output = reqwest_middleware::Result<Response>> + Send + 'async_trait>>
    where
        'life0: 'async_trait,
        'life1: 'async_trait,
        'life2: 'async_trait,
    {
        Box::pin(async move {
            // リクエストボディをログ出力
            if let Some(body) = req.body() {
                if let Ok(body_str) = String::from_utf8(body.as_bytes().unwrap_or(&[]).to_vec()) {
                    info!("Request body: {}", body_str);
                }
            }

            // JSONボディの場合
            if let Some(json) = req.json() {
                if let Ok(json_str) = serde_json::to_string_pretty(json) {
                    info!("Request JSON body: {}", json_str);
                }
            }

            let response = next.run(req, extensions).await?;

            // レスポンスボディをログ出力(必要に応じて)
            let body = response.text().await?;
            info!("Response body: {}", body);

            Ok(response)
        })
    }
}

3. ミドルウェアの適用

use reqwest_middleware::ClientBuilder;

let client = ClientBuilder::new(reqwest::Client::new())
    .with(LoggingMiddleware)
    .build();

まとめ

この記事では、Rustでaxumとreqwestを使用する際のロギング実装について説明しました。トレーシングの初期化、ミドルウェアの使用、外部API呼び出しのトレーシング、そしてリクエストボディのロギングまで、段階的に実装方法を紹介しました。

これらの技術を適切に組み合わせることで、アプリケーションの動作を詳細に把握し、効果的なデバッグや性能分析が可能になります。ただし、セキュリティとプライバシーの観点から、機密情報や個人情報を含むデータのログ出力には十分注意する必要があります。また、大量のリクエストを処理する場合は、ロギングがアプリケーションのパフォーマンスに与える影響も考慮してください。

最後に

Discussion