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..38a8f1a 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); +}