Skip to content

Logging and errors

The core crate has the rendering half of upstream's logging and traceback support:

  • LogRecord and LogRender lay out log lines the way upstream's RichHandler and Console.log do: time, level, message, source path.
  • Traceback renders a Rust error and its chain of causes in a panel.

Hooking these into the log or tracing ecosystems is not upstream behaviour, so it lives in rs-rich-ext: RichHandler.

The examples use these imports:

use rich::{level_text, Console, LogLevel, LogRecord, LogRender, Text, Traceback};

Log records

A LogRecord is one formatted log line: a level, a message, and optionally a time and a source location.

fn records(console: &Console) {
    // One record at a time; the time is whatever string you format.
    console.print(&LogRecord::new(LogLevel::Info, "Server starting").time("[12:00:01]"));
    console.print(
        &LogRecord::new(LogLevel::Warn, "Config file not found, using defaults")
            .time("[12:00:01]")
            .path("main.rs")
            .line(42),
    );
    console.print(
        &LogRecord::new(
            LogLevel::Error,
            "Could not bind to port 80: permission denied",
        )
        .time("[12:00:02]")
        .path("net.rs")
        .line(118),
    );
    console.print(&LogRecord::new(
        LogLevel::Debug,
        "no time column when there is no time",
    ));
}

Log records at info, warning, error and debug level, with times and paths

  • LogLevel has Trace, Debug, Info, Warn and Error. They print as Python's logging names (WARNING, not WARN) in the logging.level.<name> theme styles.
  • .time(s) takes the time already formatted. The core has no clock or date dependency; format with chrono, time or std::time as you like.
  • .path(file) and .line(n) fill the right-hand column.
  • The message is plain text, not markup, and wraps within its column.

LogRender

LogRecord is a convenience over LogRender, the port of upstream's _log_render.LogRender. Use LogRender directly for a stream of records: it remembers the last time it printed and blanks a repeated one, as upstream does.

fn log_render(console: &Console) {
    // Share one LogRender across a stream so a repeated time is blanked.
    let render = LogRender::new().show_level(true).level_width(Some(8));

    let lines = [
        (
            "[12:00:01]",
            "INFO",
            "Request 1 served in 3 ms",
            "http.rs",
            20,
        ),
        (
            "[12:00:01]",
            "INFO",
            "Request 2 served in 5 ms",
            "http.rs",
            20,
        ),
        ("[12:00:02]", "CRITICAL", "Worker 3 exited", "pool.rs", 97),
    ];
    for (time, level, message, path, line) in lines {
        let table = render.render(
            console,
            Text::new(message),
            Some(Text::new(time)),
            level_text(level), // styled with logging.level.<name>
            Some(path),
            Some(line),
            None, // or Some("/abs/path/http.rs") to hyperlink the path
        );
        console.print(&table);
    }
}

A log stream where the repeated time is blanked

Option Default Effect
show_time(bool) true the time column
show_level(bool) false the level column
show_path(bool) true the path column
omit_repeated_times(bool) true blank a time equal to the previous one
level_width(Option<usize>) Some(8) fixed level width, or None to fit

render(console, message, time, level, path, line, link_path) returns a Table (a grid) for one record; print it. The message is a Text, so it can be styled or built from markup. level_text(name) styles any level name the way upstream's RichHandler does, including names the enum lacks (CRITICAL). Passing link_path makes the path an OSC 8 file:// link.

Traceback

Upstream's Traceback renders a Python stack trace with source code. Rust errors carry no frames, so this port's Traceback renders what a Rust error does have: its message and its source() chain.

#[derive(Debug)]
struct ConfigError {
    path: String,
    source: std::io::Error,
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "could not load config from {}", self.path)
    }
}

impl std::error::Error for ConfigError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&self.source)
    }
}

fn load_config() -> Result<String, ConfigError> {
    Err(ConfigError {
        path: "/etc/app/config.toml".into(),
        source: std::io::Error::new(std::io::ErrorKind::NotFound, "No such file or directory"),
    })
}

fn traceback(console: &Console) {
    if let Err(error) = load_config() {
        // The message, then each `source()` as a "Caused by:" line.
        console.print(&Traceback::new(&error));
    }
}

An error with a Caused by chain in a red panel

Traceback::new(&error) takes any &dyn Error — including errors from anyhow, thiserror or std::io. Traceback::from_message(s) takes a plain string:

fn panic_message(console: &Console) {
    // Any string works, e.g. a message captured by a panic hook.
    console.print(&Traceback::from_message(
        "index out of bounds: the len is 3 but the index is 7",
    ));
}

A plain message in a traceback panel

Panics

A panic hook can render panics the same way:

fn panic_hook() {
    std::panic::set_hook(Box::new(|info| {
        let message = info
            .payload()
            .downcast_ref::<&str>()
            .map(|s| s.to_string())
            .or_else(|| info.payload().downcast_ref::<String>().cloned())
            .unwrap_or_else(|| "panic".to_string());
        let location = info
            .location()
            .map(|l| format!(" at {}:{}", l.file(), l.line()))
            .unwrap_or_default();
        Console::new().print(&Traceback::from_message(format!("{message}{location}")));
    }));
}

For stack frames, capture a std::backtrace::Backtrace in the hook and print it after the panel. rs-rich-ext can parse and render Rust, Python and Java stack traces from text: stacktrace.

Not yet ported

  • Console.log() — print a LogRecord (or a LogRender row) instead. Upstream's automatic caller path (log_locals, _stack_offset) has no Rust equivalent; pass file!() and line!() yourself.
  • Traceback's frames, source excerpts, show_locals, suppress, width, theme and install() — see divergence #19.

See also