likes
comments
collection
share

如何在rust中输出日志:在rust中打印日志的各种方式对比

作者站长头像
站长
· 阅读数 5

如何在rust中输出日志:在rust中打印日志的各种方式对比

有许多库可以在 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 文件或以编程方式完成配置。 log4rslog 兼容,这对我们来说非常好,因为这意味着我们不必仅仅为了使用 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