Logging¶
Three pieces turn log records into rich output:
StructuredEvent(rich_ext::event): one log record as data: a message, typed fields and context (time, severity, target, source location). It renders on its own as a compact or expanded line.RichHandler: lays events out like Python rich'slogging.RichHandler: time, level, message, and the source file on the right.- Adapters (features
logandtracing):LogAdapterandEventLayerturnlogrecords andtracingevents intoStructuredEvents and hand them to anEventSink, such as aRichHandler.
rs-rich-ext = { version = "…", features = ["log"] } # the log facade
rs-rich-ext = { version = "…", features = ["tracing"] } # tracing + tracing-subscriber
StructuredEvent and RichHandler need no feature.
log through RichHandler¶
Create the handler, then install a LogAdapter that sends records to it.
Nothing is installed for you: your application decides which logger is
global.
/// Route the `log` facade to `sink`. The application owns this decision:
/// constructing a `LogAdapter` never installs anything by itself.
fn install_logger(sink: Arc<dyn EventSink>) {
let logger = Box::leak(Box::new(LogAdapter::new(sink, log::LevelFilter::Debug)));
log::set_logger(logger).expect("no other logger is installed");
log::set_max_level(log::LevelFilter::Debug);
}
// `time_format` pins the clock so this output is reproducible; the
// default is the current UTC time as `[HH:MM:SS]`.
let handler = RichHandler::new(Console::new()).time_format(|| "[12:00:00]".into());
let handler = Arc::new(handler);
install_logger(handler.clone());
log::info!("Server starting on 127.0.0.1:8080");
log::debug!("GET /index.html 200 in 0.4ms");
log::warn!("cache miss for key {:?}", "user:42");
log::error!("POST /upload failed: disk full");
Reading a line from left to right:
- Time: blanked when it repeats the previous line's time.
- Level: padded to 8 cells in
logging.level.<name>. Python's names are used (WARNING,CRITICAL);log::Level::Traceshows asTRACE. - Message: run through
ReprHighlighter(numbers, strings, paths, IPs), with HTTP methods (GET,POST, …) inlogging.keyword. - Path: the file name and line, linked to the full path.
LogAdapter::new(sink, level) drops records above level, and so should
log::set_max_level. Leaking the adapter with Box::leak is the usual way to
get the &'static logger log::set_logger wants; log has no other way to
uninstall a logger anyway.
tracing through RichHandler¶
EventLayer is a tracing_subscriber::Layer. Compose it with a subscriber you
own, globally or for a scope:
use tracing_subscriber::prelude::*;
let subscriber = tracing_subscriber::registry().with(EventLayer::new(handler.clone()));
tracing::subscriber::with_default(subscriber, || {
tracing_lines();
});
tracing::info!(port = 8080u16, tls = true, "listening");
tracing::warn!(retries = 3i64, elapsed_ms = 1250.5, "upstream slow");
tracing::error!(path = "/upload", "request failed");
- The
messagefield becomes the message; every other field is appended askey=value. - Integers keep their type, including unsigned and 128-bit values; floats and
bools too. Fields recorded with
Debug(?value) become strings.
Spans¶
Every event carries the spans it happened in, outermost first, with their
fields as last recorded (a later span.record(...) updates them). The layer
keeps them in the subscriber's registry, so the subscriber must implement
LookupSpan, as tracing_subscriber::registry() and fmt() do.
let request = tracing::info_span!("request", method = "GET", path = "/items");
let _request = request.enter();
tracing::info!("authorized");
{
let _query = tracing::debug_span!("query", table = "items").entered();
tracing::info!(rows = 42u64, "fetched");
}
tracing::info!(status = 200u16, "done");
By default RichHandler puts the span chain before the message:
With span_open(true) and span_close(true) the layer also reports each span
opening, and closing with how long it was open. SpanView::Tree then draws the
spans as branches, with each event indented under its span:
use tracing_subscriber::prelude::*;
// Report spans opening and closing, and draw them as a tree.
let layer = EventLayer::new(recorder.clone())
.span_open(true)
.span_close(true);
let subscriber = tracing_subscriber::registry().with(layer);
tracing::subscriber::with_default(subscriber, span_lines);
let tree = RichHandler::new(Console::new())
.span_view(SpanView::Tree)
.show_time(false);
span_view |
Shows |
|---|---|
SpanView::Inline (default) |
outer{a=1}:inner: message; open and close events as name{…} opened and name{…} closed 1.20ms |
SpanView::Tree |
a │ guide per span, ┌ name field=value on open, └ name 1.20ms on close; ASCII consoles get \|, + and ` |
SpanView::Hidden |
the message alone |
Any EventSink receives the same span data, through
StructuredEvent::span_context() and, for open and close events,
span_marker().
Links and live displays¶
hyperlinker(Hyperlinker) links the path column through a
Hyperlinker instead of a bare file:// URL.
Its editor template, and a base directory for the relative paths tracing
reports, then apply:
use rich_ext::hyperlink::Hyperlinker;
let _editor = RichHandler::new(Console::new()).hyperlinker(
Hyperlinker::new()
.base_dir(env!("CARGO_MANIFEST_DIR")) // tracing reports paths relative to the crate
.editor("vscode://file/{path}:{line}"),
);
live(coordinator) prints through a
LiveCoordinator instead of the console. Each log line
goes above the live regions, which are repainted below it, so logging never
tears a progress display:
use rich_ext::capabilities::Capabilities;
use rich_ext::live::LiveCoordinator;
use rich_ext::target::{RenderTarget, TargetKind};
let capabilities = Capabilities::system().to_target_capabilities();
let target = RenderTarget::new(
TargetKind::Terminal,
capabilities,
rich::Theme::default_theme(),
);
let live = Arc::new(Mutex::new(LiveCoordinator::new(
std::io::stdout(),
target.clone(),
)));
let progress = live
.lock()
.unwrap()
.add(vec![rich::Segment::new("uploading… 40%", None)])
.expect("add a region");
live.lock().unwrap().refresh().expect("paint");
// Log lines print above the region, which is repainted below them.
let handler = RichHandler::new(target.console()).live(live.clone());
let subscriber = tracing_subscriber::registry().with(EventLayer::new(Arc::new(handler)));
tracing::subscriber::with_default(subscriber, || {
tracing::info!(chunk = 4u64, "uploaded");
});
live.lock().unwrap().remove(progress).expect("remove");
live.lock().unwrap().finish().expect("finish");
A message containing control characters is still printed, with the controls
shown as inert symbols (␛), rather than dropped.
Render with a console as wide as the coordinator's target, as
target.console() is; longer lines fold.
Handler options¶
RichHandler mirrors upstream's constructor arguments as builder methods:
let handler = RichHandler::new(Console::new())
.show_time(false)
.show_path(false)
.markup(true) // messages are console markup, not literal text
.keywords(["DEPLOY", "ROLLBACK"]); // replaces the HTTP-method list
let event =
StructuredEvent::new(Message::Literal("DEPLOY [bold]v2.4.1[/] to 3 hosts".into()))
.context(EventContext {
severity: Some(Severity::Warn),
..Default::default()
});
console.print(&handler.render(&event));
| Builder | Default | Upstream |
|---|---|---|
show_time(bool), show_level(bool), show_path(bool) |
all on | show_time, show_level, show_path |
omit_repeated_times(bool) |
on | omit_repeated_times |
level_width(Option<usize>) |
Some(8) |
fixed at 8 upstream |
markup(bool) |
off | markup: parse messages as console markup |
highlighter(Option<Box<dyn Highlighter + Send + Sync>>) |
ReprHighlighter |
highlighter; None disables it |
keywords(words) |
HTTP methods | keywords |
enable_link_path(bool) |
on | enable_link_path |
time_format(closure) |
UTC [HH:MM:SS] |
log_time_format |
span_view(SpanView) |
Inline |
none: upstream has no spans (Spans) |
hyperlinker(Hyperlinker) |
none (bare file://) |
none (Links) |
live(coordinator) |
the console | none (Live displays) |
Any highlighter works, including a Hyperlinker
that makes issue references, paths and URLs in messages clickable:
use rich_ext::hyperlink::Hyperlinker;
// Any `Highlighter + Send + Sync` replaces the default ReprHighlighter.
let linker = Hyperlinker::new().repository("https://github.com/acme/app");
let handler = RichHandler::new(Console::new())
.show_time(false)
.highlighter(Some(Box::new(linker)));
let event = StructuredEvent::new(Message::Literal(
"retrying after #42; see src/net.rs:88 and https://acme.dev/status".into(),
));
console.print(&handler.render(&event));
Differences from Python rich
- Time is UTC by default: local time would need a time-zone
dependency. Supply
time_formatfor local time or another format; an event's owncontext.timestampalways wins. rich_tracebackshas no counterpart. Render errors as diagnostics instead.
Beyond the facades, a handler can be used directly:
handler.emit_event(&event) prints one event to the handler's console, and
handler.render(&event) returns the laid-out row as a Table for you to print
elsewhere.
Your own sink¶
The adapters talk to an EventSink, a one-method trait. Implement it to send
events anywhere: a file, a channel, a test buffer.
/// Keeps events instead of printing them: handy in tests.
#[derive(Default)]
struct Recorder {
events: Mutex<Vec<StructuredEvent>>,
}
impl EventSink for Recorder {
fn emit(&self, event: StructuredEvent) -> std::io::Result<()> {
self.events.lock().unwrap().push(event);
Ok(())
}
}
Pass it as Arc::new(Recorder::default()) to LogAdapter::new or
EventLayer::new. The screenshots on this page were made that way: records go
to a Recorder, and RichHandler::render lays them out on an SVG console.
Errors returned by emit are dropped, never logged: logging a logging failure
could recurse forever.
Structured events¶
A StructuredEvent is plain data that renders itself. The adapters build them
for you, but you can make them directly:
let event = StructuredEvent::new(Message::Literal("request finished".into()))
.field("status", Value::Unsigned(200))
.field("route", Value::String("/api/items".into()))
.field("elapsed_ms", Value::Float(12.5))
.field(
"tags",
Value::List(vec![
Value::String("cache".into()),
Value::String("gzip".into()),
]),
)
.context(EventContext {
timestamp: Some("2026-09-23T12:00:00Z".into()),
severity: Some(Severity::Info),
target: Some("http".into()),
source: Some(SourceLocation {
path: "src/server.rs".into(),
line: 88,
column: None,
}),
..Default::default()
});
console.print(&event); // compact: one line, fields as key=value
console.print(
&event
.clone()
.field_order(vec!["route".into()]) // this field first
.hide_fields(vec!["tags".into()])
.view(EventView::Expanded), // one field per line
);
- Message:
Message::Literal(text)prints as written;Message::Markup(text)is parsed as console markup. Literal is the safe choice for anything user-supplied. - Fields:
.field(key, Value)appends a field; setting an existing key replaces its value in place.Valuecovers null, bool, signed and unsigned integers (64 and 128 bits), floats, strings, lists and maps. - Order: fields render in insertion order;
field_order(keys)moves the named ones first andhide_fields(keys)leaves some out. - Context:
EventContextholdstimestamp,severity,target,module,source,thread,taskandcorrelation_id. All optional, and nothing is filled in implicitly: no clock, thread or environment reads. - View:
Compact(default) puts fields on the message line;Expandedgives each field its own line and pretty-prints lists and maps. - Overflow:
.overflow(OverflowPolicy::…)picks how long lines fit (defaultFold); see bounded layout.
Theme keys: event.message, event.field (default cyan), event.value and
event.severity.<trace|debug|info|warn|error|fatal>.
Attaching diagnostics¶
An event can carry any number of diagnostics, rendered under its line:
use rich_ext::diagnostic::{Diagnostic, Location};
let event = StructuredEvent::new(Message::Markup("[bold]config reload[/] rejected".into()))
.context(EventContext {
severity: Some(Severity::Error),
..Default::default()
})
.diagnostic(
Diagnostic::error("unknown key `prot`")
.code("CFG002")
.location(Location::new("app.toml", Some(4), Some(1))),
);
console.print(&event);
Run the example¶
See also¶
- Diagnostics: errors, snippets and stack traces.
- Extensions:
Hyperlinkerand the theme keys. - Macros:
rich_trace!for quick, non-logging trace lines. - The
log_adapter,tracing_adapterandrich_handlerexamples incrates/rich-ext/examples. - API:
RichHandler,event,adapters,LogRender.