diff --git a/README.md b/README.md index 85e6952..cafafeb 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,19 @@ [![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 that automatically logs function calls, their inputs, and outputs. It keeps boilerplate low while making debugging and observability easy. +Logcall provides a `#[logcall]` attribute that logs function inputs and return values through the [`log`](https://crates.io/crates/log) facade. It keeps boilerplate low while making debugging and observability easy. -This is a re-implementation of [`log-derive`](https://crates.io/crates/log-derive) with [`async-trait`](https://crates.io/crates/async-trait) compatibility. +This is a reimplementation of [`log-derive`](https://crates.io/crates/log-derive) with async functions, [`async-trait`](https://crates.io/crates/async-trait), `Result`, and `Option` support. ## Installation -Add to `Cargo.toml`: +Logcall emits through `log`, so applications also need a logger. This example uses `logforth`: ```toml [dependencies] -logcall = "0.1" +log = "0.4" +logcall = "0.2.0" +logforth = { version = "0.30", features = ["starter-log"] } ``` ## Quick Start @@ -52,11 +54,13 @@ fn divide(a: i32, b: i32) -> Result { /// Logs errors at the `error` level. No log output for `Ok` variant. #[logcall(err = "error")] fn divide2(a: usize, b: usize) -> Result { - if b == 0 { - Err("Division by zero".to_string()) - } else { - Ok(a / b) - } + a.checked_div(b).ok_or("divide by zero".into()) +} + +/// Logs `Some` values at the `info` level and `None` values at the `warn` level. +#[logcall(some = "info", none = "warn")] +fn find_even(value: i32) -> Option { + (value % 2 == 0).then_some(value) } /// Logs the function call with custom input logging format. @@ -86,6 +90,8 @@ fn main() { multiply(2, 3); divide(2, 0).ok(); divide2(2, 0).ok(); + find_even(4); + find_even(3); subtract(3, 2); negate(5); ping(42); @@ -101,20 +107,35 @@ cargo run --example main Sample output: ```plaintext -2026-06-11T15:06:32.853867+08:00 DEBUG main: main.rs:5 main::add(a = 2, b = 3) => 5 -2026-06-11T15:06:32.853920+08:00 INFO main: main.rs:11 main::multiply(a = 2, b = 3) => 6 -2026-06-11T15:06:32.853928+08:00 ERROR main: main.rs:17 main::divide(a = 2, b = 0) => Err("Division by zero") -2026-06-11T15:06:32.853934+08:00 ERROR main: main.rs:27 main::divide2(a = 2, b = 0) => Err("divide by zero") -2026-06-11T15:06:32.853939+08:00 DEBUG main: main.rs:33 main::subtract(a = 3, ..) => 1 -2026-06-11T15:06:32.853943+08:00 DEBUG main: main.rs:39 main::negate(a = 5): -5 -2026-06-11T15:06:32.853947+08:00 DEBUG main: main.rs:45 main::ping(a = 42) +2026-06-12T18:18:14.935453+08:00 DEBUG main: main.rs:5 main::add(a = 2, b = 3) => 5 +2026-06-12T18:18:14.935505+08:00 INFO main: main.rs:11 main::multiply(a = 2, b = 3) => 6 +2026-06-12T18:18:14.935514+08:00 ERROR main: main.rs:17 main::divide(a = 2, b = 0) => Err("Division by zero") +2026-06-12T18:18:14.935520+08:00 ERROR main: main.rs:27 main::divide2(a = 2, b = 0) => Err("divide by zero") +2026-06-12T18:18:14.935525+08:00 INFO main: main.rs:33 main::find_even(value = 4) => Some(4) +2026-06-12T18:18:14.935529+08:00 WARN main: main.rs:33 main::find_even(value = 3) => None +2026-06-12T18:18:14.935533+08:00 DEBUG main: main.rs:39 main::subtract(a = 3, ..) => 1 +2026-06-12T18:18:14.935537+08:00 DEBUG main: main.rs:45 main::negate(a = 5): -5 +2026-06-12T18:18:14.935542+08:00 DEBUG main: main.rs:51 main::ping(a = 42) ``` +## Attribute Arguments + +Generated input and output formatting uses `Debug` by default. Supported log levels are `error`, `warn`, `info`, `debug`, and `trace`. + +- `#[logcall]` logs every call at the `debug` level. +- `#[logcall("info")]` logs every call at the selected level. +- `#[logcall(ok = "info", err = "error")]` logs `Result` variants independently. Omit `ok` or `err` to skip that variant. +- `#[logcall(some = "info", none = "warn")]` logs `Option` variants independently. Omit `some` or `none` to skip that variant. +- `#[logcall(input = "...")]` replaces the argument portion of the message. The format string can reference arguments by name. +- `#[logcall(output = "...")]` replaces the return value portion of the message. Use `output = ""` to log only the call. + +The macro supports sync functions, async functions, and async functions generated by `async-trait`. + ## Minimum Supported Rust Version (MSRV) -This crate is built against the latest stable release, and its minimum supported rustc version is 1.91.0. +This crate's minimum supported rustc version is 1.91.0. -The policy is that the minimum Rust version required to use this crate can be increased in minor version updates. For example, if Logcall 1.0 requires Rust 1.20.0, then Logcall 1.0.z for all values of z will also require Rust 1.20.0 or newer. However, Logcall 1.y for y > 0 may require a newer minimum version of Rust. +The minimum Rust version can increase in minor releases. Patch releases keep the same minimum Rust version. ## Contributing @@ -122,4 +143,4 @@ Contributions are welcome! Please submit pull requests or open issues to improve ## License -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. +This project is licensed under the MIT License. See the `LICENSE` file for details. diff --git a/examples/main.rs b/examples/main.rs index 84f1928..46396c9 100644 --- a/examples/main.rs +++ b/examples/main.rs @@ -29,6 +29,12 @@ fn divide2(a: usize, b: usize) -> Result { a.checked_div(b).ok_or("divide by zero".into()) } +/// Logs `Some` values at the `info` level and `None` values at the `warn` level. +#[logcall(some = "info", none = "warn")] +fn find_even(value: i32) -> Option { + (value % 2 == 0).then_some(value) +} + /// Logs the function call with custom input logging format. #[logcall(input = "a = {a:?}, ..")] fn subtract(a: i32, b: i32) -> i32 { @@ -56,6 +62,8 @@ fn main() { multiply(2, 3); divide(2, 0).ok(); divide2(2, 0).ok(); + find_even(4); + find_even(3); subtract(3, 2); negate(5); ping(42); diff --git a/src/lib.rs b/src/lib.rs index c193ab6..11ef800 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,10 @@ #![doc = include_str!("../README.md")] -// Instrumenting the async fn is not as straight forward as expected because `async_trait` +// Instrumenting async fn is not as straightforward as expected because `async_trait` // rewrites `async fn` into a normal fn which returns `Box`, and this stops // the macro from distinguishing `async fn` from `fn`. // -// The following code reused the `async_trait` probes from tokio-tracing [1]. +// The async_trait probes follow tokio-tracing [1]. // // [1] https://github.com/tokio-rs/tracing/blob/6a61897a/tracing-attributes/src/expand.rs @@ -210,7 +210,20 @@ impl Parse for Args { } } -/// `logcall` attribute macro that logs the function inputs and return values. +/// Logs function inputs and return values through the `log` facade. +/// +/// The generated log message includes the function path, input values, and +/// return value by default. Generated input and output formatting uses `Debug`. +/// +/// Supported forms: +/// +/// - `#[logcall]` logs at `debug`. +/// - `#[logcall("info")]` logs at a fixed level. +/// - `#[logcall(ok = "info", err = "error")]` logs `Result` variants separately. +/// - `#[logcall(some = "info", none = "warn")]` logs `Option` variants separately. +/// - `#[logcall(input = "...", output = "...")]` customizes message formatting. +/// +/// See the crate-level documentation for runnable examples. #[proc_macro_attribute] pub fn logcall( args: proc_macro::TokenStream, @@ -630,14 +643,14 @@ fn gen_output_format() -> String { } enum AsyncTraitKind<'a> { - // old construction. Contains the function + // Older construction. Contains the function. Function, - // new construction. Contains a reference to the async block + // Current construction. Contains a reference to the async block. Async(&'a ExprAsync), } struct AsyncTraitInfo<'a> { - // statement that must be patched + // Statement that must be patched. _source_stmt: &'a Stmt, kind: AsyncTraitKind<'a>, } @@ -652,19 +665,19 @@ struct AsyncTraitInfo<'a> { // Depending on the version of async-trait, we inspect the block of the function // to find if it matches the pattern // `async fn foo<...>(...) {...}; Box::pin(foo<...>(...))` (<=0.1.43), or if -// it matches `Box::pin(async move { ... }) (>=0.1.44). We the return the +// it matches `Box::pin(async move { ... }) (>=0.1.44). We then return the // statement that must be instrumented, along with some other information. -// 'gen_body' will then be able to use that information to instrument the +// `gen_block` then uses that information to instrument the // proper function/future. // (this follows the approach suggested in // https://github.com/dtolnay/async-trait/issues/45#issuecomment-571245673) fn get_async_trait_info(block: &Block, block_is_async: bool) -> Option> { - // are we in an async context? If yes, this isn't an async_trait-like pattern + // Are we in an async context? If yes, this isn't an async_trait-like pattern. if block_is_async { return None; } - // list of async functions declared inside the block + // List of async functions declared inside the block. let inside_fns = block.stmts.iter().filter_map(|stmt| { if let Stmt::Item(Item::Fn(fun)) = &stmt { // If the function is async, this is a candidate @@ -675,10 +688,10 @@ fn get_async_trait_info(block: &Block, block_is_async: bool) -> Option Option (func, args), _ => return None, }; - // is it a call to `Box::pin()`? + // Is it a call to `Box::pin()`? let path = match outside_func.as_ref() { Expr::Path(path) => &path.path, _ => return None, @@ -712,7 +725,7 @@ fn get_async_trait_info(block: &Block, block_is_async: bool) -> Option Option return None, }; - // "stringify" the path of the function called + // Stringify the path of the called function. let func_name = match **func { Expr::Path(ref func_path) => path_to_string(&func_path.path), _ => return None,