21 unstable releases (3 breaking)

0.4.0 Jan 29, 2024
0.3.10 Jan 21, 2024
0.2.0 Jan 13, 2024
0.1.8 Jan 13, 2024
0.1.3 May 30, 2023

#119 in Debugging

Download history 44/week @ 2023-12-24 14/week @ 2023-12-31 60/week @ 2024-01-07 108/week @ 2024-01-14 20/week @ 2024-01-21 37/week @ 2024-01-28 34/week @ 2024-02-04 21/week @ 2024-02-11 33/week @ 2024-02-18 113/week @ 2024-02-25 40/week @ 2024-03-03 87/week @ 2024-03-10 88/week @ 2024-03-17 135/week @ 2024-03-24 197/week @ 2024-03-31 68/week @ 2024-04-07

494 downloads per month
Used in 22 crates (4 directly)

ISC license

34KB
785 lines

Logging and error management are tightly coupled. When both logging and creating errors, you need to provide context (what the program was doing, with what, where, when).

This crate handles provides interconnected, structured logging and error handling. The logger object manages this state and provides it when logging and creating errors. Rather than add context while unwinding from an error, the context is established beforehand at various scopes by refining old logger objects with additional attributes (though context can be added afterwards as well).

use loga::{
    StandardFlags,
    ea,
    ResultContext,
    ErrContext,
};

const WARN: StandardFlags = StandardFlags::Warning;
const INFO: StandardFlags = StandardFlags::Info;

fn main1() -> Result<(), loga::Error> {
    // All errors stacked from this will have "system = main"
    let log = &loga::Log::<StandardFlags>::new().with_flags(&[WARN, INFO]).fork(ea!(system = "main"));

    // Convert the error result to `loga::Error`, add all the logger's attributes, add
    // a message, and add additional attributes.
    let res =
        http_req(
            "https://example.org",
        ).stack_context_with(log, "Example.org is down", ea!(process = "get_weather"))?;
    match launch_satellite(res.body) {
        Ok(_) => (),
        Err(e) => {
            let e = e.stack_context(log, "Failed to launch satellite");
            if let Err(e2) = shutdown_server() {
                // Attach incidental errors
                return Err(e.also(e2.into()));
            }
            return Err(e);
        },
    }
    if res.code == 295 {
        return Err(loga::err("Invalid response"));
    }
    log.log(INFO, "Obtained weather");
    return Ok(());
}

fn main() {
    match main1() {
        Ok(_) => (),
        Err(e) => loga::fatal(e),
    }
}

Goals

The goals fo this crate are ease of use and expressivitiy, both for developing code and people reading the errors/log messages.

Optimization will be considered as necessary as far as it doesn't impact ease of use and expressivity.

Event structure

Events (errors and log messages) have a tree structure, with the following dimensions:

  • Attributes at the current level of context
  • One or more errors that this error adds context to (causes)
  • One or more errors that occurred while trying to handle this error (incidental)

The errors are intended only for human consumption. Any information that may need to be handled programmatically should be in a non-error return.

Flags

Unlike traditional loggers which have a linear scale of levels, sometimes multiplied by different "loggers" or additional dimensions, this library uses a set of additive flags. If the flag in the log call is set in the flags provided when constructing the logger then the log message will be rendered, otherwise it will be skipped.

To keep things simple you can use StandardFlags with Debug, Info, Warning, etc. levels.

If you have multiple subsystems each with their own levels, you could for instance have GcDebug, GcInfo, GcWarn, HttpDebug, HttpInfo, etc. This is hopefully a simple mechanism for allowing pinpoint log control.

Flags only affect logging, not errors generation and manipulation.

Usage tips

You may want to alias the generic Log with the flags type of your choice.

In non-logging functions or objects that may be shared in multiple contexts, rather than receive a logger from the caller it may be simpler to start a new (blank) Log tree internally, or just use .context. The caller can later add attributes using .log_context.

When you raise an error, you typically only want to add context from a log only once per independent Log tree, at the deepest level of specificity (the logger closest to where you raise the error). This happens when you do log.new_err or call .log_context on an error from outside to bring it into the logging system. At all other locations you can use .context. This will avoid unnecessarily duplicating attributes.

Notes

Currently logging only happens to stderr. Adding more log destinations and formats in the future would be nice.

Dependencies

~3–14MB
~154K SLoC