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

Add a tracing filter which enables contextual, structured logging #408

Closed
wants to merge 3 commits into from

Conversation

barskern
Copy link

Tracing enables us to keep track of some contextual state throughout the lifetime of a request. This means that any logging done when processing a request will automatically also log the context. This is especially useful when processing requests in parallel because it means that one can separate intertwined logs.

The author of tracing made a good and informative video about the subject, and showing off some of the benefits.

This is based upon @emschwartz issue and his previous work.

This is mainly a rough first implementation to see if you would like this feature. If you are interested in this feature I was planning to go through all uses of log in the crate to replace with appropriate span's and event's. What do you think @seanmonstar ?

Closes #288

@seanmonstar
Copy link
Owner

I'm certainly interested in this, I think tracing can add a lot of value. Part of the reason I hadn't (yet) was I figured that applications that wanted tracing could add it themselves. Do you find that's not the case? What would you say is key in this PR solving that problem?

@barskern
Copy link
Author

barskern commented Jan 24, 2020 via email

@barskern
Copy link
Author

barskern commented Jan 24, 2020

However in retrospect I think one could also get similar functionality by appending the span to the future returned by Server::bind. I will try this out. A limitation of this is that one cannot add different spans to different filters, which the implementation using Filter::with allows.

After some testing I haven't figured out a way to do this without access to Wrap. The reason is that we need to create a span for each request, which means that we need to have some way of modifying the resulting future of a Filter (FilterBase to be more exact). As this trait is private to warp, it is currently not possible to do that (if someone manages to prove me wrong here, please do!).

In conclusion I think the best way forward would be to consider making Wrap unsealed if you want this functionality in a separate crate. However personally I would recommend you to change from log to tracing for the entire library as it brings a lot of great benefits. Especially the fact that its structured logs means that one can easily set up automated tasks to inspect logs, or easily search through logs.

@jxs
Copy link
Collaborator

jxs commented Jan 24, 2020

hi, have you tried the new Filter to Service conversion? you can now wrap a warp filter:

use std::convert::Infallible;
use warp::Filter;
use hyper::{Body, Request};
use tower_service::Service;

#[tokio::main]
async fn main() -> Result<(), hyper::error::Error> {
    let route = warp::any().map(|| "Hello From Warp!");
    let mut warp_svc = warp::service(route);
    let make_svc = hyper::service::make_service_fn(move |_| async move {
        let svc = hyper::service::service_fn(move |req: Request<Body>| async move {
            println!("before request");
            let resp = warp_svc.call(req).await;
            println!("after request");
            resp
        });
        Ok::<_, Infallible>(svc)
    });

    hyper::Server::bind(&([127, 0, 0, 1], 3030).into())
        .serve(make_svc)
        .await?;

    Ok(())
}

@barskern
Copy link
Author

barskern commented Jan 24, 2020

hi, have you tried the new Filter to Service conversion? you can now wrap a warp filter:

Oh no I haven't tried that. That looks very promising! Thank you for the tip. I will try it out a bit later today.

@LucioFranco
Copy link
Contributor

There is also an in progress tracing-tower crate https://github.com/tokio-rs/tracing/tree/master/tracing-tower

@barskern
Copy link
Author

barskern commented Jan 24, 2020

@jxs Your suggestion works perfectly (despite being a bit more verbose). Hence it is probably smarter to use this solution rather than adding dependencies and modifying warp.

If you are still interested in exploring using tracing instead of log in warp, I will leave this PR open for now. Feel free to close it or merge it when you find it applicable.

use hyper::{Body, Request};
use std::convert::Infallible;
use tower_service::Service;
use tracing_futures::Instrument;
use warp::Filter;

#[tokio::main]
async fn main() -> Result<(), hyper::error::Error> {
    // Setup a log subscriber (responsible to print to output)
    let subscriber = tracing_subscriber::fmt::Subscriber::builder()
        .with_env_filter("tracing_service=trace")
        .without_time()
        .finish();

    // Set the previously created subscriber as the global subscriber
    tracing::subscriber::set_global_default(subscriber).unwrap();
    // Redirect normal log messages to the tracing subscriber
    tracing_log::LogTracer::init().unwrap();


    let route = warp::any().map(|| "Hello From Warp!");
    let mut warp_svc = warp::service(route);

    let make_svc = hyper::service::make_service_fn(move |_| {
        async move {
            let svc = hyper::service::service_fn(move |req: Request<Body>| {
                // FIXME instead of printing the entire request, we probably want to filter out
                // some fields?
                let span = tracing::info_span!("request", req = ?req);
                async move {
                    tracing::info!("processing request");
                    let resp = warp_svc.call(req).await;
                    tracing::info!(status = ?resp.as_ref().unwrap().status().as_u16(), "processed request");

                    resp
                }
                .instrument(span)
            });
            Ok::<_, Infallible>(svc)
        }
    });

    hyper::Server::bind(&([127, 0, 0, 1], 3030).into())
        .serve(make_svc)
        .await?;

    Ok(())
}

@LucioFranco I would probably rather use that if it was published, I will keep my eye on it. Thank you for the tip!

EDIT: Modified the example to fix the usage of span. See comment from @little-dude. Thank you!

@barskern
Copy link
Author

As an aside, would you be interested in having the code above as an example?

@barskern
Copy link
Author

Using the service function solution above I met some issues when the Filter is only Clone (not Copy). I made a post on the subreddit because it seems to be a general borrow checker problem, but linking it here aswell as it might be relevant when people stumble across this PR.

https://www.reddit.com/r/rust/comments/ettmfs/trouble_moving_clonable_hyperservice_into_async/

@fhsgoncalves
Copy link

@barskern thank you!
This is exactly what I was needing!

Also, you can avoid this issue cloning the service on every move, like that:
#16 (comment)

@little-dude
Copy link

@jxs Your suggestion works perfectly (despite being a bit more verbose). Hence it is probably smarter to use this solution rather than adding dependencies and modifying warp.

If you are still interested in exploring using tracing instead of log in warp, I will leave this PR open for now. Feel free to close it or merge it when you find it applicable.

use hyper::{Body, Request};
use std::convert::Infallible;
use tower_service::Service;
use tracing_futures::Instrument;
use warp::Filter;

#[tokio::main]
async fn main() -> Result<(), hyper::error::Error> {
    // Setup a log subscriber (responsible to print to output)
    let subscriber = tracing_subscriber::fmt::Subscriber::builder()
        .with_env_filter("tracing_service=trace")
        .without_time()
        .finish();

    // Set the previously created subscriber as the global subscriber
    tracing::subscriber::set_global_default(subscriber).unwrap();
    // Redirect normal log messages to the tracing subscriber
    tracing_log::LogTracer::init().unwrap();


    let route = warp::any().map(|| "Hello From Warp!");
    let mut warp_svc = warp::service(route);

    let make_svc = hyper::service::make_service_fn(move |_| {
        async move {
            let svc = hyper::service::service_fn(move |req: Request<Body>| {
                async move {
                    // FIXME instead of printing the entire request, we probably want to filter out
                    // some fields?
                    let span = tracing::info_span!("request", req = ?req);
                    let _guard = span.enter();

                    tracing::info!("processing request");
                    let resp = warp_svc.call(req).in_current_span().await;
                    tracing::info!(status = ?resp.as_ref().unwrap().status().as_u16(), "processed request");

                    resp
                }
            });
            Ok::<_, Infallible>(svc)
        }
    });

    hyper::Server::bind(&([127, 0, 0, 1], 3030).into())
        .serve(make_svc)
        .await?;

    Ok(())
}

@LucioFranco I would probably rather use that if it was published, I will keep my eye on it. Thank you for the tip!

Thanks for sharing this example. I have a question about this portion of the snippet:

let span = tracing::info_span!("request", req = ?req);
let _guard = span.enter();

In the tracing repo there is a warning against using span.enter() in async code. Is it actually ok in this specific example?

@barskern
Copy link
Author

barskern commented Mar 13, 2020 via email

@hawkw
Copy link
Contributor

hawkw commented Mar 31, 2020

Welp, I just went and wrote my own PR to add a tracing filter PR, and only noticed this PR after I finished. Whoops!

Looks like @barskern and I have implemented more or less the same thing. My code is here if you want to compare notes.

The one main difference in my PR is that I also added a warp::trace::context filter. This is just a convenience API for using tracing spans to name different filter stacks, like

    let hello = warp::path("hello")
        .and(warp::get())
        .map(|| {
            tracing::info!("saying hello...");
            "Hello, World!"
        })
        .with(warp::trace::context("hello"));
    
    let goodbye = warp::path("goodbye")
        .and(warp::get())
        .map(|| {
            tracing::info!("saying goodbye...");
            "So long and thanks for all the fish!"
        })
        .with(warp::trace::context("goodbye"));

Using that, we can get nice nested traces like this:
warp_traces


As for the question of whether or not Warp should provide tracing support, I have some thoughts. It's definitely possible to get traces from Warp filters using the Tower compatibility, as folks have discussed above. However, this seems pretty ungainly, and something that's always seemed appealing to me about Warp is that it has a remarkably nice, fluent API. Providing Tracing Filters instead of going through the Tower compatibility layer lets users write more concise, nicer looking, "warp-ier" code. I also appreciate Warp's relative minimalism, but the Tracing filters could always be a default-off feature.

@barskern
Copy link
Author

barskern commented Apr 3, 2020 via email

@little-dude
Copy link

With this implementation would the request's span be available in .recover() ?

Right now this is my problem: I create a span for the request in .and_then(), but I don't have access to it in .recover(), so in the logs I don't have all the information I want when a request fails.

Here is a snippet from my code to illustrate the problem:

    let download_global_weights = warp::get()
        .and(warp::path::param::<ClientId>())
        .and(warp::path::param::<Token>())
        .and(handle.clone())
        // handler
        .and_then(move |id, token, handle| {
            // in this span, I have the `client_id`
            let span =
                trace_span!(parent: parent_span.clone(), "api_download_request", client_id = %id);
            handle_download_request(id, token, handle).instrument(span)
        })
        // here I won't have the `client_id` in the logs
        .recover(handle_download_rejection)
        .recover(handle_rejection)

@barskern
Copy link
Author

barskern commented Apr 9, 2020

With this implementation would the request's span be available in .recover() ?

Right now this is my problem: I create a span for the request in .and_then(), but I don't have access to it in .recover(), so in the logs I don't have all the information I want when a request fails.

Here is a snippet from my code to illustrate the problem:

    let download_global_weights = warp::get()
        .and(warp::path::param::<ClientId>())
        .and(warp::path::param::<Token>())
        .and(handle.clone())
        // handler
        .and_then(move |id, token, handle| {
            // in this span, I have the `client_id`
            let span =
                trace_span!(parent: parent_span.clone(), "api_download_request", client_id = %id);
            handle_download_request(id, token, handle).instrument(span)
        })
        // here I won't have the `client_id` in the logs
        .recover(handle_download_rejection)
        .recover(handle_rejection)

@little-dude I haven't tested, but I am quite certain that this implementation would make the span avaliable in the recover method, as long as the with call with the span is the last call in the chain.

@hlb8122
Copy link

hlb8122 commented Apr 20, 2020

Super excited for this!

@fahrradflucht fahrradflucht mentioned this pull request Apr 25, 2020
@ufoscout
Copy link

Is there any progress on this issue? Or is there something we can do to help to fix it?

As tracing is fundamental in my company, this is becoming a blocking issue for us. I know that there is a workaround using the tower::service, but we are mostly interested in the solution provided in #408 (comment)

We tried to reimplement it by ourselves but that implementation depends on private traits (BTW is there a reason not to make those traits/struct public?).

@hgzimmerman hgzimmerman mentioned this pull request Jul 8, 2020
@hgzimmerman
Copy link
Contributor

I would really like tracing functionality added to Warp, and I don't think its practical to implement it outside of the library due to warp::filter::wrap::Wrap and warp::route::Route being private.
I personally like the implementation detailed in comment #408 (comment).
@hawkw would you be willing to open a PR with the patch listed in your comment and @jxs (or another collaborator) would you be willing to assist incorporating it into the project?

hawkw added a commit to hawkw/warp that referenced this pull request Jul 16, 2020
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
@seanmonstar
Copy link
Owner

Thanks for exploring this, we've merged tracing support in #656.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add tracing support
10 participants