diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d1a23a..70a9ac6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New command `repo state` - by Awiteb - Support pull requests - by Awiteb - Add `--personal-fork` flag to `repo announce` command - by Awiteb +- Log to stderr and a file - by Awiteb ### Breaking Change diff --git a/src/cli/utils.rs b/src/cli/utils.rs index a608d0e..9d0cad1 100644 --- a/src/cli/utils.rs +++ b/src/cli/utils.rs @@ -14,7 +14,12 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::io::{self, Write}; +use std::{ + fs, + io::{self, Write}, +}; + +use crate::error::{N34Error, N34Result}; /// Displays the given prompt and reads a line of input from the user. pub fn read_line(prompt: &str) -> io::Result { @@ -43,3 +48,39 @@ pub fn prompt_bool(prompt: &str) -> io::Result { } } } + +/// Opens the logs file for writing. If the file size exceeds 5MB, it is opened +/// in write mode, otherwise in append mode. +pub fn logs_file() -> N34Result { + const FIVE_MB: u64 = 1024 * 1024 * 5; + + let logs_path = dirs::data_local_dir() + .ok_or(N34Error::CanNotFindDataPath)? + .join("n34") + .join("logs.log"); + + tracing::info!(path = %logs_path.display(), "Logs file"); + + if let Some(parent) = logs_path.parent() + && !parent.exists() + { + fs::create_dir_all(parent)?; + } + + _ = fs::File::create_new(&logs_path); + + let is_large = if let Ok(file) = fs::File::open(&logs_path) + && let Ok(metadata) = file.metadata() + { + metadata.len() >= FIVE_MB + } else { + false + }; + + fs::OpenOptions::new() + .write(true) + .append(!is_large) + .truncate(is_large) + .open(&logs_path) + .map_err(N34Error::from) +} diff --git a/src/error.rs b/src/error.rs index cd53e6f..cc60957 100644 --- a/src/error.rs +++ b/src/error.rs @@ -55,6 +55,11 @@ pub enum N34Error { Keyring(#[from] nostr_keyring::Error), #[error("{0}")] Config(#[from] ConfigError), + #[error( + "Could not determine the default data path: both `$XDG_CONFIG_HOME` and `$HOME` \ + environment variables are missing or unset." + )] + CanNotFindDataPath, #[error("No editor specified in the `EDITOR` environment variable")] EditorNotFound, #[error("The file you edited is empty. Please save your changes before exiting the editor.")] diff --git a/src/main.rs b/src/main.rs index ff0dc97..62ab692 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,13 +22,14 @@ pub mod error; pub mod nostr_utils; use std::{ + fs::File, process::ExitCode, sync::atomic::{AtomicBool, Ordering}, }; use clap::Parser; use clap_verbosity_flag::Verbosity; -use tracing::Level; +use tracing::{Level, level_filters::LevelFilter}; use tracing_subscriber::{Layer, filter, layer::SubscriberExt}; use self::cli::Cli; @@ -37,42 +38,64 @@ use self::cli::Cli; /// open. static EDITOR_OPEN: AtomicBool = AtomicBool::new(false); +/// Returns the stderr log layer +fn stderr_log_layer() -> impl Layer +where + S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>, +{ + tracing_subscriber::fmt::layer() + .with_ansi(true) + .with_writer(std::io::stderr) + .without_time() +} + /// Configures the logging level based on the provided verbosity. /// /// When verbosity is set to TRACE, includes file and line numbers in logs. -fn set_log_level(verbosity: Verbosity) { - let is_trace = verbosity - .tracing_level() - .is_some_and(|l| l == tracing::Level::TRACE); - - let logs_filter = filter::dynamic_filter_fn(move |m, _| { +fn set_log_level(verbosity: Verbosity, logs_file: File) { + let editor_filter = filter::dynamic_filter_fn(move |m, _| { // Disable all logs while editor is open verbosity.tracing_level().unwrap_or(Level::ERROR) >= *m.level() + && (m.name().starts_with("event src") || m.name().contains("nostr")) && !EDITOR_OPEN.load(Ordering::Relaxed) }); - let logs_layer = tracing_subscriber::fmt::layer() - .with_file(is_trace) - .with_line_number(is_trace) - .without_time(); - let subscriber = tracing_subscriber::registry().with(logs_layer.with_filter(logs_filter)); + let file_layer = tracing_subscriber::fmt::layer() + .with_ansi(false) + .with_writer(logs_file) + .with_file(true) + .with_line_number(true) + .with_filter(LevelFilter::TRACE); + + let subscriber = tracing_subscriber::registry() + .with(stderr_log_layer().with_filter(editor_filter)) + .with(file_layer); tracing::subscriber::set_global_default(subscriber).ok(); } +async fn try_main() -> error::N34Result<()> { + // Initialize a thread-local subscriber for logging during CLI parsing and + // post-processing. + let guard = + tracing::subscriber::set_default(tracing_subscriber::registry().with(stderr_log_layer())); + + let cli = cli::post_cli(Cli::parse())?; + let logs_file = cli::utils::logs_file()?; + + // Replace the thread-local subscriber with a global default subscriber based on + // the CLI verbosity level. + drop(guard); + set_log_level(cli.verbosity, logs_file); + + cli.run().await?; + + Ok(()) +} + #[tokio::main] async fn main() -> ExitCode { - let cli = match cli::post_cli(Cli::parse()) { - Ok(cli) => cli, - Err(err) => { - eprintln!("{err}"); - return ExitCode::FAILURE; - } - }; - - set_log_level(cli.verbosity); - - if let Err(err) = cli.run().await { - tracing::error!("{err}"); + if let Err(err) = try_main().await { + eprintln!("{err}"); return err.exit_code(); }