bark-cli 0.6.2

CLI for the bitcoin Ark protocol built by Second

use std::{cmp, process};
use std::io::Write;
use std::path::{Path, PathBuf};

/// Simple logger that splits into two logger
struct SplitLogger {
	log1: env_logger::Logger,
	log2: env_logger::Logger,
}

impl SplitLogger {
	fn init(log1: env_logger::Logger, log2: env_logger::Logger) {
		let max_level = cmp::max(log1.filter(), log2.filter());
		log::set_boxed_logger(Box::new(SplitLogger {
			log1: log1,
			log2: log2,
		})).expect("error initializing split logger");
		log::set_max_level(max_level);
	}
}

impl log::Log for SplitLogger {
	fn enabled(&self, m: &log::Metadata) -> bool {
	    self.log1.enabled(m) || self.log2.enabled(m)
	}

	fn flush(&self) {
	    self.log1.flush();
		self.log2.flush();
	}

	fn log(&self, rec: &log::Record) {
		self.log1.log(rec);
		self.log2.log(rec);
	}
}

/// Sets up logging to the terminal (unless `quiet`) and to a debug log file.
///
/// The debug log file defaults to `datadir/debug.log`, but can be
/// overridden with `logfile`, or disabled entirely with `no_logfile`.
pub fn init_logging(
	verbose: bool,
	quiet: bool,
	datadir: &Path,
	logfile: Option<PathBuf>,
	no_logfile: bool,
) {
	if verbose && quiet {
		println!("Can't set both --verbose and --quiet");
		process::exit(1);
	}

	// Builder has no clone and we don't want to repeat this.
	// Filter included: both sinks must select the same records.
	fn base(verbose: bool) -> env_logger::Builder {
		let mut builder = env_logger::Builder::new();
		builder
			.filter_module("rusqlite", log::LevelFilter::Warn)
			// Both bitcoind rpc clients log the full response body at trace,
			// which for getblock is the entire block in hex. Debug keeps the
			// request line and the errors, and drops the response bodies.
			.filter_module("bitcoind_async_client", log::LevelFilter::Debug)
			.filter_module("bitcoincore_rpc", log::LevelFilter::Debug)
			.filter_module("rustls", log::LevelFilter::Warn)
			.filter_module("reqwest", log::LevelFilter::Warn)
			.filter_module("ureq", log::LevelFilter::Warn)
			.filter_module("ureq_proto", log::LevelFilter::Warn)
			// bark-common enables tracing's "log" feature, so tracing events
			// from the grpc stack reach us as log records. At trace these are
			// per-frame and per-chunk. The "hyper" directive also covers
			// hyper_util: directives match on a target prefix.
			.filter_module("hyper", log::LevelFilter::Warn)
			.filter_module("h2", log::LevelFilter::Warn)
			.filter_module("tower", log::LevelFilter::Warn)
			.filter_module("tonic", log::LevelFilter::Info);

		// We first set the default and then let the env_logger
		// env overwrite it.
		builder.filter_level(if verbose {
			log::LevelFilter::Trace
		} else {
			log::LevelFilter::Info
		});
		builder.parse_env(env_logger::Env::new().filter("BARK_LOG"));

		builder
	}

	let terminal = if !quiet {
		let mut logger = base(verbose);

		logger
			.format(move |out, rec| {
				let now = chrono::Local::now();
				let ts = now.format("%Y-%m-%d %H:%M:%S.%3f %:z");
				let lvl = rec.level();
				let msg = rec.args();
				if verbose {
					let module = rec.module_path().expect("no module");
					if module.starts_with("bark") {
						let file = rec.file().expect("our macro provides file");
						let file = file.split("bark/src/").last().unwrap();
						let line = rec.line().expect("our macro provides line");
						writeln!(out, "[{ts} {lvl: >5} {module} {file}:{line}] {msg}")
					} else {
						writeln!(out, "[{ts} {lvl: >5} {module}] {msg}")
					}
				} else {
					writeln!(out, "[{ts} {lvl: >5}] {msg}")
				}
			})
			.target(env_logger::Target::Stderr);
		Some(logger)
	} else {
		None
	};

	// The default location is only used once the datadir actually exists;
	// an explicit --logfile is always attempted, so a bad path is reported.
	let logfile_path = if no_logfile {
		None
	} else if let Some(path) = logfile {
		Some(path)
	} else if datadir.exists() {
		Some(datadir.join("debug.log"))
	} else {
		None
	};

	let logfile = logfile_path.and_then(|path| {
		let mut opts = std::fs::File::options();
		opts.create(true).append(true);
		// The debug log records wallet activity; create it owner-only so other
		// users can't read it. mode() only applies when the file is newly
		// created, so an existing log keeps its perms.
		#[cfg(unix)]
		{
			use std::os::unix::fs::OpenOptionsExt;
			opts.mode(0o600);
		}
		match opts.open(&path) {
			Ok(mut file) => {
				// try write a newline into the file to separate commands
				let _ = file.write_all("\n\n".as_bytes());
				let mut logger = base(verbose);
				logger
					.format_timestamp_millis()
					.format_module_path(true)
					.format_file(true)
					.format_line_number(true)
					.target(env_logger::Target::Pipe(Box::new(file)));
				Some(logger)
			},
			Err(e) => {
				eprintln!("Failed to open debug log file {}: {:#}", path.display(), e);
				None
			},
		}
	});

	match (terminal, logfile) {
		(Some(mut l1), Some(mut l2)) => SplitLogger::init(l1.build(), l2.build()),
		(Some(mut l), None) => l.init(),
		(None, Some(mut l)) => l.init(),
		(None, None) => {},
	}
}