Skip to content

Commit

Permalink
feat: Add a Http Service integration with trace propagation (NATIVE-3…
Browse files Browse the repository at this point in the history
…14) (#397)

This creates a new sentry-tower feature that does trace propagation for Http Services.
  • Loading branch information
Swatinem authored Jan 11, 2022
1 parent df694a4 commit 6721801
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 3 deletions.
5 changes: 5 additions & 0 deletions sentry-tower/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ Sentry integration for tower-based crates.
"""
edition = "2018"

[features]
http = ["http_", "pin-project"]

[dependencies]
tower-layer = "0.3"
tower-service = "0.3"
http_ = { package = "http", version = "0.2.6", optional = true }
pin-project = { version = "1.0.10", optional = true }
sentry-core = { version = "0.23.0", path = "../sentry-core", default-features = false, features = ["client"] }

[dev-dependencies]
Expand Down
149 changes: 149 additions & 0 deletions sentry-tower/src/http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

use http_::Request;
use tower_layer::Layer;
use tower_service::Service;

/// Tower Layer that logs Http Request Headers.
///
/// The Service created by this Layer can also optionally start a new
/// performance monitoring transaction for each incoming request,
/// continuing the trace based on incoming distributed tracing headers.
#[derive(Clone, Default)]
pub struct SentryHttpLayer {
start_transaction: bool,
}

impl SentryHttpLayer {
/// Creates a new Layer that only logs Request Headers.
pub fn new() -> Self {
Self::default()
}

/// Creates a new Layer which starts a new performance monitoring transaction
/// for each incoming request.
pub fn with_transaction() -> Self {
Self {
start_transaction: true,
}
}
}

/// Tower Service that logs Http Request Headers.
///
/// The Service can also optionally start a new performance monitoring transaction
/// for each incoming request, continuing the trace based on incoming
/// distributed tracing headers.
#[derive(Clone)]
pub struct SentryHttpService<S> {
service: S,
start_transaction: bool,
}

impl<S> Layer<S> for SentryHttpLayer {
type Service = SentryHttpService<S>;

fn layer(&self, service: S) -> Self::Service {
Self::Service {
service,
start_transaction: self.start_transaction,
}
}
}

/// The Future returned from [`SentryHttpService`].
#[pin_project::pin_project]
pub struct SentryHttpFuture<F> {
transaction: Option<(
sentry_core::TransactionOrSpan,
Option<sentry_core::TransactionOrSpan>,
)>,
#[pin]
future: F,
}

impl<F> Future for SentryHttpFuture<F>
where
F: Future,
{
type Output = F::Output;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let slf = self.project();
match slf.future.poll(cx) {
Poll::Ready(res) => {
if let Some((transaction, parent_span)) = slf.transaction.take() {
transaction.finish();
sentry_core::configure_scope(|scope| scope.set_span(parent_span));
}
Poll::Ready(res)
}
Poll::Pending => Poll::Pending,
}
}
}

impl<S, Body> Service<Request<Body>> for SentryHttpService<S>
where
S: Service<Request<Body>>,
{
type Response = S::Response;
type Error = S::Error;
type Future = SentryHttpFuture<S::Future>;

fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}

fn call(&mut self, request: Request<Body>) -> Self::Future {
let transaction = sentry_core::configure_scope(|scope| {
let sentry_req = sentry_core::protocol::Request {
method: Some(request.method().to_string()),
url: request.uri().to_string().parse().ok(),
headers: request
.headers()
.into_iter()
.map(|(header, value)| {
(
header.to_string(),
value.to_str().unwrap_or_default().into(),
)
})
.collect(),
..Default::default()
};
scope.add_event_processor(move |mut event| {
if event.request.is_none() {
event.request = Some(sentry_req.clone());
}
Some(event)
});

if self.start_transaction {
let headers = request.headers().into_iter().flat_map(|(header, value)| {
value.to_str().ok().map(|value| (header.as_str(), value))
});
let tx_name = format!("{} {}", request.method(), request.uri().path());
let tx_ctx = sentry_core::TransactionContext::continue_from_headers(
&tx_name,
"http.server",
headers,
);
let transaction: sentry_core::TransactionOrSpan =
sentry_core::start_transaction(tx_ctx).into();
let parent_span = scope.get_span();
scope.set_span(Some(transaction.clone()));
Some((transaction, parent_span))
} else {
None
}
});

SentryHttpFuture {
transaction,
future: self.service.call(request),
}
}
}
10 changes: 8 additions & 2 deletions sentry-tower/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,19 @@
#![doc(html_logo_url = "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png")]
#![warn(missing_docs)]

use sentry_core::{Hub, SentryFuture, SentryFutureExt};
use std::marker::PhantomData;
use std::sync::Arc;
use std::task::{Context, Poll};

use sentry_core::{Hub, SentryFuture, SentryFutureExt};
use tower_layer::Layer;
use tower_service::Service;

#[cfg(feature = "http")]
mod http;
#[cfg(feature = "http")]
pub use http::*;

/// Provides a hub for each request
pub trait HubProvider<H, Request>
where
Expand Down Expand Up @@ -140,7 +146,7 @@ pub struct NewFromTopProvider;

impl<Request> HubProvider<Arc<Hub>, Request> for NewFromTopProvider {
fn hub(&self, _request: &Request) -> Arc<Hub> {
// The Clippy lint here is a falste positive, the suggestion to write
// The Clippy lint here is a false positive, the suggestion to write
// `Hub::with(Hub::new_from_top)` does not compiles:
// 143 | Hub::with(Hub::new_from_top).into()
// | ^^^^^^^^^ implementation of `std::ops::FnOnce` is not general enough
Expand Down
2 changes: 1 addition & 1 deletion sentry-types/src/protocol/v7.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub mod debugid {
/// An arbitrary (JSON) value.
pub use self::value::Value;

/// The internally useed map type.
/// The internally used map type.
pub use self::map::Map;

/// A wrapper type for collections with attached meta data.
Expand Down

0 comments on commit 6721801

Please sign in to comment.