在 Rust 中输出日志有很多不同的库,因此选择其中一个库可能很困难。当 println!dbg!eprintln! 无法满足你的要求时,拥有一种构建结构日志的方法非常重要,尤其是在生产级应用程序中。本文将帮助你了解在 Rust 中进行日志记录时,哪一个日志板库最适合你的用例。

Rust 中的日志记录是如何工作的?

简而言之,Rust 中的记录器依赖于充当 “日志记录门面” 的库,该库提供了一个可与记录器协同工作的日志记录 API。因此,例如,如果我们有一个名为 log 的库,为我们提供了一个可与记录器一起使用的日志记录实现,那么我们就需要再添加一个执行日志记录操作的库,例如,simple-logger 是众多可以使用 log 的库之一。有些日志记录门面可能只能由它们自己的特定记录器使用,例如,tracing 要求你使用 tracing-subscriber 库,或者自己实现一个自定义类型,来实现 tracing::Subscriber 接口。

我们现在就开始对比 Rust 的日志记录库吧!

log

log 是一个自称为 “轻量级日志记录门面” 的库。该库将日志记录门面定义为 “提供抽象于实际日志记录实现之上的单个日志记录 API” 的库。这基本上意味着我们需要运行另一个提供实际日志记录的库,然后使用这个库发送日志记录消息。log 还由 Rust 核心团队维护,而且可能是你在 Rust Cookbook 中看到的第一个库,就是这样。

看一个在 GitHub 代码库中找到的简单示例,它说明了如何使用 log:

use log;
  
pub fn shave_the_yak(yak: &mut Yak) {
    log::trace!("Commencing yak shaving");

    loop {
        match find_a_razor() {
            Ok(razor) => {
                log::info!("Razor located: {}", razor);
                yak.shave(razor);
                break;
            }
            Err(err) => {
                log::warn!("Unable to locate a razor: {}, retrying", err);
            }
        }
    }
}

值得注意的是,log 还兼容 许多 记录器库 - 仅在其 GitHub 代码库中就列出了 20 多个,而且列表并非详尽无遗!如果你正在寻找一个通用的记录器,那么 log 绝对适合你。但是,它也没有其他一些库那么强大,因此要记住这一点。

对于大多数常见用例,log 是使用最简单的库:你只需设置消息级别,然后发送消息即可!

快速总结:

  • 由官方 Rust 团队维护
  • 适用于几乎所有记录器库
  • 没有其他一些日志记录门面库那么强大

env-logger

env-logger 是一款简单易用、专为那些想要在小项目中实现日志记录而无意于使用可能需要大量样板文件并且重若千钧的日志记录器的开发者设计的 Rust 日志记录器。它由 Rust CLI 工作组 (WG) 所有,这意味着此项目会获得长期支持,这对我们来说是一件极好的事情。

它的设置非常简单,只需要一行语句即可完成:

let logger = Logger::from_default_env();

然后,你只需使用 RUST_LOG 环境变量运行 cargo,具体如下:

># 此命令将运行你的程序,并且仅从日志中打印出错误消息
RUST_LOG=ERROR cargo run

你还可以选择通过硬编码的方式设置应用程序的最小日志级别:

use env_logger::{Logger, Env};
  
let env = Env::new()
// 筛选出所有日志级别低于“info”的消息
  .filter_or("MY_LOG", "info")
// 打印时始终使用样式
  .write_style_or("MY_LOG_STYLE", "always");
  
let logger = Logger::from_env(env);

你是否遇到过那些特别喜欢疯狂输出日志的依赖?你还可以为特定的依赖设置日志级别(这需要结合 log 依赖项来实现):

use env_logger::Builder;
use log::LevelFilter;

let mut builder = Builder::new();

builder.filter_module("path::to::module", LevelFilter::Info);
  .unwrap();
  

话虽如此,尽管 env-logger 是一款非常方便的日志记录器,但在实际应用中它仍然存在一些不足之处。其中之一就是关于如何编写自己的日志记录管道功能的文档非常少,这可能会导致其实现非常棘手;除此之外,也不清楚此依赖项是否线程安全。不过,对于任何希望快速高效地实现日志记录功能的开发者来说,它仍然是一款非常不错的依赖项!

快速总结:

  • 由 Rust CLI 工作组所有
  • 使用简单且容易上手
  • 缺乏有关更复杂功能(比如日志附加或管道)的文档
  • 关于此依赖项是否是 100% 线程安全的还存在一些尚未明确的问题

log4rs

log4rs 是一个基于 Java 的 log4j 建模的日志记录工具 — 可能是最常用于部署的开源软件之一的日志包。这个工具包比其他包需要更多设置,可以通过 YAML 文件或通过编程完成配置。log4rslog 兼容,这对我们来说非常棒,意味着我们不必采用新的范例就能使用 log4rs

如果你想创建一个配置文件以供加载,你应该按如下方式设置你的 YAML 文件:

# 设置刷新速率
refresh_rate: 30 seconds

# 附加程序
appenders:
# 这个附加程序将追加到控制台
stdout:
  kind: console
# 这个附加程序将追加到日志文件
requests:
  kind: file
  path: "log/requests.log"
# 这只是一个简单的字符串编码器 — 这将在下面解释
  encoder:
    pattern: "{d} - {m}{n}"

# 打印到 stdout 的附加程序将仅在消息的日志级别为 warn 或更高时打印
root:
  level: warn
  appenders:
    - stdout

# 设置最小日志记录级别 - 低于最小记录级别的日志消息将不会被记录
loggers:
  app::backend::db:
    level: info

  app::requests:
    level: info
    appenders:
      - requests
    additive: false

编码器可以使用 JSON 编码或模式编码。在这里,我们决定使用模式编码,它类似于原始的 log4j 模式,但使用了 Rust 字符串格式化 — 你可以在这里了解更多关于如何格式化你的编码器模式的信息。(https://www.tutorialspoint.com/log4j/log4j_patternlayout.htm)

然后,你可以在设置程序时对其进行初始化,如下所示:

log4rs::init_file("log4rs.yml", Default::default()).unwrap();

你也可以通过编程创建你的配置:

use log::LevelFilter;
use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender;
use log4rs::encode::pattern::PatternEncoder;
use log4rs::config::{Appender, Config, Logger, Root};

fn main() {
  // 设置 ConsoleAppender 以允许将日志追加到控制台(标准输出)
  let stdout = ConsoleAppender::builder().build();

  // 设置 FileAppender 以允许将日志追加到日志文件
  let requests = FileAppender::builder()
      .encoder(Box::new(PatternEncoder::new("{d} - {m}{n}")))
      .build("log/requests.log")
      .unwrap();

  let config = Config::builder()
      .appender(Appender::builder().build("stdout", Box::new(stdout)))
      .appender(Appender::builder().build("requests", Box::new(requests)))
      .logger(Logger::builder().build("app::backend::db", LevelFilter::Info))
      .logger(Logger::builder()
          .appender("requests")
          .additive(false)
          .build("app::requests", LevelFilter::Info))
      .build(Root::builder().appender("stdout").build(LevelFilter::Warn))
      .unwrap();

  let handle = log4rs::init_config(config).unwrap();

  // 使用句柄在运行时更改记录器配置
}

您也可以用 log4rs 自动存档日志记录,这是一个非常出色的特性!大多数(如果不是所有的话)其他日志记录模块通常需要您手动实现该特性,因此将此特性内置到日志记录器本身是一个巨大的便利。我们可以将以下内容添加到 YAML 配置文件(在“追加器”下)来开始设置:

rolling_appender:
   kind: rolling_file
   path: log/foo.log
   append: true
   encoder:
     kind: pattern
     pattern: "{d} - {m}{n}"
   policy:
     kind: compound
     trigger:
       kind: size
       limit: 10 mb
   # 达到最大日志量大小后,将在成功滚动时只删除文件
     roller:
       kind: delete

现在,我们有了可填充主日志文件并将其追加到存档日志文件的策略,该策略将在主日志文件达到 10 MB 的日志后删除当前激活的日志文件,以便接收更多日志。

如您所见,log4rs 是一个极其强大的模块,可与 log 模块协同工作,以提供强大的功能,涉及 Rust 中的日志记录,它对那些已非常熟悉 Java 等语言的日志记录的人来说是一个不错的选择。但是,您需要学习如何使用它,其设置过程与其他日志记录模块相比相当复杂,因此要有心理准备。

特点总结:

  • 功能齐全,可满足所有需求
  • 需要大量样板文件或配置文件
  • 易于设置您自己的文件追加
  • 可与 log 配合使用

tracing

tracing 是一款号称“一个用于给 Rust 程序配置以收集结构化、基于事件诊断信息的框架”的库,它需要配合其专门的日志记录库 tracing-subscriber 才能使用,或者你也可以自己创建一个实现 racing::Subscriber 函数的自定义类型来配合它。它是由 Tokio 团队开发的,专为异步构建,非常适合带有 Rust 日志的 Web 应用。

tracing 使用“span”的概念来记录程序执行的流程。事件可以发生在 span 内或 span 外,也可以像非结构化日志记录一样使用(即:任意方式记录事件),但也可以表示 span 内的一个时间点。请参见下文:

use tracing::Level;

// 在任何 span 上下文之外记录一个事件:
tracing::event!(Level::DEBUG, "something happened");

// 在进入的同时创建 span
let span = tracing::span!(Level::INFO "my\_span").entered();

// 在 “my\_span” 内记录一个事件。
tracing::event!(Level::DEBUG, "something happened inside my\_span");

span 可以形成一个树结构,整个子树由其子项表示——因此,如果父 span 的子 span 没有比它“活得”更久的话,父 span 将始终与持续时间最长的子 span 一样长。

由于这可能会有点多余,tracing 还包含了其他日志外观库中用于日志记录的常规宏——即 info!error!debug!warn!trace!。这些宏也都有一个 span 版本——但是,如果你使用的是 log 并且希望在不陷入确保所有内容都在一个 span 中的复杂性麻烦而尝试 tracing,那么 tracing 可以满足你的需求。

use tracing;

tracing::debug!("Looks just like the log crate!");

tracing::info_span!("a more convenient version of creating spans!");

tracing-subscriber 是设计用于与 tracing 配合使用的日志记录库,它让你可以定义一个实现 Subscriber 特性的日志记录器。

你可以像这样启动一个采用 RUST_LOG 环境变量的订阅者:

tracing_subscriber::registry()
  .with(fmt::layer())
  .with(EnvFilter::from_default_env())
  .init();

你还可以通过编程硬编码过滤器:

use tracing_subscriber::filter::{EnvFilter, LevelFilter};

let my_filter = EnvFilter::builder()
  .with_default_directive(LevelFilter::ERROR.into())
  .from_env_lossy();

tracing_subscriber::registry()
  .with(fmt::layer())
  .with(filter)
  .init();

你还可以将过滤器分层叠加。这在你希望同时使用多个订阅器时非常有用。

如果你需要将日志导出到某个地方,还可以使用 tracing_appender 库。你希望通过使用 .with_writer() 方法将其与你的 tracing 订阅器一起添加,如下所示:

// 创建一个按小时轮换的文件追加器
let file_appender = tracing_appender::rolling::hourly("/some/directory", "prefix.log");
// 使文件追加器变为非阻塞
// 该守卫用于确保将缓冲的日志刷新至输出
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);

// 将文件追加器添加到你的 tracing 订阅器
tracing_subscriber::fmt()
.with_writer(non_blocking)
.init();

non_blocking 写入器使用了一个实现了 std::io::Write 的类型构建而成 - 因此,如果你想实现自己的基于 std::io::Write 的日志表达(比如说,你想要一个自动将所有信息导出到 BetterStack 或 Datadog 的日志表达),你应该尝试一下。请参见下文:

use std::io::Error;
  
struct TestWriter;

impl std::io::Write for TestWriter {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        let buf_len = buf.len();
        println!("{:?}", buf);
        Ok(buf_len)
    }

    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

let (non_blocking, _guard) = tracing_appender::non_blocking(TestWriter);
tracing_subscriber::fmt()
    .with_writer(non_blocking)
    .init();

如你所见,tracing 系列 crate 在功能方面提供了强大的功能,并且足以应付任何网络应用程序,由 Tokio 团队维护,因此毫无疑问它将获得长期支持。但是,使用它需要了解 tracing 的工作原理,因为它使用了其他日志 crate 中未使用的概念 - 因此,如果你需要因为任何原因从这个 crate 中迁移并且你正在使用 spans,那么你将被锁定。但是,如果你问自己“Rust 中最好的日志记录 crate 是什么”,那么就强大功能而言,你不会错选 tracing 系列 crate。

总结:

  • 需要学习一些有关 span 等的信息才能充分利用
  • 由 Tokio 团队维护,因此很有可能看到 LTS
  • 分离出的 crate 意味着你无需安装你不会使用的东西
  • 由于其构建方式,可能是在列表中最复杂的使用系统

结论

感谢阅读!既然我们已经结束,我希望你对 Rust 中的日志记录有了更深入的了解。由于有如此多的日志库,很难弄清楚你应该使用哪一个,但希望这篇文章已经为哪个库是适用于你的用例的最佳 Rust 日志记录器提供了一些帮助。