From 3515f637205713f3c14ef07aff7830a1f1d0c410 Mon Sep 17 00:00:00 2001 From: Andy Goetz Date: Sat, 5 Aug 2023 19:58:13 -0700 Subject: [PATCH] Added support for logging Jolly state --- Cargo.lock | 57 ++++++++++++++++++++++++++++++++++-- Cargo.toml | 2 ++ docs/advanced.md | 4 +++ docs/config.md | 56 +++++++++++++++++++++++++++++++++++ src/config.rs | 42 ++++++++++++++++++++++++++ src/entry.rs | 12 ++++++++ src/lib.rs | 11 ++++++- src/log.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 21 ++++++++++++- src/settings.rs | 3 +- 10 files changed, 278 insertions(+), 6 deletions(-) create mode 100644 src/log.rs diff --git a/Cargo.lock b/Cargo.lock index e64aae0..376401e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,7 +183,7 @@ dependencies = [ "log", "parking", "polling", - "rustix", + "rustix 0.37.20", "slab", "socket2", "waker-fn", @@ -833,6 +833,19 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "errno" version = "0.3.1" @@ -1370,6 +1383,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "iced" version = "0.10.0" @@ -1621,6 +1640,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.1", + "rustix 0.38.3", + "windows-sys 0.48.0", +] + [[package]] name = "jni-sys" version = "0.3.0" @@ -1645,10 +1675,12 @@ dependencies = [ "csscolorparser", "dark-light", "dirs 5.0.1", + "env_logger", "freedesktop-icons", "iced", "ico", "lazy_static", + "log", "objc", "once_cell", "opener", @@ -1786,6 +1818,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +[[package]] +name = "linux-raw-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" + [[package]] name = "lock_api" version = "0.4.10" @@ -2783,7 +2821,20 @@ dependencies = [ "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys 0.4.5", "windows-sys 0.48.0", ] @@ -3152,7 +3203,7 @@ dependencies = [ "cfg-if", "fastrand", "redox_syscall 0.3.5", - "rustix", + "rustix 0.37.20", "windows-sys 0.48.0", ] diff --git a/Cargo.toml b/Cargo.toml index b22efdb..acd9af6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ pulldown-cmark = "0.9" url = "2" once_cell = "1.18.0" resvg = "0.34.0" +env_logger = "0.10.0" +log = "0.4.19" [target.'cfg(target_os = "macos")'.dependencies] objc = "0.2" diff --git a/docs/advanced.md b/docs/advanced.md index ecb3c00..a313795 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -1,5 +1,9 @@ Below are a couple of advanced tips and tricks for using Jolly: +# Troubleshooting + +Don't forget that Jolly can log detailed trace events of its performance using `env_logger`. For more details, see the [logging documentation](config.md#log) + # Copying Links Sometimes you don't need to open a Jolly entry, just determine the location diff --git a/docs/config.md b/docs/config.md index 4731aff..0cbb636 100644 --- a/docs/config.md +++ b/docs/config.md @@ -30,6 +30,10 @@ accent_color= "orange" [config.ui.search] text_size = 40 # make search window bigger +[config.log] +file = 'path/to/logfile' +filters = "debug" + ### Jolly entries below... ``` @@ -269,3 +273,55 @@ JOLLY_DEFAULT_THEME=Adwaita cargo build As a general rule, the jolly build script will warn if the `JOLLY_DEFAULT_THEME` doesn't seem to be installed at compile time. + +# [config.log] + +The `[config.log]` table contains settings that control error logging +and debugging of Jolly. By default, Jolly will display error messages +in the UI and print them to `stderr`, but this behavior can be +customized. + +**Important Note** *When you are trying to troubleshoot a bug in +Jolly, you may be asked to supply logfiles from operating +Jolly. Please be aware that at higher log levels, Jolly will include +the Jolly entry targets, and whichever text was entered in the search +window. This may be considered sensitive information and you should +always review logs before sharing them.* + +To customize behavior, use the following fields: + +| field name | data type | description | +|------------|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------| +| `file` | *string* | Additional filename to write logs to. Always appends. | +| `filters` | *string* OR *string array* | [env_logger](https://docs.rs/env_logger/latest/env_logger/index.html#enabling-logging) filters for logging, by default, only log `error` | + + +As an example log file configuration, consider the following snippet: + +```toml +[config.log] + +# Jolly logs are stored in log file below +file = 'path/to/logfile' + +# messages from jolly crate at debug level, and cosmic_text crate at trace level +filters = ["jolly=debug", "cosmic_text=trace"] + +``` + + +## `file` — *string* + +Specify a filename for Jolly to write logs to. If the file cannot be +accessed then an error is returned. Jolly will always append to this +file if it already exists. Jolly will also continue to write trace +messages to `stderr`. + +## `filters` — *string* OR *string array* + +The `filters` key can be used to specify one or more +[env_logger](https://docs.rs/env_logger/latest/env_logger/index.html#enabling-logging) +filters. These filters are used to determine which log level is +set. By default, only `errors` are logged, which also generally would +appear in the UI. + diff --git a/src/config.rs b/src/config.rs index 32cd79c..90a2ee8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,33 @@ use toml; pub const LOGFILE_NAME: &str = "jolly.toml"; +// helper enum to allow decoding a scalar into a single vec +// original hint from here: +// https://github.com/Mingun/ksc-rs/blob/8532f701e660b07b6d2c74963fdc0490be4fae4b/src/parser.rs#L18-L42 +// (MIT LICENSE) +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(untagged)] +enum OneOrMany { + /// Single value + One(T), + /// Array of values + Vec(Vec), +} +impl From> for Vec { + fn from(from: OneOrMany) -> Self { + match from { + OneOrMany::One(val) => vec![val], + OneOrMany::Vec(vec) => vec, + } + } +} + +pub fn one_or_many<'de, T: Deserialize<'de>, D: serde::Deserializer<'de>>( + d: D, +) -> Result, D::Error> { + OneOrMany::deserialize(d).map(Vec::from) +} + // represents the data that is loaded from the main configuration file // it will always have some settings internally, even if the config // file is not found. if there is an error parsing the config, a @@ -171,4 +198,19 @@ mod tests { assert_ne!(settings.ui.search, Default::default()); } + + #[test] + fn test_one_or_many() { + use super::one_or_many; + use serde::de::value::{MapDeserializer, SeqDeserializer, UnitDeserializer}; + + let _: Vec<()> = one_or_many(UnitDeserializer::::new()).unwrap(); + + let seq_de = SeqDeserializer::<_, serde::de::value::Error>::new(std::iter::once(())); + one_or_many::, _>(seq_de).unwrap(); + + let map_de = + MapDeserializer::<_, serde::de::value::Error>::new(std::iter::once(("a", "b"))); + one_or_many::, _>(map_de).unwrap_err(); + } } diff --git a/src/entry.rs b/src/entry.rs index 32c3e94..a50659c 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -121,6 +121,15 @@ pub enum EntryType { SystemEntry(String), } +impl fmt::Display for EntryType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + EntryType::FileEntry(_) => f.write_str("FileEntry"), + EntryType::SystemEntry(_) => f.write_str("SystemEntry"), + } + } +} + impl StoreEntry { // parse a toml value into a store entry pub fn from_value(name: String, val: toml::Value) -> Result { @@ -311,6 +320,9 @@ impl StoreEntry { EntryType::SystemEntry(_) => platform::system, }; let selection = self.format_selection(searchtext); + + ::log::info!(r#"Selected Entry {}("{}")"#, &self.entry, selection); + func(&selection).map_err(Error::PlatformError) } diff --git a/src/lib.rs b/src/lib.rs index 396f0fb..3a1f1e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use ::log::trace; use iced::widget::text_input; use iced::widget::{Text, TextInput}; use iced::{clipboard, event, keyboard, subscription, widget, window}; @@ -19,6 +20,7 @@ mod custom; mod entry; pub mod error; mod icon; +mod log; mod platform; mod search_results; mod settings; @@ -69,6 +71,7 @@ pub struct Jolly { impl Jolly { fn move_to_err(&mut self, err: error::Error) -> Command<::Message> { + ::log::error!("{err}"); self.store_state = StoreLoadedState::Finished(err); Command::none() } @@ -87,6 +90,9 @@ impl Jolly { if self.modifiers.command() { let result = entry.format_selection(&self.searchtext); let msg = format!("copied to clipboard: {}", &result); + + ::log::info!("{msg}"); + let cmds = [ clipboard::write(result), self.move_to_err(error::Error::FinalMessage(msg)), @@ -120,10 +126,11 @@ impl Application for Jolly { jolly.store_state = match config.store { Ok(store) => { let msg = format!("Loaded {} entries", store.len()); + StoreLoadedState::LoadSucceeded(store, msg) } Err(e) => { - eprintln!("{e}"); + ::log::error!("{e}"); StoreLoadedState::Finished(e) } }; @@ -143,6 +150,8 @@ impl Application for Jolly { } fn update(&mut self, message: Self::Message) -> Command { + trace!("Received Message::{:?}", message); + // first, match the messages that would cause us to quit regardless of application state match message { Message::ExternalEvent(event::Event::Keyboard(e)) => { diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..524077b --- /dev/null +++ b/src/log.rs @@ -0,0 +1,76 @@ +use env_logger::Builder; +use serde::Deserialize; + +use crate::config::one_or_many; +use crate::error; + +#[derive(Deserialize, Debug, Clone, PartialEq, Default)] +#[serde(default)] +pub struct LogSettings { + file: Option, + #[serde(deserialize_with = "one_or_many")] + filters: Vec, +} + +impl LogSettings { + pub fn init_logger(&self) -> Result<(), error::Error> { + self.build_logger().map(|mut b| b.init()) + } + + fn build_logger(&self) -> Result { + let mut builder = Builder::new(); + builder + .parse_filters(&self.filters.join(",")) + .format_timestamp_micros() + .target(env_logger::fmt::Target::Stderr); + + if let Some(f) = &self.file { + let f = std::fs::OpenOptions::new() + .append(true) + .create(true) + .open(f) + .map_err(error::Error::IoError)?; + builder.target(env_logger::fmt::Target::Pipe(Box::new(f))); + } + + Ok(builder) + } +} + +#[cfg(test)] +mod tests { + use super::LogSettings; + use crate::error; + use ::log; + use env_logger::Builder; + use log::Log; + use tempfile; + + fn file_logger>(f: F) -> Result { + LogSettings { + file: Some(f.as_ref().to_string_lossy().to_string()), + filters: vec!["trace".into()], + } + .build_logger() + } + + #[test] + fn test_log_appends() { + let dir = tempfile::tempdir().unwrap(); + + let filename = dir.path().join("a"); + + let record = log::RecordBuilder::new().build(); + + for i in 1..3 { + let logger = file_logger(&filename).unwrap().build(); + logger.log(&record); + + std::mem::drop(logger); + + let linecount = std::fs::read_to_string(&filename).unwrap().lines().count(); + + assert_eq!(linecount, i); + } + } +} diff --git a/src/main.rs b/src/main.rs index 06623d1..16a4d0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,28 @@ use iced::{Application, Settings}; use jolly::{config, error, Jolly}; +use std::time::Instant; pub fn main() -> Result<(), error::Error> { - let config = config::Config::load(); + let now = Instant::now(); + + let mut config = config::Config::load(); + + let elapsed = now.elapsed(); + + // if we could not initialize the logger, we set the store to + // error, so the ui shows the issue + if let Err(e) = config.settings.log.init_logger() { + config.store = Err(e); + } + + if let Ok(s) = &config.store { + ::log::debug!( + "Loaded {} entries in {:.6} sec", + s.len(), + elapsed.as_secs_f32() + ); + } let mut settings = Settings::default(); settings.window.size = ( diff --git a/src/settings.rs b/src/settings.rs index e90158e..4e9eaf2 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,8 +1,9 @@ -use crate::ui; +use crate::{log, ui}; use serde; #[derive(serde::Deserialize, Debug, Clone, PartialEq, Default)] #[serde(default)] pub struct Settings { pub ui: ui::UISettings, + pub log: log::LogSettings, }