captains_log_helper/
lib.rs

1//! This is sub-crate of [captains-log](https://docs.rs/captains-log/latest/captains_log/)
2//!
3//! Provides a attribute macro [macro@logfn] to wrap function for convenience.
4
5#![recursion_limit = "128"]
6use proc_macro2::Span;
7use quote::quote;
8use syn::{
9    parse_macro_input, spanned::Spanned, Expr,
10    ItemFn, LitStr, Ident, Signature, FnArg, Pat, PatType,
11    Block, Stmt, ExprCall,ExprAsync, Generics, Path,
12};
13use syn::parse::{Parse, ParseStream};
14
15#[derive(Default, Debug)]
16struct Args {
17    level: Option<String>,
18}
19
20impl Parse for Args {
21    fn parse(input: ParseStream) -> syn::Result::<Self> {
22        let mut args = Self::default();
23        while !input.is_empty() {
24            if input.peek(LitStr) {
25                // [logfn("warn")]
26                let level = input.parse::<LitStr>()?;
27                args.level = Some(level.value());
28            } else if input.peek(Ident) {
29                // [logfn(warn)]
30                let ident = input.parse::<Ident>()?;
31                if args.level.is_none() {
32                    args.level = Some(ident.to_string());
33                }
34            } else {
35                return Err(input.error("unexpected token"));
36            }
37        }
38        if let Some(v) = args.level.as_ref() {
39            match v.as_str() {
40                "trace" | "debug" | "info" | "warn" | "error"=>{},
41                _=> {
42                    return Err(input.error(format!("unexpected level value: {:?}", v)));
43                }
44            }
45        }
46        Ok(args)
47    }
48}
49
50fn gen_arg_list(sig: &Signature) -> String {
51    let mut arg_list = String::new();
52    for (i, input) in sig.inputs.iter().enumerate() {
53        if i > 0 {
54            arg_list.push_str(", ");
55        }
56        match input {
57            FnArg::Typed(PatType { pat, .. }) => {
58                if let Pat::Ident(pat_ident) = &**pat {
59                    let ident = &pat_ident.ident;
60                    arg_list.push_str(&format!("{ident} = {{{ident}:?}}"));
61                }
62            }
63            FnArg::Receiver(_) => {
64                arg_list.push_str("self");
65            }
66        }
67    }
68    arg_list
69}
70
71// The following code reused the `async_trait` probes from tokio-tracing
72//
73// https://github.com/tokio-rs/tracing/blob/6a61897a/tracing-attributes/src/expand.rs
74// Copyright (c) 2019 Tokio Contributors, MIT license
75fn process_async_trait<'a>(block: &'a Block, is_async: bool) -> Option<&'a ExprAsync> {
76
77    fn path_to_string(path: &Path) -> String {
78        use std::fmt::Write;
79        let mut res = String::with_capacity(path.segments.len() * 5);
80        for i in 0..path.segments.len() {
81            write!(res, "{}", path.segments[i].ident).expect("write to string ok");
82            if i < path.segments.len() -1 {
83                res.push_str("::");
84            }
85        }
86        res
87    }
88
89    if is_async {
90        return None;
91    }
92    // last expression of the block (it determines the return value
93    // of the block, so that if we are working on a function whose
94    // `trait` or `impl` declaration is annotated by async_trait,
95    //  this is quite likely the point where the future is pinned)
96    let last_expr = block.stmts.iter().rev().find_map(|stmt| {
97        if let Stmt::Expr(expr, ..) = stmt {
98            Some(expr)
99        } else {
100            None
101        }
102    })?;
103    // is the last expression a function call?
104    let (outside_func, outside_args) = match last_expr {
105        Expr::Call(ExprCall { func, args, .. }) => (func, args),
106        _ => return None,
107    };
108    // is it a call to `Box::pin()`?
109    let path = match outside_func.as_ref() {
110        Expr::Path(path) => &path.path,
111        _ => return None,
112    };
113    if !path_to_string(path).ends_with("Box::pin") {
114        return None;
115    }
116    // Does the call take an argument? If it doesn't,
117    // it's not going to compile anyway, but that's no reason
118    // to (try to) perform an out-of-bounds access
119    if outside_args.is_empty() {
120        return None;
121    }
122    // Is the argument to Box::pin an async block that
123    // captures its arguments?
124    if let Expr::Async(async_expr) = &outside_args[0] {
125         // check that the move 'keyword' is present
126        async_expr.capture?;
127        return Some(async_expr);
128    }
129    unimplemented!("async-trait < 0.1.44 is not supported");
130}
131
132
133fn generate_function(args: Args, block: &Block,
134    async_context: bool, async_keyword: bool,
135    sig: &Signature) -> proc_macro2::TokenStream {
136    let fn_name = sig.ident.to_string();
137    let level = args.level.unwrap_or("info".to_string());
138    let level = Ident::new(&level, Span::call_site());
139    let arg_list = gen_arg_list(sig);
140    let fmt_begin = format!("<<< {} ({}) enter <<<", fn_name, arg_list);
141    let fmt_end = format!(">>> {} return {{__ret_value:?}} in {{__dur:?}} >>>", fn_name);
142    let begin_expr = quote! {
143        log::#level!(#fmt_begin);
144        let __start_ts = std::time::Instant::now();
145    };
146    let end_expr = quote! {
147        let __dur = std::time::Instant::now() - __start_ts;
148        log::#level!(#fmt_end);
149    };
150
151    if async_context {
152        let block = quote::quote_spanned!(block.span()=>
153            #begin_expr
154            let __ret_value = async { #block }.await;
155            #end_expr
156            __ret_value
157        );
158        if async_keyword { // normal async fn
159            return block.into();
160        } else { // async_trait
161            return quote::quote_spanned!(block.span()=>
162                async move {
163                    #block
164                }
165            ).into();
166        }
167    } else {
168        return quote::quote_spanned!(block.span()=>
169            #begin_expr
170            let __ret_value = (move ||#block)();
171            #end_expr
172            __ret_value
173        ).into();
174    }
175}
176
177fn output_stream(input: &ItemFn, func_body: proc_macro2::TokenStream) -> proc_macro::TokenStream {
178    let sig = &input.sig;
179    let attrs = &input.attrs;
180    let vis = &input.vis;
181    let Signature {
182        output,
183        inputs,
184        unsafety,
185        constness,
186        abi,
187        ident,
188        asyncness,
189        generics:
190            Generics {
191                params: gen_params,
192                where_clause,
193                ..
194            },
195        ..
196    } = sig;
197    quote::quote_spanned!(input.span()=>
198        #(#attrs) *
199        #vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#inputs) #output
200        #where_clause
201        {
202            #func_body
203        }
204    ).into()
205}
206
207
208/// Provide an proc_macro `#[logfn]` which log the information:
209///
210/// - function call begin, argument list
211///
212/// - function return value, duration time.
213///
214/// Supports async fn and async-trait >=0.1.44.
215///
216/// # Examples
217///
218/// ``` rust
219///
220/// use captains_log::{recipe, logfn};
221/// use log::*;
222///
223/// let builder = recipe::raw_file_logger("/tmp", "log_test", log::Level::Debug).test();
224/// builder.build().expect("setup_log");
225///
226/// // default log level to be info
227/// #[logfn]
228/// fn foo() {
229///     info!("foo");
230///     bar(1, "bar arg");
231/// }
232///
233/// // you can change log level to warn
234/// #[logfn(warn)]
235/// fn bar(a: i32, s: &str) {
236///     info!("bar");
237/// }
238/// foo();
239/// ```
240///
241/// /tmp/log_test.log will have this content:
242///
243/// ``` text
244///[2025-07-13 18:22:39.151481][INFO][test_logfn.rs:19] <<< foo () enter <<<
245///[2025-07-13 18:22:39.151774][INFO][test_logfn.rs:21] foo
246///[2025-07-13 18:22:39.151840][WARN][test_logfn.rs:25] <<< bar (a = 1, s = "bar arg") enter <<<
247///[2025-07-13 18:22:39.151886][INFO][test_logfn.rs:27] bar
248///[2025-07-13 18:22:39.151925][WARN][test_logfn.rs:25] >>> bar return () in 39.183µs >>>
249///[2025-07-13 18:22:39.151969][INFO][test_logfn.rs:19] >>> foo return () in 197.381µs >>>
250/// ```
251///
252/// # Best practice with test suit
253///
254/// You can have `#[logfn]` used with retest.
255///
256/// * When you have large test suit, you want to know which logs belong to which test case.
257///
258/// * Sometimes your test crashes, you want to find the responsible test case.
259///
260/// * The time spend in each test
261///
262/// ``` rust
263/// use rstest::*;
264/// use log::*;
265/// use captains_log::*;
266///
267/// // A show case that setup() fixture will be called twice, before each test.
268/// // In order make logs available.
269/// #[fixture]
270/// fn setup() {
271///     let builder = recipe::raw_file_logger("/tmp", "log_rstest", log::Level::Debug).test();
272///     builder.build().expect("setup_log");
273/// }
274///
275/// #[logfn]
276/// #[rstest(file_size, case(0), case(1))]
277/// fn test_rstest_foo(setup: (), file_size: usize) {
278///     info!("do something111");
279/// }
280///
281/// #[logfn]
282/// #[rstest]
283/// fn test_rstest_bar(setup: ()) {
284///     info!("do something222");
285/// }
286///
287/// #[tokio::test]
288/// #[logfn]
289/// #[rstest]
290/// async fn test_rstest_async(setup: ()) {
291///     info!("something333")
292/// }
293///
294/// ```
295///
296/// **Notice:** the order when combine tokio::test with rstest,
297/// `#[rstest]` attribute must be at the bottom to make setup fixture effective.
298///
299/// After running the test with:
300/// `cargo test -- --test-threads=1`
301///
302/// /tmp/log_rstest.log will have this content:
303///
304/// ``` text
305///[2025-07-13 18:22:39.159642][INFO][test_rstest.rs:33] <<< test_rstest_async (setup = ()) enter <<<
306///[2025-07-13 18:22:39.160255][INFO][test_rstest.rs:37] something333
307///[2025-07-13 18:22:39.160567][INFO][test_rstest.rs:33] >>> test_rstest_async return () in 564.047µs >>>
308///[2025-07-13 18:22:39.161299][INFO][test_rstest.rs:26] <<< test_rstest_bar (setup = ()) enter <<<
309///[2025-07-13 18:22:39.161643][INFO][test_rstest.rs:29] do something222
310///[2025-07-13 18:22:39.161703][INFO][test_rstest.rs:26] >>> test_rstest_bar return () in 62.681µs >>>
311///[2025-07-13 18:22:39.162169][INFO][test_rstest.rs:20] <<< test_rstest_foo (setup = (), file_size = 0) enter <<<
312///[2025-07-13 18:22:39.162525][INFO][test_rstest.rs:23] do something111
313///[2025-07-13 18:22:39.162600][INFO][test_rstest.rs:20] >>> test_rstest_foo return () in 78.457µs >>>
314///[2025-07-13 18:22:39.163050][INFO][test_rstest.rs:20] <<< test_rstest_foo (setup = (), file_size = 1) enter <<<
315///[2025-07-13 18:22:39.163320][INFO][test_rstest.rs:23] do something111
316///[2025-07-13 18:22:39.163377][INFO][test_rstest.rs:20] >>> test_rstest_foo return () in 58.747µs >>>
317/// ```
318#[proc_macro_attribute]
319pub fn logfn(
320    attr: proc_macro::TokenStream, item: proc_macro::TokenStream,
321) -> proc_macro::TokenStream {
322    let original_fn: ItemFn = parse_macro_input!(item as ItemFn);
323    let args = parse_macro_input!(attr as Args);
324//    let closure = make_closure(&original_fn);
325    let is_async = original_fn.sig.asyncness.is_some();
326
327    let body = {
328        if let Some(async_expr) = process_async_trait(&original_fn.block, is_async) {
329            let inst_block = generate_function(args, &async_expr.block, true, false, &original_fn.sig);
330            let async_attrs = &async_expr.attrs;
331            quote::quote_spanned! {async_expr.span()=>
332                Box::pin(#(#async_attrs) * #inst_block)}
333        } else {
334            generate_function(
335                args, &original_fn.block, is_async, is_async, &original_fn.sig)
336        }
337    };
338    output_stream(&original_fn, body)
339}