Rust入門日記 - Webアプリケーション編
の話題の続き。
ここでは、Web API を実装することを考える。
Rust には複数のWeb Framework があるので、どれか1つを選んでWebAPIを開発する。
手っ取り早く、今回はRESTなAPIを考える。
以下のような流れで考えていく。
- https://github.com/flosse/rust-web-framework-comparison を確認
- マルチスレッド
- tokio による非同期処理の実装
- パッケージとクレート
- とりあえずの実装
- データベースからの情報取得・書き出し
- 認証フィルタ
- クリーンアーキテクチャを適用する
Webフレームワークいろいろ
を見ると、Rust にはWeb用のフレームワークが複数存在するようだ。
WebAPIを作る前提であれば High-Level Server Frameworks の部分をとりあえず見れば良い。
自分が参考にしている書籍『実践Rustプログラミング入門』でも紹介されている、actix-web を始めとして、高機能な Rocket、パフォーマンスが良くシンプルなwarpなどがある。
今回は書籍に習って、actix-web をとりあえず触ってみる。
マルチスレッド
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.
複数スレッド起動実験
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
スレッドセーフ
スレッドセーフとは、複数のスレッドで同じリソースを取り扱ったとしても、正しい結果が得られる状態のことを指す。
例えば、共通で使う数値カウンタがあって、それを10個のスレッドで1つづカウントアップしていく実装をしたときに本来であれば、数値カウンタは10になっていてほしい。しかし、実装が適切でないとカウンタの値について古い数値を参照してカウントアップするという状況になるため、10にならない場合がある。
Javaなどでのプログラミングの記憶が蘇ってきた。例えば、List
を取り扱うとき、複数のスレッドから同一の ArrayList
と取り扱うと不整合が起きる可能性があるとドキュメントに書いてある。
これを防ぐために、Javaでは、synchronized
ブロックを用意したり、Collections.synchronizedList()
を使ったりと面倒なことをやる必要があった。
この実装は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!
とする。
また、*
による参照外しを行い、編集をする。*
は、&型
, Arc
や Rc
, 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章をじっくり読んでおきたい。
非同期の世界
Webアプリケーションを作るにあたって考える必要があるもうひとつ重要そうな要素が「非同期プログラミング」だ。
JavaScript だと、古典的なものだと callback 関数を使い、最近だと Promise
, async
, await
などを使って、プログラミングしていく。
これが Rust だとどうなっているのかを見ていく。
async な関数って何者?
Rust には構文として、async
, await
が用意されており、async
な関数は、JavaScript の async
が Promise
を返すように、Rust では Future
トレイトが実装された値を返す。(値は後でわかるわー。みたいなノリ)
IntelliJ で実際にコードを見てみると、型が見える。以下のように、関数 async hoge
の返り値は、()
ではなく、impl Future<Output=()>
になっているのが見える。
「トレイトの実装」を返しているが、何が実装されているかはわからないという「謎」な物体を async
関数は返しているのだ!謎!
この Future
トレイト自体が何を実装するべきか? というのは、ドキュメントを見てみる。
-
type Output
: 出力をどうするべきか? 関数の返り値。 -
pub fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>
- Poll: できるところまで進めて、実行状態を返すもの。(JavaScript の
Promise
でいうと、Pending
、Fulfilled
、Rejected
などという状態で管理しているもの。)
- Poll: できるところまで進めて、実行状態を返すもの。(JavaScript の
Rust の Poll
は以下のような列挙体となっている。
pub enum Poll<T> {
Ready(T),
Pending,
}
Ready が実際に返り値が使えるようになった状態を表していて、Pending はまだ作業中の状態を表している。Rustは、エラーハンドリングについては、Result
を使うので、JavaScript の Promise
のように、Rejected
な状態はないということで理解した。
Pin, Context が何者なのか? というのはまだ理解できなかった。
この、抽象的なFuture型のモノを、スレッド等を駆使してうまくコントロールしてくれるものは Rust 本体には実装されていないので、「ランタイム」を用意する必要があるらしい。
代表的なランタイム
ランタイムを自分で実装することも可能なようだ。学習には良さそうだが、実際の利用には適さない部分が出てきそうではある。
そこで、以下のようなランタイムが開発されている。
futures
ロゴがかっこいい。事実上の標準ライブラリ的な扱いらしい。
tokio
名前の由来は Tokyo から来ているらしい。例のアイドル系農家とは違う。
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
比較的最近作られたライブラリらしい。
Tokio を使ってみる
Cargo.toml へのクレート追加
Cargo.toml へ、tokio への依存を追加する。
[dependencies]
tokio = { version = "1.3.0", features = ["full"]}
はじめての Tokio
ということで、書いてみたのがこちらとなる。実行すると、5秒ほどまたされて、10+20
の答えが返される。
諸々新しい要素が出てくるので、見ていく。
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).await
はimpl Future<Output=i32>
ではなく、i32
を返してくれる。 -
tokio::time::{sleep, Duration}
: tokio にある機能で一定時間処理を止めるときなどに利用するもの。上記のコードでは5000ms止めるという非同期な関数を呼んでいて、それをawait
することで次の処理に進むのを待った状態にしている。
モジュール
これから、Webアプリを作るにあたって、コード量が多くなってくるのにも関わらず、すべて main.rb
に書くのは厳しい。Java でいうパッケージ・Ruby でいうモジュール的な仕組みがほしい。
もちろん、Rust にも方法が用意されている。
まずは1つのファイルでモジュールを使う
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つかない。シンプル!
- 継承はないので、Java にあるような
モジュールの中にモジュールを書くこともできるが、こちらも関数と同じく、デフォルトはモジュール内でしかアクセスできないモジュールとなる。
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
が長くなる問題を対処できないのでファイル分割を考える。
モジュールについてファイルを分ける
mod の中身は、モジュール名の同名のファイルを作成することで分割することができる。
先程つくった main.rs
を分割してみる。
mod my_app;
fn main() {
my_app::do_something();
my_app::my_child4::do_something();
}
my_app.rs
は、 mod my_app
の中身を表す。
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
を作ることで中身を表すことができる。
// この関数は外から呼べる
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
も作っていくことで、ファイルを分割することができる。
use キーワード
今まで、use
はライブラリを利用するときなどは利用してきたが、モジュール内の何かを、同じスコープで利用するときに使うものであるということが理解できる。
mod my_app {
pub mod child {
pub mod grand_child {
pub fn do_something () {
}
}
}
}
fn main() {
// これは長いので書くのが辛い
my_app::child::grand_child::do_something();
}
use を使うと以下のようになる。
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;
クレート内にクレートを作る
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
に以下を追加する。
[workspace]
members = ["infra"]
infra
クレートでは、src/main.rs
ではなく、src/lib.rs
が作成されている。
以下のように、コードを書き換える。
pub fn hello_from_infra() {
println!("hello");
}
ルートクレート側で、infra
クレートの機能を使いたいので依存を貼る。
[dependencies]
infra = { path = "infra"}
これで、ルートクレート側の src/main.rs
で infra
の中にある公開された要素を使うことができる。
use infra::hello_from_infra;
fn main () {
hello_from_infra();
}
復習
基本的なRustプログラミングについて学んだ
また、このスクラップではマルチスレッドプログラミングや、非同期関数、モジュールやクレートによる機能の分割についても考えてきた。
ということで、ようやく actix-web によるWebアプリケーションを作っていく。
actix-web 最初の一歩
まずはインストールする。例によって Cargo.toml
に以下を追加する。
[dependencies]
actix-web = "3"
そして、main.rs にHTTPサーバを起動するためのコードを記載していく。
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
auto reload みたいな仕組みはあるか?
node.js で express などを利用して Webアプリケーションを開発する場合 node-dev
、nodemon
のように、ファイルの変更に応じて自動的に再度プロセスを作り直す方法があった。
rust にも、同様の仕組みが存在する。
cagro-watch のインストール
cargo の拡張として、cargo watch をインストールする。
$ cargo install cargo-watch
開発時
$ cargo watch -x run
これで、コード変更時に毎回 cargo run
をし直さなくてもよくなった。
ドキュメント参照のこと
ログを残したい
最初の状態だと、ログが出てくれないので、なんだか動いているのか動いていないのか不安になってしまう。Webアプリケーションの本質的な部分とは若干はずれるところはあるが、開発時の問題解決の手がかりとしても、ログくらいは見えるようにしておきたい。
actix-web には、ミドルウェアという仕組みがあって、ロジックを実行する前後でなにかの処理を行うことができる。
ログに関するミドルウェアは actix-web 標準で用意されているため、素早くログに関する設定を行うことができる。
Rustのログの仕組み
多くのログに関する処理は、log
と呼ばれるクレートを利用しているようだ。
rust-lang
が配っているのを見るに、事実上の標準ライブラリのようで、これはログを行うための標準的なインターフェイスを提供している。
使い方を学んでみる。
適当なクレートを作り、Cargo.toml には、log = "0.4.0"
を依存として追加する。
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 がある。
これは、環境変数 RUST_LOG
の値によって、出力の仕方を制御できるシンプルなライブラリとなっている。
Cargo.toml に env_logger = "^0.8.3"
の依存を追加した上で、src/main.rs
を以下のように変えて、環境変数 RUST_LOG
をつけて実行してみる。
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 を編集する。
[dependencies]
actix-web = "3"
+ env_logger = "^0.8.3"
Logger ミドルウェアの利用
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 を利用して転送することができる。