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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
40#[non_exhaustive]
41pub enum ConfigErrorKind {
42 CouldNotOpenFile,
44 CouldNotWriteFile,
46 BadYaml,
48 BadJson,
50 InvalidValue,
52 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#[derive(Debug, Default)]
75enum ConfigErrorSource {
76 #[default]
78 None,
79 File(PathBuf),
81 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#[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 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 fn format() -> ConfigFormat;
160
161 fn name() -> &'static str;
163
164 fn path(base: &Path) -> PathBuf {
166 base.join(format!("{}.{}", Self::name(), Self::format().extension()))
167 }
168
169 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 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 #[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#[derive(Debug, Default, Clone)]
235pub struct OverridableConfig {
236 pub mode: Option<String>,
238 pub instance: Option<String>,
240 pub log_level: Option<String>,
242 pub log_format: Option<String>,
244 pub upstream: Option<String>,
246 pub upstream_dsn: Option<String>,
248 pub host: Option<String>,
250 pub port: Option<String>,
252 pub processing: Option<String>,
254 pub kafka_url: Option<String>,
256 pub redis_url: Option<String>,
258 pub id: Option<String>,
260 pub secret_key: Option<String>,
262 pub public_key: Option<String>,
264 pub outcome_source: Option<String>,
266 pub shutdown_timeout: Option<String>,
268 pub server_name: Option<String>,
270}
271
272#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
274pub struct Credentials {
275 pub secret_key: SecretKey,
277 pub public_key: PublicKey,
279 pub id: RelayId,
281}
282
283impl Credentials {
284 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 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase")]
314pub struct RelayInfo {
315 pub public_key: PublicKey,
317
318 #[serde(default)]
320 pub internal: bool,
321}
322
323impl RelayInfo {
324 pub fn new(public_key: PublicKey) -> Self {
326 Self {
327 public_key,
328 internal: false,
329 }
330 }
331}
332
333#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
335#[serde(rename_all = "camelCase")]
336pub enum RelayMode {
337 Proxy,
343
344 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
383#[serde(rename_all = "camelCase")]
384pub enum RelayInstance {
385 Default,
387
388 Canary,
390}
391
392impl RelayInstance {
393 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#[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
443fn is_default<T: Default + PartialEq>(t: &T) -> bool {
445 *t == T::default()
446}
447
448fn 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
457fn default_host() -> IpAddr {
459 if is_docker() {
460 "0.0.0.0".parse().unwrap()
462 } else {
463 "127.0.0.1".parse().unwrap()
464 }
465}
466
467#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
471#[serde(rename_all = "lowercase")]
472#[derive(Default)]
473pub enum ReadinessCondition {
474 #[default]
483 Authenticated,
484 Always,
486}
487
488#[derive(Serialize, Deserialize, Debug, Clone)]
490#[serde(default)]
491pub struct Relay {
492 pub mode: RelayMode,
494 pub instance: RelayInstance,
496 pub upstream: UpstreamDescriptor,
498 pub advertised_upstream: Option<UpstreamDescriptor>,
507 pub host: IpAddr,
509 pub port: u16,
511 pub internal_host: Option<IpAddr>,
525 pub internal_port: Option<u16>,
529 #[serde(skip_serializing_if = "is_default")]
534 pub override_project_ids: bool,
535 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#[derive(Serialize, Deserialize, Debug, Clone)]
565#[serde(default)]
566pub struct Metrics {
567 pub statsd: Option<String>,
571 pub statsd_buffer_size: Option<usize>,
575 pub prefix: String,
579 pub default_tags: BTreeMap<String, String>,
581 pub hostname_tag: Option<String>,
583 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#[derive(Serialize, Deserialize, Debug, Clone)]
605#[serde(default)]
606pub struct Limits {
607 pub max_concurrent_requests: usize,
610 pub max_concurrent_queries: usize,
615 pub max_event_size: ByteSize,
617 pub max_attachment_size: ByteSize,
619 pub max_attachment_count: usize,
621 pub max_attachments_size: ByteSize,
623 pub max_upload_size: ByteSize,
628 pub max_client_reports_size: ByteSize,
630 pub max_client_reports_count: usize,
632 pub max_check_in_size: ByteSize,
634 pub max_envelope_size: ByteSize,
636 pub max_sessions_size: ByteSize,
638 pub max_session_count: usize,
640 pub max_api_payload_size: ByteSize,
642 pub max_api_file_upload_size: ByteSize,
644 pub max_api_chunk_upload_size: ByteSize,
646 pub max_profile_size: ByteSize,
648 pub max_trace_metric_size: ByteSize,
650 pub max_log_size: ByteSize,
652 pub max_expanded_log_operations: usize,
654 pub max_expanded_span_operations: usize,
656 pub max_expanded_trace_metric_operations: usize,
658 pub max_span_size: ByteSize,
660 pub max_standalone_span_count: usize,
662 pub max_container_size: ByteSize,
664 pub max_metric_buckets_size: ByteSize,
666 pub max_replay_compressed_size: ByteSize,
668 #[serde(alias = "max_replay_size")]
670 max_replay_uncompressed_size: ByteSize,
671 pub max_replay_message_size: ByteSize,
673 pub max_removed_attribute_key_size: ByteSize,
683 pub max_thread_count: usize,
688 pub max_pool_concurrency: usize,
695 pub query_timeout: u64,
698 pub shutdown_timeout: u64,
701 pub keepalive_timeout: u64,
705 pub idle_timeout: Option<u64>,
712 pub max_connections: Option<usize>,
718 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#[derive(Debug, Default, Deserialize, Serialize, Clone)]
775#[serde(default)]
776pub struct Routing {
777 pub accept_unknown_items: Option<bool>,
787}
788
789#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
791#[serde(rename_all = "lowercase")]
792pub enum HttpEncoding {
793 #[default]
798 Identity,
799 Deflate,
805 Gzip,
812 Br,
814 Zstd,
816}
817
818impl HttpEncoding {
819 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 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 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#[derive(Serialize, Deserialize, Debug, Clone)]
866#[serde(default)]
867pub struct Http {
868 pub timeout: u32,
874 pub connection_timeout: u32,
879 pub max_retry_interval: u32,
881 pub host_header: Option<String>,
883 pub auth_interval: Option<u64>,
891 pub outage_grace_period: u64,
897 pub retry_delay: u64,
901 pub project_failure_interval: u64,
906 pub encoding: HttpEncoding,
922 pub global_metrics: bool,
929 pub forward: bool,
936 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, host_header: None,
949 auth_interval: Some(600), 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Default, Deserialize, Serialize)]
963#[serde(rename_all = "snake_case")]
964pub enum EnvelopeSpoolPartitioning {
965 ProjectKeyPair,
969 #[default]
976 RoundRobin,
977}
978
979#[derive(Debug, Serialize, Deserialize, Clone)]
981#[serde(default)]
982pub struct EnvelopeSpool {
983 pub path: Option<PathBuf>,
989 pub max_disk_size: ByteSize,
995 pub batch_size_bytes: ByteSize,
1002 pub flush_timeout_secs: Option<u64>,
1008 pub max_envelope_delay_secs: u64,
1015 pub disk_usage_refresh_frequency_ms: u64,
1020 pub max_backpressure_memory_percent: f32,
1050 pub partitions: NonZeroU8,
1057 pub partitioning: EnvelopeSpoolPartitioning,
1063 pub max_unspool_envelopes_per_second: Option<NonZeroU32>,
1069 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#[derive(Debug, Serialize, Deserialize, Default, Clone)]
1098#[serde(default)]
1099pub struct Spool {
1100 pub envelopes: EnvelopeSpool,
1102}
1103
1104#[derive(Serialize, Deserialize, Debug, Clone)]
1106#[serde(default)]
1107pub struct Cache {
1108 pub project_request_full_config: bool,
1113 pub project_expiry: u32,
1115 pub project_grace_period: u32,
1120 pub project_refresh_interval: Option<u32>,
1126 pub relay_expiry: u32,
1128 pub miss_expiry: u32,
1130 pub batch_interval: u32,
1132 pub downstream_relays_batch_interval: u32,
1134 pub batch_size: usize,
1138 pub file_interval: u32,
1140 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, project_grace_period: 120, project_refresh_interval: None,
1151 relay_expiry: 3600, miss_expiry: 60, batch_interval: 100, downstream_relays_batch_interval: 100, batch_size: 500,
1156 file_interval: 10, global_config_fetch_interval: 10, }
1159 }
1160}
1161
1162#[derive(Serialize, Deserialize, Debug, Clone)]
1164#[serde(default)]
1165pub struct Processing {
1166 pub enabled: bool,
1168 pub geoip_path: Option<PathBuf>,
1170 pub max_secs_in_future: u32,
1172 pub max_session_secs_in_past: u32,
1174 pub kafka_config: Vec<KafkaConfigParam>,
1176 pub secondary_kafka_configs: BTreeMap<String, Vec<KafkaConfigParam>>,
1196 pub topics: TopicAssignments,
1198 pub kafka_validate_topics: bool,
1200 pub reloading_producer_jitter_secs: u64,
1207 pub redis: Option<RedisConfigs>,
1209 pub attachment_chunk_size: ByteSize,
1211 pub projectconfig_cache_prefix: String,
1213 pub max_rate_limit: Option<u32>,
1215 pub quota_cache_ratio: Option<f32>,
1226 pub quota_cache_max: Option<f32>,
1233 #[serde(alias = "upload")]
1235 pub objectstore: ObjectstoreServiceConfig,
1236}
1237
1238impl Default for Processing {
1239 fn default() -> Self {
1241 Self {
1242 enabled: false,
1243 geoip_path: None,
1244 max_secs_in_future: 60, max_session_secs_in_past: 5 * 24 * 3600, 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), quota_cache_ratio: None,
1256 quota_cache_max: None,
1257 objectstore: ObjectstoreServiceConfig::default(),
1258 }
1259 }
1260}
1261
1262fn 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#[derive(Debug, Default, Serialize, Deserialize, Clone)]
1277#[serde(default)]
1278pub struct Normalization {
1279 pub level: NormalizationLevel,
1281}
1282
1283#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
1285#[serde(rename_all = "lowercase")]
1286pub enum NormalizationLevel {
1287 #[default]
1291 Default,
1292 Full,
1297}
1298
1299#[derive(Serialize, Deserialize, Clone)]
1301pub struct ObjectstoreAuthConfig {
1302 pub key_id: String,
1305
1306 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#[derive(Serialize, Deserialize, Debug, Clone)]
1321#[serde(default)]
1322pub struct ObjectstoreServiceConfig {
1323 pub objectstore_url: Option<String>,
1328
1329 pub max_concurrent_requests: usize,
1331
1332 pub max_backlog: usize,
1336
1337 pub timeout: u64,
1342
1343 pub stream_timeout: u64,
1348
1349 pub retry_delay: f64,
1351
1352 pub max_attempts: NonZeroU16,
1354
1355 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, retry_delay: 1.0,
1368 max_attempts: NonZeroU16::new(5).unwrap(),
1369 auth: None,
1370 }
1371 }
1372}
1373
1374#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1377
1378pub enum EmitOutcomes {
1379 None,
1381 AsClientReports,
1383 AsOutcomes,
1385}
1386
1387impl EmitOutcomes {
1388 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 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#[derive(Serialize, Deserialize, Debug, Clone)]
1450#[serde(default)]
1451pub struct Outcomes {
1452 pub emit_outcomes: EmitOutcomes,
1456 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#[derive(Serialize, Deserialize, Debug, Default)]
1472pub struct MinimalConfig {
1473 pub relay: Relay,
1475}
1476
1477impl MinimalConfig {
1478 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
1499mod config_relay_info {
1501 use serde::ser::SerializeMap;
1502
1503 use super::*;
1504
1505 #[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#[derive(Serialize, Deserialize, Debug, Clone)]
1555#[serde(default)]
1556pub struct AuthConfig {
1557 #[serde(skip_serializing_if = "is_default")]
1559 pub ready: ReadinessCondition,
1560
1561 #[serde(with = "config_relay_info")]
1563 pub static_relays: HashMap<RelayId, RelayInfo>,
1564
1565 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, }
1578 }
1579}
1580
1581#[derive(Serialize, Deserialize, Debug, Default, Clone)]
1583pub struct GeoIpConfig {
1584 pub path: Option<PathBuf>,
1586}
1587
1588#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1593#[serde(default)]
1594pub struct Health {
1595 pub refresh_interval_ms: u64,
1602 pub max_memory_bytes: Option<ByteSize>,
1607 pub max_memory_percent: f32,
1611 pub probe_timeout_ms: u64,
1618 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#[derive(Serialize, Deserialize, Debug, Clone)]
1640#[serde(default)]
1641pub struct Cogs {
1642 pub max_queue_size: u64,
1648 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#[derive(Debug, Clone, Serialize, Deserialize)]
1667#[serde(default)]
1668pub struct Upload {
1669 pub max_concurrent_requests: usize,
1673 pub timeout: u64,
1675 pub max_age: i64,
1679
1680 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, max_age: 60 * 60, credentials: None,
1693 }
1694 }
1695}
1696
1697#[derive(Clone, Serialize, Deserialize)]
1699pub struct UploadCredentials {
1700 #[cfg(feature = "processing")]
1702 pub signing_key: SecretKey,
1703
1704 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#[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 values: ConfigValues,
1762 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 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 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 }
1901 _ => {
1902 return Err(ConfigError::field("incomplete credentials").into());
1903 }
1904 }
1905 }
1906
1907 Ok(())
1908 }
1909
1910 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 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
1962pub struct Config {
1964 inner_access: Mutex<()>,
1973 inner: ArcSwap<ConfigInner>,
1975 overrides: Vec<OverridableConfig>,
1980 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 .field("values", &inner.values)
1994 .finish()
1995 }
1996}
1997
1998impl Config {
1999 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 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 pub fn apply_override(&mut self, overrides: OverridableConfig) -> anyhow::Result<&mut Self> {
2048 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 self.overrides.push(overrides);
2058
2059 Ok(self)
2060 }
2061
2062 pub fn config_exists<P: AsRef<Path>>(path: P) -> bool {
2064 fs::metadata(ConfigValues::path(path.as_ref())).is_ok()
2065 }
2066
2067 pub fn path(&self) -> &Path {
2069 &self.path
2070 }
2071
2072 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 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 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 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 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 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
2179pub struct ConfigSnapshot {
2187 inner: arc_swap::Guard<Arc<ConfigInner>>,
2188}
2189
2190impl ConfigSnapshot {
2191 pub fn has_credentials(&self) -> bool {
2193 self.inner.credentials.is_some()
2194 }
2195
2196 pub fn credentials(&self) -> Option<&Credentials> {
2198 self.inner.credentials.as_ref()
2199 }
2200
2201 pub fn secret_key(&self) -> Option<&SecretKey> {
2203 self.inner.credentials.as_ref().map(|x| &x.secret_key)
2204 }
2205
2206 pub fn public_key(&self) -> Option<&PublicKey> {
2208 self.inner.credentials.as_ref().map(|x| &x.public_key)
2209 }
2210
2211 pub fn relay_id(&self) -> Option<&RelayId> {
2213 self.inner.credentials.as_ref().map(|x| &x.id)
2214 }
2215
2216 pub fn relay_mode(&self) -> RelayMode {
2218 self.inner.values.relay.mode
2219 }
2220
2221 pub fn relay_instance(&self) -> RelayInstance {
2223 self.inner.values.relay.instance
2224 }
2225
2226 pub fn upstream(&self) -> &UpstreamDescriptor {
2228 &self.inner.values.relay.upstream
2229 }
2230
2231 pub fn advertised_upstream(&self) -> Option<&UpstreamDescriptor> {
2233 self.inner.values.relay.advertised_upstream.as_ref()
2234 }
2235
2236 pub fn http_host_header(&self) -> Option<&str> {
2238 self.inner.values.http.host_header.as_deref()
2239 }
2240
2241 pub fn listen_addr(&self) -> SocketAddr {
2243 (self.inner.values.relay.host, self.inner.values.relay.port).into()
2244 }
2245
2246 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 pub fn override_project_ids(&self) -> bool {
2269 self.inner.values.relay.override_project_ids
2270 }
2271
2272 pub fn config_reload_interval(&self) -> Option<Duration> {
2276 let interval = self.inner.values.relay.config_reload_interval?;
2277 Some(match interval {
2278 0 => Duration::from_millis(50),
2281 secs => Duration::from_secs(secs),
2282 })
2283 }
2284
2285 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 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 pub fn http_outage_grace_period(&self) -> Duration {
2312 Duration::from_secs(self.inner.values.http.outage_grace_period)
2313 }
2314
2315 pub fn http_retry_delay(&self) -> Duration {
2320 Duration::from_secs(self.inner.values.http.retry_delay)
2321 }
2322
2323 pub fn http_project_failure_interval(&self) -> Duration {
2325 Duration::from_secs(self.inner.values.http.project_failure_interval)
2326 }
2327
2328 pub fn http_encoding(&self) -> HttpEncoding {
2330 self.inner.values.http.encoding
2331 }
2332
2333 pub fn http_global_metrics(&self) -> bool {
2335 self.inner.values.http.global_metrics
2336 }
2337
2338 pub fn http_forward(&self) -> bool {
2343 self.inner.values.http.forward && !self.processing_enabled()
2344 }
2345
2346 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 pub fn outcome_source(&self) -> Option<&str> {
2359 self.inner.values.outcomes.source.as_deref()
2360 }
2361
2362 pub fn logging(&self) -> &relay_log::LogConfig {
2364 &self.inner.values.logging
2365 }
2366
2367 pub fn sentry(&self) -> &relay_log::SentryConfig {
2369 &self.inner.values.sentry
2370 }
2371
2372 pub fn statsd_addr(&self) -> Option<&str> {
2374 self.inner.values.metrics.statsd.as_deref()
2375 }
2376
2377 pub fn statsd_buffer_size(&self) -> Option<usize> {
2379 self.inner.values.metrics.statsd_buffer_size
2380 }
2381
2382 pub fn metrics_prefix(&self) -> &str {
2384 &self.inner.values.metrics.prefix
2385 }
2386
2387 pub fn metrics_default_tags(&self) -> &BTreeMap<String, String> {
2389 &self.inner.values.metrics.default_tags
2390 }
2391
2392 pub fn metrics_hostname_tag(&self) -> Option<&str> {
2394 self.inner.values.metrics.hostname_tag.as_deref()
2395 }
2396
2397 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 pub fn http_timeout(&self) -> Duration {
2409 Duration::from_secs(self.inner.values.http.timeout.into())
2410 }
2411
2412 pub fn http_connection_timeout(&self) -> Duration {
2414 Duration::from_secs(self.inner.values.http.connection_timeout.into())
2415 }
2416
2417 pub fn http_max_retry_interval(&self) -> Duration {
2419 Duration::from_secs(self.inner.values.http.max_retry_interval.into())
2420 }
2421
2422 pub fn http_dns_cache(&self) -> bool {
2424 self.inner.values.http.dns_cache
2425 }
2426
2427 pub fn project_cache_expiry(&self) -> Duration {
2429 Duration::from_secs(self.inner.values.cache.project_expiry.into())
2430 }
2431
2432 pub fn request_full_project_config(&self) -> bool {
2434 self.inner.values.cache.project_request_full_config
2435 }
2436
2437 pub fn relay_cache_expiry(&self) -> Duration {
2439 Duration::from_secs(self.inner.values.cache.relay_expiry.into())
2440 }
2441
2442 pub fn cache_miss_expiry(&self) -> Duration {
2444 Duration::from_secs(self.inner.values.cache.miss_expiry.into())
2445 }
2446
2447 pub fn project_grace_period(&self) -> Duration {
2449 Duration::from_secs(self.inner.values.cache.project_grace_period.into())
2450 }
2451
2452 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 pub fn query_batch_interval(&self) -> Duration {
2467 Duration::from_millis(self.inner.values.cache.batch_interval.into())
2468 }
2469
2470 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 pub fn local_cache_interval(&self) -> Duration {
2483 Duration::from_secs(self.inner.values.cache.file_interval.into())
2484 }
2485
2486 pub fn global_config_fetch_interval(&self) -> Duration {
2489 Duration::from_secs(self.inner.values.cache.global_config_fetch_interval.into())
2490 }
2491
2492 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 pub fn spool_envelopes_max_disk_size(&self) -> usize {
2519 self.inner.values.spool.envelopes.max_disk_size.as_bytes()
2520 }
2521
2522 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 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 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 pub fn spool_envelopes_max_age(&self) -> Duration {
2554 Duration::from_secs(self.inner.values.spool.envelopes.max_envelope_delay_secs)
2555 }
2556
2557 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 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 pub fn spool_partitions(&self) -> NonZeroU8 {
2579 self.inner.values.spool.envelopes.partitions
2580 }
2581
2582 pub fn spool_partitioning(&self) -> EnvelopeSpoolPartitioning {
2584 self.inner.values.spool.envelopes.partitioning
2585 }
2586
2587 pub fn spool_ephemeral(&self) -> bool {
2589 self.inner.values.spool.envelopes.ephemeral
2590 }
2591
2592 pub fn max_event_size(&self) -> usize {
2594 self.inner.values.limits.max_event_size.as_bytes()
2595 }
2596
2597 pub fn max_attachment_size(&self) -> usize {
2599 self.inner.values.limits.max_attachment_size.as_bytes()
2600 }
2601
2602 pub fn max_attachment_count(&self) -> usize {
2604 self.inner.values.limits.max_attachment_count
2605 }
2606
2607 pub fn max_attachments_size(&self) -> usize {
2610 self.inner.values.limits.max_attachments_size.as_bytes()
2611 }
2612
2613 pub fn max_upload_size(&self) -> usize {
2615 self.inner.values.limits.max_upload_size.as_bytes()
2616 }
2617
2618 pub fn max_client_reports_count(&self) -> usize {
2620 self.inner.values.limits.max_client_reports_count
2621 }
2622
2623 pub fn max_client_reports_size(&self) -> usize {
2625 self.inner.values.limits.max_client_reports_size.as_bytes()
2626 }
2627
2628 pub fn max_check_in_size(&self) -> usize {
2630 self.inner.values.limits.max_check_in_size.as_bytes()
2631 }
2632
2633 pub fn max_log_size(&self) -> usize {
2635 self.inner.values.limits.max_log_size.as_bytes()
2636 }
2637
2638 pub fn max_expanded_log_operations(&self) -> usize {
2640 self.inner.values.limits.max_expanded_log_operations
2641 }
2642
2643 pub fn max_expanded_span_operations(&self) -> usize {
2645 self.inner.values.limits.max_expanded_span_operations
2646 }
2647
2648 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 pub fn max_span_size(&self) -> usize {
2658 self.inner.values.limits.max_span_size.as_bytes()
2659 }
2660
2661 pub fn max_standalone_span_count(&self) -> usize {
2663 self.inner.values.limits.max_standalone_span_count
2664 }
2665
2666 pub fn max_container_size(&self) -> usize {
2668 self.inner.values.limits.max_container_size.as_bytes()
2669 }
2670
2671 pub fn max_envelope_size(&self) -> usize {
2675 self.inner.values.limits.max_envelope_size.as_bytes()
2676 }
2677
2678 pub fn max_session_count(&self) -> usize {
2680 self.inner.values.limits.max_session_count
2681 }
2682
2683 pub fn max_sessions_size(&self) -> usize {
2685 self.inner.values.limits.max_sessions_size.as_bytes()
2686 }
2687
2688 pub fn max_metric_buckets_size(&self) -> usize {
2690 self.inner.values.limits.max_metric_buckets_size.as_bytes()
2691 }
2692
2693 pub fn max_api_payload_size(&self) -> usize {
2695 self.inner.values.limits.max_api_payload_size.as_bytes()
2696 }
2697
2698 pub fn max_api_file_upload_size(&self) -> usize {
2700 self.inner.values.limits.max_api_file_upload_size.as_bytes()
2701 }
2702
2703 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 pub fn max_profile_size(&self) -> usize {
2714 self.inner.values.limits.max_profile_size.as_bytes()
2715 }
2716
2717 pub fn max_trace_metric_size(&self) -> usize {
2719 self.inner.values.limits.max_trace_metric_size.as_bytes()
2720 }
2721
2722 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 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 pub fn max_replay_message_size(&self) -> usize {
2746 self.inner.values.limits.max_replay_message_size.as_bytes()
2747 }
2748
2749 pub fn max_concurrent_requests(&self) -> usize {
2751 self.inner.values.limits.max_concurrent_requests
2752 }
2753
2754 pub fn max_concurrent_queries(&self) -> usize {
2756 self.inner.values.limits.max_concurrent_queries
2757 }
2758
2759 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 pub fn query_timeout(&self) -> Duration {
2770 Duration::from_secs(self.inner.values.limits.query_timeout)
2771 }
2772
2773 pub fn shutdown_timeout(&self) -> Duration {
2776 Duration::from_secs(self.inner.values.limits.shutdown_timeout)
2777 }
2778
2779 pub fn keepalive_timeout(&self) -> Duration {
2783 Duration::from_secs(self.inner.values.limits.keepalive_timeout)
2784 }
2785
2786 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 pub fn max_connections(&self) -> Option<usize> {
2797 self.inner.values.limits.max_connections
2798 }
2799
2800 pub fn tcp_listen_backlog(&self) -> u32 {
2802 self.inner.values.limits.tcp_listen_backlog
2803 }
2804
2805 pub fn cpu_concurrency(&self) -> usize {
2807 self.inner.values.limits.max_thread_count
2808 }
2809
2810 pub fn pool_concurrency(&self) -> usize {
2812 self.inner.values.limits.max_pool_concurrency
2813 }
2814
2815 pub fn query_batch_size(&self) -> usize {
2817 self.inner.values.cache.batch_size
2818 }
2819
2820 pub fn processing_enabled(&self) -> bool {
2822 self.inner.values.processing.enabled
2823 }
2824
2825 pub fn normalization_level(&self) -> NormalizationLevel {
2827 self.inner.values.normalization.level
2828 }
2829
2830 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 pub fn max_secs_in_future(&self) -> i64 {
2844 self.inner.values.processing.max_secs_in_future.into()
2845 }
2846
2847 pub fn max_session_secs_in_past(&self) -> i64 {
2849 self.inner.values.processing.max_session_secs_in_past.into()
2850 }
2851
2852 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 pub fn kafka_validate_topics(&self) -> bool {
2870 self.inner.values.processing.kafka_validate_topics
2871 }
2872
2873 pub fn reloading_producer_jitter(&self) -> Duration {
2875 Duration::from_secs(self.inner.values.processing.reloading_producer_jitter_secs)
2876 }
2877
2878 pub fn kafka_config_blob(&self) -> Result<Vec<u8>, KafkaConfigError> {
2887 let mut clusters = BTreeMap::new();
2890 let mut topics = BTreeMap::new();
2891
2892 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 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 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 pub fn unused_topic_assignments(&self) -> &relay_kafka::Unused {
2942 &self.inner.values.processing.topics.unused
2943 }
2944
2945 pub fn objectstore(&self) -> &ObjectstoreServiceConfig {
2947 &self.inner.values.processing.objectstore
2948 }
2949
2950 pub fn upload(&self) -> &Upload {
2952 &self.inner.values.upload
2953 }
2954
2955 #[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 #[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 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 pub fn attachment_chunk_size(&self) -> usize {
2988 self.inner
2989 .values
2990 .processing
2991 .attachment_chunk_size
2992 .as_bytes()
2993 }
2994
2995 pub fn metrics_max_batch_size_bytes(&self) -> usize {
2997 self.inner.values.aggregator.max_flush_bytes
2998 }
2999
3000 pub fn projectconfig_cache_prefix(&self) -> &str {
3003 &self.inner.values.processing.projectconfig_cache_prefix
3004 }
3005
3006 pub fn max_rate_limit(&self) -> Option<u64> {
3008 self.inner.values.processing.max_rate_limit.map(u32::into)
3009 }
3010
3011 pub fn quota_cache_ratio(&self) -> Option<f32> {
3013 self.inner.values.processing.quota_cache_ratio
3014 }
3015
3016 pub fn quota_cache_max(&self) -> Option<f32> {
3018 self.inner.values.processing.quota_cache_max
3019 }
3020
3021 pub fn health_refresh_interval(&self) -> Duration {
3023 Duration::from_millis(self.inner.values.health.refresh_interval_ms)
3024 }
3025
3026 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 pub fn health_max_memory_watermark_percent(&self) -> f32 {
3038 self.inner.values.health.max_memory_percent
3039 }
3040
3041 pub fn health_probe_timeout(&self) -> Duration {
3043 Duration::from_millis(self.inner.values.health.probe_timeout_ms)
3044 }
3045
3046 pub fn memory_stat_refresh_frequency_ms(&self) -> u64 {
3048 self.inner.values.health.memory_stat_refresh_frequency_ms
3049 }
3050
3051 pub fn cogs_max_queue_size(&self) -> u64 {
3053 self.inner.values.cogs.max_queue_size
3054 }
3055
3056 pub fn cogs_relay_resource_id(&self) -> &str {
3058 &self.inner.values.cogs.relay_resource_id
3059 }
3060
3061 pub fn default_aggregator_config(&self) -> &AggregatorServiceConfig {
3063 &self.inner.values.aggregator
3064 }
3065
3066 pub fn secondary_aggregator_configs(&self) -> &Vec<ScopedAggregatorConfig> {
3068 &self.inner.values.secondary_aggregators
3069 }
3070
3071 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 pub fn static_relays(&self) -> &HashMap<RelayId, RelayInfo> {
3083 &self.inner.values.auth.static_relays
3084 }
3085
3086 pub fn signature_max_age(&self) -> Duration {
3088 Duration::from_secs(self.inner.values.auth.signature_max_age)
3089 }
3090
3091 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 #[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 #[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 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 assert_eq!(resolved.physical_topic.as_str(), topic);
3223
3224 resolved
3225 .config
3226 .config_map()
3227 .iter()
3228 .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 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 #[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 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 #[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 #[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}