blob: e64b0b22465be9f9955cf6697794b5b79e6e82dd [file] [log] [blame]
#![doc(html_root_url = "https://docs.rs/tracing-attributes/0.1.5")]
#![deny(missing_debug_implementations, unreachable_pub)]
#![cfg_attr(test, deny(warnings))]
//! A procedural macro attribute for instrumenting functions with [`tracing`].
//!
//! [`tracing`] is a framework for instrumenting Rust programs to collect
//! structured, event-based diagnostic information. This crate provides the
//! [`#[instrument]`][instrument] procedural macro attribute.
//!
//! Note that this macro is also re-exported by the main `tracing` crate.
//!
//! ## Usage
//!
//! First, add this to your `Cargo.toml`:
//!
//! ```toml
//! [dependencies]
//! tracing-attributes = "0.1.0"
//! ```
//!
//! The [`#[instrument]`][instrument] attribute can now be added to a function
//! to automatically create and enter `tracing` [span] when that function is
//! called. For example:
//!
//! ```
//! use tracing_attributes::instrument;
//!
//! #[instrument]
//! pub fn my_function(my_arg: usize) {
//! // ...
//! }
//!
//! # fn main() {}
//! ```
//!
//! [`tracing`]: https://crates.io/crates/tracing
//! [span]: https://docs.rs/tracing/0.1.5/tracing/span/index.html
//! [instrument]: attr.instrument.html
extern crate proc_macro;
use std::collections::HashSet;
use std::iter;
use proc_macro::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::{
spanned::Spanned, AttributeArgs, FieldPat, FnArg, Ident, ItemFn, Lit, LitInt, Meta, MetaList,
MetaNameValue, NestedMeta, Pat, PatIdent, PatReference, PatStruct, PatTuple, PatTupleStruct,
PatType, Signature,
};
/// Instruments a function to create and enter a `tracing` [span] every time
/// the function is called.
///
/// The generated span's name will be the name of the function, and any
/// arguments to that function will be recorded as fields using `fmt::Debug`.
///
/// # Examples
/// Instrumenting a function:
/// ```
/// # use tracing_attributes::instrument;
/// #[instrument]
/// pub fn my_function(my_arg: usize) {
/// // This event will be recorded inside a span named `my_function` with the
/// // field `my_arg`.
/// tracing::info!("inside my_function!");
/// // ...
/// }
/// # fn main() {}
/// ```
/// Setting the level for the generated span:
/// ```
/// # use tracing_attributes::instrument;
/// #[instrument(level = "debug")]
/// pub fn my_function() {
/// // ...
/// }
/// # fn main() {}
/// ```
/// Overriding the generated span's target:
/// ```
/// # use tracing_attributes::instrument;
/// #[instrument(target = "my_target")]
/// pub fn my_function() {
/// // ...
/// }
/// # fn main() {}
/// ```
///
/// When the `async-await` feature flag is enabled, `async fn`s may also be
/// instrumented:
///
/// ```compile_fail
/// // this currently only compiles on nightly.
/// #![feature(async-await)]
/// # use tracing_attributes::instrument;
///
/// #[instrument]
/// pub async fn my_function() -> Result<(), ()> {
/// // ...
/// # Ok(())
/// }
/// # fn main() {}
/// ```
///
/// # Notes
/// - All argument types must implement `fmt::Debug`
/// - When using `#[instrument]` on an `async fn`, the `tracing_futures` must
/// also be specified as a dependency in `Cargo.toml`.
///
/// [span]: https://docs.rs/tracing/0.1.5/tracing/span/index.html
/// [`tracing`]: https://github.com/tokio-rs/tracing
#[proc_macro_attribute]
pub fn instrument(args: TokenStream, item: TokenStream) -> TokenStream {
let input: ItemFn = syn::parse_macro_input!(item as ItemFn);
let args = syn::parse_macro_input!(args as AttributeArgs);
// these are needed ahead of time, as ItemFn contains the function body _and_
// isn't representable inside a quote!/quote_spanned! macro
// (Syn's ToTokens isn't implemented for ItemFn)
let ItemFn {
attrs,
vis,
block,
sig,
..
} = input;
let Signature {
output: return_type,
inputs: params,
unsafety,
asyncness,
constness,
abi,
ident,
generics:
syn::Generics {
params: gen_params,
where_clause,
..
},
..
} = sig;
// function name
let ident_str = ident.to_string();
// Pull out the arguments-to-be-skipped first, so we can filter results below.
let skips = match skips(&args) {
Ok(skips) => skips,
Err(err) => return quote!(#err).into(),
};
let param_names: Vec<Ident> = params
.clone()
.into_iter()
.flat_map(|param| match param {
FnArg::Typed(PatType { pat, .. }) => param_names(*pat),
FnArg::Receiver(_) => Box::new(iter::once(Ident::new("self", param.span()))),
})
.filter(|ident| !skips.contains(ident))
.collect();
let param_names_clone = param_names.clone();
// Generate the instrumented function body.
// If the function is an `async fn`, this will wrap it in an async block,
// which is `instrument`ed using `tracing-futures`. Otherwise, this will
// enter the span and then perform the rest of the body.
let body = if asyncness.is_some() {
// We can't quote these keywords in the `quote!` macro, since their
// presence in the file will make older Rust compilers fail to build
// this crate. Instead, we construct token structs for them so the
// strings "async" and "await" never actually appear in the source code
// of this file.
let async_kwd = syn::token::Async { span: block.span() };
let await_kwd = syn::Ident::new("await", block.span());
quote_spanned! {block.span()=>
tracing_futures::Instrument::instrument(
#async_kwd move { #block },
__tracing_attr_span
)
.#await_kwd
}
} else {
quote_spanned!(block.span()=>
let __tracing_attr_guard = __tracing_attr_span.enter();
#block
)
};
let level = level(&args);
let target = target(&args);
let span_name = name(&args, ident_str);
quote!(
#(#attrs) *
#vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #return_type
#where_clause
{
let __tracing_attr_span = tracing::span!(
target: #target,
#level,
#span_name,
#(#param_names = tracing::field::debug(&#param_names_clone)),*
);
#body
}
)
.into()
}
fn param_names(pat: Pat) -> Box<dyn Iterator<Item = Ident>> {
match pat {
Pat::Ident(PatIdent { ident, .. }) => Box::new(iter::once(ident)),
Pat::Reference(PatReference { pat, .. }) => param_names(*pat),
Pat::Struct(PatStruct { fields, .. }) => Box::new(
fields
.into_iter()
.flat_map(|FieldPat { pat, .. }| param_names(*pat)),
),
Pat::Tuple(PatTuple { elems, .. }) => Box::new(elems.into_iter().flat_map(param_names)),
Pat::TupleStruct(PatTupleStruct {
pat: PatTuple { elems, .. },
..
}) => Box::new(elems.into_iter().flat_map(param_names)),
// The above *should* cover all cases of irrefutable patterns,
// but we purposefully don't do any funny business here
// (such as panicking) because that would obscure rustc's
// much more informative error message.
_ => Box::new(iter::empty()),
}
}
fn skips(args: &AttributeArgs) -> Result<HashSet<Ident>, impl ToTokens> {
let mut skips = args.iter().filter_map(|arg| match arg {
NestedMeta::Meta(Meta::List(MetaList {
ref path,
ref nested,
..
})) if path.is_ident("skip") => Some(nested),
_ => None,
});
let skip = skips.next();
// Ensure there's only one skip directive.
if let Some(list) = skips.next() {
return Err(quote_spanned! {
list.span() => compile_error!("expected only a single `skip` argument!")
});
}
// Collect the Idents inside the `skip(...)`, if it exists
Ok(skip
.iter()
.map(|list| list.iter())
.flatten()
.filter_map(|meta| match meta {
NestedMeta::Meta(Meta::Path(p)) => p.get_ident().map(Clone::clone),
_ => None,
})
.collect())
}
fn level(args: &AttributeArgs) -> impl ToTokens {
let mut levels = args.iter().filter_map(|arg| match arg {
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
ref path, ref lit, ..
})) if path.is_ident("level") => Some(lit.clone()),
_ => None,
});
let level = levels.next();
// If we found more than one arg named "level", that's a syntax error...
if let Some(lit) = levels.next() {
return quote_spanned! {lit.span()=>
compile_error!("expected only a single `level` argument!")
};
}
fn is_level(lit: &LitInt, expected: u64) -> bool {
match lit.base10_parse::<u64>() {
Ok(value) => value == expected,
Err(_) => false,
}
}
match level {
Some(Lit::Str(ref lit)) if lit.value().eq_ignore_ascii_case("trace") => {
quote!(tracing::Level::TRACE)
}
Some(Lit::Str(ref lit)) if lit.value().eq_ignore_ascii_case("debug") => {
quote!(tracing::Level::DEBUG)
}
Some(Lit::Str(ref lit)) if lit.value().eq_ignore_ascii_case("info") => {
quote!(tracing::Level::INFO)
}
Some(Lit::Str(ref lit)) if lit.value().eq_ignore_ascii_case("warn") => {
quote!(tracing::Level::WARN)
}
Some(Lit::Str(ref lit)) if lit.value().eq_ignore_ascii_case("error") => {
quote!(tracing::Level::ERROR)
}
Some(Lit::Int(ref lit)) if is_level(lit, 1) => quote!(tracing::Level::TRACE),
Some(Lit::Int(ref lit)) if is_level(lit, 2) => quote!(tracing::Level::DEBUG),
Some(Lit::Int(ref lit)) if is_level(lit, 3) => quote!(tracing::Level::INFO),
Some(Lit::Int(ref lit)) if is_level(lit, 4) => quote!(tracing::Level::WARN),
Some(Lit::Int(ref lit)) if is_level(lit, 5) => quote!(tracing::Level::ERROR),
Some(lit) => quote_spanned! {lit.span()=>
compile_error!(
"unknown verbosity level, expected one of \"trace\", \
\"debug\", \"info\", \"warn\", or \"error\", or a number 1-5"
)
},
None => quote!(tracing::Level::INFO),
}
}
fn target(args: &AttributeArgs) -> impl ToTokens {
let mut levels = args.iter().filter_map(|arg| match arg {
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
ref path, ref lit, ..
})) if path.is_ident("target") => Some(lit.clone()),
_ => None,
});
let level = levels.next();
// If we found more than one arg named "level", that's a syntax error...
if let Some(lit) = levels.next() {
return quote_spanned! {lit.span()=>
compile_error!("expected only a single `target` argument!")
};
}
match level {
Some(Lit::Str(ref lit)) => quote!(#lit),
Some(lit) => quote_spanned! {lit.span()=>
compile_error!(
"expected target to be a string literal"
)
},
None => quote!(module_path!()),
}
}
fn name(args: &AttributeArgs, default_name: String) -> impl ToTokens {
let mut names = args.iter().filter_map(|arg| match arg {
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
ref path, ref lit, ..
})) if path.is_ident("name") => Some(lit.clone()),
_ => None,
});
let name = names.next();
// If we found more than one arg named "name", that's a syntax error.
if let Some(lit) = names.next() {
return quote_spanned! {lit.span() =>
compile_error!("expected only a single `name` argument!")
};
}
match name {
Some(Lit::Str(ref lit)) => quote!(#lit),
Some(lit) => {
quote_spanned! { lit.span() => compile_error!("expected name to be a string literal") }
}
None => quote!(#default_name),
}
}