-
Notifications
You must be signed in to change notification settings - Fork 834
Description
Feature Request
Provide an easy way to print a call tree summary after each processed request or another meaningful unit of work in your application. The call tree should be based on the tracing spans. Events are ignored.
(I am writing this feature proposal after the nice encouragement by @hawkw)
Output
In the past, roughly the information that you would find in a flame graph but in text form has proven very helpful. Here is my first proposal for an output format:
[# calls] wall ms | own ms | span name
----------------------------------------------------------------------
[ 1] 7,033.22 | 20,25 | graphql
[ 1] 23.22 | 3,20 | |- database_pool.get
[ 1] 611.21 | 5,23 | |- elections.query
[ 15] 511.21 | 3,23 | | |- election_options.query
[ 15] 311.21 | 5,22 | | | |- election_comments.query
[ 1] 033.22 | 20,25 | |- commit transaction
[# calls]the total number of calls for this call tree path.wall msthe total wall time that a span with this call path was alive in ms (Subscriber::new_spanuntiltry_close). Edit: wassum msbefore but this is misleading.own msthe total wall time that executing was in a span with this call path (Subscriber::enteruntilSubscriber::leave).span nameThe name from theMetadata. We could also add the callsite for disambiguation but this is probably not necessary.some relatively short identifier for the span -- probably we should allow customizing this so that each user can create a function that creates an appropriate short name for each span.
The order of tree nodes: There should be only one entry for each call path but the spans should be sorted by the first time they were seen. Think of storing the children of each call path in something like a linked_hash_map. That way, the order of the children resembles the order in which they were called. For repeated calls, the order is often still quite readable since it is typical that some sub sequence of calls is simply repeated in a loop.
That, in practice, gives you a lovely outline of how your request was processed.
Crates
I think that we should create a new sub crate for this since the functionality is orthogonal to the rest and only users who want this should pay the price. Alternatively, it might be enabled by a feature on the tracing-subscriber crate.
The data model built by this subscriber might be useful for other summaries and might be extracted into another crate or a lib in the tracing-subscriber crate. I'd start with keeping the code together in one place, though.
Motivation
Let's assume a simple data model for the above example. Every election has several election_options for voting. Users can comment on every election_option with an election_comment.
- Counts are useful: With the call tree above e.g. "N+1" problems become obvious. One can suspect that there is one query per election because there are multiple
election_options.querycalls for only oneelections.querycall. Fortunately, the comments do not seem to be queried individually since their cardinality is the same as the parent. wall msis useful: We do see where the majority of the latency comes from.own msis useful: We can confirm that our app mostly waits for the database. At least for async this should work well, for sync code we should rely on sub spans.
Proposal
A new configurable subscriber should be created for this. I assume that the Layer/Subscriber infrastructure in tracing-subscriber is a good match but I haven't looked into the details.
Introspection into spans:
-
summary roots: By default, the subscriber should start tracking at all root spans.
the subscriber should start tracking call paths for any spans that have a marker field, like e.g.. This should be overridable.summary_root -
short names: The subscriber needs to get a short name for each span. This should be user configurable.(not necessary, the metadata for spans already includes a "name" that is suitable for this)
New spans which are not children of summary roots can be completely ignored.
When a summary root is try_closed, the summary as defined above should be printed.
Synthetic example
use tracing::{info_span, info, instrument};
#[instrument]
fn repeated(repetition: i32) {
info!("repetition: {}", repetition);
}
#[instrument]
fn nested() {
for i in 1..=10 {
repeated(i);
}
}
#[instrument]
fn request() {
nested();
repeated(-1);
// Even though the name is the same, this is a different span.
let name_clash_span = info_span!("nested");
let _enter = name_clash_span.enter();
}
request();should result in the following tree (times are obviously unrealistic):
[# calls] wall ms | own ms | span name
----------------------------------------------------------------------
[ 1] 12.22 | 0,25 | request
[ 1] 11.21 | 0,23 | |- nested
[ 10] 11.21 | 0,23 | | |- repeated
[ 1] 1.21 | 0,23 | |- repeated
[ 1] 0.27 | 0,25 | |- nested