❔

tracingで?演算子の位置をログに出す【hooqマクロの紹介・使い所2選】

に公開

hooq属性マクロを作りました

2025年4月ごろから8カ月ほどかけて属性マクロクレートhooqを作ってきました!

https://github.com/anotherhollow1125/hooq

そしてせっかく作ったhooqクレートの宣伝をすべくhooqアドベントカレンダーに25本の記事を書きました。

https://qiita.com/advent-calendar/2025/hooq

カレンダー記事執筆を通してRustのエラーハンドリング・ロギングにおけるhooqの立ち位置・強みを整理できたので、本記事では特にhooqマクロの強みを簡単に紹介したいと思います!

hooqマクロの概要

hooqは「 ? 演算子や return 、末尾式に、メソッドを挿入(フック)するマクロ」です!機能自体はシンプルだと思います。

フックするメソッドはユーザーで指定が可能です。以下は .map(|v| v * 2) を式と ? の間に挿入している例です。

Rust
use hooq::hooq;

#[hooq]
#[hooq::method(.map(|v| v * 2))]
fn double(s: &str) -> Result<u32, Box<dyn std::error::Error>> {
    let res = s.parse::<u32>()?;
    Ok(res)
}

fn double_expanded(s: &str) -> Result<u32, Box<dyn std::error::Error>> {
    let res = s.parse::<u32>().map(|v| v * 2)?;
    Ok(res)
}

#[test]
fn test() {
    assert_eq!(double("21").unwrap(), double_expanded("21").unwrap());
}

上記のような例で使うことはほとんどなさそうですが、 エラーロギング用の処理をフックする のにとても便利です。

冒頭例ではフックするメソッドを直接書いていましたがマクロ呼び出しごとに逐一書いていたのでは大変です。そのためフレーバーという仕組みを用意しています。このフレーバーは自分でカスタマイズしたものを用意することも可能です。その他マクロ利用を助ける便利機能や設定機能があるので、ぜひドキュメントをご覧いただきたいです!

https://anotherhollow1125.github.io/hooq/latest/ja/index.html

次節からは先ほど紹介したエラーロギング例の詳細を解説します。

便利な使い所1: tracingで?演算子の位置をログに出す

直書きで ? の前にログを入れる方法を除けば、tracingで楽に ? 演算子の位置を出力する(調べた限り) 唯一の 方法です。

#[tracing::instrument(err)] では この属性マクロ自体がついている行 しか得られません。これは計装付与対象関数が複数の ? を内包している時に若干不利です。

ここを #[hooq::hooq(tracing)] #[tracing::instrument] に変える[1]と、hooqマクロの効果で ? 演算子の箇所にログが挿入され、 ? の位置を得られるようになります!

#[hooq::hooq(tracing)] が挿入するメソッドは(v0.3.1では[2])以下になります。

Rust (一部疑似コード)
.inspect_err(|e| {
    let path = ファイルパス;
    let line = ?演算子がある行;
    let col = ?演算子がある列;
    let expr = 該当式を文字列化したもの;

    ::tracing::error!(
        path,
        line,
        col,
        error = %e,
        expr,
    );
})

以降、上記内容をPoCで示します。

https://github.com/anotherhollow1125/todo_app_tracing

以下に示しているのはよくあるバリデーションコードです。変換エラーが起きた際エラーログを送出するように #[tracing::instrument(err)] を付与しています。

Rust
impl TryFrom<(u32, ToDoItemInput)> for ValidatedToDoItem {
    type Error = ValidationError;

    #[tracing::instrument(err)]
    fn try_from(
        (
            id,
            ToDoItemInput {
                title,
                content,
                started_at,
                deadline_at: deadline,
            },
        ): (u32, ToDoItemInput),
    ) -> Result<Self, Self::Error> {
        let title = NonEmptyString::try_new(title.unwrap_or("untitled".to_string()))?;
        let content = NonEmptyString::try_new(content.unwrap_or("default".to_string()))?;
        let started_at = started_at.unwrap_or_else(Utc::now);

        // deadlineが存在する場合、started_atより後であること
        if let Some(deadline) = deadline
            && deadline <= started_at
        {
            return Err(ValidationError::new(
                "deadline_at must be after started_at.",
            ));
        }

        Ok(ValidatedToDoItem {
            id,
            title,
            content,
            started_at,
            deadline_at: deadline,
        })
    }
}

title と content のバリデーションがほぼ同じです。

Rust
let title = NonEmptyString::try_new(title.unwrap_or("untitled".to_string()))?;
let content = NonEmptyString::try_new(content.unwrap_or("default".to_string()))?;

それぞれでエラーになるように実行してみます。


title error with code.line.number = 34


content error with code.line.number = 34

code.line.number: 34 と表示され、どちらも同じエラーを返すため見分けがつかなくなってしまいました!これは辛いです。

ここで #[tracing::instrument(err)] を #[hooq::hooq(tracing)] #[tracing::instrument] に変えます! (err) 部分は Result::Err の時にイベント送出をしてくれる指定なのですが、それは今回はhooqの方が担うようになるので消しています。

https://github.com/anotherhollow1125/todo_app_tracing_hooq

Rust
impl TryFrom<(u32, ToDoItemInput)> for ValidatedToDoItem {
    type Error = ValidationError;

-    #[tracing::instrument(err)]
+    #[hooq::hooq(tracing)]
+    #[tracing::instrument]
    fn try_from(
        (
            id,
            ToDoItemInput {
                title,
                content,
                started_at,
                deadline_at: deadline,
            },
        ): (u32, ToDoItemInput),
    ) -> Result<Self, Self::Error> {
        let title = NonEmptyString::try_new(title.unwrap_or("untitled".to_string()))?;
        let content = NonEmptyString::try_new(content.unwrap_or("default".to_string()))?;
        let started_at = started_at.unwrap_or_else(Utc::now);

        // deadlineが存在する場合、started_atより後であること
        if let Some(deadline) = deadline
            && deadline <= started_at
        {
            return Err(ValidationError::new(
                "deadline_at must be after started_at.",
            ));
        }

        Ok(ValidatedToDoItem {
            id,
            title,
            content,
            started_at,
            deadline_at: deadline,
        })
    }
}

再び、 title と content それぞれでエラーになるように実行してみます。


title error with line = 47


content error with line = 48

line が 47 と 48 で別々な結果となりました!成功です。

こんな感じで、 #[tracing::instrument] マクロだけでは達成できない「 ? 演算子の位置をログ出力する」という機能を実現しています!

hooqマクロ導入を控える理由があるとすれば「属性マクロであること」だと想定している[3]のですが、この使い方だけはそもそもtracingの属性マクロを利用しているので完全上位互換と言っても過言ではないでしょう[4]!

便利な使い所2: なんちゃってエラートレースを出す

筆者がhooqを作ろうと決意したきっかけのうちの一つは、 anyhow::Context::with_context をすべての ? 演算子の前に挿入する癖があったためでした。というわけで、もちろんそれを代行してくれる手段としてhooqマクロは最適です!

Rust
use hooq::hooq;

#[hooq(anyhow)]
fn func1() -> anyhow::Result<()> {
    Err(anyhow::anyhow!("An error occurred in func1"))
}

#[hooq(anyhow)]
fn func2() -> anyhow::Result<()> {
    func1()?;
    Ok(())
}

#[hooq(anyhow)]
fn main() -> anyhow::Result<()> {
    func2()?;
    Ok(())
}
実行結果
Error: [src/main.rs:16:12]
  16>    func2()?
    |

Caused by:
    0: [src/main.rs:10:12]
         10>    func1()?
           |
    1: [src/main.rs:5:5]
          5>    Err(anyhow::anyhow!("An error occurred in func1"))
           |
    2: An error occurred in func1

ログを出したい関数に #[hooq(anyhow)] を付ければ、こんな感じでなんちゃってエラーログを出せます!

この機能は本当に欲しかったもので、その証拠にhooqのCI等開発で利用しているスクリプトはcargo scriptなのですがそこでhooq自体を利用していたりします。

https://github.com/anotherhollow1125/hooq/blob/main/.github/scripts/sync.rs

https://github.com/anotherhollow1125/hooq/blob/main/mdbook-source-code/new_project.rs

ただ世間一般的には RUST_BACKTRACE=1 でバックトレースを出す方が定石らしく、そのためtracingと異なりこちらは完全上位互換等として売り出すのは無理そうです。

とはいえ、color_eyreしかりmietteしかりエラートレースの"見た目"を推しているクレートをしばしば見かけます。皆さん結構見た目を気にするみたいですね。

しかしそうなると、明らかに hooqの出力するトレースはバックトレースより見やすい ためエラーロギングクレートの末席に加えてくれても良いだろうというのが筆者の今の気持ちだったりします。

まとめ・所感

以上、筆者的に「ここは絶対にhooqマクロの強みだ!」と考える使い方2選を紹介しました!他にも思いついている使い方がなくはないので、それもいずれ形にできたらなと思います。

hooqが気になった方はぜひ使ってみて、そして良かったらスターをいただけると大変励みになります🙇‍♂️🙇‍♂️🙇‍♂️

https://github.com/anotherhollow1125/hooq

ここまで読んでいただきありがとうございました!

脚注
  1. ここでは改行を省略しています ↩︎

  2. Error レベルしか送出できないのは不便だろうと考えていて改修予定範囲になります。改修後は event! をフックするようにします。 ↩︎

  3. hooq、個人的には自信作と思っているのですがX上だとあんまり反応がパッとせず、それで使われない理由を色々考えてしまっていたりします。まぁ実際使われない理由は色々思い付きはするかも... ↩︎

  4. 2つ属性マクロを書かなければならないのはちょっと煩雑かもしれないので、hooqマクロのコア部分を他のマクロも利用できるようにし、tracingにそれを内部的に使うPRでも出そうかなという野望があったりします ↩︎

Discussion