From 372b7cf9334a10a28ffe982219a037f1c5533914 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sun, 19 Apr 2026 14:03:32 -0700 Subject: [PATCH] feat(cli): log_config hook for configuring log messages --- packages/copper-proc-macros/Cargo.toml | 2 +- packages/copper-proc-macros/src/cli.rs | 12 ++ packages/copper-proc-macros/src/lib.rs | 41 ++++++ packages/copper/Cargo.toml | 18 +-- packages/copper/src/cli/flags.rs | 43 +++++- packages/copper/src/cli/mod.rs | 2 +- packages/copper/src/cli/print_init.rs | 151 ++++++++++++++------- packages/copper/src/lv.rs | 38 +++++- packages/copper/src/process/pio/spinner.rs | 10 +- 9 files changed, 244 insertions(+), 73 deletions(-) diff --git a/packages/copper-proc-macros/Cargo.toml b/packages/copper-proc-macros/Cargo.toml index e312cba..9432e11 100644 --- a/packages/copper-proc-macros/Cargo.toml +++ b/packages/copper-proc-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pistonite-cu-proc-macros" -version = "0.2.7" +version = "0.2.8" edition = "2024" description = "Proc-macros for Cu" repository = "https://github.com/Pistonite/cu" diff --git a/packages/copper-proc-macros/src/cli.rs b/packages/copper-proc-macros/src/cli.rs index c5d7a0a..de8c032 100644 --- a/packages/copper-proc-macros/src/cli.rs +++ b/packages/copper-proc-macros/src/cli.rs @@ -23,10 +23,16 @@ pub fn expand(attr: TokenStream, input: TokenStream) -> pm::Result None => pm::quote! { (|_| {}) }, }; + let fn_log_config_impl = match attrs.log_config { + Some(value) => pm::quote! { { #value } }, + None => pm::quote! { |_| cu::cli::DefaultLogConfig }, + }; + let main_impl = if is_async { pm::quote! { cu::cli::__co_run( #fn_preproc_impl, + #fn_log_config_impl, #generated_main_name, #fn_flag_impl ) @@ -35,6 +41,7 @@ pub fn expand(attr: TokenStream, input: TokenStream) -> pm::Result pm::quote! { cu::cli::__run( #fn_preproc_impl, + #fn_log_config_impl, #generated_main_name, #fn_flag_impl ) @@ -78,6 +85,10 @@ fn parse_attributes(attr: TokenStream) -> pm::Result { out.preprocess_fn = Some(attr.value); continue; } + if attr.path.is_ident("log_config") { + out.log_config = Some(attr.value); + continue; + } pm::bail!(attr, "unknown attribute"); } Ok(out) @@ -86,4 +97,5 @@ fn parse_attributes(attr: TokenStream) -> pm::Result { struct ParsedAttributes { flags_ident: Option, preprocess_fn: Option, + log_config: Option, } diff --git a/packages/copper-proc-macros/src/lib.rs b/packages/copper-proc-macros/src/lib.rs index eb2eaa9..2a6be2f 100644 --- a/packages/copper-proc-macros/src/lib.rs +++ b/packages/copper-proc-macros/src/lib.rs @@ -97,6 +97,10 @@ use pm::pre::*; /// } /// ``` /// +/// ## Attributes +/// +/// ### `preprocess` +/// /// The attribute can also take a `preprocess` function /// to process flags before initializing the CLI system. /// This can be useful to merge multiple Flags instance @@ -138,6 +142,43 @@ use pm::pre::*; /// } /// ``` /// +/// ### `log_config` +/// `log_config` takes a function that returns a `LogConfig` trait implementation, +/// which can be used to alter how each `LogRecord` is displayed. +/// +/// Below is an example that: overrides info messages in `some_library` to be debug messages +/// if the print level is info, and always show module path for that library. +/// +/// ```rust,ignore +/// # use pistonite_cu as cu; +/// use cu::pre::*; +/// +/// struct LogConfig(cu::lv::PrintLevel); +/// impl LogConfig { +/// pub fn new(flags: &cu::cli::Flags) -> Self { +/// Self(flags.print_level()) +/// } +/// } +/// impl cu::cli::LogConfig for LogConfig { +/// fn process(&self, record: &cu::lv::LogRecord) -> (cu::lv::Lv, bool) { +/// if self.0 == cu::lv::PrintLevel::Normal { +/// if let Some(m) = record.module_path() { +/// if m == "some_library" { +/// let level: cu::lv::Lv = record.level().into(); +/// let is_info = level == cu::lv::I; +/// return (if is_info { cu::lv::D } else { level }, true); +/// } +/// } +/// } +/// cu::cli::DefaultLogConfig.process(record) +/// } +/// } +/// +/// #[cu::cli(log_config = LogConfig::new)] +/// fn main(_: cu::cli::Flags) -> cu::Result<()> { +/// Ok(()) +/// } +/// ``` #[proc_macro_attribute] pub fn cli(attr: TokenStream, input: TokenStream) -> TokenStream { pm::flatten(cli::expand(attr, input)) diff --git a/packages/copper/Cargo.toml b/packages/copper/Cargo.toml index e1a1770..bd6148c 100644 --- a/packages/copper/Cargo.toml +++ b/packages/copper/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pistonite-cu" -version = "0.8.0" +version = "0.8.1" edition = "2024" description = "Battery-included common utils to speed up development of rust tools" repository = "https://github.com/Pistonite/cu" @@ -11,7 +11,7 @@ exclude = [ ] [dependencies] -pistonite-cu-proc-macros = { version = "0.2.7", path = "../copper-proc-macros" } +pistonite-cu-proc-macros = { version = "0.2.8", path = "../copper-proc-macros" } # --- Always enabled --- anyhow = "1.0.102" @@ -19,16 +19,16 @@ log = "0.4.29" # --- Command Line Interface --- oneshot = { version = "0.2.1", optional = true, features = ["std"] } -env_filter = { version = "1.0.0", optional = true } -terminal_size = { version = "0.4.3", optional = true } +env_filter = { version = "1.0.1", optional = true } +terminal_size = { version = "0.4.4", optional = true } unicode-width = { version = "0.2.2", features = ["cjk"], optional = true } -clap = { version = "4.5.60", features = ["derive"], optional = true } +clap = { version = "4.6.1", features = ["derive"], optional = true } regex = { version = "1.12.3", optional = true } ctrlc = { version = "3.5.2", optional = true } # --- Coroutine --- num_cpus = { version = "1.17.0", optional = true } -tokio = { version = "1.50.0", optional = true, features = [ +tokio = { version = "1.52.1", optional = true, features = [ "macros", "rt-multi-thread" ] } @@ -44,13 +44,13 @@ spin = {version = "0.10.0", optional = true} # for PIO serde = { version = "1.0.228", features = ["derive"], optional = true } serde_json = { version = "1.0.149", optional = true } serde_yaml_ng = { version = "0.10.0", optional = true } -toml = { version = "1.0.6", optional = true } +toml = { version = "1.1.2", optional = true } # derive derive_more = { version = "2.1.1", features = ["full"], optional = true } [target.'cfg(unix)'.dependencies] -libc = "0.2.183" +libc = "0.2.185" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_System_Console", "Win32_Storage_FileSystem", "Win32_Security", "Win32_System_SystemServices"] } @@ -58,7 +58,7 @@ windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_Syste [dev-dependencies] [dev-dependencies.tokio] -version = "1.50.0" +version = "1.52.1" features = [ "macros", "rt-multi-thread", "time" ] [features] diff --git a/packages/copper/src/cli/flags.rs b/packages/copper/src/cli/flags.rs index 5fa54b0..51961e5 100644 --- a/packages/copper/src/cli/flags.rs +++ b/packages/copper/src/cli/flags.rs @@ -1,10 +1,13 @@ use std::ffi::OsString; +use std::sync::Arc; use std::time::Instant; use clap::{Command, CommandFactory, FromArgMatches, Parser}; +use crate::cli::LogConfig; use crate::lv; +/// Common flags for `cu::cli` #[derive(Default, Debug, Clone, PartialEq, Parser)] pub struct Flags { /// Verbose. More -v makes it more verbose (opposite of --quiet) @@ -43,9 +46,8 @@ impl Flags { /// This is unsafe because it modifies environment variables. /// The [`cu::cli`](macro@crate::cli) macro generates safe call to this /// when the program only has the main thread. - pub unsafe fn apply(&self) { - let level = (self.verbose as i8 - self.quiet as i8).clamp(-2, 2); - let level: lv::Print = level.into(); + pub unsafe fn apply(&self, log_config: Arc) { + let level = self.print_level(); if level == lv::Print::VerboseVerbose { if std::env::var("RUST_BACKTRACE") .unwrap_or_default() @@ -80,7 +82,12 @@ impl Flags { None } }; - super::print_init::init_options(self.color.unwrap_or_default(), level, prompt); + super::print_init::init_options(self.color.unwrap_or_default(), level, prompt, log_config); + } + + pub fn print_level(&self) -> lv::Print { + let level = (self.verbose as i8 - self.quiet as i8).clamp(-2, 2); + level.into() } /// Merge `other` into self. Options in other will be applied on top of self (equivalent @@ -111,16 +118,25 @@ impl Flags { #[doc(hidden)] pub unsafe fn __run< TArg: clap::Parser, + TLogConfig: LogConfig + Send + Sync + 'static, FPreproc: FnOnce(&mut TArg), + FLogConfig: FnOnce(&Flags) -> TLogConfig, FExecute: FnOnce(TArg) -> crate::Result<()>, FFlag: FnOnce(&TArg) -> &Flags, >( fn_preproc: FPreproc, + fn_log_config: FLogConfig, fn_execute: FExecute, fn_flag: FFlag, ) -> std::process::ExitCode { let start = std::time::Instant::now(); - let args = unsafe { parse_args_or_help::(fn_preproc, fn_flag) }; + let args = unsafe { + parse_args_or_help::( + fn_preproc, + fn_log_config, + fn_flag, + ) + }; let result = fn_execute(args); handle_result(start, result) } @@ -135,17 +151,26 @@ pub unsafe fn __run< #[doc(hidden)] pub unsafe fn __co_run< TArg: clap::Parser + Send + 'static, + TLogConfig: LogConfig + Send + Sync + 'static, FPreproc: FnOnce(&mut TArg), + FLogConfig: FnOnce(&Flags) -> TLogConfig, FExecute: FnOnce(TArg) -> TResult + Send + 'static, TResult: Future> + Send + 'static, FFlag: FnOnce(&TArg) -> &Flags, >( fn_preproc: FPreproc, + fn_log_config: FLogConfig, fn_execute: FExecute, fn_flag: FFlag, ) -> std::process::ExitCode { let start = std::time::Instant::now(); - let args = unsafe { parse_args_or_help::(fn_preproc, fn_flag) }; + let args = unsafe { + parse_args_or_help::( + fn_preproc, + fn_log_config, + fn_flag, + ) + }; #[cfg(not(feature = "coroutine-heavy"))] let result = crate::co::block(async move { fn_execute(args).await }); #[cfg(feature = "coroutine-heavy")] @@ -156,16 +181,20 @@ pub unsafe fn __co_run< unsafe fn parse_args_or_help< TArg: Parser, + TLogConfig: LogConfig + Send + Sync + 'static, FPreproc: FnOnce(&mut TArg), + FLogConfig: FnOnce(&Flags) -> TLogConfig, FFlag: FnOnce(&TArg) -> &Flags, >( fn_preproc: FPreproc, + fn_log_config: FLogConfig, fn_flag: FFlag, ) -> TArg { let mut parsed = parse_args::(); fn_preproc(&mut parsed); let flags = fn_flag(&parsed); - unsafe { flags.apply() }; + let log_config: Arc = Arc::new(fn_log_config(flags)); + unsafe { flags.apply(log_config) }; parsed } diff --git a/packages/copper/src/cli/mod.rs b/packages/copper/src/cli/mod.rs index 1b405f1..9608417 100644 --- a/packages/copper/src/cli/mod.rs +++ b/packages/copper/src/cli/mod.rs @@ -141,7 +141,7 @@ pub use flags::__co_run; pub use flags::{__run, Flags, print_help, try_parse}; mod print_init; -pub use print_init::level; +pub use print_init::{DefaultLogConfig, LogConfig, level}; mod macros; pub use macros::__print_with_level; diff --git a/packages/copper/src/cli/print_init.rs b/packages/copper/src/cli/print_init.rs index cfd87ec..3e17ac5 100644 --- a/packages/copper/src/cli/print_init.rs +++ b/packages/copper/src/cli/print_init.rs @@ -1,5 +1,5 @@ -use std::sync::OnceLock; use std::sync::atomic::Ordering; +use std::sync::{Arc, OnceLock}; use cu::cli::printer::{PRINTER, Printer}; #[cfg(feature = "prompt")] @@ -7,11 +7,7 @@ use cu::cli::prompt::PROMPT_LEVEL; use cu::lv; use env_filter::{Builder as LogEnvBuilder, Filter as LogEnvFilter}; -static LOG_FILTER: OnceLock = OnceLock::new(); -/// Set the global log filter -pub(crate) fn set_log_filter(filter: LogEnvFilter) { - let _ = LOG_FILTER.set(filter); -} +static LOGGER: OnceLock = OnceLock::new(); /// Shorthand to quickly setup logging. Can be useful in tests. /// @@ -26,27 +22,35 @@ pub fn level(lv: &str) { "vv" => lv::Print::VerboseVerbose, _ => lv::Print::Normal, }; - init_options(lv::Color::Auto, level, Some(lv::Prompt::Block)); + init_options( + lv::Color::Auto, + level, + Some(lv::Prompt::Block), + Arc::new(DefaultLogConfig), + ); } /// Set global print options. This is usually called from clap args /// /// If prompt option is `None`, it will be `Interactive` unless env var `CI` is `true` or `1`, in which case it becomes `No`. /// Prompt option is ignored unless `prompt` feature is enabled -pub fn init_options(color: lv::Color, level: lv::Print, prompt: Option) { +pub fn init_options( + color: lv::Color, + level: lv::Print, + prompt: Option, + log_config: Arc, +) { // not using cu::env_var, since we are before log initialization let env_rust_log = std::env::var("RUST_LOG"); - let log_level = match env_rust_log { + let (log_level_filter, log_filter) = match env_rust_log { Ok(value) if !value.is_empty() => { - let mut builder = LogEnvBuilder::new(); - let filter = builder.parse(&value).build(); - let log_level = filter.filter(); - set_log_filter(filter); - log_level.max(level.into()) + let filter = LogEnvBuilder::new().parse(&value).build(); + let log_level_filter = filter.filter(); + (log_level_filter.max(level.into()), Some(filter)) } - _ => level.into(), + _ => (level.into(), None), }; - log::set_max_level(log_level); + log::set_max_level(log_level_filter); let use_color = color.is_colored_for_stdout(); lv::USE_COLOR.store(use_color, Ordering::Release); @@ -80,52 +84,49 @@ pub fn init_options(color: lv::Color, level: lv::Print, prompt: Option, + config: Arc, } -struct LogImpl; impl log::Log for LogImpl { fn enabled(&self, metadata: &log::Metadata) -> bool { - match LOG_FILTER.get() { + match &self.filter { Some(filter) => filter.enabled(metadata), None => lv::Lv::from(metadata.level()).can_print(lv::PRINT_LEVEL.get()), } } fn log(&self, record: &log::Record) { - if !self.enabled(record.metadata()) { - return; + let (level, show_module) = self.config.process(record); + if level != record.level().into() { + let metadata = log::Metadata::builder() + .level(level.into()) + .target(record.metadata().target()) + .build(); + if !self.enabled(&metadata) { + return; + } + } else { + if !self.enabled(record.metadata()) { + return; + } } - let typ: lv::Lv = record.level().into(); - let message = if typ == lv::T { + let message = if show_module { // enable source location logging in trace messages let mut message = String::new(); - message.push('['); - if let Some(p) = record.module_path() { - // aliased crate, use the shorthand - if let Some(rest) = p.strip_prefix("pistonite_") { - message.push_str(rest); - } else { - message.push_str(p); - } - message.push(' '); - } - if let Some(f) = record.file() { - let name = match f.rfind(['/', '\\']) { - None => f, - Some(i) => &f[i + 1..], - }; - message.push_str(name); - } - if let Some(l) = record.line() { - message.push(':'); - message.push_str(&format!("{l}")); - } - if message.len() > 1 { - message += "] "; - } else { - message.clear(); - } - + format_module_prefix( + &mut message, + record.module_path(), + record.file(), + record.line(), + ); use std::fmt::Write; let _: Result<_, _> = write!(&mut message, "{}", record.args()); message @@ -134,10 +135,58 @@ impl log::Log for LogImpl { }; if let Ok(mut printer) = PRINTER.lock() { if let Some(printer) = printer.as_mut() { - printer.print_message(typ, &message); + printer.print_message(level, &message); } } } fn flush(&self) {} } + +fn format_module_prefix( + message: &mut String, + module: Option<&str>, + file: Option<&str>, + line: Option, +) { + if module.is_none() && file.is_none() { + return; + } + message.push('['); + if let Some(p) = module { + // aliased crate, use the shorthand + if let Some(rest) = p.strip_prefix("pistonite_") { + message.push_str(rest); + } else { + message.push_str(p); + } + message.push(' '); + } + if let Some(f) = file { + let name = match f.rfind(['/', '\\']) { + None => f, + Some(i) => &f[i + 1..], + }; + message.push_str(name); + if let Some(l) = line { + message.push(':'); + message.push_str(&format!("{l}")); + } + } + message.push_str("] "); +} + +/// Hook to configure the level and format before logging +pub trait LogConfig { + /// Process a log record, return the level to log and if + /// the module path should be shown + fn process(&self, record: &lv::LogRecord) -> (lv::Lv, bool); +} +/// The default [`LogConfig`] +pub struct DefaultLogConfig; +impl LogConfig for DefaultLogConfig { + fn process(&self, record: &lv::LogRecord) -> (lv::Lv, bool) { + let level: lv::Lv = record.level().into(); + (record.level().into(), level == lv::T) + } +} diff --git a/packages/copper/src/lv.rs b/packages/copper/src/lv.rs index 52a05d4..99c7a97 100644 --- a/packages/copper/src/lv.rs +++ b/packages/copper/src/lv.rs @@ -1,6 +1,6 @@ //! # Logging //! -//! *Does not require any feature flag +//! *Does not require any feature flag* //! //! The logging macros (`debug`, `info`, `trace`, `warn`, `error`) are //! re-exported from the [`log`](https://docs.rs/log) crate and are @@ -14,8 +14,12 @@ //! When the `cli` feature is enabled, you also get log integration //! with CLI flags and other terminal-printing features. //! See [Command Line Interface](mod@crate::cli) +//! +//! `cu::lv` shorthands should be used within this library (for example +//! `cu::lv::D` for debug). You can also call `.into()` to convert it +//! to `log::Level`. Additionally `cu::lv::LogLevel` is a re-export of `log::Level`. -pub use log::{debug, error, info, trace, warn}; +pub use log::{Level as LogLevel, Record as LogRecord, debug, error, info, trace, warn}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -273,6 +277,20 @@ impl From for Lv { } } } +impl From for log::Level { + fn from(value: Lv) -> Self { + match value { + Lv::Error => log::Level::Error, + Lv::Hint => log::Level::Warn, + Lv::Print => log::Level::Info, + Lv::Warn => log::Level::Warn, + Lv::Info => log::Level::Info, + Lv::Debug => log::Level::Debug, + Lv::Trace => log::Level::Trace, + Lv::Off => log::Level::Error, + } + } +} impl From for Lv { fn from(value: u8) -> Self { match value { @@ -292,6 +310,22 @@ impl From for u8 { value as u8 } } + +impl std::fmt::Display for Lv { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Lv::Error => "error".fmt(f), + Lv::Hint => "hint".fmt(f), + Lv::Print => "print".fmt(f), + Lv::Warn => "warn".fmt(f), + Lv::Info => "info".fmt(f), + Lv::Debug => "debug".fmt(f), + Lv::Trace => "trace".fmt(f), + Lv::Off => "off".fmt(f), + } + } +} + /// Error pub const E: Lv = Lv::Error; /// Hint diff --git a/packages/copper/src/process/pio/spinner.rs b/packages/copper/src/process/pio/spinner.rs index 6bb5a44..cb56e81 100644 --- a/packages/copper/src/process/pio/spinner.rs +++ b/packages/copper/src/process/pio/spinner.rs @@ -191,8 +191,14 @@ impl SpinnerTask { DriverOutput::Line(line) => { if lv != Lv::Off { crate::cli::__print_with_level(lv, format_args!("{prefix}{line}")); - // erase the progress line if we decide to print it out - crate::progress!(bar, "") + if lv.enabled() { + // erase the progress line if we decide to print it out + crate::progress!(bar, "") + } else { + // the level is not visible in due to log level setting, + // so we still print it as a progress line + crate::progress!(bar, "{line}") + } } else { crate::progress!(bar, "{line}") }