From 3f2f2cd6abf67a04809ff314025a462a3c2e2446 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Wed, 19 Jun 2024 17:29:19 -0700 Subject: [PATCH] feat(docs): add tracing example (#1192) Add an example that demonstrates logging to a file for: ```shell cargo run --example tracing RUST_LOG=trace cargo run --example=tracing cat tracing.log ``` ![Made with VHS](https://vhs.charm.sh/vhs-21jgJCedh2YnFDONw0JW7l.gif) --- Cargo.toml | 8 ++ examples/tracing.rs | 154 ++++++++++++++++++++++++++++++++++++++ examples/vhs/tracing.tape | 12 +++ 3 files changed, 174 insertions(+) create mode 100644 examples/tracing.rs create mode 100644 examples/vhs/tracing.tape diff --git a/Cargo.toml b/Cargo.toml index 9d43b8ac..37c8fb62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,9 @@ rand = "0.8.5" rand_chacha = "0.3.1" rstest = "0.21.0" serde_json = "1.0.109" +tracing = "0.1.40" +tracing-appender = "0.2.3" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } [lints.rust] unsafe_code = "forbid" @@ -353,6 +356,11 @@ name = "tabs" required-features = ["crossterm"] doc-scrape-examples = true +[[example]] +name = "tracing" +required-features = ["crossterm"] +doc-scrape-examples = true + [[example]] name = "user_input" required-features = ["crossterm"] diff --git a/examples/tracing.rs b/examples/tracing.rs new file mode 100644 index 00000000..ea6e6abf --- /dev/null +++ b/examples/tracing.rs @@ -0,0 +1,154 @@ +//! # [Ratatui] Tracing example +//! +//! The latest version of this example is available in the [examples] folder in the repository. +//! +//! Please note that the examples are designed to be run against the `main` branch of the Github +//! repository. This means that you may not be able to compile with the latest release version on +//! crates.io, or the one that you have installed locally. +//! +//! See the [examples readme] for more information on finding examples that match the version of the +//! library you are using. +//! +//! [Ratatui]: https://github.com/ratatui-org/ratatui +//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples +//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md + +// A simple example demonstrating how to use the [tracing] with Ratatui to log to a file. +// +// This example demonstrates how to use the [tracing] crate with Ratatui to log to a file. The +// example sets up a simple logger that logs to a file named `tracing.log` in the current directory. +// +// Run the example with `cargo run --example tracing` and then view the `tracing.log` file to see +// the logs. To see more logs, you can run the example with `RUST_LOG=tracing=debug cargo run +// --example` +// +// For a helpful widget that handles logging, see the [tui-logger] crate. +// +// [tracing]: https://crates.io/crates/tracing +// [tui-logger]: https://crates.io/crates/tui-logger + +use std::{fs::File, io::stdout, panic, time::Duration}; + +use color_eyre::{ + config::HookBuilder, + eyre::{self, Context}, + Result, +}; +use crossterm::{ + event::{self, Event, KeyCode}, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + ExecutableCommand, +}; +use ratatui::{ + backend::{Backend, CrosstermBackend}, + terminal::Terminal, + widgets::{Block, Paragraph}, +}; +use tracing::{debug, info, instrument, trace, Level}; +use tracing_appender::{non_blocking, non_blocking::WorkerGuard}; +use tracing_subscriber::EnvFilter; + +fn main() -> Result<()> { + init_error_hooks()?; + let _guard = init_tracing()?; + info!("Starting tracing example"); + + let mut terminal = init_terminal()?; + let mut events = vec![]; // a buffer to store the recent events to display in the UI + while !should_exit(&events) { + handle_events(&mut events)?; + terminal.draw(|frame| ui(frame, &events))?; + } + restore_terminal()?; + info!("Exiting tracing example"); + println!("See the tracing.log file for the logs"); + Ok(()) +} + +fn should_exit(events: &[Event]) -> bool { + events + .iter() + .any(|event| matches!(event, Event::Key(key) if key.code == KeyCode::Char('q'))) +} + +/// Handle events and insert them into the events vector keeping only the last 10 events +#[instrument(skip(events))] +fn handle_events(events: &mut Vec) -> Result<()> { + // Render the UI at least once every 100ms + if event::poll(Duration::from_millis(100))? { + let event = event::read()?; + debug!(?event); + events.insert(0, event); + } + events.truncate(10); + Ok(()) +} + +#[instrument(skip_all)] +fn ui(frame: &mut ratatui::Frame, events: &[Event]) { + // To view this event, run the example with `RUST_LOG=tracing=debug cargo run --example tracing` + trace!(frame_count = frame.count(), event_count = events.len()); + let area = frame.size(); + let events = events.iter().map(|e| format!("{e:?}")).collect::>(); + let paragraph = Paragraph::new(events.join("\n")) + .block(Block::bordered().title("Tracing example. Press 'q' to quit.")); + frame.render_widget(paragraph, area); +} + +/// Initialize the tracing subscriber to log to a file +/// +/// This function initializes the tracing subscriber to log to a file named `tracing.log` in the +/// current directory. The function returns a [`WorkerGuard`] that must be kept alive for the +/// duration of the program to ensure that logs are flushed to the file on shutdown. The logs are +/// written in a non-blocking fashion to ensure that the logs do not block the main thread. +fn init_tracing() -> Result { + let file = File::create("tracing.log").wrap_err("failed to create tracing.log")?; + let (non_blocking, guard) = non_blocking(file); + + // By default, the subscriber is configured to log all events with a level of `DEBUG` or higher, + // but this can be changed by setting the `RUST_LOG` environment variable. + let env_filter = EnvFilter::builder() + .with_default_directive(Level::DEBUG.into()) + .from_env_lossy(); + + tracing_subscriber::fmt() + .with_writer(non_blocking) + .with_env_filter(env_filter) + .init(); + Ok(guard) +} + +/// Initialize the error hooks to ensure that the terminal is restored to a sane state before +/// exiting +fn init_error_hooks() -> Result<()> { + let (panic, error) = HookBuilder::default().into_hooks(); + let panic = panic.into_panic_hook(); + let error = error.into_eyre_hook(); + eyre::set_hook(Box::new(move |e| { + let _ = restore_terminal(); + error(e) + }))?; + panic::set_hook(Box::new(move |info| { + let _ = restore_terminal(); + panic(info); + })); + Ok(()) +} + +#[instrument] +fn init_terminal() -> Result> { + enable_raw_mode()?; + stdout().execute(EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout()); + let terminal = Terminal::new(backend)?; + debug!("terminal initialized"); + Ok(terminal) +} + +#[instrument] +fn restore_terminal() -> Result<()> { + disable_raw_mode()?; + stdout().execute(LeaveAlternateScreen)?; + debug!("terminal restored"); + Ok(()) +} diff --git a/examples/vhs/tracing.tape b/examples/vhs/tracing.tape new file mode 100644 index 00000000..a1430c6e --- /dev/null +++ b/examples/vhs/tracing.tape @@ -0,0 +1,12 @@ +# This is a vhs script. See https://github.com/charmbracelet/vhs for more info. +# To run this script, install vhs and run `vhs ./examples/barchart.tape` +Output "target/tracing.gif" +Set Theme "Aardvark Blue" +Set Width 1200 +Set Height 800 +Type "RUST_LOG=trace cargo run --example=tracing" Enter +Sleep 1s +Type @100ms "jjjjq" +Sleep 1s +Type "cat tracing.log" Enter +Sleep 10s