Skip to main content

bark_cli/
log.rs

1
2use std::{cmp, process};
3use std::io::Write;
4use std::path::{Path, PathBuf};
5
6/// Simple logger that splits into two logger
7struct SplitLogger {
8	log1: env_logger::Logger,
9	log2: env_logger::Logger,
10}
11
12impl SplitLogger {
13	fn init(log1: env_logger::Logger, log2: env_logger::Logger) {
14		let max_level = cmp::max(log1.filter(), log2.filter());
15		log::set_boxed_logger(Box::new(SplitLogger {
16			log1: log1,
17			log2: log2,
18		})).expect("error initializing split logger");
19		log::set_max_level(max_level);
20	}
21}
22
23impl log::Log for SplitLogger {
24	fn enabled(&self, m: &log::Metadata) -> bool {
25	    self.log1.enabled(m) || self.log2.enabled(m)
26	}
27
28	fn flush(&self) {
29	    self.log1.flush();
30		self.log2.flush();
31	}
32
33	fn log(&self, rec: &log::Record) {
34		self.log1.log(rec);
35		self.log2.log(rec);
36	}
37}
38
39/// Sets up logging to the terminal (unless `quiet`) and to a debug log file.
40///
41/// The debug log file defaults to `datadir/debug.log`, but can be
42/// overridden with `logfile`, or disabled entirely with `no_logfile`.
43pub fn init_logging(
44	verbose: bool,
45	quiet: bool,
46	datadir: &Path,
47	logfile: Option<PathBuf>,
48	no_logfile: bool,
49) {
50	if verbose && quiet {
51		println!("Can't set both --verbose and --quiet");
52		process::exit(1);
53	}
54
55	// Builder has no clone and we don't want to repeat this.
56	// Filter included: both sinks must select the same records.
57	fn base(verbose: bool) -> env_logger::Builder {
58		let mut builder = env_logger::Builder::new();
59		builder
60			.filter_module("rusqlite", log::LevelFilter::Warn)
61			// Both bitcoind rpc clients log the full response body at trace,
62			// which for getblock is the entire block in hex. Debug keeps the
63			// request line and the errors, and drops the response bodies.
64			.filter_module("bitcoind_async_client", log::LevelFilter::Debug)
65			.filter_module("bitcoincore_rpc", log::LevelFilter::Debug)
66			.filter_module("rustls", log::LevelFilter::Warn)
67			.filter_module("reqwest", log::LevelFilter::Warn)
68			.filter_module("ureq", log::LevelFilter::Warn)
69			.filter_module("ureq_proto", log::LevelFilter::Warn)
70			// bark-common enables tracing's "log" feature, so tracing events
71			// from the grpc stack reach us as log records. At trace these are
72			// per-frame and per-chunk. The "hyper" directive also covers
73			// hyper_util: directives match on a target prefix.
74			.filter_module("tracing::span", log::LevelFilter::Off)
75			.filter_module("hyper", log::LevelFilter::Warn)
76			.filter_module("h2", log::LevelFilter::Warn)
77			.filter_module("tower", log::LevelFilter::Warn)
78			.filter_module("tonic", log::LevelFilter::Info)
79			// The websocket stack logs every frame at trace and debug.
80			.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
81			.filter_module("tungstenite", log::LevelFilter::Warn);
82
83		// We first set the default and then let the env_logger
84		// env overwrite it.
85		builder.filter_level(if verbose {
86			log::LevelFilter::Trace
87		} else {
88			log::LevelFilter::Info
89		});
90		builder.parse_env(env_logger::Env::new().filter("BARK_LOG"));
91
92		builder
93	}
94
95	let terminal = if !quiet {
96		let mut logger = base(verbose);
97
98		logger
99			.format(move |out, rec| {
100				let now = chrono::Local::now();
101				let ts = now.format("%Y-%m-%d %H:%M:%S.%3f %:z");
102				let lvl = rec.level();
103				let msg = rec.args();
104				if verbose {
105					let module = rec.module_path().expect("no module");
106					if module.starts_with("bark") {
107						let file = rec.file().expect("our macro provides file");
108						let file = file.split("bark/src/").last().unwrap();
109						let line = rec.line().expect("our macro provides line");
110						writeln!(out, "[{ts} {lvl: >5} {module} {file}:{line}] {msg}")
111					} else {
112						writeln!(out, "[{ts} {lvl: >5} {module}] {msg}")
113					}
114				} else {
115					writeln!(out, "[{ts} {lvl: >5}] {msg}")
116				}
117			})
118			.target(env_logger::Target::Stderr);
119		Some(logger)
120	} else {
121		None
122	};
123
124	// The default location is only used once the datadir actually exists;
125	// an explicit --logfile is always attempted, so a bad path is reported.
126	let logfile_path = if no_logfile {
127		None
128	} else if let Some(path) = logfile {
129		Some(path)
130	} else if datadir.exists() {
131		Some(datadir.join("debug.log"))
132	} else {
133		None
134	};
135
136	let logfile = logfile_path.and_then(|path| {
137		let mut opts = std::fs::File::options();
138		opts.create(true).append(true);
139		// The debug log records wallet activity; create it owner-only so other
140		// users can't read it. mode() only applies when the file is newly
141		// created, so an existing log keeps its perms.
142		#[cfg(unix)]
143		{
144			use std::os::unix::fs::OpenOptionsExt;
145			opts.mode(0o600);
146		}
147		match opts.open(&path) {
148			Ok(mut file) => {
149				// try write a newline into the file to separate commands
150				let _ = file.write_all("\n\n".as_bytes());
151				let mut logger = base(verbose);
152				logger
153					.format_timestamp_millis()
154					.format_module_path(true)
155					.format_file(true)
156					.format_line_number(true)
157					.target(env_logger::Target::Pipe(Box::new(file)));
158				Some(logger)
159			},
160			Err(e) => {
161				eprintln!("Failed to open debug log file {}: {:#}", path.display(), e);
162				None
163			},
164		}
165	});
166
167	match (terminal, logfile) {
168		(Some(mut l1), Some(mut l2)) => SplitLogger::init(l1.build(), l2.build()),
169		(Some(mut l), None) => l.init(),
170		(None, Some(mut l)) => l.init(),
171		(None, None) => {},
172	}
173}