Logging and errors¶
The core crate has the rendering half of upstream's logging and traceback support:
LogRecordandLogRenderlay out log lines the way upstream'sRichHandlerandConsole.logdo: time, level, message, source path.Tracebackrenders 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:
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",
));
}
LogLevelhasTrace,Debug,Info,WarnandError. They print as Python'sloggingnames (WARNING, notWARN) in thelogging.level.<name>theme styles..time(s)takes the time already formatted. The core has no clock or date dependency; format withchrono,timeorstd::timeas 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);
}
}
| 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));
}
}
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",
));
}
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 aLogRecord(or aLogRenderrow) instead. Upstream's automatic caller path (log_locals,_stack_offset) has no Rust equivalent; passfile!()andline!()yourself.Traceback's frames, source excerpts,show_locals,suppress,width,themeandinstall()— see divergence #19.
See also¶
- Tables —
LogRenderreturns a grid table - Text and style — the
log.*andlogging.level.*styles - API:
log_render·Traceback·rich_ext::RichHandler