diff --git a/Cargo.toml b/Cargo.toml index a31feba..dd017a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ rand = "0.8" dirs = "4" git-version = "0.3.5" chrono = "0.4" +tracing = "0.1" # egui / eframe use tracing +tracing-subscriber = "0.3" serde = { version = "1", features = ["derive"] } serde_json = "1" rusqlite = "0.27" diff --git a/src/app/data/mod.rs b/src/app/data/mod.rs index 5ad33ea..adf5cfe 100644 --- a/src/app/data/mod.rs +++ b/src/app/data/mod.rs @@ -41,27 +41,36 @@ pub struct ApplicationState { pub panels: RwLock>, pub sources: RwLock>, pub storage: Mutex, + pub diagnostics: RwLock>, } impl ApplicationState { - pub fn new(path:PathBuf) -> Self { - let storage = SQLiteDataStore::new(path.clone()).unwrap(); + pub fn new(path:PathBuf) -> Result { + let storage = SQLiteDataStore::new(path.clone())?; - let panels = storage.load_panels().unwrap(); - let sources = storage.load_sources().unwrap(); + let panels = storage.load_panels()?; + let sources = storage.load_sources()?; - return ApplicationState{ + return Ok(ApplicationState{ run: true, - file_size: RwLock::new(std::fs::metadata(path.clone()).unwrap().len()), + file_size: RwLock::new(std::fs::metadata(path.clone())?.len()), file_path: path, panels: RwLock::new(panels), sources: RwLock::new(sources), storage: Mutex::new(storage), - }; + diagnostics: RwLock::new(Vec::new()), + }); } pub fn add_panel(&self, name:&str) -> Result<(), FetchError> { - let panel = self.storage.lock().expect("Storage Mutex poisoned").new_panel(name, 100, 200, 280)?; // TODO make values customizable and useful + let panel = self.storage.lock().expect("Storage Mutex poisoned") + .new_panel( + name, + 100, + 200, + 280, + self.panels.read().expect("Panels RwLock poisoned").len() as i32 // todo can this be made more compact and without acquisition? + )?; // TODO make values customizable and useful self.panels.write().expect("Panels RwLock poisoned").push(panel); Ok(()) } diff --git a/src/app/data/source.rs b/src/app/data/source.rs index ea65fca..b942f77 100644 --- a/src/app/data/source.rs +++ b/src/app/data/source.rs @@ -52,10 +52,10 @@ pub fn fetch(url:&str, query_x:&str, query_y:&str) -> Result let res = ureq::get(url).call()?.into_json()?; let x : f64; if query_x.len() > 0 { - x = jql::walker(&res, query_x)?.as_f64().unwrap(); // TODO what if it's given to us as a string? + x = jql::walker(&res, query_x)?.as_f64().ok_or(FetchError::JQLError("X query is null".to_string()))?; // TODO what if it's given to us as a string? } else { x = Utc::now().timestamp() as f64; } - let y = jql::walker(&res, query_y)?.as_f64().unwrap(); + let y = jql::walker(&res, query_y)?.as_f64().ok_or(FetchError::JQLError("Y query is null".to_string()))?; return Ok( Value { x, y } ); } \ No newline at end of file diff --git a/src/app/data/store.rs b/src/app/data/store.rs index ba55278..c70c8d8 100644 --- a/src/app/data/store.rs +++ b/src/app/data/store.rs @@ -26,7 +26,8 @@ impl SQLiteDataStore { timeserie BOOL NOT NULL, width INT NOT NULL, height INT NOT NULL, - limit_view BOOL NOT NULL + limit_view BOOL NOT NULL, + position INT NOT NULL );", [], )?; @@ -104,9 +105,7 @@ impl SQLiteDataStore { interval: row.get(3)?, last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), query_x: row.get(4)?, - // compiled_query_x: Arc::new(Mutex::new(jq_rs::compile(row.get::(4)?.as_str()).unwrap())), query_y: row.get(5)?, - // compiled_query_y: Arc::new(Mutex::new(jq_rs::compile(row.get::(5)?.as_str()).unwrap())), panel_id: row.get(6)?, color: unpack_color(row.get(7).unwrap_or(0)), visible: row.get(8)?, @@ -151,9 +150,7 @@ impl SQLiteDataStore { interval: row.get(3)?, last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), query_x: row.get(4)?, - // compiled_query_x: Arc::new(Mutex::new(jq_rs::compile(row.get::(4)?.as_str()).unwrap())), query_y: row.get(5)?, - // compiled_query_y: Arc::new(Mutex::new(jq_rs::compile(row.get::(5)?.as_str()).unwrap())), panel_id: row.get(6)?, color: unpack_color(row.get(7).unwrap_or(0)), visible: row.get(8)?, @@ -193,7 +190,7 @@ impl SQLiteDataStore { pub fn load_panels(&self) -> rusqlite::Result> { let mut panels: Vec = Vec::new(); - let mut statement = self.conn.prepare("SELECT * FROM panels")?; + let mut statement = self.conn.prepare("SELECT * FROM panels ORDER BY position")?; let panels_iter = statement.query_map([], |row| { Ok(Panel { id: row.get(0)?, @@ -217,10 +214,10 @@ impl SQLiteDataStore { } // jank! TODO make it not jank! - pub fn new_panel(&self, name: &str, view_size:i32, width: i32, height: i32) -> rusqlite::Result { + pub fn new_panel(&self, name: &str, view_size:i32, width: i32, height: i32, position: i32) -> rusqlite::Result { self.conn.execute( - "INSERT INTO panels (name, view_scroll, view_size, timeserie, width, height, limit_view) VALUES (?, ?, ?, ?, ?, ?, ?)", - params![name, true, view_size, true, width, height, false] + "INSERT INTO panels (name, view_scroll, view_size, timeserie, width, height, limit_view, position) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + params![name, true, view_size, true, width, height, false, position] )?; let mut statement = self.conn.prepare("SELECT * FROM panels WHERE name = ?")?; for panel in statement.query_map(params![name], |row| { @@ -252,10 +249,11 @@ impl SQLiteDataStore { width: i32, height: i32, limit: bool, + position: i32, ) -> rusqlite::Result { self.conn.execute( - "UPDATE panels SET name = ?, view_scroll = ?, view_size = ?, timeserie = ?, width = ?, height = ?, limit_view = ? WHERE id = ?", - params![name, view_scroll, view_size, timeserie, width, height, limit, id], + "UPDATE panels SET name = ?, view_scroll = ?, view_size = ?, timeserie = ?, width = ?, height = ?, limit_view = ?, position = ? WHERE id = ?", + params![name, view_scroll, view_size, timeserie, width, height, limit, position, id], ) } diff --git a/src/app/mod.rs b/src/app/mod.rs index 4150549..9aedce2 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -4,6 +4,7 @@ pub mod util; use std::sync::Arc; use chrono::{Utc, Local}; +use tracing::error; use eframe::egui; use eframe::egui::plot::GridMark; use eframe::egui::{RichText, plot::{Line, Plot}, Color32}; @@ -44,11 +45,12 @@ pub struct App { data: Arc, input: InputBuffer, edit: bool, + show_log: bool, } impl App { pub fn new(_cc: &eframe::CreationContext, data: Arc) -> Self { - Self { data, input: InputBuffer::default(), edit: false } + Self { data, input: InputBuffer::default(), edit: false, show_log: false } } } @@ -69,7 +71,9 @@ impl eframe::App for App { ui.label("+ panel"); eframe::egui::TextEdit::singleline(&mut self.input.panel_name).hint_text("name").desired_width(50.0).show(ui); if ui.button("add").clicked() { - self.data.add_panel(self.input.panel_name.as_str()).unwrap(); + if let Err(e) = self.data.add_panel(self.input.panel_name.as_str()) { + error!(target: "ui", "Failed to add panel: {:?}", e); + }; } ui.separator(); ui.label("+ source"); @@ -81,7 +85,7 @@ impl eframe::App for App { .selected_text(format!("panel [{}]", self.input.panel_id)) .width(70.0) .show_ui(ui, |ui| { - let pnls = self.data.panels.write().unwrap(); + let pnls = self.data.panels.write().expect("Panels RwLock poisoned"); for p in &*pnls { ui.selectable_value(&mut self.input.panel_id, p.id, p.name.as_str()); } @@ -91,7 +95,7 @@ impl eframe::App for App { ui.add(egui::Slider::new(&mut self.input.interval, 1..=60)); ui.color_edit_button_srgba(&mut self.input.color); if ui.button("add").clicked() { - self.data.add_source( + if let Err(e) = self.data.add_source( self.input.panel_id, self.input.name.as_str(), self.input.url.as_str(), @@ -99,22 +103,32 @@ impl eframe::App for App { self.input.query_y.as_str(), self.input.color, self.input.visible, - ).unwrap(); + ) { + error!(target: "ui", "Error adding souce : {:?}", e); + } } ui.separator(); } ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| { - if ui.button("×").clicked() { - frame.quit(); - } + ui.horizontal(|ui| { + if ui.small_button("×").clicked() { + frame.quit(); + } + ui.checkbox(&mut self.show_log, "log"); + }); }); }); }); egui::TopBottomPanel::bottom("footer").show(ctx, |ui| { ui.horizontal(|ui|{ - ui.label(self.data.file_path.to_str().unwrap()); + ui.label(self.data.file_path.to_str().unwrap()); // TODO maybe calculate it just once? ui.separator(); - ui.label(human_size(*self.data.file_size.read().unwrap())); + ui.label(human_size(*self.data.file_size.read().expect("Filesize RwLock poisoned"))); + let diags = self.data.diagnostics.read().expect("Diagnostics RwLock poisoned"); + if diags.len() > 0 { + ui.separator(); + ui.label(diags.last().unwrap_or(&"".to_string())); + } ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| { ui.horizontal(|ui| { ui.label(format!("v{}-{}", env!("CARGO_PKG_VERSION"), git_version::git_version!())); @@ -160,16 +174,37 @@ impl eframe::App for App { }); }); } + if self.show_log { + egui::SidePanel::right("logs-panel").show(ctx, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + ui.heading("logs"); + ui.separator(); + let msgs = self.data.diagnostics.read().expect("Diagnostics RwLock poisoned"); + ui.group(|ui| { + for msg in msgs.iter() { + ui.label(msg); + } + }); + }); + }); + } + let mut to_swap : Vec = Vec::new(); egui::CentralPanel::default().show(ctx, |ui| { egui::ScrollArea::vertical().show(ui, |ui| { - let mut panels = self.data.panels.write().unwrap(); // TODO only lock as write when editing - let sources = self.data.sources.read().unwrap(); // TODO only lock as write when editing - for panel in &mut *panels { + let mut panels = self.data.panels.write().expect("Panels RwLock poisoned"); // TODO only lock as write when editing + let sources = self.data.sources.read().expect("Sources RwLock poisoned"); // TODO only lock as write when editing + for (index, panel) in panels.iter_mut().enumerate() { ui.group(|ui| { ui.vertical(|ui| { ui.horizontal(|ui| { ui.heading(panel.name.as_str()); ui.separator(); + if self.edit && index > 0 { + if ui.small_button("up").clicked() { + to_swap.push(index); // TODO kinda jank but is there a better way? + } + ui.separator(); + } for source in &*sources { if source.panel_id == panel.id { if source.visible { @@ -249,5 +284,11 @@ impl eframe::App for App { } }); }); + if !to_swap.is_empty() { // TODO can this be done in background? idk + let mut panels = self.data.panels.write().expect("Panels RwLock poisoned"); + for index in to_swap { + panels.swap(index-1, index); + } + } } } diff --git a/src/app/util.rs b/src/app/util.rs index 55325d7..df05abf 100644 --- a/src/app/util.rs +++ b/src/app/util.rs @@ -1,7 +1,11 @@ -// if you're handling more than terabytes of data, it's the future and you ought to update this code! +use std::sync::Arc; use chrono::{DateTime, NaiveDateTime, Utc, Local}; +use tracing_subscriber::Layer; use eframe::egui::Color32; +use super::data::ApplicationState; + +// if you're handling more than terabytes of data, it's the future and you ought to update this code! const PREFIXES: &'static [&'static str] = &["", "k", "M", "G", "T"]; pub fn human_size(size: u64) -> String { @@ -49,3 +53,44 @@ pub fn repack_color(c: Color32) -> u32 { } return out; } + +pub struct InternalLogger { + state: Arc, +} + +impl InternalLogger { + pub fn new(state: Arc) -> Self { + InternalLogger { state } + } +} + +impl Layer for InternalLogger where S: tracing::Subscriber { + fn on_event( + &self, + event: &tracing::Event<'_>, + _ctx: tracing_subscriber::layer::Context<'_, S>, + ) { + let mut msg_visitor = LogMessageVisitor { msg: "".to_string() }; + event.record(&mut msg_visitor); + let out = format!("{} [{}] {}: {}", Local::now().format("%H:%M:%S"), event.metadata().level(), event.metadata().target(), msg_visitor.msg); + self.state.diagnostics.write().expect("Diagnostics RwLock poisoned").push(out); + } +} + +struct LogMessageVisitor { + msg : String, +} + +impl tracing::field::Visit for LogMessageVisitor { + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + if field.name() == "message" { + self.msg = format!("{}: '{:?}' ", field.name(), &value); + } + } + + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { + if field.name() == "message" { + self.msg = value.to_string(); + } + } +} \ No newline at end of file diff --git a/src/app/worker.rs b/src/app/worker.rs index fd8004b..c06fb7b 100644 --- a/src/app/worker.rs +++ b/src/app/worker.rs @@ -1,14 +1,15 @@ use std::sync::Arc; +use tracing::warn; use chrono::Utc; use eframe::egui::Context; use crate::app::data::{ApplicationState, source::fetch}; pub fn native_save(state:Arc) { std::thread::spawn(move || { - let storage = state.storage.lock().unwrap(); - let panels = state.panels.read().unwrap(); - for panel in &*panels { - storage.update_panel( + let storage = state.storage.lock().expect("Storage Mutex poisoned"); + let panels = state.panels.read().expect("Panels RwLock poisoned"); + for (index, panel) in panels.iter().enumerate() { + if let Err(e) = storage.update_panel( panel.id, panel.name.as_str(), panel.view_scroll, @@ -17,10 +18,13 @@ pub fn native_save(state:Arc) { panel.width, panel.height, panel.limit, - ).unwrap(); - let sources = state.sources.read().unwrap(); + index as i32, + ) { + warn!(target: "native-save", "Could not update panel #{} : {:?}", panel.id, e); + } + let sources = state.sources.read().expect("Sources RwLock poisoned"); for source in &*sources { - storage.update_source( + if let Err(e) = storage.update_source( source.id, source.panel_id, source.name.as_str(), @@ -30,7 +34,9 @@ pub fn native_save(state:Arc) { source.query_y.as_str(), source.color, source.visible, - ).unwrap(); + ) { + warn!(target: "native-save", "Could not update source #{} : {:?}", source.id, e); + } } } }); @@ -56,30 +62,41 @@ impl BackgroundWorker for NativeBackgroundWorker { } last_check = Utc::now().timestamp_millis(); - let sources = state.sources.read().unwrap(); + let sources = state.sources.read().expect("Sources RwLock poisoned"); for j in 0..sources.len() { let s_id = sources[j].id; if !sources[j].valid() { - let mut last_update = sources[j].last_fetch.write().unwrap(); + let mut last_update = sources[j].last_fetch.write().expect("Sources RwLock poisoned"); *last_update = Utc::now(); let state2 = state.clone(); let url = sources[j].url.clone(); let query_x = sources[j].query_x.clone(); let query_y = sources[j].query_y.clone(); std::thread::spawn(move || { // TODO this can overspawn if a request takes longer than the refresh interval! - let v = fetch(url.as_str(), query_x.as_str(), query_y.as_str()).unwrap(); - let store = state2.storage.lock().unwrap(); - store.put_value(s_id, v).unwrap(); - let sources = state2.sources.read().unwrap(); - sources[j].data.write().unwrap().push(v); - let mut last_update = sources[j].last_fetch.write().unwrap(); - *last_update = Utc::now(); // overwrite it so fetches comply with API slowdowns and get desynched among them + match fetch(url.as_str(), query_x.as_str(), query_y.as_str()) { + Ok(v) => { + let store = state2.storage.lock().expect("Storage mutex poisoned"); + if let Err(e) = store.put_value(s_id, v) { + warn!(target:"background-worker", "Could not put sample for source #{} in db: {:?}", s_id, e); + } else { + let sources = state2.sources.read().expect("Sources RwLock poisoned"); + sources[j].data.write().expect("Source data RwLock poisoned").push(v); + let mut last_update = sources[j].last_fetch.write().expect("Source last update RwLock poisoned"); + *last_update = Utc::now(); // overwrite it so fetches comply with API slowdowns and get desynched among them + } + }, + Err(e) => { + warn!(target:"background-worker", "Could not fetch value from {} : {:?}", url, e); + } + } }); } } - let mut fsize = state.file_size.write().expect("File Size RwLock poisoned"); - *fsize = std::fs::metadata(state.file_path.clone()).unwrap().len(); + if let Ok(meta) = std::fs::metadata(state.file_path.clone()) { + let mut fsize = state.file_size.write().expect("File Size RwLock poisoned"); + *fsize = meta.len(); + } // ignore errors ctx.request_repaint(); } diff --git a/src/main.rs b/src/main.rs index 16cdd2d..b263937 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,29 @@ mod app; use std::sync::Arc; -use crate::app::{App, data::ApplicationState, worker::{BackgroundWorker, NativeBackgroundWorker}}; +use tracing_subscriber::prelude::*; +use crate::app::{App, util::InternalLogger, data::ApplicationState, worker::{BackgroundWorker, NativeBackgroundWorker}}; // When compiling natively: #[cfg(not(target_arch = "wasm32"))] fn main() -> ! { + use tracing::metadata::LevelFilter; + + let native_options = eframe::NativeOptions::default(); let mut store_path = dirs::data_dir().unwrap_or(std::path::PathBuf::from(".")); // TODO get cwd more consistently? store_path.push("dashboard.db"); - let store = Arc::new(ApplicationState::new(store_path)); + let store = Arc::new( + ApplicationState::new(store_path).expect("Failed creating application state") + ); + + tracing_subscriber::registry() + .with(LevelFilter::INFO) + .with(tracing_subscriber::fmt::Layer::new()) + .with(InternalLogger::new(store.clone())) + .init(); eframe::run_native( // TODO replace this with a loop that ends so we can cleanly exit the background worker "dashboard",