Logo

dev-resources.site

for different kinds of informations.

Rust(Rocket)でtracingを使った詳細なエラーログ出力とエラーハンドリングの改善をしてみました

Published at
12/6/2023
Categories
rust
rocket
tracing
sqlx
Author
edo1z
Categories
4 categories in total
rust
open
rocket
open
tracing
open
sqlx
open
Author
5 person written this
edo1z
open
Rust(Rocket)でtracingを使った詳細なエラーログ出力とエラーハンドリングの改善をしてみました

RustのWebフレームワークのRocketで作っているWebアプリで、tracingを使った詳細なエラーログ出力とエラーハンドリングの改善をしてみました。

前提

クリーンアーキテクチャっぽい構成を想定しています。controller, use_case, repositoryがあります。repositoryではsqlxでPostgreSQLを使います。

リポジトリ

リポジトリは下記です。
https://github.com/edo1z/rust-rocket-sqlx-sample

tracingを使った詳細なエラーログ出力

エラー発生箇所のファイルパス、行番号、エラー発生までの経緯(どのファイルのどの関数を辿ったのか)などを、出力するために、tracingクレートを利用しました。

ログ出力例

結果的にこのようなログが出力されるようになりました。

2023-12-05T07:43:24.644738Z ERROR user_controller/update{id=12}:user_use_case/update{id=12}:user_repo/update{id=12}: rust_rocket_sqlx_sample::repositories::user_repo: [DbRepoError::SqlxError] no rows returned by a query that expected to return at least one row (src/repositories/user_repo.rs:85)
Enter fullscreen mode Exit fullscreen mode

user_controllerupdate関数(引数idが12)から、user_use_caseupdate関数(引数が12)に行き、そこから、user_repoupdate関数(引数が12)に行って、そこでエラーが発生しているということが分かりまっす。そして、エラー発生箇所は、user_repo.rsの85行目であり、エラーはDbRepoError::SqlxErrorであり、データがなかったことが分かります。分かりやすくなりました!

コードサンプル

repositoryの関数はこんな感じになっております。

#[instrument(name = "user_repo/update", skip_all, fields(id = %id))]
async fn update(
    &self,
    con: &mut PgConnection,
    id: i32,
    name: &String,
) -> Result<User, DbRepoError> {
    query_as!(
        User,
        "UPDATE users SET name = $1 WHERE id = $2 RETURNING *",
        name,
        id
    )
    .fetch_one(&mut *con)
    .await
    .map_err(|e| log_into!(e, DbRepoError))
}
Enter fullscreen mode Exit fullscreen mode

instrument属性

最初の行のinstrument属性は、tracingの機能で、これを書いておくと、エラーログ出力時に、ログ発生の経緯にこの関数のnameや、引数が出力されます。controller, use_case, repositoryに全部書いておくと、全部順番に出力されるので、一目でログ出力時の実行順序が分かります。ただ、ログに引数の値を出力させるというのは、かなり危険な香りがしますので、基本的にはskip_allを設定して、表示させても問題なく、且つ、表示させたいものは、個別にfieldsで設定するという方針にしています。これは、危ないから使わないという選択肢もあるのかもなーとは思いました。どうもデフォルトでOFFには簡単にはできなそうだったのですが、もしデフォルトOFFができると、より安心かなあと思っております。

log_into!マクロ

最後の行の、log_into!マクロは下記になっております。

#[macro_export]
macro_rules! log_into {
    ($error:expr, $target_type:ty) => {{
        let converted_error: $target_type = $error.into();
        tracing::error!("{} ({}:{})", converted_error, file!(), line!());
        converted_error
    }};
}
Enter fullscreen mode Exit fullscreen mode

エラーをtarget_typeの別のエラーに変換して、そのエラーをファイルパスと行番号と共に、tracingのerror!を使ってログ出力するマクロになります。ただの関数ですと、ファイルパス・行番号はこの関数のファイルパス及び行番号になりますので、それもあって、マクロにしております。もしファイルパス・行番号が不要であれば、自動でエラーは変換できるので、このようなマクロを使う必要はありません。

エラーハンドリングの改善

use_caseでrepository等で発生したエラーの詳細を把握し、細かくエラーハンドリングできるようにしつつ、なるべくシンプルにしたいと考えていまして、現状としては、結構いい感じな気がしてます。

ポイント① DbRepoErrorでラップ

DbRepoErrorは下記です。thiserrorクレートという便利ツールを使うことで、記述量が激減しました。fromアトリビュートを設定することで、sqlx::ErrorDbRepoError::SqlxErrorに自動変換されます。これで、各エラーの詳細を残しつつ、DbRepoErrorとしてuse_caseに渡せるようになりました。

use thiserror::Error;

#[derive(Debug, Error)]
pub enum DbRepoError {
    #[error("[DbRepoError::SerdeError] {0}")]
    SerdeError(#[from] serde_json::Error),
    #[error("[DbRepoError::SqlxError] {0}")]
    SqlxError(#[from] sqlx::Error),

    #[cfg(test)]
    #[error("Dummy error for testing")]
    DummyTestError,
}
Enter fullscreen mode Exit fullscreen mode

ポイント② use_caseで詳細をハンドリングしつつAppErrorに自動変換

AppErrorは現在こちらのようになっております。DbRepoErrorが自動的にAppError::DbErrorに変換されるようになっています。また、RocketのResponderが実装されており、レスポンスにそのままAppErrorが使えるようになっております。そして、レスポンス時のステータスコードも紐づけされています。その他、anyhowを真似したapp_err関数と、app_err!, app_err_bail!, app_err_ensure!マクロを作成しました。

AppErrorのコード(一部)

#[derive(Debug, Error)]
pub enum AppError {
    #[error("Database Error")]
    DbError(#[from] DbRepoError),
    #[error("Bad Request")]
    BadRequest,
    #[error("Unauthorized")]
    Unauthorized,
    #[error("Forbidden")]
    Forbidden,
    #[error("Not Found")]
    NotFound,
    #[error("Internal Server Error")]
    InternalServerError,
    #[error("{message}")]
    CustomError { status_code: u16, message: String },
}

impl AppError {
    pub fn new(status_code: u16, message: &str) -> Self {
        AppError::CustomError {
            status_code,
            message: message.to_string(),
        }
    }

    pub fn status_code(&self) -> u16 {
        match self {
            AppError::DbError(_) => 500,
            AppError::BadRequest => 400,
            AppError::Unauthorized => 401,
            AppError::Forbidden => 403,
            AppError::NotFound => 404,
            AppError::InternalServerError => 500,
            AppError::CustomError { status_code, .. } => *status_code,
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

use_caseのコードサンプル

下記のdeleteは、fromを使って自動変換させています。updateでは、対象データが存在しない場合は、BadRequestエラーを返すようにしています。このように、use_caseで、細かいエラーハンドリングが出来るようになりました。

#[instrument(name = "user_use_case/update", skip_all, fields(id = %id))]
async fn update(
    &self,
    repos: &Repos,
    db_con: &mut DbCon,
    id: i32,
    name: &String,
) -> Result<User, AppError> {
    repos
        .user
        .update(&mut *db_con, id, name)
        .await
        .map_err(|e| {
            if let DbRepoError::SqlxError(sqlx::Error::RowNotFound) = &e {
                AppError::BadRequest
            } else {
                AppError::from(e)
            }
        })
}

#[instrument(name = "user_use_case/delete", skip_all, fields(id = %id))]
async fn delete(&self, repos: &Repos, db_con: &mut DbCon, id: i32) -> Result<(), AppError> {
    repos
        .user
        .delete(&mut *db_con, id)
        .await
        .map_err(|e| AppError::from(e))
}
Enter fullscreen mode Exit fullscreen mode

今後やりたいこと

  • メトリクスの収集
  • Datadogで集計・グラフ表示
  • Datadog経由でSlack通知
  • レスポンスのjson化(今エラーメッセージがテキスト)
tracing Article's
30 articles in total
Favicon
Telemetry and Tracing: A Comprehensive Overview
Favicon
Observability - 6(Distributed Tracing using Jaeger)
Favicon
Trace-based Testing With OpenTelemetry: Using Tracetest with OpenTelemetry
Favicon
Wednesday Links - Edition 2024-08-21
Favicon
OpenTelemetry Tracing on Spring Boot, Java Agent vs. Micrometer Tracing
Favicon
The best way to debug slow web pages
Favicon
How to Track USDT TRC20 Transactions
Favicon
Introduction to Distributed Tracing With OpenTelemetry in .NET
Favicon
Enabling distributed tracing for containerized apps with AWS X-Ray
Favicon
Rust: Actix-web and Daily Logging
Favicon
Unlocking the Power of Distributed Tracing: Navigating the Digital Cosmos🌌🔍✨
Favicon
Microservice observability by OpenTelemetry!
Favicon
[TechStory]: How to add distributed tracing using Jaeger and OpenTelemetry into a Golang application
Favicon
Rust(Rocket)でtracingを使った詳細なエラーログ出力とエラーハンドリングの改善をしてみました
Favicon
Exploring Jaeger - Unveiling the Power of Open-Source End-to-End Distributed Tracing
Favicon
Monitoring and Testing Cloud Native APIs with Grafana
Favicon
Tracing Node.js Microservices with OpenTelemetry
Favicon
Set Up Tracing for a Node.js Application on AppSignal
Favicon
Step-by-Step Guide to Adding Logging to an Actix Web Application
Favicon
AWS Lambda Cookbook — Part 2 — AWS Lambda Observability Best Practices
Favicon
How to Build a Kafka Producer in Rust with Partitioning
Favicon
Log it Like You Mean It: The Top 12 Logging, Tracing, Monitoring & Observability Platforms for Your Success
Favicon
Monitoring, Tracing, and Observability: Get the Inside Scoop on Your System with These Tips!
Favicon
Serverless Spy Vs. Spy Chapter 2: AWS Distro for OpenTelemetry Lambda vs X-Ray SDK
Favicon
How to Add Sentry Integration to your NodeJS App
Favicon
Learning Go by examples: part 10 - Instrument your Go app with OpenTelemetry and send traces to Jaeger - Distributed Tracing
Favicon
End-to-end tracing with OpenTelemetry
Favicon
Honeycomb.io Review
Favicon
Sentry - Tracing
Favicon
Observability: the basics

Featured ones: