Rust 日誌系統實踐總結

熊皮皮發表於2018-11-22

文件列表見:Rust 移動端跨平臺複雜圖形渲染專案開發系列總結(目錄)

(上次更新:2018-12-20 新增【日誌顏色】)基於log、env_logger、fern等的使用總結,詳細配置建議參考官方說明

給工程新增第三方日誌庫依賴

給Cargo.toml檔案加上如下配置,log基本為Rust專案日誌需求的標配庫,env_logger提供了具體實現,類似策略模式:log定義操作,env_logger實現具體行為,很方便切換另一個實現了log所定義介面的庫,比如daboross/fern

[dependencies]
log = "0.4.0"
env_logger = "0.6.0"
複製程式碼

env_log配置

下面描述我們專案對env_log所作的配置。問題:修改format導致終端執行時日誌無顏色。

配置輸出時間為本地時間

env_logger預設用0時區,而北京是東8區,每次日誌輸出都少8小時,時間沒對上不方便分析日誌。0時區舉個例子:

INFO 2018-11-18T02:00:08Z: webgpu-native::registry: env_logger initialized.
複製程式碼

下面給出env_logger輸出本地時間的示例程式碼,參考了DCjanus/nabu,他用flexi_logger,略調整即可用於env_logger。加上更多自定義資訊的關鍵是修改writeln! 巨集。

#[macro_use]
extern crate log;
extern crate chrono;
extern crate env_logger;

fn init_log() {
    use chrono::Local;
    use std::io::Write;

    let env = env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "trace");
    env_logger::Builder::from_env(env)
        .format(|buf, record| {
            writeln!(
                buf,
                "{} {} [{}] {}",
                Local::now().format("%Y-%m-%d %H:%M:%S"),
                record.level(),
                record.module_path().unwrap_or("<unnamed>"),
                &record.args()
            )
        })
        .init();

    info!("env_logger initialized.");
}
複製程式碼

以上程式碼需在fn main()開始或lazy_static開始,否則剛開始部分日誌不受新配置影響。 放在lazy_static的日誌配置需要手動 啟用,比如

// 定義
lazy_static! {
    pub(crate) static ref HUB: Hub = {
        init_log();
        Hub::default()
    };
}
// 在某個入口函式中先訪問HUB,“強迫”它執行lazy_static程式碼塊
fn entry_point() {
    &*HUB; // “強迫”執行lazy_static程式碼塊
    
    HUB.some_method(); // 裡面的info!()等可正常輸出到檔案或控制檯
}
複製程式碼

以東8區為例,執行顯示:

// 修改前
[2018-11-18T02:00:08Z INFO  webgpu-native::registry] env_logger initialized.
// 修改後
2018-11-18 09:27:43 INFO [webgpu-native::registry] env_logger initialized.
複製程式碼

日誌新增行號

writeln!(
    buf,
    "{} {} [{}:{}] {}",
    Local::now().format("%Y-%m-%d %H:%M:%S"),
    record.level(),
    record.module_path().unwrap_or("<unnamed>"),
    record.line().unwrap_or(0),
    &record.args()
)
複製程式碼

執行顯示:

2018-11-18 10:38:41 INFO [webgpu-native::registry:87] env_logger initialized.
複製程式碼

日誌新增檔名

writeln!(
    buf,
    "{} {} [{}:{}:{}] {}",
    Local::now().format("%Y-%m-%d %H:%M:%S"),
    record.level(),
    record.module_path().unwrap_or("<unnamed>"),
    record.line().unwrap_or(0),
    &record.args()
)
複製程式碼

執行顯示:

2018-11-18 10:38:48 INFO [webgpu-native::registry:webgpu-native/src/registry.rs:87] env_logger initialized.
複製程式碼

日誌級別左/右對齊

  1. 左對齊
    writeln!(
        buf,
        "{:<5} {} [{}:{}] {}",
        record.level(),
        // same as previous content
    )
    複製程式碼
  2. 右對齊
    writeln!(
        buf,
        "{:>5} {} [{}:{}] {}",
        record.level(),
        // same as previous content
    )
    複製程式碼

參考:What is the easiest way to pad a string with 0 to the left?

日誌顏色

不修改format,env_logger預設用不同顏色標識level日誌。前面的修改導致這一特性“失效”,看著不直觀,當然CLion等可以用Grep Console外掛給日誌上色,如果是Terminal中執行,那還得我們修改format加上顏色才行。以下內容參考env_logger-0.6.0/src/fmt/mod.rs的DefaultFormatter原始碼,感謝 @齒輪哥 指導。

    use std::io::Write;
    let env = env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "trace");
    let mut builder = env_logger::Builder::from_env(env);
    println!("builder = {:?}", builder);
    builder
        .format(|buf, record| {
            let level = { buf.default_styled_level(record.level()) };
            write!(buf, "{}", format_args!("{:>5}", level));
            writeln!(buf, " {}", &record.args())
        })
        .init();
複製程式碼

過濾日誌級別

env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "trace");
複製程式碼

filter_or()可配置如下內建值過濾不同級別的日誌,其實我們也可自行新增filter tag:

  • "trace"
  • "info"
  • "debug"
  • "warn"
  • "error"

也可以在程式執行時傳遞命令列引數進行過濾:

$ RUST_LOG=info ./main
[2018-11-03T06:09:06Z INFO  default] starting up
複製程式碼

組合過濾條件

新增depX=Y,可同時過濾多個條件、以其中級別最高的為準,低於最高階都沒法通過,比如下面將顯示級別高於info、也高於debug級別的日誌 = 要高於info級別,乾脆設定成info就行了,真是多此一舉。

RUST_LOG=info,dep1=debug ./main
複製程式碼

動態過濾資訊

在複雜專案中往往存在多個模組,作為其中一個模組的開發者,為了定位自己負責模組的問題,過濾掉其他模組的日誌是很常見的需求,由於整體專案通常使用同一個日誌庫,逐行註釋其他模組的日誌輸出顯然是不可理的行為。另外,雖然控制檯可以做過濾處理,多條件的過濾規則編寫起來也有難度,而且可能日誌檢視系統不支援這種操作。其實,我們可以給前面一直在修改的format()加上過濾邏輯,比如:

format(|buf, record| {
    // special format for debug messages coming from our own crate.
    if record.level() > log::LevelFilter::Info && record.target() == "my_module" {
        write!(...)
    } else if /* some condition */ {
        write!(...)
    } else if /* some condition 2*/ {
        write!(...)
    } else {
        write!(...)
    }
}
複製程式碼

過濾邏輯的實現可參考fern/cmd-program.rs

fern,env_logger的另一個選擇

daboross/fern

Simple, efficient logging for Rust

fern配置起來更直觀(所下所示),目前我還沒測試它與env_logger的效能差異

// Configure logger at runtime
fern::Dispatch::new()
    // Perform allocation-free log formatting
    .format(|out, message, record| {
        out.finish(format_args!(
            "{}[{}][{}] {}",
            chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
            record.target(),
            record.level(),
            message
        ))
    })
    // Add blanket level filter -
    .level(log::LevelFilter::Debug)
    // - and per-module overrides
    .level_for("hyper", log::LevelFilter::Info)
    // Output to stdout, files, and other Dispatch configurations
    .chain(std::io::stdout())
    .chain(fern::log_file("output.log")?)
    // Apply globally
    .apply()?;

// and log using log crate macros!
info!("helllo, world!");
複製程式碼

提高日誌效能

todo

相關文章