Skip to main content

bitwarden_logging_macro/
lib.rs

1//! Proc-macro wrapper around `tracing::instrument` that enforces `skip_all` by default.
2//!
3//! Use via the [`bitwarden_logging::instrument`](../bitwarden_logging/attr.instrument.html)
4//! re-export rather than depending on this crate directly.
5
6use proc_macro::TokenStream;
7use proc_macro2::TokenStream as TokenStream2;
8use quote::quote;
9use syn::{Meta, Token, parse::Parser, punctuated::Punctuated};
10
11const REJECT_MSG: &str = "`#[bitwarden_logging::instrument]` enforces `skip_all` by default. \
12    Use `fields(name = expr)` to opt in to logging specific arguments.";
13
14/// Drop-in replacement for `#[tracing::instrument]` that defaults to `skip_all`, making
15/// field logging opt-in.
16///
17/// Pass `fields(name = expr)` to record specific values. All other `tracing::instrument`
18/// options (`name`, `level`, `target`, `ret`, `err`, `parent`, `follows_from`) flow through
19/// unchanged.
20///
21/// User-supplied `skip(...)` or `skip_all` are rejected at compile time: `skip_all` is
22/// already enforced and `fields(...)` is the way to opt back in.
23#[proc_macro_attribute]
24pub fn instrument(attr: TokenStream, item: TokenStream) -> TokenStream {
25    let item2: TokenStream2 = item.into();
26
27    let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
28    let args = match parser.parse(attr) {
29        Ok(args) => args,
30        Err(e) => {
31            let err = e.to_compile_error();
32            return quote! { #err #item2 }.into();
33        }
34    };
35
36    let mut errors = TokenStream2::new();
37    for meta in &args {
38        let Some(last) = meta.path().segments.last() else {
39            continue;
40        };
41        if last.ident == "skip" || last.ident == "skip_all" {
42            errors.extend(syn::Error::new_spanned(meta, REJECT_MSG).to_compile_error());
43        }
44    }
45
46    if !errors.is_empty() {
47        return quote! { #errors #item2 }.into();
48    }
49
50    let args_iter = args.iter();
51    quote! {
52        // Silence the `tracing_instrument` dylint on the wrapper's own emission.
53        // `unknown_lints` is paired with it so plain `cargo check` (which doesn't load
54        // dylint) doesn't warn that `tracing_instrument` is an unknown lint name.
55        #[allow(unknown_lints, tracing_instrument)]
56        #[::tracing::instrument(skip_all #(, #args_iter)*)]
57        #item2
58    }
59    .into()
60}