-
Notifications
You must be signed in to change notification settings - Fork 24
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
Tracing support #121
Comments
Moving built-in functionality into a default feature is not backwards compatible. |
Thank you for taking the time for stepping through and evaluating the different sides to this that need to be addressed for being able to move this forward as well as including a recommendation.
When I said that, I hadn't accounted for clap's API stability which seems to be less than However, the compounding affect of more crates in a stable API is not ideal. If It didn't help that it came months within a breaking release as I'm getting pressure from people on other projects to not issue breaking releases too often. Its likely been sufficient time since the last breaking release that we could consider one (for making So the main question is whether to make a breaking release with |
I'm fairly bullish on low impact breaking changes for things which fit generally into the developer tooling category. Mainly because the people impacted can fix things fairly quickly, especially when the churn is low.
Mainly because so far there's just been a single version of both tracing (5 years) and log (7 years), so a versioned feature flag doesn't seem like it adds much value. This is the sort of general rust wide problem though, so perhaps there's a general feel for what is the right thing to do.
Yes but here that only breaks those that are actively using In taking a look at this though, the main problem with making log optional is that the LogLevel trait's default function, which returns |
I know people who always set
Something I regret is focusing on This is not true for So my current thought
Nothing should need to be changed in our CI workflows because we use |
That all sounds about right to me. The other thing I'd add is notifying the various repos that implement LogLevel of the replacements (and the related change for the added log levels. It's not a huge number (24, 32 if you include forks), and the users may appreciate the heads up. |
Hmm, wasn't thinking of that part of this sigh maybe we need a |
That makes more sense |
Thinking more on this. If a breaking change is an option, then we can do better than using a number to indicate the verbosity and make LogLevel generic over the type of level filter instead. This would be breaking for any implementations of LogLevel, but mostly code compatible with apps that don't implement this manually. Taking a look over the 24 repos that do implement the trait, most are just implementing Debug, Trace, Off etc. 2 or 3 implement feature flag / config -> default log level, and 4 or 5 implement custom help messages, so any transition effort should be fairly small (at least in public code). This is working prototype code that demonstrates making the Note that this changes the The main benefit of this is that if you want log support, you just get log support, if you want tracing support, you just get tracing support. #![allow(unused)]
use clap::{ArgAction, Args, Parser};
use log::LevelFilter as LogLevelFilter;
use tracing::{level_filters::LevelFilter as TracingLevelFilter, Level as TracingLevel};
fn main() {
let args = DefaultCli::parse();
dbg!(args.verbosity.log_level_filter());
let args = LogCli::parse();
dbg!(args.verbosity.log_level_filter());
let args = TracingCli::parse();
dbg!(args.verbosity.tracing_level_filter());
}
#[derive(Debug, Parser)]
struct DefaultCli {
#[command(flatten)]
verbosity: Verbosity,
}
#[derive(Debug, Parser)]
struct LogCli {
#[command(flatten)]
verbosity: Verbosity<ErrorLevel>,
}
#[derive(Debug, Parser)]
struct TracingCli {
#[command(flatten)]
verbosity: Verbosity<TracingErrorLevel>,
}
#[derive(Debug, Clone, Default, Args)]
#[command(about = None, long_about = None)]
struct Verbosity<L: LogLevel = ErrorLevel> {
#[arg(
long,
short = 'v',
action = ArgAction::Count,
global = true,
)]
verbose: u8,
#[arg(
long,
short = 'q',
action = ArgAction::Count,
global = true,
conflicts_with = "verbose",
)]
quiet: u8,
#[arg(skip)]
phantom: std::marker::PhantomData<L>,
}
impl<L: LogLevel<LevelFilter = LogLevelFilter>> Verbosity<L> {
pub fn log_level(&self) -> Option<log::Level> {
self.log_level_filter().to_level()
}
pub fn log_level_filter(&self) -> LogLevelFilter {
let verbosity = Self::verbosity() as i16 - self.quiet as i16 + self.verbose as i16;
let verbosity = verbosity.clamp(0, 5);
match verbosity {
0 => LogLevelFilter::Off,
1 => LogLevelFilter::Error,
2 => LogLevelFilter::Warn,
3 => LogLevelFilter::Info,
4 => LogLevelFilter::Debug,
5 => LogLevelFilter::Trace,
_ => unreachable!(),
}
}
fn verbosity() -> u8 {
match L::default_level_filter() {
LogLevelFilter::Off => 0,
LogLevelFilter::Error => 1,
LogLevelFilter::Warn => 2,
LogLevelFilter::Info => 3,
LogLevelFilter::Debug => 4,
LogLevelFilter::Trace => 5,
}
}
}
impl<L: LogLevel<LevelFilter = TracingLevelFilter>> Verbosity<L> {
pub fn tracing_level(&self) -> Option<TracingLevel> {
self.tracing_level_filter().into_level()
}
pub fn tracing_level_filter(&self) -> TracingLevelFilter {
let verbosity = Self::tracing_verbosity() as i16 - self.quiet as i16 + self.verbose as i16;
let verbosity = verbosity.clamp(0, 5);
match verbosity {
0 => TracingLevelFilter::OFF,
1 => TracingLevelFilter::ERROR,
2 => TracingLevelFilter::WARN,
3 => TracingLevelFilter::INFO,
4 => TracingLevelFilter::DEBUG,
5 => TracingLevelFilter::TRACE,
_ => unreachable!(),
}
}
fn tracing_verbosity() -> u8 {
match L::default_level_filter() {
TracingLevelFilter::OFF => 0,
TracingLevelFilter::ERROR => 1,
TracingLevelFilter::WARN => 2,
TracingLevelFilter::INFO => 3,
TracingLevelFilter::DEBUG => 4,
TracingLevelFilter::TRACE => 5,
}
}
}
trait LogLevel {
type LevelFilter;
fn default_level_filter() -> Self::LevelFilter;
}
#[derive(Debug, Clone, Default)]
struct ErrorLevel;
impl LogLevel for ErrorLevel {
type LevelFilter = LogLevelFilter;
fn default_level_filter() -> Self::LevelFilter {
Self::LevelFilter::Error
}
}
#[derive(Debug, Clone, Default)]
struct TracingErrorLevel;
impl LogLevel for TracingErrorLevel {
type LevelFilter = TracingLevelFilter;
fn default_level_filter() -> Self::LevelFilter {
Self::LevelFilter::ERROR
}
} Edit: PoC PR #124 Edit 2: actually having a new type does help move a bunch of code that is specific to tracing / log to a shared spot. There's one blocker to getting this to be pure conversion methods and generic values, and that's that the log crate doesn't have a |
This is nice to have when working with the `log` crate, as it allows for more ergonomic conversions between the two types. Specifically, This allows type bounds that require Into<LevelFilter> or Into<Option<Level>> to be used. This in turn makes it possible to write more generic code that can work with tracing and log crates interchangeably. Specifically, this supports some ideas in clap-rs/clap-verbosity-flag#121
Add a new `tracing` feature flag and tracing module to support the `tracing` crate. Fixes: clap-rs#121
Add a new `tracing` feature flag and tracing module to support the `tracing` crate. Fixes: clap-rs#121
Ah, looks like I looked at things out of order. If we're including tracing, we should not be bifurcating the entire code base to do so. We should have one type for each level and one type for clap. |
Yep, I think the dual types / associated Level/LevelFilter types definitely complicates things a bit too much. I changed this to instead be a few methods that are log specific and a few that are tracing specific. If there was an open ended number of logging crates to support, this be the wrong decision, but this number should always be fairly small. |
Hi all, thank you @joshka for the new feature, I've just started using clap-verbosity-flag and wondered if the tracing support was possible and then stumble upon your issue. I've followed the development a bit, and was happy that the new version came out with this feature. But I still have an error trying to use it in my code, simply rustc doesn't seem to find the function in the lib:
And here's what I did to try to use this feature:
use clap::{Parser, ValueEnum};
use clap_verbosity_flag::Verbosity;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
//...
#[command(flatten)]
verbose: Verbosity,
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
let file_appender = tracing_appender::rolling::RollingFileAppender::builder()
.rotation(Rotation::NEVER)
.filename_prefix(Utc::now().format("%+").to_string())
.filename_suffix("test.log")
.build("logs/")
.context("Initializing rolling file appender failed")?;
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
let stdout = std::io::stdout.with_max_level(&args.verbose.tracing_level()); <--- error here
tracing_subscriber::fmt()
.with_ansi(false)
.with_writer(stdout.and(non_blocking))
.init();
//...
} I've also done a Any guidance would be appreciated! Thanks again for the awesome feature :) |
I tried to reproduce the error you get but wasn't able to #!/usr/bin/env nargo
---
[dependencies]
clap = { version = "4", features = ["derive"] }
clap-verbosity-flag = { version = "3", default-features = false, features = ["tracing"] }
tracing-subscriber = "*"
tracing = "*"
anyhow = "1"
---
use clap::Parser;
use clap_verbosity_flag::Verbosity;
use tracing_subscriber::fmt::writer::MakeWriterExt;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
//...
#[command(flatten)]
verbose: Verbosity,
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
let stdout = std::io::stdout.with_max_level(args.verbose.tracing_level().unwrap_or(tracing::Level::ERROR));
tracing_subscriber::fmt()
.with_ansi(false)
.init();
Ok(())
} |
Note: |
To understand what features are enabled for your app and why, run: cargo tree --edges features -i clap-verbosity-flag
Generally speaking, it makes sense to use the filter and not the let stdout = std::io::stdout.with_max_level(args.verbose.tracing_level().unwrap_or(tracing::Level::ERROR));
tracing_subscriber::fmt()
.with_ansi(false)
.init(); The stdout var is unused. It's a function that creates a writer with a specific level, but it's never passed to the subscriber. The subscriber builder ( Given that the with_max_level function accepts Into, it could be worthwhile implementing fn main() -> anyhow::Result<()> {
let args = Args::parse();
tracing_subscriber::fmt()
.with_max_level(args.verbose)
.with_ansi(false)
.init();
Ok(())
} (PR #128) |
I'd like to re-suggest adding tracing support. My current workaround whenever I write a cli is to add clap-verbosity-flag, tracing-log, and add a method which calls level_filter().as_trace() to the parser. This seems like code which should just be part of the verbosity flag and feature flagged in.
Previously:
tracing
besideslog
#37 was closed as another PR had a demo of manually working around the problemQuestions previously raised (paraphrased):
Tracing 0.1 has the same order of magnitude daily / all time downloads as log / clap (200M vs 340M / 300M). While this is not a perfect indication of stability / maturity, it implies that making a breaking change would have large impact on a large portion of the rust ecosystem. Development of tracing happens on its master branch which tracks a long running yet to be released 0.2.x, with changes that are backwards compatible are backported to 0.1.x. The core LevelFilter and Level types are unchanged between the versions. Also, the tracing-core crate (where level and levelfilter ultimately live) contains the following in its readme:
It's fairly common for libs that want to support multiple versions of libs to have versioned feature flags. E.g. here that would be tracing / tracing-0.1 / tracing-0.2 etc. The same question applies to log though. What happens when log 0.5 comes out? I'd recommend adding a tracing feature, and having that be mean the latest version of tracing, while adding a tracing-0.1 feature when tracing-0.2 comes out. You could pre-create a -01 feature and only use that. @epage you likely have much more insight into the right choice here than most people. Perhaps pick an approach that makes sense, or accept one that is good enough?
IMO, it should be in a feature flag enabled by default for backwards compatibility
LevelFilter
isstruct LevelFilter(Option<Level>)
, andLevelFilter::OFF
isLevelFilter(None)
. It makes some sense to have both as there are two possible use cases for these settings - directly using the value to filter logs, or using the Level to perform further config from other sources (e.g. environment vars, config files, etc). The former is simplest with LevelFilter, the latter is simplest with Option. Having just one loses symmetry with the existing log crate Level/LevelFilter values. I'd recommend keeping both.So:
tracing
feature flaglog
feature flaglog
defaultfn tracing_level() -> Option<tracing_core::metadata::Level>
fn tracing_level_filter() -> tracing_core::metadata::LevelFilter()
I'm going to open another PR that aligns with the recommendations above as the code to do this is much more concise / less ambiguous than the narrative above.
The text was updated successfully, but these errors were encountered: