Skip to content

Call tree summary, e.g. after each request #639

@kolloch

Description

@kolloch

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 ms the total wall time that a span with this call path was alive in ms (Subscriber::new_span until try_close). Edit: was sum ms before but this is misleading.
  • own ms the total wall time that executing was in a span with this call path (Subscriber::enter until Subscriber::leave).
  • span name The name from the Metadata. 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.query calls for only one elections.query call. Fortunately, the comments do not seem to be queried individually since their cardinality is the same as the parent.
  • wall ms is useful: We do see where the majority of the latency comes from.
  • own ms is 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. summary_root. This should be overridable.

  • 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind/featureNew feature or requestkind/rfcA request for comments to discuss future changes

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions