diff --git a/Cargo.lock b/Cargo.lock index 8532d62..1d956d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cast" version = "0.2.7" @@ -931,6 +937,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.8.0" @@ -1252,6 +1264,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", +] + [[package]] name = "regex" version = "1.5.4" @@ -1551,16 +1572,19 @@ dependencies = [ "futures", "lazy_static", "notify", + "parking_lot", "preserves-schema", "structopt", "syndicate", "syndicate-macros", + "termion", "tokio", "tokio-tungstenite", "tokio-util", "tracing", "tracing-futures", "tracing-subscriber", + "tui", "tungstenite", ] @@ -1578,6 +1602,18 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "termion" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +dependencies = [ + "libc", + "numtoa", + "redox_syscall", + "redox_termios", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -1793,6 +1829,19 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "tui" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c8ce4e27049eed97cfa363a5048b09d995e209994634a0efc26a14ab6c0c23" +dependencies = [ + "bitflags", + "cassowary", + "termion", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "tungstenite" version = "0.13.0" diff --git a/syndicate-server/Cargo.toml b/syndicate-server/Cargo.toml index b7c7694..8c39276 100644 --- a/syndicate-server/Cargo.toml +++ b/syndicate-server/Cargo.toml @@ -22,6 +22,7 @@ chrono = "0.4" futures = "0.3" lazy_static = "1.4" notify = "4.0" +parking_lot = "0.11" structopt = "0.3" tungstenite = "0.13" @@ -33,3 +34,6 @@ tokio-util = "0.6" tracing = "0.1" tracing-subscriber = "0.2" tracing-futures = "0.2" + +tui = "0.16" +termion = "1.5" diff --git a/syndicate-server/src/main.rs b/syndicate-server/src/main.rs index 0154cbb..6571a7f 100644 --- a/syndicate-server/src/main.rs +++ b/syndicate-server/src/main.rs @@ -21,6 +21,7 @@ mod language; mod lifecycle; mod protocol; mod services; +mod ui; mod schemas { include!(concat!(env!("OUT_DIR"), "/src/schemas/mod.rs")); @@ -46,6 +47,9 @@ struct ServerConfig { #[structopt(short = "c", long)] config: Vec, + #[structopt(short = "u", long)] + ui: bool, + #[structopt(long)] no_banner: bool, } @@ -54,7 +58,11 @@ struct ServerConfig { async fn main() -> Result<(), Box> { let config = Arc::new(ServerConfig::from_args()); - syndicate::convenient_logging()?; + if config.ui { + ui::start()?; + } else { + syndicate::convenient_logging()?; + } if !config.no_banner && !config.inferior { const BRIGHT_GREEN: &str = "\x1b[92m"; diff --git a/syndicate-server/src/ui/mod.rs b/syndicate-server/src/ui/mod.rs new file mode 100644 index 0000000..07960d9 --- /dev/null +++ b/syndicate-server/src/ui/mod.rs @@ -0,0 +1,205 @@ +use parking_lot::RwLock; + +use std::collections::VecDeque; +use std::io; +use std::sync::atomic::{AtomicBool, Ordering}; + +use syndicate::value::IOValue; +use syndicate::value::Map; +use syndicate::value::NestedValue; +use syndicate::value::Value; + +use termion::raw::IntoRawMode; + +use tracing::Event; +use tracing::Level; +use tracing::Metadata; +use tracing::field::Field; +use tracing::span::Attributes; +use tracing::span::Id; +use tracing::span::Record; + +use tracing_subscriber::prelude::*; +use tracing_subscriber::layer::Context; + +use tui::Terminal; +use tui::backend::TermionBackend; + +#[derive(Debug, Clone, Default)] +struct FieldsSnapshot(Map<&'static str, IOValue>); + +#[derive(Debug, Clone)] +struct EventSnapshot { + spans: Vec<(&'static Metadata<'static>, FieldsSnapshot)>, + event_metadata: &'static Metadata<'static>, + event_fields: FieldsSnapshot, +} + +struct LogCollector { + dirty: AtomicBool, + current_level: Level, + minimum_level: Level, + history: RwLock>, + history_limit: usize, +} + +fn write_fields(f: &mut std::fmt::Formatter, fs: &Map<&'static str, IOValue>) -> std::fmt::Result { + for (k, v) in fs.iter() { + if let Some(s) = v.value().as_string() { + write!(f, "{}={} ", k, s)?; + } else { + write!(f, "{}={:?} ", k, v)?; + } + } + Ok(()) +} + +impl std::fmt::Display for EventSnapshot { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let mut fs = self.event_fields.0.clone(); + if let Some(m) = fs.remove("message") { + match m.value().as_string() { + Some(s) => write!(f, "{} ", s)?, + None => { fs.insert("message", m); } + } + } + write_fields(f, &fs)?; + write!(f, "at {}:{}\n", + self.event_metadata.file().unwrap_or("-"), + self.event_metadata.line().unwrap_or(0))?; + for s in self.spans.iter() { + let (m, fs) = s; + write!(f, " - in {} ", m.name())?; + write_fields(f, &fs.0)?; + write!(f, "at {}:{}\n", m.file().unwrap_or("-"), m.line().unwrap_or(0))?; + } + Ok(()) + } +} + +impl FieldsSnapshot { + fn push(&mut self, field: &Field, v: IOValue) { + let name = field.name(); + match self.0.remove(name) { + None => { + self.0.insert(name, v); + } + Some(o) => match o.value().as_sequence() { + Some(vs) => { + let mut vs = vs.clone(); + vs.push(v); + self.0.insert(name, IOValue::new(vs)); + }, + None => { + self.0.insert(name, IOValue::new(vec![o, v])); + } + } + } + } +} + +impl tracing::field::Visit for FieldsSnapshot { + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + self.push(field, IOValue::new(format!("{:?}", value))); + } + fn record_f64(&mut self, field: &Field, value: f64) { + self.push(field, IOValue::new(value)); + } + fn record_i64(&mut self, field: &Field, value: i64) { + self.push(field, IOValue::new(value)); + } + fn record_u64(&mut self, field: &Field, value: u64) { + self.push(field, IOValue::new(value)); + } + fn record_bool(&mut self, field: &Field, value: bool) { + self.push(field, IOValue::new(value)); + } + fn record_str(&mut self, field: &Field, value: &str) { + self.push(field, IOValue::new(value)); + } + fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) { + let mut r = Value::simple_record("error", 1); + r.fields_vec_mut().push(IOValue::new(format!("{}", value))); + let r = r.finish(); + self.push(field, r.wrap()); + } +} + +impl LogCollector { + fn new() -> Self { + LogCollector { + dirty: AtomicBool::new(false), + current_level: Level::INFO, + minimum_level: Level::TRACE, + history: Default::default(), + history_limit: 10000, + } + } +} + +impl tracing_subscriber::registry::LookupSpan<'a> + std::fmt::Debug> + tracing_subscriber::Layer for LogCollector +where for<'a> >::Data: std::fmt::Debug +{ + fn enabled(&self, metadata: &Metadata, _ctx: Context) -> bool { + metadata.level() <= &self.minimum_level + } + + fn new_span(&self, attrs: &Attributes, id: &Id, ctx: Context) { + if let Some(s) = ctx.span(id) { + let mut snap = FieldsSnapshot::default(); + attrs.record(&mut snap); + s.extensions_mut().insert(snap); + } + } + + fn on_record(&self, span: &Id, values: &Record, ctx: Context) { + if let Some(s) = ctx.span(span) { + if let Some(snap) = s.extensions_mut().get_mut::() { + values.record(snap); + } + } + } + + fn on_event(&self, event: &Event, ctx: Context) { + if event.metadata().level() > &self.current_level { + return; + } + + let mut snap = EventSnapshot { + spans: Vec::new(), + event_metadata: event.metadata(), + event_fields: Default::default(), + }; + event.record(&mut snap.event_fields); + + if let Some(scope) = ctx.event_scope(event) { + for s in scope.from_root() { + snap.spans.push(( + s.metadata(), + s.extensions().get::().cloned().unwrap_or_else(FieldsSnapshot::default) + )); + } + } + + println!("{}", &snap); + + let mut history = self.history.write(); + history.push_front(snap); + history.truncate(self.history_limit); + self.dirty.store(true, Ordering::Relaxed); + } +} + +pub fn start() -> io::Result<()> { + let stdout = io::stdout().into_raw_mode()?; + let backend = TermionBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let subscriber = tracing_subscriber::Registry::default() + .with(LogCollector::new()); + tracing::subscriber::set_global_default(subscriber) + .expect("Could not set UI global subscriber"); + + Ok(()) +}