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"
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"

View file

@ -41,27 +41,36 @@ pub struct ApplicationState {
pub panels: RwLock<Vec<Panel>>,
pub sources: RwLock<Vec<Source>>,
pub storage: Mutex<SQLiteDataStore>,
pub diagnostics: RwLock<Vec<String>>,
}
impl ApplicationState {
pub fn new(path:PathBuf) -> Self {
let storage = SQLiteDataStore::new(path.clone()).unwrap();
pub fn new(path:PathBuf) -> Result<ApplicationState, FetchError> {
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(())
}

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 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 } );
}

View file

@ -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::<usize, String>(4)?.as_str()).unwrap())),
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)?,
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::<usize, String>(4)?.as_str()).unwrap())),
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)?,
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<Vec<Panel>> {
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| {
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<Panel> {
pub fn new_panel(&self, name: &str, view_size:i32, width: i32, height: i32, position: i32) -> rusqlite::Result<Panel> {
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<usize> {
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],
)
}

View file

@ -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<ApplicationState>,
input: InputBuffer,
edit: bool,
show_log: bool,
}
impl App {
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");
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() {
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<usize> = 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);
}
}
}
}

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 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<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 tracing::warn;
use chrono::Utc;
use eframe::egui::Context;
use crate::app::data::{ApplicationState, source::fetch};
pub fn native_save(state:Arc<ApplicationState>) {
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<ApplicationState>) {
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<ApplicationState>) {
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();
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);
}
}
});
}
}
if let Ok(meta) = std::fs::metadata(state.file_path.clone()) {
let mut fsize = state.file_size.write().expect("File Size RwLock poisoned");
*fsize = std::fs::metadata(state.file_path.clone()).unwrap().len();
*fsize = meta.len();
} // ignore errors
ctx.request_repaint();
}

View file

@ -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",