diff --git a/bin/florestad/src/cli.rs b/bin/florestad/src/cli.rs index f528e987b..595a2140e 100644 --- a/bin/florestad/src/cli.rs +++ b/bin/florestad/src/cli.rs @@ -31,11 +31,20 @@ pub struct Cli { /// Turn debugging information on pub debug: bool, - #[arg(long)] - /// Option for saving log into data_Dir + #[arg(long, value_name = "FILE")] + /// Specify location of debug log file + /// + /// Specify location of debug log file (default: debug.log). Relative paths + /// will be prefixed by the net-specific datadir location. + /// Pass --nodebuglogfile to disable writing the log to a file. + pub debuglogfile: Option, + + #[arg(long, default_value_t = false)] + /// Disable writing the log to a file /// - /// if set, log will be saved into $DATA_DIR/debug.log. - pub log_to_file: bool, + /// When set, no log file will be written. + /// This overrides --debuglogfile. + pub nodebuglogfile: bool, #[arg(long, value_name = "PATH")] /// Where should we store data. This is the directory where we'll store the chainstate, diff --git a/bin/florestad/src/logger.rs b/bin/florestad/src/logger.rs index f2b6c6dd1..07e4fa121 100644 --- a/bin/florestad/src/logger.rs +++ b/bin/florestad/src/logger.rs @@ -4,8 +4,8 @@ //! //! This module configures [`tracing_subscriber`](https://docs.rs/tracing_subscriber) with up to two output layers: //! - **stdout** – human-friendly, ANSI-coloured when attached to a real TTY. -//! - **file** – plain-text, appended to [`LOG_FILE`] inside `data_dir` via a -//! non-blocking writer. +//! - **file** – plain-text, appended to a caller-specified path (defaulting to +//! [`LOG_FILE`] inside the data directory) via a non-blocking writer. //! //! The active log level is controlled (in descending priority) by: //! 1. The `RUST_LOG` environment variable. @@ -222,12 +222,11 @@ where /// /// # Arguments /// -/// * `data_dir` – Directory in which [`LOG_FILE`] is created when -/// `log_to_file` is `true`. The directory must already exist. -/// * `log_to_file` – Append structured log output to `/`[`LOG_FILE`]. +/// * `log_file` – The absolute path to the log file. When `Some`, log output is +/// appended to this file via a non-blocking writer. When `None`, file logging +/// is disabled. /// * `log_to_stdout` – Emit log output to stdout. -/// * `debug` – Set the default log level to `debug`. When `false` the -/// level defaults to `info`. In both cases `RUST_LOG` overrides the default. +/// * `log_level` – Set the default log level. `RUST_LOG` overrides this default. /// /// # Returns /// @@ -238,16 +237,15 @@ where /// /// # Errors /// -/// Returns [`io::Error`] if `log_to_file` is `true` and [`LOG_FILE`] cannot be -/// created or opened for appending inside `data_dir`. +/// Returns [`io::Error`] if `log_file` is `Some` and the file cannot be +/// created or opened for appending. /// /// # Panics /// /// Panics if a global [`tracing`] subscriber has already been /// installed (e.g. if this function is called more than once). pub fn start_logger( - data_directory: &String, - log_to_file: bool, + log_file: Option<&str>, log_to_stdout: bool, log_level: Level, ) -> Result, io::Error> { @@ -266,27 +264,30 @@ pub fn start_logger( .with_filter(make_filter()) }); - if log_to_file { - let file_path = format!("{}/{}", data_directory, LOG_FILE); - - // Validate the log file path (`/`). + if let Some(path) = log_file { + // Validate the log file path. let _ = fs::OpenOptions::new() .create(true) .append(true) - .open(&file_path) + .open(path) .map_err(|e| { - eprintln!( - "Failed to create log file at {}/{LOG_FILE}: {e}", - data_directory - ); + eprintln!("Failed to create log file at {path}: {e}"); exit(1) }); } // Formatter for events destined to the log file. let mut guard = None; - let fmt_layer_logfile = log_to_file.then(|| { - let file_appender = tracing_appender::rolling::never(data_directory, LOG_FILE); + let fmt_layer_logfile = log_file.map(|path| { + let log_path = std::path::Path::new(path); + let directory = log_path + .parent() + .unwrap_or_else(|| std::path::Path::new(".")); + let file_name = log_path + .file_name() + .and_then(|f| f.to_str()) + .unwrap_or(LOG_FILE); + let file_appender = tracing_appender::rolling::never(directory, file_name); let (non_blocking, file_guard) = tracing_appender::non_blocking(file_appender); guard = Some(file_guard); layer() diff --git a/bin/florestad/src/main.rs b/bin/florestad/src/main.rs index 57cdbd544..b7840ec61 100644 --- a/bin/florestad/src/main.rs +++ b/bin/florestad/src/main.rs @@ -42,6 +42,7 @@ use tracing::Level; #[cfg(unix)] use crate::daemonize::Daemon; use crate::logger::start_logger; +use crate::logger::LOG_FILE; fn main() { let params = Cli::parse(); @@ -56,6 +57,9 @@ fn main() { exit(1); }); + let debug_log_file = + resolve_debug_log_file(params.nodebuglogfile, params.debuglogfile.as_deref(), &data_dir); + let config = Config { data_dir, disable_dns_seeds: params.connect.is_some() || params.disable_dns_seeds, @@ -71,10 +75,7 @@ fn main() { log_to_stdout: !params.daemon, #[cfg(not(unix))] log_to_stdout: true, - #[cfg(unix)] - log_to_file: params.log_to_file || params.daemon, - #[cfg(not(unix))] - log_to_file: params.log_to_file, + debug_log_file, assume_valid: params.assume_valid, #[cfg(feature = "zmq-server")] zmq_address: params.zmq_address, @@ -111,8 +112,7 @@ fn main() { // The guard must stay alive until the end of `main` to flush file logs when dropped. let _logger_guard = start_logger( - &config.data_dir, - config.log_to_file, + config.debug_log_file.as_deref(), config.log_to_stdout, log_level, ); @@ -189,6 +189,33 @@ fn data_dir_path(dir: Option, network: Network) -> String { base.to_string_lossy().into_owned() } +/// Resolves the debug log file path from the CLI flags. +/// +/// File logging is enabled by default (matching Bitcoin Core). The user can: +/// - Override the path with `--debuglogfile=` +/// - Disable file logging entirely with `--nodebuglogfile` +/// +/// Relative paths are prefixed by the net-specific `data_dir`. +/// Returns `None` when file logging is disabled, or `Some(absolute_path)` otherwise. +fn resolve_debug_log_file( + no_debug_log_file: bool, + debug_log_file: Option<&str>, + data_dir: &str, +) -> Option { + if no_debug_log_file { + return None; + } + + let raw = debug_log_file.unwrap_or(LOG_FILE); + let path = PathBuf::from(raw); + let absolute = if path.is_absolute() { + path + } else { + PathBuf::from(data_dir).join(path) + }; + Some(absolute.to_string_lossy().into_owned()) +} + #[cfg(test)] mod tests { use super::*; @@ -235,4 +262,65 @@ mod tests { ); } } + + /// Default: no flags → file logging enabled at `/debug.log`. + #[test] + fn test_resolve_debug_log_file_default() { + let result = resolve_debug_log_file(false, None, "/home/user/.floresta"); + let expected = PathBuf::from("/home/user/.floresta").join(LOG_FILE); + assert_eq!(result, Some(expected.to_string_lossy().into_owned())); + } + + /// `--nodebuglogfile` → file logging disabled. + #[test] + fn test_resolve_debug_log_file_disabled() { + let result = resolve_debug_log_file(true, None, "/home/user/.floresta"); + assert_eq!(result, None); + } + + /// `--nodebuglogfile` overrides `--debuglogfile`. + #[test] + fn test_resolve_debug_log_file_disabled_overrides_custom() { + let result = + resolve_debug_log_file(true, Some("custom.log"), "/home/user/.floresta"); + assert_eq!(result, None); + } + + /// `--debuglogfile custom.log` (relative) → `/custom.log`. + #[test] + fn test_resolve_debug_log_file_relative_path() { + let result = + resolve_debug_log_file(false, Some("custom.log"), "/home/user/.floresta"); + let expected = PathBuf::from("/home/user/.floresta").join("custom.log"); + assert_eq!(result, Some(expected.to_string_lossy().into_owned())); + } + + /// `--debuglogfile /tmp/floresta.log` (absolute) → used as-is. + #[test] + fn test_resolve_debug_log_file_absolute_path() { + let result = resolve_debug_log_file( + false, + Some("/tmp/floresta.log"), + "/home/user/.floresta", + ); + assert_eq!(result, Some("/tmp/floresta.log".to_string())); + } + + /// Relative path with subdirectory: `--debuglogfile logs/node.log`. + #[test] + fn test_resolve_debug_log_file_relative_subdir() { + let result = + resolve_debug_log_file(false, Some("logs/node.log"), "/home/user/.floresta"); + let expected = PathBuf::from("/home/user/.floresta").join("logs/node.log"); + assert_eq!(result, Some(expected.to_string_lossy().into_owned())); + } + + /// Works correctly with a network-specific data directory (e.g. signet). + #[test] + fn test_resolve_debug_log_file_network_specific_datadir() { + let data_dir = data_dir_path(Some("/home/user/.floresta".into()), Network::Signet); + let result = resolve_debug_log_file(false, None, &data_dir); + let expected = PathBuf::from("/home/user/.floresta/signet").join(LOG_FILE); + assert_eq!(result, Some(expected.to_string_lossy().into_owned())); + } } diff --git a/crates/floresta-node/src/florestad.rs b/crates/floresta-node/src/florestad.rs index 4f1bb250f..36af97d57 100644 --- a/crates/floresta-node/src/florestad.rs +++ b/crates/floresta-node/src/florestad.rs @@ -165,8 +165,11 @@ pub struct Config { /// Whether we should write logs to `stdout`. pub log_to_stdout: bool, - /// Whether we should log to a fs file - pub log_to_file: bool, + /// The resolved absolute path to the debug log file, or `None` if file logging is disabled. + /// + /// When set, log output is appended to this file. When `None`, no log file is written. + /// By default this is `/debug.log`. + pub debug_log_file: Option, /// Whether we should use assume utreexo pub assume_utreexo: bool, @@ -238,7 +241,7 @@ impl Config { #[cfg(feature = "json-rpc")] json_rpc_address: None, log_to_stdout: false, - log_to_file: false, + debug_log_file: None, assume_utreexo: false, debug: false, user_agent: String::new(), @@ -477,7 +480,10 @@ impl Florestad { .as_ref() .map(|x| Self::resolve_hostname(x, 8332)) .transpose()?, - format!("{data_dir}/debug.log"), + self.config + .debug_log_file + .clone() + .unwrap_or_default(), )); if self.json_rpc.set(server).is_err() {