loggy

Verify Monthly audit codecov Api Docs

An opinionated library for developing and testing rust applications that use logging.

This was initially inspired by simple-logging implementation, with additional features focusing on development of applications (as opposed to libraries). Structured messages were influenced by slog, but allow for nested structures and provide only a single format targeting human readability.

Motivation

This library was written to support the development of a non-trivial Rust binary application(s) that uses logging. The functionality provided here was factored out, both to keep it isolated from the application's code itself, and in the hope it might prove useful to others.

In essence, the focus of this library is on using logging to provide information to the human user running the console application, rather than providing logging events for analysis of the behavior of server-like application. For the latter use case, you would want something like slog.

Technically, this library is an implementation for the Rust log facade, with a few additional features thrown in.

Features

The provided features reflect the library's opinionated nature.

Message formatting

Messages are always emitted to the standard error (except for in tests, where they may be captured for use in assertions). The message format is <prefix>[<thread>]: <time> [<level]>] <module or scope>: <message>, where the thread and time may be omitted when you set up the global logger. For example:

```ignore extern crate loggy;

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

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

} ```

Logging multi-line messages (that contain \n) will generate multiple log lines, which will always be consecutive (even when logging from multiple threads). The first line will include the log level in upper case (e.g., [ERROR]), all the following will specify it in lower case (e.g., [error]). The time stamp, if included, will be identical for all these lines. This makes log messages easily grep-able, countable, etc.

Logging a message provides a structured way to format relevant additional information. The syntax is an extension of slog, allowing for nested structures. However unlike in slog, the output format is fixed. For example:

```ignore

[macro_use]

extern crate loggy;

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

Will generate the message:

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

Named scopes

By default, log messages are annotated with the name of the module generating them. To better identify specific processing stages and/or tasks, it is common to replace this by an explicit scope name; note this only applies to the current thread. Scopes can be established in three different ways:

```ignore

[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 {
}

} ```

Logging levels

Log levels are given stronger semantics:

```ignore

[macro_use]

extern crate loggy;

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

fn main() { // ... somecondition::setisanerror(basedonthecommandlineflags); // ... loggy::withscope("scope name", || { // Errors must be inside some scope. // ... if testforsomecondition { note!(somecondition::isanerror(), "some condition"); // Will be an error or a warning depending on the command line flag. } // ... }); // ... } ```

You can also use loggy::log!(level, ...) to specify the level of a message. Note that if this level is Error, the message can only be generated inside a named scope. There is no way to force a panic! this way (use note! instead).

Testing

Testing logging faces the following inconvenient truths:

Therefore, the following following assertions take a global lock to ensure messages from different tests do not interfere with each other. This has several implications:

All that said, testing the actual log messages generated by some code is a convenient and surprisingly powerful way of ensuring it behaves as expected. It also ensures that the log messages contain the expected data, something that is otherwise difficult to verify. The following assertions are available to support this:

These are intentionally not attribute macros attached to the test (like the standard #[should_panic]. This allows the expected texts to be dynamically formatted.

Setting the LOGGY_MIRROR_TO_STDERR environment variable to any non-empty value will cause all messages to be emitted to the standard error stream, together with any debug messages, even in tests. This places the debug messages in the context of the other messages, helping in debugging of tests.

Ideally, the standard error content is only reported for failing tests (this includes any debug messages). In practice, the rust mechanism for capturing the standard error does not work properly when the test spawns new threads, so any debug messages emitted from worker threads will be visible even for passing tests. This isn't a show stopper given such messages and the LOGGY_MIRROR_TO_STDERR variable are only used when actively debugging an issue.

License

loggy is licensed under the MIT License.