#![doc(html_root_url = "https://docs.rs/tracing-attributes/0.1.13")]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/tokio-rs/tracing/master/assets/logo-type.png",
issue_tracker_base_url = "https://github.com/tokio-rs/tracing/issues/"
)]
#![cfg_attr(docsrs, deny(broken_intra_doc_links))]
#![warn(
missing_debug_implementations,
missing_docs,
rust_2018_idioms,
unreachable_pub,
bad_style,
const_err,
dead_code,
improper_ctypes,
non_shorthand_field_patterns,
no_mangle_generic_items,
overflowing_literals,
path_statements,
patterns_in_fns_without_body,
private_in_public,
unconditional_recursion,
unused,
unused_allocation,
unused_comparisons,
unused_parens,
while_true
)]
#![allow(unused)]
extern crate proc_macro;
use std::collections::{HashMap, HashSet};
use std::iter;
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned, ToTokens, TokenStreamExt as _};
use syn::ext::IdentExt as _;
use syn::parse::{Parse, ParseStream};
use syn::{
punctuated::Punctuated, spanned::Spanned, AttributeArgs, Block, Expr, ExprCall, FieldPat,
FnArg, Ident, Item, ItemFn, Lit, LitInt, LitStr, Meta, MetaList, MetaNameValue, NestedMeta,
Pat, PatIdent, PatReference, PatStruct, PatTuple, PatTupleStruct, PatType, Path, Signature,
Stmt, Token,
};
#[proc_macro_attribute]
pub fn instrument(
args: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let input: ItemFn = syn::parse_macro_input!(item as ItemFn);
let args = syn::parse_macro_input!(args as InstrumentArgs);
let instrumented_function_name = input.sig.ident.to_string();
if let Some(internal_fun) = get_async_trait_info(&input.block, input.sig.asyncness.is_some()) {
let mut stmts: Vec<Stmt> = input.block.stmts.to_vec();
for stmt in &mut stmts {
if let Stmt::Item(Item::Fn(fun)) = stmt {
if fun.sig.ident == internal_fun.name {
*stmt = syn::parse2(gen_body(
fun,
args,
instrumented_function_name,
Some(internal_fun),
))
.unwrap();
break;
}
}
}
let vis = &input.vis;
let sig = &input.sig;
let attrs = &input.attrs;
quote!(
#(#attrs) *
#vis #sig {
#(#stmts) *
}
)
.into()
} else {
gen_body(&input, args, instrumented_function_name, None).into()
}
}
fn gen_body(
input: &ItemFn,
mut args: InstrumentArgs,
instrumented_function_name: String,
async_trait_fun: Option<AsyncTraitInfo>,
) -> proc_macro2::TokenStream {
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;
let err = args.err;
let warnings = args.warnings();
let span_name = args
.name
.as_ref()
.map(|name| quote!(#name))
.unwrap_or_else(|| quote!(#instrumented_function_name));
let span = (|| {
let param_names: Vec<(Ident, 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()))),
})
.map(|x| {
if async_trait_fun.is_some() && x == "_self" {
(Ident::new("self", x.span()), x)
} else {
(x.clone(), x)
}
})
.collect();
for skip in &args.skips {
if !param_names.iter().map(|(user, _)| user).any(|y| y == skip) {
return quote_spanned! {skip.span()=>
compile_error!("attempting to skip non-existent parameter")
};
}
}
let level = args.level();
let target = args.target();
let mut quoted_fields: Vec<_> = param_names
.into_iter()
.filter(|(param, _)| {
if args.skips.contains(param) {
return false;
}
if let Some(ref fields) = args.fields {
fields.0.iter().all(|Field { ref name, .. }| {
let first = name.first();
first != name.last() || !first.iter().any(|name| name == ¶m)
})
} else {
true
}
})
.map(|(user_name, real_name)| quote!(#user_name = tracing::field::debug(&#real_name)))
.collect();
if let (Some(ref async_trait_fun), Some(Fields(ref mut fields))) =
(async_trait_fun, &mut args.fields)
{
let mut replacer = SelfReplacer {
ty: async_trait_fun.self_type.clone(),
};
for e in fields.iter_mut().filter_map(|f| f.value.as_mut()) {
syn::visit_mut::visit_expr_mut(&mut replacer, e);
}
}
let custom_fields = &args.fields;
quote!(tracing::span!(
target: #target,
#level,
#span_name,
#(#quoted_fields,)*
#custom_fields
))
})();
let body = if asyncness.is_some() {
if err {
quote_spanned! {block.span()=>
let __tracing_attr_span = #span;
tracing::Instrument::instrument(async move {
match async move { #block }.await {
#[allow(clippy::unit_arg)]
Ok(x) => Ok(x),
Err(e) => {
tracing::error!(error = %e);
Err(e)
}
}
}, __tracing_attr_span).await
}
} else {
quote_spanned!(block.span()=>
let __tracing_attr_span = #span;
tracing::Instrument::instrument(
async move { #block },
__tracing_attr_span
)
.await
)
}
} else if err {
quote_spanned!(block.span()=>
let __tracing_attr_span = #span;
let __tracing_attr_guard = __tracing_attr_span.enter();
#[allow(clippy::redundant_closure_call)]
match (move || #block)() {
#[allow(clippy::unit_arg)]
Ok(x) => Ok(x),
Err(e) => {
tracing::error!(error = %e);
Err(e)
}
}
)
} else {
quote_spanned!(block.span()=>
let __tracing_attr_span = #span;
let __tracing_attr_guard = __tracing_attr_span.enter();
#block
)
};
quote!(
#(#attrs) *
#vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #return_type
#where_clause
{
#warnings
#body
}
)
}
#[derive(Default, Debug)]
struct InstrumentArgs {
level: Option<Level>,
name: Option<LitStr>,
target: Option<LitStr>,
skips: HashSet<Ident>,
fields: Option<Fields>,
err: bool,
parse_warnings: Vec<syn::Error>,
}
impl InstrumentArgs {
fn level(&self) -> impl ToTokens {
fn is_level(lit: &LitInt, expected: u64) -> bool {
match lit.base10_parse::<u64>() {
Ok(value) => value == expected,
Err(_) => false,
}
}
match &self.level {
Some(Level::Str(ref lit)) if lit.value().eq_ignore_ascii_case("trace") => {
quote!(tracing::Level::TRACE)
}
Some(Level::Str(ref lit)) if lit.value().eq_ignore_ascii_case("debug") => {
quote!(tracing::Level::DEBUG)
}
Some(Level::Str(ref lit)) if lit.value().eq_ignore_ascii_case("info") => {
quote!(tracing::Level::INFO)
}
Some(Level::Str(ref lit)) if lit.value().eq_ignore_ascii_case("warn") => {
quote!(tracing::Level::WARN)
}
Some(Level::Str(ref lit)) if lit.value().eq_ignore_ascii_case("error") => {
quote!(tracing::Level::ERROR)
}
Some(Level::Int(ref lit)) if is_level(lit, 1) => quote!(tracing::Level::TRACE),
Some(Level::Int(ref lit)) if is_level(lit, 2) => quote!(tracing::Level::DEBUG),
Some(Level::Int(ref lit)) if is_level(lit, 3) => quote!(tracing::Level::INFO),
Some(Level::Int(ref lit)) if is_level(lit, 4) => quote!(tracing::Level::WARN),
Some(Level::Int(ref lit)) if is_level(lit, 5) => quote!(tracing::Level::ERROR),
Some(Level::Path(ref pat)) => quote!(#pat),
Some(lit) => quote! {
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(&self) -> impl ToTokens {
if let Some(ref target) = self.target {
quote!(#target)
} else {
quote!(module_path!())
}
}
fn warnings(&self) -> impl ToTokens {
let warnings = self.parse_warnings.iter().map(|err| {
let msg = format!("found unrecognized input, {}", err);
let msg = LitStr::new(&msg, err.span());
quote_spanned! {err.span()=>
#[warn(deprecated)]
{
#[deprecated(since = "not actually deprecated", note = #msg)]
const TRACING_INSTRUMENT_WARNING: () = ();
let _ = TRACING_INSTRUMENT_WARNING;
}
}
});
quote! {
{ #(#warnings)* }
}
}
}
impl Parse for InstrumentArgs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut args = Self::default();
while !input.is_empty() {
let lookahead = input.lookahead1();
if lookahead.peek(kw::name) {
if args.name.is_some() {
return Err(input.error("expected only a single `name` argument"));
}
let name = input.parse::<StrArg<kw::name>>()?.value;
args.name = Some(name);
} else if lookahead.peek(LitStr) {
if args.name.is_some() {
return Err(input.error("expected only a single `name` argument"));
}
args.name = Some(input.parse()?);
} else if lookahead.peek(kw::target) {
if args.target.is_some() {
return Err(input.error("expected only a single `target` argument"));
}
let target = input.parse::<StrArg<kw::target>>()?.value;
args.target = Some(target);
} else if lookahead.peek(kw::level) {
if args.level.is_some() {
return Err(input.error("expected only a single `level` argument"));
}
args.level = Some(input.parse()?);
} else if lookahead.peek(kw::skip) {
if !args.skips.is_empty() {
return Err(input.error("expected only a single `skip` argument"));
}
let Skips(skips) = input.parse()?;
args.skips = skips;
} else if lookahead.peek(kw::fields) {
if args.fields.is_some() {
return Err(input.error("expected only a single `fields` argument"));
}
args.fields = Some(input.parse()?);
} else if lookahead.peek(kw::err) {
let _ = input.parse::<kw::err>()?;
args.err = true;
} else if lookahead.peek(Token![,]) {
let _ = input.parse::<Token![,]>()?;
} else {
args.parse_warnings.push(lookahead.error());
let _ = input.parse::<proc_macro2::TokenTree>();
}
}
Ok(args)
}
}
struct StrArg<T> {
value: LitStr,
_p: std::marker::PhantomData<T>,
}
impl<T: Parse> Parse for StrArg<T> {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let _ = input.parse::<T>()?;
let _ = input.parse::<Token![=]>()?;
let value = input.parse()?;
Ok(Self {
value,
_p: std::marker::PhantomData,
})
}
}
struct Skips(HashSet<Ident>);
impl Parse for Skips {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let _ = input.parse::<kw::skip>();
let content;
let _ = syn::parenthesized!(content in input);
let names: Punctuated<Ident, Token![,]> = content.parse_terminated(Ident::parse_any)?;
let mut skips = HashSet::new();
for name in names {
if skips.contains(&name) {
return Err(syn::Error::new(
name.span(),
"tried to skip the same field twice",
));
} else {
skips.insert(name);
}
}
Ok(Self(skips))
}
}
#[derive(Debug)]
struct Fields(Punctuated<Field, Token![,]>);
#[derive(Debug)]
struct Field {
name: Punctuated<Ident, Token![.]>,
value: Option<Expr>,
kind: FieldKind,
}
#[derive(Debug, Eq, PartialEq)]
enum FieldKind {
Debug,
Display,
Value,
}
impl Parse for Fields {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let _ = input.parse::<kw::fields>();
let content;
let _ = syn::parenthesized!(content in input);
let fields: Punctuated<_, Token![,]> = content.parse_terminated(Field::parse)?;
Ok(Self(fields))
}
}
impl ToTokens for Fields {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.0.to_tokens(tokens)
}
}
impl Parse for Field {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut kind = FieldKind::Value;
if input.peek(Token![%]) {
input.parse::<Token![%]>()?;
kind = FieldKind::Display;
} else if input.peek(Token![?]) {
input.parse::<Token![?]>()?;
kind = FieldKind::Debug;
};
let name = Punctuated::parse_separated_nonempty_with(input, Ident::parse_any)?;
let value = if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
if input.peek(Token![%]) {
input.parse::<Token![%]>()?;
kind = FieldKind::Display;
} else if input.peek(Token![?]) {
input.parse::<Token![?]>()?;
kind = FieldKind::Debug;
};
Some(input.parse()?)
} else {
None
};
Ok(Self { name, kind, value })
}
}
impl ToTokens for Field {
fn to_tokens(&self, tokens: &mut TokenStream) {
if let Some(ref value) = self.value {
let name = &self.name;
let kind = &self.kind;
tokens.extend(quote! {
#name = #kind#value
})
} else if self.kind == FieldKind::Value {
let name = &self.name;
tokens.extend(quote!(#name = tracing::field::Empty))
} else {
self.kind.to_tokens(tokens);
self.name.to_tokens(tokens);
}
}
}
impl ToTokens for FieldKind {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
FieldKind::Debug => tokens.extend(quote! { ? }),
FieldKind::Display => tokens.extend(quote! { % }),
_ => {}
}
}
}
#[derive(Debug)]
enum Level {
Str(LitStr),
Int(LitInt),
Path(Path),
}
impl Parse for Level {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let _ = input.parse::<kw::level>()?;
let _ = input.parse::<Token![=]>()?;
let lookahead = input.lookahead1();
if lookahead.peek(LitStr) {
Ok(Self::Str(input.parse()?))
} else if lookahead.peek(LitInt) {
Ok(Self::Int(input.parse()?))
} else if lookahead.peek(Ident) {
Ok(Self::Path(input.parse()?))
} else {
Err(lookahead.error())
}
}
}
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)),
_ => Box::new(iter::empty()),
}
}
mod kw {
syn::custom_keyword!(fields);
syn::custom_keyword!(skip);
syn::custom_keyword!(level);
syn::custom_keyword!(target);
syn::custom_keyword!(name);
syn::custom_keyword!(err);
}
fn get_async_trait_function(block: &Block, block_is_async: bool) -> Option<&ItemFn> {
if block_is_async {
return None;
}
let mut inside_funs = Vec::new();
let mut last_expr = None;
for stmt in &block.stmts {
if let Stmt::Item(Item::Fn(fun)) = &stmt {
if fun.sig.asyncness.is_some() {
inside_funs.push(fun);
}
} else if let Stmt::Expr(e) = &stmt {
last_expr = Some(e);
}
}
if let Some(Expr::Call(ExprCall {
func: outside_func,
args: outside_args,
..
})) = last_expr
{
if let Expr::Path(path) = outside_func.as_ref() {
if "Box::pin" == path_to_string(&path.path) {
if outside_args.is_empty() {
return None;
}
if let Expr::Call(ExprCall { func, args, .. }) = &outside_args[0] {
if let Expr::Path(inside_path) = func.as_ref() {
let func_name = path_to_string(&inside_path.path);
for fun in inside_funs {
if fun.sig.ident == func_name {
return Some(fun);
}
}
}
}
}
}
}
None
}
struct AsyncTraitInfo {
name: String,
self_type: Option<syn::TypePath>,
}
fn get_async_trait_info(block: &Block, block_is_async: bool) -> Option<AsyncTraitInfo> {
let fun = get_async_trait_function(block, block_is_async)?;
let self_type = fun
.sig
.inputs
.iter()
.map(|arg| {
if let FnArg::Typed(ty) = arg {
if let Pat::Ident(PatIdent { ident, .. }) = &*ty.pat {
if ident == "_self" {
let mut ty = &*ty.ty;
if let syn::Type::Reference(syn::TypeReference { elem, .. }) = ty {
ty = &*elem;
}
if let syn::Type::Path(tp) = ty {
return Some(tp.clone());
}
}
}
}
None
})
.next();
let self_type = match self_type {
Some(x) => x,
None => None,
};
Some(AsyncTraitInfo {
name: fun.sig.ident.to_string(),
self_type,
})
}
fn path_to_string(path: &Path) -> String {
use std::fmt::Write;
let mut res = String::with_capacity(path.segments.len() * 5);
for i in 0..path.segments.len() {
write!(&mut res, "{}", path.segments[i].ident)
.expect("writing to a String should never fail");
if i < path.segments.len() - 1 {
res.push_str("::");
}
}
res
}
struct SelfReplacer {
ty: Option<syn::TypePath>,
}
impl syn::visit_mut::VisitMut for SelfReplacer {
fn visit_ident_mut(&mut self, id: &mut Ident) {
if id == "self" {
*id = Ident::new("_self", id.span())
}
}
fn visit_type_mut(&mut self, ty: &mut syn::Type) {
if let syn::Type::Path(syn::TypePath { ref mut path, .. }) = ty {
if path_to_string(path) == "Self" {
if let Some(ref true_type) = self.ty {
*path = true_type.path.clone();
}
}
}
}
}