diff --git a/src/dfx/Cargo.toml b/src/dfx/Cargo.toml index d5bf06c6d4..42ac7b11a0 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -46,9 +46,9 @@ serde_cbor = "0.10" serde_json = "1.0.40" serde_repr = "0.1.5" signal-hook = "0.1.13" -slog = "2.5.2" -slog-term = "2.5.0" +slog = { version = "2.5.2", features = ["max_level_trace"] } slog-async = "2.4.0" +slog-term = "2.5.0" sysinfo = "0.9.6" tar = "0.4.26" tempfile = "3.1.0" diff --git a/src/dfx/src/lib/logger.rs b/src/dfx/src/lib/logger.rs index 0c7205df30..ed9e86d83c 100644 --- a/src/dfx/src/lib/logger.rs +++ b/src/dfx/src/lib/logger.rs @@ -1,3 +1,24 @@ +use crate::config::dfx_version_str; +use slog::{Drain, Level, Logger}; +use slog_async; +use slog_term; +use std::fs::File; +use std::path::PathBuf; + +/// The logging mode to use. +pub enum LoggingMode { + /// The default mode for logging; output without any decoration, to STDERR. + Stderr, + + /// Tee logging to a file (in addition to STDERR). This mimics the verbose flag. + /// So it would be similar to `dfx ... |& tee /some/file.txt + Tee(PathBuf), + + /// Output Debug logs and up to a file, regardless of verbosity, keep the STDERR output + /// the same (with verbosity). + File(PathBuf), +} + /// A Slog formatter that writes to a term decorator, without any formatting. pub struct PlainFormat where @@ -33,3 +54,53 @@ impl slog::Drain for PlainFormat { }) } } + +/// Create a log drain. +fn create_drain(mode: LoggingMode) -> Logger { + match mode { + LoggingMode::Stderr => Logger::root( + PlainFormat::new(slog_term::PlainSyncDecorator::new(std::io::stderr())).fuse(), + slog::o!(), + ), + LoggingMode::File(out) => { + let file = File::create(out).expect("Couldn't open log file"); + let decorator = slog_term::PlainDecorator::new(file); + let drain = slog_term::FullFormat::new(decorator).build().fuse(); + Logger::root(slog_async::Async::new(drain).build().fuse(), slog::o!()) + } + // A Tee mode is basically 2 drains duplicated. + LoggingMode::Tee(out) => Logger::root( + slog::Duplicate::new( + create_drain(LoggingMode::Stderr), + create_drain(LoggingMode::File(out)), + ) + .fuse(), + slog::o!(), + ), + } +} + +/// Create a root logger. +/// The verbose_level can be negative, in which case it's a quiet mode which removes warnings, +/// then errors entirely. +pub fn create_root_logger(verbose_level: i64, mode: LoggingMode) -> Logger { + let log_level = match verbose_level { + -3 => Level::Critical, + -2 => Level::Error, + -1 => Level::Warning, + 0 => Level::Info, + 1 => Level::Debug, + x => { + if x > 0 { + Level::Trace + } else { + return Logger::root(slog::Discard, slog::o!()); + } + } + }; + + let drain = slog::LevelFilter::new(create_drain(mode), log_level).fuse(); + let drain = slog_async::Async::new(drain).build().fuse(); + + Logger::root(drain, slog::o!("version" => dfx_version_str())) +} diff --git a/src/dfx/src/main.rs b/src/dfx/src/main.rs index 9bb446a0a6..7371fa2b13 100644 --- a/src/dfx/src/main.rs +++ b/src/dfx/src/main.rs @@ -2,12 +2,11 @@ use crate::commands::CliCommand; use crate::config::{dfx_version, dfx_version_str}; use crate::lib::environment::{Environment, EnvironmentImpl}; use crate::lib::error::*; -use clap::{App, AppSettings, Arg}; +use crate::lib::logger::{create_root_logger, LoggingMode}; +use clap::{App, AppSettings, Arg, ArgMatches}; use ic_http_agent::AgentError; use slog; -use slog::Drain; -use slog_async; -use slog_term; +use std::path::PathBuf; mod commands; mod config; @@ -31,6 +30,19 @@ fn cli(_: &impl Environment) -> App<'_, '_> { .short("q") .multiple(true), ) + .arg( + Arg::with_name("logmode") + .long("log") + .takes_value(true) + .possible_values(&["stderr", "tee", "file"]) + .default_value("stderr"), + ) + .arg( + Arg::with_name("logfile") + .long("log-file") + .long("logfile") + .takes_value(true), + ) .subcommands( commands::builtin() .into_iter() @@ -111,31 +123,24 @@ fn maybe_redirect_dfx(env: &impl Environment) -> Option<()> { None } -/// Setup a logger with the proper configuration. -/// The verbose_level can be negative, in which case it's a quiet mode which removes warnings, -/// then errors entirely. -fn setup_logging(verbose_level: i64) -> slog::Logger { - let log_level = match verbose_level { - -3 => slog::Level::Critical, - -2 => slog::Level::Error, - -1 => slog::Level::Warning, - 0 => slog::Level::Info, - 1 => slog::Level::Debug, - x => { - if x > 0 { - slog::Level::Trace - } else { - return slog::Logger::root(slog::Discard, slog::o!()); - } - } +/// Setup a logger with the proper configuration, based on arguments. +/// Returns a topple of whether or not to have a progress bar, and a logger. +fn setup_logging(matches: &ArgMatches<'_>) -> (bool, slog::Logger) { + // Create a logger with our argument matches. + let level = matches.occurrences_of("verbose") as i64 - matches.occurrences_of("quiet") as i64; + + let mode = match matches.value_of("logmode") { + Some("tee") => LoggingMode::Tee(PathBuf::from( + matches.value_of("logfile").unwrap_or("log.txt"), + )), + Some("file") => LoggingMode::File(PathBuf::from( + matches.value_of("logfile").unwrap_or("log.txt"), + )), + _ => LoggingMode::Stderr, }; - let plain = slog_term::PlainSyncDecorator::new(std::io::stderr()); - let drain = lib::logger::PlainFormat::new(plain).fuse(); - let drain = slog::LevelFilter::new(drain, log_level).fuse(); - let drain = slog_async::Async::new(drain).build().fuse(); - - slog::Logger::root(drain, slog::o!("version" => dfx_version_str())) + // Only show the progress bar if the level is INFO or more. + (level >= 0, create_root_logger(level, mode)) } fn main() { @@ -147,12 +152,7 @@ fn main() { let matches = cli(&env).get_matches(); - // Create a logger with our argument matches. - let level = - matches.occurrences_of("verbose") as i64 - matches.occurrences_of("quiet") as i64; - let log = setup_logging(level); - // Only show the progress bar if the level is INFO or more. - let progress_bar = level >= 0; + let (progress_bar, log) = setup_logging(&matches); // Need to recreate the environment because we use it to get matches. // TODO(hansl): resolve this double-create problem.