Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement tracing support with associated type on LogLevel trait #124

Closed
wants to merge 11 commits into from
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,13 @@ pre-release-replacements = [
[badges]
codecov = { repository = "clap-rs/clap-verbosity-flag" }

[features]
default = ["log"]
log = ["dep:log"]

[dependencies]
log = "0.4.1"
clap = { version = "4.0.0", default-features = false, features = ["std", "derive"] }
log = { version = "0.4.1", optional = true }

[dev-dependencies]
clap = { version = "4.5.4", default-features = false, features = ["help", "usage"] }
Expand Down
276 changes: 114 additions & 162 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,16 @@
#![warn(clippy::print_stderr)]
#![warn(clippy::print_stdout)]

pub use log::Level;
pub use log::LevelFilter;
/// These types are re-exported for backwards compatibility only.
#[cfg(any(doc, feature = "log"))]
#[doc(hidden)]
pub use self::log::{ErrorLevel, InfoLevel, WarnLevel};

#[cfg(any(doc, feature = "log"))]
pub mod log;

/// Logging flags to `#[command(flatten)]` into your CLI
#[cfg(any(doc, feature = "log"))]
#[derive(clap::Args, Debug, Clone, Default)]
#[command(about = None, long_about = None)]
pub struct Verbosity<L: LogLevel = ErrorLevel> {
Expand Down Expand Up @@ -91,7 +97,40 @@ pub struct Verbosity<L: LogLevel = ErrorLevel> {
phantom: std::marker::PhantomData<L>,
}

impl<L: LogLevel> Verbosity<L> {
/// Logging flags to `#[command(flatten)]` into your CLI
#[cfg(not(any(doc, feature = "log")))]
#[derive(clap::Args, Debug, Clone, Default)]
#[command(about = None, long_about = None)]
pub struct Verbosity<L: LogLevel> {
#[arg(
long,
short = 'v',
action = clap::ArgAction::Count,
global = true,
help = L::verbose_help(),
long_help = L::verbose_long_help(),
)]
verbose: u8,

#[arg(
long,
short = 'q',
action = clap::ArgAction::Count,
global = true,
help = L::quiet_help(),
long_help = L::quiet_long_help(),
conflicts_with = "verbose",
)]
quiet: u8,

#[arg(skip)]
phantom: std::marker::PhantomData<L>,
}

impl<L: LogLevel> Verbosity<L>
where
Filter: Into<Option<L::Level>> + Into<L::LevelFilter> + From<Option<L::Level>>,
{
/// Create a new verbosity instance by explicitly setting the values
pub fn new(verbose: u8, quiet: u8) -> Self {
Verbosity {
Expand All @@ -110,63 +149,105 @@ impl<L: LogLevel> Verbosity<L> {
/// Get the log level.
///
/// `None` means all output is disabled.
pub fn log_level(&self) -> Option<Level> {
level_enum(self.verbosity())
pub fn log_level(&self) -> Option<L::Level> {
self.filter().into()
}

/// Get the log level filter.
pub fn log_level_filter(&self) -> LevelFilter {
level_enum(self.verbosity())
.map(|l| l.to_level_filter())
.unwrap_or(LevelFilter::Off)
pub fn log_level_filter(&self) -> L::LevelFilter {
self.filter().into()
}

/// If the user requested complete silence (i.e. not just no-logging).
pub fn is_silent(&self) -> bool {
self.log_level().is_none()
self.filter() == Filter::Off
}

fn verbosity(&self) -> u8 {
let default_verbosity = level_value(L::default());
let verbosity = default_verbosity as i16 - self.quiet as i16 + self.verbose as i16;
verbosity.clamp(0, u8::MAX as i16) as u8
fn filter(&self) -> Filter {
let offset = self.verbose as i16 - self.quiet as i16;
Filter::from(L::default()).with_offset(offset)
}
}

fn level_value(level: Option<Level>) -> u8 {
match level {
None => 0,
Some(Level::Error) => 1,
Some(Level::Warn) => 2,
Some(Level::Info) => 3,
Some(Level::Debug) => 4,
Some(Level::Trace) => 5,
/// An internal representation of the log level filter.
///
/// Used to calculate the log level and filter.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Filter {
joshka marked this conversation as resolved.
Show resolved Hide resolved
Off,
Error,
Warn,
Info,
Debug,
Trace,
}

impl Filter {
/// Apply an offset to the filter level.
fn with_offset(&self, offset: i16) -> Filter {
let value = self.as_usize() as i16 + offset;
const MAX_LEVEL: i16 = 5;
Self::from_usize(value.clamp(0, MAX_LEVEL) as usize)
}

/// Convert the filter to a usize for arithmetic.
///
/// usize avoids negative values (and is used in the log crate).
fn as_usize(&self) -> usize {
match self {
Filter::Off => 0,
Filter::Error => 1,
Filter::Warn => 2,
Filter::Info => 3,
Filter::Debug => 4,
Filter::Trace => 5,
}
}

/// Convert a usize back to a filter.
fn from_usize(value: usize) -> Self {
match value {
0 => Filter::Off,
1 => Filter::Error,
2 => Filter::Warn,
3 => Filter::Info,
4 => Filter::Debug,
5.. => Filter::Trace,
}
}
}

fn level_enum(verbosity: u8) -> Option<Level> {
match verbosity {
0 => None,
1 => Some(Level::Error),
2 => Some(Level::Warn),
3 => Some(Level::Info),
4 => Some(Level::Debug),
5..=u8::MAX => Some(Level::Trace),
impl fmt::Display for Filter {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have a Display impl?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verbosity had a Display impl, which previously displayed the int value. In the associated type approach, Filter needed to be pub, so this kinda made sense, but in the crate specific methods approach it can be an implementation detail instead and so just making Verbosity's Display impl is the right approach.

6c0fef9

fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Filter::Off => write!(f, "off"),
Filter::Error => write!(f, "error"),
Filter::Warn => write!(f, "warn"),
Filter::Info => write!(f, "info"),
Filter::Debug => write!(f, "debug"),
Filter::Trace => write!(f, "trace"),
}
}
}

use std::fmt;

impl<L: LogLevel> fmt::Display for Verbosity<L> {
impl<L: LogLevel> fmt::Display for Verbosity<L>
where
Filter: Into<Option<L::Level>> + Into<L::LevelFilter> + From<Option<L::Level>>,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.verbosity())
write!(f, "{}", self.filter())
}
}

/// Customize the default log-level and associated help
pub trait LogLevel {
type Level;
type LevelFilter;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no associated types used

With Filter, we shouldn't need any of this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was either use associated types, or create crate specific methods (e.g. log_level, tracing_level). I think the specific methods are a much simpler approach.

7b8b965


/// Base-line level before applying `--verbose` and `--quiet`
fn default() -> Option<Level>;
fn default() -> Option<Self::Level>;

/// Short-help message for `--verbose`
fn verbose_help() -> Option<&'static str> {
Expand All @@ -189,39 +270,6 @@ pub trait LogLevel {
}
}

/// Default to [`log::Level::Error`]
#[allow(clippy::exhaustive_structs)]
#[derive(Copy, Clone, Debug, Default)]
pub struct ErrorLevel;

impl LogLevel for ErrorLevel {
fn default() -> Option<Level> {
Some(Level::Error)
}
}

/// Default to [`log::Level::Warn`]
#[allow(clippy::exhaustive_structs)]
#[derive(Copy, Clone, Debug, Default)]
pub struct WarnLevel;

impl LogLevel for WarnLevel {
fn default() -> Option<Level> {
Some(Level::Warn)
}
}

/// Default to [`log::Level::Info`]
#[allow(clippy::exhaustive_structs)]
#[derive(Copy, Clone, Debug, Default)]
pub struct InfoLevel;

impl LogLevel for InfoLevel {
fn default() -> Option<Level> {
Some(Level::Info)
}
}

#[cfg(test)]
mod test {
use super::*;
Expand All @@ -237,100 +285,4 @@ mod test {
use clap::CommandFactory;
Cli::command().debug_assert();
}

#[test]
fn verbosity_error_level() {
let tests = [
// verbose, quiet, expected_level, expected_filter
(0, 0, Some(Level::Error), LevelFilter::Error),
(1, 0, Some(Level::Warn), LevelFilter::Warn),
(2, 0, Some(Level::Info), LevelFilter::Info),
(3, 0, Some(Level::Debug), LevelFilter::Debug),
(4, 0, Some(Level::Trace), LevelFilter::Trace),
(5, 0, Some(Level::Trace), LevelFilter::Trace),
(255, 0, Some(Level::Trace), LevelFilter::Trace),
(0, 1, None, LevelFilter::Off),
(0, 2, None, LevelFilter::Off),
(0, 255, None, LevelFilter::Off),
(255, 255, Some(Level::Error), LevelFilter::Error),
];

for (verbose, quiet, expected_level, expected_filter) in tests.iter() {
let v = Verbosity::<ErrorLevel>::new(*verbose, *quiet);
assert_eq!(
v.log_level(),
*expected_level,
"verbose = {verbose}, quiet = {quiet}"
);
assert_eq!(
v.log_level_filter(),
*expected_filter,
"verbose = {verbose}, quiet = {quiet}"
);
}
}

#[test]
fn verbosity_warn_level() {
let tests = [
// verbose, quiet, expected_level, expected_filter
(0, 0, Some(Level::Warn), LevelFilter::Warn),
(1, 0, Some(Level::Info), LevelFilter::Info),
(2, 0, Some(Level::Debug), LevelFilter::Debug),
(3, 0, Some(Level::Trace), LevelFilter::Trace),
(4, 0, Some(Level::Trace), LevelFilter::Trace),
(255, 0, Some(Level::Trace), LevelFilter::Trace),
(0, 1, Some(Level::Error), LevelFilter::Error),
(0, 2, None, LevelFilter::Off),
(0, 3, None, LevelFilter::Off),
(0, 255, None, LevelFilter::Off),
(255, 255, Some(Level::Warn), LevelFilter::Warn),
];

for (verbose, quiet, expected_level, expected_filter) in tests.iter() {
let v = Verbosity::<WarnLevel>::new(*verbose, *quiet);
assert_eq!(
v.log_level(),
*expected_level,
"verbose = {verbose}, quiet = {quiet}"
);
assert_eq!(
v.log_level_filter(),
*expected_filter,
"verbose = {verbose}, quiet = {quiet}"
);
}
}

#[test]
fn verbosity_info_level() {
let tests = [
// verbose, quiet, expected_level, expected_filter
(0, 0, Some(Level::Info), LevelFilter::Info),
(1, 0, Some(Level::Debug), LevelFilter::Debug),
(2, 0, Some(Level::Trace), LevelFilter::Trace),
(3, 0, Some(Level::Trace), LevelFilter::Trace),
(255, 0, Some(Level::Trace), LevelFilter::Trace),
(0, 1, Some(Level::Warn), LevelFilter::Warn),
(0, 2, Some(Level::Error), LevelFilter::Error),
(0, 3, None, LevelFilter::Off),
(0, 4, None, LevelFilter::Off),
(0, 255, None, LevelFilter::Off),
(255, 255, Some(Level::Info), LevelFilter::Info),
];

for (verbose, quiet, expected_level, expected_filter) in tests.iter() {
let v = Verbosity::<InfoLevel>::new(*verbose, *quiet);
assert_eq!(
v.log_level(),
*expected_level,
"verbose = {verbose}, quiet = {quiet}"
);
assert_eq!(
v.log_level_filter(),
*expected_filter,
"verbose = {verbose}, quiet = {quiet}"
);
}
}
}
Loading