Skip to content

Commit 6a479ff

Browse files
committed
feat: Add a Http Service integration with trace propagation (NATIVE-314)
This creates a new sentry-tower feature that does trace propagation for Http Services.
1 parent 281f3ff commit 6a479ff

File tree

3 files changed

+114
-2
lines changed

3 files changed

+114
-2
lines changed

sentry-tower/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ Sentry integration for tower-based crates.
1111
"""
1212
edition = "2018"
1313

14+
[features]
15+
http = []
16+
1417
[dependencies]
1518
tower-layer = "0.3"
1619
tower-service = "0.3"
20+
http = { version = "0.2.6", optional = true }
1721
sentry-core = { version = "0.23.0", path = "../sentry-core", default-features = false, features = ["client"] }
1822

1923
[dev-dependencies]

sentry-tower/src/http.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use std::collections::BTreeMap;
2+
use std::future::Future;
3+
use std::pin::Pin;
4+
use std::task::{Context, Poll};
5+
6+
use tower_layer::Layer;
7+
use tower_service::Service;
8+
9+
#[derive(Clone)]
10+
pub struct SentryHttpLayer;
11+
12+
#[derive(Clone)]
13+
pub struct SentryHttpService<S> {
14+
service: S,
15+
}
16+
17+
impl<S> Layer<S> for SentryHttpLayer {
18+
type Service = SentryHttpService<S>;
19+
20+
fn layer(&self, service: S) -> Self::Service {
21+
Self::Service { service }
22+
}
23+
}
24+
25+
pub struct SentryHttpFuture<F> {
26+
transaction: Option<sentry_core::Transaction>,
27+
future: F,
28+
}
29+
30+
impl Future for SentryHttpFuture<F>
31+
where
32+
F: Future,
33+
{
34+
type Output = F::Output;
35+
36+
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
37+
// https://doc.rust-lang.org/std/pin/index.html#pinning-is-structural-for-field
38+
let future = unsafe { self.map_unchecked_mut(|s| &mut s.future) };
39+
match future.poll(cx) {
40+
Poll::Ready(res) => {
41+
if let Some(transaction) = self.transaction.take() {
42+
transaction.finish();
43+
}
44+
Poll::Ready(res)
45+
}
46+
Poll::Pending => Poll::Pending,
47+
}
48+
}
49+
}
50+
51+
impl<S, Body> Service<Request<Body>> for SentryHttpService<S>
52+
where
53+
S: Service<Request<Body>>,
54+
{
55+
type Response = S::Response;
56+
type Error = S::Error;
57+
type Future = SentryHttpFuture<S::Future>;
58+
59+
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
60+
self.service.poll_ready(cx)
61+
}
62+
63+
fn call(&mut self, request: Request<Body>) -> Self::Future {
64+
let transaction = sentry::configure_scope(|scope| {
65+
// https://develop.sentry.dev/sdk/event-payloads/request/
66+
let mut ctx = BTreeMap::new();
67+
68+
// TODO: method, url, query_string
69+
70+
// headers
71+
let mut headers = BTreeMap::new();
72+
for (header, value) in request.headers() {
73+
headers.insert(
74+
header.to_string(),
75+
value.to_str().unwrap_or("<Opaque header value>").into(),
76+
);
77+
}
78+
ctx.insert("headers".into(), headers);
79+
80+
scope.set_context("request", sentry_core::protocol::Context::Other(ctx));
81+
82+
// TODO: maybe make transaction creation optional?
83+
let transaction = if true {
84+
let tx_ctx = sentry_core::TransactionContext::continue_from_headers(
85+
// TODO: whats the name here?
86+
"",
87+
"http",
88+
request.headers(),
89+
);
90+
Some(sentry_core::start_transaction(tx_ctx))
91+
} else {
92+
None
93+
};
94+
95+
scope.set_span(transaction.clone());
96+
transaction
97+
});
98+
99+
SentryHttpFuture {
100+
transaction,
101+
future: self.service.call(request),
102+
}
103+
}
104+
}

sentry-tower/src/lib.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,17 @@
102102
#![doc(html_logo_url = "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png")]
103103
#![warn(missing_docs)]
104104

105-
use sentry_core::{Hub, SentryFuture, SentryFutureExt};
106105
use std::marker::PhantomData;
107106
use std::sync::Arc;
108107
use std::task::{Context, Poll};
108+
109+
use sentry_core::{Hub, SentryFuture, SentryFutureExt};
109110
use tower_layer::Layer;
110111
use tower_service::Service;
111112

113+
#[cfg(feature = "http")]
114+
mod http;
115+
112116
/// Provides a hub for each request
113117
pub trait HubProvider<H, Request>
114118
where
@@ -140,7 +144,7 @@ pub struct NewFromTopProvider;
140144

141145
impl<Request> HubProvider<Arc<Hub>, Request> for NewFromTopProvider {
142146
fn hub(&self, _request: &Request) -> Arc<Hub> {
143-
// The Clippy lint here is a falste positive, the suggestion to write
147+
// The Clippy lint here is a false positive, the suggestion to write
144148
// `Hub::with(Hub::new_from_top)` does not compiles:
145149
// 143 | Hub::with(Hub::new_from_top).into()
146150
// | ^^^^^^^^^ implementation of `std::ops::FnOnce` is not general enough

0 commit comments

Comments
 (0)