Skip to main content

relay_config/
config.rs

1use std::collections::btree_map::Entry;
2use std::collections::{BTreeMap, HashMap};
3use std::error::Error;
4use std::io::Write;
5use std::net::{IpAddr, SocketAddr};
6use std::num::{NonZeroU8, NonZeroU16, NonZeroU32};
7use std::path::{Path, PathBuf};
8use std::str::FromStr;
9use std::sync::{Arc, Mutex, PoisonError};
10use std::time::Duration;
11use std::{env, fmt, fs, io};
12
13use anyhow::Context;
14use arc_swap::ArcSwap;
15use relay_auth::{PublicKey, RelayId, SecretKey, generate_key_pair, generate_relay_id};
16use relay_common::Dsn;
17use relay_kafka::{
18    ConfigError as KafkaConfigError, KafkaConfigParam, KafkaTopic, KafkaTopicConfig,
19    TopicAssignments,
20};
21use relay_metrics::MetricNamespace;
22use serde::de::{DeserializeOwned, Unexpected, Visitor};
23use serde::{Deserialize, Deserializer, Serialize, Serializer};
24use uuid::Uuid;
25
26use crate::aggregator::{AggregatorServiceConfig, ScopedAggregatorConfig};
27use crate::byte_size::ByteSize;
28use crate::upstream::UpstreamDescriptor;
29use crate::{RedisConfig, RedisConfigs, RedisConfigsRef, build_redis_configs};
30
31const DEFAULT_NETWORK_OUTAGE_GRACE_PERIOD: u64 = 10;
32
33static CONFIG_YAML_HEADER: &str = r###"# Please see the relevant documentation.
34# Performance tuning: https://docs.sentry.io/product/relay/operating-guidelines/
35# All config options: https://docs.sentry.io/product/relay/options/
36"###;
37
38/// Indicates config related errors.
39#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
40#[non_exhaustive]
41pub enum ConfigErrorKind {
42    /// Failed to open the file.
43    CouldNotOpenFile,
44    /// Failed to save a file.
45    CouldNotWriteFile,
46    /// Parsing YAML failed.
47    BadYaml,
48    /// Parsing JSON failed.
49    BadJson,
50    /// Invalid config value
51    InvalidValue,
52    /// The user attempted to run Relay with processing enabled, but uses a binary that was
53    /// compiled without the processing feature.
54    ProcessingNotAvailable,
55}
56
57impl fmt::Display for ConfigErrorKind {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            Self::CouldNotOpenFile => write!(f, "could not open config file"),
61            Self::CouldNotWriteFile => write!(f, "could not write config file"),
62            Self::BadYaml => write!(f, "could not parse yaml config file"),
63            Self::BadJson => write!(f, "could not parse json config file"),
64            Self::InvalidValue => write!(f, "invalid config value"),
65            Self::ProcessingNotAvailable => write!(
66                f,
67                "was not compiled with processing, cannot enable processing"
68            ),
69        }
70    }
71}
72
73/// Defines the source of a config error
74#[derive(Debug, Default)]
75enum ConfigErrorSource {
76    /// An error occurring independently.
77    #[default]
78    None,
79    /// An error originating from a configuration file.
80    File(PathBuf),
81    /// An error originating in a field override (an env var, or a CLI parameter).
82    FieldOverride(String),
83}
84
85impl fmt::Display for ConfigErrorSource {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        match self {
88            ConfigErrorSource::None => Ok(()),
89            ConfigErrorSource::File(file_name) => {
90                write!(f, " (file {})", file_name.display())
91            }
92            ConfigErrorSource::FieldOverride(name) => write!(f, " (field {name})"),
93        }
94    }
95}
96
97/// Indicates config related errors.
98#[derive(Debug)]
99pub struct ConfigError {
100    source: ConfigErrorSource,
101    kind: ConfigErrorKind,
102}
103
104impl ConfigError {
105    #[inline]
106    fn new(kind: ConfigErrorKind) -> Self {
107        Self {
108            source: ConfigErrorSource::None,
109            kind,
110        }
111    }
112
113    #[inline]
114    fn field(field: &'static str) -> Self {
115        Self {
116            source: ConfigErrorSource::FieldOverride(field.to_owned()),
117            kind: ConfigErrorKind::InvalidValue,
118        }
119    }
120
121    #[inline]
122    fn file(kind: ConfigErrorKind, p: impl AsRef<Path>) -> Self {
123        Self {
124            source: ConfigErrorSource::File(p.as_ref().to_path_buf()),
125            kind,
126        }
127    }
128
129    /// Returns the error kind of the error.
130    pub fn kind(&self) -> ConfigErrorKind {
131        self.kind
132    }
133}
134
135impl fmt::Display for ConfigError {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        write!(f, "{}{}", self.kind(), self.source)
138    }
139}
140
141impl Error for ConfigError {}
142
143enum ConfigFormat {
144    Yaml,
145    Json,
146}
147
148impl ConfigFormat {
149    pub fn extension(&self) -> &'static str {
150        match self {
151            ConfigFormat::Yaml => "yml",
152            ConfigFormat::Json => "json",
153        }
154    }
155}
156
157trait ConfigObject: DeserializeOwned + Serialize {
158    /// The format in which to serialize this configuration.
159    fn format() -> ConfigFormat;
160
161    /// The basename of the config file.
162    fn name() -> &'static str;
163
164    /// The full filename of the config file, including the file extension.
165    fn path(base: &Path) -> PathBuf {
166        base.join(format!("{}.{}", Self::name(), Self::format().extension()))
167    }
168
169    /// Loads the config file from a file within the given directory location.
170    fn load(base: &Path) -> anyhow::Result<Self> {
171        let path = Self::path(base);
172
173        let f = fs::File::open(&path)
174            .with_context(|| ConfigError::file(ConfigErrorKind::CouldNotOpenFile, &path))?;
175        let f = io::BufReader::new(f);
176
177        let mut source = {
178            let file = serde_vars::FileSource::default()
179                .with_variable_prefix("${file:")
180                .with_variable_suffix("}")
181                .with_base_path(base);
182            let env = serde_vars::EnvSource::default()
183                .with_variable_prefix("${")
184                .with_variable_suffix("}");
185            (file, env)
186        };
187        match Self::format() {
188            ConfigFormat::Yaml => {
189                serde_vars::deserialize(serde_yaml::Deserializer::from_reader(f), &mut source)
190                    .with_context(|| ConfigError::file(ConfigErrorKind::BadYaml, &path))
191            }
192            ConfigFormat::Json => {
193                serde_vars::deserialize(&mut serde_json::Deserializer::from_reader(f), &mut source)
194                    .with_context(|| ConfigError::file(ConfigErrorKind::BadJson, &path))
195            }
196        }
197    }
198
199    /// Writes the configuration to a file within the given directory location.
200    fn save(&self, base: &Path) -> anyhow::Result<()> {
201        let path = Self::path(base);
202        let mut options = fs::OpenOptions::new();
203        options.write(true).truncate(true).create(true);
204
205        // Remove all non-user permissions for the newly created file
206        #[cfg(unix)]
207        {
208            use std::os::unix::fs::OpenOptionsExt;
209            options.mode(0o600);
210        }
211
212        let mut f = options
213            .open(&path)
214            .with_context(|| ConfigError::file(ConfigErrorKind::CouldNotWriteFile, &path))?;
215
216        match Self::format() {
217            ConfigFormat::Yaml => {
218                f.write_all(CONFIG_YAML_HEADER.as_bytes())?;
219                serde_yaml::to_writer(&mut f, self)
220                    .with_context(|| ConfigError::file(ConfigErrorKind::CouldNotWriteFile, &path))?
221            }
222            ConfigFormat::Json => serde_json::to_writer_pretty(&mut f, self)
223                .with_context(|| ConfigError::file(ConfigErrorKind::CouldNotWriteFile, &path))?,
224        }
225
226        f.write_all(b"\n").ok();
227
228        Ok(())
229    }
230}
231
232/// Structure used to hold information about configuration overrides via
233/// CLI parameters or environment variables
234#[derive(Debug, Default, Clone)]
235pub struct OverridableConfig {
236    /// The operation mode of this relay.
237    pub mode: Option<String>,
238    /// The instance type of this relay.
239    pub instance: Option<String>,
240    /// The log level of this relay.
241    pub log_level: Option<String>,
242    /// The log format of this relay.
243    pub log_format: Option<String>,
244    /// The upstream relay or sentry instance.
245    pub upstream: Option<String>,
246    /// Alternate upstream provided through a Sentry DSN. Key and project will be ignored.
247    pub upstream_dsn: Option<String>,
248    /// The host the relay should bind to (network interface).
249    pub host: Option<String>,
250    /// The port to bind for the unencrypted relay HTTP server.
251    pub port: Option<String>,
252    /// "true" if processing is enabled "false" otherwise
253    pub processing: Option<String>,
254    /// the kafka bootstrap.servers configuration string
255    pub kafka_url: Option<String>,
256    /// the redis server url
257    pub redis_url: Option<String>,
258    /// The globally unique ID of the relay.
259    pub id: Option<String>,
260    /// The secret key of the relay
261    pub secret_key: Option<String>,
262    /// The public key of the relay
263    pub public_key: Option<String>,
264    /// Outcome source
265    pub outcome_source: Option<String>,
266    /// shutdown timeout
267    pub shutdown_timeout: Option<String>,
268    /// Server name reported in the Sentry SDK.
269    pub server_name: Option<String>,
270}
271
272/// The relay credentials
273#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
274pub struct Credentials {
275    /// The secret key of the relay
276    pub secret_key: SecretKey,
277    /// The public key of the relay
278    pub public_key: PublicKey,
279    /// The globally unique ID of the relay.
280    pub id: RelayId,
281}
282
283impl Credentials {
284    /// Generates new random credentials.
285    pub fn generate() -> Self {
286        relay_log::info!("generating new relay credentials");
287        let (secret_key, public_key) = generate_key_pair();
288        Self {
289            secret_key,
290            public_key,
291            id: generate_relay_id(),
292        }
293    }
294
295    /// Serializes this configuration to JSON.
296    pub fn to_json_string(&self) -> anyhow::Result<String> {
297        serde_json::to_string(self)
298            .with_context(|| ConfigError::new(ConfigErrorKind::CouldNotWriteFile))
299    }
300}
301
302impl ConfigObject for Credentials {
303    fn format() -> ConfigFormat {
304        ConfigFormat::Json
305    }
306    fn name() -> &'static str {
307        "credentials"
308    }
309}
310
311/// Information on a downstream Relay.
312#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase")]
314pub struct RelayInfo {
315    /// The public key that this Relay uses to authenticate and sign requests.
316    pub public_key: PublicKey,
317
318    /// Marks an internal relay that has privileged access to more project configuration.
319    #[serde(default)]
320    pub internal: bool,
321}
322
323impl RelayInfo {
324    /// Creates a new RelayInfo
325    pub fn new(public_key: PublicKey) -> Self {
326        Self {
327            public_key,
328            internal: false,
329        }
330    }
331}
332
333/// The operation mode of a relay.
334#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
335#[serde(rename_all = "camelCase")]
336pub enum RelayMode {
337    /// This relay acts as a proxy for all requests and events.
338    ///
339    /// Events are normalized and rate limits from the upstream are enforced, but the relay will not
340    /// fetch project configurations from the upstream or perform PII stripping. All events are
341    /// accepted unless overridden on the file system.
342    Proxy,
343
344    /// Project configurations are managed by the upstream.
345    ///
346    /// Project configurations are always fetched from the upstream, unless they are statically
347    /// overridden in the file system. This relay must be allowed in the upstream Sentry. This is
348    /// only possible, if the upstream is Sentry directly, or another managed Relay.
349    Managed,
350}
351
352impl<'de> Deserialize<'de> for RelayMode {
353    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
354    where
355        D: Deserializer<'de>,
356    {
357        let s = String::deserialize(deserializer)?;
358        match s.as_str() {
359            "proxy" => Ok(RelayMode::Proxy),
360            "managed" => Ok(RelayMode::Managed),
361            "static" => Err(serde::de::Error::custom(
362                "Relay mode 'static' has been removed. Please use 'managed' or 'proxy' instead.",
363            )),
364            other => Err(serde::de::Error::unknown_variant(
365                other,
366                &["proxy", "managed"],
367            )),
368        }
369    }
370}
371
372impl fmt::Display for RelayMode {
373    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
374        match self {
375            RelayMode::Proxy => write!(f, "proxy"),
376            RelayMode::Managed => write!(f, "managed"),
377        }
378    }
379}
380
381/// The instance type of Relay.
382#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
383#[serde(rename_all = "camelCase")]
384pub enum RelayInstance {
385    /// This Relay is run as a default instance.
386    Default,
387
388    /// This Relay is run as a canary instance where experiments can be run.
389    Canary,
390}
391
392impl RelayInstance {
393    /// Returns `true` if the [`RelayInstance`] is of type [`RelayInstance::Canary`].
394    pub fn is_canary(&self) -> bool {
395        matches!(self, RelayInstance::Canary)
396    }
397}
398
399impl fmt::Display for RelayInstance {
400    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
401        match self {
402            RelayInstance::Default => write!(f, "default"),
403            RelayInstance::Canary => write!(f, "canary"),
404        }
405    }
406}
407
408impl FromStr for RelayInstance {
409    type Err = fmt::Error;
410
411    fn from_str(s: &str) -> Result<Self, Self::Err> {
412        match s {
413            "canary" => Ok(RelayInstance::Canary),
414            _ => Ok(RelayInstance::Default),
415        }
416    }
417}
418
419/// Error returned when parsing an invalid [`RelayMode`].
420#[derive(Clone, Copy, Debug, Eq, PartialEq)]
421pub struct ParseRelayModeError;
422
423impl fmt::Display for ParseRelayModeError {
424    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425        write!(f, "Relay mode must be one of: managed or proxy")
426    }
427}
428
429impl Error for ParseRelayModeError {}
430
431impl FromStr for RelayMode {
432    type Err = ParseRelayModeError;
433
434    fn from_str(s: &str) -> Result<Self, Self::Err> {
435        match s {
436            "proxy" => Ok(RelayMode::Proxy),
437            "managed" => Ok(RelayMode::Managed),
438            _ => Err(ParseRelayModeError),
439        }
440    }
441}
442
443/// Returns `true` if this value is equal to `Default::default()`.
444fn is_default<T: Default + PartialEq>(t: &T) -> bool {
445    *t == T::default()
446}
447
448/// Checks if we are running in docker.
449fn is_docker() -> bool {
450    if fs::metadata("/.dockerenv").is_ok() {
451        return true;
452    }
453
454    fs::read_to_string("/proc/self/cgroup").is_ok_and(|s| s.contains("/docker"))
455}
456
457/// Default value for the "bind" configuration.
458fn default_host() -> IpAddr {
459    if is_docker() {
460        // Docker images rely on this service being exposed
461        "0.0.0.0".parse().unwrap()
462    } else {
463        "127.0.0.1".parse().unwrap()
464    }
465}
466
467/// Controls responses from the readiness health check endpoint based on authentication.
468///
469/// Independent of the the readiness condition, shutdown always switches Relay into unready state.
470#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
471#[serde(rename_all = "lowercase")]
472#[derive(Default)]
473pub enum ReadinessCondition {
474    /// (default) Relay is ready when authenticated and connected to the upstream.
475    ///
476    /// Before authentication has succeeded and during network outages, Relay responds as not ready.
477    /// Relay reauthenticates based on the `http.auth_interval` parameter. During reauthentication,
478    /// Relay remains ready until authentication fails.
479    ///
480    /// Authentication is only required for Relays in managed mode. Other Relays will only check for
481    /// network outages.
482    #[default]
483    Authenticated,
484    /// Relay reports readiness regardless of the authentication and networking state.
485    Always,
486}
487
488/// Relay specific configuration values.
489#[derive(Serialize, Deserialize, Debug, Clone)]
490#[serde(default)]
491pub struct Relay {
492    /// The operation mode of this Relay.
493    pub mode: RelayMode,
494    /// The instance type of this Relay.
495    pub instance: RelayInstance,
496    /// The upstream Relay or Sentry instance.
497    pub upstream: UpstreamDescriptor,
498    /// The upstream advertised to downstream Relay instances.
499    ///
500    /// This value will be advertised to downstream Relays as the upstream to use when forwarding
501    /// data. It can be used for traffic routing and balancing, it must not redirect to a different
502    /// Sentry instance.
503    ///
504    /// Downstream Relays will treat the advertised upstream as the same logical component as this instance
505    /// and re-use already established authentication keys.
506    pub advertised_upstream: Option<UpstreamDescriptor>,
507    /// The host the relay should bind to (network interface).
508    pub host: IpAddr,
509    /// The port to bind for the unencrypted relay HTTP server.
510    pub port: u16,
511    /// The host the relay should bind to (network interface) for internally exposed APIs, like
512    /// health checks.
513    ///
514    /// If not configured, internal routes are exposed on the main HTTP server.
515    ///
516    /// Note: configuring the internal http server on an address which overlaps with the main
517    /// server (e.g. main on `0.0.0.0:3000` and internal on `127.0.0.1:3000`) is a misconfiguration
518    /// resulting in approximately half of the requests sent to `127.0.0.1:3000` to fail, as the handling
519    /// http server is chosen by the operating system 'at random'.
520    ///
521    /// As a best practice you should always choose different ports to avoid this issue.
522    ///
523    /// Defaults to [`Self::host`].
524    pub internal_host: Option<IpAddr>,
525    /// The port to bind for internally exposed APIs.
526    ///
527    /// Defaults to [`Self::port`].
528    pub internal_port: Option<u16>,
529    /// Always override project IDs from the URL and DSN with the identifier used at the upstream.
530    ///
531    /// Enable this setting for Relays used to redirect traffic to a migrated Sentry instance.
532    /// Validation of project identifiers can be safely skipped in these cases.
533    #[serde(skip_serializing_if = "is_default")]
534    pub override_project_ids: bool,
535    /// Interval in seconds for Relay to check if its configuration changed.
536    ///
537    /// If configured Relay will periodically check its configuration for changes
538    /// and hot reload it.
539    ///
540    /// Hot reloading is only supported for a limited set of values.
541    ///
542    /// Defaults to `None` / off.
543    pub config_reload_interval: Option<u64>,
544}
545
546impl Default for Relay {
547    fn default() -> Self {
548        Relay {
549            mode: RelayMode::Managed,
550            instance: RelayInstance::Default,
551            upstream: "https://sentry.io/".parse().unwrap(),
552            advertised_upstream: None,
553            host: default_host(),
554            port: 3000,
555            internal_host: None,
556            internal_port: None,
557            override_project_ids: false,
558            config_reload_interval: None,
559        }
560    }
561}
562
563/// Control the metrics.
564#[derive(Serialize, Deserialize, Debug, Clone)]
565#[serde(default)]
566pub struct Metrics {
567    /// Hostname and port of the statsd server.
568    ///
569    /// Defaults to `None`.
570    pub statsd: Option<String>,
571    /// Buffer size used for metrics sent to the statsd socket.
572    ///
573    /// Defaults to `None`.
574    pub statsd_buffer_size: Option<usize>,
575    /// Common prefix that should be added to all metrics.
576    ///
577    /// Defaults to `"sentry.relay"`.
578    pub prefix: String,
579    /// Default tags to apply to all metrics.
580    pub default_tags: BTreeMap<String, String>,
581    /// Tag name to report the hostname to for each metric. Defaults to not sending such a tag.
582    pub hostname_tag: Option<String>,
583    /// Interval for periodic metrics emitted from Relay.
584    ///
585    /// Setting it to `0` seconds disables the periodic metrics.
586    /// Defaults to 5 seconds.
587    pub periodic_secs: u64,
588}
589
590impl Default for Metrics {
591    fn default() -> Self {
592        Metrics {
593            statsd: None,
594            statsd_buffer_size: None,
595            prefix: "sentry.relay".into(),
596            default_tags: BTreeMap::new(),
597            hostname_tag: None,
598            periodic_secs: 5,
599        }
600    }
601}
602
603/// Controls various limits
604#[derive(Serialize, Deserialize, Debug, Clone)]
605#[serde(default)]
606pub struct Limits {
607    /// How many requests can be sent concurrently from Relay to the upstream before Relay starts
608    /// buffering.
609    pub max_concurrent_requests: usize,
610    /// How many queries can be sent concurrently from Relay to the upstream before Relay starts
611    /// buffering.
612    ///
613    /// The concurrency of queries is additionally constrained by `max_concurrent_requests`.
614    pub max_concurrent_queries: usize,
615    /// The maximum payload size for events.
616    pub max_event_size: ByteSize,
617    /// The maximum size for each attachment.
618    pub max_attachment_size: ByteSize,
619    /// The maximum amount of attachments in a single envelope.
620    pub max_attachment_count: usize,
621    /// The maximum combined size for all attachments in an envelope or request.
622    pub max_attachments_size: ByteSize,
623    /// The maximum size for a TUS upload request body.
624    ///
625    /// Applies to incoming request bytes at every hop and also to decoded bytes when writing to
626    /// objectstore.
627    pub max_upload_size: ByteSize,
628    /// The maximum combined size for all client reports in an envelope or request.
629    pub max_client_reports_size: ByteSize,
630    /// The maximum number of client report items per envelope.
631    pub max_client_reports_count: usize,
632    /// The maximum payload size for a monitor check-in.
633    pub max_check_in_size: ByteSize,
634    /// The maximum payload size for an entire envelopes. Individual limits still apply.
635    pub max_envelope_size: ByteSize,
636    /// The maximum combined size for all sessions in an envelope in bytes.
637    pub max_sessions_size: ByteSize,
638    /// The maximum number of session items per envelope.
639    pub max_session_count: usize,
640    /// The maximum payload size for general API requests.
641    pub max_api_payload_size: ByteSize,
642    /// The maximum payload size for file uploads and chunks.
643    pub max_api_file_upload_size: ByteSize,
644    /// The maximum payload size for chunks
645    pub max_api_chunk_upload_size: ByteSize,
646    /// The maximum payload size for a profile
647    pub max_profile_size: ByteSize,
648    /// The maximum payload size for a trace metric.
649    pub max_trace_metric_size: ByteSize,
650    /// The maximum payload size for a log.
651    pub max_log_size: ByteSize,
652    /// The maximum number of operations that can occur in a log expansion.
653    pub max_expanded_log_operations: usize,
654    /// The maximum number of operations that can occur in a span expansion.
655    pub max_expanded_span_operations: usize,
656    /// The maximum number of operations that can occur in a trace metric expansion.
657    pub max_expanded_trace_metric_operations: usize,
658    /// The maximum payload size for a span.
659    pub max_span_size: ByteSize,
660    /// The maximum amount of standalone transaction spans per envelope.
661    pub max_standalone_span_count: usize,
662    /// The maximum payload size for an item container.
663    pub max_container_size: ByteSize,
664    /// The maximum payload size for metric buckets.
665    pub max_metric_buckets_size: ByteSize,
666    /// The maximum payload size for a compressed replay.
667    pub max_replay_compressed_size: ByteSize,
668    /// The maximum payload size for an uncompressed replay.
669    #[serde(alias = "max_replay_size")]
670    max_replay_uncompressed_size: ByteSize,
671    /// The maximum size for a replay recording Kafka message.
672    pub max_replay_message_size: ByteSize,
673    /// The byte size limit up to which Relay will retain
674    /// keys of invalid/removed attributes.
675    ///
676    /// This is only relevant for EAP items (spans, logs, …).
677    /// In principle, we want to record all deletions of attributes,
678    /// but we have to institute some limit to protect our infrastructure
679    /// against excessive metadata sizes.
680    ///
681    /// Defaults to 10KiB.
682    pub max_removed_attribute_key_size: ByteSize,
683    /// The maximum number of threads to spawn for CPU and web work, each.
684    ///
685    /// The total number of threads spawned will roughly be `2 * max_thread_count`. Defaults to
686    /// the number of logical CPU cores on the host.
687    pub max_thread_count: usize,
688    /// Controls the maximum concurrency of each worker thread.
689    ///
690    /// Increasing the concurrency, can lead to a better utilization of worker threads by
691    /// increasing the amount of I/O done concurrently.
692    //
693    /// Currently has no effect on defaults to `1`.
694    pub max_pool_concurrency: usize,
695    /// The maximum number of seconds a query is allowed to take across retries. Individual requests
696    /// have lower timeouts. Defaults to 30 seconds.
697    pub query_timeout: u64,
698    /// The maximum number of seconds to wait for pending envelopes after receiving a shutdown
699    /// signal.
700    pub shutdown_timeout: u64,
701    /// Server keep-alive timeout in seconds.
702    ///
703    /// By default, keep-alive is set to 5 seconds.
704    pub keepalive_timeout: u64,
705    /// Server idle timeout in seconds.
706    ///
707    /// The idle timeout limits the amount of time a connection is kept open without activity.
708    /// Setting this too short may abort connections before Relay is able to send a response.
709    ///
710    /// By default there is no idle timeout.
711    pub idle_timeout: Option<u64>,
712    /// Sets the maximum number of concurrent connections.
713    ///
714    /// Upon reaching the limit, the server will stop accepting connections.
715    ///
716    /// By default there is no limit.
717    pub max_connections: Option<usize>,
718    /// The TCP listen backlog.
719    ///
720    /// Configures the TCP listen backlog for the listening socket of Relay.
721    /// See [`man listen(2)`](https://man7.org/linux/man-pages/man2/listen.2.html)
722    /// for a more detailed description of the listen backlog.
723    ///
724    /// Defaults to `1024`, a value [google has been using for a long time](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=19f92a030ca6d772ab44b22ee6a01378a8cb32d4).
725    pub tcp_listen_backlog: u32,
726}
727
728impl Default for Limits {
729    fn default() -> Self {
730        Limits {
731            max_concurrent_requests: 100,
732            max_concurrent_queries: 5,
733            max_event_size: ByteSize::mebibytes(1),
734            max_attachment_size: ByteSize::mebibytes(200),
735            max_attachment_count: 30,
736            max_attachments_size: ByteSize::mebibytes(200),
737            max_upload_size: ByteSize::mebibytes(10240),
738            max_client_reports_size: ByteSize::kibibytes(100),
739            max_client_reports_count: 100,
740            max_check_in_size: ByteSize::kibibytes(100),
741            max_envelope_size: ByteSize::mebibytes(200),
742            max_sessions_size: ByteSize::mebibytes(10),
743            max_session_count: 100,
744            max_api_payload_size: ByteSize::mebibytes(20),
745            max_api_file_upload_size: ByteSize::mebibytes(40),
746            max_api_chunk_upload_size: ByteSize::mebibytes(100),
747            max_profile_size: ByteSize::mebibytes(50),
748            max_trace_metric_size: ByteSize::mebibytes(1),
749            max_log_size: ByteSize::mebibytes(2),
750            max_expanded_log_operations: 3_000_000,
751            max_expanded_span_operations: 3_000_000,
752            max_expanded_trace_metric_operations: 3_000_000,
753            max_span_size: ByteSize::mebibytes(10),
754            max_standalone_span_count: 25,
755            max_container_size: ByteSize::mebibytes(12),
756            max_metric_buckets_size: ByteSize::mebibytes(1),
757            max_replay_compressed_size: ByteSize::mebibytes(10),
758            max_replay_uncompressed_size: ByteSize::mebibytes(100),
759            max_replay_message_size: ByteSize::mebibytes(15),
760            max_thread_count: num_cpus::get(),
761            max_pool_concurrency: 1,
762            query_timeout: 30,
763            shutdown_timeout: 10,
764            keepalive_timeout: 5,
765            idle_timeout: None,
766            max_connections: None,
767            tcp_listen_backlog: 1024,
768            max_removed_attribute_key_size: ByteSize::kibibytes(10),
769        }
770    }
771}
772
773/// Controls traffic steering.
774#[derive(Debug, Default, Deserialize, Serialize, Clone)]
775#[serde(default)]
776pub struct Routing {
777    /// Accept and forward unknown Envelope items to the upstream.
778    ///
779    /// Forwarding unknown items should be enabled in most cases to allow proxying traffic for newer
780    /// SDK versions. The upstream in Sentry makes the final decision on which items are valid. If
781    /// this is disabled, just the unknown items are removed from Envelopes, and the rest is
782    /// processed as usual.
783    ///
784    /// Defaults to `true` for all Relay modes other than processing mode. In processing mode, this
785    /// is disabled by default since the item cannot be handled.
786    pub accept_unknown_items: Option<bool>,
787}
788
789/// Http content encoding for both incoming and outgoing web requests.
790#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
791#[serde(rename_all = "lowercase")]
792pub enum HttpEncoding {
793    /// Identity function without no compression.
794    ///
795    /// This is the default encoding and does not require the presence of the `content-encoding`
796    /// HTTP header.
797    #[default]
798    Identity,
799    /// Compression using a [zlib](https://en.wikipedia.org/wiki/Zlib) structure with
800    /// [deflate](https://en.wikipedia.org/wiki/DEFLATE) encoding.
801    ///
802    /// These structures are defined in [RFC 1950](https://datatracker.ietf.org/doc/html/rfc1950)
803    /// and [RFC 1951](https://datatracker.ietf.org/doc/html/rfc1951).
804    Deflate,
805    /// A format using the [Lempel-Ziv coding](https://en.wikipedia.org/wiki/LZ77_and_LZ78#LZ77)
806    /// (LZ77), with a 32-bit CRC.
807    ///
808    /// This is the original format of the UNIX gzip program. The HTTP/1.1 standard also recommends
809    /// that the servers supporting this content-encoding should recognize `x-gzip` as an alias, for
810    /// compatibility purposes.
811    Gzip,
812    /// A format using the [Brotli](https://en.wikipedia.org/wiki/Brotli) algorithm.
813    Br,
814    /// A format using the [Zstd](https://en.wikipedia.org/wiki/Zstd) compression algorithm.
815    Zstd,
816}
817
818impl HttpEncoding {
819    /// Parses a [`HttpEncoding`] from its `content-encoding` header value.
820    ///
821    /// Accepts aliases, whitespace, and mixed case; unknown values fall back to identity.
822    /// Use [`Self::parse_strict`] when validating an incoming request header.
823    pub fn parse(str: &str) -> Self {
824        let str = str.trim();
825        if str.eq_ignore_ascii_case("zstd") {
826            Self::Zstd
827        } else if str.eq_ignore_ascii_case("br") {
828            Self::Br
829        } else if str.eq_ignore_ascii_case("gzip") || str.eq_ignore_ascii_case("x-gzip") {
830            Self::Gzip
831        } else if str.eq_ignore_ascii_case("deflate") {
832            Self::Deflate
833        } else {
834            Self::Identity
835        }
836    }
837
838    /// Parses a canonical `Content-Encoding` value, returning `None` for unsupported values.
839    pub fn parse_strict(value: &str) -> Option<Self> {
840        match value {
841            "identity" => Some(Self::Identity),
842            "deflate" => Some(Self::Deflate),
843            "gzip" => Some(Self::Gzip),
844            "br" => Some(Self::Br),
845            "zstd" => Some(Self::Zstd),
846            _ => None,
847        }
848    }
849
850    /// Returns the value for the `content-encoding` HTTP header.
851    ///
852    /// Returns `None` for [`Identity`](Self::Identity), and `Some` for other encodings.
853    pub fn name(&self) -> Option<&'static str> {
854        match self {
855            Self::Identity => None,
856            Self::Deflate => Some("deflate"),
857            Self::Gzip => Some("gzip"),
858            Self::Br => Some("br"),
859            Self::Zstd => Some("zstd"),
860        }
861    }
862}
863
864/// Controls authentication with upstream.
865#[derive(Serialize, Deserialize, Debug, Clone)]
866#[serde(default)]
867pub struct Http {
868    /// Timeout for upstream requests in seconds.
869    ///
870    /// This timeout covers the time from sending the request until receiving response headers.
871    /// Neither the connection process and handshakes, nor reading the response body is covered in
872    /// this timeout.
873    pub timeout: u32,
874    /// Timeout for establishing connections with the upstream in seconds.
875    ///
876    /// This includes SSL handshakes. Relay reuses connections when the upstream supports connection
877    /// keep-alive. Connections are retained for a maximum 75 seconds, or 15 seconds of inactivity.
878    pub connection_timeout: u32,
879    /// Maximum interval between failed request retries in seconds.
880    pub max_retry_interval: u32,
881    /// The custom HTTP Host header to send to the upstream.
882    pub host_header: Option<String>,
883    /// The interval in seconds at which Relay attempts to reauthenticate with the upstream server.
884    ///
885    /// Re-authentication happens even when Relay is idle. If authentication fails, Relay reverts
886    /// back into startup mode and tries to establish a connection. During this time, incoming
887    /// envelopes will be buffered.
888    ///
889    /// Defaults to `600` (10 minutes).
890    pub auth_interval: Option<u64>,
891    /// The maximum time of experiencing uninterrupted network failures until Relay considers that
892    /// it has encountered a network outage in seconds.
893    ///
894    /// During a network outage relay will try to reconnect and will buffer all upstream messages
895    /// until it manages to reconnect.
896    pub outage_grace_period: u64,
897    /// The time Relay waits before retrying an upstream request, in seconds.
898    ///
899    /// This time is only used before going into a network outage mode.
900    pub retry_delay: u64,
901    /// The interval in seconds for continued failed project fetches at which Relay will error.
902    ///
903    /// A successful fetch resets this interval. Relay does nothing during long
904    /// times without emitting requests.
905    pub project_failure_interval: u64,
906    /// Content encoding to apply to upstream store requests.
907    ///
908    /// By default, Relay applies `zstd` content encoding to compress upstream requests. Compression
909    /// can be disabled to reduce CPU consumption, but at the expense of increased network traffic.
910    ///
911    /// This setting applies to all store requests of SDK data, including events, transactions,
912    /// envelopes and sessions. At the moment, this does not apply to Relay's internal queries.
913    ///
914    /// Available options are:
915    ///
916    ///  - `identity`: Disables compression.
917    ///  - `deflate`: Compression using a zlib header with deflate encoding.
918    ///  - `gzip` (default): Compression using gzip.
919    ///  - `br`: Compression using the brotli algorithm.
920    ///  - `zstd`: Compression using the zstd algorithm.
921    pub encoding: HttpEncoding,
922    /// Submit metrics globally through a shared endpoint.
923    ///
924    /// As opposed to regular envelopes which are sent to an endpoint inferred from the project's
925    /// DSN, this submits metrics to the global endpoint with Relay authentication.
926    ///
927    /// This option does not have any effect on processing mode.
928    pub global_metrics: bool,
929    /// Controls whether the forward endpoint is enabled.
930    ///
931    /// The forward endpoint forwards unknown API requests to the upstream.
932    ///
933    /// Relay instances with processing enabled are expected to support the latest API and do never
934    /// support forwarding requests to Sentry.
935    pub forward: bool,
936    /// Enables an async DNS resolver through the `hickory-dns` crate, which uses an LRU cache for
937    /// the resolved entries. This helps to limit the amount of requests made to the upstream DNS
938    /// server (important for K8s infrastructure).
939    pub dns_cache: bool,
940}
941
942impl Default for Http {
943    fn default() -> Self {
944        Http {
945            timeout: 5,
946            connection_timeout: 3,
947            max_retry_interval: 60, // 1 minute
948            host_header: None,
949            auth_interval: Some(600), // 10 minutes
950            outage_grace_period: DEFAULT_NETWORK_OUTAGE_GRACE_PERIOD,
951            retry_delay: 1,
952            project_failure_interval: 90,
953            encoding: HttpEncoding::Zstd,
954            global_metrics: false,
955            forward: true,
956            dns_cache: true,
957        }
958    }
959}
960
961/// Strategy used to assign envelopes to buffer partitions.
962#[derive(Clone, Copy, Debug, Eq, PartialEq, Default, Deserialize, Serialize)]
963#[serde(rename_all = "snake_case")]
964pub enum EnvelopeSpoolPartitioning {
965    /// Envelopes with the same project key pair land on the same partition.
966    ///
967    /// Keeps per-project state, disk files, and event ordering co-located on one partition.
968    ProjectKeyPair,
969    /// Envelopes are distributed across partitions in a round-robin fashion (default).
970    ///
971    /// This prevents "hot" partitions when a single project pair dominates traffic, but has
972    /// trade-offs:
973    /// - Per-project LIFO ordering is no longer preserved across partitions.
974    /// - Per-partition memory footprint grows since every partition sees every project.
975    #[default]
976    RoundRobin,
977}
978
979/// Persistent buffering configuration for incoming envelopes.
980#[derive(Debug, Serialize, Deserialize, Clone)]
981#[serde(default)]
982pub struct EnvelopeSpool {
983    /// The path of the SQLite database file(s) which persist the data.
984    ///
985    /// Based on the number of partitions, more database files will be created within the same path.
986    ///
987    /// If not set, the envelopes will be buffered in memory.
988    pub path: Option<PathBuf>,
989    /// The maximum size of the buffer to keep, in bytes.
990    ///
991    /// When the on-disk buffer reaches this size, new envelopes will be dropped.
992    ///
993    /// Defaults to 500MB.
994    pub max_disk_size: ByteSize,
995    /// Size of the batch of compressed envelopes that are spooled to disk at once.
996    ///
997    /// Note that this is the size after which spooling will be triggered but it does not guarantee
998    /// that exactly this size will be spooled, it can be greater or equal.
999    ///
1000    /// Defaults to 10 KiB.
1001    pub batch_size_bytes: ByteSize,
1002    /// Time after which a batch is flushed, regardless of batch size.
1003    ///
1004    /// The age of the batch is only checked when a new envelope comes in, but in practice this
1005    /// has the desired effect: High-volume projects always form full batches, low-volume batches
1006    /// flush individual envelopes to keep memory usage low.
1007    pub flush_timeout_secs: Option<u64>,
1008    /// Maximum time between receiving the envelope and processing it.
1009    ///
1010    /// When envelopes spend too much time in the buffer (e.g. because their project cannot be loaded),
1011    /// they are dropped.
1012    ///
1013    /// Defaults to 24h.
1014    pub max_envelope_delay_secs: u64,
1015    /// The refresh frequency in ms of how frequently disk usage is updated by querying SQLite
1016    /// internal page stats.
1017    ///
1018    /// Defaults to 100ms.
1019    pub disk_usage_refresh_frequency_ms: u64,
1020    /// The relative memory usage above which the buffer service will stop dequeueing envelopes.
1021    ///
1022    /// Only applies when [`Self::path`] is set.
1023    ///
1024    /// This value should be lower than [`Health::max_memory_percent`] to prevent flip-flopping.
1025    ///
1026    /// Warning: This threshold can cause the buffer service to deadlock when the buffer consumes
1027    /// excessive memory (as influenced by [`Self::batch_size_bytes`]).
1028    ///
1029    /// This scenario arises when the buffer stops spooling due to reaching the
1030    /// [`Self::max_backpressure_memory_percent`] limit, but the batch threshold for spooling
1031    /// ([`Self::batch_size_bytes`]) is never reached. As a result, no data is spooled, memory usage
1032    /// continues to grow, and the system becomes deadlocked.
1033    ///
1034    /// ### Example
1035    /// Suppose the system has 1GB of available memory and is configured to spool only after
1036    /// accumulating 10GB worth of envelopes. If Relay consumes 900MB of memory, it will stop
1037    /// unspooling due to reaching the [`Self::max_backpressure_memory_percent`] threshold.
1038    ///
1039    /// However, because the buffer hasn't accumulated the 10GB needed to trigger spooling,
1040    /// no data will be offloaded. Memory usage keeps increasing until it hits the
1041    /// [`Health::max_memory_percent`] threshold, e.g., at 950MB. At this point:
1042    ///
1043    /// - No more envelopes are accepted.
1044    /// - The buffer remains stuck, as unspooling won’t resume until memory drops below 900MB which
1045    ///   will not happen.
1046    /// - A deadlock occurs, with the system unable to recover without manual intervention.
1047    ///
1048    /// Defaults to 90% (5% less than max memory).
1049    pub max_backpressure_memory_percent: f32,
1050    /// Number of partitions of the buffer.
1051    ///
1052    /// A partition is a separate instance of the buffer which has its own isolated queue, stacks
1053    /// and other resources.
1054    ///
1055    /// Defaults to 1.
1056    pub partitions: NonZeroU8,
1057    /// Strategy used to assign envelopes to buffer partitions.
1058    ///
1059    /// Defaults to partitioning by `ProjectKeyPair`, which keeps all envelopes of a given project
1060    /// pair on the same partition. See [`EnvelopeSpoolPartitioning`] for alternatives and
1061    /// trade-offs.
1062    pub partitioning: EnvelopeSpoolPartitioning,
1063    /// Maximum number of envelopes unspooled from disk per second.
1064    ///
1065    /// The limit applies per Relay instance, not per spool partition.
1066    ///
1067    /// Defaults to `None`, which disables the limit.
1068    pub max_unspool_envelopes_per_second: Option<NonZeroU32>,
1069    /// Whether the database defined in `path` is on an ephemeral storage disk.
1070    ///
1071    /// With `ephemeral: true`, Relay does not spool in-flight data to disk
1072    /// during graceful shutdown. Instead, it attempts to process all data before it terminates.
1073    ///
1074    /// Defaults to `false`.
1075    pub ephemeral: bool,
1076}
1077
1078impl Default for EnvelopeSpool {
1079    fn default() -> Self {
1080        Self {
1081            path: None,
1082            max_disk_size: ByteSize::mebibytes(500),
1083            batch_size_bytes: ByteSize::kibibytes(10),
1084            max_envelope_delay_secs: 24 * 60 * 60,
1085            disk_usage_refresh_frequency_ms: 100,
1086            max_backpressure_memory_percent: 0.8,
1087            partitions: NonZeroU8::new(1).unwrap(),
1088            partitioning: EnvelopeSpoolPartitioning::default(),
1089            ephemeral: false,
1090            flush_timeout_secs: None,
1091            max_unspool_envelopes_per_second: None,
1092        }
1093    }
1094}
1095
1096/// Persistent buffering configuration.
1097#[derive(Debug, Serialize, Deserialize, Default, Clone)]
1098#[serde(default)]
1099pub struct Spool {
1100    /// Configuration for envelope spooling.
1101    pub envelopes: EnvelopeSpool,
1102}
1103
1104/// Controls internal caching behavior.
1105#[derive(Serialize, Deserialize, Debug, Clone)]
1106#[serde(default)]
1107pub struct Cache {
1108    /// The full project state will be requested by this Relay if set to `true`.
1109    ///
1110    /// Relay instances that receive the full project config have full access to quota config
1111    /// and perform dynamic sampling.
1112    pub project_request_full_config: bool,
1113    /// The cache timeout for project configurations in seconds.
1114    pub project_expiry: u32,
1115    /// Continue using project state this many seconds after cache expiry while a new state is
1116    /// being fetched. This is added on top of `project_expiry`.
1117    ///
1118    /// Default is 2 minutes.
1119    pub project_grace_period: u32,
1120    /// Refresh a project after the specified seconds.
1121    ///
1122    /// The time must be between expiry time and the grace period.
1123    ///
1124    /// By default there are no refreshes enabled.
1125    pub project_refresh_interval: Option<u32>,
1126    /// The cache timeout for downstream relay info (public keys) in seconds.
1127    pub relay_expiry: u32,
1128    /// The cache timeout for non-existing entries.
1129    pub miss_expiry: u32,
1130    /// The buffer timeout for batched project config queries before sending them upstream in ms.
1131    pub batch_interval: u32,
1132    /// The buffer timeout for batched queries of downstream relays in ms. Defaults to 100ms.
1133    pub downstream_relays_batch_interval: u32,
1134    /// The maximum number of project configs to fetch from Sentry at once. Defaults to 500.
1135    ///
1136    /// `cache.batch_interval` controls how quickly batches are sent, this controls the batch size.
1137    pub batch_size: usize,
1138    /// Interval for watching local cache override files in seconds.
1139    pub file_interval: u32,
1140    /// Interval for fetching new global configs from the upstream, in seconds.
1141    pub global_config_fetch_interval: u32,
1142}
1143
1144impl Default for Cache {
1145    fn default() -> Self {
1146        Cache {
1147            project_request_full_config: false,
1148            project_expiry: 300,       // 5 minutes
1149            project_grace_period: 120, // 2 minutes
1150            project_refresh_interval: None,
1151            relay_expiry: 3600,                    // 1 hour
1152            miss_expiry: 60,                       // 1 minute
1153            batch_interval: 100,                   // 100ms
1154            downstream_relays_batch_interval: 100, // 100ms
1155            batch_size: 500,
1156            file_interval: 10,                // 10 seconds
1157            global_config_fetch_interval: 10, // 10 seconds
1158        }
1159    }
1160}
1161
1162/// Controls Sentry-internal event processing.
1163#[derive(Serialize, Deserialize, Debug, Clone)]
1164#[serde(default)]
1165pub struct Processing {
1166    /// True if the Relay should do processing. Defaults to `false`.
1167    pub enabled: bool,
1168    /// GeoIp DB file source.
1169    pub geoip_path: Option<PathBuf>,
1170    /// Maximum future timestamp of ingested events.
1171    pub max_secs_in_future: u32,
1172    /// Maximum age of ingested sessions. Older sessions will be dropped.
1173    pub max_session_secs_in_past: u32,
1174    /// Kafka producer configurations.
1175    pub kafka_config: Vec<KafkaConfigParam>,
1176    /// Additional kafka producer configurations.
1177    ///
1178    /// The `kafka_config` is the default producer configuration used for all topics. A secondary
1179    /// kafka config can be referenced in `topics:` like this:
1180    ///
1181    /// ```yaml
1182    /// secondary_kafka_configs:
1183    ///   mycustomcluster:
1184    ///     - name: 'bootstrap.servers'
1185    ///       value: 'sentry_kafka_metrics:9093'
1186    ///
1187    /// topics:
1188    ///   transactions: ingest-transactions
1189    ///   metrics:
1190    ///     name: ingest-metrics
1191    ///     config: mycustomcluster
1192    /// ```
1193    ///
1194    /// Then metrics will be produced to an entirely different Kafka cluster.
1195    pub secondary_kafka_configs: BTreeMap<String, Vec<KafkaConfigParam>>,
1196    /// Kafka topic names.
1197    pub topics: TopicAssignments,
1198    /// Whether to validate the supplied topics by calling Kafka's metadata endpoints.
1199    pub kafka_validate_topics: bool,
1200    /// Upper bound, in seconds, on the random delay before a Kafka producer swaps its client
1201    /// after the Kafka config changed.
1202    ///
1203    /// Spreads swaps out so that a config change does not have every pod in the fleet reconnect
1204    /// at once. Keep it well below how often the Kafka config is expected to change. Tests set
1205    /// this to `0` to observe a swap without waiting.
1206    pub reloading_producer_jitter_secs: u64,
1207    /// Redis hosts to connect to for storing state for rate limits.
1208    pub redis: Option<RedisConfigs>,
1209    /// Maximum chunk size of attachments for Kafka.
1210    pub attachment_chunk_size: ByteSize,
1211    /// Prefix to use when looking up project configs in Redis. Defaults to "relayconfig".
1212    pub projectconfig_cache_prefix: String,
1213    /// Maximum rate limit to report to clients.
1214    pub max_rate_limit: Option<u32>,
1215    /// Configures the quota cache ratio between `0.0` and `1.0`.
1216    ///
1217    /// The quota cache, caches the specified ratio of remaining quota in memory to reduce the
1218    /// amount of synchronizations required with Redis.
1219    ///
1220    /// The ratio is applied to the (per second) rate of the quota, not the total limit.
1221    /// For example a quota with limit 100 with a 10 second window is treated equally to a quota of
1222    /// 10 with a 1 second window.
1223    ///
1224    /// By default quota caching is disabled.
1225    pub quota_cache_ratio: Option<f32>,
1226    /// Relative amount of the total quota limit to which quota caching is applied.
1227    ///
1228    /// If exceeded, the rate limiter will no longer cache the quota and sync with Redis on every call instead.
1229    /// Lowering this value reduces the probability of incorrectly over-accepting.
1230    ///
1231    /// Must be between `0.0` and `1.0`, by default there is no limit configured.
1232    pub quota_cache_max: Option<f32>,
1233    /// Configuration for the objectstore service.
1234    #[serde(alias = "upload")]
1235    pub objectstore: ObjectstoreServiceConfig,
1236}
1237
1238impl Default for Processing {
1239    /// Constructs a disabled processing configuration.
1240    fn default() -> Self {
1241        Self {
1242            enabled: false,
1243            geoip_path: None,
1244            max_secs_in_future: 60,                  // 1 minute
1245            max_session_secs_in_past: 5 * 24 * 3600, // 5 days
1246            kafka_config: Vec::new(),
1247            secondary_kafka_configs: BTreeMap::new(),
1248            topics: TopicAssignments::default(),
1249            kafka_validate_topics: false,
1250            reloading_producer_jitter_secs: 60,
1251            redis: None,
1252            attachment_chunk_size: ByteSize::mebibytes(1),
1253            projectconfig_cache_prefix: "relayconfig".to_owned(),
1254            max_rate_limit: Some(300), // 5 minutes
1255            quota_cache_ratio: None,
1256            quota_cache_max: None,
1257            objectstore: ObjectstoreServiceConfig::default(),
1258        }
1259    }
1260}
1261
1262/// Picks a cluster name for the default `kafka_config` that no secondary config uses.
1263///
1264/// Only relevant to [`ConfigSnapshot::kafka_config_blob`]: the blob names every cluster, while
1265/// Relay's own config leaves the default one anonymous. Appends underscores to `default` until
1266/// it is unused.
1267fn default_cluster_name(processing: &Processing) -> String {
1268    let mut name = String::from("default");
1269    while processing.secondary_kafka_configs.contains_key(&name) {
1270        name.push('_');
1271    }
1272    name
1273}
1274
1275/// Configuration for normalization in this Relay.
1276#[derive(Debug, Default, Serialize, Deserialize, Clone)]
1277#[serde(default)]
1278pub struct Normalization {
1279    /// Level of normalization for Relay to apply to incoming data.
1280    pub level: NormalizationLevel,
1281}
1282
1283/// Configuration for the level of normalization this Relay should do.
1284#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
1285#[serde(rename_all = "lowercase")]
1286pub enum NormalizationLevel {
1287    /// Runs normalization, excluding steps that break future compatibility.
1288    ///
1289    /// Processing Relays run [`NormalizationLevel::Full`] if this option is set.
1290    #[default]
1291    Default,
1292    /// Run full normalization.
1293    ///
1294    /// It includes steps that break future compatibility and should only run in
1295    /// the last layer of relays.
1296    Full,
1297}
1298
1299/// Configuration options for objectstore's auth scheme.
1300#[derive(Serialize, Deserialize, Clone)]
1301pub struct ObjectstoreAuthConfig {
1302    /// Identifier for the private key used to sign objectstore's tokens. Must correspond to a
1303    /// public key configured in objectstore.
1304    pub key_id: String,
1305
1306    /// EdDSA private key used to sign Objectstore's tokens, in PEM format.
1307    pub signing_key: String,
1308}
1309
1310impl fmt::Debug for ObjectstoreAuthConfig {
1311    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1312        f.debug_struct("ObjectstoreAuthConfig")
1313            .field("key_id", &self.key_id)
1314            .field("signing_key", &"[redacted]")
1315            .finish()
1316    }
1317}
1318
1319/// Configuration values for the objectstore service.
1320#[derive(Serialize, Deserialize, Debug, Clone)]
1321#[serde(default)]
1322pub struct ObjectstoreServiceConfig {
1323    /// The base URL for the objectstore service.
1324    ///
1325    /// This defaults to [`None`], which means that the service will be disabled,
1326    /// unless a proper configuration is provided.
1327    pub objectstore_url: Option<String>,
1328
1329    /// Maximum concurrency of uploads.
1330    pub max_concurrent_requests: usize,
1331
1332    /// Maximum size of the service input queue when `max_concurrent_requests` is saturated.
1333    ///
1334    /// The service will loadshed if this threshold is reached.
1335    pub max_backlog: usize,
1336
1337    /// Maximum duration of an attachment upload in seconds. Uploads that take longer are discarded.
1338    ///
1339    /// NOTE: This timeout applies to attachments that are already in-memory. Streaming uploads
1340    /// might take longer and are restricted independently by [`Self::stream_timeout`].
1341    pub timeout: u64,
1342
1343    /// Maximum duration of an upload stream.
1344    ///
1345    /// Streams get a larger default timeout because their duration depends on the client
1346    /// as well as the server.
1347    pub stream_timeout: u64,
1348
1349    /// Time between upload attempts.
1350    pub retry_delay: f64,
1351
1352    /// Maximum number of attempts made to upload.
1353    pub max_attempts: NonZeroU16,
1354
1355    /// Configuration values for objectstore's auth scheme.
1356    pub auth: Option<ObjectstoreAuthConfig>,
1357}
1358
1359impl Default for ObjectstoreServiceConfig {
1360    fn default() -> Self {
1361        Self {
1362            objectstore_url: None,
1363            max_concurrent_requests: 10,
1364            max_backlog: 20,
1365            timeout: 60,
1366            stream_timeout: 5 * 60, // synced with `Upload::timeout`
1367            retry_delay: 1.0,
1368            max_attempts: NonZeroU16::new(5).unwrap(),
1369            auth: None,
1370        }
1371    }
1372}
1373
1374/// Determines how to emit outcomes.
1375/// For compatibility reasons, this can either be true, false or AsClientReports
1376#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1377
1378pub enum EmitOutcomes {
1379    /// Do not emit any outcomes.
1380    None,
1381    /// Emit outcomes as client reports.
1382    AsClientReports,
1383    /// Emit outcomes as outcomes.
1384    AsOutcomes,
1385}
1386
1387impl EmitOutcomes {
1388    /// Returns true of outcomes are emitted via http, kafka, or client reports.
1389    pub fn any(&self) -> bool {
1390        !matches!(self, EmitOutcomes::None)
1391    }
1392}
1393
1394impl Serialize for EmitOutcomes {
1395    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1396    where
1397        S: Serializer,
1398    {
1399        // For compatibility, serialize None and AsOutcomes as booleans.
1400        match self {
1401            Self::None => serializer.serialize_bool(false),
1402            Self::AsClientReports => serializer.serialize_str("as_client_reports"),
1403            Self::AsOutcomes => serializer.serialize_bool(true),
1404        }
1405    }
1406}
1407
1408struct EmitOutcomesVisitor;
1409
1410impl Visitor<'_> for EmitOutcomesVisitor {
1411    type Value = EmitOutcomes;
1412
1413    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1414        formatter.write_str("true, false, 'as_client_reports'")
1415    }
1416
1417    fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
1418    where
1419        E: serde::de::Error,
1420    {
1421        Ok(if v {
1422            EmitOutcomes::AsOutcomes
1423        } else {
1424            EmitOutcomes::None
1425        })
1426    }
1427
1428    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1429    where
1430        E: serde::de::Error,
1431    {
1432        match v {
1433            "as_client_reports" => Ok(EmitOutcomes::AsClientReports),
1434            _ => Err(E::invalid_value(Unexpected::Str(v), &self)),
1435        }
1436    }
1437}
1438
1439impl<'de> Deserialize<'de> for EmitOutcomes {
1440    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1441    where
1442        D: Deserializer<'de>,
1443    {
1444        deserializer.deserialize_any(EmitOutcomesVisitor)
1445    }
1446}
1447
1448/// Outcome generation specific configuration values.
1449#[derive(Serialize, Deserialize, Debug, Clone)]
1450#[serde(default)]
1451pub struct Outcomes {
1452    /// Controls whether outcomes will be emitted when processing is disabled.
1453    /// Processing relays always emit outcomes (for backwards compatibility).
1454    /// Can take the following values: false, "as_client_reports", true
1455    pub emit_outcomes: EmitOutcomes,
1456    /// Defines the source string registered in the outcomes originating from
1457    /// this Relay (typically something like the region or the layer).
1458    pub source: Option<String>,
1459}
1460
1461impl Default for Outcomes {
1462    fn default() -> Self {
1463        Outcomes {
1464            emit_outcomes: EmitOutcomes::AsClientReports,
1465            source: None,
1466        }
1467    }
1468}
1469
1470/// Minimal version of a config for dumping out.
1471#[derive(Serialize, Deserialize, Debug, Default)]
1472pub struct MinimalConfig {
1473    /// The relay part of the config.
1474    pub relay: Relay,
1475}
1476
1477impl MinimalConfig {
1478    /// Saves the config in the given config folder as config.yml
1479    pub fn save_in_folder<P: AsRef<Path>>(&self, p: P) -> anyhow::Result<()> {
1480        let path = p.as_ref();
1481        if fs::metadata(path).is_err() {
1482            fs::create_dir_all(path)
1483                .with_context(|| ConfigError::file(ConfigErrorKind::CouldNotOpenFile, path))?;
1484        }
1485        self.save(path)
1486    }
1487}
1488
1489impl ConfigObject for MinimalConfig {
1490    fn format() -> ConfigFormat {
1491        ConfigFormat::Yaml
1492    }
1493
1494    fn name() -> &'static str {
1495        "config"
1496    }
1497}
1498
1499/// Alternative serialization of RelayInfo for config file using snake case.
1500mod config_relay_info {
1501    use serde::ser::SerializeMap;
1502
1503    use super::*;
1504
1505    // Uses snake_case as opposed to camelCase.
1506    #[derive(Debug, Serialize, Deserialize, Clone)]
1507    struct RelayInfoConfig {
1508        public_key: PublicKey,
1509        #[serde(default)]
1510        internal: bool,
1511    }
1512
1513    impl From<RelayInfoConfig> for RelayInfo {
1514        fn from(v: RelayInfoConfig) -> Self {
1515            RelayInfo {
1516                public_key: v.public_key,
1517                internal: v.internal,
1518            }
1519        }
1520    }
1521
1522    impl From<RelayInfo> for RelayInfoConfig {
1523        fn from(v: RelayInfo) -> Self {
1524            RelayInfoConfig {
1525                public_key: v.public_key,
1526                internal: v.internal,
1527            }
1528        }
1529    }
1530
1531    pub(super) fn deserialize<'de, D>(des: D) -> Result<HashMap<RelayId, RelayInfo>, D::Error>
1532    where
1533        D: Deserializer<'de>,
1534    {
1535        let map = HashMap::<RelayId, RelayInfoConfig>::deserialize(des)?;
1536        Ok(map.into_iter().map(|(k, v)| (k, v.into())).collect())
1537    }
1538
1539    pub(super) fn serialize<S>(elm: &HashMap<RelayId, RelayInfo>, ser: S) -> Result<S::Ok, S::Error>
1540    where
1541        S: Serializer,
1542    {
1543        let mut map = ser.serialize_map(Some(elm.len()))?;
1544
1545        for (k, v) in elm {
1546            map.serialize_entry(k, &RelayInfoConfig::from(v.clone()))?;
1547        }
1548
1549        map.end()
1550    }
1551}
1552
1553/// Authentication options.
1554#[derive(Serialize, Deserialize, Debug, Clone)]
1555#[serde(default)]
1556pub struct AuthConfig {
1557    /// Controls responses from the readiness health check endpoint based on authentication.
1558    #[serde(skip_serializing_if = "is_default")]
1559    pub ready: ReadinessCondition,
1560
1561    /// Statically authenticated downstream relays.
1562    #[serde(with = "config_relay_info")]
1563    pub static_relays: HashMap<RelayId, RelayInfo>,
1564
1565    /// How old a signature can be before it is considered invalid, in seconds.
1566    ///
1567    /// Defaults to 5 minutes.
1568    pub signature_max_age: u64,
1569}
1570
1571impl Default for AuthConfig {
1572    fn default() -> Self {
1573        Self {
1574            ready: ReadinessCondition::default(),
1575            static_relays: HashMap::new(),
1576            signature_max_age: 300, // 5 minutes
1577        }
1578    }
1579}
1580
1581/// GeoIp database configuration options.
1582#[derive(Serialize, Deserialize, Debug, Default, Clone)]
1583pub struct GeoIpConfig {
1584    /// The path to GeoIP database.
1585    pub path: Option<PathBuf>,
1586}
1587
1588/// Settings to control Relay's health checks.
1589///
1590/// After breaching one of the configured thresholds, Relay will
1591/// return an `unhealthy` status from its health endpoint.
1592#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1593#[serde(default)]
1594pub struct Health {
1595    /// Interval to refresh internal health checks.
1596    ///
1597    /// Shorter intervals will decrease the time it takes the health check endpoint to report
1598    /// issues, but can also increase sporadic unhealthy responses.
1599    ///
1600    /// Defaults to `3000`` (3 seconds).
1601    pub refresh_interval_ms: u64,
1602    /// Maximum memory watermark in bytes.
1603    ///
1604    /// By default, there is no absolute limit set and the watermark
1605    /// is only controlled by setting [`Self::max_memory_percent`].
1606    pub max_memory_bytes: Option<ByteSize>,
1607    /// Maximum memory watermark as a percentage of maximum system memory.
1608    ///
1609    /// Defaults to `0.95` (95%).
1610    pub max_memory_percent: f32,
1611    /// Health check probe timeout in milliseconds.
1612    ///
1613    /// Any probe exceeding the timeout will be considered failed.
1614    /// This limits the max execution time of Relay health checks.
1615    ///
1616    /// Defaults to 900 milliseconds.
1617    pub probe_timeout_ms: u64,
1618    /// The refresh frequency of memory stats which are used to poll memory
1619    /// usage of Relay.
1620    ///
1621    /// The implementation of memory stats guarantees that the refresh will happen at
1622    /// least every `x` ms since memory readings are lazy and are updated only if needed.
1623    pub memory_stat_refresh_frequency_ms: u64,
1624}
1625
1626impl Default for Health {
1627    fn default() -> Self {
1628        Self {
1629            refresh_interval_ms: 3000,
1630            max_memory_bytes: None,
1631            max_memory_percent: 0.95,
1632            probe_timeout_ms: 900,
1633            memory_stat_refresh_frequency_ms: 100,
1634        }
1635    }
1636}
1637
1638/// COGS configuration.
1639#[derive(Serialize, Deserialize, Debug, Clone)]
1640#[serde(default)]
1641pub struct Cogs {
1642    /// Maximium amount of COGS measurements allowed to backlog.
1643    ///
1644    /// Any additional COGS measurements recorded will be dropped.
1645    ///
1646    /// Defaults to `10_000`.
1647    pub max_queue_size: u64,
1648    /// Relay COGS resource id.
1649    ///
1650    /// All Relay related COGS measurements are emitted with this resource id.
1651    ///
1652    /// Defaults to `relay_service`.
1653    pub relay_resource_id: String,
1654}
1655
1656impl Default for Cogs {
1657    fn default() -> Self {
1658        Self {
1659            max_queue_size: 10_000,
1660            relay_resource_id: "relay_service".to_owned(),
1661        }
1662    }
1663}
1664
1665/// Configuration for the upload service.
1666#[derive(Debug, Clone, Serialize, Deserialize)]
1667#[serde(default)]
1668pub struct Upload {
1669    /// Maximum number of uploads that the service accepts.
1670    ///
1671    /// Additional uploads will be rejected.
1672    pub max_concurrent_requests: usize,
1673    /// Maximum time spent trying to upload, in seconds.
1674    pub timeout: u64,
1675    /// The maximum time between creating the upload and uploading the data / the attachment placeholder.
1676    ///
1677    /// In seconds.
1678    pub max_age: i64,
1679
1680    /// Credentials used for signing & verifying upload locations.
1681    ///
1682    /// If omitted, relay's default [`Credentials`] are used.
1683    pub credentials: Option<UploadCredentials>,
1684}
1685
1686impl Default for Upload {
1687    fn default() -> Self {
1688        Self {
1689            max_concurrent_requests: 100,
1690            timeout: 5 * 60,  // five minutes
1691            max_age: 60 * 60, // 1h
1692            credentials: None,
1693        }
1694    }
1695}
1696
1697/// Credentials used for signing & verifying upload locations.
1698#[derive(Clone, Serialize, Deserialize)]
1699pub struct UploadCredentials {
1700    /// Key used to sign upload locations.
1701    #[cfg(feature = "processing")]
1702    pub signing_key: SecretKey,
1703
1704    /// Key used to verify upload locations.
1705    pub verification_key: PublicKey,
1706}
1707
1708impl fmt::Debug for UploadCredentials {
1709    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1710        let Self {
1711            #[cfg(feature = "processing")]
1712                signing_key: _,
1713            verification_key,
1714        } = self;
1715        let mut b = f.debug_struct("UploadCredentials");
1716        #[cfg(feature = "processing")]
1717        b.field("signing_key", &"[redacted]");
1718        b.field("verification_key", verification_key).finish()
1719    }
1720}
1721
1722/// All configuration values that can be deserialized from `config.yml`.
1723#[derive(Serialize, Deserialize, Debug, Default, Clone)]
1724#[serde(default)]
1725#[allow(missing_docs)]
1726pub struct ConfigValues {
1727    pub relay: Relay,
1728    pub http: Http,
1729    pub cache: Cache,
1730    pub spool: Spool,
1731    pub limits: Limits,
1732    pub logging: relay_log::LogConfig,
1733    pub routing: Routing,
1734    pub metrics: Metrics,
1735    pub sentry: relay_log::SentryConfig,
1736    pub processing: Processing,
1737    pub outcomes: Outcomes,
1738    pub aggregator: AggregatorServiceConfig,
1739    pub secondary_aggregators: Vec<ScopedAggregatorConfig>,
1740    pub auth: AuthConfig,
1741    pub geoip: GeoIpConfig,
1742    pub normalization: Normalization,
1743    pub health: Health,
1744    pub cogs: Cogs,
1745    pub upload: Upload,
1746}
1747
1748impl ConfigObject for ConfigValues {
1749    fn format() -> ConfigFormat {
1750        ConfigFormat::Yaml
1751    }
1752
1753    fn name() -> &'static str {
1754        "config"
1755    }
1756}
1757
1758#[derive(Default, Clone)]
1759struct ConfigInner {
1760    /// Relay's config values.
1761    values: ConfigValues,
1762    /// Configured Relay credentials.
1763    ///
1764    /// Credentials may be missing for proxy mode.
1765    credentials: Option<Credentials>,
1766}
1767
1768impl ConfigInner {
1769    fn apply_overrides(&mut self, overrides: &OverridableConfig) -> anyhow::Result<()> {
1770        if let Some(log_level) = &overrides.log_level {
1771            self.values.logging.level = log_level.parse()?;
1772        }
1773
1774        if let Some(log_format) = &overrides.log_format {
1775            self.values.logging.format = log_format.parse()?;
1776        }
1777
1778        let relay = &mut self.values.relay;
1779        if let Some(mode) = &overrides.mode {
1780            relay.mode = mode
1781                .parse::<RelayMode>()
1782                .with_context(|| ConfigError::field("mode"))?;
1783        }
1784        if let Some(deployment) = &overrides.instance {
1785            relay.instance = deployment
1786                .parse::<RelayInstance>()
1787                .with_context(|| ConfigError::field("deployment"))?;
1788        }
1789        if let Some(upstream) = &overrides.upstream {
1790            relay.upstream = upstream
1791                .parse::<UpstreamDescriptor>()
1792                .with_context(|| ConfigError::field("upstream"))?;
1793        } else if let Some(upstream_dsn) = &overrides.upstream_dsn {
1794            relay.upstream = upstream_dsn
1795                .parse::<Dsn>()
1796                .map(|dsn| UpstreamDescriptor::from_dsn(&dsn))
1797                .with_context(|| ConfigError::field("upstream_dsn"))?;
1798        }
1799        if let Some(host) = &overrides.host {
1800            relay.host = host
1801                .parse::<IpAddr>()
1802                .with_context(|| ConfigError::field("host"))?;
1803        }
1804        if let Some(port) = &overrides.port {
1805            relay.port = port
1806                .as_str()
1807                .parse()
1808                .with_context(|| ConfigError::field("port"))?;
1809        }
1810
1811        let processing = &mut self.values.processing;
1812        if let Some(enabled) = &overrides.processing {
1813            match enabled.to_lowercase().as_str() {
1814                "true" | "1" => processing.enabled = true,
1815                "false" | "0" | "" => processing.enabled = false,
1816                _ => return Err(ConfigError::field("processing").into()),
1817            }
1818        }
1819        if let Some(redis) = overrides.redis_url.clone() {
1820            processing.redis = Some(RedisConfigs::Unified(RedisConfig::single(redis)))
1821        }
1822        if let Some(kafka_url) = overrides.kafka_url.clone() {
1823            let existing = processing
1824                .kafka_config
1825                .iter_mut()
1826                .find(|e| e.name == "bootstrap.servers");
1827
1828            if let Some(config_param) = existing {
1829                config_param.value = kafka_url;
1830            } else {
1831                self.values.processing.kafka_config.push(KafkaConfigParam {
1832                    name: "bootstrap.servers".to_owned(),
1833                    value: kafka_url,
1834                })
1835            }
1836        }
1837
1838        if overrides.outcome_source.is_some() {
1839            self.values.outcomes.source = overrides.outcome_source.clone();
1840        }
1841
1842        if let Some(shutdown_timeout) = &overrides.shutdown_timeout
1843            && let Ok(shutdown_timeout) = shutdown_timeout.parse::<u64>()
1844        {
1845            self.values.limits.shutdown_timeout = shutdown_timeout;
1846        }
1847
1848        if let Some(server_name) = overrides.server_name.clone() {
1849            self.values.sentry.server_name = Some(server_name.into());
1850        }
1851
1852        let id = if let Some(id) = &overrides.id {
1853            let id = Uuid::parse_str(id).with_context(|| ConfigError::field("id"))?;
1854            Some(id)
1855        } else {
1856            None
1857        };
1858        let public_key = if let Some(public_key) = &overrides.public_key {
1859            let public_key = public_key
1860                .parse::<PublicKey>()
1861                .with_context(|| ConfigError::field("public_key"))?;
1862            Some(public_key)
1863        } else {
1864            None
1865        };
1866
1867        let secret_key = if let Some(secret_key) = &overrides.secret_key {
1868            let secret_key = secret_key
1869                .parse::<SecretKey>()
1870                .with_context(|| ConfigError::field("secret_key"))?;
1871            Some(secret_key)
1872        } else {
1873            None
1874        };
1875
1876        if let Some(credentials) = &mut self.credentials {
1877            //we have existing credentials we may override some entries
1878            if let Some(id) = id {
1879                credentials.id = id;
1880            }
1881            if let Some(public_key) = public_key {
1882                credentials.public_key = public_key;
1883            }
1884            if let Some(secret_key) = secret_key {
1885                credentials.secret_key = secret_key
1886            }
1887        } else {
1888            //no existing credentials we may only create the full credentials
1889            match (id, public_key, secret_key) {
1890                (Some(id), Some(public_key), Some(secret_key)) => {
1891                    self.credentials = Some(Credentials {
1892                        secret_key,
1893                        public_key,
1894                        id,
1895                    })
1896                }
1897                (None, None, None) => {
1898                    // nothing provided, we'll just leave the credentials None, maybe we
1899                    // don't need them in the current command or we'll override them later
1900                }
1901                _ => {
1902                    return Err(ConfigError::field("incomplete credentials").into());
1903                }
1904            }
1905        }
1906
1907        Ok(())
1908    }
1909
1910    /// Merges reloadable parts of the config from `other` into `self`.
1911    ///
1912    /// Returns `true` if any parts of the config were updated.
1913    fn reload_with(&mut self, other: &Self) -> bool {
1914        let mut changed = false;
1915
1916        if self.values.health != other.values.health {
1917            relay_log::debug!("updating health");
1918            self.values.health = other.values.health.clone();
1919            changed = true;
1920        }
1921
1922        // Broker settings only. Producers hold their topic name and shard list from startup,
1923        // so `topics` changes cannot be applied here at all, including moving a topic to
1924        // another cluster via its `config` key. Those need a restart.
1925        //
1926        // Merging `topics` anyway would also break later pushes: `kafka_config_blob` reads it
1927        // live, so a renamed topic would be keyed by its new name while the producer still
1928        // selects the old one.
1929        //
1930        // Running Kafka clients pick this up via `ConfigReloadService`.
1931        if self.values.processing.kafka_config != other.values.processing.kafka_config
1932            || self.values.processing.secondary_kafka_configs
1933                != other.values.processing.secondary_kafka_configs
1934        {
1935            relay_log::debug!("updating kafka config");
1936            self.values.processing.kafka_config = other.values.processing.kafka_config.clone();
1937            self.values.processing.secondary_kafka_configs =
1938                other.values.processing.secondary_kafka_configs.clone();
1939            changed = true;
1940        }
1941
1942        if self.values.spool.envelopes.max_unspool_envelopes_per_second
1943            != other
1944                .values
1945                .spool
1946                .envelopes
1947                .max_unspool_envelopes_per_second
1948        {
1949            relay_log::debug!("updating spool.envelopes.max_unspool_envelopes_per_second");
1950            self.values.spool.envelopes.max_unspool_envelopes_per_second = other
1951                .values
1952                .spool
1953                .envelopes
1954                .max_unspool_envelopes_per_second;
1955            changed = true;
1956        }
1957
1958        changed
1959    }
1960}
1961
1962/// Relay's Configuration.
1963pub struct Config {
1964    /// A mutex to serialize all write accesses to `inner`.
1965    ///
1966    /// Accessing the arc swap is done with compare and swap, but we still want serialized
1967    /// access and operations to guarantee consistency when dealing with e.g. the filesystem.
1968    ///
1969    /// The mutex must be acquired before changing `inner`. Methods taking `&mut` can omit
1970    /// this, as the `&mut` requirement already satisfies that there is no concurrent access
1971    /// possible.
1972    inner_access: Mutex<()>,
1973    /// The actual config.
1974    inner: ArcSwap<ConfigInner>,
1975    /// A list of overrides applied to the config, in order.
1976    ///
1977    /// When re-loading the configuration these overrides need to be applied again
1978    /// in the same order as they were applied originally.
1979    overrides: Vec<OverridableConfig>,
1980    /// Path from which the config is loaded.
1981    ///
1982    /// This is Relay's configuration directory.
1983    path: PathBuf,
1984}
1985
1986impl fmt::Debug for Config {
1987    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1988        let inner = self.inner.load();
1989
1990        f.debug_struct("Config")
1991            .field("path", &self.path)
1992            // Only print specific parts of `inner` to not leak the credentials.
1993            .field("values", &inner.values)
1994            .finish()
1995    }
1996}
1997
1998impl Config {
1999    /// Loads a config from a given config folder.
2000    pub fn from_path<P: AsRef<Path>>(path: P) -> anyhow::Result<Config> {
2001        let path = env::current_dir()
2002            .map(|x| x.join(path.as_ref()))
2003            .unwrap_or_else(|_| path.as_ref().to_path_buf());
2004
2005        let inner = ConfigInner {
2006            values: ConfigValues::load(&path)?,
2007            credentials: match Credentials::path(&path).exists() {
2008                true => Some(Credentials::load(&path)?),
2009                false => None,
2010            },
2011        };
2012
2013        let config = Config {
2014            inner_access: Mutex::new(()),
2015            inner: ArcSwap::from_pointee(inner),
2016            overrides: Vec::new(),
2017            path: path.clone(),
2018        };
2019
2020        if cfg!(not(feature = "processing")) && config.current().processing_enabled() {
2021            return Err(ConfigError::file(ConfigErrorKind::ProcessingNotAvailable, &path).into());
2022        }
2023
2024        Ok(config)
2025    }
2026
2027    /// Creates a config from a JSON value.
2028    ///
2029    /// This is mostly useful for tests.
2030    pub fn from_json_value(value: serde_json::Value) -> anyhow::Result<Config> {
2031        Ok(Config {
2032            inner_access: Mutex::new(()),
2033            inner: ArcSwap::from_pointee(ConfigInner {
2034                values: serde_json::from_value(value)
2035                    .with_context(|| ConfigError::new(ConfigErrorKind::BadJson))?,
2036                credentials: None,
2037            }),
2038            overrides: Vec::new(),
2039            path: PathBuf::new(),
2040        })
2041    }
2042
2043    /// Override configuration with values coming from other sources (e.g. env variables or
2044    /// command line parameters).
2045    ///
2046    /// If applying the overrides fails, the config may be left in an inconsistent state.
2047    pub fn apply_override(&mut self, overrides: OverridableConfig) -> anyhow::Result<&mut Self> {
2048        // We could introduce a config builder which operates on mutable configs, which would eliminate
2049        // the need for this `try_rcu` dance here.
2050        crate::utils::try_rcu(&self.inner, |inner| {
2051            let mut new = ConfigInner::clone(inner);
2052            new.apply_overrides(&overrides)?;
2053            Ok::<_, anyhow::Error>(Arc::new(new))
2054        })?;
2055
2056        // Overrides successfully applied.
2057        self.overrides.push(overrides);
2058
2059        Ok(self)
2060    }
2061
2062    /// Checks if the config is already initialized.
2063    pub fn config_exists<P: AsRef<Path>>(path: P) -> bool {
2064        fs::metadata(ConfigValues::path(path.as_ref())).is_ok()
2065    }
2066
2067    /// Returns the filename of the config file.
2068    pub fn path(&self) -> &Path {
2069        &self.path
2070    }
2071
2072    /// Dumps out a YAML string of the values.
2073    pub fn to_yaml_string(&self) -> anyhow::Result<String> {
2074        serde_yaml::to_string(&self.inner.load().values)
2075            .with_context(|| ConfigError::new(ConfigErrorKind::CouldNotWriteFile))
2076    }
2077
2078    /// Set new credentials.
2079    ///
2080    /// This also writes the credentials back to the file, if this config was loaded from the file-system.
2081    pub fn replace_credentials(
2082        &mut self,
2083        credentials: Option<Credentials>,
2084    ) -> anyhow::Result<bool> {
2085        if self.inner.load().credentials == credentials {
2086            return Ok(false);
2087        }
2088
2089        if !self.path.is_empty() {
2090            match &credentials {
2091                Some(creds) => {
2092                    creds.save(&self.path)?;
2093                }
2094                None => {
2095                    let path = Credentials::path(&self.path);
2096                    if fs::metadata(&path).is_ok() {
2097                        fs::remove_file(&path).with_context(|| {
2098                            ConfigError::file(ConfigErrorKind::CouldNotWriteFile, &path)
2099                        })?;
2100                    }
2101                }
2102            }
2103        }
2104
2105        // Note: there is never anyone racing on the `ArcSwap` as long as `Self` borrowed mutably.
2106        //
2107        // We can improve this if we split out mutable operations into a separate struct and only
2108        // once `frozen()` we change to an `ArcSwap` internally.
2109        self.inner.rcu(|inner| {
2110            let mut inner = ConfigInner::clone(inner);
2111            inner.credentials = credentials.clone();
2112            Arc::new(inner)
2113        });
2114
2115        Ok(true)
2116    }
2117
2118    /// Reloads the configuration from disk.
2119    ///
2120    /// Returns `true` if the configuration changed.
2121    ///
2122    /// In order for a config to be reloadable it must've been loaded from a path. The original
2123    /// config will be re-read and reloadable parts of the config will be replaced with their updated
2124    /// values.
2125    ///
2126    /// If the reload fails for any reason, the current config is untouched.
2127    pub fn reload(&self) -> anyhow::Result<bool> {
2128        if self.path.is_empty() {
2129            return Ok(false);
2130        }
2131
2132        let _access = self
2133            .inner_access
2134            .lock()
2135            .unwrap_or_else(PoisonError::into_inner);
2136
2137        let mut new_config = Self::from_path(&self.path)?;
2138        for overrides in &self.overrides {
2139            new_config.apply_override(overrides.clone())?;
2140        }
2141        let new_config = new_config.current();
2142
2143        let mut changed = false;
2144
2145        // Since we do have the `_access` lock, this will always succeed on the first try.
2146        self.inner.rcu(|inner| {
2147            let mut new_inner = ConfigInner::clone(inner);
2148            changed = new_inner.reload_with(&new_config.inner);
2149            match changed {
2150                true => Arc::new(new_inner),
2151                false => Arc::clone(inner),
2152            }
2153        });
2154
2155        Ok(changed)
2156    }
2157
2158    /// Acquires a current [`snapshot`](ConfigSnapshot) of the config.
2159    ///
2160    /// A snapshot is the way to actually consume values from the config. A snapshot should ideally be acquired
2161    /// once per unit of work.
2162    pub fn current(&self) -> ConfigSnapshot {
2163        let inner = self.inner.load();
2164        ConfigSnapshot { inner }
2165    }
2166}
2167
2168impl Default for Config {
2169    fn default() -> Self {
2170        Self {
2171            inner_access: Mutex::new(()),
2172            inner: ArcSwap::from_pointee(Default::default()),
2173            overrides: Vec::new(),
2174            path: PathBuf::new(),
2175        }
2176    }
2177}
2178
2179/// A config snapshot is a point in time snapshot of the [`Config`].
2180///
2181/// The [`Config`] may change over time, to guarantee a consistent view of the config
2182/// a snapshot must be acquired first.
2183///
2184/// The snapshot should not be stored in a long lasting datastructure. As a rule of thumb it should
2185/// only exist on the stack.
2186pub struct ConfigSnapshot {
2187    inner: arc_swap::Guard<Arc<ConfigInner>>,
2188}
2189
2190impl ConfigSnapshot {
2191    /// Returns `true` if the config is ready to use.
2192    pub fn has_credentials(&self) -> bool {
2193        self.inner.credentials.is_some()
2194    }
2195
2196    /// Return the current credentials.
2197    pub fn credentials(&self) -> Option<&Credentials> {
2198        self.inner.credentials.as_ref()
2199    }
2200
2201    /// Returns the secret key if set.
2202    pub fn secret_key(&self) -> Option<&SecretKey> {
2203        self.inner.credentials.as_ref().map(|x| &x.secret_key)
2204    }
2205
2206    /// Returns the public key if set.
2207    pub fn public_key(&self) -> Option<&PublicKey> {
2208        self.inner.credentials.as_ref().map(|x| &x.public_key)
2209    }
2210
2211    /// Returns the relay ID.
2212    pub fn relay_id(&self) -> Option<&RelayId> {
2213        self.inner.credentials.as_ref().map(|x| &x.id)
2214    }
2215
2216    /// Returns the relay mode.
2217    pub fn relay_mode(&self) -> RelayMode {
2218        self.inner.values.relay.mode
2219    }
2220
2221    /// Returns the instance type of relay.
2222    pub fn relay_instance(&self) -> RelayInstance {
2223        self.inner.values.relay.instance
2224    }
2225
2226    /// Returns the upstream target as descriptor.
2227    pub fn upstream(&self) -> &UpstreamDescriptor {
2228        &self.inner.values.relay.upstream
2229    }
2230
2231    /// Returns the advertised upstream for downstream instances as descriptor.
2232    pub fn advertised_upstream(&self) -> Option<&UpstreamDescriptor> {
2233        self.inner.values.relay.advertised_upstream.as_ref()
2234    }
2235
2236    /// Returns the custom HTTP "Host" header.
2237    pub fn http_host_header(&self) -> Option<&str> {
2238        self.inner.values.http.host_header.as_deref()
2239    }
2240
2241    /// Returns the listen address.
2242    pub fn listen_addr(&self) -> SocketAddr {
2243        (self.inner.values.relay.host, self.inner.values.relay.port).into()
2244    }
2245
2246    /// Returns the listen address for internal APIs.
2247    ///
2248    /// Internal APIs are APIs which do not need to be publicly exposed,
2249    /// like health checks.
2250    ///
2251    /// Returns `None` when there is no explicit address configured for internal APIs,
2252    /// and they should instead be exposed on the main [`Self::listen_addr`].
2253    pub fn listen_addr_internal(&self) -> Option<SocketAddr> {
2254        match (
2255            self.inner.values.relay.internal_host,
2256            self.inner.values.relay.internal_port,
2257        ) {
2258            (Some(host), None) => Some((host, self.inner.values.relay.port).into()),
2259            (None, Some(port)) => Some((self.inner.values.relay.host, port).into()),
2260            (Some(host), Some(port)) => Some((host, port).into()),
2261            (None, None) => None,
2262        }
2263    }
2264
2265    /// Returns `true` when project IDs should be overriden rather than validated.
2266    ///
2267    /// Defaults to `false`, which requires project ID validation.
2268    pub fn override_project_ids(&self) -> bool {
2269        self.inner.values.relay.override_project_ids
2270    }
2271
2272    /// Returns the interval to check for configuration changes.
2273    ///
2274    /// `None` if the config should never be reloaded.
2275    pub fn config_reload_interval(&self) -> Option<Duration> {
2276        let interval = self.inner.values.relay.config_reload_interval?;
2277        Some(match interval {
2278            // Useful for tests to be able to configure this to a very small value,
2279            // but we also don't want it to be actually 0.
2280            0 => Duration::from_millis(50),
2281            secs => Duration::from_secs(secs),
2282        })
2283    }
2284
2285    /// Returns `true` if Relay requires authentication for readiness.
2286    ///
2287    /// See [`ReadinessCondition`] for more information.
2288    pub fn requires_auth(&self) -> bool {
2289        match self.inner.values.auth.ready {
2290            ReadinessCondition::Authenticated => self.relay_mode() == RelayMode::Managed,
2291            ReadinessCondition::Always => false,
2292        }
2293    }
2294
2295    /// Returns the interval at which Realy should try to re-authenticate with the upstream.
2296    ///
2297    /// Always disabled in processing mode.
2298    pub fn http_auth_interval(&self) -> Option<Duration> {
2299        if self.processing_enabled() {
2300            return None;
2301        }
2302
2303        match self.inner.values.http.auth_interval {
2304            None | Some(0) => None,
2305            Some(secs) => Some(Duration::from_secs(secs)),
2306        }
2307    }
2308
2309    /// The maximum time of experiencing uninterrupted network failures until Relay considers that
2310    /// it has encountered a network outage.
2311    pub fn http_outage_grace_period(&self) -> Duration {
2312        Duration::from_secs(self.inner.values.http.outage_grace_period)
2313    }
2314
2315    /// Time Relay waits before retrying an upstream request.
2316    ///
2317    /// Before going into a network outage, Relay may fail to make upstream
2318    /// requests. This is the time Relay waits before retrying the same request.
2319    pub fn http_retry_delay(&self) -> Duration {
2320        Duration::from_secs(self.inner.values.http.retry_delay)
2321    }
2322
2323    /// Time of continued project request failures before Relay emits an error.
2324    pub fn http_project_failure_interval(&self) -> Duration {
2325        Duration::from_secs(self.inner.values.http.project_failure_interval)
2326    }
2327
2328    /// Content encoding of upstream requests.
2329    pub fn http_encoding(&self) -> HttpEncoding {
2330        self.inner.values.http.encoding
2331    }
2332
2333    /// Returns whether metrics should be sent globally through a shared endpoint.
2334    pub fn http_global_metrics(&self) -> bool {
2335        self.inner.values.http.global_metrics
2336    }
2337
2338    /// Returns `true` if Relay supports forwarding unknown API requests.
2339    ///
2340    /// Relay instances with processing enabled are expected to support the latest API and do never
2341    /// support forwarding requests to Sentry.
2342    pub fn http_forward(&self) -> bool {
2343        self.inner.values.http.forward && !self.processing_enabled()
2344    }
2345
2346    /// Returns whether this Relay should emit outcomes.
2347    ///
2348    /// This is `true` either if `outcomes.emit_outcomes` is explicitly enabled, or if this Relay is
2349    /// in processing mode.
2350    pub fn emit_outcomes(&self) -> EmitOutcomes {
2351        if self.processing_enabled() {
2352            return EmitOutcomes::AsOutcomes;
2353        }
2354        self.inner.values.outcomes.emit_outcomes
2355    }
2356
2357    /// The originating source of the outcome
2358    pub fn outcome_source(&self) -> Option<&str> {
2359        self.inner.values.outcomes.source.as_deref()
2360    }
2361
2362    /// Returns logging configuration.
2363    pub fn logging(&self) -> &relay_log::LogConfig {
2364        &self.inner.values.logging
2365    }
2366
2367    /// Returns logging configuration.
2368    pub fn sentry(&self) -> &relay_log::SentryConfig {
2369        &self.inner.values.sentry
2370    }
2371
2372    /// Returns the addresses for statsd metrics.
2373    pub fn statsd_addr(&self) -> Option<&str> {
2374        self.inner.values.metrics.statsd.as_deref()
2375    }
2376
2377    /// Returns the addresses for statsd metrics.
2378    pub fn statsd_buffer_size(&self) -> Option<usize> {
2379        self.inner.values.metrics.statsd_buffer_size
2380    }
2381
2382    /// Return the prefix for statsd metrics.
2383    pub fn metrics_prefix(&self) -> &str {
2384        &self.inner.values.metrics.prefix
2385    }
2386
2387    /// Returns the default tags for statsd metrics.
2388    pub fn metrics_default_tags(&self) -> &BTreeMap<String, String> {
2389        &self.inner.values.metrics.default_tags
2390    }
2391
2392    /// Returns the name of the hostname tag that should be attached to each outgoing metric.
2393    pub fn metrics_hostname_tag(&self) -> Option<&str> {
2394        self.inner.values.metrics.hostname_tag.as_deref()
2395    }
2396
2397    /// Returns the interval for periodic metrics emitted from Relay.
2398    ///
2399    /// `None` if periodic metrics are disabled.
2400    pub fn metrics_periodic_interval(&self) -> Option<Duration> {
2401        match self.inner.values.metrics.periodic_secs {
2402            0 => None,
2403            secs => Some(Duration::from_secs(secs)),
2404        }
2405    }
2406
2407    /// Returns the default timeout for all upstream HTTP requests.
2408    pub fn http_timeout(&self) -> Duration {
2409        Duration::from_secs(self.inner.values.http.timeout.into())
2410    }
2411
2412    /// Returns the connection timeout for all upstream HTTP requests.
2413    pub fn http_connection_timeout(&self) -> Duration {
2414        Duration::from_secs(self.inner.values.http.connection_timeout.into())
2415    }
2416
2417    /// Returns the failed upstream request retry interval.
2418    pub fn http_max_retry_interval(&self) -> Duration {
2419        Duration::from_secs(self.inner.values.http.max_retry_interval.into())
2420    }
2421
2422    /// Returns `true` if relay should use an in-process cache for DNS lookups.
2423    pub fn http_dns_cache(&self) -> bool {
2424        self.inner.values.http.dns_cache
2425    }
2426
2427    /// Returns the expiry timeout for cached projects.
2428    pub fn project_cache_expiry(&self) -> Duration {
2429        Duration::from_secs(self.inner.values.cache.project_expiry.into())
2430    }
2431
2432    /// Returns `true` if the full project state should be requested from upstream.
2433    pub fn request_full_project_config(&self) -> bool {
2434        self.inner.values.cache.project_request_full_config
2435    }
2436
2437    /// Returns the expiry timeout for cached relay infos (public keys).
2438    pub fn relay_cache_expiry(&self) -> Duration {
2439        Duration::from_secs(self.inner.values.cache.relay_expiry.into())
2440    }
2441
2442    /// Returns the expiry timeout for cached misses before trying to refetch.
2443    pub fn cache_miss_expiry(&self) -> Duration {
2444        Duration::from_secs(self.inner.values.cache.miss_expiry.into())
2445    }
2446
2447    /// Returns the grace period for project caches.
2448    pub fn project_grace_period(&self) -> Duration {
2449        Duration::from_secs(self.inner.values.cache.project_grace_period.into())
2450    }
2451
2452    /// Returns the refresh interval for a project.
2453    ///
2454    /// Validates the refresh time to be between the grace period and expiry.
2455    pub fn project_refresh_interval(&self) -> Option<Duration> {
2456        self.inner
2457            .values
2458            .cache
2459            .project_refresh_interval
2460            .map(Into::into)
2461            .map(Duration::from_secs)
2462    }
2463
2464    /// Returns the duration in which batchable project config queries are
2465    /// collected before sending them in a single request.
2466    pub fn query_batch_interval(&self) -> Duration {
2467        Duration::from_millis(self.inner.values.cache.batch_interval.into())
2468    }
2469
2470    /// Returns the duration in which downstream relays are requested from upstream.
2471    pub fn downstream_relays_batch_interval(&self) -> Duration {
2472        Duration::from_millis(
2473            self.inner
2474                .values
2475                .cache
2476                .downstream_relays_batch_interval
2477                .into(),
2478        )
2479    }
2480
2481    /// Returns the interval in seconds in which local project configurations should be reloaded.
2482    pub fn local_cache_interval(&self) -> Duration {
2483        Duration::from_secs(self.inner.values.cache.file_interval.into())
2484    }
2485
2486    /// Returns the interval in seconds in which fresh global configs should be
2487    /// fetched from  upstream.
2488    pub fn global_config_fetch_interval(&self) -> Duration {
2489        Duration::from_secs(self.inner.values.cache.global_config_fetch_interval.into())
2490    }
2491
2492    /// Returns the path of the buffer file if the `cache.persistent_envelope_buffer.path` is configured.
2493    ///
2494    /// In case a partition with id > 0 is supplied, the filename of the envelopes path will be
2495    /// suffixed with `.{partition_id}`.
2496    pub fn spool_envelopes_path(&self, partition_id: u8) -> Option<PathBuf> {
2497        let mut path = self
2498            .inner
2499            .values
2500            .spool
2501            .envelopes
2502            .path
2503            .as_ref()
2504            .map(|path| path.to_owned())?;
2505
2506        if partition_id == 0 {
2507            return Some(path);
2508        }
2509
2510        let file_name = path.file_name().and_then(|f| f.to_str())?;
2511        let new_file_name = format!("{file_name}.{partition_id}");
2512        path.set_file_name(new_file_name);
2513
2514        Some(path)
2515    }
2516
2517    /// The maximum size of the buffer, in bytes.
2518    pub fn spool_envelopes_max_disk_size(&self) -> usize {
2519        self.inner.values.spool.envelopes.max_disk_size.as_bytes()
2520    }
2521
2522    /// Number of encoded envelope bytes that need to be accumulated before
2523    /// flushing one batch to disk.
2524    pub fn spool_envelopes_batch_size_bytes(&self) -> usize {
2525        self.inner
2526            .values
2527            .spool
2528            .envelopes
2529            .batch_size_bytes
2530            .as_bytes()
2531    }
2532
2533    /// Time after which a batch of envelopes is flushed to disk, regardless of its size.
2534    pub fn spool_envelopes_flush_timeout(&self) -> Option<Duration> {
2535        self.inner
2536            .values
2537            .spool
2538            .envelopes
2539            .flush_timeout_secs
2540            .map(Duration::from_secs)
2541    }
2542
2543    /// Maximum number of envelopes unspooled from disk per second.
2544    pub fn spool_max_unspool_envelopes_per_second(&self) -> Option<NonZeroU32> {
2545        self.inner
2546            .values
2547            .spool
2548            .envelopes
2549            .max_unspool_envelopes_per_second
2550    }
2551
2552    /// Returns the time after which we drop envelopes as a [`Duration`] object.
2553    pub fn spool_envelopes_max_age(&self) -> Duration {
2554        Duration::from_secs(self.inner.values.spool.envelopes.max_envelope_delay_secs)
2555    }
2556
2557    /// Returns the refresh frequency for disk usage monitoring as a [`Duration`] object.
2558    pub fn spool_disk_usage_refresh_frequency_ms(&self) -> Duration {
2559        Duration::from_millis(
2560            self.inner
2561                .values
2562                .spool
2563                .envelopes
2564                .disk_usage_refresh_frequency_ms,
2565        )
2566    }
2567
2568    /// Returns the relative memory usage up to which the disk buffer will unspool envelopes.
2569    pub fn spool_max_backpressure_memory_percent(&self) -> f32 {
2570        self.inner
2571            .values
2572            .spool
2573            .envelopes
2574            .max_backpressure_memory_percent
2575    }
2576
2577    /// Returns the number of partitions for the buffer.
2578    pub fn spool_partitions(&self) -> NonZeroU8 {
2579        self.inner.values.spool.envelopes.partitions
2580    }
2581
2582    /// Returns the strategy used to assign envelopes to buffer partitions.
2583    pub fn spool_partitioning(&self) -> EnvelopeSpoolPartitioning {
2584        self.inner.values.spool.envelopes.partitioning
2585    }
2586
2587    /// Returns `true` if the data is stored on ephemeral disks.
2588    pub fn spool_ephemeral(&self) -> bool {
2589        self.inner.values.spool.envelopes.ephemeral
2590    }
2591
2592    /// Returns the maximum size of an event payload in bytes.
2593    pub fn max_event_size(&self) -> usize {
2594        self.inner.values.limits.max_event_size.as_bytes()
2595    }
2596
2597    /// Returns the maximum size of each attachment.
2598    pub fn max_attachment_size(&self) -> usize {
2599        self.inner.values.limits.max_attachment_size.as_bytes()
2600    }
2601
2602    /// The maximum amount of attachments in a single envelope.
2603    pub fn max_attachment_count(&self) -> usize {
2604        self.inner.values.limits.max_attachment_count
2605    }
2606
2607    /// Returns the maximum combined size of attachments or payloads containing attachments
2608    /// (minidump, unreal, standalone attachments) in bytes.
2609    pub fn max_attachments_size(&self) -> usize {
2610        self.inner.values.limits.max_attachments_size.as_bytes()
2611    }
2612
2613    /// Returns the maximum size of a TUS upload request body.
2614    pub fn max_upload_size(&self) -> usize {
2615        self.inner.values.limits.max_upload_size.as_bytes()
2616    }
2617
2618    /// Returns the maximum number of client reports per envelope.
2619    pub fn max_client_reports_count(&self) -> usize {
2620        self.inner.values.limits.max_client_reports_count
2621    }
2622
2623    /// Returns the maximum combined size of client reports in bytes.
2624    pub fn max_client_reports_size(&self) -> usize {
2625        self.inner.values.limits.max_client_reports_size.as_bytes()
2626    }
2627
2628    /// Returns the maximum payload size of a monitor check-in in bytes.
2629    pub fn max_check_in_size(&self) -> usize {
2630        self.inner.values.limits.max_check_in_size.as_bytes()
2631    }
2632
2633    /// Returns the maximum payload size of a log in bytes.
2634    pub fn max_log_size(&self) -> usize {
2635        self.inner.values.limits.max_log_size.as_bytes()
2636    }
2637
2638    /// Returns the maximum number of operations to allow for a log expansion.
2639    pub fn max_expanded_log_operations(&self) -> usize {
2640        self.inner.values.limits.max_expanded_log_operations
2641    }
2642
2643    /// Returns the maximum number of operations to allow for a span expansion.
2644    pub fn max_expanded_span_operations(&self) -> usize {
2645        self.inner.values.limits.max_expanded_span_operations
2646    }
2647
2648    /// Returns the maximum number of operations to allow for a span expansion.
2649    pub fn max_expanded_trace_metric_operations(&self) -> usize {
2650        self.inner
2651            .values
2652            .limits
2653            .max_expanded_trace_metric_operations
2654    }
2655
2656    /// Returns the maximum payload size of a span in bytes.
2657    pub fn max_span_size(&self) -> usize {
2658        self.inner.values.limits.max_span_size.as_bytes()
2659    }
2660
2661    /// Returns the maximum amount of standalone transaction spans per envelope.
2662    pub fn max_standalone_span_count(&self) -> usize {
2663        self.inner.values.limits.max_standalone_span_count
2664    }
2665
2666    /// Returns the maximum payload size of an item container in bytes.
2667    pub fn max_container_size(&self) -> usize {
2668        self.inner.values.limits.max_container_size.as_bytes()
2669    }
2670
2671    /// Returns the maximum size of an envelope payload in bytes.
2672    ///
2673    /// Individual item size limits still apply.
2674    pub fn max_envelope_size(&self) -> usize {
2675        self.inner.values.limits.max_envelope_size.as_bytes()
2676    }
2677
2678    /// Returns the maximum number of sessions per envelope.
2679    pub fn max_session_count(&self) -> usize {
2680        self.inner.values.limits.max_session_count
2681    }
2682
2683    /// Returns the maximum combined size for all sessions in an envelope in bytes.
2684    pub fn max_sessions_size(&self) -> usize {
2685        self.inner.values.limits.max_sessions_size.as_bytes()
2686    }
2687
2688    /// Returns the maximum payload size of metric buckets in bytes.
2689    pub fn max_metric_buckets_size(&self) -> usize {
2690        self.inner.values.limits.max_metric_buckets_size.as_bytes()
2691    }
2692
2693    /// Returns the maximum payload size for general API requests.
2694    pub fn max_api_payload_size(&self) -> usize {
2695        self.inner.values.limits.max_api_payload_size.as_bytes()
2696    }
2697
2698    /// Returns the maximum payload size for file uploads and chunks.
2699    pub fn max_api_file_upload_size(&self) -> usize {
2700        self.inner.values.limits.max_api_file_upload_size.as_bytes()
2701    }
2702
2703    /// Returns the maximum payload size for chunks
2704    pub fn max_api_chunk_upload_size(&self) -> usize {
2705        self.inner
2706            .values
2707            .limits
2708            .max_api_chunk_upload_size
2709            .as_bytes()
2710    }
2711
2712    /// Returns the maximum payload size for a profile
2713    pub fn max_profile_size(&self) -> usize {
2714        self.inner.values.limits.max_profile_size.as_bytes()
2715    }
2716
2717    /// Returns the maximum payload size for a trace metric.
2718    pub fn max_trace_metric_size(&self) -> usize {
2719        self.inner.values.limits.max_trace_metric_size.as_bytes()
2720    }
2721
2722    /// Returns the maximum payload size for a compressed replay.
2723    pub fn max_replay_compressed_size(&self) -> usize {
2724        self.inner
2725            .values
2726            .limits
2727            .max_replay_compressed_size
2728            .as_bytes()
2729    }
2730
2731    /// Returns the maximum payload size for an uncompressed replay.
2732    pub fn max_replay_uncompressed_size(&self) -> usize {
2733        self.inner
2734            .values
2735            .limits
2736            .max_replay_uncompressed_size
2737            .as_bytes()
2738    }
2739
2740    /// Returns the maximum message size for an uncompressed replay.
2741    ///
2742    /// This is greater than max_replay_compressed_size because
2743    /// it can include additional metadata about the replay in
2744    /// addition to the recording.
2745    pub fn max_replay_message_size(&self) -> usize {
2746        self.inner.values.limits.max_replay_message_size.as_bytes()
2747    }
2748
2749    /// Returns the maximum number of active requests
2750    pub fn max_concurrent_requests(&self) -> usize {
2751        self.inner.values.limits.max_concurrent_requests
2752    }
2753
2754    /// Returns the maximum number of active queries
2755    pub fn max_concurrent_queries(&self) -> usize {
2756        self.inner.values.limits.max_concurrent_queries
2757    }
2758
2759    /// Returns the maximum combined size of keys of invalid attributes.
2760    pub fn max_removed_attribute_key_size(&self) -> usize {
2761        self.inner
2762            .values
2763            .limits
2764            .max_removed_attribute_key_size
2765            .as_bytes()
2766    }
2767
2768    /// The maximum number of seconds a query is allowed to take across retries.
2769    pub fn query_timeout(&self) -> Duration {
2770        Duration::from_secs(self.inner.values.limits.query_timeout)
2771    }
2772
2773    /// The maximum number of seconds to wait for pending envelopes after receiving a shutdown
2774    /// signal.
2775    pub fn shutdown_timeout(&self) -> Duration {
2776        Duration::from_secs(self.inner.values.limits.shutdown_timeout)
2777    }
2778
2779    /// Returns the server keep-alive timeout in seconds.
2780    ///
2781    /// By default keep alive is set to a 5 seconds.
2782    pub fn keepalive_timeout(&self) -> Duration {
2783        Duration::from_secs(self.inner.values.limits.keepalive_timeout)
2784    }
2785
2786    /// Returns the server idle timeout in seconds.
2787    pub fn idle_timeout(&self) -> Option<Duration> {
2788        self.inner
2789            .values
2790            .limits
2791            .idle_timeout
2792            .map(Duration::from_secs)
2793    }
2794
2795    /// Returns the maximum connections.
2796    pub fn max_connections(&self) -> Option<usize> {
2797        self.inner.values.limits.max_connections
2798    }
2799
2800    /// TCP listen backlog to configure on Relay's listening socket.
2801    pub fn tcp_listen_backlog(&self) -> u32 {
2802        self.inner.values.limits.tcp_listen_backlog
2803    }
2804
2805    /// Returns the number of cores to use for thread pools.
2806    pub fn cpu_concurrency(&self) -> usize {
2807        self.inner.values.limits.max_thread_count
2808    }
2809
2810    /// Returns the number of tasks that can run concurrently in the worker pool.
2811    pub fn pool_concurrency(&self) -> usize {
2812        self.inner.values.limits.max_pool_concurrency
2813    }
2814
2815    /// Returns the maximum size of a project config query.
2816    pub fn query_batch_size(&self) -> usize {
2817        self.inner.values.cache.batch_size
2818    }
2819
2820    /// True if the Relay should do processing.
2821    pub fn processing_enabled(&self) -> bool {
2822        self.inner.values.processing.enabled
2823    }
2824
2825    /// Level of normalization for Relay to apply to incoming data.
2826    pub fn normalization_level(&self) -> NormalizationLevel {
2827        self.inner.values.normalization.level
2828    }
2829
2830    /// The path to the GeoIp database required for event processing.
2831    pub fn geoip_path(&self) -> Option<&Path> {
2832        self.inner.values.geoip.path.as_deref().or(self
2833            .inner
2834            .values
2835            .processing
2836            .geoip_path
2837            .as_deref())
2838    }
2839
2840    /// Maximum future timestamp of ingested data.
2841    ///
2842    /// Events past this timestamp will be adjusted to `now()`. Sessions will be dropped.
2843    pub fn max_secs_in_future(&self) -> i64 {
2844        self.inner.values.processing.max_secs_in_future.into()
2845    }
2846
2847    /// Maximum age of ingested sessions. Older sessions will be dropped.
2848    pub fn max_session_secs_in_past(&self) -> i64 {
2849        self.inner.values.processing.max_session_secs_in_past.into()
2850    }
2851
2852    /// Configuration name and list of Kafka configuration parameters for a given topic.
2853    pub fn kafka_configs(
2854        &self,
2855        topic: KafkaTopic,
2856    ) -> Result<KafkaTopicConfig<'_>, KafkaConfigError> {
2857        self.inner
2858            .values
2859            .processing
2860            .topics
2861            .get(topic)
2862            .kafka_configs(
2863                &self.inner.values.processing.kafka_config,
2864                &self.inner.values.processing.secondary_kafka_configs,
2865            )
2866    }
2867
2868    /// Whether to validate the topics against Kafka.
2869    pub fn kafka_validate_topics(&self) -> bool {
2870        self.inner.values.processing.kafka_validate_topics
2871    }
2872
2873    /// Upper bound on the random delay before a Kafka producer swaps its client.
2874    pub fn reloading_producer_jitter(&self) -> Duration {
2875        Duration::from_secs(self.inner.values.processing.reloading_producer_jitter_secs)
2876    }
2877
2878    /// Builds the Kafka config blob describing every configured topic.
2879    ///
2880    /// # This is a stand-in
2881    ///
2882    /// The blob is meant to be opaque to Relay: one is published per region covering every
2883    /// cluster and topic, and Relay passes the bytes to Arroyo without interpreting them. That
2884    /// delivery channel does not yet exist, so this builds a blob out of Relay's own
2885    /// `kafka_config`, `secondary_kafka_configs` and `topics` instead.
2886    pub fn kafka_config_blob(&self) -> Result<Vec<u8>, KafkaConfigError> {
2887        // Sorted throughout, so an unchanged config always serializes to the same bytes and the
2888        // caller can skip pushing by comparing them.
2889        let mut clusters = BTreeMap::new();
2890        let mut topics = BTreeMap::new();
2891
2892        // A topic without a `config` uses the default `kafka_config`. The blob has no notion of
2893        // a default, so that becomes a cluster under a name derived to not collide with any
2894        // `secondary_kafka_configs` entry the user may have written.
2895        let default_cluster = default_cluster_name(&self.inner.values.processing);
2896
2897        for topic in KafkaTopic::iter() {
2898            let topic_config = self.kafka_configs(*topic)?;
2899
2900            for params in topic_config.topics() {
2901                let cluster_name = params.config_name.unwrap_or(&default_cluster);
2902
2903                let config: BTreeMap<_, _> = params
2904                    .params
2905                    .iter()
2906                    .map(|param| (param.name.as_str(), param.value.as_str()))
2907                    .collect();
2908
2909                clusters.insert(
2910                    cluster_name.to_owned(),
2911                    serde_json::json!({ "config": config }),
2912                );
2913
2914                // Arroyo keys the blob by logical topic and maps it to one physical topic. Relay's
2915                // sharded topics are one logical topic to many physical ones, which Arroyo cannot
2916                // express yet, so each physical topic is its own entry and maps to itself.
2917                //
2918                // That also means one topic name cannot point at two clusters.
2919                let entry = serde_json::json!({ "physical_topics": [{ "cluster": cluster_name }] });
2920                match topics.entry(params.topic_name.clone()) {
2921                    Entry::Vacant(vacant) => {
2922                        vacant.insert(entry);
2923                    }
2924                    Entry::Occupied(occupied) if *occupied.get() != entry => {
2925                        return Err(KafkaConfigError::AmbiguousTopic(params.topic_name.clone()));
2926                    }
2927                    Entry::Occupied(_) => {}
2928                }
2929            }
2930        }
2931
2932        // Serializing maps of strings cannot fail.
2933        Ok(serde_json::to_vec(&serde_json::json!({
2934            "clusters": clusters,
2935            "topics": topics,
2936        }))
2937        .expect("kafka config blob is serializable"))
2938    }
2939
2940    /// All unused but configured topic assignments.
2941    pub fn unused_topic_assignments(&self) -> &relay_kafka::Unused {
2942        &self.inner.values.processing.topics.unused
2943    }
2944
2945    /// Configuration of the objectstore service.
2946    pub fn objectstore(&self) -> &ObjectstoreServiceConfig {
2947        &self.inner.values.processing.objectstore
2948    }
2949
2950    /// Configuration of the upload service.
2951    pub fn upload(&self) -> &Upload {
2952        &self.inner.values.upload
2953    }
2954
2955    /// Returns the key used to sign upload locations.
2956    #[cfg(feature = "processing")]
2957    pub fn upload_signing_key(&self) -> Option<&SecretKey> {
2958        self.upload()
2959            .credentials
2960            .as_ref()
2961            .map(|c| &c.signing_key)
2962            .or(self.credentials().map(|c| &c.secret_key))
2963    }
2964
2965    /// Returns the key used to verify upload locations.
2966    #[cfg(feature = "processing")]
2967    pub fn upload_verification_key(&self) -> Option<&PublicKey> {
2968        self.upload()
2969            .credentials
2970            .as_ref()
2971            .map(|c| &c.verification_key)
2972            .or(self.credentials().map(|c| &c.public_key))
2973    }
2974
2975    /// Redis servers to connect to for project configs, rate limiting, and metrics metadata.
2976    pub fn redis(&self) -> Option<RedisConfigsRef<'_>> {
2977        let redis_configs = self.inner.values.processing.redis.as_ref()?;
2978
2979        Some(build_redis_configs(
2980            redis_configs,
2981            self.cpu_concurrency() as u32,
2982            self.pool_concurrency() as u32,
2983        ))
2984    }
2985
2986    /// Chunk size of attachments in bytes.
2987    pub fn attachment_chunk_size(&self) -> usize {
2988        self.inner
2989            .values
2990            .processing
2991            .attachment_chunk_size
2992            .as_bytes()
2993    }
2994
2995    /// Maximum metrics batch size in bytes.
2996    pub fn metrics_max_batch_size_bytes(&self) -> usize {
2997        self.inner.values.aggregator.max_flush_bytes
2998    }
2999
3000    /// Default prefix to use when looking up project configs in Redis. This is only done when
3001    /// Relay is in processing mode.
3002    pub fn projectconfig_cache_prefix(&self) -> &str {
3003        &self.inner.values.processing.projectconfig_cache_prefix
3004    }
3005
3006    /// Maximum rate limit to report to clients in seconds.
3007    pub fn max_rate_limit(&self) -> Option<u64> {
3008        self.inner.values.processing.max_rate_limit.map(u32::into)
3009    }
3010
3011    /// Amount of remaining quota which is cached in memory.
3012    pub fn quota_cache_ratio(&self) -> Option<f32> {
3013        self.inner.values.processing.quota_cache_ratio
3014    }
3015
3016    /// Maximum limit (ratio) for the in memory quota cache.
3017    pub fn quota_cache_max(&self) -> Option<f32> {
3018        self.inner.values.processing.quota_cache_max
3019    }
3020
3021    /// Interval to refresh internal health checks.
3022    pub fn health_refresh_interval(&self) -> Duration {
3023        Duration::from_millis(self.inner.values.health.refresh_interval_ms)
3024    }
3025
3026    /// Maximum memory watermark in bytes.
3027    pub fn health_max_memory_watermark_bytes(&self) -> u64 {
3028        self.inner
3029            .values
3030            .health
3031            .max_memory_bytes
3032            .as_ref()
3033            .map_or(u64::MAX, |b| b.as_bytes() as u64)
3034    }
3035
3036    /// Maximum memory watermark as a percentage of maximum system memory.
3037    pub fn health_max_memory_watermark_percent(&self) -> f32 {
3038        self.inner.values.health.max_memory_percent
3039    }
3040
3041    /// Health check probe timeout.
3042    pub fn health_probe_timeout(&self) -> Duration {
3043        Duration::from_millis(self.inner.values.health.probe_timeout_ms)
3044    }
3045
3046    /// Refresh frequency for polling new memory stats.
3047    pub fn memory_stat_refresh_frequency_ms(&self) -> u64 {
3048        self.inner.values.health.memory_stat_refresh_frequency_ms
3049    }
3050
3051    /// Maximum amount of COGS measurements buffered in memory.
3052    pub fn cogs_max_queue_size(&self) -> u64 {
3053        self.inner.values.cogs.max_queue_size
3054    }
3055
3056    /// Resource ID to use for Relay COGS measurements.
3057    pub fn cogs_relay_resource_id(&self) -> &str {
3058        &self.inner.values.cogs.relay_resource_id
3059    }
3060
3061    /// Returns configuration for the default metrics aggregator.
3062    pub fn default_aggregator_config(&self) -> &AggregatorServiceConfig {
3063        &self.inner.values.aggregator
3064    }
3065
3066    /// Returns configuration for non-default metrics aggregator.
3067    pub fn secondary_aggregator_configs(&self) -> &Vec<ScopedAggregatorConfig> {
3068        &self.inner.values.secondary_aggregators
3069    }
3070
3071    /// Returns aggregator config for a given metrics namespace.
3072    pub fn aggregator_config_for(&self, namespace: MetricNamespace) -> &AggregatorServiceConfig {
3073        for entry in &self.inner.values.secondary_aggregators {
3074            if entry.condition.matches(Some(namespace)) {
3075                return &entry.config;
3076            }
3077        }
3078        &self.inner.values.aggregator
3079    }
3080
3081    /// Return the statically configured Relays.
3082    pub fn static_relays(&self) -> &HashMap<RelayId, RelayInfo> {
3083        &self.inner.values.auth.static_relays
3084    }
3085
3086    /// Returns the max age a signature is considered valid, in seconds.
3087    pub fn signature_max_age(&self) -> Duration {
3088        Duration::from_secs(self.inner.values.auth.signature_max_age)
3089    }
3090
3091    /// Returns `true` if unknown items should be accepted and forwarded.
3092    pub fn accept_unknown_items(&self) -> bool {
3093        let forward = self.inner.values.routing.accept_unknown_items;
3094        forward.unwrap_or_else(|| !self.processing_enabled())
3095    }
3096}
3097
3098impl fmt::Debug for ConfigSnapshot {
3099    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3100        f.debug_struct("ConfigSnapshot")
3101            .field("values", &self.inner.values)
3102            .finish()
3103    }
3104}
3105
3106#[cfg(test)]
3107mod tests {
3108    use super::*;
3109
3110    #[test]
3111    fn test_http_encoding_parse_strict() {
3112        for (value, expected) in [
3113            ("identity", Some(HttpEncoding::Identity)),
3114            ("gzip", Some(HttpEncoding::Gzip)),
3115            ("deflate", Some(HttpEncoding::Deflate)),
3116            ("br", Some(HttpEncoding::Br)),
3117            ("zstd", Some(HttpEncoding::Zstd)),
3118            ("", None),
3119            ("unknown", None),
3120            ("x-gzip", None),
3121            ("GZIP", None),
3122            (" gzip ", None),
3123            ("zstd, gzip", None),
3124        ] {
3125            assert_eq!(HttpEncoding::parse_strict(value), expected, "{value:?}");
3126        }
3127    }
3128
3129    #[cfg(feature = "processing")]
3130    #[test]
3131    fn test_upload_secret_key_from_file() {
3132        let path = env::temp_dir().join(Uuid::new_v4().to_string());
3133        fs::create_dir(&path).unwrap();
3134        fs::write(
3135            path.join("my_secret.txt"),
3136            "U3LSQM5NorvgnoYHW_aZpc_43nuuh3lhs3zjjcBwaks",
3137        )
3138        .unwrap();
3139        fs::write(
3140            ConfigValues::path(&path),
3141            r#"
3142    upload:
3143        credentials:
3144            signing_key: ${file:my_secret.txt}
3145            verification_key: "VNS8haF0VTnuMMDR2t-f7AgnmUcXmcdzV3SVksSk34s""#,
3146        )
3147        .unwrap();
3148
3149        let config = Config::from_path(&path).unwrap().current();
3150
3151        fs::remove_dir_all(path).unwrap();
3152
3153        let signing_key = &config.upload().credentials.as_ref().unwrap().signing_key;
3154        assert_eq!(
3155            signing_key.to_string(),
3156            "U3LSQM5NorvgnoYHW_aZpc_43nuuh3lhs3zjjcBwaks"
3157        );
3158    }
3159
3160    #[test]
3161    fn test_emit_outcomes() {
3162        for (serialized, deserialized) in &[
3163            ("true", EmitOutcomes::AsOutcomes),
3164            ("false", EmitOutcomes::None),
3165            ("\"as_client_reports\"", EmitOutcomes::AsClientReports),
3166        ] {
3167            let value: EmitOutcomes = serde_json::from_str(serialized).unwrap();
3168            assert_eq!(value, *deserialized);
3169            assert_eq!(serde_json::to_string(&value).unwrap(), *serialized);
3170        }
3171    }
3172
3173    #[test]
3174    fn test_emit_outcomes_invalid() {
3175        assert!(serde_json::from_str::<EmitOutcomes>("asdf").is_err());
3176    }
3177
3178    /// Builds a config from YAML under `processing`.
3179    #[cfg(feature = "processing")]
3180    fn processing_config(yaml: &str) -> Config {
3181        let path = env::temp_dir().join(Uuid::new_v4().to_string());
3182        fs::create_dir(&path).unwrap();
3183        fs::write(ConfigValues::path(&path), yaml).unwrap();
3184        Config::from_path(&path).unwrap()
3185    }
3186
3187    /// A generated blob must resolve to the same broker params Relay would have built directly.
3188    #[cfg(feature = "processing")]
3189    #[test]
3190    fn test_kafka_config_blob_round_trips() {
3191        use sentry_arroyo::backends::kafka::config_blob::{ConfigBlob, ProducerSelector};
3192
3193        // Covers sharding, a secondary cluster, and a secondary config named `default`.
3194        let config = processing_config(
3195            r#"
3196processing:
3197  enabled: true
3198  kafka_config:
3199    - {name: "bootstrap.servers", value: "primary:9092"}
3200    - {name: "compression.type", value: "lz4"}
3201  secondary_kafka_configs:
3202    default:
3203      - {name: "bootstrap.servers", value: "impostor:9092"}
3204  topics:
3205    ingest-events: ["events-1", "events-2"]
3206    ingest-metrics:
3207      name: "metrics-topic"
3208      config: "default"
3209"#,
3210        );
3211        let config = config.current();
3212
3213        let blob = config.kafka_config_blob().unwrap();
3214        let parsed = ConfigBlob::parse(&blob).unwrap();
3215
3216        let resolve = |topic: &str| {
3217            let resolved = parsed
3218                .producer_config(&ProducerSelector::new(topic, "relay"))
3219                .unwrap_or_else(|_| panic!("{topic} is missing from the blob"));
3220
3221            // Each entry must produce to the topic it is keyed by.
3222            assert_eq!(resolved.physical_topic.as_str(), topic);
3223
3224            resolved
3225                .config
3226                .config_map()
3227                .iter()
3228                // Arroyo adds these itself when building a producer config.
3229                .filter(|(key, _)| !matches!(key.as_str(), "statistics.interval.ms" | "client.id"))
3230                .map(|(key, value)| (key.clone(), value.clone()))
3231                .collect::<BTreeMap<_, _>>()
3232        };
3233
3234        for (topic, physical) in [
3235            (KafkaTopic::Events, ["events-1", "events-2"].as_slice()),
3236            (KafkaTopic::MetricsSessions, ["metrics-topic"].as_slice()),
3237        ] {
3238            let params = config.kafka_configs(topic).unwrap();
3239
3240            for (shard, physical) in params.topics().iter().zip(physical) {
3241                let expected: BTreeMap<_, _> = shard
3242                    .params
3243                    .iter()
3244                    .map(|param| (param.name.clone(), param.value.clone()))
3245                    .collect();
3246
3247                assert_eq!(
3248                    resolve(physical),
3249                    expected,
3250                    "{physical} resolved differently"
3251                );
3252            }
3253        }
3254
3255        // The secondary config named `default` must not have swallowed the anonymous one.
3256        assert_eq!(
3257            resolve("events-1").get("bootstrap.servers").unwrap(),
3258            "primary:9092"
3259        );
3260        assert_eq!(
3261            resolve("metrics-topic").get("bootstrap.servers").unwrap(),
3262            "impostor:9092"
3263        );
3264    }
3265
3266    /// Moving a topic to a different cluster must change the broker it resolves to.
3267    #[cfg(feature = "processing")]
3268    #[test]
3269    fn test_topic_can_move_between_clusters() {
3270        use sentry_arroyo::backends::kafka::config_blob::{ConfigBlob, ProducerSelector};
3271
3272        let resolve = |yaml: &str| {
3273            let config = processing_config(yaml);
3274            let blob = config.current().kafka_config_blob().unwrap();
3275            ConfigBlob::parse(&blob)
3276                .unwrap()
3277                .producer_config(&ProducerSelector::new("events-topic", "relay"))
3278                .unwrap()
3279                .config
3280                .get_config_value("bootstrap.servers")
3281                .unwrap()
3282                .clone()
3283        };
3284
3285        let before = resolve(
3286            r#"
3287processing:
3288  enabled: true
3289  kafka_config:
3290    - {name: "bootstrap.servers", value: "primary:9092"}
3291  secondary_kafka_configs:
3292    other:
3293      - {name: "bootstrap.servers", value: "other:9092"}
3294  topics:
3295    ingest-events: "events-topic"
3296"#,
3297        );
3298
3299        // Same physical topic, now pointed at the secondary cluster.
3300        let after = resolve(
3301            r#"
3302processing:
3303  enabled: true
3304  kafka_config:
3305    - {name: "bootstrap.servers", value: "primary:9092"}
3306  secondary_kafka_configs:
3307    other:
3308      - {name: "bootstrap.servers", value: "other:9092"}
3309  topics:
3310    ingest-events:
3311      name: "events-topic"
3312      config: "other"
3313"#,
3314        );
3315
3316        assert_eq!(before, "primary:9092");
3317        assert_eq!(after, "other:9092");
3318    }
3319
3320    /// The blob is keyed by topic name, so one name on two clusters cannot be expressed. That
3321    /// must fail loudly instead of sending one of the two to the wrong cluster.
3322    #[cfg(feature = "processing")]
3323    #[test]
3324    fn test_kafka_config_blob_rejects_topic_on_two_clusters() {
3325        let config = processing_config(
3326            r#"
3327processing:
3328  enabled: true
3329  kafka_config:
3330    - {name: "bootstrap.servers", value: "primary:9092"}
3331  secondary_kafka_configs:
3332    other:
3333      - {name: "bootstrap.servers", value: "other:9092"}
3334  topics:
3335    outcomes:
3336      name: "outcomes-topic"
3337      config: "other"
3338    outcomes-billing: "outcomes-topic"
3339"#,
3340        );
3341
3342        assert!(matches!(
3343            config.current().kafka_config_blob(),
3344            Err(KafkaConfigError::AmbiguousTopic(topic)) if topic == "outcomes-topic"
3345        ));
3346    }
3347
3348    /// Broker settings reload; topic assignments do not, since producers hold theirs from
3349    /// startup.
3350    #[test]
3351    fn test_reload_takes_kafka_config_but_not_topics() {
3352        let mut current = ConfigValues::default();
3353        current.processing.kafka_config = vec![KafkaConfigParam {
3354            name: "bootstrap.servers".to_owned(),
3355            value: "old:9092".to_owned(),
3356        }];
3357        current.processing.topics.events = "old-topic".to_owned().into();
3358
3359        let mut other = ConfigValues::default();
3360        other.processing.kafka_config = vec![KafkaConfigParam {
3361            name: "bootstrap.servers".to_owned(),
3362            value: "new:9092".to_owned(),
3363        }];
3364        other.processing.topics.events = "new-topic".to_owned().into();
3365
3366        let mut current = ConfigInner {
3367            values: current,
3368            ..Default::default()
3369        };
3370        let other = ConfigInner {
3371            values: other,
3372            ..Default::default()
3373        };
3374
3375        assert!(current.reload_with(&other));
3376        assert_eq!(current.values.processing.kafka_config[0].value, "new:9092");
3377
3378        let assignment = format!("{:?}", current.values.processing.topics.events);
3379        assert!(
3380            assignment.contains("old-topic"),
3381            "topic assignment was reloaded, but the producers cannot pick it up: {assignment}"
3382        );
3383    }
3384}