trace_tools_attributes/
lib.rs

1// Copyright 2022 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Attribute macro for extending functions and futures with the `Observe` trait.
5
6#![deny(missing_docs, warnings)]
7
8use proc_macro2::TokenStream;
9use quote::{quote, quote_spanned};
10use syn::{
11    parse::{Parse, ParseStream},
12    parse_macro_input,
13    spanned::Spanned,
14    Attribute, Signature, Visibility,
15};
16
17/// Attribute macro for extending functions and futures with the `Observe` trait.
18///
19/// This instruments the function or future with a `tracing` span with the `trace_tools::observe` target, so that
20/// it can be filtered by subscribers. It also records the location of the calling code to the span as
21/// fields. This is assigned to `loc.file`, `loc.line` and `loc.col` fields, similar to how `tokio` instruments
22/// tasks internally.
23///
24/// As such, `tokio` tasks, any functions or futures instrumented with `tracing`, and any functions or futures
25/// instrumented with the `Observe` trait or macro will be wrapped in spans that contain similarly structured
26/// information for diagnostics. The only difference should be the span target and the span name (if
27/// available).
28///
29/// # Examples
30///
31/// A future or function can be wrapped in a `tracing` span with the following:
32/// ```ignore
33/// use trace_tools::observe;
34///
35/// #[observe]
36/// pub async fn say_hello() {
37///     println!("hello");
38/// }
39/// ```
40///
41/// This will generate a span equivalent to the following:
42/// ```ignore
43/// // Location of the function signature.
44/// let location = std::panic::Location::caller();
45///
46/// tracing::trace_span!(
47///     "trace_tools::observe",
48///     "observed",
49///     observed.name = "say_hello",
50///     loc.file = location.file(),
51///     loc.line = location.line(),
52///     loc.col = location.column(),
53/// );
54/// ```
55///
56/// The future or function will then run inside the context of the generated span:
57/// ```ignore
58/// let _guard = span.enter();
59///
60/// async move {
61///     println!("hello");
62/// }
63/// .await;
64/// ```
65#[proc_macro_attribute]
66pub fn observe(
67    _args: proc_macro::TokenStream,
68    input: proc_macro::TokenStream,
69) -> proc_macro::TokenStream {
70    let observe_impl = parse_macro_input!(input as ObserveImpl);
71    observe_impl.gen_tokens().into()
72}
73
74#[derive(Debug)]
75struct ObserveImpl {
76    attributes: Vec<Attribute>,
77    visibility: Visibility,
78    signature: Signature,
79    block: TokenStream,
80}
81
82impl Parse for ObserveImpl {
83    fn parse(input: ParseStream) -> syn::Result<Self> {
84        let attributes = input.call(Attribute::parse_outer)?;
85        let visibility = input.parse()?;
86        let signature = input.parse()?;
87        let block = input.parse()?;
88
89        Ok(Self {
90            attributes,
91            visibility,
92            signature,
93            block,
94        })
95    }
96}
97
98impl ObserveImpl {
99    fn gen_tokens(self) -> TokenStream {
100        let ObserveImpl {
101            attributes,
102            visibility,
103            signature,
104            block,
105        } = self;
106
107        let fn_name = signature.ident.to_string();
108
109        let block = match &signature.asyncness {
110            Some(_) => Self::gen_async_block(fn_name, &block),
111            None => Self::gen_block(fn_name, &block),
112        };
113
114        quote! {
115            #(#attributes)*
116            #visibility #signature
117            {
118                #block
119            }
120        }
121    }
122
123    fn gen_block(fn_name: String, block: &TokenStream) -> TokenStream {
124        let span = quote! {
125            {
126                let location = std::panic::Location::caller();
127
128                tracing::trace_span!(
129                    target: "trace_tools::observe",
130                    "observed",
131                    observed.name = #fn_name,
132                    loc.file = location.file(),
133                    loc.line = location.line(),
134                    loc.col = location.column(),
135                )
136            }
137        };
138
139        quote_spanned! {
140            block.span() => {
141                let span = #span;
142                let _guard = span.enter();
143                #block
144            }
145        }
146    }
147
148    fn gen_async_block(fn_name: String, block: &TokenStream) -> TokenStream {
149        let observed_future = quote_spanned! {
150            block.span() => async move #block
151        };
152
153        quote! {
154            trace_tools::Observe::observe(#observed_future, #fn_name).await
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::{quote, ObserveImpl, TokenStream};
162
163    #[test]
164    fn observe_async() {
165        let input: TokenStream = quote! {
166            async fn test_observe_fn() {
167                println!("observing this function");
168            }
169        };
170
171        let observe_impl = syn::parse2::<ObserveImpl>(input);
172        assert!(observe_impl.is_ok());
173
174        let output_tokens = observe_impl.unwrap().gen_tokens();
175        let expected_tokens: TokenStream = quote! {
176            async fn test_observe_fn() {
177                trace_tools::Observe::observe(
178                    async move {
179                        println!("observing this function");
180                    },
181                    "test_observe_fn"
182                ).await
183            }
184        };
185
186        assert_eq!(output_tokens.to_string(), expected_tokens.to_string());
187    }
188
189    #[test]
190    fn observe() {
191        let input: TokenStream = quote! {
192            fn test_observe_fn() {
193                println!("observing this function");
194            }
195        };
196
197        let observe_impl = syn::parse2::<ObserveImpl>(input);
198        assert!(observe_impl.is_ok());
199
200        let output_tokens = observe_impl.unwrap().gen_tokens();
201        let expected_tokens: TokenStream = quote! {
202            fn test_observe_fn() {
203                {
204                    let span = {
205                        let location = std::panic::Location::caller();
206
207                        tracing::trace_span!(
208                            target: "trace_tools::observe",
209                            "observed",
210                            observed.name = "test_observe_fn",
211                            loc.file = location.file(),
212                            loc.line = location.line(),
213                            loc.col = location.column(),
214                        )
215                    };
216
217                    let _guard = span.enter();
218                    {
219                        println!("observing this function");
220                    }
221                }
222            }
223        };
224
225        assert_eq!(output_tokens.to_string(), expected_tokens.to_string());
226    }
227}