📃

Rust の slog の基本設定

2024/05/20に公開

開発において効果的なログ管理は非常に重要です。
slogは構造化ログのための強力なライブラリなのですが、意外とまとまった記事が見つかりませんでした。もちろん公式サイトにはちゃんと書いてあるのですが、基本的な内容がほしいなと思いまとめてみました。

全体像 (logging.rs)

最初に、DEFAULTとしてslogを設定している具体例を示し、順に解説していきます。
この例ではバージョン情報が全体のログに付与されるように設定してあります。

logging.rs
use once_cell::sync::Lazy;
pub use slog::*;
use slog_async::Async;

pub static DEFAULT: Lazy<Logger> = Lazy::new(|| {
    let mk_term = || {
        slog_term::FullFormat::new(slog_term::TermDecorator::new().build())
            .build()
            .fuse()
    };

    let mk_json = || slog_json::Json::default(std::io::stdout()).fuse();

    let format = std::env::var("RUST_LOG_FORMAT").unwrap_or_default();
    let drain = match format.as_str() {
        "json" => Async::default(slog_envlogger::new(mk_json())).fuse(),
        _ => Async::default(slog_envlogger::new(mk_term())).fuse(),
    };

    Logger::root(
        drain,
        o!(
            "version" => env!("CARGO_PKG_VERSION"),
        ),
    )
});
Cargo.toml
[dependencies]
slog = { version = "2.7.0", features = ["max_level_trace", "release_max_level_info"]  }
slog-async = "2.8.0"
slog-term = "2.9.1"
slog-json = "2.6.1"
slog-envlogger = "2.2.0"

slog::* を再エキスポート

賛否が分かれるかもしれませんが、、、

pub use slog::*;

これによってloggingをインポートするだけでslogのマクロにアクセスできるようになります。

環境変数でログレベルを設定

slog_envloggerを使うと環境変数RUST_LOGで柔軟にログレベルを変更することが可能です。

let drain = Async::default(slog_envlogger::new(mk_term())).fuse();
export RUST_LOG=debug

この設定により、debugレベルまでのログが出力されるようになります。

最大ログレベルの制御

featureで最大のログレベルを設定できます。例えば、以下のように設定することで、開発中は詳細なtraceレベルのログを取得し、リリースビルドではinfoレベルまでのログのみを出力することができます。これにより、不要なログ出力を抑え、実行時のパフォーマンスを向上させることができます。
(参考: Notable details

slog = { version = "2.7.0", features = ["max_level_trace", "release_max_level_info"] }

注意点として、環境変数RUST_LOGで設定したログレベルは、このfeatureで指定した最大ログレベルを超えることはできません。(エラーになるわけではなくfeatureでの値が優先されます)

ログフォーマットの指定

独自のやり方ですが、ログのフォーマットも環境変数RUST_LOG_FORMATを用いて簡単に変更できるようにしてみました。

let format = std::env::var("RUST_LOG_FORMAT").unwrap_or_default();
let drain = match format.as_str() {
    "json" => Async::default(slog_envlogger::new(mk_json())).fuse(),
    _ => Async::default(slog_envlogger::new(mk_term())).fuse(),
};

デフォルトではターミナルフォーマットが使用され、運用環境やデバッグのニーズに応じて簡単に切り替えることができます。

slog-env-cfg というのもあるのですが、メンテされてないっぽい)

使い方

次のように使います。

use logging::*;

let log = DEFAULT.new(o!("function" => "main"));
info!(log, "Starting up");
debug!(log, "log level check");
trace!(log, "log level check");
error!(log, "log level check");
warn!(log, "log level check");
crit!(log, "log level check");

構造化

以下のようにログに key=>value の値を付けることができます。
value に使う値はslog::Valueを実装していることが期待されていて、ほとんどの型はそのままでいけます。そうでない場合は%を付けておけば OK です。
(参考: fmt::Display and fmt::Debug values

let log = DEFAULT.new(o!(
  "function" => "get_calculated_value",
  "id" => self.id,
));
info!(log, "start");

let a = 1_u8;
let b = BigUint::from(2_u8);
let c = BigDecimal::from(3_u8);

debug!(log, "details";
  "a" => a,
  "b" => %b,
  "c" => %c,
);

まとめ

slogを利用することで、型セーフな構造化されたロギングができるようになります。Asyncでマルチスレッド環境でも安全です。
構造化されたロギングの利点は、ログメッセージに加えて、関連するコンテキスト情報を一緒に記録することです。これによりログの分析や検索が容易になり、問題の特定やデバッグが迅速に行えるようになります。

例えば AWS の環境であれば、json で S3 に書き出しておけば Athena のクエリで key=>value の値で検索することが可能です。

Golang にも 1.21 から標準で入ってますし、皆さんもログをどんどん構造化していきましょう!

Discussion