diff --git a/Cargo.lock b/Cargo.lock index 606d4420f5e..c53eee33a7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4518,6 +4518,7 @@ name = "relay-config" version = "26.8.0" dependencies = [ "anyhow", + "arc-swap", "human-size", "insta", "num_cpus", @@ -5736,9 +5737,9 @@ dependencies = [ [[package]] name = "serde-vars" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31ed6ad0418012bd1f7493bb01d9b107e81aec23042be9292d7d154909a5c45" +checksum = "3a65612ae74ca3f9679f104fee36535603891b589ec4ce665569940600c6f2a4" dependencies = [ "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 1035679de78..6bd3c3336fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -213,7 +213,7 @@ sentry-types = "0.41" sentry_protos = "0.7" serde = { version = "=1.0.228", features = ["derive", "rc"] } serde-transcode = "1" -serde-vars = "0.3" +serde-vars = "0.4" serde_bytes = "0.11" serde_json = "1" serde_path_to_error = "0.1" diff --git a/relay-config/Cargo.toml b/relay-config/Cargo.toml index 04c9b3598c2..e79d34642b4 100644 --- a/relay-config/Cargo.toml +++ b/relay-config/Cargo.toml @@ -18,6 +18,7 @@ workspace = true [dependencies] anyhow = { workspace = true } +arc-swap = { workspace = true } human-size = { workspace = true } num_cpus = { workspace = true } relay-auth = { workspace = true } diff --git a/relay-config/src/byte_size.rs b/relay-config/src/byte_size.rs index 6c5484c27cf..73b2647e735 100644 --- a/relay-config/src/byte_size.rs +++ b/relay-config/src/byte_size.rs @@ -35,6 +35,7 @@ use serde::{Serialize, de}; /// let size = ByteSize::kibibytes(42); /// assert_eq!("42KiB", size.to_string()); /// ``` +#[derive(Copy, Clone)] pub struct ByteSize(Size); impl ByteSize { diff --git a/relay-config/src/config.rs b/relay-config/src/config.rs index d79f9445162..feda7318d10 100644 --- a/relay-config/src/config.rs +++ b/relay-config/src/config.rs @@ -1,14 +1,16 @@ -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::error::Error; use std::io::Write; use std::net::{IpAddr, SocketAddr}; use std::num::{NonZeroU8, NonZeroU16}; use std::path::{Path, PathBuf}; use std::str::FromStr; +use std::sync::Arc; use std::time::Duration; use std::{env, fmt, fs, io}; use anyhow::Context; +use arc_swap::ArcSwap; use relay_auth::{PublicKey, RelayId, SecretKey, generate_key_pair, generate_relay_id}; use relay_common::Dsn; use relay_kafka::{ @@ -137,6 +139,15 @@ impl fmt::Display for ConfigError { impl Error for ConfigError {} +#[derive(Debug, Default, Clone)] +struct LoadedConfig { + config: C, + /// A list of files this config is built from. + /// + /// The config may be built from multiple files due to the support for `${file:}` references + /// in arbitrary config keys. + source_files: BTreeSet, +} enum ConfigFormat { Yaml, Json, @@ -164,24 +175,27 @@ trait ConfigObject: DeserializeOwned + Serialize { } /// Loads the config file from a file within the given directory location. - fn load(base: &Path) -> anyhow::Result { + fn load(base: &Path) -> anyhow::Result> { let path = Self::path(base); let f = fs::File::open(&path) .with_context(|| ConfigError::file(ConfigErrorKind::CouldNotOpenFile, &path))?; let f = io::BufReader::new(f); + let mut source_files = BTreeSet::new(); + let mut source = { let file = serde_vars::FileSource::default() .with_variable_prefix("${file:") .with_variable_suffix("}") - .with_base_path(base); + .with_base_path(base) + .with_file_system(crate::source::TrackingFileSystem(&mut source_files)); let env = serde_vars::EnvSource::default() .with_variable_prefix("${") .with_variable_suffix("}"); (file, env) }; - match Self::format() { + let config = match Self::format() { ConfigFormat::Yaml => { serde_vars::deserialize(serde_yaml::Deserializer::from_reader(f), &mut source) .with_context(|| ConfigError::file(ConfigErrorKind::BadYaml, &path)) @@ -190,7 +204,15 @@ trait ConfigObject: DeserializeOwned + Serialize { serde_vars::deserialize(&mut serde_json::Deserializer::from_reader(f), &mut source) .with_context(|| ConfigError::file(ConfigErrorKind::BadJson, &path)) } - } + }?; + + // The base config path is also a dependency of the entire config. + source_files.insert(path); + + Ok(LoadedConfig { + config, + source_files, + }) } /// Writes the configuration to a file within the given directory location. @@ -228,7 +250,7 @@ trait ConfigObject: DeserializeOwned + Serialize { /// Structure used to hold information about configuration overrides via /// CLI parameters or environment variables -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct OverridableConfig { /// The operation mode of this relay. pub mode: Option, @@ -483,7 +505,7 @@ pub enum ReadinessCondition { } /// Relay specific configuration values. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct Relay { /// The operation mode of this Relay. @@ -560,7 +582,7 @@ impl Default for Relay { } /// Control the metrics. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct Metrics { /// Hostname and port of the statsd server. @@ -600,7 +622,7 @@ impl Default for Metrics { } /// Controls various limits -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct Limits { /// How many requests can be sent concurrently from Relay to the upstream before Relay starts @@ -761,7 +783,7 @@ impl Default for Limits { } /// Controls traffic steering. -#[derive(Debug, Default, Deserialize, Serialize)] +#[derive(Debug, Default, Deserialize, Serialize, Clone)] #[serde(default)] pub struct Routing { /// Accept and forward unknown Envelope items to the upstream. @@ -837,7 +859,7 @@ impl HttpEncoding { } /// Controls authentication with upstream. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct Http { /// Timeout for upstream requests in seconds. @@ -952,7 +974,7 @@ pub enum EnvelopeSpoolPartitioning { } /// Persistent buffering configuration for incoming envelopes. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(default)] pub struct EnvelopeSpool { /// The path of the SQLite database file(s) which persist the data. @@ -1055,7 +1077,7 @@ impl Default for EnvelopeSpool { } /// Persistent buffering configuration. -#[derive(Debug, Serialize, Deserialize, Default)] +#[derive(Debug, Serialize, Deserialize, Default, Clone)] #[serde(default)] pub struct Spool { /// Configuration for envelope spooling. @@ -1063,7 +1085,7 @@ pub struct Spool { } /// Controls internal caching behavior. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct Cache { /// The full project state will be requested by this Relay if set to `true`. @@ -1130,7 +1152,7 @@ impl Default for Cache { } /// Controls Sentry-internal event processing. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct Processing { /// True if the Relay should do processing. Defaults to `false`. @@ -1222,7 +1244,7 @@ impl Default for Processing { } /// Configuration for normalization in this Relay. -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize, Clone)] #[serde(default)] pub struct Normalization { /// Level of normalization for Relay to apply to incoming data. @@ -1246,7 +1268,7 @@ pub enum NormalizationLevel { } /// Configuration options for objectstore's auth scheme. -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct ObjectstoreAuthConfig { /// Identifier for the private key used to sign objectstore's tokens. Must correspond to a /// public key configured in objectstore. @@ -1266,7 +1288,7 @@ impl fmt::Debug for ObjectstoreAuthConfig { } /// Configuration values for the objectstore service. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct ObjectstoreServiceConfig { /// The base URL for the objectstore service. @@ -1402,7 +1424,7 @@ impl<'de> Deserialize<'de> for EmitOutcomes { } /// Outcome generation specific configuration values. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct Outcomes { /// Controls whether outcomes will be emitted when processing is disabled. @@ -1515,7 +1537,7 @@ mod config_relay_info { } /// Authentication options. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct AuthConfig { /// Controls responses from the readiness health check endpoint based on authentication. @@ -1543,37 +1565,17 @@ impl Default for AuthConfig { } /// GeoIp database configuration options. -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct GeoIpConfig { /// The path to GeoIP database. pub path: Option, } -/// Cardinality Limiter configuration options. -#[derive(Serialize, Deserialize, Debug)] -#[serde(default)] -pub struct CardinalityLimiter { - /// Cache vacuum interval in seconds for the in memory cache. - /// - /// The cache will scan for expired values based on this interval. - /// - /// Defaults to 180 seconds, 3 minutes. - pub cache_vacuum_interval: u64, -} - -impl Default for CardinalityLimiter { - fn default() -> Self { - Self { - cache_vacuum_interval: 180, - } - } -} - /// Settings to control Relay's health checks. /// /// After breaching one of the configured thresholds, Relay will /// return an `unhealthy` status from its health endpoint. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct Health { /// Interval to refresh internal health checks. @@ -1620,7 +1622,7 @@ impl Default for Health { } /// COGS configuration. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct Cogs { /// Maximium amount of COGS measurements allowed to backlog. @@ -1704,7 +1706,7 @@ impl fmt::Debug for UploadCredentials { } /// All configuration values that can be deserialized from `config.yml`. -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] #[serde(default)] #[allow(missing_docs)] pub struct ConfigValues { @@ -1724,7 +1726,6 @@ pub struct ConfigValues { pub auth: AuthConfig, pub geoip: GeoIpConfig, pub normalization: Normalization, - pub cardinality_limiter: CardinalityLimiter, pub health: Health, pub cogs: Cogs, pub upload: Upload, @@ -1740,104 +1741,55 @@ impl ConfigObject for ConfigValues { } } -/// Config struct. -pub struct Config { +#[derive(Default, Clone)] +struct ConfigInner { + /// Relay's config values. values: ConfigValues, + /// Configured Relay credentials. + /// + /// Credentials may be missing for proxy mode. credentials: Option, - path: PathBuf, -} - -impl fmt::Debug for Config { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Config") - .field("path", &self.path) - .field("values", &self.values) - .finish() - } + /// All source files the config was parsed from. + source_files: BTreeSet, } -impl Config { - /// Loads a config from a given config folder. - pub fn from_path>(path: P) -> anyhow::Result { - let path = env::current_dir() - .map(|x| x.join(path.as_ref())) - .unwrap_or_else(|_| path.as_ref().to_path_buf()); - - let config = Config { - values: ConfigValues::load(&path)?, - credentials: if Credentials::path(&path).exists() { - Some(Credentials::load(&path)?) - } else { - None - }, - path: path.clone(), - }; - - if cfg!(not(feature = "processing")) && config.processing_enabled() { - return Err(ConfigError::file(ConfigErrorKind::ProcessingNotAvailable, &path).into()); +impl ConfigInner { + fn apply_overrides(&mut self, overrides: &OverridableConfig) -> anyhow::Result<()> { + if let Some(log_level) = &overrides.log_level { + self.values.logging.level = log_level.parse()?; } - Ok(config) - } - - /// Creates a config from a JSON value. - /// - /// This is mostly useful for tests. - pub fn from_json_value(value: serde_json::Value) -> anyhow::Result { - Ok(Config { - values: serde_json::from_value(value) - .with_context(|| ConfigError::new(ConfigErrorKind::BadJson))?, - credentials: None, - path: PathBuf::new(), - }) - } + if let Some(log_format) = &overrides.log_format { + self.values.logging.format = log_format.parse()?; + } - /// Override configuration with values coming from other sources (e.g. env variables or - /// command line parameters) - pub fn apply_override( - &mut self, - mut overrides: OverridableConfig, - ) -> anyhow::Result<&mut Self> { let relay = &mut self.values.relay; - - if let Some(mode) = overrides.mode { + if let Some(mode) = &overrides.mode { relay.mode = mode .parse::() .with_context(|| ConfigError::field("mode"))?; } - - if let Some(deployment) = overrides.instance { + if let Some(deployment) = &overrides.instance { relay.instance = deployment .parse::() .with_context(|| ConfigError::field("deployment"))?; } - - if let Some(log_level) = overrides.log_level { - self.values.logging.level = log_level.parse()?; - } - - if let Some(log_format) = overrides.log_format { - self.values.logging.format = log_format.parse()?; - } - - if let Some(upstream) = overrides.upstream { + if let Some(upstream) = &overrides.upstream { relay.upstream = upstream .parse::() .with_context(|| ConfigError::field("upstream"))?; - } else if let Some(upstream_dsn) = overrides.upstream_dsn { + } else if let Some(upstream_dsn) = &overrides.upstream_dsn { relay.upstream = upstream_dsn .parse::() .map(|dsn| UpstreamDescriptor::from_dsn(&dsn)) .with_context(|| ConfigError::field("upstream_dsn"))?; } - - if let Some(host) = overrides.host { + if let Some(host) = &overrides.host { relay.host = host .parse::() .with_context(|| ConfigError::field("host"))?; } - - if let Some(port) = overrides.port { + if let Some(port) = &overrides.port { relay.port = port .as_str() .parse() @@ -1845,19 +1797,17 @@ impl Config { } let processing = &mut self.values.processing; - if let Some(enabled) = overrides.processing { + if let Some(enabled) = &overrides.processing { match enabled.to_lowercase().as_str() { "true" | "1" => processing.enabled = true, "false" | "0" | "" => processing.enabled = false, _ => return Err(ConfigError::field("processing").into()), } } - - if let Some(redis) = overrides.redis_url { + if let Some(redis) = overrides.redis_url.clone() { processing.redis = Some(RedisConfigs::Unified(RedisConfig::single(redis))) } - - if let Some(kafka_url) = overrides.kafka_url { + if let Some(kafka_url) = overrides.kafka_url.clone() { let existing = processing .kafka_config .iter_mut() @@ -1866,20 +1816,34 @@ impl Config { if let Some(config_param) = existing { config_param.value = kafka_url; } else { - processing.kafka_config.push(KafkaConfigParam { + self.values.processing.kafka_config.push(KafkaConfigParam { name: "bootstrap.servers".to_owned(), value: kafka_url, }) } } - // credentials overrides - let id = if let Some(id) = overrides.id { - let id = Uuid::parse_str(&id).with_context(|| ConfigError::field("id"))?; + + if overrides.outcome_source.is_some() { + self.values.outcomes.source = overrides.outcome_source.clone(); + } + + if let Some(shutdown_timeout) = &overrides.shutdown_timeout + && let Ok(shutdown_timeout) = shutdown_timeout.parse::() + { + self.values.limits.shutdown_timeout = shutdown_timeout; + } + + if let Some(server_name) = overrides.server_name.clone() { + self.values.sentry.server_name = Some(server_name.into()); + } + + let id = if let Some(id) = &overrides.id { + let id = Uuid::parse_str(id).with_context(|| ConfigError::field("id"))?; Some(id) } else { None }; - let public_key = if let Some(public_key) = overrides.public_key { + let public_key = if let Some(public_key) = &overrides.public_key { let public_key = public_key .parse::() .with_context(|| ConfigError::field("public_key"))?; @@ -1888,7 +1852,7 @@ impl Config { None }; - let secret_key = if let Some(secret_key) = overrides.secret_key { + let secret_key = if let Some(secret_key) = &overrides.secret_key { let secret_key = secret_key .parse::() .with_context(|| ConfigError::field("secret_key"))?; @@ -1896,10 +1860,6 @@ impl Config { } else { None }; - let outcomes = &mut self.values.outcomes; - if overrides.outcome_source.is_some() { - outcomes.source = overrides.outcome_source.take(); - } if let Some(credentials) = &mut self.credentials { //we have existing credentials we may override some entries @@ -1932,17 +1892,103 @@ impl Config { } } - let limits = &mut self.values.limits; - if let Some(shutdown_timeout) = overrides.shutdown_timeout - && let Ok(shutdown_timeout) = shutdown_timeout.parse::() - { - limits.shutdown_timeout = shutdown_timeout; + Ok(()) + } +} + +/// Relay's Configuration. +pub struct Config { + /// The actual config. + inner: ArcSwap, + /// A list of overrides applied to the config, in order. + /// + /// When re-loading the configuration these overrides need to be applied again + /// in the same order as they were applied originally. + overrides: Vec, + /// Path from which the config is loaded. + /// + /// This is Relay's configuration directory. + path: PathBuf, +} + +impl fmt::Debug for Config { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let inner = self.inner.load(); + + f.debug_struct("Config") + .field("path", &self.path) + // Only print specific parts of `inner` to not leak the credentials. + .field("values", &inner.values) + .field("source_files", &inner.source_files) + .finish() + } +} + +impl Config { + /// Loads a config from a given config folder. + pub fn from_path>(path: P) -> anyhow::Result { + let path = env::current_dir() + .map(|x| x.join(path.as_ref())) + .unwrap_or_else(|_| path.as_ref().to_path_buf()); + + let values = ConfigValues::load(&path)?; + let mut inner = ConfigInner { + values: values.config, + credentials: None, + source_files: values.source_files, + }; + + if Credentials::path(&path).exists() { + let credentials = Credentials::load(&path)?; + inner.credentials = Some(credentials.config); + inner.source_files.extend(credentials.source_files); } - if let Some(server_name) = overrides.server_name { - self.values.sentry.server_name = Some(server_name.into()); + let config = Config { + inner: ArcSwap::from_pointee(inner), + overrides: Vec::new(), + path: path.clone(), + }; + + if cfg!(not(feature = "processing")) && config.current().processing_enabled() { + return Err(ConfigError::file(ConfigErrorKind::ProcessingNotAvailable, &path).into()); } + Ok(config) + } + + /// Creates a config from a JSON value. + /// + /// This is mostly useful for tests. + pub fn from_json_value(value: serde_json::Value) -> anyhow::Result { + Ok(Config { + inner: ArcSwap::from_pointee(ConfigInner { + values: serde_json::from_value(value) + .with_context(|| ConfigError::new(ConfigErrorKind::BadJson))?, + credentials: None, + source_files: Default::default(), + }), + overrides: Vec::new(), + path: PathBuf::new(), + }) + } + + /// Override configuration with values coming from other sources (e.g. env variables or + /// command line parameters). + /// + /// If applying the overrides fails, the config may be left in an inconsistent state. + pub fn apply_override(&mut self, overrides: OverridableConfig) -> anyhow::Result<&mut Self> { + // We could introduce a config builder which operates on mutable configs, which would eliminate + // the need for this `try_rcu` dance here. + crate::utils::try_rcu(&self.inner, |inner| { + let mut new = ConfigInner::clone(inner); + new.apply_overrides(&overrides)?; + Ok::<_, anyhow::Error>(Arc::new(new)) + })?; + + // Overrides successfully applied. + self.overrides.push(overrides); + Ok(self) } @@ -1958,104 +2004,135 @@ impl Config { /// Dumps out a YAML string of the values. pub fn to_yaml_string(&self) -> anyhow::Result { - serde_yaml::to_string(&self.values) + serde_yaml::to_string(&self.inner.load().values) .with_context(|| ConfigError::new(ConfigErrorKind::CouldNotWriteFile)) } - /// Regenerates the relay credentials. - /// - /// This also writes the credentials back to the file. - pub fn regenerate_credentials(&mut self, save: bool) -> anyhow::Result<()> { - let creds = Credentials::generate(); - if save { - creds.save(&self.path)?; - } - self.credentials = Some(creds); - Ok(()) - } - - /// Return the current credentials - pub fn credentials(&self) -> Option<&Credentials> { - self.credentials.as_ref() - } - /// Set new credentials. /// - /// This also writes the credentials back to the file. + /// This also writes the credentials back to the file, if this config was loaded from the file-system. pub fn replace_credentials( &mut self, credentials: Option, ) -> anyhow::Result { - if self.credentials == credentials { + if self.inner.load().credentials == credentials { return Ok(false); } - match credentials { - Some(ref creds) => { - creds.save(&self.path)?; - } - None => { - let path = Credentials::path(&self.path); - if fs::metadata(&path).is_ok() { - fs::remove_file(&path).with_context(|| { - ConfigError::file(ConfigErrorKind::CouldNotWriteFile, &path) - })?; + if !self.path.is_empty() { + match &credentials { + Some(creds) => { + creds.save(&self.path)?; + } + None => { + let path = Credentials::path(&self.path); + if fs::metadata(&path).is_ok() { + fs::remove_file(&path).with_context(|| { + ConfigError::file(ConfigErrorKind::CouldNotWriteFile, &path) + })?; + } } } } - self.credentials = credentials; + // Note: there is never anyone racing on the `ArcSwap` as long as `Self` borrowed mutably. + // + // We can improve this if we split out mutable operations into a separate struct and only + // once `frozen()` we change to an `ArcSwap` internally. + self.inner.rcu(|inner| { + let mut inner = ConfigInner::clone(inner); + inner.credentials = credentials.clone(); + Arc::new(inner) + }); + Ok(true) } + /// Acquires a current [`snapshot`](ConfigSnapshot) of the config. + /// + /// A snapshot is the way to actually consume values from the config. A snapshot should ideally be acquired + /// once per unit of work. + pub fn current(&self) -> ConfigSnapshot { + let inner = self.inner.load(); + ConfigSnapshot { inner } + } +} + +impl Default for Config { + fn default() -> Self { + Self { + inner: ArcSwap::from_pointee(Default::default()), + overrides: Vec::new(), + path: PathBuf::new(), + } + } +} + +/// A config snapshot is a point in time snapshot of the [`Config`]. +/// +/// The [`Config`] may change over time, to guarantee a consistent view of the config +/// a snapshot must be acquired first. +/// +/// The snapshot should not be stored in a long lasting datastructure. As a rule of thumb it should +/// only exist on the stack. +pub struct ConfigSnapshot { + inner: arc_swap::Guard>, +} + +impl ConfigSnapshot { /// Returns `true` if the config is ready to use. pub fn has_credentials(&self) -> bool { - self.credentials.is_some() + self.inner.credentials.is_some() + } + + /// Return the current credentials. + pub fn credentials(&self) -> Option<&Credentials> { + self.inner.credentials.as_ref() } /// Returns the secret key if set. pub fn secret_key(&self) -> Option<&SecretKey> { - self.credentials.as_ref().map(|x| &x.secret_key) + self.inner.credentials.as_ref().map(|x| &x.secret_key) } /// Returns the public key if set. pub fn public_key(&self) -> Option<&PublicKey> { - self.credentials.as_ref().map(|x| &x.public_key) + self.inner.credentials.as_ref().map(|x| &x.public_key) } /// Returns the relay ID. pub fn relay_id(&self) -> Option<&RelayId> { - self.credentials.as_ref().map(|x| &x.id) + self.inner.credentials.as_ref().map(|x| &x.id) } /// Returns the relay mode. pub fn relay_mode(&self) -> RelayMode { - self.values.relay.mode + self.inner.values.relay.mode } /// Returns the instance type of relay. pub fn relay_instance(&self) -> RelayInstance { - self.values.relay.instance + self.inner.values.relay.instance } /// Returns the upstream target as descriptor. pub fn upstream(&self) -> &UpstreamDescriptor { - &self.values.relay.upstream + &self.inner.values.relay.upstream } /// Returns the advertised upstream for downstream instances as descriptor. pub fn advertised_upstream(&self) -> Option<&UpstreamDescriptor> { - self.values.relay.advertised_upstream.as_ref() + self.inner.values.relay.advertised_upstream.as_ref() } /// Returns the custom HTTP "Host" header. pub fn http_host_header(&self) -> Option<&str> { - self.values.http.host_header.as_deref() + self.inner.values.http.host_header.as_deref() } /// Returns the listen address. pub fn listen_addr(&self) -> SocketAddr { - (self.values.relay.host, self.values.relay.port).into() + (self.inner.values.relay.host, self.inner.values.relay.port).into() } /// Returns the listen address for internal APIs. @@ -2067,11 +2144,11 @@ impl Config { /// and they should instead be exposed on the main [`Self::listen_addr`]. pub fn listen_addr_internal(&self) -> Option { match ( - self.values.relay.internal_host, - self.values.relay.internal_port, + self.inner.values.relay.internal_host, + self.inner.values.relay.internal_port, ) { - (Some(host), None) => Some((host, self.values.relay.port).into()), - (None, Some(port)) => Some((self.values.relay.host, port).into()), + (Some(host), None) => Some((host, self.inner.values.relay.port).into()), + (None, Some(port)) => Some((self.inner.values.relay.host, port).into()), (Some(host), Some(port)) => Some((host, port).into()), (None, None) => None, } @@ -2079,9 +2156,9 @@ impl Config { /// Returns the TLS listen address. pub fn tls_listen_addr(&self) -> Option { - if self.values.relay.tls_identity_path.is_some() { - let port = self.values.relay.tls_port.unwrap_or(3443); - Some((self.values.relay.host, port).into()) + if self.inner.values.relay.tls_identity_path.is_some() { + let port = self.inner.values.relay.tls_port.unwrap_or(3443); + Some((self.inner.values.relay.host, port).into()) } else { None } @@ -2089,26 +2166,26 @@ impl Config { /// Returns the path to the identity bundle pub fn tls_identity_path(&self) -> Option<&Path> { - self.values.relay.tls_identity_path.as_deref() + self.inner.values.relay.tls_identity_path.as_deref() } /// Returns the password for the identity bundle pub fn tls_identity_password(&self) -> Option<&str> { - self.values.relay.tls_identity_password.as_deref() + self.inner.values.relay.tls_identity_password.as_deref() } /// Returns `true` when project IDs should be overriden rather than validated. /// /// Defaults to `false`, which requires project ID validation. pub fn override_project_ids(&self) -> bool { - self.values.relay.override_project_ids + self.inner.values.relay.override_project_ids } /// Returns `true` if Relay requires authentication for readiness. /// /// See [`ReadinessCondition`] for more information. pub fn requires_auth(&self) -> bool { - match self.values.auth.ready { + match self.inner.values.auth.ready { ReadinessCondition::Authenticated => self.relay_mode() == RelayMode::Managed, ReadinessCondition::Always => false, } @@ -2122,7 +2199,7 @@ impl Config { return None; } - match self.values.http.auth_interval { + match self.inner.values.http.auth_interval { None | Some(0) => None, Some(secs) => Some(Duration::from_secs(secs)), } @@ -2131,7 +2208,7 @@ impl Config { /// The maximum time of experiencing uninterrupted network failures until Relay considers that /// it has encountered a network outage. pub fn http_outage_grace_period(&self) -> Duration { - Duration::from_secs(self.values.http.outage_grace_period) + Duration::from_secs(self.inner.values.http.outage_grace_period) } /// Time Relay waits before retrying an upstream request. @@ -2139,22 +2216,22 @@ impl Config { /// Before going into a network outage, Relay may fail to make upstream /// requests. This is the time Relay waits before retrying the same request. pub fn http_retry_delay(&self) -> Duration { - Duration::from_secs(self.values.http.retry_delay) + Duration::from_secs(self.inner.values.http.retry_delay) } /// Time of continued project request failures before Relay emits an error. pub fn http_project_failure_interval(&self) -> Duration { - Duration::from_secs(self.values.http.project_failure_interval) + Duration::from_secs(self.inner.values.http.project_failure_interval) } /// Content encoding of upstream requests. pub fn http_encoding(&self) -> HttpEncoding { - self.values.http.encoding + self.inner.values.http.encoding } /// Returns whether metrics should be sent globally through a shared endpoint. pub fn http_global_metrics(&self) -> bool { - self.values.http.global_metrics + self.inner.values.http.global_metrics } /// Returns `true` if Relay supports forwarding unknown API requests. @@ -2162,7 +2239,7 @@ impl Config { /// Relay instances with processing enabled are expected to support the latest API and do never /// support forwarding requests to Sentry. pub fn http_forward(&self) -> bool { - self.values.http.forward && !self.processing_enabled() + self.inner.values.http.forward && !self.processing_enabled() } /// Returns whether this Relay should emit outcomes. @@ -2173,64 +2250,64 @@ impl Config { if self.processing_enabled() { return EmitOutcomes::AsOutcomes; } - self.values.outcomes.emit_outcomes + self.inner.values.outcomes.emit_outcomes } /// Returns the maximum number of outcomes that are batched before being sent pub fn outcome_batch_size(&self) -> usize { - self.values.outcomes.batch_size + self.inner.values.outcomes.batch_size } /// Returns the maximum interval that an outcome may be batched pub fn outcome_batch_interval(&self) -> Duration { - Duration::from_millis(self.values.outcomes.batch_interval) + Duration::from_millis(self.inner.values.outcomes.batch_interval) } /// The originating source of the outcome pub fn outcome_source(&self) -> Option<&str> { - self.values.outcomes.source.as_deref() + self.inner.values.outcomes.source.as_deref() } /// Returns logging configuration. pub fn logging(&self) -> &relay_log::LogConfig { - &self.values.logging + &self.inner.values.logging } /// Returns logging configuration. pub fn sentry(&self) -> &relay_log::SentryConfig { - &self.values.sentry + &self.inner.values.sentry } /// Returns the addresses for statsd metrics. pub fn statsd_addr(&self) -> Option<&str> { - self.values.metrics.statsd.as_deref() + self.inner.values.metrics.statsd.as_deref() } /// Returns the addresses for statsd metrics. pub fn statsd_buffer_size(&self) -> Option { - self.values.metrics.statsd_buffer_size + self.inner.values.metrics.statsd_buffer_size } /// Return the prefix for statsd metrics. pub fn metrics_prefix(&self) -> &str { - &self.values.metrics.prefix + &self.inner.values.metrics.prefix } /// Returns the default tags for statsd metrics. pub fn metrics_default_tags(&self) -> &BTreeMap { - &self.values.metrics.default_tags + &self.inner.values.metrics.default_tags } /// Returns the name of the hostname tag that should be attached to each outgoing metric. pub fn metrics_hostname_tag(&self) -> Option<&str> { - self.values.metrics.hostname_tag.as_deref() + self.inner.values.metrics.hostname_tag.as_deref() } /// Returns the interval for periodic metrics emitted from Relay. /// /// `None` if periodic metrics are disabled. pub fn metrics_periodic_interval(&self) -> Option { - match self.values.metrics.periodic_secs { + match self.inner.values.metrics.periodic_secs { 0 => None, secs => Some(Duration::from_secs(secs)), } @@ -2238,42 +2315,43 @@ impl Config { /// Returns the default timeout for all upstream HTTP requests. pub fn http_timeout(&self) -> Duration { - Duration::from_secs(self.values.http.timeout.into()) + Duration::from_secs(self.inner.values.http.timeout.into()) } /// Returns the connection timeout for all upstream HTTP requests. pub fn http_connection_timeout(&self) -> Duration { - Duration::from_secs(self.values.http.connection_timeout.into()) + Duration::from_secs(self.inner.values.http.connection_timeout.into()) } /// Returns the failed upstream request retry interval. pub fn http_max_retry_interval(&self) -> Duration { - Duration::from_secs(self.values.http.max_retry_interval.into()) + Duration::from_secs(self.inner.values.http.max_retry_interval.into()) } /// Returns `true` if relay should use an in-process cache for DNS lookups. pub fn http_dns_cache(&self) -> bool { - self.values.http.dns_cache + self.inner.values.http.dns_cache } /// Returns the expiry timeout for cached projects. pub fn project_cache_expiry(&self) -> Duration { - Duration::from_secs(self.values.cache.project_expiry.into()) + Duration::from_secs(self.inner.values.cache.project_expiry.into()) } /// Returns `true` if the full project state should be requested from upstream. pub fn request_full_project_config(&self) -> bool { - self.values.cache.project_request_full_config + self.inner.values.cache.project_request_full_config } /// Returns the expiry timeout for cached relay infos (public keys). pub fn relay_cache_expiry(&self) -> Duration { - Duration::from_secs(self.values.cache.relay_expiry.into()) + Duration::from_secs(self.inner.values.cache.relay_expiry.into()) } /// Returns the maximum number of buffered envelopes pub fn envelope_buffer_size(&self) -> usize { - self.values + self.inner + .values .cache .envelope_buffer_size .try_into() @@ -2282,19 +2360,20 @@ impl Config { /// Returns the expiry timeout for cached misses before trying to refetch. pub fn cache_miss_expiry(&self) -> Duration { - Duration::from_secs(self.values.cache.miss_expiry.into()) + Duration::from_secs(self.inner.values.cache.miss_expiry.into()) } /// Returns the grace period for project caches. pub fn project_grace_period(&self) -> Duration { - Duration::from_secs(self.values.cache.project_grace_period.into()) + Duration::from_secs(self.inner.values.cache.project_grace_period.into()) } /// Returns the refresh interval for a project. /// /// Validates the refresh time to be between the grace period and expiry. pub fn project_refresh_interval(&self) -> Option { - self.values + self.inner + .values .cache .project_refresh_interval .map(Into::into) @@ -2304,23 +2383,29 @@ impl Config { /// Returns the duration in which batchable project config queries are /// collected before sending them in a single request. pub fn query_batch_interval(&self) -> Duration { - Duration::from_millis(self.values.cache.batch_interval.into()) + Duration::from_millis(self.inner.values.cache.batch_interval.into()) } /// Returns the duration in which downstream relays are requested from upstream. pub fn downstream_relays_batch_interval(&self) -> Duration { - Duration::from_millis(self.values.cache.downstream_relays_batch_interval.into()) + Duration::from_millis( + self.inner + .values + .cache + .downstream_relays_batch_interval + .into(), + ) } /// Returns the interval in seconds in which local project configurations should be reloaded. pub fn local_cache_interval(&self) -> Duration { - Duration::from_secs(self.values.cache.file_interval.into()) + Duration::from_secs(self.inner.values.cache.file_interval.into()) } /// Returns the interval in seconds in which fresh global configs should be /// fetched from upstream. pub fn global_config_fetch_interval(&self) -> Duration { - Duration::from_secs(self.values.cache.global_config_fetch_interval.into()) + Duration::from_secs(self.inner.values.cache.global_config_fetch_interval.into()) } /// Returns the path of the buffer file if the `cache.persistent_envelope_buffer.path` is configured. @@ -2329,6 +2414,7 @@ impl Config { /// suffixed with `.{partition_id}`. pub fn spool_envelopes_path(&self, partition_id: u8) -> Option { let mut path = self + .inner .values .spool .envelopes @@ -2349,166 +2435,193 @@ impl Config { /// The maximum size of the buffer, in bytes. pub fn spool_envelopes_max_disk_size(&self) -> usize { - self.values.spool.envelopes.max_disk_size.as_bytes() + self.inner.values.spool.envelopes.max_disk_size.as_bytes() } /// Number of encoded envelope bytes that need to be accumulated before /// flushing one batch to disk. pub fn spool_envelopes_batch_size_bytes(&self) -> usize { - self.values.spool.envelopes.batch_size_bytes.as_bytes() + self.inner + .values + .spool + .envelopes + .batch_size_bytes + .as_bytes() } /// Returns the time after which we drop envelopes as a [`Duration`] object. pub fn spool_envelopes_max_age(&self) -> Duration { - Duration::from_secs(self.values.spool.envelopes.max_envelope_delay_secs) + Duration::from_secs(self.inner.values.spool.envelopes.max_envelope_delay_secs) } /// Returns the refresh frequency for disk usage monitoring as a [`Duration`] object. pub fn spool_disk_usage_refresh_frequency_ms(&self) -> Duration { - Duration::from_millis(self.values.spool.envelopes.disk_usage_refresh_frequency_ms) + Duration::from_millis( + self.inner + .values + .spool + .envelopes + .disk_usage_refresh_frequency_ms, + ) } /// Returns the relative memory usage up to which the disk buffer will unspool envelopes. pub fn spool_max_backpressure_memory_percent(&self) -> f32 { - self.values.spool.envelopes.max_backpressure_memory_percent + self.inner + .values + .spool + .envelopes + .max_backpressure_memory_percent } /// Returns the number of partitions for the buffer. pub fn spool_partitions(&self) -> NonZeroU8 { - self.values.spool.envelopes.partitions + self.inner.values.spool.envelopes.partitions } /// Returns the strategy used to assign envelopes to buffer partitions. pub fn spool_partitioning(&self) -> EnvelopeSpoolPartitioning { - self.values.spool.envelopes.partitioning + self.inner.values.spool.envelopes.partitioning } /// Returns `true` if the data is stored on ephemeral disks. pub fn spool_ephemeral(&self) -> bool { - self.values.spool.envelopes.ephemeral + self.inner.values.spool.envelopes.ephemeral } /// Returns the maximum size of an event payload in bytes. pub fn max_event_size(&self) -> usize { - self.values.limits.max_event_size.as_bytes() + self.inner.values.limits.max_event_size.as_bytes() } /// Returns the maximum size of each attachment. pub fn max_attachment_size(&self) -> usize { - self.values.limits.max_attachment_size.as_bytes() + self.inner.values.limits.max_attachment_size.as_bytes() } /// The maximum amount of attachments in a single envelope. pub fn max_attachment_count(&self) -> usize { - self.values.limits.max_attachment_count + self.inner.values.limits.max_attachment_count } /// Returns the maximum combined size of attachments or payloads containing attachments /// (minidump, unreal, standalone attachments) in bytes. pub fn max_attachments_size(&self) -> usize { - self.values.limits.max_attachments_size.as_bytes() + self.inner.values.limits.max_attachments_size.as_bytes() } /// Returns the maximum size of a TUS upload request body. pub fn max_upload_size(&self) -> usize { - self.values.limits.max_upload_size.as_bytes() + self.inner.values.limits.max_upload_size.as_bytes() } /// Returns the maximum number of client reports per envelope. pub fn max_client_reports_count(&self) -> usize { - self.values.limits.max_client_reports_count + self.inner.values.limits.max_client_reports_count } /// Returns the maximum combined size of client reports in bytes. pub fn max_client_reports_size(&self) -> usize { - self.values.limits.max_client_reports_size.as_bytes() + self.inner.values.limits.max_client_reports_size.as_bytes() } /// Returns the maximum payload size of a monitor check-in in bytes. pub fn max_check_in_size(&self) -> usize { - self.values.limits.max_check_in_size.as_bytes() + self.inner.values.limits.max_check_in_size.as_bytes() } /// Returns the maximum payload size of a log in bytes. pub fn max_log_size(&self) -> usize { - self.values.limits.max_log_size.as_bytes() + self.inner.values.limits.max_log_size.as_bytes() } /// Returns the maximum payload size of a span in bytes. pub fn max_span_size(&self) -> usize { - self.values.limits.max_span_size.as_bytes() + self.inner.values.limits.max_span_size.as_bytes() } /// Returns the maximum amount of standalone transaction spans per envelope. pub fn max_standalone_span_count(&self) -> usize { - self.values.limits.max_standalone_span_count + self.inner.values.limits.max_standalone_span_count } /// Returns the maximum payload size of an item container in bytes. pub fn max_container_size(&self) -> usize { - self.values.limits.max_container_size.as_bytes() + self.inner.values.limits.max_container_size.as_bytes() } /// Returns the maximum size of an envelope payload in bytes. /// /// Individual item size limits still apply. pub fn max_envelope_size(&self) -> usize { - self.values.limits.max_envelope_size.as_bytes() + self.inner.values.limits.max_envelope_size.as_bytes() } /// Returns the maximum number of sessions per envelope. pub fn max_session_count(&self) -> usize { - self.values.limits.max_session_count + self.inner.values.limits.max_session_count } /// Returns the maximum combined size for all sessions in an envelope in bytes. pub fn max_sessions_size(&self) -> usize { - self.values.limits.max_sessions_size.as_bytes() + self.inner.values.limits.max_sessions_size.as_bytes() } /// Returns the maximum payload size of a statsd metric in bytes. pub fn max_statsd_size(&self) -> usize { - self.values.limits.max_statsd_size.as_bytes() + self.inner.values.limits.max_statsd_size.as_bytes() } /// Returns the maximum payload size of metric buckets in bytes. pub fn max_metric_buckets_size(&self) -> usize { - self.values.limits.max_metric_buckets_size.as_bytes() + self.inner.values.limits.max_metric_buckets_size.as_bytes() } /// Returns the maximum payload size for general API requests. pub fn max_api_payload_size(&self) -> usize { - self.values.limits.max_api_payload_size.as_bytes() + self.inner.values.limits.max_api_payload_size.as_bytes() } /// Returns the maximum payload size for file uploads and chunks. pub fn max_api_file_upload_size(&self) -> usize { - self.values.limits.max_api_file_upload_size.as_bytes() + self.inner.values.limits.max_api_file_upload_size.as_bytes() } /// Returns the maximum payload size for chunks pub fn max_api_chunk_upload_size(&self) -> usize { - self.values.limits.max_api_chunk_upload_size.as_bytes() + self.inner + .values + .limits + .max_api_chunk_upload_size + .as_bytes() } /// Returns the maximum payload size for a profile pub fn max_profile_size(&self) -> usize { - self.values.limits.max_profile_size.as_bytes() + self.inner.values.limits.max_profile_size.as_bytes() } /// Returns the maximum payload size for a trace metric. pub fn max_trace_metric_size(&self) -> usize { - self.values.limits.max_trace_metric_size.as_bytes() + self.inner.values.limits.max_trace_metric_size.as_bytes() } /// Returns the maximum payload size for a compressed replay. pub fn max_replay_compressed_size(&self) -> usize { - self.values.limits.max_replay_compressed_size.as_bytes() + self.inner + .values + .limits + .max_replay_compressed_size + .as_bytes() } /// Returns the maximum payload size for an uncompressed replay. pub fn max_replay_uncompressed_size(&self) -> usize { - self.values.limits.max_replay_uncompressed_size.as_bytes() + self.inner + .values + .limits + .max_replay_uncompressed_size + .as_bytes() } /// Returns the maximum message size for an uncompressed replay. @@ -2517,106 +2630,110 @@ impl Config { /// it can include additional metadata about the replay in /// addition to the recording. pub fn max_replay_message_size(&self) -> usize { - self.values.limits.max_replay_message_size.as_bytes() + self.inner.values.limits.max_replay_message_size.as_bytes() } /// Returns the maximum number of active requests pub fn max_concurrent_requests(&self) -> usize { - self.values.limits.max_concurrent_requests + self.inner.values.limits.max_concurrent_requests } /// Returns the maximum number of active queries pub fn max_concurrent_queries(&self) -> usize { - self.values.limits.max_concurrent_queries + self.inner.values.limits.max_concurrent_queries } /// Returns the maximum combined size of keys of invalid attributes. pub fn max_removed_attribute_key_size(&self) -> usize { - self.values.limits.max_removed_attribute_key_size.as_bytes() + self.inner + .values + .limits + .max_removed_attribute_key_size + .as_bytes() } /// The maximum number of seconds a query is allowed to take across retries. pub fn query_timeout(&self) -> Duration { - Duration::from_secs(self.values.limits.query_timeout) + Duration::from_secs(self.inner.values.limits.query_timeout) } /// The maximum number of seconds to wait for pending envelopes after receiving a shutdown /// signal. pub fn shutdown_timeout(&self) -> Duration { - Duration::from_secs(self.values.limits.shutdown_timeout) + Duration::from_secs(self.inner.values.limits.shutdown_timeout) } /// Returns the server keep-alive timeout in seconds. /// /// By default keep alive is set to a 5 seconds. pub fn keepalive_timeout(&self) -> Duration { - Duration::from_secs(self.values.limits.keepalive_timeout) + Duration::from_secs(self.inner.values.limits.keepalive_timeout) } /// Returns the server idle timeout in seconds. pub fn idle_timeout(&self) -> Option { - self.values.limits.idle_timeout.map(Duration::from_secs) + self.inner + .values + .limits + .idle_timeout + .map(Duration::from_secs) } /// Returns the maximum connections. pub fn max_connections(&self) -> Option { - self.values.limits.max_connections + self.inner.values.limits.max_connections } /// TCP listen backlog to configure on Relay's listening socket. pub fn tcp_listen_backlog(&self) -> u32 { - self.values.limits.tcp_listen_backlog + self.inner.values.limits.tcp_listen_backlog } /// Returns the number of cores to use for thread pools. pub fn cpu_concurrency(&self) -> usize { - self.values.limits.max_thread_count + self.inner.values.limits.max_thread_count } /// Returns the number of tasks that can run concurrently in the worker pool. pub fn pool_concurrency(&self) -> usize { - self.values.limits.max_pool_concurrency + self.inner.values.limits.max_pool_concurrency } /// Returns the maximum size of a project config query. pub fn query_batch_size(&self) -> usize { - self.values.cache.batch_size - } - - /// Get filename for static project config. - pub fn project_configs_path(&self) -> PathBuf { - self.path.join("projects") + self.inner.values.cache.batch_size } /// True if the Relay should do processing. pub fn processing_enabled(&self) -> bool { - self.values.processing.enabled + self.inner.values.processing.enabled } /// Level of normalization for Relay to apply to incoming data. pub fn normalization_level(&self) -> NormalizationLevel { - self.values.normalization.level + self.inner.values.normalization.level } /// The path to the GeoIp database required for event processing. pub fn geoip_path(&self) -> Option<&Path> { - self.values - .geoip - .path - .as_deref() - .or(self.values.processing.geoip_path.as_deref()) + self.inner.values.geoip.path.as_deref().or(self + .inner + .values + .processing + .geoip_path + .as_deref()) } /// Maximum future timestamp of ingested data. /// /// Events past this timestamp will be adjusted to `now()`. Sessions will be dropped. pub fn max_secs_in_future(&self) -> i64 { - self.values.processing.max_secs_in_future.into() + self.inner.values.processing.max_secs_in_future.into() } /// Maximum age of ingested sessions. Older sessions will be dropped. pub fn max_session_secs_in_past(&self) -> i64 { - self.values.processing.max_session_secs_in_past.into() + self.inner.values.processing.max_session_secs_in_past.into() } /// Configuration name and list of Kafka configuration parameters for a given topic. @@ -2624,30 +2741,35 @@ impl Config { &self, topic: KafkaTopic, ) -> Result, KafkaConfigError> { - self.values.processing.topics.get(topic).kafka_configs( - &self.values.processing.kafka_config, - &self.values.processing.secondary_kafka_configs, - ) + self.inner + .values + .processing + .topics + .get(topic) + .kafka_configs( + &self.inner.values.processing.kafka_config, + &self.inner.values.processing.secondary_kafka_configs, + ) } /// Whether to validate the topics against Kafka. pub fn kafka_validate_topics(&self) -> bool { - self.values.processing.kafka_validate_topics + self.inner.values.processing.kafka_validate_topics } /// All unused but configured topic assignments. pub fn unused_topic_assignments(&self) -> &relay_kafka::Unused { - &self.values.processing.topics.unused + &self.inner.values.processing.topics.unused } /// Configuration of the objectstore service. pub fn objectstore(&self) -> &ObjectstoreServiceConfig { - &self.values.processing.objectstore + &self.inner.values.processing.objectstore } /// Configuration of the upload service. pub fn upload(&self) -> &Upload { - &self.values.upload + &self.inner.values.upload } /// Returns the key used to sign upload locations. @@ -2670,10 +2792,9 @@ impl Config { .or(self.credentials().map(|c| &c.public_key)) } - /// Redis servers to connect to for project configs, cardinality limits, - /// rate limiting, and metrics metadata. + /// Redis servers to connect to for project configs, rate limiting, and metrics metadata. pub fn redis(&self) -> Option> { - let redis_configs = self.values.processing.redis.as_ref()?; + let redis_configs = self.inner.values.processing.redis.as_ref()?; Some(build_redis_configs( redis_configs, @@ -2684,50 +2805,48 @@ impl Config { /// Chunk size of attachments in bytes. pub fn attachment_chunk_size(&self) -> usize { - self.values.processing.attachment_chunk_size.as_bytes() + self.inner + .values + .processing + .attachment_chunk_size + .as_bytes() } /// Maximum metrics batch size in bytes. pub fn metrics_max_batch_size_bytes(&self) -> usize { - self.values.aggregator.max_flush_bytes + self.inner.values.aggregator.max_flush_bytes } /// Default prefix to use when looking up project configs in Redis. This is only done when /// Relay is in processing mode. pub fn projectconfig_cache_prefix(&self) -> &str { - &self.values.processing.projectconfig_cache_prefix + &self.inner.values.processing.projectconfig_cache_prefix } /// Maximum rate limit to report to clients in seconds. pub fn max_rate_limit(&self) -> Option { - self.values.processing.max_rate_limit.map(u32::into) + self.inner.values.processing.max_rate_limit.map(u32::into) } /// Amount of remaining quota which is cached in memory. pub fn quota_cache_ratio(&self) -> Option { - self.values.processing.quota_cache_ratio + self.inner.values.processing.quota_cache_ratio } /// Maximum limit (ratio) for the in memory quota cache. pub fn quota_cache_max(&self) -> Option { - self.values.processing.quota_cache_max - } - - /// Cache vacuum interval for the cardinality limiter in memory cache. - /// - /// The cache will scan for expired values based on this interval. - pub fn cardinality_limiter_cache_vacuum_interval(&self) -> Duration { - Duration::from_secs(self.values.cardinality_limiter.cache_vacuum_interval) + self.inner.values.processing.quota_cache_max } /// Interval to refresh internal health checks. pub fn health_refresh_interval(&self) -> Duration { - Duration::from_millis(self.values.health.refresh_interval_ms) + Duration::from_millis(self.inner.values.health.refresh_interval_ms) } /// Maximum memory watermark in bytes. pub fn health_max_memory_watermark_bytes(&self) -> u64 { - self.values + self.inner + .values .health .max_memory_bytes .as_ref() @@ -2736,73 +2855,72 @@ impl Config { /// Maximum memory watermark as a percentage of maximum system memory. pub fn health_max_memory_watermark_percent(&self) -> f32 { - self.values.health.max_memory_percent + self.inner.values.health.max_memory_percent } /// Health check probe timeout. pub fn health_probe_timeout(&self) -> Duration { - Duration::from_millis(self.values.health.probe_timeout_ms) + Duration::from_millis(self.inner.values.health.probe_timeout_ms) } /// Refresh frequency for polling new memory stats. pub fn memory_stat_refresh_frequency_ms(&self) -> u64 { - self.values.health.memory_stat_refresh_frequency_ms + self.inner.values.health.memory_stat_refresh_frequency_ms } /// Maximum amount of COGS measurements buffered in memory. pub fn cogs_max_queue_size(&self) -> u64 { - self.values.cogs.max_queue_size + self.inner.values.cogs.max_queue_size } /// Resource ID to use for Relay COGS measurements. pub fn cogs_relay_resource_id(&self) -> &str { - &self.values.cogs.relay_resource_id + &self.inner.values.cogs.relay_resource_id } /// Returns configuration for the default metrics aggregator. pub fn default_aggregator_config(&self) -> &AggregatorServiceConfig { - &self.values.aggregator + &self.inner.values.aggregator } /// Returns configuration for non-default metrics aggregator. pub fn secondary_aggregator_configs(&self) -> &Vec { - &self.values.secondary_aggregators + &self.inner.values.secondary_aggregators } /// Returns aggregator config for a given metrics namespace. pub fn aggregator_config_for(&self, namespace: MetricNamespace) -> &AggregatorServiceConfig { - for entry in &self.values.secondary_aggregators { + for entry in &self.inner.values.secondary_aggregators { if entry.condition.matches(Some(namespace)) { return &entry.config; } } - &self.values.aggregator + &self.inner.values.aggregator } /// Return the statically configured Relays. pub fn static_relays(&self) -> &HashMap { - &self.values.auth.static_relays + &self.inner.values.auth.static_relays } /// Returns the max age a signature is considered valid, in seconds. pub fn signature_max_age(&self) -> Duration { - Duration::from_secs(self.values.auth.signature_max_age) + Duration::from_secs(self.inner.values.auth.signature_max_age) } /// Returns `true` if unknown items should be accepted and forwarded. pub fn accept_unknown_items(&self) -> bool { - let forward = self.values.routing.accept_unknown_items; + let forward = self.inner.values.routing.accept_unknown_items; forward.unwrap_or_else(|| !self.processing_enabled()) } } -impl Default for Config { - fn default() -> Self { - Self { - values: ConfigValues::default(), - credentials: None, - path: PathBuf::new(), - } +impl fmt::Debug for ConfigSnapshot { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ConfigSnapshot") + .field("values", &self.inner.values) + .field("source_files", &self.inner.source_files) + .finish() } } @@ -2837,14 +2955,14 @@ cache: fs::write( ConfigValues::path(&path), r#" -upload: - credentials: - signing_key: ${file:my_secret.txt} - verification_key: "VNS8haF0VTnuMMDR2t-f7AgnmUcXmcdzV3SVksSk34s""#, + upload: + credentials: + signing_key: ${file:my_secret.txt} + verification_key: "VNS8haF0VTnuMMDR2t-f7AgnmUcXmcdzV3SVksSk34s""#, ) .unwrap(); - let config = Config::from_path(&path).unwrap(); + let config = Config::from_path(&path).unwrap().current(); fs::remove_dir_all(path).unwrap(); diff --git a/relay-config/src/lib.rs b/relay-config/src/lib.rs index 91dad374f08..d837e8935ea 100644 --- a/relay-config/src/lib.rs +++ b/relay-config/src/lib.rs @@ -10,7 +10,9 @@ pub mod aggregator; mod byte_size; mod config; mod redis; +mod source; mod upstream; +mod utils; pub use crate::aggregator::{AggregatorServiceConfig, ScopedAggregatorConfig}; pub use crate::byte_size::*; diff --git a/relay-config/src/source.rs b/relay-config/src/source.rs new file mode 100644 index 00000000000..df9fd30975c --- /dev/null +++ b/relay-config/src/source.rs @@ -0,0 +1,19 @@ +use std::collections::BTreeSet; +use std::path::PathBuf; + +/// A custom [`serde_vars::source::FileSystem`], which loads files from the file system, but also +/// keeps track of the files read. +#[derive(Debug)] +pub struct TrackingFileSystem<'a>(pub &'a mut BTreeSet); + +impl<'a> serde_vars::source::FileSystem for TrackingFileSystem<'a> { + fn read(&mut self, path: &std::path::Path) -> std::io::Result> { + self.0.insert(path.to_owned()); + std::fs::read(path) + } + + fn read_to_string(&mut self, path: &std::path::Path) -> std::io::Result { + self.0.insert(path.to_owned()); + std::fs::read_to_string(path) + } +} diff --git a/relay-config/src/utils.rs b/relay-config/src/utils.rs new file mode 100644 index 00000000000..9364775c1d6 --- /dev/null +++ b/relay-config/src/utils.rs @@ -0,0 +1,51 @@ +use std::sync::Arc; + +use arc_swap::ArcSwap; + +/// Fallible Read-Copy-Update of a value contained in an [`ArcSwap`]. +/// +/// See also: [`ArcSwap::rcu`]. +pub fn try_rcu(swap: &ArcSwap, mut f: F) -> Result<(), E> +where + F: FnMut(&Arc) -> Result, E>, +{ + let mut cur = swap.load_full(); + loop { + let new = f(&cur)?; + let prev = swap.compare_and_swap(&cur, new); + if Arc::ptr_eq(&cur, &prev) { + return Ok(()); + } + // Someone else updated the value before us, retry with the latest version. + cur = Arc::clone(&prev); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_try_rcu_replaces_value() { + let swap = ArcSwap::from_pointee(1); + let old = swap.load_full(); + + try_rcu(&swap, |value| Ok::<_, ()>(Arc::new(**value + 1))).unwrap(); + + let new = swap.load_full(); + assert_eq!(*new, 2); + assert!(!Arc::ptr_eq(&old, &new)); + } + + #[test] + fn test_try_rcu_keeps_unchanged_value() { + let swap = ArcSwap::from_pointee(1); + let old = swap.load_full(); + + try_rcu(&swap, |value| Ok::<_, ()>(Arc::clone(value))).unwrap(); + + let new = swap.load_full(); + assert_eq!(*new, 1); + assert!(Arc::ptr_eq(&old, &new)); + } +} diff --git a/relay-kafka/src/config.rs b/relay-kafka/src/config.rs index f404340d202..8ba0ffc3dc2 100644 --- a/relay-kafka/src/config.rs +++ b/relay-kafka/src/config.rs @@ -74,7 +74,7 @@ impl KafkaTopic { macro_rules! define_topic_assignments { ($($field_name:ident : ($kafka_topic:path, $default_topic:literal, $doc:literal)),* $(,)?) => { /// Configuration for topics. - #[derive(Deserialize, Serialize, Debug)] + #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(default)] pub struct TopicAssignments { $( @@ -144,7 +144,7 @@ define_topic_assignments! { } /// A list of all currently, by this Relay, unused topic configurations. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct Unused(Vec); impl Unused { @@ -171,7 +171,7 @@ impl<'de> de::Deserialize<'de> for Unused { /// custom kafka cluster, or an array of topic names/configs for sharded topics. /// /// See documentation for `secondary_kafka_configs` for more information. -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Clone)] pub struct TopicAssignment(Vec); impl<'de> de::Deserialize<'de> for TopicAssignment { @@ -207,7 +207,7 @@ impl<'de> de::Deserialize<'de> for TopicAssignment { } /// Configuration for topic -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct TopicConfig { /// The topic name to use. #[serde(rename = "name")] @@ -310,7 +310,7 @@ impl TopicAssignment { } /// A name value pair of Kafka config parameter. -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct KafkaConfigParam { /// Name of the Kafka config parameter. pub name: String, diff --git a/relay-server/benches/benches.rs b/relay-server/benches/benches.rs index 1c73b18ac32..cf651ecc972 100644 --- a/relay-server/benches/benches.rs +++ b/relay-server/benches/benches.rs @@ -234,6 +234,7 @@ fn benchmark_envelope_buffer(c: &mut Criterion) { })) .unwrap() .into(); + let current_config = config.current(); let memory_checker = MemoryChecker::new(MemoryStat::default(), config.clone()); group.throughput(Throughput::Elements( @@ -260,10 +261,13 @@ fn benchmark_envelope_buffer(c: &mut Criterion) { }, |envelopes| { runtime.block_on(async { - let mut buffer = - PolymorphicEnvelopeBuffer::from_config(0, &config, memory_checker.clone()) - .await - .unwrap(); + let mut buffer = PolymorphicEnvelopeBuffer::from_config( + 0, + ¤t_config, + memory_checker.clone(), + ) + .await + .unwrap(); for envelope in envelopes.into_iter() { buffer.push(envelope).await.unwrap(); } @@ -292,10 +296,13 @@ fn benchmark_envelope_buffer(c: &mut Criterion) { }, |envelopes| { runtime.block_on(async { - let mut buffer = - PolymorphicEnvelopeBuffer::from_config(0, &config, memory_checker.clone()) - .await - .unwrap(); + let mut buffer = PolymorphicEnvelopeBuffer::from_config( + 0, + ¤t_config, + memory_checker.clone(), + ) + .await + .unwrap(); let n = envelopes.len(); for envelope in envelopes.into_iter() { let public_key = envelope.meta().public_key(); diff --git a/relay-server/src/endpoints/attachments.rs b/relay-server/src/endpoints/attachments.rs index a740f297e69..e63e10eab9b 100644 --- a/relay-server/src/endpoints/attachments.rs +++ b/relay-server/src/endpoints/attachments.rs @@ -3,7 +3,7 @@ use axum::http::StatusCode; use axum::response::IntoResponse; use axum::routing::{MethodRouter, post}; use multer::{Field, Multipart}; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_event_schema::protocol::EventId; use relay_quotas::DataCategory; use serde::Deserialize; @@ -32,7 +32,7 @@ impl AttachmentStrategy for AttachmentsAttachmentStrategy { &self, field: Field<'static>, item: Managed, - config: &Config, + config: &ConfigSnapshot, ) -> Result>, BadStoreRequest> { Ok(Some(read_field_into_item(field, item, config).await?)) } @@ -46,7 +46,7 @@ async fn multipart_to_envelope( ) -> Result>, BadStoreRequest> { let items = utils::multipart_items( multipart, - state.config(), + &state.config(), AttachmentsAttachmentStrategy, &meta, state.outcome_aggregator(), @@ -76,7 +76,7 @@ pub async fn handle( Ok(StatusCode::CREATED) } -pub fn route(config: &Config) -> MethodRouter { +pub fn route(config: &ConfigSnapshot) -> MethodRouter { post(handle) .route_layer(RequestBodyLimitLayer::new(config.max_attachments_size())) .route_layer(DefaultBodyLimit::disable()) diff --git a/relay-server/src/endpoints/common.rs b/relay-server/src/endpoints/common.rs index 217072c5d55..be64e60d029 100644 --- a/relay-server/src/endpoints/common.rs +++ b/relay-server/src/endpoints/common.rs @@ -8,7 +8,7 @@ use bytes::Bytes; use chrono::Utc; use futures::TryStreamExt; use futures::stream::BoxStream; -use relay_config::{Config, RelayMode}; +use relay_config::{ConfigSnapshot, RelayMode}; use relay_event_schema::protocol::{EventId, EventType}; use relay_quotas::{DataCategory, RateLimits}; use relay_statsd::metric; @@ -437,10 +437,12 @@ pub async fn handle_managed_envelope( ))); }; + let config = state.config(); + // If configured, remove unknown items at the very beginning. If the envelope is // empty, we fail the request with a special control flow error to skip checks and // queueing, that still results in a `200 OK` response. - utils::remove_unknown_items(state.config(), &mut envelope); + utils::remove_unknown_items(&config, &mut envelope); let event_id = envelope.event_id(); if envelope.is_empty() { @@ -466,7 +468,7 @@ pub async fn handle_managed_envelope( }); } - if let Err(offender) = utils::check_envelope_size_limits(state.config(), &envelope) { + if let Err(offender) = utils::check_envelope_size_limits(&config, &envelope) { return Err(envelope.reject_err(( Outcome::Invalid(DiscardReason::ItemTooLarge(offender)), BadStoreRequest::ItemTooLarge(offender), @@ -548,7 +550,7 @@ pub async fn upload_stream( stream: S, content_type: Option, mut item: Managed, - config: &Config, + config: &ConfigSnapshot, project: ProjectContext, upload: &Addr, referrer: &'static str, @@ -577,7 +579,7 @@ async fn upload_stream_inner( stream: S, content_type: Option, item: &mut Managed, - config: &Config, + config: &ConfigSnapshot, project: ProjectContext, upload: &Addr, referrer: &'static str, diff --git a/relay-server/src/endpoints/envelope.rs b/relay-server/src/endpoints/envelope.rs index f00bc4d02d0..f2a35054a6c 100644 --- a/relay-server/src/endpoints/envelope.rs +++ b/relay-server/src/endpoints/envelope.rs @@ -8,7 +8,7 @@ use axum::response::IntoResponse; use axum::routing::{MethodRouter, post}; use axum::{Json, RequestExt}; use bytes::Bytes; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_event_schema::protocol::EventId; use serde::Serialize; @@ -124,6 +124,6 @@ async fn handle( Ok(Json(StoreResponse { id })) } -pub fn route(config: &Config) -> MethodRouter { +pub fn route(config: &ConfigSnapshot) -> MethodRouter { post(handle).route_layer(DefaultBodyLimit::max(config.max_envelope_size())) } diff --git a/relay-server/src/endpoints/forward.rs b/relay-server/src/endpoints/forward.rs index 4963ba1b294..0e5aa71d0d1 100644 --- a/relay-server/src/endpoints/forward.rs +++ b/relay-server/src/endpoints/forward.rs @@ -12,7 +12,7 @@ use axum::handler::Handler; use axum::http::{HeaderMap, HeaderValue, StatusCode, Uri}; use axum::response::{IntoResponse, Response}; use relay_common::glob2::GlobMatcher; -use relay_config::Config; +use relay_config::ConfigSnapshot; use tower_http::limit::RequestBodyLimitLayer; use crate::extractors::ForwardedFor; @@ -32,7 +32,9 @@ async fn handle( headers: HeaderMap, data: Body, ) -> impl IntoResponse { - if !state.config().http_forward() { + let config = state.config(); + + if !config.http_forward() { return StatusCode::NOT_FOUND.into_response(); } @@ -47,7 +49,7 @@ async fn handle( .with_headers(headers) .with_forwarded_for(forwarded_for) .with_body(data) - .with_config(state.config()) + .with_config(&config) .send_to(state.upstream_relay()) .await .into_response() @@ -81,7 +83,7 @@ static SPECIAL_ROUTES: LazyLock> = LazyLock::new(|| { }); /// Returns the maximum request body size for a route path. -fn get_limit_for_path(path: &str, config: &Config) -> usize { +fn get_limit_for_path(path: &str, config: &ConfigSnapshot) -> usize { match SPECIAL_ROUTES.test(path) { Some(SpecialRoute::FileUpload) => config.max_api_file_upload_size(), Some(SpecialRoute::ChunkUpload) => config.max_api_chunk_upload_size(), @@ -102,7 +104,7 @@ fn get_limit_for_path(path: &str, config: &Config) -> usize { /// - Use it as [`Handler`] directly in router methods when registering this as a route. /// - Call this manually from other request handlers to conditionally forward from other endpoints. pub fn forward(state: ServiceState, req: Request) -> impl Future { - let limit = get_limit_for_path(req.uri().path(), state.config()); + let limit = get_limit_for_path(req.uri().path(), &state.config()); handle // `RequestBodyLimitLayer` checks the stream, DefaultBodyLimit does not. .layer(RequestBodyLimitLayer::new(limit)) diff --git a/relay-server/src/endpoints/integrations/otlp.rs b/relay-server/src/endpoints/integrations/otlp.rs index c30073de393..b16a60316ea 100644 --- a/relay-server/src/endpoints/integrations/otlp.rs +++ b/relay-server/src/endpoints/integrations/otlp.rs @@ -4,7 +4,7 @@ use axum::response::{IntoResponse, Response}; use axum::routing::{MethodRouter, post}; use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceResponse; use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceResponse; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_dynamic_config::Feature; use crate::endpoints::common; @@ -20,7 +20,7 @@ use crate::service::ServiceState; /// The integration currently supports the following endpoints: /// - V1 Traces /// - V1 Logs -pub fn routes(config: &Config) -> axum::Router { +pub fn routes(config: &ConfigSnapshot) -> axum::Router { axum::Router::new() .route("/v1/traces", traces::route(config)) .route("/v1/traces/", traces::route(config)) @@ -56,7 +56,7 @@ mod traces { Ok(SuccessResponse::new(format)) } - pub fn route(config: &Config) -> MethodRouter { + pub fn route(config: &ConfigSnapshot) -> MethodRouter { post(handle).route_layer(DefaultBodyLimit::max(config.max_container_size())) } } @@ -90,7 +90,7 @@ mod logs { Ok(SuccessResponse::new(format)) } - pub fn route(config: &Config) -> MethodRouter { + pub fn route(config: &ConfigSnapshot) -> MethodRouter { post(handle).route_layer(DefaultBodyLimit::max(config.max_container_size())) } } diff --git a/relay-server/src/endpoints/integrations/vercel.rs b/relay-server/src/endpoints/integrations/vercel.rs index 686daaa90c9..3dfeb0ad551 100644 --- a/relay-server/src/endpoints/integrations/vercel.rs +++ b/relay-server/src/endpoints/integrations/vercel.rs @@ -2,7 +2,7 @@ use axum::extract::DefaultBodyLimit; use axum::http::StatusCode; use axum::response::IntoResponse; use axum::routing::{MethodRouter, post}; -use relay_config::Config; +use relay_config::ConfigSnapshot; use crate::endpoints::common; use crate::envelope::ContentType; @@ -14,7 +14,7 @@ use crate::service::ServiceState; /// /// The integration currently supports the following endpoints: /// - Vercel Log Drain -pub fn routes(config: &Config) -> axum::Router { +pub fn routes(config: &ConfigSnapshot) -> axum::Router { axum::Router::new() .route("/logs", logs::route(config)) .route("/logs/", logs::route(config)) @@ -45,7 +45,7 @@ mod logs { Ok(StatusCode::ACCEPTED) } - pub fn route(config: &Config) -> MethodRouter { + pub fn route(config: &ConfigSnapshot) -> MethodRouter { post(handle).route_layer(DefaultBodyLimit::max(config.max_container_size())) } } diff --git a/relay-server/src/endpoints/minidump.rs b/relay-server/src/endpoints/minidump.rs index d59997d47a5..0d19c3927ee 100644 --- a/relay-server/src/endpoints/minidump.rs +++ b/relay-server/src/endpoints/minidump.rs @@ -7,7 +7,7 @@ use flate2::read::GzDecoder; use futures::{self, Stream, StreamExt, TryStreamExt}; use liblzma::read::XzDecoder; use multer::{Field, Multipart}; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_dynamic_config::Feature; use relay_event_schema::protocol::EventId; use relay_quotas::{DataCategory, RateLimits}; @@ -301,7 +301,7 @@ impl<'a> AttachmentStrategy for MinidumpAttachmentStrategy<'a> { &self, field: Field<'static>, item: Managed, - config: &Config, + config: &ConfigSnapshot, ) -> Result>, BadStoreRequest> { let read_inline = async |field: Field<'static>, item: Managed| { let is_minidump = matches!(item.attachment_type(), Some(AttachmentType::Minidump)); @@ -383,7 +383,7 @@ pub async fn upload_stream_checked( stream: S, content_type: Option, mut item: Managed, - config: &Config, + config: &ConfigSnapshot, project: ProjectContext, upload: &Addr, referrer: &'static str, @@ -447,7 +447,7 @@ async fn multipart_to_items( let mut items = utils::multipart_items( multipart, - config, + &config, minidump_attachment_strategy, meta, state.outcome_aggregator(), @@ -601,7 +601,7 @@ async fn raw_minidump_to_item( stream, Some(ContentType::Minidump.to_string()), item, - state.config(), + &state.config(), upload_context.project, upload_context.upload, "minidump", @@ -724,7 +724,7 @@ async fn handle( Ok(TextResponse(id)) } -pub fn route(config: &Config) -> MethodRouter { +pub fn route(config: &ConfigSnapshot) -> MethodRouter { post(handle) .route_layer(RequestBodyLimitLayer::new( config.max_upload_size() + config.max_attachments_size(), @@ -952,6 +952,7 @@ mod tests { .body(Body::from(multipart_body)).unwrap(); let config = Config::default(); + let config = config.current(); let request_meta = RequestMeta::new( "https://a94ae32be2582e0bbd7a4cbb95971fee:@sentry.io/42" diff --git a/relay-server/src/endpoints/mod.rs b/relay-server/src/endpoints/mod.rs index 012b0aa438a..45be990e59d 100644 --- a/relay-server/src/endpoints/mod.rs +++ b/relay-server/src/endpoints/mod.rs @@ -31,7 +31,7 @@ mod upload; use axum::extract::DefaultBodyLimit; use axum::routing::{Router, any, get, post}; -use relay_config::Config; +use relay_config::ConfigSnapshot; use crate::middlewares; use crate::service::ServiceState; @@ -43,7 +43,7 @@ const BATCH_JSON_BODY_LIMIT: usize = 50_000_000; // 50 MB /// All of Relay's routes. /// /// This includes [`public_routes`] as well as [`internal_routes`]. -pub fn all_routes(config: &Config) -> Router { +pub fn all_routes(config: &ConfigSnapshot) -> Router { public_routes_raw(config).merge(internal_routes(config)) } @@ -51,7 +51,7 @@ pub fn all_routes(config: &Config) -> Router { /// /// Routes which do not need to be exposed. #[rustfmt::skip] -pub fn internal_routes(_: &Config) -> Router{ +pub fn internal_routes(_: &ConfigSnapshot) -> Router{ Router::new() .route("/api/relay/healthcheck/{kind}/", get(health_check::handle)) .route("/api/relay/autoscaling/", get(autoscaling::handle)) @@ -62,13 +62,13 @@ pub fn internal_routes(_: &Config) -> Router{ /// Relay's public routes. /// /// Routes which are public API and must be exposed. -pub fn public_routes(config: &Config) -> Router { +pub fn public_routes(config: &ConfigSnapshot) -> Router { // Exclude internal routes, they must be configured separately. public_routes_raw(config).route("/api/relay/{*not_found}", any(statics::not_found)) } #[rustfmt::skip] -fn public_routes_raw(config: &Config) -> Router { +fn public_routes_raw(config: &ConfigSnapshot) -> Router { // Sentry Web API routes pointing to /api/0/relays/ let web_routes = Router::new() .route("/api/0/relays/projectconfigs/", post(project_configs::handle)) diff --git a/relay-server/src/endpoints/monitor.rs b/relay-server/src/endpoints/monitor.rs index c3f2a2c6809..c883df85245 100644 --- a/relay-server/src/endpoints/monitor.rs +++ b/relay-server/src/endpoints/monitor.rs @@ -4,7 +4,7 @@ use axum::http::StatusCode; use axum::response::IntoResponse; use axum::routing::{MethodFilter, MethodRouter, on}; use axum::{Json, RequestExt}; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_event_schema::protocol::EventId; use relay_monitors::{CheckIn, CheckInStatus}; use serde::Deserialize; @@ -74,7 +74,7 @@ async fn handle( Ok(StatusCode::ACCEPTED) } -pub fn route(config: &Config) -> MethodRouter { +pub fn route(config: &ConfigSnapshot) -> MethodRouter { on(MethodFilter::GET.or(MethodFilter::POST), handle) .route_layer(DefaultBodyLimit::max(config.max_event_size())) } diff --git a/relay-server/src/endpoints/nel.rs b/relay-server/src/endpoints/nel.rs index 81a9fb67439..bfa7ee511f9 100644 --- a/relay-server/src/endpoints/nel.rs +++ b/relay-server/src/endpoints/nel.rs @@ -4,7 +4,7 @@ use axum::extract::DefaultBodyLimit; use axum::http::StatusCode; use axum::response::IntoResponse; use axum::routing::{MethodRouter, post}; -use relay_config::Config; +use relay_config::ConfigSnapshot; use crate::endpoints::common; use crate::extractors::{IntegrationBuilder, Mime}; @@ -41,6 +41,6 @@ async fn handle( Ok(StatusCode::OK) } -pub fn route(config: &Config) -> MethodRouter { +pub fn route(config: &ConfigSnapshot) -> MethodRouter { post(handle).route_layer(DefaultBodyLimit::max(config.max_container_size())) } diff --git a/relay-server/src/endpoints/playstation.rs b/relay-server/src/endpoints/playstation.rs index 363de6b3782..57f4b5e604f 100644 --- a/relay-server/src/endpoints/playstation.rs +++ b/relay-server/src/endpoints/playstation.rs @@ -5,7 +5,7 @@ use axum::extract::{DefaultBodyLimit, Request}; use axum::response::IntoResponse; use axum::routing::{MethodRouter, post}; use multer::{Field, Multipart}; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_dynamic_config::Feature; use relay_quotas::DataCategory; use relay_system::Addr; @@ -140,7 +140,7 @@ impl<'a> AttachmentStrategy for PlaystationAttachmentStrategy<'a> { &self, field: Field<'static>, item: Managed, - config: &Config, + config: &ConfigSnapshot, ) -> Result>, BadStoreRequest> { match &self.upload_context { Some(upload_context) if self.infer_type(&field) != AttachmentType::Prosperodump => { @@ -200,7 +200,7 @@ async fn multipart_to_items( ) -> Result, BadStoreRequest> { let mut items = utils::multipart_items( multipart, - state.config(), + &state.config(), PlaystationAttachmentStrategy { upload_context }, meta, state.outcome_aggregator(), @@ -260,7 +260,7 @@ async fn handle( Ok(TextResponse(id).into_response()) } -pub fn route(config: &Config) -> MethodRouter { +pub fn route(config: &ConfigSnapshot) -> MethodRouter { post(handle) .route_layer(RequestBodyLimitLayer::new( config.max_upload_size() + config.max_attachments_size(), diff --git a/relay-server/src/endpoints/register.rs b/relay-server/src/endpoints/register.rs index a2c214f487f..f7d134dfe48 100644 --- a/relay-server/src/endpoints/register.rs +++ b/relay-server/src/endpoints/register.rs @@ -13,7 +13,7 @@ pub async fn challenge(state: ServiceState, headers: HeaderMap, body: Bytes) -> .with_upstream(None) .with_headers(headers) .with_body(body) - .with_config(state.config()) + .with_config(&state.config()) .send_to(state.upstream_relay()) .await } @@ -25,7 +25,7 @@ pub async fn response(state: ServiceState, headers: HeaderMap, body: Bytes) -> i .with_upstream(None) .with_headers(headers) .with_body(body) - .with_config(state.config()) + .with_config(&state.config()) .send_to(state.upstream_relay()) .await } diff --git a/relay-server/src/endpoints/security_report.rs b/relay-server/src/endpoints/security_report.rs index 73028f35161..131f5283ab1 100644 --- a/relay-server/src/endpoints/security_report.rs +++ b/relay-server/src/endpoints/security_report.rs @@ -6,7 +6,7 @@ use axum::response::IntoResponse; use axum::routing::{MethodRouter, post}; use bytes::Bytes; use itertools::Either; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_event_schema::protocol::EventId; use serde::Deserialize; use serde_json::value::RawValue; @@ -106,6 +106,6 @@ async fn handle( Ok(().into_response()) } -pub fn route(config: &Config) -> MethodRouter { +pub fn route(config: &ConfigSnapshot) -> MethodRouter { post(handle).route_layer(DefaultBodyLimit::max(config.max_event_size())) } diff --git a/relay-server/src/endpoints/store.rs b/relay-server/src/endpoints/store.rs index cc712e4129d..40009a72fd4 100644 --- a/relay-server/src/endpoints/store.rs +++ b/relay-server/src/endpoints/store.rs @@ -9,7 +9,7 @@ use axum::routing::{MethodRouter, post}; use bytes::Bytes; use data_encoding::BASE64; use flate2::bufread::ZlibDecoder; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_event_schema::protocol::EventId; use serde::{Deserialize, Serialize}; @@ -52,7 +52,7 @@ fn decode_bytes(body: Bytes, limit: usize) -> Result { fn parse_event( mut body: Bytes, meta: RequestMeta, - config: &Config, + config: &ConfigSnapshot, ) -> Result, BadStoreRequest> { // The body may be zlib compressed and encoded as base64. Decode it transparently if this is the // case. @@ -113,7 +113,7 @@ async fn handle_post( envelope::CONTENT_TYPE => { Envelope::parse_request(body, meta).map_err(BadStoreRequest::InvalidEnvelope)? } - _ => parse_event(body, meta, state.config())?, + _ => parse_event(body, meta, &state.config())?, }; if envelope.is_internal() { return Err(BadStoreRequest::InternalEnvelope.into()); @@ -146,13 +146,13 @@ async fn handle_get( meta: RequestMeta, Query(query): Query, ) -> axum::response::Result { - let envelope = parse_event(query.sentry_data.into(), meta, state.config())?; + let envelope = parse_event(query.sentry_data.into(), meta, &state.config())?; common::handle_envelope(&state, envelope) .await? .check_rate_limits()?; Ok(([(header::CONTENT_TYPE, "image/gif")], PIXEL)) } -pub fn route(config: &Config) -> MethodRouter { +pub fn route(config: &ConfigSnapshot) -> MethodRouter { (post(handle_post).get(handle_get)).route_layer(DefaultBodyLimit::max(config.max_event_size())) } diff --git a/relay-server/src/endpoints/unreal.rs b/relay-server/src/endpoints/unreal.rs index 477fe799434..f0b21bde071 100644 --- a/relay-server/src/endpoints/unreal.rs +++ b/relay-server/src/endpoints/unreal.rs @@ -2,7 +2,7 @@ use axum::extract::{DefaultBodyLimit, FromRequest, Query}; use axum::response::IntoResponse; use axum::routing::{MethodRouter, post}; use bytes::Bytes; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_event_schema::protocol::EventId; use serde::Deserialize; @@ -67,7 +67,7 @@ async fn handle( Ok(TextResponse(id)) } -pub fn route(config: &Config) -> MethodRouter { +pub fn route(config: &ConfigSnapshot) -> MethodRouter { post(handle) .route_layer(DefaultBodyLimit::max(config.max_attachments_size())) .route_layer(axum::middleware::from_fn(middlewares::content_length)) diff --git a/relay-server/src/endpoints/upload.rs b/relay-server/src/endpoints/upload.rs index fe5744738dd..4b28c7d3cc3 100644 --- a/relay-server/src/endpoints/upload.rs +++ b/relay-server/src/endpoints/upload.rs @@ -15,7 +15,7 @@ use axum::routing::{MethodRouter, patch, post}; use chrono::Utc; use futures::StreamExt; use http::header; -use relay_config::{Config, UpstreamDescriptor}; +use relay_config::{ConfigSnapshot, UpstreamDescriptor}; use relay_dynamic_config::Feature; use relay_system::SendError; use tower_http::limit::RequestBodyLimitLayer; @@ -40,13 +40,13 @@ use crate::statsd::RelayCounters; use crate::utils::{ApiErrorResponse, MeteredStream}; use crate::utils::{BoundedStream, find_error_source, tus}; -pub fn route_post(config: &Config) -> MethodRouter { +pub fn route_post(config: &ConfigSnapshot) -> MethodRouter { post(handle_post) .route_layer(RequestBodyLimitLayer::new(config.max_upload_size())) .route_layer(DefaultBodyLimit::disable()) } -pub fn route_patch(config: &Config) -> MethodRouter { +pub fn route_patch(config: &ConfigSnapshot) -> MethodRouter { patch(handle_patch) .route_layer(RequestBodyLimitLayer::new(config.max_upload_size())) .route_layer(DefaultBodyLimit::disable()) diff --git a/relay-server/src/lib.rs b/relay-server/src/lib.rs index bc07a9db132..c60022e31c8 100644 --- a/relay-server/src/lib.rs +++ b/relay-server/src/lib.rs @@ -293,17 +293,18 @@ use crate::services::server::HttpServer; /// the `config` passed into this funciton. pub fn run(config: Config) -> anyhow::Result<()> { let config = Arc::new(config); + let current_config = config.current(); relay_log::info!("relay server starting"); // Creates the main runtime. - let runtime = crate::service::create_runtime("main-rt", config.cpu_concurrency()); + let runtime = crate::service::create_runtime("main-rt", current_config.cpu_concurrency()); let handle = runtime.handle().clone(); // Run the system and block until a shutdown signal is sent to this process. Inside, start a // web server and run all relevant services. See the `actors` module documentation for more // information on all services. runtime.block_on(async { - Controller::start(config.shutdown_timeout()); + Controller::start(current_config.shutdown_timeout()); let mut services = handle.service_set(); diff --git a/relay-server/src/processing/errors/errors/nswitch.rs b/relay-server/src/processing/errors/errors/nswitch.rs index 5a3b2f23485..a38d7c751b3 100644 --- a/relay-server/src/processing/errors/errors/nswitch.rs +++ b/relay-server/src/processing/errors/errors/nswitch.rs @@ -326,7 +326,7 @@ fn decompress_data_zstd(data: Bytes, dictionary_id: u8) -> std::io::Result Context<'static> { - static CONFIG: std::sync::LazyLock = std::sync::LazyLock::new(|| { + static CONFIG: std::sync::LazyLock = std::sync::LazyLock::new(|| { let mut config = Config::default(); config .apply_override(OverridableConfig { @@ -347,7 +347,7 @@ mod tests { ..Default::default() }) .unwrap(); - config + config.current() }); static PROJECT_INFO: std::sync::LazyLock = std::sync::LazyLock::new(|| { diff --git a/relay-server/src/processing/errors/errors/utils/attachment.rs b/relay-server/src/processing/errors/errors/utils/attachment.rs index b5f61ac67c7..ff79b70b932 100644 --- a/relay-server/src/processing/errors/errors/utils/attachment.rs +++ b/relay-server/src/processing/errors/errors/utils/attachment.rs @@ -1,4 +1,4 @@ -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_event_schema::protocol::{Breadcrumb, Event, Values}; use relay_protocol::{Annotated, Array, Object}; @@ -7,7 +7,7 @@ use crate::services::processor::ProcessingError; use crate::utils::rmp; pub fn event_from_attachments( - config: &Config, + config: &ConfigSnapshot, event_item: Option, breadcrumbs_item1: Option, breadcrumbs_item2: Option, @@ -59,7 +59,7 @@ pub fn event_from_attachments( } fn extract_attached_event( - config: &Config, + config: &ConfigSnapshot, item: Option, ) -> Result, ProcessingError> { let item = match item { @@ -83,7 +83,7 @@ fn extract_attached_event( } fn parse_msgpack_breadcrumbs( - config: &Config, + config: &ConfigSnapshot, item: Option, ) -> Result, ProcessingError> { let mut breadcrumbs = Array::new(); @@ -117,15 +117,19 @@ fn parse_msgpack_breadcrumbs( #[cfg(test)] mod tests { - use std::collections::BTreeMap; use chrono::{DateTime, TimeZone, Utc}; + use relay_config::Config; use crate::envelope::{ContentType, ItemType}; use super::*; + fn config() -> ConfigSnapshot { + Config::default().current() + } + fn create_breadcrumbs_item(breadcrumbs: &[(Option>, &str)]) -> Item { let mut data = Vec::new(); @@ -161,7 +165,7 @@ mod tests { let item = create_breadcrumbs_item(&[(None, "item1")]); // NOTE: using (Some, None) here: - let result = event_from_attachments(&Config::default(), None, Some(item), None); + let result = event_from_attachments(&config(), None, Some(item), None); let event = result.unwrap().0; let breadcrumbs = breadcrumbs_from_event(&event); @@ -176,7 +180,7 @@ mod tests { let item = create_breadcrumbs_item(&[(None, "item2")]); // NOTE: using (None, Some) here: - let result = event_from_attachments(&Config::default(), None, None, Some(item)); + let result = event_from_attachments(&config(), None, None, Some(item)); let event = result.unwrap().0; let breadcrumbs = breadcrumbs_from_event(&event); @@ -191,7 +195,7 @@ mod tests { let item1 = create_breadcrumbs_item(&[(None, "crumb1")]); let item2 = create_breadcrumbs_item(&[(None, "crumb2"), (None, "crumb3")]); - let result = event_from_attachments(&Config::default(), None, Some(item1), Some(item2)); + let result = event_from_attachments(&config(), None, Some(item1), Some(item2)); let event = result.unwrap().0; let breadcrumbs = breadcrumbs_from_event(&event); @@ -206,7 +210,7 @@ mod tests { let item1 = create_breadcrumbs_item(&[(None, "none"), (Some(d1), "d1")]); let item2 = create_breadcrumbs_item(&[(Some(d2), "d2")]); - let result = event_from_attachments(&Config::default(), None, Some(item1), Some(item2)); + let result = event_from_attachments(&config(), None, Some(item1), Some(item2)); let event = result.unwrap().0; let breadcrumbs = breadcrumbs_from_event(&event); @@ -224,7 +228,7 @@ mod tests { let item1 = create_breadcrumbs_item(&[(Some(d2), "d2")]); let item2 = create_breadcrumbs_item(&[(None, "none"), (Some(d1), "d1")]); - let result = event_from_attachments(&Config::default(), None, Some(item1), Some(item2)); + let result = event_from_attachments(&config(), None, Some(item1), Some(item2)); let event = result.unwrap().0; let breadcrumbs = breadcrumbs_from_event(&event); @@ -240,8 +244,7 @@ mod tests { let item2 = create_breadcrumbs_item(&[]); let item3 = create_breadcrumbs_item(&[]); - let result = - event_from_attachments(&Config::default(), Some(item1), Some(item2), Some(item3)); + let result = event_from_attachments(&config(), Some(item1), Some(item2), Some(item3)); // regression test to ensure we don't fail parsing an empty file result.expect("event_from_attachments"); @@ -262,12 +265,12 @@ mod tests { fn test_msgpack_deep_nesting_is_rejected() { // ~200 KB payload, comfortably under the 1 MiB max_event_size, but 200k levels deep. let payload = deeply_nested_msgpack_event(200_000); - assert!(payload.len() < Config::default().max_event_size()); + assert!(payload.len() < config().max_event_size()); let mut item = Item::new(ItemType::Attachment); item.set_payload(ContentType::MsgPack, payload); - let result = extract_attached_event(&Config::default(), Some(item)); + let result = extract_attached_event(&config(), Some(item)); assert!( matches!(result, Err(ProcessingError::InvalidMsgpack(_))), diff --git a/relay-server/src/processing/forward.rs b/relay-server/src/processing/forward.rs index c8526774afa..5de93bfd6f4 100644 --- a/relay-server/src/processing/forward.rs +++ b/relay-server/src/processing/forward.rs @@ -1,4 +1,4 @@ -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_dynamic_config::GlobalConfig; use relay_dynamic_config::{RetentionConfig, RetentionsConfig}; #[cfg(feature = "processing")] @@ -105,7 +105,7 @@ pub trait Forward { #[derive(Copy, Clone, Debug)] pub struct ForwardContext<'a> { /// The Relay configuration. - pub config: &'a Config, + pub config: &'a ConfigSnapshot, /// A view of the currently active global configuration. #[cfg_attr(not(feature = "processing"), expect(unused))] pub global_config: &'a GlobalConfig, diff --git a/relay-server/src/processing/mod.rs b/relay-server/src/processing/mod.rs index 10935058e82..1ebed3c1c9d 100644 --- a/relay-server/src/processing/mod.rs +++ b/relay-server/src/processing/mod.rs @@ -7,7 +7,7 @@ //! The processor service, will then do its actual work using the processing logic defined here. use relay_cogs::FeatureWeights; -use relay_config::{Config, RelayMode}; +use relay_config::{ConfigSnapshot, RelayMode}; use relay_dynamic_config::GlobalConfig; use relay_quotas::RateLimits; @@ -80,7 +80,7 @@ pub trait Processor { #[derive(Copy, Clone, Debug)] pub struct Context<'a> { /// The Relay configuration. - pub config: &'a Config, + pub config: &'a ConfigSnapshot, /// A view of the currently active global configuration. pub global_config: &'a GlobalConfig, /// Project configuration associated with the unit of work. @@ -127,9 +127,10 @@ impl<'a> Context<'a> { impl Context<'static> { /// Returns a [`Context`] with default values for testing. pub fn for_test() -> Self { + use relay_config::Config; use std::sync::LazyLock; - static CONFIG: LazyLock = LazyLock::new(Default::default); + static CONFIG: LazyLock = LazyLock::new(|| Config::default().current()); static GLOBAL_CONFIG: LazyLock = LazyLock::new(Default::default); static PROJECT_INFO: LazyLock = LazyLock::new(Default::default); static RATE_LIMITS: LazyLock = LazyLock::new(Default::default); diff --git a/relay-server/src/processing/transactions/profile.rs b/relay-server/src/processing/transactions/profile.rs index 07075b30bdf..e40afa30264 100644 --- a/relay-server/src/processing/transactions/profile.rs +++ b/relay-server/src/processing/transactions/profile.rs @@ -3,7 +3,7 @@ use relay_dynamic_config::GlobalConfig; use relay_quotas::{DataCategory, Scoping}; use std::net::IpAddr; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_event_schema::protocol::{Contexts, Event, ProfileContext}; use relay_filter::ProjectFiltersConfig; use relay_profiling::{ProfileId, ProfileType}; @@ -148,7 +148,7 @@ pub fn scrub_profiler_id(event: &mut Annotated) { fn expand_profile( item: &mut Item, event: &Event, - config: &Config, + config: &ConfigSnapshot, client_ip: Option, filter_settings: &ProjectFiltersConfig, global_config: &GlobalConfig, diff --git a/relay-server/src/processing/transactions/spans.rs b/relay-server/src/processing/transactions/spans.rs index e429ae7ccdf..b1bd15362cc 100644 --- a/relay-server/src/processing/transactions/spans.rs +++ b/relay-server/src/processing/transactions/spans.rs @@ -3,7 +3,7 @@ use std::error::Error; use crate::processing; use crate::processing::utils::event::event_type; use relay_base_schema::events::EventType; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_event_normalization::eap::{Ingress, Pipeline}; use relay_event_schema::protocol::{Event, Measurement, Measurements, Span, SpanV2, TraceContext}; use relay_metrics::MetricNamespace; @@ -14,7 +14,7 @@ use relay_sampling::DynamicSamplingContext; pub fn extract_from_event( dsc: Option<&DynamicSamplingContext>, event: &Annotated, - config: &Config, + config: &ConfigSnapshot, server_sample_rate: Option, ) -> Vec, ()>> { // Only extract spans from transactions (not errors). diff --git a/relay-server/src/processing/utils/attachments.rs b/relay-server/src/processing/utils/attachments.rs index 4b8bc0af450..ab58565e22b 100644 --- a/relay-server/src/processing/utils/attachments.rs +++ b/relay-server/src/processing/utils/attachments.rs @@ -1,7 +1,7 @@ use std::error::Error; use std::time::Instant; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_pii::{PiiAttachmentsProcessor, PiiConfig, SelectorPathItem, SelectorSpec}; use relay_statsd::metric; @@ -35,7 +35,7 @@ pub fn validate_attachments( } #[cfg_attr(not(feature = "processing"), expect(unused_variables))] -fn validate(item: &Item, config: &Config) -> Result<(), ProcessingError> { +fn validate(item: &Item, config: &ConfigSnapshot) -> Result<(), ProcessingError> { #[cfg(not(feature = "processing"))] return Ok(()); diff --git a/relay-server/src/processing/utils/event.rs b/relay-server/src/processing/utils/event.rs index 36287862a40..e511f0f0102 100644 --- a/relay-server/src/processing/utils/event.rs +++ b/relay-server/src/processing/utils/event.rs @@ -9,7 +9,7 @@ use chrono::Duration as SignedDuration; use relay_auth::RelayVersion; use relay_base_schema::events::EventType; use relay_base_schema::project::ProjectId; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_config::NormalizationLevel; use relay_event_normalization::GeoIpLookup; use relay_event_normalization::{ @@ -62,7 +62,7 @@ pub fn finalize<'a>( event: &mut Annotated, attachments: impl Iterator, metrics: &mut Metrics, - config: &Config, + config: &ConfigSnapshot, ) -> Result<(), ProcessingError> { let inner_event = match event.value_mut() { Some(event) => event, diff --git a/relay-server/src/service.rs b/relay-server/src/service.rs index a9b242c7b5c..1967c0d0428 100644 --- a/relay-server/src/service.rs +++ b/relay-server/src/service.rs @@ -40,7 +40,7 @@ use anyhow::Result; use axum::extract::FromRequestParts; use axum::http::request::Parts; use relay_cogs::Cogs; -use relay_config::{Config, EmitOutcomes, RelayMode}; +use relay_config::{Config, ConfigSnapshot, EmitOutcomes, RelayMode}; #[cfg(feature = "processing")] use relay_config::{RedisConfigRef, RedisConfigsRef}; #[cfg(feature = "processing")] @@ -110,7 +110,7 @@ pub fn create_runtime(name: &'static str, threads: usize) -> relay_system::Runti .build() } -fn create_processor_pool(config: &Config) -> Result { +fn create_processor_pool(config: &ConfigSnapshot) -> Result { // Adjust thread count for small cpu counts to not have too many idle cores // and distribute workload better. let thread_count = match config.cpu_concurrency() { @@ -130,7 +130,7 @@ fn create_processor_pool(config: &Config) -> Result Result { +fn create_store_pool(config: &ConfigSnapshot) -> Result { // Spawn a store worker for every 12 threads in the processor pool. // This ratio was found empirically and may need adjustments in the future. // @@ -168,11 +168,12 @@ impl ServiceState { config: Arc, ) -> Result { let upstream_relay = services.start(UpstreamRelayService::new(config.clone())); + let current_config = config.current(); #[cfg(feature = "processing")] - let redis_clients = config + let redis_clients = current_config .redis() - .filter(|_| config.processing_enabled()) + .filter(|_| current_config.processing_enabled()) .map(create_redis_clients) .transpose() .context(ServiceError::Redis)?; @@ -190,22 +191,22 @@ impl ServiceState { // We create an instance of `MemoryStat` which can be supplied composed with any arbitrary // configuration object down the line. - let memory_stat = MemoryStat::new(config.memory_stat_refresh_frequency_ms()); + let memory_stat = MemoryStat::new(current_config.memory_stat_refresh_frequency_ms()); // Create an address for the `EnvelopeProcessor`, which can be injected into the // other services. - let (processor, processor_rx) = match config.relay_mode() { + let (processor, processor_rx) = match current_config.relay_mode() { RelayMode::Proxy => channel(ProxyProcessorService::name()), RelayMode::Managed => channel(EnvelopeProcessorService::name()), }; let (aggregator, aggregator_rx) = channel(RouterService::name()); - let outcome_aggregator = match config.emit_outcomes() { + let outcome_aggregator = match current_config.emit_outcomes() { EmitOutcomes::None => services.start(NullOutcomeProducerService::new()), - _ => match config.relay_mode() { + _ => match current_config.relay_mode() { RelayMode::Proxy => services.start(ClientReportOutcomeProducerService::new( - &config, + ¤t_config, processor.clone(), )), RelayMode::Managed => services.start(OutcomeProducerService::new( @@ -237,9 +238,9 @@ impl ServiceState { let metric_outcomes = MetricOutcomes::new(outcome_aggregator.clone()); #[cfg(feature = "processing")] - let store_pool = create_store_pool(&config)?; + let store_pool = create_store_pool(¤t_config)?; #[cfg(feature = "processing")] - let store = config + let store = current_config .processing_enabled() .then(|| { StoreService::create( @@ -253,15 +254,16 @@ impl ServiceState { .transpose()?; #[cfg(feature = "processing")] - let objectstore = ObjectstoreService::new(config.objectstore(), store.clone())?.map(|s| { - let concurrent = ConcurrentService::new(s) - .with_backlog_limit(config.objectstore().max_backlog) - .with_concurrency_limit(config.objectstore().max_concurrent_requests); - services.start(concurrent) - }); + let objectstore = ObjectstoreService::new(current_config.objectstore(), store.clone())? + .map(|s| { + let concurrent = ConcurrentService::new(s) + .with_backlog_limit(current_config.objectstore().max_backlog) + .with_concurrency_limit(current_config.objectstore().max_concurrent_requests); + services.start(concurrent) + }); let envelope_buffer = PartitionedEnvelopeBuffer::create( - config.spool_partitions(), + current_config.spool_partitions(), config.clone(), memory_stat.clone(), global_config_rx.clone(), @@ -271,7 +273,7 @@ impl ServiceState { services, ); - let (processor_pool, aggregator_handle, autoscaling) = match config.relay_mode() { + let (processor_pool, aggregator_handle, autoscaling) = match current_config.relay_mode() { RelayMode::Proxy => { services.start_with( ProxyProcessorService::new( @@ -287,20 +289,23 @@ impl ServiceState { (None, None, None) } RelayMode::Managed => { - let processor_pool = create_processor_pool(&config)?; + let processor_pool = create_processor_pool(¤t_config)?; let router = RouterService::new( handle.clone(), - config.default_aggregator_config().clone(), - config.secondary_aggregator_configs().clone(), + current_config.default_aggregator_config().clone(), + current_config.secondary_aggregator_configs().clone(), Some(processor.clone().recipient()), project_cache_handle.clone(), ); let router_handle = router.handle(); services.start_with(router, aggregator_rx); - let cogs = CogsService::new(&config); - let cogs = Cogs::new(CogsServiceRecorder::new(&config, services.start(cogs))); + let cogs = CogsService::new(¤t_config); + let cogs = Cogs::new(CogsServiceRecorder::new( + ¤t_config, + services.start(cogs), + )); services.start_with( EnvelopeProcessorService::new( @@ -394,9 +399,9 @@ impl ServiceState { }) } - /// Returns a reference to the Relay configuration. - pub fn config(&self) -> &Config { - &self.inner.config + /// Returns a snapshot of the Relay configuration. + pub fn config(&self) -> ConfigSnapshot { + self.inner.config.current() } /// Returns a reference to the [`MemoryChecker`] which is a [`Config`] aware wrapper on the diff --git a/relay-server/src/services/buffer/envelope_buffer/mod.rs b/relay-server/src/services/buffer/envelope_buffer/mod.rs index ecbd960539b..5e1a51f097e 100644 --- a/relay-server/src/services/buffer/envelope_buffer/mod.rs +++ b/relay-server/src/services/buffer/envelope_buffer/mod.rs @@ -8,7 +8,7 @@ use std::time::Duration; use chrono::{DateTime, Utc}; use hashbrown::HashSet; use relay_base_schema::project::ProjectKey; -use relay_config::Config; +use relay_config::ConfigSnapshot; use tokio::time::{Instant, timeout}; use crate::envelope::Envelope; @@ -52,7 +52,7 @@ impl PolymorphicEnvelopeBuffer { /// depending on the given configuration. pub async fn from_config( partition_id: u8, - config: &Config, + config: &ConfigSnapshot, memory_checker: MemoryChecker, ) -> Result { let buffer = if config.spool_envelopes_path(partition_id).is_some() { @@ -278,7 +278,10 @@ impl EnvelopeBuffer { #[allow(dead_code)] impl EnvelopeBuffer { /// Creates an empty sqlite-based buffer. - pub async fn new(partition_id: u8, config: &Config) -> Result { + pub async fn new( + partition_id: u8, + config: &ConfigSnapshot, + ) -> Result { Ok(Self { stacks_by_project: Default::default(), priority_queue: Default::default(), @@ -715,6 +718,7 @@ impl Readiness { mod tests { use relay_base_schema::project::ProjectId; use relay_common::Dsn; + use relay_config::Config; use relay_event_schema::protocol::EventId; use relay_sampling::DynamicSamplingContext; use std::str::FromStr; @@ -1075,8 +1079,11 @@ mod tests { .into_string() .unwrap(); let config = mock_config(&path); - let mut store = SqliteEnvelopeStore::prepare(0, &config).await.unwrap(); - let mut buffer = EnvelopeBuffer::::new(0, &config) + let current_config = config.current(); + let mut store = SqliteEnvelopeStore::prepare(0, ¤t_config) + .await + .unwrap(); + let mut buffer = EnvelopeBuffer::::new(0, ¤t_config) .await .unwrap(); diff --git a/relay-server/src/services/buffer/envelope_store/sqlite.rs b/relay-server/src/services/buffer/envelope_store/sqlite.rs index a9dfbf02631..50774aa9006 100644 --- a/relay-server/src/services/buffer/envelope_store/sqlite.rs +++ b/relay-server/src/services/buffer/envelope_store/sqlite.rs @@ -14,7 +14,7 @@ use chrono::{DateTime, Utc}; use futures::stream::StreamExt; use hashbrown::HashSet; use relay_base_schema::project::{ParseProjectKeyError, ProjectKey}; -use relay_config::Config; +use relay_config::ConfigSnapshot; use serde::{Deserialize, Serialize}; use sqlx::migrate::MigrateError; use sqlx::query::Query; @@ -313,7 +313,7 @@ impl SqliteEnvelopeStore { /// the folders where data will be stored. pub async fn prepare( partition_id: u8, - config: &Config, + config: &ConfigSnapshot, ) -> Result { // If no path is provided, we can't do disk spooling. let Some(path) = config.spool_envelopes_path(partition_id) else { diff --git a/relay-server/src/services/buffer/mod.rs b/relay-server/src/services/buffer/mod.rs index 3d6a466fbf8..1bf236d4b57 100644 --- a/relay-server/src/services/buffer/mod.rs +++ b/relay-server/src/services/buffer/mod.rs @@ -11,7 +11,7 @@ use ahash::RandomState; use chrono::DateTime; use chrono::Utc; use relay_base_schema::project::ProjectKey; -use relay_config::{Config, EnvelopeSpoolPartitioning}; +use relay_config::{Config, ConfigSnapshot, EnvelopeSpoolPartitioning}; use relay_system::Receiver; use relay_system::ServiceSpawn; use relay_system::ServiceSpawnExt as _; @@ -96,7 +96,7 @@ impl PartitionedEnvelopeBuffer { outcome_aggregator: Addr, services: &dyn ServiceSpawn, ) -> Arc { - let partitioning = Partitioning::new(config.spool_partitioning()); + let partitioning = Partitioning::new(config.current().spool_partitioning()); let mut envelope_buffers = Vec::with_capacity(partitions.get() as usize); for partition_id in 0..partitions.get() { @@ -375,14 +375,14 @@ impl EnvelopeBufferService { } fn memory_ready(&self) -> bool { - self.memory_stat.memory().used_percent() - <= self.config.spool_max_backpressure_memory_percent() + let config = self.config.current(); + self.memory_stat.memory().used_percent() <= config.spool_max_backpressure_memory_percent() } /// Tries to pop an envelope for a ready project. async fn try_pop( partition_tag: &str, - config: &Config, + config: &ConfigSnapshot, buffer: &mut PolymorphicEnvelopeBuffer, services: &Services, ) -> Result { @@ -597,7 +597,7 @@ impl EnvelopeBufferService { } } -fn is_expired(last_received_at: DateTime, config: &Config) -> bool { +fn is_expired(last_received_at: DateTime, config: &ConfigSnapshot) -> bool { (Utc::now() - last_received_at) .to_std() .is_ok_and(|age| age > config.spool_envelopes_max_age()) @@ -607,17 +607,19 @@ impl Service for EnvelopeBufferService { type Interface = EnvelopeBuffer; async fn run(mut self, mut rx: Receiver) { - let config = self.config.clone(); - let memory_checker = MemoryChecker::new(self.memory_stat.clone(), config.clone()); + let memory_checker = MemoryChecker::new(self.memory_stat.clone(), self.config.clone()); let mut global_config_rx = self.global_config_rx.clone(); let services = self.services.clone(); let dequeue = Arc::::new(true.into()); - let mut buffer = - PolymorphicEnvelopeBuffer::from_config(self.partition_id, &config, memory_checker) - .await - .expect("failed to start the envelope buffer service"); + let mut buffer = PolymorphicEnvelopeBuffer::from_config( + self.partition_id, + &self.config.current(), + memory_checker, + ) + .await + .expect("failed to start the envelope buffer service"); buffer.initialize().await; @@ -650,6 +652,7 @@ impl Service for EnvelopeBufferService { partition_id = &partition_tag ); let mut sleep = DEFAULT_SLEEP; + let config = self.config.current(); tokio::select! { // NOTE: we do not select a bias here. @@ -1043,7 +1046,9 @@ mod tests { let mut envelope = new_managed_envelope(false, "foo"); envelope.envelope_mut().meta_mut().set_received_at( Utc::now() - - chrono::Duration::seconds(2 * config.spool_envelopes_max_age().as_secs() as i64), + - chrono::Duration::seconds( + 2 * config.current().spool_envelopes_max_age().as_secs() as i64, + ), ); addr.send(EnvelopeBuffer::Push(envelope)); diff --git a/relay-server/src/services/buffer/stack_provider/sqlite.rs b/relay-server/src/services/buffer/stack_provider/sqlite.rs index 41ad2108029..0648e8dcf0b 100644 --- a/relay-server/src/services/buffer/stack_provider/sqlite.rs +++ b/relay-server/src/services/buffer/stack_provider/sqlite.rs @@ -1,6 +1,6 @@ use std::error::Error; -use relay_config::Config; +use relay_config::ConfigSnapshot; use crate::services::buffer::common::ProjectKeyPair; use crate::services::buffer::envelope_stack::caching::CachingEnvelopeStack; @@ -24,8 +24,11 @@ pub struct SqliteStackProvider { #[warn(dead_code)] impl SqliteStackProvider { - /// Creates a new [`SqliteStackProvider`] from the provided [`Config`]. - pub async fn new(partition_id: u8, config: &Config) -> Result { + /// Creates a new [`SqliteStackProvider`] from the provided [`ConfigSnapshot`]. + pub async fn new( + partition_id: u8, + config: &ConfigSnapshot, + ) -> Result { let envelope_store = SqliteEnvelopeStore::prepare(partition_id, config).await?; Ok(Self { envelope_store, @@ -164,7 +167,9 @@ mod tests { #[tokio::test] async fn test_flush() { let config = mock_config(); - let mut stack_provider = SqliteStackProvider::new(0, &config).await.unwrap(); + let mut stack_provider = SqliteStackProvider::new(0, &config.current()) + .await + .unwrap(); let own_key = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(); let sampling_key = ProjectKey::parse("b81ae32be2584e0bbd7a4cbb95971fe1").unwrap(); diff --git a/relay-server/src/services/cogs.rs b/relay-server/src/services/cogs.rs index 392377e459b..63b11a4a5f6 100644 --- a/relay-server/src/services/cogs.rs +++ b/relay-server/src/services/cogs.rs @@ -1,7 +1,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use relay_cogs::{CogsMeasurement, CogsRecorder, ResourceId}; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_system::{Addr, FromMessage, Interface, Service}; use crate::statsd::RelayCounters; @@ -24,7 +24,7 @@ pub struct CogsService { } impl CogsService { - pub fn new(config: &Config) -> Self { + pub fn new(config: &ConfigSnapshot) -> Self { Self { relay_resource_id: config.cogs_relay_resource_id().to_owned(), } @@ -73,7 +73,7 @@ pub struct CogsServiceRecorder { impl CogsServiceRecorder { /// Creates a new recorder forwarding messages to [`CogsService`]. - pub fn new(config: &Config, addr: Addr) -> Self { + pub fn new(config: &ConfigSnapshot, addr: Addr) -> Self { Self { addr, max_size: config.cogs_max_queue_size(), @@ -102,6 +102,8 @@ impl CogsRecorder for CogsServiceRecorder { mod tests { use std::time::Duration; + use relay_config::Config; + use super::*; #[test] @@ -113,7 +115,7 @@ mod tests { } })) .unwrap(); - let recorder = CogsServiceRecorder::new(&config, addr.clone()); + let recorder = CogsServiceRecorder::new(&config.current(), addr.clone()); for _ in 0..5 { recorder.record(CogsMeasurement { diff --git a/relay-server/src/services/global_config.rs b/relay-server/src/services/global_config.rs index fdbc0fa6cf2..04bfb0ef9f9 100644 --- a/relay-server/src/services/global_config.rs +++ b/relay-server/src/services/global_config.rs @@ -253,7 +253,7 @@ impl GlobalConfigService { fn schedule_fetch(&mut self) { if !self.shutdown && self.fetch_handle.is_idle() { self.fetch_handle - .set(self.config.global_config_fetch_interval()); + .set(self.config.current().global_config_fetch_interval()); } } @@ -345,7 +345,7 @@ impl Service for GlobalConfigService { let mut shutdown_handle = Controller::shutdown_handle(); relay_log::info!("global config service starting"); - if self.config.relay_mode() == RelayMode::Managed { + if self.config.current().relay_mode() == RelayMode::Managed { relay_log::info!("requesting global config from upstream"); self.request_global_config(); } else { @@ -394,7 +394,7 @@ mod tests { use std::sync::Arc; use std::time::Duration; - use relay_config::{Config, RelayMode}; + use relay_config::{Config, Credentials, RelayMode}; use relay_system::{Controller, Service, ShutdownMode}; use relay_test::mock_service; @@ -417,8 +417,10 @@ mod tests { Controller::start(Duration::from_secs(1)); let mut config = Config::default(); - config.regenerate_credentials(false).unwrap(); - let fetch_interval = config.global_config_fetch_interval(); + config + .replace_credentials(Some(Credentials::generate())) + .unwrap(); + let fetch_interval = config.current().global_config_fetch_interval(); let service = GlobalConfigService::new(Arc::new(config), upstream) .0 @@ -448,9 +450,11 @@ mod tests { } })) .unwrap(); - config.regenerate_credentials(false).unwrap(); + config + .replace_credentials(Some(Credentials::generate())) + .unwrap(); - let fetch_interval = config.global_config_fetch_interval(); + let fetch_interval = config.current().global_config_fetch_interval(); let service = GlobalConfigService::new(Arc::new(config), upstream) .0 .start_detached(); @@ -476,7 +480,7 @@ mod tests { })) .unwrap(); - let fetch_interval = config.global_config_fetch_interval(); + let fetch_interval = config.current().global_config_fetch_interval(); let service = GlobalConfigService::new(Arc::new(config), upstream) .0 diff --git a/relay-server/src/services/health_check.rs b/relay-server/src/services/health_check.rs index 45d41651fd7..9b8aac0ae96 100644 --- a/relay-server/src/services/health_check.rs +++ b/relay-server/src/services/health_check.rs @@ -108,13 +108,14 @@ impl HealthCheckService { } fn system_memory_probe(&mut self) -> Status { + let config = self.config.current(); if let MemoryCheck::Exceeded(memory) = self.memory_checker.check_memory_percent() { relay_log::error!( "Not enough memory, {} / {} ({:.2}% >= {:.2}%)", memory.used, memory.total, memory.used_percent() * 100.0, - self.config.health_max_memory_watermark_percent() * 100.0, + config.health_max_memory_watermark_percent() * 100.0, ); return Status::Unhealthy; } @@ -125,7 +126,7 @@ impl HealthCheckService { memory.used, memory.total, memory.used, - self.config.health_max_memory_watermark_bytes(), + config.health_max_memory_watermark_bytes(), ); return Status::Unhealthy; } @@ -134,7 +135,7 @@ impl HealthCheckService { } async fn auth_probe(&self) -> Status { - if !self.config.requires_auth() { + if !self.config.current().requires_auth() { return Status::Healthy; } @@ -159,7 +160,7 @@ impl HealthCheckService { } async fn probe(&self, name: &'static str, fut: impl Future) -> Status { - match timeout(self.config.health_probe_timeout(), fut).await { + match timeout(self.config.current().health_probe_timeout(), fut).await { Err(_) => { relay_log::error!("Health check probe '{name}' timed out"); Status::Unhealthy @@ -192,9 +193,10 @@ impl Service for HealthCheckService { async fn run(mut self, mut rx: relay_system::Receiver) { let (update_tx, update_rx) = watch::channel(StatusUpdate::new(Status::Unhealthy)); - let check_interval = self.config.health_refresh_interval(); + let config = self.config.current(); + let check_interval = config.health_refresh_interval(); // Add 10% buffer to the internal timeouts to avoid race conditions. - let status_timeout = (check_interval + self.config.health_probe_timeout()).mul_f64(1.1); + let status_timeout = (check_interval + config.health_probe_timeout()).mul_f64(1.1); relay_system::spawn!(async move { let shutdown = Controller::shutdown_handle(); diff --git a/relay-server/src/services/outcome/metric.rs b/relay-server/src/services/outcome/metric.rs index 1077ea942da..518feb32289 100644 --- a/relay-server/src/services/outcome/metric.rs +++ b/relay-server/src/services/outcome/metric.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::sync::LazyLock; use relay_base_schema::data_category::DataCategory; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_event_schema::protocol::{ClientReport, DiscardedEvent}; use relay_metrics::{Bucket, BucketValue, MetricName, MetricNamespace, UnixTimestamp}; use relay_protocol::FiniteF64; @@ -28,7 +28,7 @@ const CLIENT_DISCARD_MRI: &str = "c:outcomes/client_discard@none"; const CARDINALITY_LIMITED_MRI: &str = "c:outcomes/cardinality_limited@none"; /// Converts a [`TrackOutcome`] to a metric [`Bucket`]. -pub fn to_metric(outcome: &TrackOutcome, config: &Config) -> Bucket { +pub fn to_metric(outcome: &TrackOutcome, config: &ConfigSnapshot) -> Bucket { static ACCEPTED: LazyLock = LazyLock::new(|| OUTCOME_ACCEPTED_MRI.into()); static FILTERED: LazyLock = LazyLock::new(|| FILTERED_MRI.into()); static RATE_LIMITED: LazyLock = LazyLock::new(|| RATE_LIMITED_MRI.into()); @@ -199,6 +199,7 @@ mod tests { use relay_base_schema::data_category::DataCategory; use relay_base_schema::organization::OrganizationId; use relay_base_schema::project::ProjectId; + use relay_config::Config; use relay_filter::FilterStatKey; use relay_metrics::{MetricNamespace, MetricType}; use relay_quotas::Scoping; @@ -216,13 +217,14 @@ mod tests { } } - fn config() -> Config { + fn config() -> ConfigSnapshot { Config::from_json_value(serde_json::json!({ "outcomes": { "source": "I bims", } })) .unwrap() + .current() } fn bucket( diff --git a/relay-server/src/services/outcome/service.rs b/relay-server/src/services/outcome/service.rs index b93e0928d69..bb8bd3893df 100644 --- a/relay-server/src/services/outcome/service.rs +++ b/relay-server/src/services/outcome/service.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use std::time::Duration; use chrono::{DateTime, Utc}; -use relay_config::Config; +use relay_config::{Config, ConfigSnapshot}; use relay_event_schema::protocol::{ClientReport, DiscardedEvent, EventId}; use relay_metrics::{MetricNamespace, UnixTimestamp}; use relay_quotas::{DataCategory, Scoping}; @@ -88,9 +88,10 @@ impl OutcomeProducerService { fn handle_message(&self, message: TrackOutcome) { send_outcome_metric(&message); + let config = self.config.current(); self.aggregator.send(MergeBuckets { project_key: message.scoping.project_key, - buckets: vec![outcome::metric::to_metric(&message, &self.config)], + buckets: vec![outcome::metric::to_metric(&message, &config)], }) } } @@ -123,7 +124,7 @@ pub struct ClientReportOutcomeProducerService { } impl ClientReportOutcomeProducerService { - pub fn new(config: &Config, processor: Addr) -> Self { + pub fn new(config: &ConfigSnapshot, processor: Addr) -> Self { let agg = &config .aggregator_config_for(MetricNamespace::Outcomes) .aggregator; diff --git a/relay-server/src/services/processor.rs b/relay-server/src/services/processor.rs index 9fd3c1b5526..3171a35c68f 100644 --- a/relay-server/src/services/processor.rs +++ b/relay-server/src/services/processor.rs @@ -18,7 +18,7 @@ use futures::future::BoxFuture; use relay_base_schema::project::{ProjectId, ProjectKey}; use relay_cogs::{AppFeature, Cogs, FeatureWeights, ResourceId, Token}; use relay_common::time::UnixTimestamp; -use relay_config::{Config, EmitOutcomes, HttpEncoding, UpstreamDescriptor}; +use relay_config::{Config, ConfigSnapshot, EmitOutcomes, HttpEncoding, UpstreamDescriptor}; use relay_event_normalization::{ClockDriftProcessor, GeoIpLookup}; use relay_event_schema::processor::ProcessingAction; use relay_event_schema::protocol::ClientReport; @@ -568,7 +568,9 @@ impl EnvelopeProcessorService { addrs: Addrs, metric_outcomes: MetricOutcomes, ) -> Self { - let geoip_lookup = config + let c = config.current(); + + let geoip_lookup = c .geoip_path() .and_then( |p| match GeoIpLookup::open(p).context(ServiceError::GeoIp) { @@ -588,8 +590,8 @@ impl EnvelopeProcessorService { #[cfg(feature = "processing")] let rate_limiter = redis.map(|redis| { RedisRateLimiter::new(redis.quotas) - .max_limit(config.max_rate_limit()) - .cache(config.quota_cache_ratio(), config.quota_cache_max()) + .max_limit(c.max_rate_limit()) + .cache(c.quota_cache_ratio(), c.quota_cache_max()) }); let quota_limiter = Arc::new(QuotaRateLimiter::new( @@ -695,9 +697,10 @@ impl EnvelopeProcessorService { cogs.cancel(); let global_config = self.inner.global_config.current().unwrap_or_default(); + let config = self.inner.config.current(); let ctx = processing::Context { - config: &self.inner.config, + config: &config, global_config: &global_config, project_info: &message.project_info, sampling_project_info: message.sampling_project_info.as_deref(), @@ -877,7 +880,11 @@ impl EnvelopeProcessorService { match output.serialize_envelope(ctx) { Ok(envelope) => { let envelope = ManagedEnvelope::from(envelope); - self.submit_envelope_upstream(envelope, ctx.project_info.upstream.clone()); + self.submit_envelope_upstream( + envelope, + ctx.config, + ctx.project_info.upstream.clone(), + ); } Err(_) => relay_log::error!("failed to serialize output to an envelope"), }; @@ -886,6 +893,7 @@ impl EnvelopeProcessorService { fn submit_envelope_upstream( &self, mut envelope: ManagedEnvelope, + config: &ConfigSnapshot, // Currently allowed to be optional as code is migrated to respect the upstream override // provided from the project config. Eventually must be available and is required. upstream: Option, @@ -900,7 +908,7 @@ impl EnvelopeProcessorService { // Any item which is produced by processing is handled in `submit_upstream`, // metrics are sent to the store directly and outcomes must be produced to Kafka // instead of being sent onward as client report. - if self.inner.config.processing_enabled() { + if config.processing_enabled() { relay_log::error!( "attempt to forward envelope to http upstream when processing is enabled" ); @@ -915,7 +923,7 @@ impl EnvelopeProcessorService { envelope.envelope_mut().set_sent_at(Utc::now()); relay_log::trace!("sending envelope to sentry endpoint"); - let http_encoding = self.inner.config.http_encoding(); + let http_encoding = config.http_encoding(); let result = envelope.envelope().to_vec().and_then(|v| { encode_payload(&v.into(), http_encoding).map_err(EnvelopeError::PayloadIoFailed) }); @@ -963,7 +971,8 @@ impl EnvelopeProcessorService { return; } - let upstream = self.inner.config.upstream(); + let config = self.inner.config.current(); + let upstream = config.upstream(); let dsn = PartialDsn::outbound(&scoping, upstream); let mut envelope = Envelope::from_request(None, RequestMeta::outbound(dsn)); @@ -984,7 +993,7 @@ impl EnvelopeProcessorService { } let envelope = ManagedEnvelope::new(envelope, self.inner.addrs.outcome_aggregator.clone()); - self.submit_envelope_upstream(envelope, None); + self.submit_envelope_upstream(envelope, &self.inner.config.current(), None); } fn check_buckets( @@ -1245,8 +1254,9 @@ impl EnvelopeProcessorService { buckets, } = message; - let batch_size = self.inner.config.metrics_max_batch_size_bytes(); - let upstream = self.inner.config.upstream(); + let config = self.inner.config.current(); + let batch_size = config.metrics_max_batch_size_bytes(); + let upstream = config.upstream(); for ProjectBuckets { buckets, @@ -1280,7 +1290,7 @@ impl EnvelopeProcessorService { distribution(RelayDistributions::BucketsPerBatch) = batch.len() as u64 ); - self.submit_envelope_upstream(envelope, project_info.upstream.clone()); + self.submit_envelope_upstream(envelope, &config, project_info.upstream.clone()); num_batches += 1; } @@ -1302,7 +1312,7 @@ impl EnvelopeProcessorService { } let (unencoded, project_info) = partition.take(); - let http_encoding = self.inner.config.http_encoding(); + let http_encoding = self.inner.config.current().http_encoding(); let encoded = match encode_payload(&unencoded, http_encoding) { Ok(payload) => payload, Err(error) => { @@ -1341,7 +1351,7 @@ impl EnvelopeProcessorService { buckets, } = message; - let batch_size = self.inner.config.metrics_max_batch_size_bytes(); + let batch_size = self.inner.config.current().metrics_max_batch_size_bytes(); let mut partitions = BTreeMap::new(); let mut partition_splits = 0; @@ -1414,8 +1424,10 @@ impl EnvelopeProcessorService { self.check_buckets(*project_key, &pb.project_info, &pb.rate_limits, buckets); } + let config = self.inner.config.current(); + #[cfg(feature = "processing")] - if self.inner.config.processing_enabled() + if config.processing_enabled() && let Some(ref store_forwarder) = self.inner.addrs.store_forwarder { return self @@ -1425,13 +1437,13 @@ impl EnvelopeProcessorService { // Processing Relays never send outcomes as client reports, which is why this check is after // the processing check. - if self.inner.config.emit_outcomes() == EmitOutcomes::AsClientReports { + if config.emit_outcomes() == EmitOutcomes::AsClientReports { // Remove client reports from metrics to be sent, if configured as client reports // and send them separately. message = self.encode_metrics_client_reports(message); } - if self.inner.config.http_global_metrics() { + if config.http_global_metrics() { self.encode_metrics_global(message) } else { self.encode_metrics_envelope(message) @@ -1476,7 +1488,7 @@ impl EnvelopeProcessorService { .buckets .values() .map(|s| { - if self.inner.config.processing_enabled() { + if self.inner.config.current().processing_enabled() { // Processing does not encode the metrics but instead rate limit the metrics, // which scales by count and not size. relay_metrics::cogs::ByCount(&s.buckets).into() @@ -2170,7 +2182,7 @@ mod tests { let processor = create_test_processor(Config::from_json_value(config.clone()).unwrap()).await; - let config = Config::from_json_value(config).unwrap(); + let config = Config::from_json_value(config).unwrap().current(); let ctx = processing::Context { config: &config, project_info: &project_info, diff --git a/relay-server/src/services/projects/cache/handle.rs b/relay-server/src/services/projects/cache/handle.rs index 5ae0dcb6ff5..59b6d9067fe 100644 --- a/relay-server/src/services/projects/cache/handle.rs +++ b/relay-server/src/services/projects/cache/handle.rs @@ -32,7 +32,7 @@ impl ProjectCacheHandle { // Always trigger a fetch after retrieving the project to make sure the state is up to date. self.fetch(project_key); - Project::new(project, &self.config) + Project::new(project, self.config.current()) } /// Awaits until the given project state becomes ready (enabled or disabled). @@ -77,7 +77,7 @@ impl ProjectCacheHandle { let change_listener = project.outdated(); if !project.project_state().is_pending() { drop(change_listener); - return Project::new(project, &self.config); + return Project::new(project, self.config.current()); } change_listener.await; } @@ -107,8 +107,10 @@ impl fmt::Debug for ProjectCacheHandle { #[cfg(test)] mod test { - use super::*; use crate::services::projects::project::ProjectState; + use relay_config::Config; + + use super::*; impl ProjectCacheHandle { /// Creates a new [`ProjectCacheHandle`] for testing only. @@ -117,7 +119,7 @@ mod test { pub fn for_test() -> Self { Self { shared: Default::default(), - config: Default::default(), + config: Arc::new(Config::default()), service: Addr::dummy(), project_changes: broadcast::channel(999_999).0, } diff --git a/relay-server/src/services/projects/cache/project.rs b/relay-server/src/services/projects/cache/project.rs index f9edf37777d..de850faa1d1 100644 --- a/relay-server/src/services/projects/cache/project.rs +++ b/relay-server/src/services/projects/cache/project.rs @@ -1,6 +1,7 @@ +use std::marker::PhantomData; use std::sync::Arc; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_quotas::{CachedRateLimits, DataCategory, MetricNamespaceScoping, RateLimits}; use crate::Envelope; @@ -14,12 +15,21 @@ use crate::utils::{CheckLimits, EnvelopeLimiter}; /// A loaded project. pub struct Project<'a> { shared: SharedProject, - config: &'a Config, + config: ConfigSnapshot, + // This lifetime is a leftover from before we started introducing a reloadable + // configuration. It's not yet removed to keep changes a bit more isolated to config. + // + // This will be removed in a follow-up PR. I promise. + _lifetime: PhantomData<&'a ()>, } impl<'a> Project<'a> { - pub(crate) fn new(shared: SharedProject, config: &'a Config) -> Self { - Self { shared, config } + pub(crate) fn new(shared: SharedProject, config: ConfigSnapshot) -> Self { + Self { + shared, + config, + _lifetime: PhantomData, + } } /// Returns a reference to the currently cached project state. @@ -65,7 +75,7 @@ impl<'a> Project<'a> { scoping = state.scope_request(envelope.meta()); envelope.scope(scoping); - if let Err(reason) = state.check_envelope(envelope, self.config) { + if let Err(reason) = state.check_envelope(envelope, &self.config) { return Err(envelope .reject_err(Outcome::Invalid(reason)) .map(|_| reason)); @@ -133,7 +143,7 @@ mod tests { use super::*; - fn create_project(config: &Config, data: Option) -> Project<'_> { + fn create_project(config: ConfigSnapshot, data: Option) -> Project<'static> { let mut project_info = ProjectInfo { project_id: Some(ProjectId::new(42)), ..Default::default() @@ -167,9 +177,9 @@ mod tests { #[tokio::test] async fn test_track_nested_spans_outcomes() { - let config = Default::default(); + let config = relay_config::Config::default().current(); let project = create_project( - &config, + config, Some(json!({ "quotas": [{ "id": "foo", @@ -240,9 +250,9 @@ mod tests { #[tokio::test] async fn test_track_nested_spans_outcomes_predefined() { - let config = Default::default(); + let config = relay_config::Config::default().current(); let project = create_project( - &config, + config, Some(json!({ "quotas": [{ "id": "foo", diff --git a/relay-server/src/services/projects/cache/service.rs b/relay-server/src/services/projects/cache/service.rs index 63b9b3ad91b..1d0b47a9975 100644 --- a/relay-server/src/services/projects/cache/service.rs +++ b/relay-server/src/services/projects/cache/service.rs @@ -82,7 +82,7 @@ impl ProjectCacheService { let project_events_tx = broadcast::channel(PROJECT_EVENTS_CHANNEL_SIZE).0; Self { - store: ProjectStore::new(&config), + store: ProjectStore::new(&config.current()), source, config, scheduled_fetches: FuturesScheduled::default(), diff --git a/relay-server/src/services/projects/cache/state.rs b/relay-server/src/services/projects/cache/state.rs index a57cd07f658..dcb8f2a39c9 100644 --- a/relay-server/src/services/projects/cache/state.rs +++ b/relay-server/src/services/projects/cache/state.rs @@ -8,6 +8,7 @@ use tokio::time::Instant; use arc_swap::ArcSwap; use relay_base_schema::project::ProjectKey; +use relay_config::ConfigSnapshot; use relay_quotas::CachedRateLimits; use relay_statsd::metric; @@ -39,7 +40,7 @@ pub struct ProjectStore { } impl ProjectStore { - pub fn new(config: &relay_config::Config) -> Self { + pub fn new(config: &ConfigSnapshot) -> Self { Self { config: Config::new(config), shared: Default::default(), @@ -266,7 +267,7 @@ struct Config { } impl Config { - fn new(config: &relay_config::Config) -> Self { + fn new(config: &ConfigSnapshot) -> Self { let expiry = config.project_cache_expiry(); let grace_period = config.project_grace_period(); @@ -935,6 +936,8 @@ struct ExpiryTime(Instant); mod tests { use std::time::Duration; + use relay_config::Config; + use super::*; async fn collect_evicted(store: &mut ProjectStore) -> Vec { @@ -961,7 +964,7 @@ mod tests { #[tokio::test(start_paused = true)] async fn test_store_fetch() { let project_key = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(); - let mut store = ProjectStore::new(&Default::default()); + let mut store = ProjectStore::new(&Config::default().current()); let fetch = store.try_begin_fetch(project_key).unwrap(); assert_eq!(fetch.project_key(), project_key); @@ -1003,13 +1006,14 @@ mod tests { async fn test_store_fetch_pending_does_not_replace_state() { let project_key = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(); let mut store = ProjectStore::new( - &relay_config::Config::from_json_value(serde_json::json!({ + &Config::from_json_value(serde_json::json!({ "cache": { "project_expiry": 5, "project_grace_period": 5, } })) - .unwrap(), + .unwrap() + .current(), ); let fetch = store.try_begin_fetch(project_key).unwrap(); @@ -1036,13 +1040,14 @@ mod tests { let project_key1 = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(); let project_key2 = ProjectKey::parse("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(); let mut store = ProjectStore::new( - &relay_config::Config::from_json_value(serde_json::json!({ + &Config::from_json_value(serde_json::json!({ "cache": { "project_expiry": 5, "project_grace_period": 0, } })) - .unwrap(), + .unwrap() + .current(), ); let fetch = store.try_begin_fetch(project_key1).unwrap(); @@ -1075,13 +1080,14 @@ mod tests { let project_key1 = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(); let project_key2 = ProjectKey::parse("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(); let mut store = ProjectStore::new( - &relay_config::Config::from_json_value(serde_json::json!({ + &Config::from_json_value(serde_json::json!({ "cache": { "project_expiry": 5, "project_grace_period": 0, } })) - .unwrap(), + .unwrap() + .current(), ); let fetch = store.try_begin_fetch(project_key1).unwrap(); @@ -1114,13 +1120,14 @@ mod tests { async fn test_store_evict_projects_stale() { let project_key = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(); let mut store = ProjectStore::new( - &relay_config::Config::from_json_value(serde_json::json!({ + &Config::from_json_value(serde_json::json!({ "cache": { "project_expiry": 5, "project_grace_period": 5, } })) - .unwrap(), + .unwrap() + .current(), ); let fetch = store.try_begin_fetch(project_key).unwrap(); @@ -1144,13 +1151,14 @@ mod tests { async fn test_store_no_eviction_during_fetch() { let project_key = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(); let mut store = ProjectStore::new( - &relay_config::Config::from_json_value(serde_json::json!({ + &Config::from_json_value(serde_json::json!({ "cache": { "project_expiry": 5, "project_grace_period": 5, } })) - .unwrap(), + .unwrap() + .current(), ); let fetch = store.try_begin_fetch(project_key).unwrap(); @@ -1189,14 +1197,15 @@ mod tests { async fn test_store_refresh() { let project_key = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(); let mut store = ProjectStore::new( - &relay_config::Config::from_json_value(serde_json::json!({ + &Config::from_json_value(serde_json::json!({ "cache": { "project_expiry": 5, "project_grace_period": 5, "project_refresh_interval": 7, } })) - .unwrap(), + .unwrap() + .current(), ); let fetch = store.try_begin_fetch(project_key).unwrap(); @@ -1238,14 +1247,15 @@ mod tests { async fn test_store_refresh_overtaken_by_eviction() { let project_key = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(); let mut store = ProjectStore::new( - &relay_config::Config::from_json_value(serde_json::json!({ + &Config::from_json_value(serde_json::json!({ "cache": { "project_expiry": 5, "project_grace_period": 5, "project_refresh_interval": 7, } })) - .unwrap(), + .unwrap() + .current(), ); let fetch = store.try_begin_fetch(project_key).unwrap(); @@ -1278,14 +1288,15 @@ mod tests { async fn test_store_refresh_during_eviction() { let project_key = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(); let mut store = ProjectStore::new( - &relay_config::Config::from_json_value(serde_json::json!({ + &Config::from_json_value(serde_json::json!({ "cache": { "project_expiry": 5, "project_grace_period": 5, "project_refresh_interval": 7, } })) - .unwrap(), + .unwrap() + .current(), ); let fetch = store.try_begin_fetch(project_key).unwrap(); diff --git a/relay-server/src/services/projects/project/info.rs b/relay-server/src/services/projects/project/info.rs index 59b3625c9ba..22df7f61b7e 100644 --- a/relay-server/src/services/projects/project/info.rs +++ b/relay-server/src/services/projects/project/info.rs @@ -4,7 +4,7 @@ use chrono::{DateTime, Duration, Utc}; use relay_base_schema::organization::OrganizationId; use relay_base_schema::project::{ProjectId, ProjectKey}; -use relay_config::{Config, UpstreamDescriptor}; +use relay_config::{ConfigSnapshot, UpstreamDescriptor}; use relay_dynamic_config::{Feature, LimitedProjectConfig, ProjectConfig, SignatureVerification}; use relay_filter::matches_any_origin; use relay_quotas::{Quota, Scoping}; @@ -128,7 +128,7 @@ impl ProjectInfo { pub fn check_envelope( &self, envelope: &Envelope, - config: &Config, + config: &ConfigSnapshot, ) -> Result<(), DiscardReason> { // Verify that the stated project id in the DSN matches the public key used to retrieve this // project state. @@ -170,7 +170,7 @@ impl ProjectInfo { fn check_envelope_signature( &self, envelope: &Envelope, - config: &Config, + config: &ConfigSnapshot, ) -> Result<(), DiscardReason> { if envelope.meta().request_trust().is_trusted() { return Ok(()); @@ -205,7 +205,7 @@ impl ProjectInfo { /// If the project state has not been loaded, this check is skipped because the project /// identifier is not yet known. Likewise, this check is skipped for the legacy store endpoint /// which comes without a project ID. The id is later overwritten in `check_envelope`. - fn is_valid_project_id(&self, stated_id: Option, config: &Config) -> bool { + fn is_valid_project_id(&self, stated_id: Option, config: &ConfigSnapshot) -> bool { match (self.project_id, stated_id, config.override_project_ids()) { (Some(actual_id), Some(stated_id), false) => actual_id == stated_id, _ => true, diff --git a/relay-server/src/services/projects/source/mod.rs b/relay-server/src/services/projects/source/mod.rs index 5b24d102cd7..dd000d2eebd 100644 --- a/relay-server/src/services/projects/source/mod.rs +++ b/relay-server/src/services/projects/source/mod.rs @@ -60,7 +60,9 @@ impl ProjectSource { no_cache: bool, current_revision: Revision, ) -> Result { - match self.config.relay_mode() { + let config = self.config.current(); + + match config.relay_mode() { RelayMode::Proxy => return Ok(ProjectState::Dummy.into()), RelayMode::Managed => (), // Proceed with loading the config from redis or upstream } @@ -78,7 +80,7 @@ impl ProjectSource { // // If it is pending, we must fallback to fetching from the upstream. Ok(SourceProjectState::New(state)) => { - let state = state.sanitized(self.config.processing_enabled()); + let state = state.sanitized(config.processing_enabled()); if !state.is_pending() { return Ok(state.into()); } @@ -107,7 +109,7 @@ impl ProjectSource { Ok(match state { SourceProjectState::New(state) => { - SourceProjectState::New(state.sanitized(self.config.processing_enabled())) + SourceProjectState::New(state.sanitized(config.processing_enabled())) } SourceProjectState::NotModified => SourceProjectState::NotModified, }) diff --git a/relay-server/src/services/projects/source/redis.rs b/relay-server/src/services/projects/source/redis.rs index 3a2a8f57216..0b7acb2b12c 100644 --- a/relay-server/src/services/projects/source/redis.rs +++ b/relay-server/src/services/projects/source/redis.rs @@ -1,5 +1,5 @@ use relay_base_schema::project::ProjectKey; -use relay_config::Config; +use relay_config::{Config, ConfigSnapshot}; use relay_redis::{AsyncRedisClient, RedisError}; use relay_statsd::metric; use std::fmt::Debug; @@ -64,10 +64,12 @@ impl RedisProjectSource { revision: Revision, ) -> Result { let mut connection = self.redis.get_connection().await?; + let config = self.config.current(); + // Only check for the revision if we were passed a revision. if let Some(revision) = revision.as_str() { let current_revision: Option = cmd("GET") - .arg(self.get_redis_rev_key(key)) + .arg(get_redis_rev_key(&config, key)) .query_async(&mut connection) .await .map_err(RedisError::Redis)?; @@ -85,7 +87,7 @@ impl RedisProjectSource { } let raw_response_opt: Option> = cmd("GET") - .arg(self.get_redis_project_config_key(key)) + .arg(get_redis_project_config_key(&config, key)) .query_async(&mut connection) .await .map_err(RedisError::Redis)?; @@ -121,16 +123,16 @@ impl RedisProjectSource { Ok(SourceProjectState::New(response)) } } +} - fn get_redis_project_config_key(&self, key: ProjectKey) -> String { - let prefix = self.config.projectconfig_cache_prefix(); - format!("{prefix}:{key}") - } +fn get_redis_project_config_key(config: &ConfigSnapshot, key: ProjectKey) -> String { + let prefix = config.projectconfig_cache_prefix(); + format!("{prefix}:{key}") +} - fn get_redis_rev_key(&self, key: ProjectKey) -> String { - let prefix = self.config.projectconfig_cache_prefix(); - format!("{prefix}:{key}.rev") - } +fn get_redis_rev_key(config: &ConfigSnapshot, key: ProjectKey) -> String { + let prefix = config.projectconfig_cache_prefix(); + format!("{prefix}:{key}.rev") } #[cfg(test)] diff --git a/relay-server/src/services/projects/source/upstream.rs b/relay-server/src/services/projects/source/upstream.rs index ba71f9697fd..4abafe7f46d 100644 --- a/relay-server/src/services/projects/source/upstream.rs +++ b/relay-server/src/services/projects/source/upstream.rs @@ -8,7 +8,7 @@ use std::time::Duration; use futures::future; use itertools::Itertools; use relay_base_schema::project::ProjectKey; -use relay_config::Config; +use relay_config::{Config, ConfigSnapshot}; use relay_dynamic_config::ErrorBoundary; use relay_statsd::metric; use relay_system::{ @@ -262,16 +262,17 @@ impl UpstreamProjectSourceService { /// Creates a new [`UpstreamProjectSourceService`] instance. pub fn new(config: Arc, upstream_relay: Addr) -> Self { let (inner_tx, inner_rx) = mpsc::unbounded_channel(); + let current_config = config.current(); Self { - backoff: RetryBackoff::new(config.http_max_retry_interval()), + backoff: RetryBackoff::new(current_config.http_max_retry_interval()), state_channels: HashMap::new(), fetch_handle: SleepHandle::idle(), upstream_relay, inner_tx, inner_rx, last_failed_fetch: None, - failure_interval: config.http_project_failure_interval(), + failure_interval: current_config.http_project_failure_interval(), config, } } @@ -281,15 +282,16 @@ impl UpstreamProjectSourceService { /// If previous queries succeeded, this will be the general batch interval. Additionally, an /// exponentially increasing backoff is used for retrying the upstream request. fn next_backoff(&mut self) -> Duration { - self.config.query_batch_interval() + self.backoff.next_backoff() + self.config.current().query_batch_interval() + self.backoff.next_backoff() } /// Prepares the batches of the cache and nocache channels which could be used to request the /// project states. fn prepare_batches(&mut self) -> ChannelsBatch { let now = Instant::now(); - let batch_size = self.config.query_batch_size(); - let num_batches = self.config.max_concurrent_queries(); + let config = self.config.current(); + let batch_size = config.query_batch_size(); + let num_batches = config.max_concurrent_queries(); // Pop N items from state_channels. Intuitively, we would use // `state_channels.drain().take(n)`, but that clears the entire hashmap regardless how @@ -377,7 +379,7 @@ impl UpstreamProjectSourceService { /// This assumes that currently no request is running. If the upstream request fails or new /// channels are pushed in the meanwhile, this will reschedule automatically. async fn fetch_states( - config: Arc, + config: ConfigSnapshot, upstream_relay: Addr, channels: ChannelsBatch, ) -> Vec> { @@ -600,7 +602,7 @@ impl UpstreamProjectSourceService { return; } - let config = self.config.clone(); + let config = self.config.current(); let inner_tx = self.inner_tx.clone(); let channels = self.prepare_batches(); let upstream_relay = self.upstream_relay.clone(); @@ -626,7 +628,7 @@ impl UpstreamProjectSourceService { sender, ) = message; - let query_timeout = self.config.query_timeout(); + let query_timeout = self.config.current().query_timeout(); // If there is already channel for the requested project key, we attach to it, // otherwise create a new one. @@ -707,7 +709,7 @@ mod tests { let UpstreamRelay::SendRequest(mut req) = upstream_rx.recv().await.unwrap() else { panic!() }; - req.configure(&config); + req.configure(&config.current()); req }}; } diff --git a/relay-server/src/services/proxy_processor.rs b/relay-server/src/services/proxy_processor.rs index a8474f3805b..6c0da9e1d72 100644 --- a/relay-server/src/services/proxy_processor.rs +++ b/relay-server/src/services/proxy_processor.rs @@ -57,7 +57,8 @@ impl ProxyProcessorService { scoping, } = message; - let upstream = self.config.upstream(); + let config = self.config.current(); + let upstream = config.upstream(); let dsn = PartialDsn::outbound(&scoping, upstream); let mut envelope = Envelope::from_request(None, RequestMeta::outbound(dsn)); @@ -88,7 +89,8 @@ impl ProxyProcessorService { } relay_log::trace!("sending envelope to sentry endpoint"); - let http_encoding = self.config.http_encoding(); + let config = self.config.current(); + let http_encoding = config.http_encoding(); let result = envelope.envelope().to_vec().and_then(|v| { encode_payload(&v.into(), http_encoding).map_err(EnvelopeError::PayloadIoFailed) }); diff --git a/relay-server/src/services/relays.rs b/relay-server/src/services/relays.rs index 9d584182756..016c3def791 100644 --- a/relay-server/src/services/relays.rs +++ b/relay-server/src/services/relays.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use relay_auth::{PublicKey, RelayId}; -use relay_config::{Config, RelayInfo}; +use relay_config::{Config, ConfigSnapshot, RelayInfo}; use relay_system::{ Addr, BroadcastChannel, BroadcastResponse, BroadcastSender, FromMessage, Interface, Service, }; @@ -135,7 +135,7 @@ enum RelayState { impl RelayState { /// Returns `true` if this cache entry is still valid. - fn is_valid_cache(&self, config: &Config) -> bool { + fn is_valid_cache(&self, config: &ConfigSnapshot) -> bool { match *self { RelayState::Exists { checked_at, .. } => { checked_at.elapsed() < config.relay_cache_expiry() @@ -192,12 +192,13 @@ pub struct RelayCacheService { impl RelayCacheService { /// Creates a new [`RelayCache`] service. pub fn new(config: Arc, upstream_relay: Addr) -> Self { + let current_config = config.current(); Self { - static_relays: config.static_relays().clone(), + static_relays: current_config.static_relays().clone(), relays: HashMap::new(), channels: HashMap::new(), fetch_channel: mpsc::channel(1), - backoff: RetryBackoff::new(config.http_max_retry_interval()), + backoff: RetryBackoff::new(current_config.http_max_retry_interval()), delay: SleepHandle::idle(), config, upstream_relay, @@ -215,7 +216,7 @@ impl RelayCacheService { /// If previous queries succeeded, this will be the general batch interval. Additionally, an /// exponentially increasing backoff is used for retrying the upstream request. fn next_backoff(&mut self) -> Duration { - self.config.downstream_relays_batch_interval() + self.backoff.next_backoff() + self.config.current().downstream_relays_batch_interval() + self.backoff.next_backoff() } /// Schedules a batched upstream query with exponential backoff. @@ -308,13 +309,13 @@ impl RelayCacheService { } if let Some(key) = self.relays.get(&relay_id) - && key.is_valid_cache(&self.config) + && key.is_valid_cache(&self.config.current()) { sender.send(key.as_option().cloned()); return; } - if self.config.credentials().is_none() { + if !self.config.current().has_credentials() { relay_log::error!( "no credentials configured. relay {relay_id} cannot send requests to this relay", ); diff --git a/relay-server/src/services/server/mod.rs b/relay-server/src/services/server/mod.rs index b85716de85d..2abaa504342 100644 --- a/relay-server/src/services/server/mod.rs +++ b/relay-server/src/services/server/mod.rs @@ -7,7 +7,7 @@ use axum::extract::Request; use axum::http::{HeaderName, HeaderValue, header}; use axum_server::Handle; use hyper_util::rt::TokioTimer; -use relay_config::Config; +use relay_config::{Config, ConfigSnapshot}; use relay_system::{Controller, Service, Shutdown}; use sentry::integrations::tower::{NewSentryLayer, SentryHttpLayer}; use tokio::net::TcpSocket; @@ -56,7 +56,10 @@ pub enum ServerError { type App = NormalizePath; /// Build the axum application with all routes and middleware. -fn make_app(service: ServiceState, f: impl FnOnce(&Config) -> axum::Router) -> App { +fn make_app( + service: ServiceState, + f: impl FnOnce(&ConfigSnapshot) -> axum::Router, +) -> App { // Build the router middleware into a single service which runs _after_ routing. Service // builder order defines layers added first will be called first. This means: // - Requests go from top to bottom @@ -82,14 +85,14 @@ fn make_app(service: ServiceState, f: impl FnOnce(&Config) -> axum::Router Result { +fn listen(addr: SocketAddr, config: &ConfigSnapshot) -> Result { let socket = match addr { SocketAddr::V4(_) => TcpSocket::new_v4(), SocketAddr::V6(_) => TcpSocket::new_v6(), @@ -101,7 +104,7 @@ fn listen(addr: SocketAddr, config: &Config) -> Result Ok(socket.listen(config.tcp_listen_backlog())?.into_std()?) } -async fn serve(listener: TcpListener, app: App, config: &Config) -> std::io::Result<()> { +async fn serve(listener: TcpListener, app: App, config: &ConfigSnapshot) -> std::io::Result<()> { let handle = Handle::new(); let acceptor = self::acceptor::RelayAcceptor::new() @@ -164,17 +167,19 @@ pub struct HttpServer { impl HttpServer { pub fn new(config: Arc, service: ServiceState) -> Result { + let current_config = config.current(); + // Inform the user about a removed feature. - if config.tls_listen_addr().is_some() - || config.tls_identity_password().is_some() - || config.tls_identity_path().is_some() + if current_config.tls_listen_addr().is_some() + || current_config.tls_identity_password().is_some() + || current_config.tls_identity_path().is_some() { return Err(ServerError::TlsNotSupported); } - let listener = listen(config.listen_addr(), &config)?; - let internal_listener = match config.listen_addr_internal() { - Some(addr) => Some(listen(addr, &config)?), + let listener = listen(current_config.listen_addr(), ¤t_config)?; + let internal_listener = match current_config.listen_addr_internal() { + Some(addr) => Some(listen(addr, ¤t_config)?), None => None, }; @@ -197,12 +202,13 @@ impl Service for HttpServer { listener, internal_listener, } = self; + let current_config = config.current(); - let listen_addr = config.listen_addr(); + let listen_addr = current_config.listen_addr(); relay_log::info!("spawning http server"); relay_log::info!(" listening on http://{listen_addr}/"); - if let Some(internal_addr) = config.listen_addr_internal() { + if let Some(internal_addr) = current_config.listen_addr_internal() { relay_log::info!(" listening on http://{internal_addr}/ [internal]"); } relay_statsd::metric!(counter(RelayCounters::ServerStarting) += 1); @@ -212,13 +218,13 @@ impl Service for HttpServer { let internal = make_app(service, crate::endpoints::internal_routes); tokio::try_join!( - serve(listener, public, &config), - serve(internal_listener, internal, &config), + serve(listener, public, ¤t_config), + serve(internal_listener, internal, ¤t_config), ) .map(drop) } else { let app = make_app(service, crate::endpoints::all_routes); - serve(listener, app, &config).await + serve(listener, app, ¤t_config).await } .expect("axum listener to not fail") } diff --git a/relay-server/src/services/stats.rs b/relay-server/src/services/stats.rs index 6c0d6cd6703..5550edec4e3 100644 --- a/relay-server/src/services/stats.rs +++ b/relay-server/src/services/stats.rs @@ -143,7 +143,7 @@ impl RelayStats { } async fn upstream_status(&self) { - if self.config.relay_mode() == RelayMode::Managed + if self.config.current().relay_mode() == RelayMode::Managed && let Ok(is_outage) = self.upstream_relay.send(IsNetworkOutage).await { metric!(gauge(RelayGauges::NetworkOutage) = u64::from(is_outage)); @@ -225,7 +225,12 @@ impl Service for RelayStats { type Interface = (); async fn run(self, _rx: relay_system::Receiver) { - let Some(mut ticker) = self.config.metrics_periodic_interval().map(interval) else { + let Some(mut ticker) = self + .config + .current() + .metrics_periodic_interval() + .map(interval) + else { return; }; diff --git a/relay-server/src/services/store.rs b/relay-server/src/services/store.rs index 3695845321b..e455cf9ec20 100644 --- a/relay-server/src/services/store.rs +++ b/relay-server/src/services/store.rs @@ -20,7 +20,7 @@ use relay_base_schema::data_category::DataCategory; use relay_base_schema::organization::OrganizationId; use relay_base_schema::project::ProjectId; use relay_common::time::UnixTimestamp; -use relay_config::Config; +use relay_config::{Config, ConfigSnapshot}; use relay_event_schema::protocol::{Event, EventId, SpanV2, datetime_to_timestamp}; use relay_kafka::{ClientError, KafkaClient, KafkaTopic, Message, SerializationOutput}; use relay_metrics::{ @@ -86,7 +86,7 @@ struct Producer { } impl Producer { - pub fn create(config: &Config) -> anyhow::Result { + pub fn create(config: &ConfigSnapshot) -> anyhow::Result { let mut client_builder = KafkaClient::builder(); for topic in KafkaTopic::iter() { @@ -442,7 +442,7 @@ impl StoreService { global_config: GlobalConfigHandle, metric_outcomes: MetricOutcomes, ) -> anyhow::Result { - let producer = Producer::create(&config)?; + let producer = Producer::create(&config.current())?; Ok(Self { pool, config, @@ -569,7 +569,7 @@ impl StoreService { retention, } = message; - let batch_size = self.config.metrics_max_batch_size_bytes(); + let batch_size = self.config.current().metrics_max_batch_size_bytes(); let mut error = None; let global_config = self.global_config.current().unwrap_or_default(); @@ -950,9 +950,10 @@ impl StoreService { let payload = item.payload(); let placeholder: AttachmentPlaceholder<'_> = serde_json::from_slice(&payload).map_err(|_| StoreError::InvalidAttachmentRef)?; + let config = self.config.current(); let location = SignedLocation::::try_from_str(placeholder.location) .ok_or(StoreError::InvalidAttachmentRef)? - .verify(Utc::now(), &self.config) + .verify(Utc::now(), &config) .map_err(|_| StoreError::InvalidAttachmentRef)?; let store_key = location.key; @@ -982,7 +983,7 @@ impl StoreService { let payload = item.payload(); let size = item.len(); - let max_chunk_size = self.config.attachment_chunk_size(); + let max_chunk_size = self.config.current().attachment_chunk_size(); let payload = if size == 0 { AttachmentPayload::Chunked(0) diff --git a/relay-server/src/services/upload.rs b/relay-server/src/services/upload.rs index f33ed3bca33..da939b7fa42 100644 --- a/relay-server/src/services/upload.rs +++ b/relay-server/src/services/upload.rs @@ -18,7 +18,7 @@ use relay_auth::SignatureError; #[cfg(feature = "processing")] use relay_auth::SignatureHeader; use relay_base_schema::project::ProjectId; -use relay_config::{Config, HttpEncoding, UpstreamDescriptor}; +use relay_config::{Config, ConfigSnapshot, HttpEncoding, UpstreamDescriptor}; use relay_quotas::Scoping; use relay_system::{ Addr, AsyncResponse, ConcurrentService, FromMessage, Interface, LoadShed, SendError, Sender, @@ -192,6 +192,7 @@ pub fn create_service( upstream: &Addr, #[cfg(feature = "processing")] objectstore: &Option>, ) -> ConcurrentService { + let current_config = config.current(); let backend = create_backend( config, upstream, @@ -199,12 +200,12 @@ pub fn create_service( objectstore, ); let service = Service { - timeout: Duration::from_secs(config.upload().timeout), + timeout: Duration::from_secs(current_config.upload().timeout), backend, }; ConcurrentService::new(service) .with_backlog_limit(0) - .with_concurrency_limit(config.upload().max_concurrent_requests) + .with_concurrency_limit(current_config.upload().max_concurrent_requests) } fn create_backend( @@ -281,6 +282,7 @@ impl Service { #[cfg(feature = "processing")] Backend::Objectstore { addr, config } => { use crate::services::objectstore::UploadRef; + let config = config.current(); // Create the key: let key = Uuid::now_v7().as_simple().to_string(); @@ -318,7 +320,7 @@ impl Service { upload_id: upload_id.map(|s| s.to_string()), other: Default::default(), } - .try_sign(config) + .try_sign(&config) } } } @@ -341,6 +343,7 @@ impl Service { #[cfg(feature = "processing")] Backend::Objectstore { addr, config } => { use crate::services::objectstore::UploadRef; + let config = config.current(); let Location { project_id, @@ -348,7 +351,7 @@ impl Service { length, upload_id, other, - } = location.verify(received, config)?; + } = location.verify(received, &config)?; let scoping = project.scoping; debug_assert_eq!(scoping.project_id, project_id); @@ -376,7 +379,7 @@ impl Service { upload_id: None, other, } - .try_sign(config) + .try_sign(&config) } } } @@ -502,7 +505,7 @@ impl Location { } #[cfg(feature = "processing")] - fn try_sign(self, config: &Config) -> Result, Error> { + fn try_sign(self, config: &ConfigSnapshot) -> Result, Error> { let uri = self.try_to_uri()?; let secret_key = config.upload_signing_key().ok_or(Error::SigningFailed)?; let signature = secret_key.sign_with_header( @@ -633,7 +636,11 @@ impl SignedLocation { /// /// Fails if the signature is outdated or incorrect. #[cfg(feature = "processing")] - pub fn verify(self, received: DateTime, config: &Config) -> Result, Error> { + pub fn verify( + self, + received: DateTime, + config: &ConfigSnapshot, + ) -> Result, Error> { let location = self.location.try_to_uri()?; let max_age = chrono::Duration::seconds(config.upload().max_age); let public_key = config @@ -866,7 +873,7 @@ impl UpstreamRequest for UploadRequest { Ok(()) } - fn configure(&mut self, config: &Config) { + fn configure(&mut self, config: &ConfigSnapshot) { if let RequestKind::Upload { encoding, .. } = &mut self.kind { *encoding = config.http_encoding(); } @@ -913,7 +920,7 @@ mod tests { fn config( relay_credentials: Credentials, credentials: Option, - ) -> Config { + ) -> ConfigSnapshot { let mut config = Config::from_json_value(serde_json::json!({ "upload": { "credentials": credentials, @@ -928,7 +935,7 @@ mod tests { ..Default::default() }) .unwrap(); - config + config.current() } #[test] diff --git a/relay-server/src/services/upstream.rs b/relay-server/src/services/upstream.rs index 8f750ebb959..94ce25f0025 100644 --- a/relay-server/src/services/upstream.rs +++ b/relay-server/src/services/upstream.rs @@ -18,7 +18,7 @@ use itertools::Itertools; use relay_auth::{ RegisterChallenge, RegisterRequest, RegisterResponse, Registration, SecretKey, Signature, }; -use relay_config::{Config, Credentials, RelayMode, UpstreamDescriptor}; +use relay_config::{Config, ConfigSnapshot, Credentials, RelayMode, UpstreamDescriptor}; use relay_quotas::{ DataCategories, QuotaScope, RateLimit, RateLimitScope, RateLimits, ReasonCode, RetryAfter, Scoping, @@ -459,7 +459,7 @@ pub trait UpstreamRequest: Send + Sync + fmt::Debug { /// creation time the configuration is not available. /// /// This method is optional and defaults to a no-op. - fn configure(&mut self, _config: &Config) {} + fn configure(&mut self, _config: &ConfigSnapshot) {} /// Callback to build the outgoing web request. /// @@ -625,7 +625,7 @@ where self.query.route() } - fn configure(&mut self, config: &Config) { + fn configure(&mut self, config: &ConfigSnapshot) { // This config attribute is needed during `respond`, which does not have access to the // config. For this reason, we need to store it on the request struct. self.max_response_size = config.max_api_payload_size(); @@ -906,13 +906,14 @@ struct SharedClient { impl SharedClient { /// Creates a new `SharedClient` instance. pub fn build(config: Arc) -> Self { + let current_config = config.current(); let reqwest = reqwest::ClientBuilder::new() - .connect_timeout(config.http_connection_timeout()) - .timeout(config.http_timeout()) + .connect_timeout(current_config.http_connection_timeout()) + .timeout(current_config.http_timeout()) // In the forward endpoint, this means that content negotiation is done twice, and the // response body is first decompressed by the client, then re-compressed by the server. .gzip(true) - .hickory_dns(config.http_dns_cache()) + .hickory_dns(current_config.http_dns_cache()) .build() .unwrap(); @@ -927,20 +928,21 @@ impl SharedClient { fn build_request( &self, request: &mut dyn UpstreamRequest, + config: &ConfigSnapshot, ) -> Result { tokio::task::block_in_place(|| { let url = request .upstream() - .unwrap_or_else(|| self.config.upstream()) + .unwrap_or_else(|| config.upstream()) .get_url(request.path().as_ref()); let mut builder = RequestBuilder::reqwest(self.reqwest.request(request.method(), url)); - if let Some(host_header) = self.config.http_host_header() { + if let Some(host_header) = config.http_host_header() { builder.header("Host", host_header.as_bytes()); } if request.set_relay_id() - && let Some(credentials) = self.config.credentials() + && let Some(credentials) = config.credentials() { builder.header("X-Sentry-Relay-Id", credentials.id.to_string()); } @@ -949,7 +951,7 @@ impl SharedClient { if let Some(payload) = request.sign() && let Some(signature) = payload - .create_signature(self.config.credentials().map(|cred| &cred.secret_key)) + .create_signature(config.credentials().map(|cred| &cred.secret_key)) .map_err(|_| UpstreamRequestError::NoCredentials)? { builder.header("x-sentry-relay-signature", &signature.0); @@ -984,6 +986,7 @@ impl SharedClient { &self, request: &dyn UpstreamRequest, response: Response, + config: &ConfigSnapshot, ) -> Result { let status = response.status(); @@ -1015,7 +1018,7 @@ impl SharedClient { // payload stream, regardless of the status code. Parsing the JSON body may fail, which is a // non-fatal failure as the upstream is not expected to always include a valid JSON // response. - let json_result = response.json(self.config.max_api_payload_size()).await; + let json_result = response.json(config.max_api_payload_size()).await; if let Some(upstream_limits) = upstream_limits { Err(UpstreamRequestError::RateLimited(upstream_limits)) @@ -1031,10 +1034,12 @@ impl SharedClient { &self, request: &mut dyn UpstreamRequest, ) -> Result { - request.configure(&self.config); - let client_request = self.build_request(request)?; + let config = self.config.current(); + request.configure(&config); + let client_request = self.build_request(request, &config)?; let response = self.reqwest.execute(client_request).await?; - self.transform_response(request, Response(response)).await + self.transform_response(request, Response(response), &config) + .await } /// Convenience method to send a query to the upstream and await the result. @@ -1212,7 +1217,7 @@ impl AuthState { /// /// - Relays in managed mode require authentication. The state is set to `AuthState::Unknown`. /// - Other Relays do not require authentication. The state is set to `AuthState::Registered`. - pub fn init(config: &Config) -> Self { + pub fn init(config: &ConfigSnapshot) -> Self { match config.relay_mode() { RelayMode::Managed => AuthState::Unknown, _ => AuthState::Registered, @@ -1290,12 +1295,13 @@ impl AuthMonitor { /// Returns `Some` if authentication should be retried. Returns `None` if authentication is /// permanent. fn renew_auth_interval(&self) -> Option { - if self.config.processing_enabled() { + let config = self.config.current(); + if config.processing_enabled() { // processing relays do NOT re-authenticate None } else { // only relays that have a configured auth-interval reauthenticate - self.config.http_auth_interval() + config.http_auth_interval() } } @@ -1319,8 +1325,9 @@ impl AuthMonitor { &mut self, credentials: &Credentials, ) -> Result<(), UpstreamRequestError> { + let config = self.config.current(); relay_log::info!( - descriptor = %self.config.upstream(), + descriptor = %config.upstream(), "registering with upstream" ); @@ -1353,18 +1360,18 @@ impl AuthMonitor { /// - The upstream responded with a permanent rejection (auth denied). /// - All subscibers have shut down and the action channel is closed. pub async fn run(mut self) { - if self.config.relay_mode() != RelayMode::Managed { + let current_config = self.config.current(); + if current_config.relay_mode() != RelayMode::Managed { return; } - let config = self.config.clone(); - let Some(credentials) = config.credentials() else { + let Some(credentials) = current_config.credentials() else { // This is checked during setup by `check_config` and should never happen. relay_log::error!("authentication called without credentials"); return; }; - let mut backoff = RetryBackoff::new(self.config.http_max_retry_interval()); + let mut backoff = RetryBackoff::new(current_config.http_max_retry_interval()); loop { match self.authenticate(credentials).await { @@ -1484,7 +1491,7 @@ impl ConnectionMonitor { /// Performs connection attempts with exponential backoff until successful. async fn connect(client: SharedClient, tx: ActionTx) { - let mut backoff = RetryBackoff::new(client.config.http_max_retry_interval()); + let mut backoff = RetryBackoff::new(client.config.current().http_max_retry_interval()); loop { let next_backoff = backoff.next_backoff(); @@ -1520,7 +1527,7 @@ impl ConnectionMonitor { self.state = ConnectionState::Interrupted(first_error); // Only take action if we exceeded the grace period. - if first_error + self.client.config.http_outage_grace_period() <= now { + if first_error + self.client.config.current().http_outage_grace_period() <= now { let return_tx = return_tx.clone(); let task = relay_system::spawn!(Self::connect(self.client.clone(), return_tx)); self.state = ConnectionState::Reconnecting(task); @@ -1678,6 +1685,7 @@ impl Service for UpstreamRelayService { async fn run(self, mut rx: relay_system::Receiver) { let Self { config } = self; + let current_config = config.current(); let client = SharedClient::build(config.clone()); @@ -1699,10 +1707,10 @@ impl Service for UpstreamRelayService { // and authentication state. let mut broker = UpstreamBroker { client: client.clone(), - queue: UpstreamQueue::new(config.http_retry_delay()), - auth_state: AuthState::init(&config), + queue: UpstreamQueue::new(current_config.http_retry_delay()), + auth_state: AuthState::init(¤t_config), conn: ConnectionMonitor::new(client), - permits: config.max_concurrent_requests(), + permits: current_config.max_concurrent_requests(), action_tx, }; diff --git a/relay-server/src/testutils.rs b/relay-server/src/testutils.rs index 4f08143730d..a1c94ba9baf 100644 --- a/relay-server/src/testutils.rs +++ b/relay-server/src/testutils.rs @@ -89,6 +89,7 @@ pub async fn create_test_processor(config: Config) -> EnvelopeProcessorService { #[cfg(feature = "processing")] let redis_clients = config + .current() .redis() .map(|c| create_redis_clients(c)) .transpose() @@ -124,6 +125,7 @@ pub async fn create_test_processor_with_addrs( ) -> EnvelopeProcessorService { #[cfg(feature = "processing")] let redis_clients = config + .current() .redis() .map(|c| create_redis_clients(c)) .transpose() diff --git a/relay-server/src/utils/forward.rs b/relay-server/src/utils/forward.rs index 0e644be64f7..ca8a0a02709 100644 --- a/relay-server/src/utils/forward.rs +++ b/relay-server/src/utils/forward.rs @@ -10,7 +10,7 @@ use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode, header}; use axum::response::{IntoResponse, Response}; use bytes::Bytes; use hyper::body::{Frame, SizeHint}; -use relay_config::{Config, UpstreamDescriptor}; +use relay_config::{ConfigSnapshot, UpstreamDescriptor}; use relay_system::Addr; use sync_wrapper::SyncWrapper; use tokio::sync::oneshot; @@ -311,8 +311,8 @@ impl ForwardRequestBuilder { self } - /// Applies the specified Relay [`Config`] to the forwarded request. - pub fn with_config(mut self, config: &Config) -> Self { + /// Applies the specified Relay [`ConfigSnapshot`] to the forwarded request. + pub fn with_config(mut self, config: &ConfigSnapshot) -> Self { self.timeout = config.http_timeout(); self } diff --git a/relay-server/src/utils/memory.rs b/relay-server/src/utils/memory.rs index b729aac6061..7e87416aa47 100644 --- a/relay-server/src/utils/memory.rs +++ b/relay-server/src/utils/memory.rs @@ -204,7 +204,8 @@ impl MemoryChecker { /// Checks if the used percentage of memory is below the specified threshold. pub fn check_memory_percent(&self) -> MemoryCheck { let memory = self.memory_stat.memory(); - if memory.used_percent() < self.config.health_max_memory_watermark_percent() { + let config = self.config.current(); + if memory.used_percent() < config.health_max_memory_watermark_percent() { return MemoryCheck::Ok(memory); } @@ -214,7 +215,8 @@ impl MemoryChecker { /// Checks if the used memory (in bytes) is below the specified threshold. pub fn check_memory_bytes(&self) -> MemoryCheck { let memory = self.memory_stat.memory(); - if memory.used < self.config.health_max_memory_watermark_bytes() { + let config = self.config.current(); + if memory.used < config.health_max_memory_watermark_bytes() { return MemoryCheck::Ok(memory); } @@ -227,8 +229,9 @@ impl MemoryChecker { /// enough memory. pub fn check_memory(&self) -> MemoryCheck { let memory = self.memory_stat.memory(); - if memory.used_percent() < self.config.health_max_memory_watermark_percent() - && memory.used < self.config.health_max_memory_watermark_bytes() + let config = self.config.current(); + if memory.used_percent() < config.health_max_memory_watermark_percent() + && memory.used < config.health_max_memory_watermark_bytes() { return MemoryCheck::Ok(memory); } diff --git a/relay-server/src/utils/multipart.rs b/relay-server/src/utils/multipart.rs index ba2dc942c4f..3213f36630d 100644 --- a/relay-server/src/utils/multipart.rs +++ b/relay-server/src/utils/multipart.rs @@ -5,7 +5,7 @@ use axum::extract::Request; use bytes::Bytes; use futures::TryStreamExt; use multer::{Field, Multipart}; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_quotas::DataCategory; use relay_system::Addr; use serde::{Deserialize, Serialize}; @@ -182,7 +182,7 @@ pub trait AttachmentStrategy { &self, field: Field<'static>, item: Managed, - config: &Config, + config: &ConfigSnapshot, ) -> impl Future>, BadStoreRequest>> + Send; } @@ -206,7 +206,7 @@ pub fn read_bytes_into_item( pub async fn read_field_into_item( field: Field<'static>, mut item: Managed, - config: &Config, + config: &ConfigSnapshot, ) -> Result, multer::Error> { let content_type = field .content_type() @@ -246,7 +246,7 @@ pub async fn read_field_into_item( pub async fn multipart_items( mut multipart: Multipart<'static>, - config: &Config, + config: &ConfigSnapshot, attachment_strategy: impl AttachmentStrategy, request_meta: &RequestMeta, outcome_aggregator: &Addr, @@ -330,6 +330,8 @@ pub fn multipart_from_request(request: Request) -> Result, Ba mod tests { use std::convert::Infallible; + use relay_config::Config; + use super::*; fn mock_request_meta() -> RequestMeta { @@ -426,7 +428,8 @@ mod tests { "max_attachment_size": 5 } })) - .unwrap(); + .unwrap() + .current(); struct MockAttachmentStrategy; impl AttachmentStrategy for MockAttachmentStrategy { @@ -434,7 +437,7 @@ mod tests { &self, field: Field<'static>, item: Managed, - config: &Config, + config: &ConfigSnapshot, ) -> Result>, BadStoreRequest> { Ok(Some(read_field_into_item(field, item, config).await?)) } @@ -474,12 +477,13 @@ mod tests { let stream = futures::stream::once(async move { Ok::<_, Infallible>(data) }); - let config = &Config::from_json_value(serde_json::json!({ + let config = Config::from_json_value(serde_json::json!({ "limits": { "max_attachments_size": 5 } })) - .unwrap(); + .unwrap() + .current(); let multipart = Multipart::new(stream, "X-BOUNDARY"); @@ -489,7 +493,7 @@ mod tests { &self, field: Field<'static>, item: Managed, - config: &Config, + config: &ConfigSnapshot, ) -> Result>, BadStoreRequest> { Ok(Some(read_field_into_item(field, item, config).await?)) } @@ -501,7 +505,7 @@ mod tests { let result = multipart_items( multipart, - config, + &config, MockAttachmentStrategy, &mock_request_meta(), &Addr::dummy(), diff --git a/relay-server/src/utils/sizes.rs b/relay-server/src/utils/sizes.rs index 905440b897c..b46ffcfb7c1 100644 --- a/relay-server/src/utils/sizes.rs +++ b/relay-server/src/utils/sizes.rs @@ -1,4 +1,4 @@ -use relay_config::Config; +use relay_config::ConfigSnapshot; use crate::envelope::{Envelope, Item, ItemType}; use crate::integrations::Integration; @@ -12,9 +12,9 @@ use crate::statsd::RelayCounters; /// an `Err` containing the offending item type, in which case the envelope should be discarded /// and a `413 Payload Too Large` response should be given. /// -/// Each envelope item is checked against its limits defined in the [`Config`]. +/// Each envelope item is checked against its limits defined in the [`ConfigSnapshot`]. pub fn check_envelope_size_limits( - config: &Config, + config: &ConfigSnapshot, envelope: &Envelope, ) -> Result<(), DiscardItemType> { // TODO(#6249 / RELAY-272): These limits are built from the old `limits` config, @@ -211,7 +211,7 @@ impl Limit { /// /// If Relay is configured to drop unknown items, this function removes them from the Envelope. All /// known items will be retained. -pub fn remove_unknown_items(config: &Config, envelope: &mut Managed>) { +pub fn remove_unknown_items(config: &ConfigSnapshot, envelope: &mut Managed>) { if config.accept_unknown_items() { return; } diff --git a/relay-server/src/utils/unreal.rs b/relay-server/src/utils/unreal.rs index 3eae73920f2..77f78284d23 100644 --- a/relay-server/src/utils/unreal.rs +++ b/relay-server/src/utils/unreal.rs @@ -1,6 +1,6 @@ use bytes::Bytes; use chrono::{TimeZone, Utc}; -use relay_config::Config; +use relay_config::ConfigSnapshot; use relay_event_schema::protocol::{ AsPair, Breadcrumb, ClientSdkInfo, Context, Contexts, DeviceContext, Event, EventId, GpuContext, LenientString, Level, LogEntry, Message, OsContext, TagEntry, Tags, Timestamp, @@ -24,7 +24,7 @@ const MAX_NUM_UNREAL_LOGS: usize = 40; const CLIENT_SDK_NAME: &str = "unreal.crashreporter"; /// Extracts the items from an Unreal 4 crash report payload. -fn extract_items(payload: Bytes, config: &Config) -> Result { +fn extract_items(payload: Bytes, config: &ConfigSnapshot) -> Result { let mut items = Items::new(); let crash = Unreal4Crash::parse_with_limit(&payload, config.max_envelope_size())?; @@ -56,7 +56,10 @@ fn extract_items(payload: Bytes, config: &Config) -> Result Result { +pub fn expand_unreal( + payload: Bytes, + config: &ConfigSnapshot, +) -> Result { let items = extract_items(payload, config)?; let mut context = items @@ -393,6 +396,7 @@ pub struct ProcessedUnrealReport { #[cfg(test)] mod tests { + use relay_config::Config; use relay_protocol::SerializableAnnotated; use super::*; @@ -472,7 +476,7 @@ mod tests { let payload = Bytes::from_static(bytes); // Everything parses with default config: - let config = Config::default(); + let config = Config::default().current(); let items = extract_items(payload.clone(), &config).unwrap(); assert_eq!(items.len(), 4); @@ -482,7 +486,8 @@ mod tests { "max_attachment_count": 3 } })) - .unwrap(); + .unwrap() + .current(); let items = extract_items(payload, &config).unwrap(); assert_eq!(items.len(), 3); } diff --git a/relay/src/cli.rs b/relay/src/cli.rs index ed26e44bcfd..baa83039afd 100644 --- a/relay/src/cli.rs +++ b/relay/src/cli.rs @@ -60,6 +60,7 @@ pub fn execute() -> Result<()> { // SAFETY: The function cannot be called from a multi threaded environment, // this is the main entry point where no other threads have been spawned yet. unsafe { + let config = config.current(); relay_log::init(config.logging(), config.sentry()); } @@ -147,7 +148,7 @@ pub fn extract_config_env_vars() -> OverridableConfig { pub fn manage_credentials(mut config: Config, matches: &ArgMatches) -> Result<()> { // generate completely new credentials if let Some(matches) = matches.subcommand_matches("generate") { - if config.has_credentials() && !matches.get_flag("overwrite") { + if config.current().has_credentials() && !matches.get_flag("overwrite") { bail!("aborting because credentials already exist. Pass --overwrite to force."); } let credentials = Credentials::generate(); @@ -156,7 +157,7 @@ pub fn manage_credentials(mut config: Config, matches: &ArgMatches) -> Result<() } else { config.replace_credentials(Some(credentials))?; println!("Generated new credentials"); - setup::dump_credentials(&config); + setup::dump_credentials(&config.current()); } } else if let Some(matches) = matches.subcommand_matches("set") { let mut prompted = false; @@ -166,7 +167,7 @@ pub fn manage_credentials(mut config: Config, matches: &ArgMatches) -> Result<() .parse() .map_err(|_| anyhow!("invalid secret key supplied"))?, ), - None => config.credentials().map(|x| x.secret_key.clone()), + None => config.current().credentials().map(|x| x.secret_key.clone()), }; let public_key = match matches.get_one::("public_key") { Some(value) => Some( @@ -174,7 +175,7 @@ pub fn manage_credentials(mut config: Config, matches: &ArgMatches) -> Result<() .parse() .map_err(|_| anyhow!("invalid public key supplied"))?, ), - None => config.credentials().map(|x| x.public_key.clone()), + None => config.current().credentials().map(|x| x.public_key.clone()), }; let id = match matches.get_one::("id").map(String::as_str) { Some("random") => Some(Uuid::new_v4()), @@ -183,7 +184,7 @@ pub fn manage_credentials(mut config: Config, matches: &ArgMatches) -> Result<() .parse() .map_err(|_| anyhow!("invalid relay id supplied"))?, ), - None => config.credentials().map(|x| x.id), + None => config.current().credentials().map(|x| x.id), }; let changed = config.replace_credentials(Some(Credentials { secret_key: match secret_key { @@ -222,10 +223,10 @@ pub fn manage_credentials(mut config: Config, matches: &ArgMatches) -> Result<() } } else { println!("Stored updated credentials:"); - setup::dump_credentials(&config); + setup::dump_credentials(&config.current()); } } else if let Some(matches) = matches.subcommand_matches("remove") { - if config.has_credentials() { + if config.current().has_credentials() { if matches.get_flag("yes") || Confirm::with_theme(get_theme()) .with_prompt("Remove stored credentials?") @@ -238,11 +239,11 @@ pub fn manage_credentials(mut config: Config, matches: &ArgMatches) -> Result<() println!("No credentials"); } } else if matches.subcommand_matches("show").is_some() { - if !config.has_credentials() { + if !config.current().has_credentials() { bail!("no stored credentials"); } else { println!("Credentials:"); - setup::dump_credentials(&config); + setup::dump_credentials(&config.current()); } } else { unreachable!(); @@ -321,11 +322,11 @@ pub fn init_config>(config_path: P, _matches: &ArgMatches) -> Res } let mut config = Config::from_path(&config_path)?; - if config.relay_mode() == RelayMode::Managed && !config.has_credentials() { + if config.current().relay_mode() == RelayMode::Managed && !config.current().has_credentials() { let credentials = Credentials::generate(); config.replace_credentials(Some(credentials))?; println!("Generated new credentials"); - setup::dump_credentials(&config); + setup::dump_credentials(&config.current()); done_something = true; } @@ -365,8 +366,8 @@ pub fn generate_completions(matches: &ArgMatches) -> Result<()> { pub fn run(config: Config, _matches: &ArgMatches) -> Result<()> { setup::dump_spawn_infos(&config); - setup::check_config(&config)?; - setup::init_metrics(&config)?; + setup::check_config(&config.current())?; + setup::init_metrics(&config.current())?; relay_server::run(config)?; Ok(()) } diff --git a/relay/src/healthcheck.rs b/relay/src/healthcheck.rs index f6f0e732407..0d664b9d8a3 100644 --- a/relay/src/healthcheck.rs +++ b/relay/src/healthcheck.rs @@ -15,6 +15,8 @@ pub fn healthcheck(config: &Config, matches: &ArgMatches) -> Result<()> { .get_one::("timeout") .expect("`timeout` is required"); + let config = config.current(); + let addr = matches .get_one::("addr") .copied() diff --git a/relay/src/setup.rs b/relay/src/setup.rs index 8dcbd2f27f3..446abbc7e3d 100644 --- a/relay/src/setup.rs +++ b/relay/src/setup.rs @@ -1,13 +1,13 @@ #[cfg(feature = "processing")] use anyhow::Context; use anyhow::Result; -use relay_config::{Config, RelayMode}; +use relay_config::{Config, ConfigSnapshot, RelayMode}; use relay_server::MemoryStat; use relay_statsd::MetricsConfig; /// Validates that the `batch_size_bytes` of the configuration is correct and doesn't lead to /// deadlocks in the buffer. -fn assert_batch_size_bytes(config: &Config) -> Result<()> { +fn assert_batch_size_bytes(config: &ConfigSnapshot) -> Result<()> { // We create a temporary memory reading used just for the config check. let memory = MemoryStat::current_memory(); @@ -30,8 +30,8 @@ fn assert_batch_size_bytes(config: &Config) -> Result<()> { Ok(()) } -pub fn check_config(config: &Config) -> Result<()> { - if config.relay_mode() == RelayMode::Managed && config.credentials().is_none() { +pub fn check_config(config: &ConfigSnapshot) -> Result<()> { + if config.relay_mode() == RelayMode::Managed && !config.has_credentials() { anyhow::bail!( "relay has no credentials, which are required in managed mode. \ Generate some with \"relay credentials generate\" first.", @@ -73,6 +73,8 @@ pub fn dump_spawn_infos(config: &Config) { config.path().display() ); } + + let config = config.current(); relay_log::info!(" relay mode: {}", config.relay_mode()); match config.relay_id() { @@ -87,7 +89,7 @@ pub fn dump_spawn_infos(config: &Config) { } /// Dumps out credential info. -pub fn dump_credentials(config: &Config) { +pub fn dump_credentials(config: &ConfigSnapshot) { match config.relay_id() { Some(id) => println!(" relay id: {id}"), None => println!(" relay id: -"), @@ -99,7 +101,7 @@ pub fn dump_credentials(config: &Config) { } /// Initialize the metric system. -pub fn init_metrics(config: &Config) -> Result<()> { +pub fn init_metrics(config: &ConfigSnapshot) -> Result<()> { let Some(host) = config.statsd_addr() else { return Ok(()); }; diff --git a/tools/bench-buffer/src/main.rs b/tools/bench-buffer/src/main.rs index 5d1c319adbf..d8b6c4b6939 100644 --- a/tools/bench-buffer/src/main.rs +++ b/tools/bench-buffer/src/main.rs @@ -81,7 +81,7 @@ async fn main() { ); let memory_checker = MemoryChecker::new(MemoryStat::default(), config.clone()); - let buffer = PolymorphicEnvelopeBuffer::from_config(0, &config, memory_checker) + let buffer = PolymorphicEnvelopeBuffer::from_config(0, &config.current(), memory_checker) .await .unwrap();