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