feat: improved logs, added log panels, added up btn

improved a lot logging, using tracing from tokio.
Now there's a subscriber which tracks log messages and makes them
available inside the state, so that we can show them in the gui.
Made a jank logs side panel.
Made panels reorderable in a kinda weird but meh way
This commit is contained in:
əlemi 2022-06-12 23:52:06 +02:00
parent 67a0a4ec5c
commit ee9e91534e
Signed by: alemi
GPG key ID: A4895B84D311642C
8 changed files with 180 additions and 56 deletions

View file

@ -17,6 +17,8 @@ rand = "0.8"
dirs = "4" dirs = "4"
git-version = "0.3.5" git-version = "0.3.5"
chrono = "0.4" chrono = "0.4"
tracing = "0.1" # egui / eframe use tracing
tracing-subscriber = "0.3"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
rusqlite = "0.27" rusqlite = "0.27"

View file

@ -41,27 +41,36 @@ pub struct ApplicationState {
pub panels: RwLock<Vec<Panel>>, pub panels: RwLock<Vec<Panel>>,
pub sources: RwLock<Vec<Source>>, pub sources: RwLock<Vec<Source>>,
pub storage: Mutex<SQLiteDataStore>, pub storage: Mutex<SQLiteDataStore>,
pub diagnostics: RwLock<Vec<String>>,
} }
impl ApplicationState { impl ApplicationState {
pub fn new(path:PathBuf) -> Self { pub fn new(path:PathBuf) -> Result<ApplicationState, FetchError> {
let storage = SQLiteDataStore::new(path.clone()).unwrap(); let storage = SQLiteDataStore::new(path.clone())?;
let panels = storage.load_panels().unwrap(); let panels = storage.load_panels()?;
let sources = storage.load_sources().unwrap(); let sources = storage.load_sources()?;
return ApplicationState{ return Ok(ApplicationState{
run: true, 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, file_path: path,
panels: RwLock::new(panels), panels: RwLock::new(panels),
sources: RwLock::new(sources), sources: RwLock::new(sources),
storage: Mutex::new(storage), storage: Mutex::new(storage),
}; diagnostics: RwLock::new(Vec::new()),
});
} }
pub fn add_panel(&self, name:&str) -> Result<(), FetchError> { 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); self.panels.write().expect("Panels RwLock poisoned").push(panel);
Ok(()) Ok(())
} }

View file

@ -52,10 +52,10 @@ pub fn fetch(url:&str, query_x:&str, query_y:&str) -> Result<Value, FetchError>
let res = ureq::get(url).call()?.into_json()?; let res = ureq::get(url).call()?.into_json()?;
let x : f64; let x : f64;
if query_x.len() > 0 { 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 { } else {
x = Utc::now().timestamp() as f64; 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 } ); return Ok( Value { x, y } );
} }

View file

@ -26,7 +26,8 @@ impl SQLiteDataStore {
timeserie BOOL NOT NULL, timeserie BOOL NOT NULL,
width INT NOT NULL, width INT NOT NULL,
height 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)?, interval: row.get(3)?,
last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
query_x: row.get(4)?, query_x: row.get(4)?,
// compiled_query_x: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(4)?.as_str()).unwrap())),
query_y: row.get(5)?, query_y: row.get(5)?,
// compiled_query_y: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(5)?.as_str()).unwrap())),
panel_id: row.get(6)?, panel_id: row.get(6)?,
color: unpack_color(row.get(7).unwrap_or(0)), color: unpack_color(row.get(7).unwrap_or(0)),
visible: row.get(8)?, visible: row.get(8)?,
@ -151,9 +150,7 @@ impl SQLiteDataStore {
interval: row.get(3)?, interval: row.get(3)?,
last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
query_x: row.get(4)?, query_x: row.get(4)?,
// compiled_query_x: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(4)?.as_str()).unwrap())),
query_y: row.get(5)?, query_y: row.get(5)?,
// compiled_query_y: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(5)?.as_str()).unwrap())),
panel_id: row.get(6)?, panel_id: row.get(6)?,
color: unpack_color(row.get(7).unwrap_or(0)), color: unpack_color(row.get(7).unwrap_or(0)),
visible: row.get(8)?, visible: row.get(8)?,
@ -193,7 +190,7 @@ impl SQLiteDataStore {
pub fn load_panels(&self) -> rusqlite::Result<Vec<Panel>> { pub fn load_panels(&self) -> rusqlite::Result<Vec<Panel>> {
let mut panels: Vec<Panel> = Vec::new(); let mut panels: Vec<Panel> = 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| { let panels_iter = statement.query_map([], |row| {
Ok(Panel { Ok(Panel {
id: row.get(0)?, id: row.get(0)?,
@ -217,10 +214,10 @@ impl SQLiteDataStore {
} }
// jank! TODO make it not jank! // jank! TODO make it not jank!
pub fn new_panel(&self, name: &str, view_size:i32, width: i32, height: i32) -> rusqlite::Result<Panel> { pub fn new_panel(&self, name: &str, view_size:i32, width: i32, height: i32, position: i32) -> rusqlite::Result<Panel> {
self.conn.execute( self.conn.execute(
"INSERT INTO panels (name, view_scroll, view_size, timeserie, width, height, limit_view) VALUES (?, ?, ?, ?, ?, ?, ?)", "INSERT INTO panels (name, view_scroll, view_size, timeserie, width, height, limit_view, position) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
params![name, true, view_size, true, width, height, false] params![name, true, view_size, true, width, height, false, position]
)?; )?;
let mut statement = self.conn.prepare("SELECT * FROM panels WHERE name = ?")?; let mut statement = self.conn.prepare("SELECT * FROM panels WHERE name = ?")?;
for panel in statement.query_map(params![name], |row| { for panel in statement.query_map(params![name], |row| {
@ -252,10 +249,11 @@ impl SQLiteDataStore {
width: i32, width: i32,
height: i32, height: i32,
limit: bool, limit: bool,
position: i32,
) -> rusqlite::Result<usize> { ) -> rusqlite::Result<usize> {
self.conn.execute( self.conn.execute(
"UPDATE panels SET name = ?, view_scroll = ?, view_size = ?, timeserie = ?, width = ?, height = ?, limit_view = ? WHERE 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, id], params![name, view_scroll, view_size, timeserie, width, height, limit, position, id],
) )
} }

View file

@ -4,6 +4,7 @@ pub mod util;
use std::sync::Arc; use std::sync::Arc;
use chrono::{Utc, Local}; use chrono::{Utc, Local};
use tracing::error;
use eframe::egui; use eframe::egui;
use eframe::egui::plot::GridMark; use eframe::egui::plot::GridMark;
use eframe::egui::{RichText, plot::{Line, Plot}, Color32}; use eframe::egui::{RichText, plot::{Line, Plot}, Color32};
@ -44,11 +45,12 @@ pub struct App {
data: Arc<ApplicationState>, data: Arc<ApplicationState>,
input: InputBuffer, input: InputBuffer,
edit: bool, edit: bool,
show_log: bool,
} }
impl App { impl App {
pub fn new(_cc: &eframe::CreationContext, data: Arc<ApplicationState>) -> Self { pub fn new(_cc: &eframe::CreationContext, data: Arc<ApplicationState>) -> 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"); ui.label("+ panel");
eframe::egui::TextEdit::singleline(&mut self.input.panel_name).hint_text("name").desired_width(50.0).show(ui); eframe::egui::TextEdit::singleline(&mut self.input.panel_name).hint_text("name").desired_width(50.0).show(ui);
if ui.button("add").clicked() { 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.separator();
ui.label("+ source"); ui.label("+ source");
@ -81,7 +85,7 @@ impl eframe::App for App {
.selected_text(format!("panel [{}]", self.input.panel_id)) .selected_text(format!("panel [{}]", self.input.panel_id))
.width(70.0) .width(70.0)
.show_ui(ui, |ui| { .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 { for p in &*pnls {
ui.selectable_value(&mut self.input.panel_id, p.id, p.name.as_str()); 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.add(egui::Slider::new(&mut self.input.interval, 1..=60));
ui.color_edit_button_srgba(&mut self.input.color); ui.color_edit_button_srgba(&mut self.input.color);
if ui.button("add").clicked() { if ui.button("add").clicked() {
self.data.add_source( if let Err(e) = self.data.add_source(
self.input.panel_id, self.input.panel_id,
self.input.name.as_str(), self.input.name.as_str(),
self.input.url.as_str(), self.input.url.as_str(),
@ -99,22 +103,32 @@ impl eframe::App for App {
self.input.query_y.as_str(), self.input.query_y.as_str(),
self.input.color, self.input.color,
self.input.visible, self.input.visible,
).unwrap(); ) {
error!(target: "ui", "Error adding souce : {:?}", e);
}
} }
ui.separator(); ui.separator();
} }
ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| { ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| {
if ui.button("×").clicked() { ui.horizontal(|ui| {
frame.quit(); if ui.small_button("×").clicked() {
} frame.quit();
}
ui.checkbox(&mut self.show_log, "log");
});
}); });
}); });
}); });
egui::TopBottomPanel::bottom("footer").show(ctx, |ui| { egui::TopBottomPanel::bottom("footer").show(ctx, |ui| {
ui.horizontal(|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.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.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(format!("v{}-{}", env!("CARGO_PKG_VERSION"), git_version::git_version!())); 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<usize> = Vec::new();
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| { egui::ScrollArea::vertical().show(ui, |ui| {
let mut panels = self.data.panels.write().unwrap(); // TODO only lock as write when editing let mut panels = self.data.panels.write().expect("Panels RwLock poisoned"); // TODO only lock as write when editing
let sources = self.data.sources.read().unwrap(); // 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 panel in &mut *panels { for (index, panel) in panels.iter_mut().enumerate() {
ui.group(|ui| { ui.group(|ui| {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.heading(panel.name.as_str()); ui.heading(panel.name.as_str());
ui.separator(); 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 { for source in &*sources {
if source.panel_id == panel.id { if source.panel_id == panel.id {
if source.visible { 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);
}
}
} }
} }

View file

@ -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 chrono::{DateTime, NaiveDateTime, Utc, Local};
use tracing_subscriber::Layer;
use eframe::egui::Color32; 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"]; const PREFIXES: &'static [&'static str] = &["", "k", "M", "G", "T"];
pub fn human_size(size: u64) -> String { pub fn human_size(size: u64) -> String {
@ -49,3 +53,44 @@ pub fn repack_color(c: Color32) -> u32 {
} }
return out; return out;
} }
pub struct InternalLogger {
state: Arc<ApplicationState>,
}
impl InternalLogger {
pub fn new(state: Arc<ApplicationState>) -> Self {
InternalLogger { state }
}
}
impl<S> Layer<S> 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();
}
}
}

View file

@ -1,14 +1,15 @@
use std::sync::Arc; use std::sync::Arc;
use tracing::warn;
use chrono::Utc; use chrono::Utc;
use eframe::egui::Context; use eframe::egui::Context;
use crate::app::data::{ApplicationState, source::fetch}; use crate::app::data::{ApplicationState, source::fetch};
pub fn native_save(state:Arc<ApplicationState>) { pub fn native_save(state:Arc<ApplicationState>) {
std::thread::spawn(move || { std::thread::spawn(move || {
let storage = state.storage.lock().unwrap(); let storage = state.storage.lock().expect("Storage Mutex poisoned");
let panels = state.panels.read().unwrap(); let panels = state.panels.read().expect("Panels RwLock poisoned");
for panel in &*panels { for (index, panel) in panels.iter().enumerate() {
storage.update_panel( if let Err(e) = storage.update_panel(
panel.id, panel.id,
panel.name.as_str(), panel.name.as_str(),
panel.view_scroll, panel.view_scroll,
@ -17,10 +18,13 @@ pub fn native_save(state:Arc<ApplicationState>) {
panel.width, panel.width,
panel.height, panel.height,
panel.limit, panel.limit,
).unwrap(); index as i32,
let sources = state.sources.read().unwrap(); ) {
warn!(target: "native-save", "Could not update panel #{} : {:?}", panel.id, e);
}
let sources = state.sources.read().expect("Sources RwLock poisoned");
for source in &*sources { for source in &*sources {
storage.update_source( if let Err(e) = storage.update_source(
source.id, source.id,
source.panel_id, source.panel_id,
source.name.as_str(), source.name.as_str(),
@ -30,7 +34,9 @@ pub fn native_save(state:Arc<ApplicationState>) {
source.query_y.as_str(), source.query_y.as_str(),
source.color, source.color,
source.visible, 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(); 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() { for j in 0..sources.len() {
let s_id = sources[j].id; let s_id = sources[j].id;
if !sources[j].valid() { 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(); *last_update = Utc::now();
let state2 = state.clone(); let state2 = state.clone();
let url = sources[j].url.clone(); let url = sources[j].url.clone();
let query_x = sources[j].query_x.clone(); let query_x = sources[j].query_x.clone();
let query_y = sources[j].query_y.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! 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(); match fetch(url.as_str(), query_x.as_str(), query_y.as_str()) {
let store = state2.storage.lock().unwrap(); Ok(v) => {
store.put_value(s_id, v).unwrap(); let store = state2.storage.lock().expect("Storage mutex poisoned");
let sources = state2.sources.read().unwrap(); if let Err(e) = store.put_value(s_id, v) {
sources[j].data.write().unwrap().push(v); warn!(target:"background-worker", "Could not put sample for source #{} in db: {:?}", s_id, e);
let mut last_update = sources[j].last_fetch.write().unwrap(); } else {
*last_update = Utc::now(); // overwrite it so fetches comply with API slowdowns and get desynched among them 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"); if let Ok(meta) = std::fs::metadata(state.file_path.clone()) {
*fsize = std::fs::metadata(state.file_path.clone()).unwrap().len(); let mut fsize = state.file_size.write().expect("File Size RwLock poisoned");
*fsize = meta.len();
} // ignore errors
ctx.request_repaint(); ctx.request_repaint();
} }

View file

@ -1,17 +1,29 @@
mod app; mod app;
use std::sync::Arc; 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: // When compiling natively:
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn main() -> ! { fn main() -> ! {
use tracing::metadata::LevelFilter;
let native_options = eframe::NativeOptions::default(); let native_options = eframe::NativeOptions::default();
let mut store_path = dirs::data_dir().unwrap_or(std::path::PathBuf::from(".")); // TODO get cwd more consistently? let mut store_path = dirs::data_dir().unwrap_or(std::path::PathBuf::from(".")); // TODO get cwd more consistently?
store_path.push("dashboard.db"); 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 eframe::run_native( // TODO replace this with a loop that ends so we can cleanly exit the background worker
"dashboard", "dashboard",