From c9e9130cdf18469c77783ef98e708a8cce62c0ff Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 18 Apr 2023 22:03:17 +0200 Subject: [PATCH] Sentry transport based on hyper to get rid of reqwest --- Cargo.lock | 93 +-------- crates/cli/Cargo.toml | 3 +- crates/cli/src/main.rs | 8 + crates/cli/src/sentry_transport/mod.rs | 131 +++++++++++++ crates/cli/src/sentry_transport/ratelimit.rs | 185 ++++++++++++++++++ .../cli/src/sentry_transport/tokio_thread.rs | 118 +++++++++++ 6 files changed, 447 insertions(+), 91 deletions(-) create mode 100644 crates/cli/src/sentry_transport/mod.rs create mode 100644 crates/cli/src/sentry_transport/ratelimit.rs create mode 100644 crates/cli/src/sentry_transport/tokio_thread.rs diff --git a/Cargo.lock b/Cargo.lock index 474dc1be..170551e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2681,19 +2681,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" -dependencies = [ - "http", - "hyper", - "rustls 0.20.8", - "tokio", - "tokio-rustls 0.23.4", -] - [[package]] name = "hyper-rustls" version = "0.24.0" @@ -2871,12 +2858,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ipnet" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" - [[package]] name = "iri-string" version = "0.7.0" @@ -3157,6 +3138,7 @@ dependencies = [ "camino", "clap", "dotenv", + "httpdate", "hyper", "indoc", "itertools", @@ -3357,7 +3339,7 @@ dependencies = [ "http", "http-body", "hyper", - "hyper-rustls 0.24.0", + "hyper-rustls", "mas-tower", "once_cell", "opentelemetry", @@ -3501,7 +3483,7 @@ dependencies = [ "http", "http-body", "hyper", - "hyper-rustls 0.24.0", + "hyper-rustls", "mas-http", "mas-iana", "mas-jose", @@ -5022,45 +5004,6 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" -[[package]] -name = "reqwest" -version = "0.11.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" -dependencies = [ - "base64 0.21.0", - "bytes 1.4.0", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-rustls 0.23.2", - "ipnet", - "js-sys", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls 0.20.8", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-rustls 0.23.4", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots 0.22.6", - "winreg", -] - [[package]] name = "retain_mut" version = "0.1.9" @@ -5368,17 +5311,11 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5ce6d3512e2617c209ec1e86b0ca2fea06454cd34653c91092bf0f3ec41f8e3" dependencies = [ - "httpdate", - "reqwest", - "rustls 0.20.8", "sentry-backtrace", "sentry-contexts", "sentry-core", "sentry-panic", "sentry-tower", - "tokio", - "ureq", - "webpki-roots 0.22.6", ] [[package]] @@ -6673,21 +6610,6 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" -[[package]] -name = "ureq" -version = "2.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d" -dependencies = [ - "base64 0.13.1", - "log", - "once_cell", - "rustls 0.20.8", - "url", - "webpki", - "webpki-roots 0.22.6", -] - [[package]] name = "url" version = "2.3.1" @@ -7382,15 +7304,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - [[package]] name = "wiremock" version = "0.5.18" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 6278993c..3f7c2f12 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -40,7 +40,7 @@ opentelemetry-zipkin = { version = "0.16.0", features = ["opentelemetry-http"], opentelemetry-http = { version = "0.7.0", features = ["tokio", "hyper"], optional = true } opentelemetry-prometheus = { version = "0.11.0", optional = true } prometheus = { version = "0.13.3", optional = true } -sentry = { version = "0.30.0", default-features = false, features = ["backtrace", "contexts", "panic", "reqwest", "rustls", "tower"] } +sentry = { version = "0.30.0", default-features = false, features = ["backtrace", "contexts", "panic", "tower"] } sentry-tracing = "0.30.0" sentry-tower = { version = "0.30.0", features = ["http"] } @@ -59,6 +59,7 @@ mas-tasks = { path = "../tasks" } mas-templates = { path = "../templates" } mas-tower = { path = "../tower" } oauth2-types = { path = "../oauth2-types" } +httpdate = "1.0.2" [dev-dependencies] indoc = "2.0.1" diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index e05411df..b11893de 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -17,6 +17,8 @@ #![warn(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] +use std::sync::Arc; + use anyhow::Context; use clap::Parser; use mas_config::TelemetryConfig; @@ -25,7 +27,10 @@ use tracing_subscriber::{ filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, Registry, }; +use crate::sentry_transport::HyperTransportFactory; + mod commands; +mod sentry_transport; mod server; mod telemetry; mod util; @@ -70,6 +75,9 @@ async fn try_main() -> anyhow::Result<()> { let sentry = sentry::init(( telemetry_config.sentry.dsn.as_deref(), sentry::ClientOptions { + transport: Some(Arc::new(HyperTransportFactory::new( + mas_http::make_untraced_client().await?, + ))), traces_sample_rate: 1.0, auto_session_tracking: true, session_mode: sentry::SessionMode::Request, diff --git a/crates/cli/src/sentry_transport/mod.rs b/crates/cli/src/sentry_transport/mod.rs new file mode 100644 index 00000000..2fa2816d --- /dev/null +++ b/crates/cli/src/sentry_transport/mod.rs @@ -0,0 +1,131 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Implements a transport for Sentry based on Hyper. +//! +//! This avoids the dependency on `reqwest`, which helps avoiding having two +//! HTTP and TLS stacks in the binary. +//! +//! The [`ratelimit`] and [`tokio_thread`] modules are directly copied from the +//! Sentry codebase. + +use std::{sync::Arc, time::Duration}; + +use hyper::{client::connect::Connect, header::RETRY_AFTER, Client, StatusCode}; +use sentry::{sentry_debug, ClientOptions, Transport, TransportFactory}; + +use self::tokio_thread::TransportThread; + +mod ratelimit; +mod tokio_thread; + +pub struct HyperTransport { + thread: TransportThread, +} + +pub struct HyperTransportFactory { + client: Client, +} + +impl HyperTransportFactory { + pub fn new(client: Client) -> Self { + Self { client } + } +} + +impl TransportFactory for HyperTransportFactory +where + C: Connect + Clone + Send + Sync + 'static, +{ + fn create_transport(&self, options: &ClientOptions) -> Arc { + Arc::new(HyperTransport::new(options, self.client.clone())) + } +} + +impl HyperTransport { + pub fn new(options: &ClientOptions, client: Client) -> Self + where + C: Connect + Clone + Send + Sync + 'static, + { + let dsn = options.dsn.as_ref().unwrap(); + let user_agent = options.user_agent.clone(); + let auth = dsn.to_auth(Some(&user_agent)).to_string(); + let url = dsn.envelope_api_url().to_string(); + + let thread = TransportThread::new(move |envelope, mut rl| { + let mut body = Vec::new(); + envelope.to_writer(&mut body).unwrap(); + + let request = hyper::Request::post(&url) + .header("X-Sentry-Auth", &auth) + .body(hyper::Body::from(body)) + .unwrap(); + + let fut = client.request(request); + + async move { + match fut.await { + Ok(response) => { + if let Some(sentry_header) = response + .headers() + .get("x-sentry-rate-limits") + .and_then(|x| x.to_str().ok()) + { + rl.update_from_sentry_header(sentry_header); + } else if let Some(retry_after) = response + .headers() + .get(RETRY_AFTER) + .and_then(|x| x.to_str().ok()) + { + rl.update_from_retry_after(retry_after); + } else if response.status() == StatusCode::TOO_MANY_REQUESTS { + rl.update_from_429(); + } + + match hyper::body::to_bytes(response.into_body()).await { + Err(err) => { + sentry_debug!("Failed to read sentry response: {}", err); + } + Ok(bytes) => { + let text = String::from_utf8_lossy(&bytes); + sentry_debug!("Get response: `{}`", text); + } + } + } + Err(err) => { + sentry_debug!("Failed to send envelope: {}", err); + } + } + + rl + } + }); + + Self { thread } + } +} + +impl Transport for HyperTransport { + fn send_envelope(&self, envelope: sentry::Envelope) { + self.thread.send(envelope); + } + + fn flush(&self, timeout: Duration) -> bool { + self.thread.flush(timeout) + } + + fn shutdown(&self, timeout: Duration) -> bool { + self.flush(timeout) + } +} diff --git a/crates/cli/src/sentry_transport/ratelimit.rs b/crates/cli/src/sentry_transport/ratelimit.rs new file mode 100644 index 00000000..d0543365 --- /dev/null +++ b/crates/cli/src/sentry_transport/ratelimit.rs @@ -0,0 +1,185 @@ +// Taken from sentry/transports/ratelimit.rs + +use std::time::{Duration, SystemTime}; + +use httpdate::parse_http_date; +use sentry::{protocol::EnvelopeItem, Envelope}; + +/// A Utility that helps with rate limiting sentry requests. +#[derive(Debug, Default)] +pub struct RateLimiter { + global: Option, + error: Option, + session: Option, + transaction: Option, + attachment: Option, + profile: Option, +} + +impl RateLimiter { + /// Create a new RateLimiter. + pub fn new() -> Self { + Self::default() + } + + /// Updates the RateLimiter with information from a `Retry-After` header. + pub fn update_from_retry_after(&mut self, header: &str) { + let new_time = if let Ok(value) = header.parse::() { + SystemTime::now() + Duration::from_secs(value.ceil() as u64) + } else if let Ok(value) = parse_http_date(header) { + value + } else { + SystemTime::now() + Duration::from_secs(60) + }; + + self.global = Some(new_time); + } + + /// Updates the RateLimiter with information from a `X-Sentry-Rate-Limits` + /// header. + pub fn update_from_sentry_header(&mut self, header: &str) { + // = (,)+ + // =