有许多库可以在 Rust 中输出日志,有时很难选择该使用哪一个。当 println!
、 dbg!
和 eprintln!
无法解决问题时,找到一种方便记录日志的方法就很重要,尤其是在生产级应用程序中。本文将帮助您深入了解在 Rust 日志记录方面最适合您的用例的日志 crate。
Rust 中的日志如何工作?
简而言之:Rust 中的日志依赖于一个库来充当“日志门面”——以便其他crate可以和日志 API 配合工作。例如,如果我们有一个像 log
这样的crate,它为我们提供了可以与日志记录器一起使用的日志记录实现,那么我们还需要添加一个实际执行日志记录的crate 例如, simple-logger
是可以使用 log
的众多crate之一。某些日志门面可能只能由特定日志记录器使用 - 例如, tracing
要求您使用 tracing-subscriber
crate,要么实现您自己的自定义类型,该类型实现 tracing::Subscriber
。
话不多说,下边开始 Rust 日志crate的比较!
log
log
是一个自称为“轻量级日志记录门面”的crate。该crate将日志门面定义为一个库,“提供一个抽象实际日志记录实现的单一日志记录 API”——本质上,这意味着我们需要运行另一个提供实际日志记录的库,然后使用此crate来提供日志消息。 Log 也由 Rust 核心团队维护,并且可能是您在 Rust Cookbook 上看到的第一个 crate,所以就是这样。
下面是一个关于如何使用它的简单示例,取自 GitHub 存储库:
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
还与许多日志记录器crate兼容 - 仅他们的 GitHub 存储库就列出了 20 多个日志记录器crate,并且是一个非详尽的列表!如果您正在寻找一款多功能记录器,这绝对适合您。然而,它也不像其他一些crate那么强大,要记住这一点。
对于大多数常见的用例,使用 log crate 是最简单的:只需设置消息级别,然后发送消息!
A quick summary: 快速总结:
- 由官方 Rust 团队维护
- 适用于几乎所有日志记录器crate
- 不像其他一些特殊日志 crate 那么强大
env-logger 环境记录器
env-logger 是一个简单的 Rust 日志记录器,它易于使用,对于任何想要实现日志记录但不希望需要大量样板文件的 大型项目来说都非常方便。它由 Rust CLI 工作组 (WG) 所有,这意味着它将获得长期支持,这对我们来说非常好。
它可以在一行语句中设置:
let logger = Logger::from_default_env();
然后,只需像这样从 Cargo 运行程序,并在命令前面添加 RUST_LOG
环境变量:
# This command will run your program and only print out error messages from logs
RUST_LOG=ERROR cargo run
还可以在应用程序中硬编码最低日志级别,如下所示:
use env_logger::{Logger, Env};
let env = Env::new()
// filters out any messages that aren't at "info" log level or above
.filter_or("MY_LOG", "info")
// always use styles when printing
.write_style_or("MY_LOG_STYLE", "always");
let logger = Logger::from_env(env);
还可以设置特定依赖项的日志级别(这与 log
crate结合使用):
use env_logger::Builder;
use log::LevelFilter;
let mut builder = Builder::new();
builder.filter_module("path::to::module", LevelFilter::Info);
.unwrap();
然而,尽管它很方便, env-logger
确实遇到了您可能在生产级应用程序中寻找的一些问题:即,几乎没有关于为日志编写自己的管道的记录功能。可能会使其实现起来相当棘手,而且也不清楚这个crate是否是线程安全的。不用说,它对于任何快速而混乱的日志记录来说都是非常有用的!
A quick summary: 快速总结:
- 由 Rust CLI 工作组所有
- 使用简单,使用感觉良好
- 缺乏有关日志附加/管道等更复杂功能的文档
- 关于 crate 是否 100% 线程安全的一些不清楚的问题
log4rs
log4rs 是一个以 Java 的 log4j 为模型的日志包,log4j 是一个日志包,可能是部署最多的开源软件之一。该板条箱比其他板条箱需要更多的设置,并且可以使用 YAML 文件或以编程方式完成配置。 log4rs
与 log
兼容,这对我们来说非常好,因为这意味着我们不必仅仅为了使用 log4rs
就采用新的范例。
如果您想创建一个配置文件来加载,您可以像这样设置 YAML 文件:
# set a refresh rate
refresh_rate: 30 seconds
# appenders
appenders:
# this appender will append to the console
stdout:
kind: console
# this appender will append to a log file
requests:
kind: file
path: "log/requests.log"
# this is a simple string encoder - this will be explained below
encoder:
pattern: "{d} - {m}{n}"
# the appender that prints to stdout will only print if the log level of the message is warn or above
root:
level: warn
appenders:
- stdout
# set minimum logging level - log messages below the mnimum won't be recorded
loggers:
app::backend::db:
level: info
app::requests:
level: info
appenders:
- requests
additive: false
编码器encoder可以使用 JSON 编码或模式编码。在这里,我们决定使用模式编码,它与原始 log4j 模式类似,但使用 Rust 字符串格式 - 您可以在此处查看有关如何格式化编码器模式的更多信息。
然后可以在设置程序时初始化它,如下所示:
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() {
// set up ConsoleAppender to allow appending logs to the console (stdout)
let stdout = ConsoleAppender::builder().build();
// set up FileAppender to allow appending logs to a log file
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();
// use handle to change logger configuration at runtime
}
还可以使用 log4rs
自动归档日志,这非常棒!对于大多数(如果不是所有)其他记录器包,此功能必须由您自己手动实现,因此将此功能内置到记录器本身中会带来巨大的便利。我们可以通过将以下内容添加到 YAML 配置文件(在“appenders”下)来开始设置它:
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
# upon reaching the max log size, the file simply gets deleted on successful roll
roller:
kind: delete
现在我们有一个策略,写入主日志文件,然后在主日志文件达到 10 MB 的日志时将其附加到存档日志文件,然后删除当前活动的日志文件以准备接收更多日志。
正如您所看到的, log4rs
是一个极其通用的 crate,它可以与前面提到的 log
crate 配合使用,提供有关 Rust 日志的强大功能,如果您是来自像 Java 这样的语言,您已经了解其心智模型,只是想了解如何在 Rust 中进行日志记录。然而,作为交换,你必须学习如何设置记录器,与其他记录箱相比,设置本身相当复杂,所以请记住这一点。
Summary: 概括:
- 多功能的大型一体式crate
- 需要大量的样板代码或配置文件
- 轻松设置您自己的文件附加日志出口服务
- 适配
log
crate
tracing
tracing是一个自称为“用于检测 Rust 程序以收集结构化、基于事件的诊断信息的框架”的包,需要使用其对应的记录器 tracing-subscriber
或实现 tracing::Subscriber
tracing
使用“跨度(spans)”的概念,用于记录程序的执行流程。事件可以发生在跨度(spans)内部或外部,也可以类似于非结构化日志记录(即,仅以任何方式记录事件)使用,但也可以表示跨度(spans)内的时间点。见下文:
use tracing::Level;
// records an event outside of any span context:
tracing::event!(Level::DEBUG, "something happened");
// create the span while entering it
let span = tracing::span!(Level::INFO"my_span").entered();
// records an event within "my_span".
tracing::event!(Level::DEBUG, "something happened inside my_span");
跨度(spans)可以形成树结构,整个子树由其子级表示 - 因此,父级 Span 的持续时间将始终与其寿命最长的子级 Span 一样长(如果不是更长的话)。
因为所有这些可能有点过多, tracing
还包含了其他日志外观库中用于日志记录的常规宏 - 即 info!
、 error!
、 debug!
、 warn!
和 trace!
。每个宏都有一个跨度(spans)版本 - 但如果您来自 log
并且想要尝试 tracing
而不会迷失在尝试确保一切正常的复杂性中很快,tracing就为您提供了支持。
use tracing;
tracing::debug!("Looks just like the log crate!");
tracing::info_span!("a more convenient version of creating spans!");
Tracing-subscriber 是一个日志 create,旨在与 tracing
一起使用,让您定义一个实现 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 crate。需要使用 .with_writer()
方法将其添加到您的跟踪订阅者中,如下所示:
// create a file appender that rotates hourly
let file_appender = tracing_appender::rolling::hourly("/some/directory", "prefix.log");
// make the file appender non-blocking
// the guard exists to make sure buffered logs get flushed to output
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
// add the file appender to your tracing subscriber
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 提供了强大的功能,并且对于任何 Web 应用程序来说都足够强大,并且由 Tokio 团队维护,因此肯定会受到支持许久。但是,使用它需要了解 tracing
的工作原理,因为它使用其他日志crate中未使用的概念 - 因此,如果您出于某种原因需要从该crate迁移,并且您将被锁定,那么您将被锁定重新使用跨度span。但是,如果您问自己“Rust 中最好的日志crate是什么”,那么就这个crate系列的强大功能而言,您选择 tracing
crate不会出错。
Summary: 概括:
- 需要了解一些关于跨度span等的知识才能充分利用
- 由 Tokio 团队维护,因此很可能会看到 LTS
- 拆分crate 意味着不必安装不打算使用的东西
- 由于其构建方式,可能是列表中最复杂的系统
Conclusions 结论
谢谢阅读!现在我们已经结束了,我希望您对 Rust 登录有更好的了解。日志箱如此之多,很难确定您应该使用哪一个,但希望本文能够让您清楚地了解哪个crate是最适合您的用例的 Rust 日志库。
Logging in Rust - How to Get Started