fix: make logger work while loading config file (#581)

This commit is contained in:
SteveLauC
2023-10-17 11:19:47 +08:00
committed by GitHub
parent 6b871e7949
commit a8c29fd1a2
5 changed files with 122 additions and 59 deletions

View File

@@ -10,6 +10,8 @@ use color_eyre::eyre::Context;
use crate::error::TopgradeError; use crate::error::TopgradeError;
use tracing::debug;
/// Like [`Output`], but UTF-8 decoded. /// Like [`Output`], but UTF-8 decoded.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Utf8Output { pub struct Utf8Output {
@@ -183,7 +185,7 @@ impl CommandExt for Command {
let err = TopgradeError::ProcessFailedWithOutput(program, output.status, stderr.into_owned()); let err = TopgradeError::ProcessFailedWithOutput(program, output.status, stderr.into_owned());
let ret = Err(err).with_context(|| message); let ret = Err(err).with_context(|| message);
tracing::debug!("Command failed: {ret:?}"); debug!("Command failed: {ret:?}");
ret ret
} }
} }
@@ -203,7 +205,7 @@ impl CommandExt for Command {
let (program, _) = get_program_and_args(self); let (program, _) = get_program_and_args(self);
let err = TopgradeError::ProcessFailed(program, status); let err = TopgradeError::ProcessFailed(program, status);
let ret = Err(err).with_context(|| format!("Command failed: `{command}`")); let ret = Err(err).with_context(|| format!("Command failed: `{command}`"));
tracing::debug!("Command failed: {ret:?}"); debug!("Command failed: {ret:?}");
ret ret
} }
} }
@@ -239,6 +241,6 @@ fn format_program_and_args(cmd: &Command) -> String {
fn log(cmd: &Command) -> String { fn log(cmd: &Command) -> String {
let command = format_program_and_args(cmd); let command = format_program_and_args(cmd);
tracing::debug!("Executing command `{command}`"); debug!("Executing command `{command}`");
command command
} }

View File

@@ -17,17 +17,19 @@ use regex::Regex;
use regex_split::RegexSplit; use regex_split::RegexSplit;
use serde::Deserialize; use serde::Deserialize;
use strum::{EnumIter, EnumString, EnumVariantNames, IntoEnumIterator}; use strum::{EnumIter, EnumString, EnumVariantNames, IntoEnumIterator};
use tracing::debug;
use which_crate::which; use which_crate::which;
use super::utils::{editor, hostname};
use crate::command::CommandExt; use crate::command::CommandExt;
use crate::sudo::SudoKind; use crate::sudo::SudoKind;
use crate::utils::string_prepend_str; use crate::utils::string_prepend_str;
use tracing::{debug, error};
use super::utils::{editor, hostname};
pub static EXAMPLE_CONFIG: &str = include_str!("../config.example.toml"); pub static EXAMPLE_CONFIG: &str = include_str!("../config.example.toml");
/// Topgrade's default log level.
pub const DEFAULT_LOG_LEVEL: &str = "warn";
#[allow(unused_macros)] #[allow(unused_macros)]
macro_rules! str_value { macro_rules! str_value {
($section:ident, $value:ident) => { ($section:ident, $value:ident) => {
@@ -607,11 +609,11 @@ impl ConfigFile {
*/ */
for include in dir_include { for include in dir_include {
let include_contents = fs::read_to_string(&include).map_err(|e| { let include_contents = fs::read_to_string(&include).map_err(|e| {
tracing::error!("Unable to read {}", include.display()); error!("Unable to read {}", include.display());
e e
})?; })?;
let include_contents_parsed = toml::from_str(include_contents.as_str()).map_err(|e| { let include_contents_parsed = toml::from_str(include_contents.as_str()).map_err(|e| {
tracing::error!("Failed to deserialize {}", include.display()); error!("Failed to deserialize {}", include.display());
e e
})?; })?;
@@ -628,7 +630,7 @@ impl ConfigFile {
} }
let mut contents_non_split = fs::read_to_string(&config_path).map_err(|e| { let mut contents_non_split = fs::read_to_string(&config_path).map_err(|e| {
tracing::error!("Unable to read {}", config_path.display()); error!("Unable to read {}", config_path.display());
e e
})?; })?;
@@ -641,7 +643,7 @@ impl ConfigFile {
for contents in contents_split { for contents in contents_split {
let config_file_include_only: ConfigFileIncludeOnly = toml::from_str(contents).map_err(|e| { let config_file_include_only: ConfigFileIncludeOnly = toml::from_str(contents).map_err(|e| {
tracing::error!("Failed to deserialize an include section of {}", config_path.display()); error!("Failed to deserialize an include section of {}", config_path.display());
e e
})?; })?;
@@ -654,28 +656,24 @@ impl ConfigFile {
let include_contents = match fs::read_to_string(&include_path) { let include_contents = match fs::read_to_string(&include_path) {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
tracing::error!("Unable to read {}: {}", include_path.display(), e); error!("Unable to read {}: {}", include_path.display(), e);
continue; continue;
} }
}; };
match toml::from_str::<Self>(&include_contents) { match toml::from_str::<Self>(&include_contents) {
Ok(include_parsed) => result.merge(include_parsed), Ok(include_parsed) => result.merge(include_parsed),
Err(e) => { Err(e) => {
tracing::error!("Failed to deserialize {}: {}", include_path.display(), e); error!("Failed to deserialize {}: {}", include_path.display(), e);
continue; continue;
} }
}; };
debug!("Configuration include found: {}", include_path.display());
} }
} else {
debug!("No include paths found in {}", config_path.display());
} }
} }
match toml::from_str::<Self>(contents) { match toml::from_str::<Self>(contents) {
Ok(contents) => result.merge(contents), Ok(contents) => result.merge(contents),
Err(e) => tracing::error!("Failed to deserialize {}: {}", config_path.display(), e), Err(e) => error!("Failed to deserialize {}: {}", config_path.display(), e),
} }
} }
@@ -713,8 +711,6 @@ impl ConfigFile {
} }
} }
debug!("Loaded configuration: {:?}", result);
Ok(result) Ok(result)
} }
@@ -832,7 +828,7 @@ pub struct CommandLineArgs {
/// Tracing filter directives. /// Tracing filter directives.
/// ///
/// See: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/struct.EnvFilter.html /// See: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/struct.EnvFilter.html
#[clap(long, default_value = "warn")] #[clap(long, default_value = DEFAULT_LOG_LEVEL)]
pub log_filter: String, pub log_filter: String,
/// Print completion script for the given shell and exit /// Print completion script for the given shell and exit
@@ -860,6 +856,27 @@ impl CommandLineArgs {
pub fn env_variables(&self) -> &Vec<String> { pub fn env_variables(&self) -> &Vec<String> {
&self.env &self.env
} }
/// In Topgrade, filter directives come from 3 places:
/// 1. CLI option `--log-filter`
/// 2. Config file
/// 3. `debug` if the `--verbose` option is present
///
/// Before loading the configuration file, we need our logger to work, so this
/// function will return directives coming from part 1 and 2.
///
///
/// When the configuration file is loaded, `Config::tracing_filter_directives()`
/// will return all the 3 parts.
pub fn tracing_filter_directives(&self) -> String {
let mut ret = self.log_filter.clone();
if self.verbose {
ret.push(',');
ret.push_str("debug");
}
ret
}
} }
/// Represents the application configuration /// Represents the application configuration
@@ -867,6 +884,7 @@ impl CommandLineArgs {
/// The struct holds the loaded configuration file, as well as the arguments parsed from the command line. /// The struct holds the loaded configuration file, as well as the arguments parsed from the command line.
/// Its provided methods decide the appropriate options based on combining the configuration file and the /// Its provided methods decide the appropriate options based on combining the configuration file and the
/// command line arguments. /// command line arguments.
#[derive(Debug)]
pub struct Config { pub struct Config {
opt: CommandLineArgs, opt: CommandLineArgs,
config_file: ConfigFile, config_file: ConfigFile,
@@ -883,7 +901,7 @@ impl Config {
ConfigFile::read(opt.config.clone()).unwrap_or_else(|e| { ConfigFile::read(opt.config.clone()).unwrap_or_else(|e| {
// Inform the user about errors when loading the configuration, // Inform the user about errors when loading the configuration,
// but fallback to the default config to at least attempt to do something // but fallback to the default config to at least attempt to do something
tracing::error!("failed to load configuration: {}", e); error!("failed to load configuration: {}", e);
ConfigFile::default() ConfigFile::default()
}) })
} else { } else {
@@ -1442,6 +1460,14 @@ impl Config {
self.opt.verbose self.opt.verbose
} }
/// After loading the config file, filter directives consist of 3 parts:
///
/// 1. directives from the configuration file
/// 2. directives from the CLI options `--log-filter`
/// 3. `debug`, which would be enabled if the `--verbose` option is present
///
/// Previous directive will be overwritten if a directive with the same target
/// appear later.
pub fn tracing_filter_directives(&self) -> String { pub fn tracing_filter_directives(&self) -> String {
let mut ret = String::new(); let mut ret = String::new();
if let Some(directives) = self.config_file.misc.as_ref().and_then(|m| m.log_filters.as_ref()) { if let Some(directives) = self.config_file.misc.as_ref().and_then(|m| m.log_filters.as_ref()) {

View File

@@ -1,5 +1,6 @@
//! A stub for Ctrl + C handling. //! A stub for Ctrl + C handling.
use crate::ctrlc::interrupted::set_interrupted; use crate::ctrlc::interrupted::set_interrupted;
use tracing::error;
use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE}; use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE};
use winapi::um::consoleapi::SetConsoleCtrlHandler; use winapi::um::consoleapi::SetConsoleCtrlHandler;
use winapi::um::wincon::CTRL_C_EVENT; use winapi::um::wincon::CTRL_C_EVENT;
@@ -16,6 +17,6 @@ extern "system" fn handler(ctrl_type: DWORD) -> BOOL {
pub fn set_handler() { pub fn set_handler() {
if 0 == unsafe { SetConsoleCtrlHandler(Some(handler), TRUE) } { if 0 == unsafe { SetConsoleCtrlHandler(Some(handler), TRUE) } {
tracing::error!("Cannot set a control C handler") error!("Cannot set a control C handler")
} }
} }

View File

@@ -26,6 +26,8 @@ use self::error::Upgraded;
use self::steps::{remote::*, *}; use self::steps::{remote::*, *};
use self::terminal::*; use self::terminal::*;
use self::utils::{install_color_eyre, install_tracing, update_tracing};
mod command; mod command;
mod config; mod config;
mod ctrlc; mod ctrlc;
@@ -53,6 +55,18 @@ fn run() -> Result<()> {
ctrlc::set_handler(); ctrlc::set_handler();
let opt = CommandLineArgs::parse(); let opt = CommandLineArgs::parse();
// Set up the logger with the filter directives from:
// 1. CLI option `--log-filter`
// 2. `debug` if the `--verbose` option is present
// We do this because we need our logger to work while loading the
// configuration file.
//
// When the configuration file is loaded, update the logger with the full
// filter directives.
//
// For more info, see the comments in `CommandLineArgs::tracing_filter_directives()`
// and `Config::tracing_filter_directives()`.
let reload_handle = install_tracing(&opt.tracing_filter_directives())?;
if let Some(shell) = opt.gen_completion { if let Some(shell) = opt.gen_completion {
let cmd = &mut CommandLineArgs::command(); let cmd = &mut CommandLineArgs::command();
@@ -84,7 +98,8 @@ fn run() -> Result<()> {
} }
let config = Config::load(opt)?; let config = Config::load(opt)?;
install_tracing(&config.tracing_filter_directives())?; // Update the logger with the full filter directives.
update_tracing(&reload_handle, &config.tracing_filter_directives())?;
set_title(config.set_title()); set_title(config.set_title());
display_time(config.display_time()); display_time(config.display_time());
set_desktop_notifications(config.notify_each_step()); set_desktop_notifications(config.notify_each_step());
@@ -94,6 +109,7 @@ fn run() -> Result<()> {
debug!("{:?}", std::env::args()); debug!("{:?}", std::env::args());
debug!("Binary path: {:?}", std::env::current_exe()); debug!("Binary path: {:?}", std::env::current_exe());
debug!("Self Update: {:?}", cfg!(feature = "self-update")); debug!("Self Update: {:?}", cfg!(feature = "self-update"));
debug!("Configuration: {:?}", config);
if config.run_in_tmux() && env::var("TOPGRADE_INSIDE_TMUX").is_err() { if config.run_in_tmux() && env::var("TOPGRADE_INSIDE_TMUX").is_err() {
#[cfg(unix)] #[cfg(unix)]
@@ -565,39 +581,3 @@ fn main() {
} }
} }
} }
fn install_tracing(filter_directives: &str) -> Result<()> {
use tracing_subscriber::fmt;
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
let env_filter = EnvFilter::try_new(filter_directives)
.or_else(|_| EnvFilter::try_from_default_env())
.or_else(|_| EnvFilter::try_new("info"))?;
let fmt_layer = fmt::layer()
.with_target(false)
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
.without_time();
let registry = tracing_subscriber::registry();
registry.with(env_filter).with(fmt_layer).init();
Ok(())
}
fn install_color_eyre() -> Result<()> {
color_eyre::config::HookBuilder::new()
// Don't display the backtrace reminder by default:
// Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
// Run with RUST_BACKTRACE=full to include source snippets.
.display_env_section(false)
// Display location information by default:
// Location:
// src/steps.rs:92
.display_location_section(true)
.install()
}

View File

@@ -5,9 +5,17 @@ use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use tracing::{debug, error}; use tracing::{debug, error};
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::reload::{Handle, Layer};
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{fmt, Registry};
use tracing_subscriber::{registry, EnvFilter};
use crate::command::CommandExt; use crate::command::CommandExt;
use crate::config::DEFAULT_LOG_LEVEL;
use crate::error::SkipStep; use crate::error::SkipStep;
pub trait PathExt pub trait PathExt
@@ -251,3 +259,49 @@ pub fn check_is_python_2_or_shim(python: PathBuf) -> Result<PathBuf> {
Ok(python) Ok(python)
} }
/// Set up the tracing logger
///
/// # Return value
/// A reload handle will be returned so that we can change the log level at
/// runtime.
pub fn install_tracing(filter_directives: &str) -> Result<Handle<EnvFilter, Registry>> {
let env_filter = EnvFilter::try_new(filter_directives)
.or_else(|_| EnvFilter::try_from_default_env())
.or_else(|_| EnvFilter::try_new(DEFAULT_LOG_LEVEL))?;
let fmt_layer = fmt::layer()
.with_target(false)
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
.without_time();
let (filter, reload_handle) = Layer::new(env_filter);
registry().with(filter).with(fmt_layer).init();
Ok(reload_handle)
}
/// Update the tracing logger with new `filter_directives`.
pub fn update_tracing(reload_handle: &Handle<EnvFilter, Registry>, filter_directives: &str) -> Result<()> {
let new = EnvFilter::try_new(filter_directives)
.or_else(|_| EnvFilter::try_from_default_env())
.or_else(|_| EnvFilter::try_new(DEFAULT_LOG_LEVEL))?;
reload_handle.modify(|old| *old = new)?;
Ok(())
}
/// Set up the error handler crate
pub fn install_color_eyre() -> Result<()> {
color_eyre::config::HookBuilder::new()
// Don't display the backtrace reminder by default:
// Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
// Run with RUST_BACKTRACE=full to include source snippets.
.display_env_section(false)
// Display location information by default:
// Location:
// src/steps.rs:92
.display_location_section(true)
.install()
}