You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-06 06:02:40 +03:00
Sentry transport based on hyper to get rid of reqwest
This commit is contained in:
93
Cargo.lock
generated
93
Cargo.lock
generated
@@ -2681,19 +2681,6 @@ dependencies = [
|
|||||||
"want",
|
"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]]
|
[[package]]
|
||||||
name = "hyper-rustls"
|
name = "hyper-rustls"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
@@ -2871,12 +2858,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ipnet"
|
|
||||||
version = "2.7.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iri-string"
|
name = "iri-string"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -3157,6 +3138,7 @@ dependencies = [
|
|||||||
"camino",
|
"camino",
|
||||||
"clap",
|
"clap",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
|
"httpdate",
|
||||||
"hyper",
|
"hyper",
|
||||||
"indoc",
|
"indoc",
|
||||||
"itertools",
|
"itertools",
|
||||||
@@ -3357,7 +3339,7 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-rustls 0.24.0",
|
"hyper-rustls",
|
||||||
"mas-tower",
|
"mas-tower",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
@@ -3501,7 +3483,7 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-rustls 0.24.0",
|
"hyper-rustls",
|
||||||
"mas-http",
|
"mas-http",
|
||||||
"mas-iana",
|
"mas-iana",
|
||||||
"mas-jose",
|
"mas-jose",
|
||||||
@@ -5022,45 +5004,6 @@ version = "0.6.29"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
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]]
|
[[package]]
|
||||||
name = "retain_mut"
|
name = "retain_mut"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -5368,17 +5311,11 @@ version = "0.30.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5ce6d3512e2617c209ec1e86b0ca2fea06454cd34653c91092bf0f3ec41f8e3"
|
checksum = "b5ce6d3512e2617c209ec1e86b0ca2fea06454cd34653c91092bf0f3ec41f8e3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"httpdate",
|
|
||||||
"reqwest",
|
|
||||||
"rustls 0.20.8",
|
|
||||||
"sentry-backtrace",
|
"sentry-backtrace",
|
||||||
"sentry-contexts",
|
"sentry-contexts",
|
||||||
"sentry-core",
|
"sentry-core",
|
||||||
"sentry-panic",
|
"sentry-panic",
|
||||||
"sentry-tower",
|
"sentry-tower",
|
||||||
"tokio",
|
|
||||||
"ureq",
|
|
||||||
"webpki-roots 0.22.6",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6673,21 +6610,6 @@ version = "0.7.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
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]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@@ -7382,15 +7304,6 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winreg"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wiremock"
|
name = "wiremock"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
|
@@ -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-http = { version = "0.7.0", features = ["tokio", "hyper"], optional = true }
|
||||||
opentelemetry-prometheus = { version = "0.11.0", optional = true }
|
opentelemetry-prometheus = { version = "0.11.0", optional = true }
|
||||||
prometheus = { version = "0.13.3", 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-tracing = "0.30.0"
|
||||||
sentry-tower = { version = "0.30.0", features = ["http"] }
|
sentry-tower = { version = "0.30.0", features = ["http"] }
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ mas-tasks = { path = "../tasks" }
|
|||||||
mas-templates = { path = "../templates" }
|
mas-templates = { path = "../templates" }
|
||||||
mas-tower = { path = "../tower" }
|
mas-tower = { path = "../tower" }
|
||||||
oauth2-types = { path = "../oauth2-types" }
|
oauth2-types = { path = "../oauth2-types" }
|
||||||
|
httpdate = "1.0.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
indoc = "2.0.1"
|
indoc = "2.0.1"
|
||||||
|
@@ -17,6 +17,8 @@
|
|||||||
#![warn(clippy::pedantic)]
|
#![warn(clippy::pedantic)]
|
||||||
#![allow(clippy::module_name_repetitions)]
|
#![allow(clippy::module_name_repetitions)]
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use mas_config::TelemetryConfig;
|
use mas_config::TelemetryConfig;
|
||||||
@@ -25,7 +27,10 @@ use tracing_subscriber::{
|
|||||||
filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, Registry,
|
filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, Registry,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::sentry_transport::HyperTransportFactory;
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
|
mod sentry_transport;
|
||||||
mod server;
|
mod server;
|
||||||
mod telemetry;
|
mod telemetry;
|
||||||
mod util;
|
mod util;
|
||||||
@@ -70,6 +75,9 @@ async fn try_main() -> anyhow::Result<()> {
|
|||||||
let sentry = sentry::init((
|
let sentry = sentry::init((
|
||||||
telemetry_config.sentry.dsn.as_deref(),
|
telemetry_config.sentry.dsn.as_deref(),
|
||||||
sentry::ClientOptions {
|
sentry::ClientOptions {
|
||||||
|
transport: Some(Arc::new(HyperTransportFactory::new(
|
||||||
|
mas_http::make_untraced_client().await?,
|
||||||
|
))),
|
||||||
traces_sample_rate: 1.0,
|
traces_sample_rate: 1.0,
|
||||||
auto_session_tracking: true,
|
auto_session_tracking: true,
|
||||||
session_mode: sentry::SessionMode::Request,
|
session_mode: sentry::SessionMode::Request,
|
||||||
|
131
crates/cli/src/sentry_transport/mod.rs
Normal file
131
crates/cli/src/sentry_transport/mod.rs
Normal file
@@ -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<C> {
|
||||||
|
client: Client<C>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> HyperTransportFactory<C> {
|
||||||
|
pub fn new(client: Client<C>) -> Self {
|
||||||
|
Self { client }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> TransportFactory for HyperTransportFactory<C>
|
||||||
|
where
|
||||||
|
C: Connect + Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
fn create_transport(&self, options: &ClientOptions) -> Arc<dyn Transport> {
|
||||||
|
Arc::new(HyperTransport::new(options, self.client.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HyperTransport {
|
||||||
|
pub fn new<C>(options: &ClientOptions, client: Client<C>) -> 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)
|
||||||
|
}
|
||||||
|
}
|
185
crates/cli/src/sentry_transport/ratelimit.rs
Normal file
185
crates/cli/src/sentry_transport/ratelimit.rs
Normal file
@@ -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<SystemTime>,
|
||||||
|
error: Option<SystemTime>,
|
||||||
|
session: Option<SystemTime>,
|
||||||
|
transaction: Option<SystemTime>,
|
||||||
|
attachment: Option<SystemTime>,
|
||||||
|
profile: Option<SystemTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<f64>() {
|
||||||
|
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) {
|
||||||
|
// <rate-limit> = (<group>,)+
|
||||||
|
// <group> = <time>:(<category>;)+:<scope>(:<reason>)?
|
||||||
|
|
||||||
|
let mut parse_group = |group: &str| {
|
||||||
|
let mut splits = group.split(':');
|
||||||
|
let seconds = splits.next()?.parse::<f64>().ok()?;
|
||||||
|
let categories = splits.next()?;
|
||||||
|
let _scope = splits.next()?;
|
||||||
|
|
||||||
|
let new_time = Some(SystemTime::now() + Duration::from_secs(seconds.ceil() as u64));
|
||||||
|
|
||||||
|
if categories.is_empty() {
|
||||||
|
self.global = new_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
for category in categories.split(';') {
|
||||||
|
match category {
|
||||||
|
"error" => self.error = new_time,
|
||||||
|
"session" => self.session = new_time,
|
||||||
|
"transaction" => self.transaction = new_time,
|
||||||
|
"attachment" => self.attachment = new_time,
|
||||||
|
"profile" => self.profile = new_time,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(())
|
||||||
|
};
|
||||||
|
|
||||||
|
for group in header.split(',') {
|
||||||
|
parse_group(group.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the RateLimiter in response to a `429` status code.
|
||||||
|
pub fn update_from_429(&mut self) {
|
||||||
|
self.global = Some(SystemTime::now() + Duration::from_secs(60));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query the RateLimiter if a certain category of event is currently rate
|
||||||
|
/// limited.
|
||||||
|
///
|
||||||
|
/// If the given category is rate limited, it will return the remaining
|
||||||
|
/// [`Duration`] for which it is.
|
||||||
|
pub fn is_disabled(&self, category: RateLimitingCategory) -> Option<Duration> {
|
||||||
|
if let Some(ts) = self.global {
|
||||||
|
let time_left = ts.duration_since(SystemTime::now()).ok();
|
||||||
|
if time_left.is_some() {
|
||||||
|
return time_left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let time_left = match category {
|
||||||
|
RateLimitingCategory::Any => self.global,
|
||||||
|
RateLimitingCategory::Error => self.error,
|
||||||
|
RateLimitingCategory::Session => self.session,
|
||||||
|
RateLimitingCategory::Transaction => self.transaction,
|
||||||
|
RateLimitingCategory::Attachment => self.attachment,
|
||||||
|
RateLimitingCategory::Profile => self.profile,
|
||||||
|
}?;
|
||||||
|
time_left.duration_since(SystemTime::now()).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query the RateLimiter for a certain category of event.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the category is *not* rate limited and should be sent.
|
||||||
|
pub fn is_enabled(&self, category: RateLimitingCategory) -> bool {
|
||||||
|
self.is_disabled(category).is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filters the [`Envelope`] according to the current rate limits.
|
||||||
|
///
|
||||||
|
/// Returns [`None`] if all the envelope items were filtered out.
|
||||||
|
pub fn filter_envelope(&self, envelope: Envelope) -> Option<Envelope> {
|
||||||
|
envelope.filter(|item| {
|
||||||
|
self.is_enabled(match item {
|
||||||
|
EnvelopeItem::Event(_) => RateLimitingCategory::Error,
|
||||||
|
EnvelopeItem::SessionUpdate(_) | EnvelopeItem::SessionAggregates(_) => {
|
||||||
|
RateLimitingCategory::Session
|
||||||
|
}
|
||||||
|
EnvelopeItem::Transaction(_) => RateLimitingCategory::Transaction,
|
||||||
|
EnvelopeItem::Attachment(_) => RateLimitingCategory::Attachment,
|
||||||
|
EnvelopeItem::Profile(_) => RateLimitingCategory::Profile,
|
||||||
|
_ => RateLimitingCategory::Any,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Category of payload that a Rate Limit refers to.
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum RateLimitingCategory {
|
||||||
|
/// Rate Limit for any kind of payload.
|
||||||
|
Any,
|
||||||
|
/// Rate Limit pertaining to Errors.
|
||||||
|
Error,
|
||||||
|
/// Rate Limit pertaining to Sessions.
|
||||||
|
Session,
|
||||||
|
/// Rate Limit pertaining to Transactions.
|
||||||
|
Transaction,
|
||||||
|
/// Rate Limit pertaining to Attachments.
|
||||||
|
Attachment,
|
||||||
|
/// Rate Limit pertaining to Profiles.
|
||||||
|
Profile,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sentry_header() {
|
||||||
|
let mut rl = RateLimiter::new();
|
||||||
|
rl.update_from_sentry_header("120:error:project:reason, 60:session:foo");
|
||||||
|
|
||||||
|
assert!(rl.is_disabled(RateLimitingCategory::Error).unwrap() <= Duration::from_secs(120));
|
||||||
|
assert!(rl.is_disabled(RateLimitingCategory::Session).unwrap() <= Duration::from_secs(60));
|
||||||
|
assert!(rl.is_disabled(RateLimitingCategory::Transaction).is_none());
|
||||||
|
assert!(rl.is_disabled(RateLimitingCategory::Any).is_none());
|
||||||
|
|
||||||
|
rl.update_from_sentry_header(
|
||||||
|
r#"
|
||||||
|
30::bar,
|
||||||
|
120:invalid:invalid,
|
||||||
|
4711:foo;bar;baz;security:project
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
rl.is_disabled(RateLimitingCategory::Transaction).unwrap() <= Duration::from_secs(30)
|
||||||
|
);
|
||||||
|
assert!(rl.is_disabled(RateLimitingCategory::Any).unwrap() <= Duration::from_secs(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_retry_after() {
|
||||||
|
let mut rl = RateLimiter::new();
|
||||||
|
rl.update_from_retry_after("60");
|
||||||
|
|
||||||
|
assert!(rl.is_disabled(RateLimitingCategory::Error).unwrap() <= Duration::from_secs(60));
|
||||||
|
assert!(rl.is_disabled(RateLimitingCategory::Session).unwrap() <= Duration::from_secs(60));
|
||||||
|
assert!(
|
||||||
|
rl.is_disabled(RateLimitingCategory::Transaction).unwrap() <= Duration::from_secs(60)
|
||||||
|
);
|
||||||
|
assert!(rl.is_disabled(RateLimitingCategory::Any).unwrap() <= Duration::from_secs(60));
|
||||||
|
}
|
||||||
|
}
|
118
crates/cli/src/sentry_transport/tokio_thread.rs
Normal file
118
crates/cli/src/sentry_transport/tokio_thread.rs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
// Copied from sentry/transports/tokio_thread.rs
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
mpsc::{sync_channel, SyncSender},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
thread::{self, JoinHandle},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use sentry::{sentry_debug, Envelope};
|
||||||
|
|
||||||
|
use super::ratelimit::{RateLimiter, RateLimitingCategory};
|
||||||
|
|
||||||
|
enum Task {
|
||||||
|
SendEnvelope(Envelope),
|
||||||
|
Flush(SyncSender<()>),
|
||||||
|
Shutdown,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TransportThread {
|
||||||
|
sender: SyncSender<Task>,
|
||||||
|
shutdown: Arc<AtomicBool>,
|
||||||
|
handle: Option<JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransportThread {
|
||||||
|
pub fn new<SendFn, SendFuture>(mut send: SendFn) -> Self
|
||||||
|
where
|
||||||
|
SendFn: FnMut(Envelope, RateLimiter) -> SendFuture + Send + 'static,
|
||||||
|
// NOTE: returning RateLimiter here, otherwise we are in borrow hell
|
||||||
|
SendFuture: std::future::Future<Output = RateLimiter>,
|
||||||
|
{
|
||||||
|
let (sender, receiver) = sync_channel(30);
|
||||||
|
let shutdown = Arc::new(AtomicBool::new(false));
|
||||||
|
let shutdown_worker = shutdown.clone();
|
||||||
|
let handle = thread::Builder::new()
|
||||||
|
.name("sentry-transport".into())
|
||||||
|
.spawn(move || {
|
||||||
|
// create a runtime on the transport thread
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut rl = RateLimiter::new();
|
||||||
|
|
||||||
|
// and block on an async fn in this runtime/thread
|
||||||
|
rt.block_on(async move {
|
||||||
|
for task in receiver.into_iter() {
|
||||||
|
if shutdown_worker.load(Ordering::SeqCst) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let envelope = match task {
|
||||||
|
Task::SendEnvelope(envelope) => envelope,
|
||||||
|
Task::Flush(sender) => {
|
||||||
|
sender.send(()).ok();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Task::Shutdown => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(time_left) = rl.is_disabled(RateLimitingCategory::Any) {
|
||||||
|
sentry_debug!(
|
||||||
|
"Skipping event send because we're disabled due to rate limits for {}s",
|
||||||
|
time_left.as_secs()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match rl.filter_envelope(envelope) {
|
||||||
|
Some(envelope) => {
|
||||||
|
rl = send(envelope, rl).await;
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
sentry_debug!("Envelope was discarded due to per-item rate limits");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
sender,
|
||||||
|
shutdown,
|
||||||
|
handle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&self, envelope: Envelope) {
|
||||||
|
// Using send here would mean that when the channel fills up for whatever
|
||||||
|
// reason, trying to send an envelope would block everything. We'd rather
|
||||||
|
// drop the envelope in that case.
|
||||||
|
if let Err(e) = self.sender.try_send(Task::SendEnvelope(envelope)) {
|
||||||
|
sentry_debug!("envelope dropped: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flush(&self, timeout: Duration) -> bool {
|
||||||
|
let (sender, receiver) = sync_channel(1);
|
||||||
|
let _ = self.sender.send(Task::Flush(sender));
|
||||||
|
receiver.recv_timeout(timeout).is_err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TransportThread {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.shutdown.store(true, Ordering::SeqCst);
|
||||||
|
let _ = self.sender.send(Task::Shutdown);
|
||||||
|
if let Some(handle) = self.handle.take() {
|
||||||
|
handle.join().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user