Open14

Rust入門日記 - Webアプリケーション編

ピン留めされたアイテム
ooharabucyouooharabucyou

https://zenn.dev/kawahara/scraps/5a22db01d86ec9
https://zenn.dev/kawahara/scraps/5482ab07ffb39e

の話題の続き。
ここでは、Web API を実装することを考える。

Rust には複数のWeb Framework があるので、どれか1つを選んでWebAPIを開発する。
手っ取り早く、今回はRESTなAPIを考える。

以下のような流れで考えていく。

  • https://github.com/flosse/rust-web-framework-comparison を確認
  • マルチスレッド
  • tokio による非同期処理の実装
  • パッケージとクレート
  • とりあえずの実装
  • データベースからの情報取得・書き出し
  • 認証フィルタ
  • クリーンアーキテクチャを適用する
ooharabucyouooharabucyou

Webフレームワークいろいろ

https://github.com/flosse/rust-web-framework-comparison
を見ると、Rust にはWeb用のフレームワークが複数存在するようだ。

WebAPIを作る前提であれば High-Level Server Frameworks の部分をとりあえず見れば良い。
自分が参考にしている書籍『実践Rustプログラミング入門』でも紹介されている、actix-web を始めとして、高機能な Rocket、パフォーマンスが良くシンプルなwarpなどがある。

今回は書籍に習って、actix-web をとりあえず触ってみる。

ooharabucyouooharabucyou

マルチスレッド

Webフレームワーク上を利用してHTTPサーバを動作させるときに、シングルスレッドでやっても良いが、CPUの数に合わせ複数のスレッドで立てることがある。
いきなり actix-web の チュートリアル から始めてもいいが、後々困らないようにするために、スレッドについて先に学ぶ。

スレッドを作って動かす

まずは基本。標準ライブラリの thread を use する。thread::spawn にクロージャー (関数オブジェクトの一種。PHPだと無名関数だったり、Rubyだったらブロックを渡したりなやつ。) を渡して、それを新しいスレッド実行するというもの。

use std::thread;

fn main() {
    thread::spawn(|| {
        println!("Hello, from thread.");
    });
}

が、上記は一見動きそうだが、何も表示されない場合がある。

$ cargo run
$ cargo run
Hello, from thread.
$ cargo run
$ cargo run

これは、スレッドの中の処理が実行完了するより前に、main() の処理が完了した状態になってしまったため、出力がされない場合があるという状態になっている。
そこで、spawn() により渡される値である、JoinHandle<T> (T は、クロージャー自体の返り値) のメソッド join() を利用して、スレッドの処理が完了するまで待つことができる。

join()Result<T> を返すので、スレッドで行われた処理結果を受け取るといったこともできる。
今回は、unwrap() により、エラーが起きたときは、panic! が起きるようにしてしまっている。

use std::thread;
use std::thread::JoinHandle;

fn main() {
    let handle:JoinHandle<()> = thread::spawn(|| {
        println!("Hello, from thread.");
    });

    handle.join().unwrap();
}

これにより、確実にメッセージが出力されるようになった。

$ cargo run
Hello, from thread.
$ cargo run
Hello, from thread.
$ cargo run
Hello, from thread.
ooharabucyouooharabucyou

複数スレッド起動実験

10スレッド起動して、すべて join してみるテスト。
クロージャーと変数の取り扱いで注意点あり。

use std::thread;
use std::thread::JoinHandle;

fn main() {
    let mut handles:Vec<JoinHandle<()>> = Vec::new();
    for i in 1..=10 {
        let test = String::from("test");
        // i や test など、このブロックの中で使える変数を使う場合は、
        // move キーワードを書いて、各種変数をムーブする。
        // 書かない場合はライフサイクルが終わってしまう後に、iが参照されるのを防ぐために
        // コンパイラがエラーを起こす。
        handles.push(thread::spawn(move || {
            println!("Hello, from thread. {}, {}", i, test);
        }));

        // ムーブ済みな、変数test をここで使おうとするとエラーとなる。
    }

    for handle in handles {
        handle.join().unwrap();
    }
}

何度が実行してみると、spawn() した順番と、実際にスレッドの中の処理が実行される順序はバラバラである。

$ cargo run
Hello, from thread. 7, test
Hello, from thread. 10, test
Hello, from thread. 5, test
Hello, from thread. 6, test
Hello, from thread. 1, test
Hello, from thread. 3, test
Hello, from thread. 8, test
Hello, from thread. 2, test
Hello, from thread. 9, test
Hello, from thread. 4, test
ooharabucyouooharabucyou

スレッドセーフ

スレッドセーフとは、複数のスレッドで同じリソースを取り扱ったとしても、正しい結果が得られる状態のことを指す。
例えば、共通で使う数値カウンタがあって、それを10個のスレッドで1つづカウントアップしていく実装をしたときに本来であれば、数値カウンタは10になっていてほしい。しかし、実装が適切でないとカウンタの値について古い数値を参照してカウントアップするという状況になるため、10にならない場合がある。

Javaなどでのプログラミングの記憶が蘇ってきた。例えば、List を取り扱うとき、複数のスレッドから同一の ArrayList と取り扱うと不整合が起きる可能性があるとドキュメントに書いてある。
これを防ぐために、Javaでは、synchronized ブロックを用意したり、Collections.synchronizedList() を使ったりと面倒なことをやる必要があった。

https://docs.oracle.com/javase/jp/8/docs/api/java/util/ArrayList.html

この実装はsynchronizedされません。

話を Rust に戻すと、Rust でも同じくスレッドセーフを意識する必要がある。

共有メモリとムーブ問題

複数スレッドで共有の変を参照したいとなったときに、ムーブされてしまったものを再度ブームできないという Rust の所有権ルールが問題になることがある。
スレッド実験のコードで move キーワードによりクロージャーに変数をムーブしている挙動を見るに、共有の変数をムーブするといろいろ困ることが起きてしまう。

そこで今回のように複数から参照される可能性のある値を共通で使いたい場合は、Arc で包む。
Arc は参照カウンタを実装しているポインタの一種で、しかもスレッドセーフな機能となっている。

use std::thread;
use std::thread::JoinHandle;
use std::sync::Arc;

fn main() {
    // shared は Vec<i32> のポインタ
    let shared: Arc<Vec<i32>> = Arc::new(vec![1, 2, 3]);
    let mut handles: Vec<JoinHandle<()>> = Vec::new();
    for i in 1..=10 {
        // ポインタをコピーする。(実体がコピーされるわけではない)
        // move ではこのブロックの中で使われるものしか move されないので
        // ここで参照を作っておく
        let s_ref:Arc<Vec<i32>> = shared.clone();
        handles.push(thread::spawn(move || {
            // Arc<Vec<i32>> は普通に Vec<i32> のように使える
            println!("Hello, from thread. {}, {:?}", i, s_ref);
        }));
    }

    for handle in handles {
        handle.join().unwrap();
    }
}

仮に、Arc の部分を、スレッドセーフでない実装として用意されている、参照ポインタである std::rc::Rc を使おうとすると、コンパイルエラーとなる。安心!

編集が必要な場合は、さらに Mutex で包んだ上で、Arc で包み、参照の管理を行う。
Mutex は複数のスレッドでロックを行うために必要な機能を提供している。
lock() により、他のスレッドで参照されるのを防ぐことができる。(ほかのスレッドでエラーが起きると、lock() 時にエラーとなるので、Result が返ってくる。ここでは unwrap() により結果だけを取り出し、エラー時は panic! とする。
また、* による参照外しを行い、編集をする。* は、&型, ArcRc, MutexGuard のような参照を表しているものから、中に入っているもの見に行くために使う演算子となっている。*c は、編集可能な u32 となる。

use std::thread;
use std::thread::JoinHandle;
use std::sync::{Arc, Mutex, MutexGuard};

fn main() {
    let shared_counter: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
    let mut handles: Vec<JoinHandle<()>> = Vec::new();
    for i in 1..=10 {
        let shared_counter_ref:Arc<Mutex<u32>> = shared_counter.clone();
        handles.push(thread::spawn(move || {
            let mut c:MutexGuard<u32> = shared_counter_ref.lock().unwrap();
            *c += 1;
            println!("Hello, from thread. {}, {}", i, c);
        }));
    }

    for handle in handles {
        handle.join().unwrap();
    }

    // 10
    println!("{}", *shared_counter.lock().unwrap());
}

上記のコードは、shared_counter の値が必ず10になることが保障されるが、当然ながら利用するときにスレッドがロックされる。パフォーマンスのこともあるので、可能な限り共有リソースへの書き出しを伴うアクションを避けておきたい。

この辺は大変奥深いので、The Rust Programming Language の15章〜16章をじっくり読んでおきたい。

https://doc.rust-jp.rs/book-ja/ch15-00-smart-pointers.html

https://doc.rust-jp.rs/book-ja/ch16-00-concurrency.html

ooharabucyouooharabucyou

非同期の世界

Webアプリケーションを作るにあたって考える必要があるもうひとつ重要そうな要素が「非同期プログラミング」だ。
JavaScript だと、古典的なものだと callback 関数を使い、最近だと Promise, async, await などを使って、プログラミングしていく。

https://azu.github.io/promises-book/

これが Rust だとどうなっているのかを見ていく。

async な関数って何者?

Rust には構文として、async, await が用意されており、async な関数は、JavaScript の asyncPromise を返すように、Rust では Future トレイトが実装された値を返す。(値は後でわかるわー。みたいなノリ)

IntelliJ で実際にコードを見てみると、型が見える。以下のように、関数 async hoge の返り値は、() ではなく、impl Future<Output=()> になっているのが見える。

「トレイトの実装」を返しているが、何が実装されているかはわからないという「謎」な物体を async 関数は返しているのだ!謎!
この Future トレイト自体が何を実装するべきか? というのは、ドキュメントを見てみる。

https://doc.rust-lang.org/std/future/trait.Future.html

  • type Output: 出力をどうするべきか? 関数の返り値。
  • pub fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>
    • Poll: できるところまで進めて、実行状態を返すもの。(JavaScript の Promiseでいうと、PendingFulfilledRejected などという状態で管理しているもの。)

Rust の Poll は以下のような列挙体となっている。

pub enum Poll<T> {
    Ready(T),
    Pending,
}

Ready が実際に返り値が使えるようになった状態を表していて、Pending はまだ作業中の状態を表している。Rustは、エラーハンドリングについては、Result を使うので、JavaScript の Promise のように、Rejected な状態はないということで理解した。

Pin, Context が何者なのか? というのはまだ理解できなかった。

https://tech-blog.optim.co.jp/entry/2019/07/05/173000

https://tech-blog.optim.co.jp/entry/2019/11/08/163000

https://tech-blog.optim.co.jp/entry/2020/03/05/160000

この、抽象的なFuture型のモノを、スレッド等を駆使してうまくコントロールしてくれるものは Rust 本体には実装されていないので、「ランタイム」を用意する必要があるらしい。

代表的なランタイム

ランタイムを自分で実装することも可能なようだ。学習には良さそうだが、実際の利用には適さない部分が出てきそうではある。

https://qiita.com/legokichi/items/1beb3dce317ef45a927b

そこで、以下のようなランタイムが開発されている。

futures

https://github.com/rust-lang/futures-rs

ロゴがかっこいい。事実上の標準ライブラリ的な扱いらしい。

tokio

https://github.com/tokio-rs/tokio

名前の由来は Tokyo から来ているらしい。例のアイドル系農家とは違う。

https://www.reddit.com/r/rust/comments/d3ld9z/how_tokio_crate_got_its_name_like_that/

I enjoyed visiting Tokio (Tokyo) the city and I liked the "io" suffix and how it plays w/ Mio as well. I don't know... naming is hard so I didn't spend too much time thinking about it.

非常によく使われるライブラリ。
actix-web についても、こちらが使われているらしい。

async-std

https://github.com/async-rs/async-std

比較的最近作られたライブラリらしい。

ooharabucyouooharabucyou

Tokio を使ってみる

Cargo.toml へのクレート追加

Cargo.toml へ、tokio への依存を追加する。

[dependencies]
tokio = { version = "1.3.0", features = ["full"]}

はじめての Tokio

ということで、書いてみたのがこちらとなる。実行すると、5秒ほどまたされて、10+20 の答えが返される。
諸々新しい要素が出てくるので、見ていく。

main.rs
use tokio::time::{sleep, Duration};

async fn add(left: i32, right: i32) -> i32 {
    sleep(Duration::from_millis(5000)).await;
    return left + right;
}

#[tokio::main]
async fn main() {
    let ans = add(10, 20).await;
    println!("{}", ans);
}
  • #[tokio::main]: #[xxx] はアトリビュートと呼ばれるもの。関数などに意味づけを行うために利用するもの。Javaだとアノテーション、Python だとデコレーター、PHPだと8から同じ名前でアトリビュートが同じような役割として存在する。記法としてはPHPのものに近い。
    • Tokio はこのアトリビュートがついているものをタスクとして実行しようとする。
  • .await: async 関数の実行について、処理を止めて待つということを表すためのキーワード (Futureのメソッドなどではない。) これを書くことで、add(i32, i32).awaitimpl Future<Output=i32> ではなく、i32 を返してくれる。
  • tokio::time::{sleep, Duration}: tokio にある機能で一定時間処理を止めるときなどに利用するもの。上記のコードでは5000ms止めるという非同期な関数を呼んでいて、それを await することで次の処理に進むのを待った状態にしている。
ooharabucyouooharabucyou

モジュール

これから、Webアプリを作るにあたって、コード量が多くなってくるのにも関わらず、すべて main.rb に書くのは厳しい。Java でいうパッケージ・Ruby でいうモジュール的な仕組みがほしい。

もちろん、Rust にも方法が用意されている。

まずは1つのファイルでモジュールを使う

main.rs
mod my_app {
    pub fn do_something() {
        println!("hello, world");
    }
}

fn main() {
    my_app::do_something()
}
  • mod によりモジュールを定義する。上記では、my_app という名前にしている。
  • モジュールの中にある関数、構造体にモジュールの外 (fn main() はモジュールの外) からアクセスできるようにする場合は、pub をアクセス修飾子として記載する必要がある。書かない場合は、同じモジュールからしかアクセスできない。
    • 継承はないので、Java にあるような protected は存在しない。pub か 書かないかの2つかない。シンプル!

モジュールの中にモジュールを書くこともできるが、こちらも関数と同じく、デフォルトはモジュール内でしかアクセスできないモジュールとなる。

main.rs
mod my_app {
    // mod の中に mod を作ることができる
    // この mod は、外から呼べない (my_app mod 内からしか呼べない)
    mod my_child1 {
        // この関数は外から呼べる
        pub fn do_something () {
            do_something_internal();
        }

        // この関数は my_child1 内でしか呼べない
        fn do_something_internal() {
            println!("hello, world from my_child1");
        }
    }

    mod my_child2 {
        // この関数は外から呼べる
        pub fn do_something() {
            println!("hello, world from my_child2");
        }
    }

    mod my_child3 {
        // この関数は外から呼べる
        pub fn do_something () {
            // 親モジュール(my_app)にある、my_child1 にある do_something() を呼び出す
            super::my_child1::do_something();
        }
    }

    // この mod は外から呼べる
    pub mod my_child4 {
        // この関数は外から呼べる
        pub fn do_something () {
            println!("hello, world from my_child4");
        }
    }

    pub fn do_something() {
        println!("hello, world");
        my_child1::do_something();
        my_child2::do_something();
        my_child3::do_something();
    }
}

fn main() {
    my_app::do_something();
    my_app::my_child4::do_something();

    // my_app::my_child1 は公開されていないので、以下をコメントアウトすると
    // コンパイルエラーになる。
    // my_app::my_child1::do_something();
}

これだと、main.rs が長くなる問題を対処できないのでファイル分割を考える。

ooharabucyouooharabucyou

モジュールについてファイルを分ける

mod の中身は、モジュール名の同名のファイルを作成することで分割することができる。
先程つくった main.rs を分割してみる。

main.rs
mod my_app;

fn main() {
    my_app::do_something();
    my_app::my_child4::do_something();
}

my_app.rs は、 mod my_app の中身を表す。

my_app.rs
mod my_child1;
mod my_child2;
mod my_child3;
pub mod my_child4;

pub fn do_something() {
    println!("hello, world");
    my_child1::do_something();
    my_child2::do_something();
    my_child3::do_something();
}

さらに、my_appモジュール内で定義している、my_child1モジュールについては、my_app というフォルダを作り、その中に my_child1.rs を作ることで中身を表すことができる。

my_child1.rs
// この関数は外から呼べる
pub fn do_something () {
    do_something_internal();
}

// この関数は my_child1 内でしか呼べない
fn do_something_internal() {
    println!("hello, world from my_child1");
}

同様に、my_child2.rs, my_child3.rs, my_child4.rs も作っていくことで、ファイルを分割することができる。

ooharabucyouooharabucyou

use キーワード

今まで、use はライブラリを利用するときなどは利用してきたが、モジュール内の何かを、同じスコープで利用するときに使うものであるということが理解できる。

main.rb
mod my_app {
    pub mod child {
        pub mod grand_child {
            pub fn do_something () {
            }
        }
    }
}

fn main() {
    // これは長いので書くのが辛い
    my_app::child::grand_child::do_something();
}

use を使うと以下のようになる。

main.rb
mod my_app {
    pub mod child {
        pub mod grand_child {
            pub fn do_something () {
            }
        }
    }
}

use my_app::child::grand_child::do_something;

fn main() {
    // grand_child 内の、do_something が展開されるので、使える!
    do_something();
}

use キーワードは、以下のように使うこともできる。

// 2つの要素を use する
use my_app::child::grand_child::{do_something1, do_something2}
// これは、以下と同じ意味である
// use my_app::child::grand_child::do_something1;
// use my_app::child::grand_child::do_something2;
// grand_child 内の中身をすべて展開する。
// 仮に同名の関数などがあったばあいエラーとなるので注意
use my_app::child::grand_child::*
// 同名の関数などを use しようとする場合は、as を利用して名前を変えることで
// 衝突を回避することができる
use my_app::child::do_something as child_do_something;
use my_app::child::grand_child::do_something as grand_child_do_something; 
ooharabucyouooharabucyou

クレート内にクレートを作る

Rust プログラムの単位についていろいろ出てきたので整理する。
以下のような関係になっている。 (cargoを使う場合)

  • ルートクレート: 今まで main.rb を置いていたコンパイルの始点。Cargo によって管理する。
  • クレート: コードを管理する単位。cargo によって管理され、binクレート (実行するファイルを作るもの) と、libクレート (ほかのコードから参照するライブラリ) に分かれる。Cargo.toml[dependencies] に記載することにより、 https://crates.io/ に登録されているクレートを入手して、自分のクレートで使うことができる。 crates.io に自分のクレートを登録することもできる。
  • モジュール: プログラムの中で、プログラムをまとめる単位。

クレートに関しては、クレートの中にクレートを作ることもできる。Cargoのワークスペースという機能を利用して、ルートクレートに、libクレートを作って機能を分割することを考える。

ルートクレートで以下を実行する

$ cargo new infra --lib
     Created library `infra` package

infra という名前のクレートが作成される。ルートクレート側に、このクレートをメンバーとして登録するために、cargo.toml に以下を追加する。

Cargo.toml
[workspace]
members = ["infra"]

infra クレートでは、src/main.rs ではなく、src/lib.rs が作成されている。
以下のように、コードを書き換える。

infra/src/lib.rs
pub fn hello_from_infra() {
    println!("hello");
}

ルートクレート側で、infra クレートの機能を使いたいので依存を貼る。

Cargo.toml
[dependencies]
infra = { path = "infra"}

これで、ルートクレート側の src/main.rsinfra の中にある公開された要素を使うことができる。

src/main.rs
use infra::hello_from_infra;

fn main () {
    hello_from_infra();
}
ooharabucyouooharabucyou

復習

基本的なRustプログラミングについて学んだ
https://zenn.dev/kawahara/scraps/5a22db01d86ec9

また、このスクラップではマルチスレッドプログラミングや、非同期関数、モジュールやクレートによる機能の分割についても考えてきた。
ということで、ようやく actix-web によるWebアプリケーションを作っていく。

actix-web 最初の一歩

まずはインストールする。例によって Cargo.toml に以下を追加する。

Cargo.toml
[dependencies]
actix-web = "3"

そして、main.rs にHTTPサーバを起動するためのコードを記載していく。

src/main.rs
use actix_web::{get, App, HttpServer, Responder};

#[get("/")]
async fn index() -> impl Responder {
    // 文字列をそのまま返す
    "Hello World"
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // HTTPサーバを8080で起動
    HttpServer::new(|| App::new().service(index))
        .bind("127.0.0.1:8080")?
        .run()
        .await
}

ログも特に何もしてくれないけど、かんたんなWebアプリサーバができた。

  • HttpServer::new でWebサーバを作る。new にはアプリケーションの設定を返すクロージャを渡す
  • run() が非同期関数のため、main() も非同期関数とする。
  • サービスとして登録している、index() 関数には、HTTP の Path を表すアトリビュートを記載する。
  • index() 関数は非同期関数とする。そうしないとエラーとなる。
  • index() 関数の返り値は、Responder トレイとの実装となる。今回は、文字列 &str を返していて、一見ダメなんじゃないか? と思いきや、actic-web のほうで、文字列についての実装を行っているので、問題ない。 https://github.com/actix/actix-web/blob/master/src/responder.rs#L106
    • この実装を見るに、String::from("Hello World") としても行ける。String に対しても、Responderトレイトの実装が行われており、内部的には HttpResponse 構造体に変換されて、処理される。

以下でサーバを起動する。

$ cargo run

別のシェルでHTTP Request を試す。

$ curl --dump-header - http://localhost:8080/
HTTP/1.1 200 OK
content-length: 11
content-type: text/plain; charset=utf-8
date: Mon, 15 Mar 2021 12:47:50 GMT

Hello World

存在しないパスにアクセスすると、404は返してくれるが、特に挙動を指定していない場合はBodyはからになるようだ。

$ curl --dump-header - http://localhost:8080/hoge
HTTP/1.1 404 Not Found
content-length: 0
date: Mon, 15 Mar 2021 12:48:09 GMT

ooharabucyouooharabucyou

auto reload みたいな仕組みはあるか?

node.js で express などを利用して Webアプリケーションを開発する場合 node-devnodemon のように、ファイルの変更に応じて自動的に再度プロセスを作り直す方法があった。
rust にも、同様の仕組みが存在する。

cagro-watch のインストール

cargo の拡張として、cargo watch をインストールする。

$ cargo install cargo-watch

開発時

$ cargo watch -x run

これで、コード変更時に毎回 cargo run をし直さなくてもよくなった。

ドキュメント参照のこと

https://actix.rs/docs/autoreload/

ooharabucyouooharabucyou

ログを残したい

最初の状態だと、ログが出てくれないので、なんだか動いているのか動いていないのか不安になってしまう。Webアプリケーションの本質的な部分とは若干はずれるところはあるが、開発時の問題解決の手がかりとしても、ログくらいは見えるようにしておきたい。

actix-web には、ミドルウェアという仕組みがあって、ロジックを実行する前後でなにかの処理を行うことができる。
ログに関するミドルウェアは actix-web 標準で用意されているため、素早くログに関する設定を行うことができる。

Rustのログの仕組み

多くのログに関する処理は、log と呼ばれるクレートを利用しているようだ。
rust-lang が配っているのを見るに、事実上の標準ライブラリのようで、これはログを行うための標準的なインターフェイスを提供している。

https://github.com/rust-lang/log

使い方を学んでみる。
適当なクレートを作り、Cargo.toml には、log = "0.4.0" を依存として追加する。

main.rs には、以下のようなコードを記述する。

src/main.rs
use log::{trace, debug, info, warn, error};

fn main () {
    let i = 100;
    trace!("trace {}", i);
    debug!("debug {}", i);
    info!("hello {}", i);
    warn!("warn {}", i);
    error!("error {}", i);
}

println! マクロのように、変数を {} によって置き換えることができる。
ログの出力をどうするか自体は、log クレートでは用意していないため、実行しても何もでない。

ログの出力については、シンプルな実装だと env_logger がある。

https://github.com/env-logger-rs/env_logger

これは、環境変数 RUST_LOG の値によって、出力の仕方を制御できるシンプルなライブラリとなっている。

Cargo.toml に env_logger = "^0.8.3" の依存を追加した上で、src/main.rs を以下のように変えて、環境変数 RUST_LOG をつけて実行してみる。

src/main.rs
use log::{trace, debug, info, warn, error};

fn main () {
    env_logger::init();
    let i = 100;
    trace!("trace {}", i);
    debug!("debug {}", i);
    info!("hello {}", i);
    warn!("warn {}", i);
    error!("error {}", i);
}

ログのレベルは trace < debug < info < warn < error という序列になっているようだ。

$ RUST_LOG=info cargo run
[2021-03-22T12:15:58Z INFO  multi] hello 100
[2021-03-22T12:15:58Z WARN  multi] warn 100
[2021-03-22T12:15:58Z ERROR multi] error 100

info を指定することで、標準出力に、info 以上のログを出力することができた。

actix-web での利用

Cargo.toml の編集

今回はログを残すために、env_logger を使いたいので、Cargo.toml を編集する。

Cargo.toml
[dependencies]
actix-web = "3"
+ env_logger = "^0.8.3"

Logger ミドルウェアの利用

src/main.rs
use actix_web::{get, App, HttpServer, Responder};
use actix_web::middleware::Logger;

#[get("/")]
async fn index() -> impl Responder {
    String::from("Hello World")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init();
    // サーバを8080で起動。
    // Logger::default() をミドルウェアとして登録。
    HttpServer::new(|| App::new().wrap(Logger::default()).service(index))
        .bind("127.0.0.1:8080")?
        .run()
        .await
}

アプリケーションの設定時に、wrap() にて、actix_web::middleware::Logger::default() を渡す。
これにより、リクエストを受けて、レスポンスするという処理を挟み込むように、ログの処理が入る。リクエストを受けてから、レスポンスをするまでに経過した時間などをログに残している様子がわかる。

$ RUST_LOG="info" cargo run 
(省略)
[2021-03-22T11:38:48Z INFO  actix_server::builder] Starting 8 workers
[2021-03-22T11:38:48Z INFO  actix_server::builder] Starting "actix-web-service-127.0.0.1:8080" service on 127.0.0.1:8080
[2021-03-22T11:38:57Z INFO  actix_web::middleware::logger] 127.0.0.1:64180 "GET / HTTP/1.1" 200 11 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" 0.000172

ロギキングライブラリいろいろ

env_logger はシンプルで、コンテナ上で動かしログは標準出力で出す形式で問題ないという場合はこれで良さそうだ。だが、ファイル形式での出力をしたり、かつ複雑な設定を行いたい場合は、log4rs クレートなどを利用すると、YAMLにより複雑な構成を行うことができる。
また、syslog クレートなどを利用すると、ログ情報を syslog を利用して転送することができる。

https://github.com/estk/log4rs

https://github.com/Geal/rust-syslog