Skip to content

Support constant expressions as instrument field names #3158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions tracing-attributes/src/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use proc_macro2::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::ext::IdentExt as _;
use syn::parse::{Parse, ParseStream};
use syn::token::Brace;

/// Arguments to `#[instrument(err(...))]` and `#[instrument(ret(...))]` which describe how the
/// return value event should be emitted.
Expand Down Expand Up @@ -307,7 +308,7 @@ pub(crate) struct Fields(pub(crate) Punctuated<Field, Token![,]>);

#[derive(Clone, Debug)]
pub(crate) struct Field {
pub(crate) name: Punctuated<Ident, Token![.]>,
pub(crate) name: FieldName,
pub(crate) value: Option<Expr>,
pub(crate) kind: FieldKind,
}
Expand All @@ -319,6 +320,25 @@ pub(crate) enum FieldKind {
Value,
}

#[derive(Clone, Debug)]
pub(crate) enum FieldName {
Expr(Expr),
Punctuated(Punctuated<Ident, Token![.]>),
}

impl ToTokens for FieldName {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
FieldName::Expr(expr) => {
Brace::default().surround(tokens, |tokens| {
expr.to_tokens(tokens)
});
},
FieldName::Punctuated(punctuated) => punctuated.to_tokens(tokens),
}
}
}

impl Parse for Fields {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let _ = input.parse::<kw::fields>();
Expand All @@ -345,7 +365,18 @@ impl Parse for Field {
input.parse::<Token![?]>()?;
kind = FieldKind::Debug;
};
let name = Punctuated::parse_separated_nonempty_with(input, Ident::parse_any)?;
// Parse name as either an expr between braces or a dotted identifier.
let name = if input.peek(syn::token::Brace) {
let content;
let _ = syn::braced!(content in input);
let expr = content.call(Expr::parse)?;
FieldName::Expr(expr)
} else {
FieldName::Punctuated(Punctuated::parse_separated_nonempty_with(
input,
Ident::parse_any,
)?)
};
let value = if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
if input.peek(Token![%]) {
Expand Down
14 changes: 11 additions & 3 deletions tracing-attributes/src/expand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use syn::{
};

use crate::{
attr::{Field, Fields, FormatMode, InstrumentArgs, Level},
attr::{Field, FieldName, Fields, FormatMode, InstrumentArgs, Level},
MaybeItemFn, MaybeItemFnRef,
};

Expand Down Expand Up @@ -211,8 +211,16 @@ fn gen_block<B: ToTokens>(
// and allow them to be formatted by the custom field.
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 == &param)
match name {
// #3158: Expressions cannot be evaluated at compile time and will
// incur a runtime cost to de-duplicate.
FieldName::Expr(_) => true,
FieldName::Punctuated(punctuated) => {
let first = punctuated.first();
first != punctuated.last()
|| !first.iter().any(|name| name == &param)
}
}
})
} else {
true
Expand Down
25 changes: 18 additions & 7 deletions tracing-attributes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,19 +206,22 @@ mod expand;
///
/// # Adding Fields
///
/// Additional fields (key-value pairs with arbitrary data) can be passed to
/// Additional fields (key-value pairs with arbitrary data) can be passed
/// to the generated span through the `fields` argument on the
/// `#[instrument]` macro. Strings, integers or boolean literals are accepted values
/// `#[instrument]` macro. Arbitrary expressions are accepted as value
/// for each field. The name of the field must be a single valid Rust
/// identifier, nested (dotted) field names are not supported. Any
/// identifier, or a constant expression that evaluates to one, enclosed in curly
/// braces. Note that nested (dotted) field names are also supported. Any
/// Rust expression can be used as a field value in this manner. These
/// expressions will be evaluated at the beginning of the function's body, so
/// arguments to the function may be used in these expressions. Field names may
/// also be specified *without* values. Doing so will result in an [empty field]
/// whose value may be recorded later within the function body.
///
/// Note that overlap between the names of fields and (non-skipped) arguments
/// will result in a compile error.
/// Note that defining a field with the same name as a (non-skipped)
/// argument will implicitly skip the argument, unless the field is provided
/// via a constant expression (e.g. {EXPR} or {const_fn()}) as deduplicating
/// would incur a runtime cost.
///
/// ## Examples
///
Expand Down Expand Up @@ -409,8 +412,16 @@ mod expand;
///
/// ```
/// # use tracing_attributes::instrument;
/// #[instrument(fields(foo="bar", id=1, show=true))]
/// fn my_function(arg: usize) {
/// #[derive(Debug)]
/// struct Argument;
/// impl Argument {
/// fn bar(&self) -> &'static str {
/// "bar"
/// }
/// }
/// const FOOBAR: &'static str = "foo.bar";
/// #[instrument(fields(foo="bar", id=1, show=true, {FOOBAR}=%arg.bar()))]
/// fn my_function(arg: Argument) {
/// // ...
/// }
/// ```
Expand Down
75 changes: 75 additions & 0 deletions tracing-attributes/tests/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,34 @@ fn fn_string(s: String) {
#[instrument(fields(keywords.impl.type.fn = _arg), skip(_arg))]
fn fn_keyword_ident_in_field(_arg: &str) {}

const CONST_FIELD_NAME: &str = "foo.bar";

#[instrument(fields({CONST_FIELD_NAME} = "baz"))]
fn fn_const_field_name() {}

const fn get_const_fn_field_name() -> &'static str {
"foo.bar"
}

#[instrument(fields({get_const_fn_field_name()} = "baz"))]
fn fn_const_fn_field_name() {}

struct FieldNames {}
impl FieldNames {
const FOO_BAR: &'static str = "foo.bar";
}

#[instrument(fields({FieldNames::FOO_BAR} = "baz"))]
fn fn_struct_const_field_name() {}

#[instrument(fields({"foo"} = "bar"))]
fn fn_string_field_name() {}

const CLASHY_FIELD_NAME: &str = "s";

#[instrument(fields({CLASHY_FIELD_NAME} = "foo"))]
fn fn_clashy_const_field_name(s: &str) { let _ = s; }

#[derive(Debug)]
struct HasField {
my_field: &'static str,
Expand Down Expand Up @@ -159,6 +187,53 @@ fn keyword_ident_in_field_name() {
run_test(span, || fn_keyword_ident_in_field("test"));
}

#[test]
fn expr_const_field_name() {
let span = expect::span().with_fields(expect::field("foo.bar").with_value(&"baz").only());
run_test(span, || {
fn_const_field_name();
});
}

#[test]
fn expr_const_fn_field_name() {
let span = expect::span().with_fields(expect::field("foo.bar").with_value(&"baz").only());
run_test(span, || {
fn_const_fn_field_name();
});
}

#[test]
fn struct_const_field_name() {
let span = expect::span().with_fields(expect::field("foo.bar").with_value(&"baz").only());
run_test(span, || {
fn_struct_const_field_name();
});
}

#[test]
fn string_field_name() {
let span = expect::span().with_fields(expect::field("foo").with_value(&"bar").only());
run_test(span, || {
fn_string_field_name();
});
}

#[test]
fn clashy_const_field_name() {
let span = expect::span().with_fields(
// #3158: To be consistent with event! and span! macros, the duplicated value should be
// dropped, but checking for duplicated fields would incur a significant runtime cost, as
// non-trivial constants (const, const fn, ...) cannot be evaluated at compile time.
expect::field("s")
.with_value(&"foo")
.and(expect::field("s").with_value(&"hello world")),
);
run_test(span, || {
fn_clashy_const_field_name("hello world");
});
}

fn run_test<F: FnOnce() -> T, T>(span: NewSpan, fun: F) {
let (subscriber, handle) = subscriber::mock()
.new_span(span)
Expand Down