Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 32 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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| {
Expand All @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions examples/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand All @@ -53,4 +65,6 @@ fn main() {
divide(2, 0).ok();
divide2(2, 0).ok();
subtract(3, 2);
negate(5);
ping(42);
}
75 changes: 62 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,19 @@ enum Args {
Simple {
level: String,
input_format: Option<String>,
output_format: Option<String>,
},
Result {
ok_level: Option<String>,
err_level: Option<String>,
input_format: Option<String>,
output_format: Option<String>,
},
Option {
some_level: Option<String>,
none_level: Option<String>,
input_format: Option<String>,
output_format: Option<String>,
},
}

Expand All @@ -59,6 +62,7 @@ impl Parse for Args {
some_level: Option<String>,
none_level: Option<String>,
input_format: Option<String>,
output_format: Option<String>,
}

impl Parse for ArgContext {
Expand Down Expand Up @@ -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(),
Expand All @@ -154,6 +167,7 @@ impl Parse for Args {
some_level,
none_level,
input_format,
output_format,
} = input.parse::<ArgContext>()?;

if ok_level.is_some() || err_level.is_some() {
Expand All @@ -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() {
Expand All @@ -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()),
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

The default log level has been changed from "info" to "debug". While this change is reflected in the documentation, it represents a behavioral change that is not directly related to the stated purpose of this PR (customizing output format). This could be a breaking change for users who rely on the default "info" level. Consider either reverting this change and making it part of a separate PR, or explicitly documenting this breaking change in the PR description.

Suggested change
level: simple_level.unwrap_or_else(|| "debug".to_string()),
level: simple_level.unwrap_or_else(|| "info".to_string()),

Copilot uses AI. Check for mistakes.
input_format,
output_format,
})
}
}
Expand Down Expand Up @@ -278,18 +295,21 @@ fn gen_block(
Args::Simple {
level,
input_format,
output_format,
} => gen_plain_label_block(
block,
async_context,
async_keyword,
sig,
&level,
input_format,
output_format,
),
Args::Result {
ok_level,
err_level,
input_format,
output_format,
} => gen_result_label_block(
block,
async_context,
Expand All @@ -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,
Expand All @@ -311,6 +333,7 @@ fn gen_block(
some_level,
none_level,
input_format,
output_format,
),
}
}
Expand All @@ -322,12 +345,14 @@ fn gen_plain_label_block(
sig: &Signature,
level: &str,
input_format: Option<String>,
output_format: Option<String>,
) -> 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)]
Expand All @@ -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)]
Expand All @@ -363,6 +389,7 @@ fn gen_plain_label_block(
}
}

#[allow(clippy::too_many_arguments)]
fn gen_result_label_block(
block: &Block,
async_context: bool,
Expand All @@ -371,9 +398,11 @@ fn gen_result_label_block(
ok_level: Option<String>,
err_level: Option<String>,
input_format: Option<String>,
output_format: Option<String>,
) -> 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;
Expand All @@ -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;
Expand Down Expand Up @@ -445,6 +474,7 @@ fn gen_result_label_block(
}
}

#[allow(clippy::too_many_arguments)]
fn gen_option_label_block(
block: &Block,
async_context: bool,
Expand All @@ -453,9 +483,11 @@ fn gen_option_label_block(
some_level: Option<String>,
none_level: Option<String>,
input_format: Option<String>,
output_format: Option<String>,
) -> 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;
Expand All @@ -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;
Expand Down Expand Up @@ -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() {}
Expand All @@ -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:?}"
Expand All @@ -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,
Expand Down
Loading