#logging #testing #applications #log-messages #opinionated #scope #developing

loggy

一个面向开发和测试使用日志的 Rust 应用程序的库

17 个版本

0.5.3 2022 年 12 月 9 日
0.5.2 2022 年 11 月 18 日
0.5.1 2021 年 12 月 28 日
0.4.0 2021 年 12 月 24 日
0.1.7 2017 年 12 月 22 日

#257调试

Download history 9/week @ 2024-07-01 203/week @ 2024-07-29

每月 203 下载

MIT 许可证

40KB
495 代码行

loggy v0.5.3

Verify Monthly audit codecov Api Docs

一个面向开发和测试使用日志的 Rust 应用程序的库。

这个库最初是受 simple-logging 实现的启发,增加了针对应用程序开发(而不是库)的功能。结构化消息受到 slog 的影响,但允许嵌套结构,并提供仅针对人类可读性的单一格式。

动机

编写这个库是为了支持开发和测试使用日志的非平凡 Rust 二进制应用程序。这里提供的功能被提取出来,一方面是为了将其与应用程序代码本身隔离开来,另一方面是希望它可能对其他人也有用。

本质上,这个库的重点是使用日志向运行控制台应用程序的人类用户提供信息,而不是为类似服务器应用程序的行为分析提供日志事件。对于后者用例,您可能需要像 slog 这样的东西。

技术上,这个库是对 Rust 日志外观的实现,增加了几个额外功能。

功能

提供的功能反映了库的倾向性。

消息格式化

消息始终被输出到标准错误(测试期间除外,此时可能被捕获用于断言)。消息格式为 <prefix>[<thread>]: <time> [<level]>] <module or scope>: <message>,其中线程和时间在配置全局记录器时可以省略。例如

extern crate loggy;

fn main() {
    log::set_logger(&loggy::Loggy {
        prefix: "...", // Typically, the name of the program.
        show_time: true, // Or false, if you prefer.
        show_thread: true, // Or false, if you prefer.
    }).unwrap();
    log::set_max_level(log::LevelFilter::Info); // Or whatever level you want.

    // Use loggy facilities in the rest of the code.
    // ...
}

记录多行消息(包含 \n)将生成多个日志行,这些行将始终连续(即使来自多个线程)。第一行将包含大写的日志级别(例如,[ERROR]),所有后续行将指定小写(例如,[error])。如果包含时间戳,所有这些行的时间戳将相同。这使得日志消息易于 grep、计数等。

记录消息提供了一种结构化方式来格式化相关的附加信息。语法是 slog 的扩展,允许嵌套结构。然而,与 slog 不同,输出格式是固定的。例如

#[macro_use]
extern crate loggy;

fn foo() {
    let value = "bar";
    loggy::info!(
        "some text {}", 1;
        value,
        label {
            sub_field => value,
        }
    );
}

将生成以下消息

program name: [INFO] scope name: some text 1
program name: [info]   value: bar
program name: [info]   label:
program name: [info]     sub_field: bar

命名作用域

默认情况下,日志消息会标注生成它们的模块名称。为了更好地识别特定的处理阶段和/或任务,通常用显式的作用域名称替换它;注意这仅适用于当前线程。作用域可以以三种不同的方式建立

#[macro_use]
extern crate loggy;

#[loggy::scope("scope name")]
fn foo() {
    // Log messages generated here will be prefixed by the scope name instead of the module name.
    // ...
}

#[loggy::scope]
fn bar() {
    // Log messages generated here will be prefixed by the function name `bar` instead of the module name.
    // ...
}

fn baz() {
    loggy::with_scope("scope name", || {
        // Log messages generated here will be prefixed by the scope name instead of the module name.
        // ...
    });

    if some_condition {
        let _scope = loggy::Scope::new("scope name");
        // Log messages generated here will be prefixed by the scope name instead of the module name.
        // ...
    } else {
    }
}

日志级别

日志级别具有更强的语义

  • loggy::panic! 使用 Error 级别记录(即,格式化为日志消息),但始终转换为 std::panic!(即,终止当前线程)。

  • loggy::error! 具有不同的用途。它也指示不可恢复的错误,但允许代码继续,可能报告额外的错误,并在当前命名作用域结束时自动调用 panic!,表示此作用域失败并报告错误总数。在命名作用域外部调用 loggy::error! 是不允许的;它被转换为通用“错误!必须仅在作用域内使用”的 std::panic! 消息。错误,像恐慌一样,总是报告,不管日志级别如何。

  • loggy::warn! 只有在日志级别至少为 Warn 时才会报告,否则将被静默忽略。警告指定了代码有合理方式恢复并继续正常执行的不正常情况。这可以在任何命名作用域外部使用。

  • loggy::note! 允许代码实现“将警告视为错误”的功能。它需要一个额外的布尔标志来指定是否将其视为 error!warn!。由于这可以被视为 error!,因此它必须仅在作用域内使用。通常,使用全局标志来决定是否将特定类别的警告视为错误很有用,例如使用命令行标志。可以按以下方式完成:

#[macro_use]
extern crate loggy;

mod some_condition {
    loggy::is_an_error!(false); // By default, not an error.
}

fn main() {
    // ...
    some_condition::set_is_an_error(based_on_the_command_line_flags);
    // ...
    loggy::with_scope("scope name", || { // Errors must be inside some scope.
        // ...
        if test_for_some_condition {
            note!(some_condition::is_an_error(), "some condition"); // Will be an error or a warning depending on the command line flag.
        }
        // ...
    });
    // ...
}
  • loggy::info! 仅在日志级别至少为 Info 时报告,否则被静默忽略。信息消息应该很少,以指示程序的整体进度。

  • loggy::debug! 专门用于调试程序,针对的是代码开发者而不是程序的用户。调试消息始终在调试构建中发出;在发布构建中,只有当日志级别至少为 Debug 时才会发出。调试消息的格式包括一个额外的 <文件>:<>: 前缀,以标识其确切的源代码位置。最后,调试消息始终输出到标准错误,并且永远不会在测试中捕获(见下文),这使得可以调试检查预期日志的测试。

  • loggy::todox!loggy::debug! 相同。它允许使用 cargo todox 扩展,以确保在调试完成后代码中不留下任何遗留的调试消息。

  • loggy::trace! 仅在日志级别至少为 Trace 时报告,否则被静默忽略。跟踪消息以非常详细的方式描述程序进度,可能会生成非常大的日志。

您还可以使用 loggy::log!(level, ...) 来指定消息的级别。请注意,如果此级别为 Error,则消息只能生成在命名作用域内。没有强制执行 panic! 的方法(请改用 note!)。

测试

测试日志面临以下不便之处:

  • Rust log 面具强制使用所有线程共享的单个全局日志记录器。一旦设置,您甚至不能替换它。

  • 默认情况下,cargo test 使用多个线程并行运行测试。

  • 捕获日志消息的测试应捕获测试生成的所有子线程产生的所有内容。

因此,以下断言采取全局锁以确保不同测试的消息不会相互干扰。这有几个含义:

  • 测试断言必须设置一个捕获消息的记录器,因此不要将日志测试与任何设置全局记录器的代码结合使用。

  • 测试断言将顺序运行,一次一个,无论由 cargo test 产生的线程数量如何。这仍然允许非日志测试(不使用以下断言)并行运行。

  • 嵌套日志断言将导致死锁。这样做本身就没有意义,所以请不要这样做。

话虽如此,测试某些代码生成的实际日志消息是一种方便且出人意料强大的方法,以确保其按预期行为。它还确保日志消息包含预期数据,这是其他情况下很难验证的。以下断言可用于支持此功能

  • assert_logs(expected_log, || { ... }) 执行一些代码,并断言实际日志与(未缩进的)expected_log 完全相同。关键的是,它可以嵌套,因此您可以部分检查日志。外部 assert_logs(或 assert_logs_panics)收集的日志不包括内部 assert_logs 捕获的日志。

  • assert_panics(expected_panic, || { ... }) 执行一些代码,并断言它以(未缩进的)expected_panic 潜逃,忽略日志。

  • assert_logs_panics(expected_log, expected_panic, || { ... }) 执行一些代码,并断言实际日志符合预期,并且代码还以预期消息崩溃。

  • assert_writes(expected_text, |writer| { ... }) 提供了便利性,断言代码将(未缩进的)expected_text 写入到 writer: &mut dyn IoWrite。这实际上应该在更通用的包中。

这些故意不是附加到测试的属性宏(如标准的 #[should_panic])。这允许预期的文本进行动态格式化。

将环境变量 LOGGY_MIRROR_TO_STDERR 设置为任何非空值将导致所有消息都被输出到标准错误流,包括调试信息,即使在测试中也是如此。这会将调试信息置于其他消息的上下文中,有助于测试调试。

理想情况下,标准错误内容仅报告失败的测试(这包括任何调试信息)。在实际操作中,由于测试会创建新线程,Rust 捕获标准错误的机制可能无法正常工作,因此即使对于通过测试,从工作线程发出的调试信息也会可见。但这不是致命的,因为此类消息和 LOGGY_MIRROR_TO_STDERR 变量仅在积极调试问题时才会使用。

许可证

loggy 采用 MIT 许可证

依赖项

~2.6–8MB
~66K SLoC