Customize verbosity (#956)

This commit is contained in:
Matthias Endler 2023-02-24 23:53:09 +01:00 committed by GitHub
parent 1350a0bdf6
commit 7874195bbb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 159 additions and 163 deletions

View file

@ -1,7 +1,7 @@
############################# Display #############################
# Verbose program output
verbose = true
verbose = "warn"
# Don't show interactive progress bar while checking links.
no_progress = true

View file

@ -12,7 +12,7 @@ use tokio_stream::wrappers::ReceiverStream;
use tokio_stream::StreamExt;
use crate::formatters::response::ResponseFormatter;
use crate::verbosity::{Verbosity, WarnLevel};
use crate::verbosity::Verbosity;
use crate::{cache::Cache, stats::ResponseStats, ExitCode};
use lychee_lib::{Client, Request, Response};
@ -28,7 +28,7 @@ where
let (send_req, recv_req) = mpsc::channel(params.cfg.max_concurrency);
let (send_resp, recv_resp) = mpsc::channel(params.cfg.max_concurrency);
let max_concurrency = params.cfg.max_concurrency;
let stats = if params.cfg.verbose.is_verbose() {
let stats = if params.cfg.verbose.log_level() >= log::Level::Info {
ResponseStats::extended()
} else {
ResponseStats::default()
@ -113,7 +113,7 @@ where
/// Reads from the request channel and updates the progress bar status
async fn progress_bar_task(
mut recv_resp: mpsc::Receiver<Response>,
verbose: Verbosity<WarnLevel>,
verbose: Verbosity,
pb: Option<ProgressBar>,
formatter: Arc<Box<dyn ResponseFormatter>>,
mut stats: ResponseStats,
@ -212,16 +212,16 @@ fn show_progress(
progress_bar: &Option<ProgressBar>,
response: &Response,
formatter: &Arc<Box<dyn ResponseFormatter>>,
verbose: &Verbosity<WarnLevel>,
verbose: &Verbosity,
) -> Result<()> {
let out = formatter.write_response(response)?;
if let Some(pb) = progress_bar {
pb.inc(1);
pb.set_message(out.clone());
if verbose.is_verbose() {
if verbose.log_level() >= log::Level::Info {
pb.println(out);
}
} else if verbose.is_verbose()
} else if verbose.log_level() >= log::Level::Info
|| (!response.status().is_success() && !response.status().is_excluded())
{
writeln!(output, "{out}")?;
@ -255,11 +255,30 @@ mod tests {
&None,
&response,
&formatter,
&Verbosity::new(0, 0),
&Verbosity::default(),
)
.unwrap();
info!("{:?}", String::from_utf8_lossy(&buf));
assert!(buf.is_empty());
}
#[test]
fn test_show_cached_responses_in_progress_debug_output() {
let mut buf = Vec::new();
let response = Response(
InputSource::Stdin,
ResponseBody {
uri: Uri::try_from("http://127.0.0.1").unwrap(),
status: Status::Cached(CacheStatus::Ok(200)),
},
);
let formatter: Arc<Box<dyn ResponseFormatter>> =
Arc::new(Box::new(formatters::response::Raw::new()));
show_progress(&mut buf, &None, &response, &formatter, &Verbosity::debug()).unwrap();
assert!(!buf.is_empty());
let buf = String::from_utf8_lossy(&buf);
assert_eq!(buf, "↻ [200] http://127.0.0.1/ | Cached: OK (cached)\n");
}
}

View file

@ -7,7 +7,6 @@ use std::path::PathBuf;
use tokio_stream::StreamExt;
use crate::verbosity::Verbosity;
use crate::verbosity::WarnLevel;
use crate::ExitCode;
use super::CommandParams;
@ -57,7 +56,7 @@ where
let excluded = params.client.is_excluded(&request.uri);
if excluded && !params.cfg.verbose.is_verbose() {
if excluded && params.cfg.verbose.log_level() < log::Level::Info {
continue;
}
if let Err(e) = write(&mut writer, &request, &params.cfg.verbose, excluded) {
@ -75,17 +74,17 @@ where
fn write(
writer: &mut Box<dyn Write>,
request: &Request,
verbosity: &Verbosity<WarnLevel>,
verbosity: &Verbosity,
excluded: bool,
) -> io::Result<()> {
// Only print `data:` URIs if verbose mode is enabled
if request.uri.is_data() && !verbosity.is_verbose() {
// Only print `data:` URIs if verbose mode is at least `info`.
if request.uri.is_data() && verbosity.log_level() < log::Level::Info {
return Ok(());
}
let request = if verbosity.is_verbose() {
// Only print source in verbose mode. This way the normal link output
// can be fed into another tool without data mangling.
// Only print source if verbose mode is at least `info`. This way the normal
// link output can be fed into another tool without data mangling.
let request = if verbosity.log_level() >= log::Level::Info {
request.to_string()
} else {
request.uri.to_string()

View file

@ -303,7 +303,7 @@ async fn run(opts: &LycheeOptions) -> Result<i32> {
if let Some(output) = &opts.config.output {
fs::write(output, formatted).context("Cannot write status output to file")?;
} else {
if opts.config.verbose.log_level() == Some(log::Level::Debug) && !is_empty {
if opts.config.verbose.log_level() >= log::Level::Info && !is_empty {
// separate summary from the verbose list of links above
// with a newline
writeln!(io::stdout())?;

View file

@ -1,5 +1,5 @@
use crate::parse::{parse_base, parse_statuscodes};
use crate::verbosity::{Verbosity, WarnLevel};
use crate::verbosity::Verbosity;
use anyhow::{anyhow, Context, Error, Result};
use clap::{arg, Parser};
use const_format::{concatcp, formatcp};
@ -83,7 +83,7 @@ default_function! {
timeout: usize = DEFAULT_TIMEOUT_SECS;
retry_wait_time: usize = DEFAULT_RETRY_WAIT_TIME_SECS;
method: String = DEFAULT_METHOD.to_string();
verbosity: Verbosity<WarnLevel> = Verbosity::new(1, 0);
verbosity: Verbosity = Verbosity::default();
}
// Macro for merging configuration values
@ -144,7 +144,7 @@ pub(crate) struct Config {
/// Verbose program output
#[clap(flatten)]
#[serde(default = "verbosity")]
pub(crate) verbose: Verbosity<WarnLevel>,
pub(crate) verbose: Verbosity,
/// Do not show progress bar.
/// This is recommended for non-interactive shells (e.g. for continuous integration)
@ -365,7 +365,7 @@ impl Config {
self, toml;
// Keys with defaults to assign
verbose: Verbosity::new(1, 0);
verbose: Verbosity::default();
cache: false;
no_progress: false;
max_redirects: DEFAULT_MAX_REDIRECTS;

View file

@ -1,202 +1,165 @@
//! Control `log` level with a `--verbose` flag for your CLI
//!
//! The original source is from
//! [this crate](https://github.com/clap-rs/clap-verbosity-flag).
//! Modifications were made to add support for serializing the `Verbosity`
//! struct and to add a convenience method to get the verbosity status.
//! Easily add a `--verbose` flag to CLIs using Structopt
//!
//! # Examples
//!
//! To get `--quiet` and `--verbose` flags through your entire program, just `flatten`
//! [`Verbosity`]:
//! ```rust,no_run
//! # use clap::Parser;
//! # use clap_verbosity_flag::Verbosity;
//! #
//! # /// Le CLI
//! # #[derive(Debug, Parser)]
//! # struct Cli {
//! #[command(flatten)]
//! verbose: Verbosity,
//! # }
//! ```
//! use clap::Parser;
//! use clap_verbosity_flag::Verbosity;
//!
//! /// Le CLI
//! #[derive(Debug, Parser)]
//! struct Cli {
//! #[command(flatten)]
//! verbose: Verbosity,
//! }
//!
//! You can then use this to configure your logger:
//! ```rust,no_run
//! # use clap::Parser;
//! # use clap_verbosity_flag::Verbosity;
//! #
//! # /// Le CLI
//! # #[derive(Debug, Parser)]
//! # struct Cli {
//! # #[command(flatten)]
//! # verbose: Verbosity,
//! # }
//! let cli = Cli::parse();
//! env_logger::Builder::new()
//! .filter_level(cli.verbose.log_level_filter())
//! .init();
//! ```
//!
//! By default, this will only report errors.
//! This will only report errors.
//! - `-q` silences output
//! - `-v` show warnings
//! - `-vv` show info
//! - `-vvv` show debug
//! - `-vvvv` show trace
//!
//! You can also customize the default logging level:
//! ```rust,no_run
//! # use clap::Parser;
//! use clap_verbosity_flag::{Verbosity, InfoLevel};
//!
//! /// Le CLI
//! #[derive(Debug, Parser)]
//! struct Cli {
//! #[command(flatten)]
//! verbose: Verbosity<InfoLevel>,
//! }
//! ```
//!
//! Or implement [`LogLevel`] yourself for more control.
#[derive(clap::Args, Eq, PartialEq, Debug, Deserialize, Clone)]
pub(crate) struct Verbosity<L: LogLevel = ErrorLevel> {
#[clap(
use log::Level;
use log::LevelFilter;
use serde::Deserialize;
#[derive(clap::Args, Debug, Default, Clone, PartialEq, Eq)]
pub(crate) struct Verbosity {
/// Pass many times for more log output
///
/// By default, it'll only report errors and warnings.
/// Passing `-v` one time also prints info,
/// `-vv` enables debugging logging, `-vvv` trace.
#[arg(
long,
short = 'v',
action = clap::ArgAction::Count,
global = true,
help = L::verbose_help(),
long_help = L::verbose_long_help(),
help = Self::verbose_help(),
long_help = Self::verbose_long_help(),
conflicts_with = "quiet",
)]
verbose: u8,
#[clap(
#[arg(
long,
short = 'q',
action = clap::ArgAction::Count,
global = true,
help = L::quiet_help(),
long_help = L::quiet_long_help(),
help = Self::quiet_help(),
long_help = Self::quiet_long_help(),
conflicts_with = "verbose",
)]
quiet: u8,
#[clap(skip)]
phantom: std::marker::PhantomData<L>,
}
impl<L: LogLevel> Verbosity<L> {
/// Create a new verbosity instance by explicitly setting the values
pub(crate) const fn new(verbose: u8, quiet: u8) -> Self {
Verbosity {
verbose,
quiet,
phantom: std::marker::PhantomData,
}
}
impl Verbosity {
/// Get the log level.
///
/// `None` means all output is disabled.
pub(crate) fn log_level(&self) -> Option<log::Level> {
pub(crate) const fn log_level(&self) -> Level {
level_enum(self.verbosity())
}
/// Get the log level filter.
pub(crate) fn log_level_filter(&self) -> log::LevelFilter {
level_enum(self.verbosity()).map_or(log::LevelFilter::Off, |l| l.to_level_filter())
pub(crate) fn log_level_filter(&self) -> LevelFilter {
level_enum(self.verbosity()).to_level_filter()
}
/// Shorthand to check if the user requested "more verbose" output
/// (assuming the default log level is `Warn`)
pub(crate) fn is_verbose(&self) -> bool {
self.log_level() >= log::LevelFilter::Info.to_level()
#[allow(clippy::cast_possible_wrap)]
const fn verbosity(&self) -> i8 {
level_value(log::Level::Warn) - (self.quiet as i8) + (self.verbose as i8)
}
fn verbosity(&self) -> i8 {
#![allow(clippy::cast_possible_wrap)]
level_value(L::default()) - (self.quiet as i8) + (self.verbose as i8)
const fn verbose_help() -> &'static str {
"Set verbosity level; more output per occurrence (e.g. `-v` or `-vv`)"
}
const fn verbose_long_help() -> Option<&'static str> {
None
}
const fn quiet_help() -> &'static str {
"Less output per occurrence (e.g. `-q` or `-qq`)"
}
const fn quiet_long_help() -> Option<&'static str> {
None
}
}
const fn level_value(level: Option<log::Level>) -> i8 {
#[cfg(test)]
impl Verbosity {
pub(crate) const fn debug() -> Self {
Self {
#[allow(clippy::cast_sign_loss)]
verbose: level_value(log::Level::Debug) as u8,
quiet: 0,
}
}
}
// Implement Deserialize for `Verbosity`
// This can be deserialized from a string like "warn", "warning", or "Warning"
// for example
impl<'de> Deserialize<'de> for Verbosity {
#[allow(clippy::cast_sign_loss)]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let level = match s.to_lowercase().as_str() {
"error" => Level::Error,
"warn" | "warning" => Level::Warn,
"info" => Level::Info,
"debug" => Level::Debug,
"trace" => Level::Trace,
level => {
return Err(serde::de::Error::custom(format!(
"invalid log level `{level}`"
)))
}
};
Ok(Verbosity {
verbose: level_value(level) as u8,
quiet: 0,
})
}
}
const fn level_value(level: Level) -> i8 {
match level {
None => -1,
Some(log::Level::Error) => 0,
Some(log::Level::Warn) => 1,
Some(log::Level::Info) => 2,
Some(log::Level::Debug) => 3,
Some(log::Level::Trace) => 4,
log::Level::Error => 0,
log::Level::Warn => 1,
log::Level::Info => 2,
log::Level::Debug => 3,
log::Level::Trace => 4,
}
}
const fn level_enum(verbosity: i8) -> Option<log::Level> {
const fn level_enum(verbosity: i8) -> log::Level {
match verbosity {
std::i8::MIN..=-1 => None,
0 => Some(log::Level::Error),
1 => Some(log::Level::Warn),
2 => Some(log::Level::Info),
3 => Some(log::Level::Debug),
4..=std::i8::MAX => Some(log::Level::Trace),
0 => log::Level::Error,
1 => log::Level::Warn,
2 => log::Level::Info,
3 => log::Level::Debug,
_ => log::Level::Trace,
}
}
use std::fmt;
use serde::Deserialize;
impl<L: LogLevel> fmt::Display for Verbosity<L> {
impl fmt::Display for Verbosity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.verbosity())
}
}
pub(crate) trait LogLevel {
fn default() -> Option<log::Level>;
fn verbose_help() -> Option<&'static str> {
Some("Set verbosity level; more output per occurrence (e.g. `-v` or `-vv`)")
}
fn verbose_long_help() -> Option<&'static str> {
None
}
fn quiet_help() -> Option<&'static str> {
Some("Less output per occurrence (e.g. `-q` or `-qq`)")
}
fn quiet_long_help() -> Option<&'static str> {
None
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub(crate) struct ErrorLevel;
impl LogLevel for ErrorLevel {
fn default() -> Option<log::Level> {
Some(log::Level::Error)
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub(crate) struct WarnLevel;
impl LogLevel for WarnLevel {
fn default() -> Option<log::Level> {
Some(log::Level::Warn)
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub(crate) struct InfoLevel;
impl LogLevel for InfoLevel {
fn default() -> Option<log::Level> {
Some(log::Level::Info)
write!(f, "{}", self.verbose)
}
}
@ -215,4 +178,11 @@ mod test {
use clap::CommandFactory;
Cli::command().debug_assert();
}
#[test]
fn test_default_log_level() {
let verbosity = Verbosity::default();
assert_eq!(verbosity.log_level(), Level::Warn);
assert!(verbosity.log_level() >= Level::Warn);
}
}

View file

@ -9,7 +9,7 @@ use crate::{ErrorKind, InputSource};
/// Both, local and remote targets are supported.
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
#[allow(variant_size_differences)]
#[serde(try_from = "&str")]
#[serde(try_from = "String")]
pub enum Base {
/// Local file path pointing to root directory
Local(PathBuf),
@ -72,6 +72,14 @@ impl TryFrom<&str> for Base {
}
}
impl TryFrom<String> for Base {
type Error = ErrorKind;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_from(value.as_str())
}
}
#[cfg(test)]
mod test_base {
use crate::Result;