From 4c0be32bf555c54ce8e91fbaf801614001c1ef5b Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Tue, 23 Mar 2021 18:55:51 +0200 Subject: [PATCH] Implement Text User Interface to show log streams in multiple "windows" Switch to 'slog' crate for logging, it gives us the flexibility that we need for the widget to scroll logs on TUI --- Cargo.lock | 197 ++++++++++++++++++++++++++++++++++------ Cargo.toml | 10 +- src/bin/pageserver.rs | 75 +++++++++++++-- src/control_plane.rs | 4 +- src/lib.rs | 4 + src/page_cache.rs | 1 - src/page_service.rs | 4 +- src/tui.rs | 198 ++++++++++++++++++++++++++++++++++++++++ src/tui_event.rs | 97 ++++++++++++++++++++ src/tui_logger.rs | 206 ++++++++++++++++++++++++++++++++++++++++++ src/walreceiver.rs | 2 +- src/walredo.rs | 2 + 12 files changed, 758 insertions(+), 42 deletions(-) create mode 100644 src/tui.rs create mode 100644 src/tui_event.rs create mode 100644 src/tui_logger.rs diff --git a/Cargo.lock b/Cargo.lock index aef4a83785..50f6282716 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "arc-swap" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d7d63395147b81a9e570bcc6243aaf71c017bd666d4909cfef0085bdda8d73" + [[package]] name = "arrayref" version = "0.3.6" @@ -293,6 +299,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cc" version = "1.0.67" @@ -435,6 +447,16 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.5" @@ -442,7 +464,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" dependencies = [ "libc", - "redox_users", + "redox_users 0.3.5", + "winapi", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.0", "winapi", ] @@ -1018,6 +1051,12 @@ dependencies = [ "libc", ] +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + [[package]] name = "once_cell" version = "1.7.2" @@ -1079,6 +1118,7 @@ version = "0.1.0" dependencies = [ "byteorder", "bytes", + "chrono", "clap", "crossbeam-channel", "futures", @@ -1089,10 +1129,16 @@ dependencies = [ "rand 0.8.3", "regex", "rust-s3", - "stderrlog", + "slog", + "slog-async", + "slog-scope", + "slog-stdlog", + "slog-term", + "termion", "tokio", "tokio-postgres", "tokio-stream", + "tui", ] [[package]] @@ -1373,6 +1419,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_termios" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" +dependencies = [ + "redox_syscall 0.2.5", +] + [[package]] name = "redox_users" version = "0.3.5" @@ -1384,6 +1439,16 @@ dependencies = [ "rust-argon2", ] +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom 0.2.2", + "redox_syscall 0.2.5", +] + [[package]] name = "regex" version = "1.4.5" @@ -1498,6 +1563,12 @@ dependencies = [ "url", ] +[[package]] +name = "rustversion" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5d2a036dc6d2d8fd16fde3498b04306e29bd193bf306a57427019b823d5acd" + [[package]] name = "ryu" version = "1.0.5" @@ -1635,6 +1706,59 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +[[package]] +name = "slog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" + +[[package]] +name = "slog-async" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60813879f820c85dbc4eabf3269befe374591289019775898d56a81a804fbdc" +dependencies = [ + "crossbeam-channel", + "slog", + "take_mut", + "thread_local", +] + +[[package]] +name = "slog-scope" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f95a4b4c3274cd2869549da82b57ccc930859bdbf5bcea0424bc5f140b3c786" +dependencies = [ + "arc-swap", + "lazy_static", + "slog", +] + +[[package]] +name = "slog-stdlog" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8228ab7302adbf4fcb37e66f3cda78003feb521e7fd9e3847ec117a7784d0f5a" +dependencies = [ + "log", + "slog", + "slog-scope", +] + +[[package]] +name = "slog-term" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95c1e7e5aab61ced6006149ea772770b84a0d16ce0f7885def313e4829946d76" +dependencies = [ + "atty", + "chrono", + "slog", + "term", + "thread_local", +] + [[package]] name = "smallvec" version = "1.6.1" @@ -1662,19 +1786,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "stderrlog" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a53e2eff3e94a019afa6265e8ee04cb05b9d33fe9f5078b14e4e391d155a38" -dependencies = [ - "atty", - "chrono", - "log", - "termcolor", - "thread_local", -] - [[package]] name = "stringprep" version = "0.1.2" @@ -1708,6 +1819,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + [[package]] name = "tempfile" version = "3.2.0" @@ -1723,12 +1840,26 @@ dependencies = [ ] [[package]] -name = "termcolor" -version = "1.1.2" +name = "term" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" dependencies = [ - "winapi-util", + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "termion" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +dependencies = [ + "libc", + "numtoa", + "redox_syscall 0.2.5", + "redox_termios", ] [[package]] @@ -1914,6 +2045,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "tui" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ced152a8e9295a5b168adc254074525c17ac4a83c90b2716274cc38118bddc9" +dependencies = [ + "bitflags", + "cassowary", + "termion", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "typenum" version = "1.13.0" @@ -1938,6 +2082,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + [[package]] name = "unicode-width" version = "0.1.8" @@ -2132,15 +2282,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 9dd4a9b37b..f4d02d2625 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chrono = "0.4.19" crossbeam-channel = "0.5.0" rand = "0.8.3" regex = "1.4.5" @@ -14,9 +15,16 @@ bytes = "1.0.1" byteorder = "1.4.3" futures = "0.3.13" lazy_static = "1.4.0" +slog-stdlog = "4.1.0" +slog-async = "2.6.0" +slog-scope = "4.4.0" +slog-term = "2.8.0" +slog = "2.7.0" log = "0.4.14" -stderrlog = "0.5.1" clap = "2.33.0" +termion = "1.5.6" +tui = "0.14.0" + rust-s3 = { git = "https://github.com/hlinnaka/rust-s3", features = ["no-verify-ssl"] } tokio = { version = "1.3.0", features = ["full"] } tokio-stream = { version = "0.1.4" } diff --git a/src/bin/pageserver.rs b/src/bin/pageserver.rs index 6135543f55..f5f80e856a 100644 --- a/src/bin/pageserver.rs +++ b/src/bin/pageserver.rs @@ -8,8 +8,14 @@ use std::{net::IpAddr, thread}; use clap::{App, Arg}; +use slog; +use slog_stdlog; +use slog_scope; +use slog::Drain; + use pageserver::page_service; use pageserver::restore_s3; +use pageserver::tui; use pageserver::walreceiver; use pageserver::walredo; use pageserver::PageServerConf; @@ -27,6 +33,11 @@ fn main() -> Result<(), Error> { .long("wal-producer") .takes_value(true) .help("connect to the WAL sender (postgres or wal_acceptor) on ip:port (default: 127.0.0.1:65432)")) + .arg(Arg::with_name("interactive") + .short("i") + .long("interactive") + .takes_value(false) + .help("Interactive mode")) .arg(Arg::with_name("daemonize") .short("d") .long("daemonize") @@ -41,6 +52,7 @@ fn main() -> Result<(), Error> { let mut conf = PageServerConf { data_dir: String::from("."), daemonize: false, + interactive: false, wal_producer_ip: "127.0.0.1".parse::().unwrap(), wal_producer_port: 65432, skip_recovery: false, @@ -54,6 +66,10 @@ fn main() -> Result<(), Error> { conf.daemonize = true; } + if arg_matches.is_present("interactive") { + conf.interactive = true; + } + if arg_matches.is_present("skip_recovery") { conf.skip_recovery = true; } @@ -71,11 +87,30 @@ fn start_pageserver(conf: PageServerConf) -> Result<(), Error> { let mut threads = Vec::new(); // Initialize logger - stderrlog::new() - .verbosity(3) - .module("pageserver") - .init() - .unwrap(); + let _scope_guard; + if !conf.interactive { + _scope_guard = init_noninteractive_logging(); + } else { + _scope_guard = tui::init_logging(); + } + let _log_guard = slog_stdlog::init().unwrap(); + // Note: this `info!(...)` macro comes from `log` crate + info!("standard logging redirected to slog"); + + let tui_thread: Option>; + if conf.interactive { + // Initialize the UI + tui_thread = Some( + thread::Builder::new() + .name("UI thread".into()).spawn( + || { + let _ = tui::ui_main(); + }).unwrap()); + //threads.push(tui_thread); + } else { + tui_thread = None; + } + info!("starting..."); // Initialize the WAL applicator @@ -117,9 +152,33 @@ fn start_pageserver(conf: PageServerConf) -> Result<(), Error> { .unwrap(); threads.push(page_server_thread); - // never returns. - for t in threads { - t.join().unwrap() + if tui_thread.is_some() { + // The TUI thread exits when the user asks to Quit. + tui_thread.unwrap().join().unwrap(); + } else { + // In non-interactive mode, wait forever. + for t in threads { + t.join().unwrap() + } } Ok(()) } + +fn init_noninteractive_logging() -> slog_scope::GlobalLoggerGuard { + let decorator = slog_term::TermDecorator::new().build(); + let drain = slog_term::FullFormat::new(decorator).build().fuse(); + let drain = slog_async::Async::new(drain).chan_size(1000).build().fuse(); + let drain = slog::Filter::new(drain, + |record: &slog::Record| { + if record.level().is_at_least(slog::Level::Info) { + return true; + } + if record.level().is_at_least(slog::Level::Debug) && record.module().starts_with("pageserver") { + return true; + } + return false; + } + ).fuse(); + let logger = slog::Logger::root(drain, slog::o!()); + return slog_scope::set_global_logger(logger); +} diff --git a/src/control_plane.rs b/src/control_plane.rs index e260813e64..bee7cbd56b 100644 --- a/src/control_plane.rs +++ b/src/control_plane.rs @@ -102,7 +102,7 @@ impl ComputeControlPlane { // allocate new node entry with generated port let node_id = self.nodes.len() + 1; let node = PostgresNode { - node_id: node_id, + _node_id: node_id, port: self.get_port(), ip: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), pgdata: self.work_dir.join(format!("compute/pg{}", node_id)), @@ -156,7 +156,7 @@ impl ComputeControlPlane { /////////////////////////////////////////////////////////////////////////////// pub struct PostgresNode { - node_id: usize, + _node_id: usize, port: u32, ip: IpAddr, pgdata: PathBuf, diff --git a/src/lib.rs b/src/lib.rs index 875a447e14..7fd0f63de6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,10 +9,14 @@ pub mod restore_s3; pub mod waldecoder; pub mod walreceiver; pub mod walredo; +pub mod tui; +pub mod tui_event; +mod tui_logger; pub struct PageServerConf { pub data_dir: String, pub daemonize: bool, + pub interactive: bool, pub wal_producer_ip: IpAddr, pub wal_producer_port: u32, pub skip_recovery: bool, diff --git a/src/page_cache.rs b/src/page_cache.rs index 2c741abaf2..01d512c6a0 100644 --- a/src/page_cache.rs +++ b/src/page_cache.rs @@ -290,7 +290,6 @@ pub fn collect_records_for_apply(entry: &CacheEntry) -> (Option, Vec = Arc::new(TuiLogger::default()); + pub static ref WALRECEIVER_DRAIN: Arc = Arc::new(TuiLogger::default()); + pub static ref WALREDO_DRAIN: Arc = Arc::new(TuiLogger::default()); + pub static ref CATCHALL_DRAIN: Arc = Arc::new(TuiLogger::default()); +} + +pub fn init_logging() -> slog_scope::GlobalLoggerGuard { + + let pageservice_drain = slog::Filter::new(PAGESERVICE_DRAIN.as_ref(), + |record: &slog::Record| { + if record.level().is_at_least(slog::Level::Debug) && record.module().starts_with("pageserver::page_service") { + return true; + } + return false; + } + ).fuse(); + + let walredo_drain = slog::Filter::new(WALREDO_DRAIN.as_ref(), + |record: &slog::Record| { + if record.level().is_at_least(slog::Level::Debug) && record.module().starts_with("pageserver::walredo") { + return true; + } + return false; + } + ).fuse(); + + let walreceiver_drain = slog::Filter::new(WALRECEIVER_DRAIN.as_ref(), + |record: &slog::Record| { + if record.level().is_at_least(slog::Level::Debug) && record.module().starts_with("pageserver::walreceiver") { + return true; + } + return false; + } + ).fuse(); + + let catchall_drain = slog::Filter::new(CATCHALL_DRAIN.as_ref(), + |record: &slog::Record| { + if record.level().is_at_least(slog::Level::Info) { + return true; + } + if record.level().is_at_least(slog::Level::Debug) && record.module().starts_with("pageserver") { + return true; + } + return false; + } + ).fuse(); + + let drain = pageservice_drain; + let drain = slog::Duplicate::new(drain, walreceiver_drain).fuse(); + let drain = slog::Duplicate::new(drain, walredo_drain).fuse(); + let drain = slog::Duplicate::new(drain, catchall_drain).fuse(); + let drain = slog_async::Async::new(drain).chan_size(1000).build().fuse(); + let drain = slog::Filter::new(drain, + |record: &slog::Record| { + + if record.level().is_at_least(slog::Level::Info) { + return true; + } + if record.level().is_at_least(slog::Level::Debug) && record.module().starts_with("pageserver") { + return true; + } + + return false; + } + ).fuse(); + let logger = slog::Logger::root(drain, slog::o!()); + return slog_scope::set_global_logger(logger); +} + +pub fn ui_main<'b>() -> Result<(), Box> { + // Terminal initialization + let stdout = io::stdout().into_raw_mode()?; + let stdout = MouseTerminal::from(stdout); + let stdout = AlternateScreen::from(stdout); + let backend = TermionBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // Setup event handlers + let events = Events::new(); + + loop { + terminal.draw(|f| { + let size = f.size(); + + // +---------------+---------------+ + // | | | + // | top_top_left | | + // | | | + // +---------------+ top_right | + // | | | + // | top_bot_left | | + // | | | + // +---------------+---------------+ + // | | + // | bottom | + // | | + // +---------------+---------------+ + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(70), Constraint::Percentage(30)].as_ref()) + .split(size); + let top_chunk = chunks[0]; + let bottom_chunk = chunks[1]; + + let top_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(top_chunk); + let top_left_chunk = top_chunks[0]; + let top_right_chunk = top_chunks[1]; + + let c = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(top_left_chunk); + let top_top_left_chunk = c[0]; + let top_bot_left_chunk = c[1]; + + let w = TuiLoggerWidget::default(PAGESERVICE_DRAIN.as_ref()) + .block(Block::default() + .borders(Borders::ALL) + .title("Page Service") + .border_type(BorderType::Rounded)) + .show_module(false) + .style_error(Style::default().fg(Color::Red)) + .style_warn(Style::default().fg(Color::Yellow)) + .style_info(Style::default().fg(Color::Green)); + f.render_widget(w, top_top_left_chunk); + + let w = TuiLoggerWidget::default(WALREDO_DRAIN.as_ref()) + .block(Block::default() + .borders(Borders::ALL) + .title("WAL Redo") + .border_type(BorderType::Rounded)) + .show_module(false) + .style_error(Style::default().fg(Color::Red)) + .style_warn(Style::default().fg(Color::Yellow)) + .style_info(Style::default().fg(Color::Green)); + f.render_widget(w, top_bot_left_chunk); + + let w = TuiLoggerWidget::default(WALRECEIVER_DRAIN.as_ref()) + .block(Block::default() + .borders(Borders::ALL) + .title("WAL Receiver") + .border_type(BorderType::Rounded)) + .show_module(false) + .style_error(Style::default().fg(Color::Red)) + .style_warn(Style::default().fg(Color::Yellow)) + .style_info(Style::default().fg(Color::Green)); + f.render_widget(w, top_right_chunk); + + let w = TuiLoggerWidget::default(CATCHALL_DRAIN.as_ref()) + .block(Block::default() + .borders(Borders::ALL) + .title("Other log") + .border_type(BorderType::Rounded)) + .show_module(true) + .style_error(Style::default().fg(Color::Red)) + .style_warn(Style::default().fg(Color::Yellow)) + .style_info(Style::default().fg(Color::Green)); + f.render_widget(w, bottom_chunk); + + })?; + + // If ther user presses 'q', quit. + if let Event::Input(key) = events.next()? { + match key { + Key::Char('q') => { + break; + } + _ => (), + } + } + } + + terminal.show_cursor().unwrap(); + terminal.clear().unwrap(); + + Ok(()) +} diff --git a/src/tui_event.rs b/src/tui_event.rs new file mode 100644 index 0000000000..c0e25da864 --- /dev/null +++ b/src/tui_event.rs @@ -0,0 +1,97 @@ +use std::io; +use std::sync::mpsc; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use std::thread; +use std::time::Duration; + +use termion::event::Key; +use termion::input::TermRead; + +#[allow(dead_code)] +pub enum Event { + Input(I), + Tick, +} + +/// A small event handler that wrap termion input and tick events. Each event +/// type is handled in its own thread and returned to a common `Receiver` +#[allow(dead_code)] +pub struct Events { + rx: mpsc::Receiver>, + input_handle: thread::JoinHandle<()>, + ignore_exit_key: Arc, + tick_handle: thread::JoinHandle<()>, +} + +#[derive(Debug, Clone, Copy)] +pub struct Config { + pub exit_key: Key, + pub tick_rate: Duration, +} + +impl Default for Config { + fn default() -> Config { + Config { + exit_key: Key::Char('q'), + tick_rate: Duration::from_millis(250), + } + } +} + +impl Events { + pub fn new() -> Events { + Events::with_config(Config::default()) + } + + pub fn with_config(config: Config) -> Events { + let (tx, rx) = mpsc::channel(); + let ignore_exit_key = Arc::new(AtomicBool::new(false)); + let input_handle = { + let tx = tx.clone(); + let ignore_exit_key = ignore_exit_key.clone(); + thread::spawn(move || { + let stdin = io::stdin(); + for evt in stdin.keys() { + if let Ok(key) = evt { + if let Err(err) = tx.send(Event::Input(key)) { + eprintln!("{}", err); + return; + } + if !ignore_exit_key.load(Ordering::Relaxed) && key == config.exit_key { + return; + } + } + } + }) + }; + let tick_handle = { + thread::spawn(move || loop { + if tx.send(Event::Tick).is_err() { + break; + } + thread::sleep(config.tick_rate); + }) + }; + Events { + rx, + ignore_exit_key, + input_handle, + tick_handle, + } + } + + pub fn next(&self) -> Result, mpsc::RecvError> { + self.rx.recv() + } + + pub fn disable_exit_key(&mut self) { + self.ignore_exit_key.store(true, Ordering::Relaxed); + } + + pub fn enable_exit_key(&mut self) { + self.ignore_exit_key.store(false, Ordering::Relaxed); + } +} diff --git a/src/tui_logger.rs b/src/tui_logger.rs new file mode 100644 index 0000000000..c1a563cf90 --- /dev/null +++ b/src/tui_logger.rs @@ -0,0 +1,206 @@ +// +// A TUI Widget that displays log entries +// +// This is heavily inspired by gin66's tui_logger crate at https://github.com/gin66/tui-logger, +// but I wrote this based on the 'slog' module, which simplified things a lot. tui-logger also +// implemented the slog Drain trait, but it had a model of one global buffer for the records. +// With this implementation, each TuiLogger is a separate ring buffer and separate slog Drain. +// Also, I didn't do any of the "hot log" stuff that gin66's implementation had, you can use an +// AsyncDrain to buffer and handle overflow if desired. +// +use std::collections::VecDeque; +use std::sync::Mutex; +use std::time::SystemTime; +use chrono::offset::Local; +use chrono::DateTime; +use slog; +use slog::{Drain, OwnedKVList, Record, Level}; +use slog_async::AsyncRecord; +use tui::buffer::Buffer; +use tui::layout::{Rect}; +use tui::style::{Style, Modifier}; +use tui::text::{Span, Spans}; +use tui::widgets::{Block, Widget, Paragraph, Wrap}; + +// Size of the log ring buffer, in # of records +static BUFFER_SIZE: usize = 1000; + +pub struct TuiLogger { + events: Mutex>, +} + +impl<'a> Default for TuiLogger { + fn default() -> TuiLogger { + TuiLogger { + events: Mutex::new(VecDeque::with_capacity(BUFFER_SIZE)), + } + } +} + +impl Drain for TuiLogger { + type Ok = (); + type Err = slog::Error; + + fn log(&self, + record: &Record, + values: &OwnedKVList) + -> Result { + + let mut events = self.events.lock().unwrap(); + + let now = SystemTime::now(); + let asyncrec = AsyncRecord::from(record, values); + events.push_front((now, asyncrec)); + + if events.len() > BUFFER_SIZE { + events.pop_back(); + } + + return Ok(()); + } +} + +// TuiLoggerWidget renders a TuiLogger ring buffer +pub struct TuiLoggerWidget<'b> { + block: Option>, + /// Base style of the widget + style: Style, + /// Level based style + style_error: Option