撰寫日誌(logging)是除錯(debug)和事件分析時經常會使用的方式。Rust生態圈有log這個crate,針對訊息重要性(等級)提供了不同的巨集讓函式庫或是應用程式輸出日誌(log),不過它並沒有限定日誌最終會被輸出到哪,Rust開發者在引入有用到log這個crate的函式庫後,可以自行決定日誌的輸出方式。



log將訊息依重要性依序分為tracedebuginfowarnerror五個等級,trace是最不重要的,error則是最重要的。這五種等級有各自對應的類函數(function-like)巨集(以下稱這些巨集為日誌巨集),用法如下:

#[macro_use]
extern crate log;

pub fn add(a: i32, b: i32) -> i32 {
    info!("{a} + {b}");

    a + b
}

日誌巨集的撰寫格式和println!類似,可以參考這篇文章來了解格式化文字的方式。除此之外,日誌巨集還多了標籤的功能,可以對日誌訊息做「分類」的動作,用法如下:

#[macro_use]
extern crate log;

pub fn add(a: i32, b: i32) -> i32 {
    trace!(target: "call_function", "add");
    info!("{a} + {b}");

    a + b
}

如果未在日誌巨集的第一個參數使用target:來定義標籤名稱,則會將標籤名稱預設為呼叫這個巨集的模組的路徑。例如上面的程式其實等同於:

#[macro_use]
extern crate log;

pub fn add(a: i32, b: i32) -> i32 {
    trace!(target: "call_function", "add");
    info!(target: core::module_path!(), "{} + {}", a, b);

    a + b
}

至此log這個crate的用法就介紹完了,超簡單。但是如果我們在src/main.rs中呼叫上面這個add函數的話,會發現程式並不會輸出任何日誌訊息。

因為我們還需要引用其它的logger實作,才能真正的讓這些日誌巨集發揮其該有的功能。以下會介紹一些常用的logger實作。

env_logger

env_logger是一個輕巧的logger,顧名思義,它可以透過環境變數在程式執行階段進行組態設定。

基本用法如下:

fn main() {
    env_logger::init();

    ...
}

預設的情況下,env_logger會將日誌訊息輸出到標準錯誤(stderr),若想改成輸出到標準輸出(stdout),可以將程式改寫如下:

fn main() {
    env_logger::builder().target(env_logger::Target::Stdout).init();

    ...
}

控制env_logger行為的環境變數為RUST_LOG,其值的寫法和用途如下:

  • info:輸出等級至少為info的日誌訊息。
  • hello:輸出hello這個模組的所有日誌訊息。
  • hello=info:輸出hello這個模組中等級至少為info的日誌訊息。
  • error,hello=info:輸出等級至少為error的日誌訊息,但是在hello這個模組中會輸出等級至少為info的日誌訊息。
  • error/regex1,hello=info/regex2:輸出等級至少為error的日誌訊息,這些訊息要符合regex1代表的正規表示式。但是在hello這個模組中會輸出等級至少為info的日誌訊息,這些訊息要符合regex2代表的正規表示式。

RUST_LOG_STYLE環境變數可以控制env_logger輸出的訊息要不要帶有顏色或是粗斜體等風格,設成never可以禁用文字風格。

若想要自訂日誌訊息的格式可以將程式改寫如下:

use std::io::Write;

fn main() {
    env_logger::builder()
        .format(|buf, record| writeln!(buf, "{}({}): {}", record.level(), record.target(), record.args()))
        .init();

    ...
}

如果要在Rust測試中抓取日誌訊息(為了讓這些訊息只有在測試失敗的時候才會被輸出),在初始化env_logger時可以使用如下的程式敘述:

env_logger::builder().is_test(true).init();

env_logger需要透過設定RUST_LOG環境變數才能真正輸出日誌訊息,如果想要預設RUST_LOG環境變數的值,程式可以這樣寫:

use std::env;

fn main() {
    if env::var("RUST_LOG").is_err() {
        env::set_var("RUST_LOG", "debug");
    }

    env_logger::init();

    ...
}

pretty_env_logger

pretty_env_loggerenv_logger的視覺加強版,用法與env_logger基本上是一樣的。不過pretty_env_logger並沒有提供builder函數,而是改用formatted_builderformatted_timed_builder函數,這兩個函數內部會去呼叫env_logger::Builderformat方法來設定文字格式。

基本用法如下:

fn main() {
    pretty_env_logger::init();

    ...
}

simplelog

simplelog提供了三種輕巧的logger,分別是什麼風格都不加、單純將訊息寫入標準輸出或是標準錯誤的SimpleLogger、有加風格的TermLogger、能將日誌寫入某個Writer(例如檔案)的WriteLogger。另外它還提供了CombinedLogger,讓我們可以同時多個以上提到的logger。

SimpleLogger的基本用法如下:

fn main() {
    simplelog::SimpleLogger::init(simplelog::LevelFilter::Info, simplelog::Config::default()).unwrap();

    ...
}

LevelFilter列舉可以設定能輸出的日誌訊息之最小等級。對於error等級的日誌訊息,SimpleLogger會將其寫進標準錯誤,其餘等級的日誌訊息會寫入標準輸出。

TermLogger的基本用法如下:

fn main() {
    simplelog::TermLogger::init(simplelog::LevelFilter::Info, simplelog::Config::default(), simplelog::TerminalMode::Stderr).unwrap();

    ...
}

TerminalMode列舉可以設定TermLogger要將日誌訊息輸出到標準輸出或標準錯誤。或者可以使用其Mixed變體,將error等級的日誌訊息寫進標準錯誤,其餘等級的日誌訊息會寫入標準輸出。

WriteLogger的基本用法如下:

use std::fs::File;

fn main() {
    simplelog::WriteLogger::init(simplelog::LevelFilter::Info, simplelog::Config::default(), File::create("app.log").unwrap()).unwrap();

    ...
}

以上用法,會將日誌訊息寫進目前工作目錄的app.log檔案中。

CombinedLogger的基本用法如下:

use std::fs::File;

fn main() {
    simplelog::CombinedLogger::init(vec![
        simplelog::SimpleLogger::new(simplelog::LevelFilter::Info, simplelog::Config::default()),
        simplelog::WriteLogger::new(simplelog::LevelFilter::Info, simplelog::Config::default(), File::create("app.log").unwrap()),
    ])
    .unwrap();

    ...
}

以上用法,會將日誌訊息寫進目前工作目錄的app.log檔案,以及標準輸出和標準錯誤中。

若要修改日誌訊息的格式,就不能直接使用simplelog::Config結構體,而是要透過simplelog::ConfigBuilder結構體來產生simplelog::Config結構實體。如下:

fn main() {
    simplelog::SimpleLogger::init(
        simplelog::LevelFilter::Info,
        simplelog::ConfigBuilder::new().set_time_format_str("%+").set_time_to_local(true).build(),
    )
    .unwrap();

    ...
}

simplelog不能完全自訂訊息的格式,只能按照ConfigBuilder結構實體提供的現有方法來設定。

syslog

syslog是Unik-like的作業系統用來統一管理系統日誌所用的協議或者說服務。syslog這個crate可以讓log的日誌訊息輸出給syslog進行管理。

基本用法如下:

fn main() {
    let formatter = syslog::Formatter3164 {
        facility: syslog::Facility::LOG_USER,
        hostname: None,
        process: String::from("myprogram"),
        pid: unsafe { libc::getpid() },
    };

    let logger = syslog::unix(formatter).expect("could not connect to syslog");

    log::set_boxed_logger(Box::new(syslog::BasicLogger::new(logger))).map(|_| log::set_max_level(log::LevelFilter::Info)).expect("could not register logger");

    ...
}

console_log

console_log可以在Webassembly程式中將日誌訊息輸出到主控台(console)。

基本用法如下:

fn main() {
    console_log::init_with_level(log::Level::Info).unwrap();

    ...
}