Skip to content

Commit 45af4ee

Browse files
jswrennhawkw
authored andcommitted
attributes: permit #[instrument(follows_from = …)] (#2093)
This PR extends the `#[instrument]` attribute to accept an optional `follows_from = …` argument that supplies any number of `Span::follows_from` relationships to the generated `Span`. ## Motivation This PR resolves #879. ## Solution This PR largely follows the implementation strategy articulated by @hawkw: #879 (comment) In that comment, @hawkw suggests taking one of two approaches: 1. each `follows_from` relationship is supplied with a distinct `follows_from` argument 2. the `follows_from` argument is provided once, and its value is a **list** of indirect causes I take the second approach, since it is slightly more flexible: it allows for the number of indirect causes to vary at runtime. This addition is complemented by changes to `tracing-mock` to permit making `follows_from` assertions for testing purposes.
1 parent bead0db commit 45af4ee

File tree

5 files changed

+181
-2
lines changed

5 files changed

+181
-2
lines changed

tracing-attributes/src/attr.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub(crate) struct InstrumentArgs {
1212
pub(crate) name: Option<LitStr>,
1313
target: Option<LitStr>,
1414
pub(crate) parent: Option<Expr>,
15+
pub(crate) follows_from: Option<Expr>,
1516
pub(crate) skips: HashSet<Ident>,
1617
pub(crate) skip_all: bool,
1718
pub(crate) fields: Option<Fields>,
@@ -130,6 +131,12 @@ impl Parse for InstrumentArgs {
130131
}
131132
let parent = input.parse::<ExprArg<kw::parent>>()?;
132133
args.parent = Some(parent.value);
134+
} else if lookahead.peek(kw::follows_from) {
135+
if args.target.is_some() {
136+
return Err(input.error("expected only a single `follows_from` argument"));
137+
}
138+
let follows_from = input.parse::<ExprArg<kw::follows_from>>()?;
139+
args.follows_from = Some(follows_from.value);
133140
} else if lookahead.peek(kw::level) {
134141
if args.level.is_some() {
135142
return Err(input.error("expected only a single `level` argument"));
@@ -399,6 +406,7 @@ mod kw {
399406
syn::custom_keyword!(level);
400407
syn::custom_keyword!(target);
401408
syn::custom_keyword!(parent);
409+
syn::custom_keyword!(follows_from);
402410
syn::custom_keyword!(name);
403411
syn::custom_keyword!(err);
404412
syn::custom_keyword!(ret);

tracing-attributes/src/expand.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ fn gen_block<B: ToTokens>(
8888

8989
let level = args.level();
9090

91+
let follows_from = args.follows_from.iter();
92+
let follows_from = quote! {
93+
#(for cause in #follows_from {
94+
__tracing_attr_span.follows_from(cause);
95+
})*
96+
};
97+
9198
// generate this inside a closure, so we can return early on errors.
9299
let span = (|| {
93100
// Pull out the arguments-to-be-skipped first, so we can filter results
@@ -261,6 +268,7 @@ fn gen_block<B: ToTokens>(
261268
let __tracing_attr_span = #span;
262269
let __tracing_instrument_future = #mk_fut;
263270
if !__tracing_attr_span.is_disabled() {
271+
#follows_from
264272
tracing::Instrument::instrument(
265273
__tracing_instrument_future,
266274
__tracing_attr_span
@@ -287,6 +295,7 @@ fn gen_block<B: ToTokens>(
287295
let __tracing_attr_guard;
288296
if tracing::level_enabled!(#level) {
289297
__tracing_attr_span = #span;
298+
#follows_from
290299
__tracing_attr_guard = __tracing_attr_span.enter();
291300
}
292301
);

tracing-attributes/src/lib.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,24 @@ mod expand;
368368
/// fn my_method(&self) {}
369369
/// }
370370
/// ```
371+
/// Specifying [`follows_from`] relationships:
372+
/// ```
373+
/// # use tracing_attributes::instrument;
374+
/// #[instrument(follows_from = causes)]
375+
/// pub fn my_function(causes: &[tracing::Id]) {
376+
/// // ...
377+
/// }
378+
/// ```
379+
/// Any expression of type `impl IntoIterator<Item = impl Into<Option<Id>>>`
380+
/// may be provided to `follows_from`; e.g.:
381+
/// ```
382+
/// # use tracing_attributes::instrument;
383+
/// #[instrument(follows_from = [cause])]
384+
/// pub fn my_function(cause: &tracing::span::EnteredSpan) {
385+
/// // ...
386+
/// }
387+
/// ```
388+
///
371389
///
372390
/// To skip recording an argument, pass the argument's name to the `skip`:
373391
///
@@ -524,6 +542,8 @@ mod expand;
524542
/// [`INFO`]: https://docs.rs/tracing/latest/tracing/struct.Level.html#associatedconstant.INFO
525543
/// [empty field]: https://docs.rs/tracing/latest/tracing/field/struct.Empty.html
526544
/// [field syntax]: https://docs.rs/tracing/latest/tracing/#recording-fields
545+
/// [`follows_from`]: https://docs.rs/tracing/latest/tracing/struct.Span.html#method.follows_from
546+
/// [`tracing`]: https://github.com/tokio-rs/tracing
527547
/// [`fmt::Debug`]: std::fmt::Debug
528548
#[proc_macro_attribute]
529549
pub fn instrument(
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use tracing::{subscriber::with_default, Id, Level, Span};
2+
use tracing_attributes::instrument;
3+
use tracing_mock::*;
4+
5+
#[instrument(follows_from = causes, skip(causes))]
6+
fn with_follows_from_sync(causes: impl IntoIterator<Item = impl Into<Option<Id>>>) {}
7+
8+
#[instrument(follows_from = causes, skip(causes))]
9+
async fn with_follows_from_async(causes: impl IntoIterator<Item = impl Into<Option<Id>>>) {}
10+
11+
#[instrument(follows_from = [&Span::current()])]
12+
fn follows_from_current() {}
13+
14+
#[test]
15+
fn follows_from_sync_test() {
16+
let cause_a = span::mock().named("cause_a");
17+
let cause_b = span::mock().named("cause_b");
18+
let cause_c = span::mock().named("cause_c");
19+
let consequence = span::mock().named("with_follows_from_sync");
20+
21+
let (subscriber, handle) = subscriber::mock()
22+
.new_span(cause_a.clone())
23+
.new_span(cause_b.clone())
24+
.new_span(cause_c.clone())
25+
.new_span(consequence.clone())
26+
.follows_from(consequence.clone(), cause_a)
27+
.follows_from(consequence.clone(), cause_b)
28+
.follows_from(consequence.clone(), cause_c)
29+
.enter(consequence.clone())
30+
.exit(consequence)
31+
.done()
32+
.run_with_handle();
33+
34+
with_default(subscriber, || {
35+
let cause_a = tracing::span!(Level::TRACE, "cause_a");
36+
let cause_b = tracing::span!(Level::TRACE, "cause_b");
37+
let cause_c = tracing::span!(Level::TRACE, "cause_c");
38+
39+
with_follows_from_sync(&[cause_a, cause_b, cause_c])
40+
});
41+
42+
handle.assert_finished();
43+
}
44+
45+
#[test]
46+
fn follows_from_async_test() {
47+
let cause_a = span::mock().named("cause_a");
48+
let cause_b = span::mock().named("cause_b");
49+
let cause_c = span::mock().named("cause_c");
50+
let consequence = span::mock().named("with_follows_from_async");
51+
52+
let (subscriber, handle) = subscriber::mock()
53+
.new_span(cause_a.clone())
54+
.new_span(cause_b.clone())
55+
.new_span(cause_c.clone())
56+
.new_span(consequence.clone())
57+
.follows_from(consequence.clone(), cause_a)
58+
.follows_from(consequence.clone(), cause_b)
59+
.follows_from(consequence.clone(), cause_c)
60+
.enter(consequence.clone())
61+
.exit(consequence)
62+
.done()
63+
.run_with_handle();
64+
65+
with_default(subscriber, || {
66+
block_on_future(async {
67+
let cause_a = tracing::span!(Level::TRACE, "cause_a");
68+
let cause_b = tracing::span!(Level::TRACE, "cause_b");
69+
let cause_c = tracing::span!(Level::TRACE, "cause_c");
70+
71+
with_follows_from_async(&[cause_a, cause_b, cause_c]).await
72+
})
73+
});
74+
75+
handle.assert_finished();
76+
}
77+
78+
#[test]
79+
fn follows_from_current_test() {
80+
let cause = span::mock().named("cause");
81+
let consequence = span::mock().named("follows_from_current");
82+
83+
let (subscriber, handle) = subscriber::mock()
84+
.new_span(cause.clone())
85+
.enter(cause.clone())
86+
.new_span(consequence.clone())
87+
.follows_from(consequence.clone(), cause.clone())
88+
.enter(consequence.clone())
89+
.exit(consequence)
90+
.exit(cause)
91+
.done()
92+
.run_with_handle();
93+
94+
with_default(subscriber, || {
95+
tracing::span!(Level::TRACE, "cause").in_scope(follows_from_current)
96+
});
97+
98+
handle.assert_finished();
99+
}

tracing-mock/src/subscriber.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ use tracing::{
2323
#[derive(Debug, Eq, PartialEq)]
2424
pub enum Expect {
2525
Event(MockEvent),
26+
FollowsFrom {
27+
consequence: MockSpan,
28+
cause: MockSpan,
29+
},
2630
Enter(MockSpan),
2731
Exit(MockSpan),
2832
CloneSpan(MockSpan),
@@ -97,6 +101,12 @@ where
97101
self
98102
}
99103

104+
pub fn follows_from(mut self, consequence: MockSpan, cause: MockSpan) -> Self {
105+
self.expected
106+
.push_back(Expect::FollowsFrom { consequence, cause });
107+
self
108+
}
109+
100110
pub fn event(mut self, event: MockEvent) -> Self {
101111
self.expected.push_back(Expect::Event(event));
102112
self
@@ -250,8 +260,37 @@ where
250260
}
251261
}
252262

253-
fn record_follows_from(&self, _span: &Id, _follows: &Id) {
254-
// TODO: it should be possible to expect spans to follow from other spans
263+
fn record_follows_from(&self, consequence_id: &Id, cause_id: &Id) {
264+
let spans = self.spans.lock().unwrap();
265+
if let Some(consequence_span) = spans.get(consequence_id) {
266+
if let Some(cause_span) = spans.get(cause_id) {
267+
println!(
268+
"[{}] record_follows_from: {} (id={:?}) follows {} (id={:?})",
269+
self.name, consequence_span.name, consequence_id, cause_span.name, cause_id,
270+
);
271+
match self.expected.lock().unwrap().pop_front() {
272+
None => {}
273+
Some(Expect::FollowsFrom {
274+
consequence: ref expected_consequence,
275+
cause: ref expected_cause,
276+
}) => {
277+
if let Some(name) = expected_consequence.name() {
278+
assert_eq!(name, consequence_span.name);
279+
}
280+
if let Some(name) = expected_cause.name() {
281+
assert_eq!(name, cause_span.name);
282+
}
283+
}
284+
Some(ex) => ex.bad(
285+
&self.name,
286+
format_args!(
287+
"consequence {:?} followed cause {:?}",
288+
consequence_span.name, cause_span.name
289+
),
290+
),
291+
}
292+
}
293+
};
255294
}
256295

257296
fn new_span(&self, span: &Attributes<'_>) -> Id {
@@ -442,6 +481,10 @@ impl Expect {
442481
"\n[{}] expected event {}\n[{}] but instead {}",
443482
name, e, name, what,
444483
),
484+
Expect::FollowsFrom { consequence, cause } => panic!(
485+
"\n[{}] expected consequence {} to follow cause {} but instead {}",
486+
name, consequence, cause, what,
487+
),
445488
Expect::Enter(e) => panic!(
446489
"\n[{}] expected to enter {}\n[{}] but instead {}",
447490
name, e, name, what,

0 commit comments

Comments
 (0)