Skip to content
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

subscriber: add an abstraction for building visitors #241

Merged
merged 23 commits into from
Oct 8, 2019
Merged

Conversation

hawkw
Copy link
Member

@hawkw hawkw commented Aug 5, 2019

Motivation

The tracing-fmt crate crate currently uses a NewVisitor trait to
abstract over constructing field visitors of different types or with
different configurations:

pub trait NewVisitor<'a> {
type Visitor: field::Visit + 'a;
fn make(&self, writer: &'a mut dyn fmt::Write, is_empty: bool) -> Self::Visitor;
}

There have been other use-cases where having this abstraction seems
valuable. For example, issue #73 is a proposal for adding utilities for
wrapping a visitor to skip a set of excluded field names. A MakeVisitor
abstraction would allow us to provide much more ergonomic APIs for
composing this kind of visitor behaviour.

Additionally, the current trait in tracing-fmt is tied pretty closely
to `tracing-fmt- specific behaviour. A generalized version of this trait
should be generic over the input type used to build the visitor.

Solution

This PR adds a new MakeVisitor trait in tracing-subscriber.
MakeVisitor generalizes the NewVisitor trait in tracing-fmt to
represent constructing visitors for arbitrary targets. In addition, I've
added some additional traits representing common patterns for visitors,
such as those that format to a fmt::Write instance or write to an IO
stream, or those that produce output once a set of fields have been
visited. I've also added some extension traits & combinators to make
composing visitors easier, and to demonstrate that the trait in this
branch can be used for implementing this kind of wrapper.

I've also rewritten the tracing-fmt crate to use the new
tracing-subscriber::MakeVisitor trait rather than its homegrown
fmt-specifc version.

Closes: #240

Signed-off-by: Eliza Weisman [email protected]

}

#[derive(Debug)]
pub struct VisitDelimited<D, V> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if I am understanding this correctly, this would mean that you'd simply have to instantiate a VisitDelimited for a new visitor, without having to wrap the Visitor construction in a separate type?


fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
self.delimit();
self.inner.record_debug(field, value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are the errors from the delegation to inner's being caught? Am I missing something?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The record_$TYPE functions don't return errors, so if the inner visitor is also fallible, it's expected to capture any errors that might occur. The wrapper's finish method will call finish on the inner visitor, so if the inner visitor also tracks an error, it will be returned if the wrapper hasn't also failed.

@hawkw hawkw changed the title [WIP] first pass on MakeVisitor abstraction subscriber: add an abstraction for building visitors Aug 15, 2019
@hawkw hawkw marked this pull request as ready for review August 16, 2019 21:26
@hawkw hawkw added crate/subscriber Related to the `tracing-subscriber` crate kind/feature New feature or request labels Aug 16, 2019
@hawkw
Copy link
Member Author

hawkw commented Aug 16, 2019

I've spent some time playing around with this, and I think it's about ready for review. Please let me know what you think!

pub trait FormatFields<'writer> {
fn format_fields<R: RecordFields>(
&self,
writer: &'writer mut dyn fmt::Write,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to be generic over the fields but not over the writer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the writer is the MakeVisitor trait's target type, which the trait is generic over. This means that we would have to make the FormatFields trait (rather than the function) generic over a writer, and thus also make the FmtSubscriber itself generic over a writer. This won't work since we will need to pass multiple writer types in (sometimes a String, and sometimes an io stream), so we need to use a trait object to erase the concrete type. On the other hand, the RecordFields is only a parameter of the method, so implementations of FormatFields can't implement the trait only for specific implementors of RecordFields. I thought it was better to avoid the overhead of a trait object or that since it's not strictly required.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could make the method generic over both a writer and a RecordFields, but the writer will eventually become a trait object if we use it to create a visitor. That might, actually, be better, as implementations that don't use the blanket impl for MakeVisitor could avoid a trait object...

@hawkw hawkw mentioned this pull request Aug 25, 2019
6 tasks
@hawkw hawkw requested a review from a team August 30, 2019 20:53
@hawkw hawkw added this to the tracing-subscriber 0.1 milestone Sep 2, 2019
hawkw added a commit that referenced this pull request Sep 2, 2019
In the near future, the `tracing-subscriber` crate should be ready for a
stable 0.1 release. Before we can do that, we should do some pre-release
polish, such as denying warnings, updating idioms, and adding missing
documentation.

This branch performs most of the necessary polish for
`tracing-subscriber`. Please note the following:

* The next release will not be version 0.1, as PR #241 would be a 
  breaking change, and I'd like to land that first. We'll probably do
  an alpha.4 first.
* I didn't add `deny(warnings)`, as that will also deny deprecation 
  warnings, and I've had bad experiences with deprecations breaking
  CI in the past. Instead, I've denied most other warnings explicitly.
  The downside to this is a *very* long list of deny attributes.
* The docs added in this branch are pretty rough. It would be good to
  expand them some more in the future, hopefully before 0.1.

Signed-off-by: Eliza Weisman <[email protected]>
@hawkw hawkw removed this from the tracing-subscriber 0.1 milestone Sep 4, 2019
fn delimit(&mut self)
where
V: VisitFmt,
D: AsRef<str>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really a review comment, but more a Rust question, but why not lift these bounds to the impl block instead of specifying it on the method?

Copy link
Contributor

@bIgBV bIgBV left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! I left some general nits. Mostly around things that I felt weren't really clear.

}

/// A visitor wrapper that inserts a delimiter after the wrapped visitor formats
/// a field value.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe expand on this comment saying something along the lines of "The delimiter is inserted after it is visited the first time"?

Right now you will have to separately see the impl of the delimit method and where it's being called from to understand how it fits together.

where
V: Visit,
{
#[inline]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again more a rust question than anything, but won't llvm automatically inline such small methods when at opt-level=2 ?

}
/// Extension trait implemented by visitors to indicate that they write to a
/// `fmt::Write` instance, and allow access to that writer.
pub trait VisitFmt: VisitOutput<fmt::Result> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was initially confused by the VisitFmt name as the trait only has the writer method on it. But goddamn do I love how this abstraction separates the two writers and still brings it all together. It's awesome!

/// .finish();
/// # drop(subscriber)
/// ```
pub fn fmt_fields<N2>(self, fmt_fields: N2) -> Builder<N2, E, F, W>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know naming is a hard problem but maybe have a different variable for the generic? N and N2 are a little confusing as the NewVisitor type has been removed right?

}
/// Extension trait implemented by visitors to indicate that they write to a
/// `fmt::Write` instance, and allow access to that writer.
pub trait VisitFmt: VisitOutput<fmt::Result> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I'm a bit on the fence about that I'd love to get some opinions on: We could make the VisitFmt traits trait aliases for VisitOutput<fmt::Result> + AsMut<dyn fmt::Write>; that way, any type implementing AsMut could also get a blanket implementation for free. On the other hand, it means introducing a trait alias.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me that the trait alias for VisitOutput<fmt::Result> + AsMut<dyn fmt::Write> could be added in non-backwards incompatible change, so I wouldn't gate on this.

@davidbarsky
Copy link
Member

This looks good to me; the build is failing on missing docs + missing debug impls: https://dev.azure.com/tracing/tracing/_build/results?buildId=721&view=logs&j=84c7964f-8fe4-5ae5-6e5b-8951524b2943&t=a6204e26-b351-5262-402a-ed02abe9e163&l=443.

I think with that + resolved conflicts, this could merged?

@hawkw hawkw force-pushed the eliza/new-visitor branch 2 times, most recently from ed365c3 to 3d2ff32 Compare October 7, 2019 16:44
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
hawkw and others added 8 commits October 8, 2019 12:01
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
@hawkw
Copy link
Member Author

hawkw commented Oct 8, 2019

@davidbarsky @LucioFranco this is just about ready to merge, if either of you wanted to take a final look first.

@davidbarsky
Copy link
Member

@hawkw no issues with merging this as is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
crate/subscriber Related to the `tracing-subscriber` crate kind/feature New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

subscriber: generalized NewVisitor abstraction
4 participants