From 3c8626e51d5f55655400afbc34e3c6b3b0db9608 Mon Sep 17 00:00:00 2001 From: andylokandy Date: Thu, 11 Dec 2025 23:12:54 +0800 Subject: [PATCH 1/2] feat: output format --- README.md | 65 ++++++++++++++++++------------------- examples/main.rs | 14 ++++++++ src/lib.rs | 75 +++++++++++++++++++++++++++++++++++-------- tests/ui/ok/output.rs | 14 ++++++++ 4 files changed, 122 insertions(+), 46 deletions(-) create mode 100644 tests/ui/ok/output.rs diff --git a/README.md b/README.md index d954eb0..ae3eaa5 100644 --- a/README.md +++ b/README.md @@ -6,22 +6,22 @@ [![CI Status](https://img.shields.io/github/actions/workflow/status/fast/logcall/ci.yml?style=flat-square&logo=github)](https://github.com/fast/logcall/actions) [![License](https://img.shields.io/crates/l/logcall?style=flat-square&logo=)](https://crates.io/crates/logcall) -Logcall is a Rust procedural macro crate designed to automatically log function calls, their inputs, and their outputs. This macro facilitates debugging and monitoring by providing detailed logs of function executions with minimal boilerplate code. +Logcall is a Rust procedural macro crate that automatically logs function calls, their inputs, and outputs. It keeps boilerplate low while making debugging and observability easy. -This is a re-implementation of the [`log-derive`](https://crates.io/crates/log-derive) crate with [`async-trait`](https://crates.io/crates/async-trait) compatibility. +This is a re-implementation of [`log-derive`](https://crates.io/crates/log-derive) with [`async-trait`](https://crates.io/crates/async-trait) compatibility. ## Installation -Add `logcall` to your `Cargo.toml`: +Add to `Cargo.toml`: ```toml [dependencies] logcall = "0.1" ``` -## Usage +## Quick Start -Import the `logcall` crate and use the macro to annotate your functions: +Annotate functions with `#[logcall]` and configure logging with `logforth`: ```rust use logcall::logcall; @@ -66,6 +66,18 @@ fn subtract(a: i32, b: i32) -> i32 { a - b } +/// Logs the function call with custom output logging format. +#[logcall(output = ": {:?}")] +fn negate(a: i32) -> i32 { + -a +} + +/// Omits the return value from the log output. +#[logcall(output = "")] +fn ping(a: i32) -> i32 { + a +} + fn main() { logforth::builder() .dispatch(|d| { @@ -79,42 +91,29 @@ fn main() { divide(2, 0).ok(); divide2(2, 0).ok(); subtract(3, 2); + negate(5); + ping(42); } ``` -### Log Output +### Example Run -When the `main` function runs, it initializes the logger and logs each function call as specified: +```bash +cargo run --example main +``` + +Sample output (from 2025-12-11): ```plaintext -2024-12-22T07:02:59.787586+08:00[Asia/Shanghai] DEBUG main: main.rs:6 main::add(a = 2, b = 3) => 5 -2024-12-22T07:02:59.816839+08:00[Asia/Shanghai] INFO main: main.rs:12 main::multiply(a = 2, b = 3) => 6 -2024-12-22T07:02:59.816929+08:00[Asia/Shanghai] ERROR main: main.rs:18 main::divide(a = 2, b = 0) => Err("Division by zero") -2024-12-22T07:02:59.816957+08:00[Asia/Shanghai] ERROR main: main.rs:28 main::divide2(a = 2, b = 0) => Err("Division by zero") -2024-12-22T07:02:59.816980+08:00[Asia/Shanghai] DEBUG main: main.rs:38 main::subtract(a = 3, ..) => 1 +2025-12-11T23:08:39.201289+08:00[Asia/Shanghai] DEBUG main: main.rs:6 main::add(a = 2, b = 3) => 5 +2025-12-11T23:08:39.211065+08:00[Asia/Shanghai] INFO main: main.rs:12 main::multiply(a = 2, b = 3) => 6 +2025-12-11T23:08:39.211086+08:00[Asia/Shanghai] ERROR main: main.rs:18 main::divide(a = 2, b = 0) => Err("Division by zero") +2025-12-11T23:08:39.211118+08:00[Asia/Shanghai] ERROR main: main.rs:28 main::divide2(a = 2, b = 0) => Err("Division by zero") +2025-12-11T23:08:39.211148+08:00[Asia/Shanghai] DEBUG main: main.rs:38 main::subtract(a = 3, ..) => 1 +2025-12-11T23:08:39.211162+08:00[Asia/Shanghai] DEBUG main: main.rs:44 main::negate(a = 5): -5 +2025-12-11T23:08:39.211172+08:00[Asia/Shanghai] DEBUG main: main.rs:50 main::ping(a = 42) ``` -## Customization - -- **Default Log Level**: If no log level is specified, `logcall` logs at the `debug` level: - ```rust,ignore - #[logcall] - ``` -- **Specify Log Level**: Use the macro parameters to specify log level: - ```rust,ignore - #[logcall("info")] -- **Specify Log Levels for `Result`**: Use the `ok` and `err` parameters to specify log levels for `Ok` and `Err` variants: - ```rust,ignore - #[logcall(err = "error")] - #[logcall(ok = "info", err = "error")] - ``` -- **Customize Input Logging**: Use the `input` parameter to customize the input log format: - ```rust,ignore - #[logcall(input = "a = {a:?}, ..")] - #[logcall("info", input = "a = {a:?}, ..")] - #[logcall(ok = "info", err = "error", input = "a = {a:?}, ..")] - ``` - ## Minimum Supported Rust Version (MSRV) This crate is built against the latest stable release, and its minimum supported rustc version is 1.80.0. diff --git a/examples/main.rs b/examples/main.rs index bb6eb33..4bcf981 100644 --- a/examples/main.rs +++ b/examples/main.rs @@ -40,6 +40,18 @@ fn subtract(a: i32, b: i32) -> i32 { a - b } +/// Logs the function call with custom output logging format. +#[logcall(output = ": {:?}")] +fn negate(a: i32) -> i32 { + -a +} + +/// Omits the return value from the log output. +#[logcall(output = "")] +fn ping(a: i32) -> i32 { + a +} + fn main() { logforth::builder() .dispatch(|d| { @@ -53,4 +65,6 @@ fn main() { divide(2, 0).ok(); divide2(2, 0).ok(); subtract(3, 2); + negate(5); + ping(42); } diff --git a/src/lib.rs b/src/lib.rs index eb612ba..916bb97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,16 +36,19 @@ enum Args { Simple { level: String, input_format: Option, + output_format: Option, }, Result { ok_level: Option, err_level: Option, input_format: Option, + output_format: Option, }, Option { some_level: Option, none_level: Option, input_format: Option, + output_format: Option, }, } @@ -59,6 +62,7 @@ impl Parse for Args { some_level: Option, none_level: Option, input_format: Option, + output_format: Option, } impl Parse for ArgContext { @@ -128,6 +132,15 @@ impl Parse for Args { } ctx.input_format = Some(level.value()); } + "output" => { + if ctx.output_format.is_some() { + return Err(syn::Error::new( + level.span(), + "output specified multiple times", + )); + } + ctx.output_format = Some(level.value()); + } _ => { return Err(syn::Error::new( ident.span(), @@ -154,6 +167,7 @@ impl Parse for Args { some_level, none_level, input_format, + output_format, } = input.parse::()?; if ok_level.is_some() || err_level.is_some() { @@ -169,6 +183,7 @@ impl Parse for Args { ok_level, err_level, input_format, + output_format, }) } else if some_level.is_some() || none_level.is_some() { if simple_level.is_some() { @@ -178,11 +193,13 @@ impl Parse for Args { some_level, none_level, input_format, + output_format, }) } else { Ok(Args::Simple { - level: simple_level.unwrap_or_else(|| "info".to_string()), + level: simple_level.unwrap_or_else(|| "debug".to_string()), input_format, + output_format, }) } } @@ -278,6 +295,7 @@ fn gen_block( Args::Simple { level, input_format, + output_format, } => gen_plain_label_block( block, async_context, @@ -285,11 +303,13 @@ fn gen_block( sig, &level, input_format, + output_format, ), Args::Result { ok_level, err_level, input_format, + output_format, } => gen_result_label_block( block, async_context, @@ -298,11 +318,13 @@ fn gen_block( ok_level, err_level, input_format, + output_format, ), Args::Option { some_level, none_level, input_format, + output_format, } => gen_option_label_block( block, async_context, @@ -311,6 +333,7 @@ fn gen_block( some_level, none_level, input_format, + output_format, ), } } @@ -322,12 +345,14 @@ fn gen_plain_label_block( sig: &Signature, level: &str, input_format: Option, + output_format: Option, ) -> proc_macro2::TokenStream { // Generate the instrumented function body. // If the function is an `async fn`, this will wrap it in an async block. if async_context { let input_format = input_format.unwrap_or_else(|| gen_input_format(sig)); - let log = gen_log(level, "__input_string", "__ret_value"); + let output_format = output_format.unwrap_or_else(|| gen_output_format()); + let log = gen_log(level, "__input_string", &output_format, "__ret_value"); let block = quote::quote_spanned!(block.span()=> #[allow(unknown_lints)] #[allow(clippy::useless_format)] @@ -348,7 +373,8 @@ fn gen_plain_label_block( } } else { let input_format = input_format.unwrap_or_else(|| gen_input_format(sig)); - let log = gen_log(level, "__input_string", "__ret_value"); + let output_format = output_format.unwrap_or_else(|| gen_output_format()); + let log = gen_log(level, "__input_string", &output_format, "__ret_value"); quote::quote_spanned!(block.span()=> #[allow(unknown_lints)] #[allow(clippy::useless_format)] @@ -363,6 +389,7 @@ fn gen_plain_label_block( } } +#[allow(clippy::too_many_arguments)] fn gen_result_label_block( block: &Block, async_context: bool, @@ -371,9 +398,11 @@ fn gen_result_label_block( ok_level: Option, err_level: Option, input_format: Option, + output_format: Option, ) -> proc_macro2::TokenStream { + let output_format = output_format.unwrap_or_else(gen_output_format); let ok_arm = if let Some(ok_level) = ok_level { - let log_ok = gen_log(&ok_level, "__input_string", "__ret_value"); + let log_ok = gen_log(&ok_level, "__input_string", &output_format, "__ret_value"); quote::quote_spanned!(block.span()=> __ret_value@Ok(_) => { #log_ok; @@ -386,7 +415,7 @@ fn gen_result_label_block( ) }; let err_arm = if let Some(err_level) = err_level { - let log_err = gen_log(&err_level, "__input_string", "__ret_value"); + let log_err = gen_log(&err_level, "__input_string", &output_format, "__ret_value"); quote::quote_spanned!(block.span()=> __ret_value@Err(_) => { #log_err; @@ -445,6 +474,7 @@ fn gen_result_label_block( } } +#[allow(clippy::too_many_arguments)] fn gen_option_label_block( block: &Block, async_context: bool, @@ -453,9 +483,11 @@ fn gen_option_label_block( some_level: Option, none_level: Option, input_format: Option, + output_format: Option, ) -> proc_macro2::TokenStream { + let output_format = output_format.unwrap_or_else(gen_output_format); let some_arm = if let Some(some_level) = some_level { - let log_some = gen_log(&some_level, "__input_string", "__ret_value"); + let log_some = gen_log(&some_level, "__input_string", &output_format, "__ret_value"); quote::quote_spanned!(block.span()=> __ret_value@Some(_) => { #log_some; @@ -468,7 +500,7 @@ fn gen_option_label_block( ) }; let none_arm = if let Some(none_level) = none_level { - let log_none = gen_log(&none_level, "__input_string", "__ret_value"); + let log_none = gen_log(&none_level, "__input_string", &output_format, "__ret_value"); quote::quote_spanned!(block.span()=> None => { #log_none; @@ -527,14 +559,17 @@ fn gen_option_label_block( } } -fn gen_log(level: &str, input_string: &str, return_value: &str) -> proc_macro2::TokenStream { +fn gen_log( + level: &str, + input_string: &str, + output_format: &str, + return_value: &str, +) -> proc_macro2::TokenStream { let level = level.to_lowercase(); if !["error", "warn", "info", "debug", "trace"].contains(&level.as_str()) { abort_call_site!("unknown log level"); } let level: Ident = Ident::new(&level, Span::call_site()); - let input_string: Ident = Ident::new(input_string, Span::call_site()); - let return_value: Ident = Ident::new(return_value, Span::call_site()); let fn_name = quote::quote! { { fn f() {} @@ -546,9 +581,19 @@ fn gen_log(level: &str, input_string: &str, return_value: &str) -> proc_macro2:: name.trim_end_matches("::{{closure}}") } }; - quote::quote!( - log::#level! ("{}({}) => {:?}", #fn_name, #input_string, &#return_value) - ) + let input_string: Ident = Ident::new(input_string, Span::call_site()); + let format_string = format!("{{}}({{}}){output_format}"); + + if output_format.replace("{{", "").contains("{") { + let return_value: Ident = Ident::new(return_value, Span::call_site()); + quote::quote!( + log::#level! (#format_string, #fn_name, #input_string, &#return_value) + ) + } else { + quote::quote!( + log::#level! (#format_string, #fn_name, #input_string) + ) + } } // fn(a: usize, b: usize) => "a = {a:?}, b = {b:?}" @@ -573,6 +618,10 @@ fn gen_input_format(sig: &Signature) -> String { input_format } +fn gen_output_format() -> String { + " => {:?}".to_string() +} + enum AsyncTraitKind<'a> { // old construction. Contains the function Function, diff --git a/tests/ui/ok/output.rs b/tests/ui/ok/output.rs new file mode 100644 index 0000000..a842ff7 --- /dev/null +++ b/tests/ui/ok/output.rs @@ -0,0 +1,14 @@ +#[logcall::logcall(output = " output = {}")] +fn output_custom(a: u32) -> u32 { + a + 1 +} + +#[logcall::logcall(output = "")] +fn output_suppressed(a: u32) -> u32 { + a +} + +fn main() { + output_custom(1); + output_suppressed(3); +} From 1ac518ba18f0e380c833999ec77f69cdbea1c14b Mon Sep 17 00:00:00 2001 From: andylokandy Date: Thu, 11 Dec 2025 23:15:04 +0800 Subject: [PATCH 2/2] fix --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 916bb97..38a8f1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -351,7 +351,7 @@ fn gen_plain_label_block( // If the function is an `async fn`, this will wrap it in an async block. if async_context { let input_format = input_format.unwrap_or_else(|| gen_input_format(sig)); - let output_format = output_format.unwrap_or_else(|| gen_output_format()); + let output_format = output_format.unwrap_or_else(gen_output_format); let log = gen_log(level, "__input_string", &output_format, "__ret_value"); let block = quote::quote_spanned!(block.span()=> #[allow(unknown_lints)] @@ -373,7 +373,7 @@ fn gen_plain_label_block( } } else { let input_format = input_format.unwrap_or_else(|| gen_input_format(sig)); - let output_format = output_format.unwrap_or_else(|| gen_output_format()); + let output_format = output_format.unwrap_or_else(gen_output_format); let log = gen_log(level, "__input_string", &output_format, "__ret_value"); quote::quote_spanned!(block.span()=> #[allow(unknown_lints)]