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,
625 pub max_client_reports_size: ByteSize,
627 pub max_client_reports_count: usize,
629 pub max_check_in_size: ByteSize,
631 pub max_envelope_size: ByteSize,
633 pub max_sessions_size: ByteSize,
635 pub max_session_count: usize,
637 pub max_api_payload_size: ByteSize,
639 pub max_api_file_upload_size: ByteSize,
641 pub max_api_chunk_upload_size: ByteSize,
643 pub max_profile_size: ByteSize,
645 pub max_trace_metric_size: ByteSize,
647 pub max_log_size: ByteSize,
649 pub max_expanded_log_operations: usize,
651 pub max_expanded_span_operations: usize,
653 pub max_expanded_trace_metric_operations: usize,
655 pub max_span_size: ByteSize,
657 pub max_standalone_span_count: usize,
659 pub max_container_size: ByteSize,
661 pub max_metric_buckets_size: ByteSize,
663 pub max_replay_compressed_size: ByteSize,
665 #[serde(alias = "max_replay_size")]
667 max_replay_uncompressed_size: ByteSize,
668 pub max_replay_message_size: ByteSize,
670 pub max_removed_attribute_key_size: ByteSize,
680 pub max_thread_count: usize,
685 pub max_pool_concurrency: usize,
692 pub query_timeout: u64,
695 pub shutdown_timeout: u64,
698 pub keepalive_timeout: u64,
702 pub idle_timeout: Option<u64>,
709 pub max_connections: Option<usize>,
715 pub tcp_listen_backlog: u32,
723}
724
725impl Default for Limits {
726 fn default() -> Self {
727 Limits {
728 max_concurrent_requests: 100,
729 max_concurrent_queries: 5,
730 max_event_size: ByteSize::mebibytes(1),
731 max_attachment_size: ByteSize::mebibytes(200),
732 max_attachment_count: 30,
733 max_attachments_size: ByteSize::mebibytes(200),
734 max_upload_size: ByteSize::mebibytes(10240),
735 max_client_reports_size: ByteSize::kibibytes(100),
736 max_client_reports_count: 100,
737 max_check_in_size: ByteSize::kibibytes(100),
738 max_envelope_size: ByteSize::mebibytes(200),
739 max_sessions_size: ByteSize::mebibytes(10),
740 max_session_count: 100,
741 max_api_payload_size: ByteSize::mebibytes(20),
742 max_api_file_upload_size: ByteSize::mebibytes(40),
743 max_api_chunk_upload_size: ByteSize::mebibytes(100),
744 max_profile_size: ByteSize::mebibytes(50),
745 max_trace_metric_size: ByteSize::mebibytes(1),
746 max_log_size: ByteSize::mebibytes(2),
747 max_expanded_log_operations: 3_000_000,
748 max_expanded_span_operations: 3_000_000,
749 max_expanded_trace_metric_operations: 3_000_000,
750 max_span_size: ByteSize::mebibytes(10),
751 max_standalone_span_count: 25,
752 max_container_size: ByteSize::mebibytes(12),
753 max_metric_buckets_size: ByteSize::mebibytes(1),
754 max_replay_compressed_size: ByteSize::mebibytes(10),
755 max_replay_uncompressed_size: ByteSize::mebibytes(100),
756 max_replay_message_size: ByteSize::mebibytes(15),
757 max_thread_count: num_cpus::get(),
758 max_pool_concurrency: 1,
759 query_timeout: 30,
760 shutdown_timeout: 10,
761 keepalive_timeout: 5,
762 idle_timeout: None,
763 max_connections: None,
764 tcp_listen_backlog: 1024,
765 max_removed_attribute_key_size: ByteSize::kibibytes(10),
766 }
767 }
768}
769
770#[derive(Debug, Default, Deserialize, Serialize, Clone)]
772#[serde(default)]
773pub struct Routing {
774 pub accept_unknown_items: Option<bool>,
784}
785
786#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
788#[serde(rename_all = "lowercase")]
789pub enum HttpEncoding {
790 #[default]
795 Identity,
796 Deflate,
802 Gzip,
809 Br,
811 Zstd,
813}
814
815impl HttpEncoding {
816 pub fn parse(str: &str) -> Self {
818 let str = str.trim();
819 if str.eq_ignore_ascii_case("zstd") {
820 Self::Zstd
821 } else if str.eq_ignore_ascii_case("br") {
822 Self::Br
823 } else if str.eq_ignore_ascii_case("gzip") || str.eq_ignore_ascii_case("x-gzip") {
824 Self::Gzip
825 } else if str.eq_ignore_ascii_case("deflate") {
826 Self::Deflate
827 } else {
828 Self::Identity
829 }
830 }
831
832 pub fn name(&self) -> Option<&'static str> {
836 match self {
837 Self::Identity => None,
838 Self::Deflate => Some("deflate"),
839 Self::Gzip => Some("gzip"),
840 Self::Br => Some("br"),
841 Self::Zstd => Some("zstd"),
842 }
843 }
844}
845
846#[derive(Serialize, Deserialize, Debug, Clone)]
848#[serde(default)]
849pub struct Http {
850 pub timeout: u32,
856 pub connection_timeout: u32,
861 pub max_retry_interval: u32,
863 pub host_header: Option<String>,
865 pub auth_interval: Option<u64>,
873 pub outage_grace_period: u64,
879 pub retry_delay: u64,
883 pub project_failure_interval: u64,
888 pub encoding: HttpEncoding,
904 pub global_metrics: bool,
911 pub forward: bool,
918 pub dns_cache: bool,
922}
923
924impl Default for Http {
925 fn default() -> Self {
926 Http {
927 timeout: 5,
928 connection_timeout: 3,
929 max_retry_interval: 60, host_header: None,
931 auth_interval: Some(600), outage_grace_period: DEFAULT_NETWORK_OUTAGE_GRACE_PERIOD,
933 retry_delay: 1,
934 project_failure_interval: 90,
935 encoding: HttpEncoding::Zstd,
936 global_metrics: false,
937 forward: true,
938 dns_cache: true,
939 }
940 }
941}
942
943#[derive(Clone, Copy, Debug, Eq, PartialEq, Default, Deserialize, Serialize)]
945#[serde(rename_all = "snake_case")]
946pub enum EnvelopeSpoolPartitioning {
947 ProjectKeyPair,
951 #[default]
958 RoundRobin,
959}
960
961#[derive(Debug, Serialize, Deserialize, Clone)]
963#[serde(default)]
964pub struct EnvelopeSpool {
965 pub path: Option<PathBuf>,
971 pub max_disk_size: ByteSize,
977 pub batch_size_bytes: ByteSize,
984 pub flush_timeout_secs: Option<u64>,
990 pub max_envelope_delay_secs: u64,
997 pub disk_usage_refresh_frequency_ms: u64,
1002 pub max_backpressure_memory_percent: f32,
1032 pub partitions: NonZeroU8,
1039 pub partitioning: EnvelopeSpoolPartitioning,
1045 pub max_unspool_envelopes_per_second: Option<NonZeroU32>,
1051 pub ephemeral: bool,
1058}
1059
1060impl Default for EnvelopeSpool {
1061 fn default() -> Self {
1062 Self {
1063 path: None,
1064 max_disk_size: ByteSize::mebibytes(500),
1065 batch_size_bytes: ByteSize::kibibytes(10),
1066 max_envelope_delay_secs: 24 * 60 * 60,
1067 disk_usage_refresh_frequency_ms: 100,
1068 max_backpressure_memory_percent: 0.8,
1069 partitions: NonZeroU8::new(1).unwrap(),
1070 partitioning: EnvelopeSpoolPartitioning::default(),
1071 ephemeral: false,
1072 flush_timeout_secs: None,
1073 max_unspool_envelopes_per_second: None,
1074 }
1075 }
1076}
1077
1078#[derive(Debug, Serialize, Deserialize, Default, Clone)]
1080#[serde(default)]
1081pub struct Spool {
1082 pub envelopes: EnvelopeSpool,
1084}
1085
1086#[derive(Serialize, Deserialize, Debug, Clone)]
1088#[serde(default)]
1089pub struct Cache {
1090 pub project_request_full_config: bool,
1095 pub project_expiry: u32,
1097 pub project_grace_period: u32,
1102 pub project_refresh_interval: Option<u32>,
1108 pub relay_expiry: u32,
1110 pub miss_expiry: u32,
1112 pub batch_interval: u32,
1114 pub downstream_relays_batch_interval: u32,
1116 pub batch_size: usize,
1120 pub file_interval: u32,
1122 pub global_config_fetch_interval: u32,
1124}
1125
1126impl Default for Cache {
1127 fn default() -> Self {
1128 Cache {
1129 project_request_full_config: false,
1130 project_expiry: 300, project_grace_period: 120, project_refresh_interval: None,
1133 relay_expiry: 3600, miss_expiry: 60, batch_interval: 100, downstream_relays_batch_interval: 100, batch_size: 500,
1138 file_interval: 10, global_config_fetch_interval: 10, }
1141 }
1142}
1143
1144#[derive(Serialize, Deserialize, Debug, Clone)]
1146#[serde(default)]
1147pub struct Processing {
1148 pub enabled: bool,
1150 pub geoip_path: Option<PathBuf>,
1152 pub max_secs_in_future: u32,
1154 pub max_session_secs_in_past: u32,
1156 pub kafka_config: Vec<KafkaConfigParam>,
1158 pub secondary_kafka_configs: BTreeMap<String, Vec<KafkaConfigParam>>,
1178 pub topics: TopicAssignments,
1180 pub kafka_validate_topics: bool,
1182 pub reloading_producer_jitter_secs: u64,
1189 pub redis: Option<RedisConfigs>,
1191 pub attachment_chunk_size: ByteSize,
1193 pub projectconfig_cache_prefix: String,
1195 pub max_rate_limit: Option<u32>,
1197 pub quota_cache_ratio: Option<f32>,
1208 pub quota_cache_max: Option<f32>,
1215 #[serde(alias = "upload")]
1217 pub objectstore: ObjectstoreServiceConfig,
1218}
1219
1220impl Default for Processing {
1221 fn default() -> Self {
1223 Self {
1224 enabled: false,
1225 geoip_path: None,
1226 max_secs_in_future: 60, max_session_secs_in_past: 5 * 24 * 3600, kafka_config: Vec::new(),
1229 secondary_kafka_configs: BTreeMap::new(),
1230 topics: TopicAssignments::default(),
1231 kafka_validate_topics: false,
1232 reloading_producer_jitter_secs: 60,
1233 redis: None,
1234 attachment_chunk_size: ByteSize::mebibytes(1),
1235 projectconfig_cache_prefix: "relayconfig".to_owned(),
1236 max_rate_limit: Some(300), quota_cache_ratio: None,
1238 quota_cache_max: None,
1239 objectstore: ObjectstoreServiceConfig::default(),
1240 }
1241 }
1242}
1243
1244fn default_cluster_name(processing: &Processing) -> String {
1250 let mut name = String::from("default");
1251 while processing.secondary_kafka_configs.contains_key(&name) {
1252 name.push('_');
1253 }
1254 name
1255}
1256
1257#[derive(Debug, Default, Serialize, Deserialize, Clone)]
1259#[serde(default)]
1260pub struct Normalization {
1261 pub level: NormalizationLevel,
1263}
1264
1265#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
1267#[serde(rename_all = "lowercase")]
1268pub enum NormalizationLevel {
1269 #[default]
1273 Default,
1274 Full,
1279}
1280
1281#[derive(Serialize, Deserialize, Clone)]
1283pub struct ObjectstoreAuthConfig {
1284 pub key_id: String,
1287
1288 pub signing_key: String,
1290}
1291
1292impl fmt::Debug for ObjectstoreAuthConfig {
1293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1294 f.debug_struct("ObjectstoreAuthConfig")
1295 .field("key_id", &self.key_id)
1296 .field("signing_key", &"[redacted]")
1297 .finish()
1298 }
1299}
1300
1301#[derive(Serialize, Deserialize, Debug, Clone)]
1303#[serde(default)]
1304pub struct ObjectstoreServiceConfig {
1305 pub objectstore_url: Option<String>,
1310
1311 pub max_concurrent_requests: usize,
1313
1314 pub max_backlog: usize,
1318
1319 pub timeout: u64,
1324
1325 pub stream_timeout: u64,
1330
1331 pub retry_delay: f64,
1333
1334 pub max_attempts: NonZeroU16,
1336
1337 pub auth: Option<ObjectstoreAuthConfig>,
1339}
1340
1341impl Default for ObjectstoreServiceConfig {
1342 fn default() -> Self {
1343 Self {
1344 objectstore_url: None,
1345 max_concurrent_requests: 10,
1346 max_backlog: 20,
1347 timeout: 60,
1348 stream_timeout: 5 * 60, retry_delay: 1.0,
1350 max_attempts: NonZeroU16::new(5).unwrap(),
1351 auth: None,
1352 }
1353 }
1354}
1355
1356#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1359
1360pub enum EmitOutcomes {
1361 None,
1363 AsClientReports,
1365 AsOutcomes,
1367}
1368
1369impl EmitOutcomes {
1370 pub fn any(&self) -> bool {
1372 !matches!(self, EmitOutcomes::None)
1373 }
1374}
1375
1376impl Serialize for EmitOutcomes {
1377 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1378 where
1379 S: Serializer,
1380 {
1381 match self {
1383 Self::None => serializer.serialize_bool(false),
1384 Self::AsClientReports => serializer.serialize_str("as_client_reports"),
1385 Self::AsOutcomes => serializer.serialize_bool(true),
1386 }
1387 }
1388}
1389
1390struct EmitOutcomesVisitor;
1391
1392impl Visitor<'_> for EmitOutcomesVisitor {
1393 type Value = EmitOutcomes;
1394
1395 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1396 formatter.write_str("true, false, 'as_client_reports'")
1397 }
1398
1399 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
1400 where
1401 E: serde::de::Error,
1402 {
1403 Ok(if v {
1404 EmitOutcomes::AsOutcomes
1405 } else {
1406 EmitOutcomes::None
1407 })
1408 }
1409
1410 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1411 where
1412 E: serde::de::Error,
1413 {
1414 match v {
1415 "as_client_reports" => Ok(EmitOutcomes::AsClientReports),
1416 _ => Err(E::invalid_value(Unexpected::Str(v), &self)),
1417 }
1418 }
1419}
1420
1421impl<'de> Deserialize<'de> for EmitOutcomes {
1422 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1423 where
1424 D: Deserializer<'de>,
1425 {
1426 deserializer.deserialize_any(EmitOutcomesVisitor)
1427 }
1428}
1429
1430#[derive(Serialize, Deserialize, Debug, Clone)]
1432#[serde(default)]
1433pub struct Outcomes {
1434 pub emit_outcomes: EmitOutcomes,
1438 pub source: Option<String>,
1441}
1442
1443impl Default for Outcomes {
1444 fn default() -> Self {
1445 Outcomes {
1446 emit_outcomes: EmitOutcomes::AsClientReports,
1447 source: None,
1448 }
1449 }
1450}
1451
1452#[derive(Serialize, Deserialize, Debug, Default)]
1454pub struct MinimalConfig {
1455 pub relay: Relay,
1457}
1458
1459impl MinimalConfig {
1460 pub fn save_in_folder<P: AsRef<Path>>(&self, p: P) -> anyhow::Result<()> {
1462 let path = p.as_ref();
1463 if fs::metadata(path).is_err() {
1464 fs::create_dir_all(path)
1465 .with_context(|| ConfigError::file(ConfigErrorKind::CouldNotOpenFile, path))?;
1466 }
1467 self.save(path)
1468 }
1469}
1470
1471impl ConfigObject for MinimalConfig {
1472 fn format() -> ConfigFormat {
1473 ConfigFormat::Yaml
1474 }
1475
1476 fn name() -> &'static str {
1477 "config"
1478 }
1479}
1480
1481mod config_relay_info {
1483 use serde::ser::SerializeMap;
1484
1485 use super::*;
1486
1487 #[derive(Debug, Serialize, Deserialize, Clone)]
1489 struct RelayInfoConfig {
1490 public_key: PublicKey,
1491 #[serde(default)]
1492 internal: bool,
1493 }
1494
1495 impl From<RelayInfoConfig> for RelayInfo {
1496 fn from(v: RelayInfoConfig) -> Self {
1497 RelayInfo {
1498 public_key: v.public_key,
1499 internal: v.internal,
1500 }
1501 }
1502 }
1503
1504 impl From<RelayInfo> for RelayInfoConfig {
1505 fn from(v: RelayInfo) -> Self {
1506 RelayInfoConfig {
1507 public_key: v.public_key,
1508 internal: v.internal,
1509 }
1510 }
1511 }
1512
1513 pub(super) fn deserialize<'de, D>(des: D) -> Result<HashMap<RelayId, RelayInfo>, D::Error>
1514 where
1515 D: Deserializer<'de>,
1516 {
1517 let map = HashMap::<RelayId, RelayInfoConfig>::deserialize(des)?;
1518 Ok(map.into_iter().map(|(k, v)| (k, v.into())).collect())
1519 }
1520
1521 pub(super) fn serialize<S>(elm: &HashMap<RelayId, RelayInfo>, ser: S) -> Result<S::Ok, S::Error>
1522 where
1523 S: Serializer,
1524 {
1525 let mut map = ser.serialize_map(Some(elm.len()))?;
1526
1527 for (k, v) in elm {
1528 map.serialize_entry(k, &RelayInfoConfig::from(v.clone()))?;
1529 }
1530
1531 map.end()
1532 }
1533}
1534
1535#[derive(Serialize, Deserialize, Debug, Clone)]
1537#[serde(default)]
1538pub struct AuthConfig {
1539 #[serde(skip_serializing_if = "is_default")]
1541 pub ready: ReadinessCondition,
1542
1543 #[serde(with = "config_relay_info")]
1545 pub static_relays: HashMap<RelayId, RelayInfo>,
1546
1547 pub signature_max_age: u64,
1551}
1552
1553impl Default for AuthConfig {
1554 fn default() -> Self {
1555 Self {
1556 ready: ReadinessCondition::default(),
1557 static_relays: HashMap::new(),
1558 signature_max_age: 300, }
1560 }
1561}
1562
1563#[derive(Serialize, Deserialize, Debug, Default, Clone)]
1565pub struct GeoIpConfig {
1566 pub path: Option<PathBuf>,
1568}
1569
1570#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1575#[serde(default)]
1576pub struct Health {
1577 pub refresh_interval_ms: u64,
1584 pub max_memory_bytes: Option<ByteSize>,
1589 pub max_memory_percent: f32,
1593 pub probe_timeout_ms: u64,
1600 pub memory_stat_refresh_frequency_ms: u64,
1606}
1607
1608impl Default for Health {
1609 fn default() -> Self {
1610 Self {
1611 refresh_interval_ms: 3000,
1612 max_memory_bytes: None,
1613 max_memory_percent: 0.95,
1614 probe_timeout_ms: 900,
1615 memory_stat_refresh_frequency_ms: 100,
1616 }
1617 }
1618}
1619
1620#[derive(Serialize, Deserialize, Debug, Clone)]
1622#[serde(default)]
1623pub struct Cogs {
1624 pub max_queue_size: u64,
1630 pub relay_resource_id: String,
1636}
1637
1638impl Default for Cogs {
1639 fn default() -> Self {
1640 Self {
1641 max_queue_size: 10_000,
1642 relay_resource_id: "relay_service".to_owned(),
1643 }
1644 }
1645}
1646
1647#[derive(Debug, Clone, Serialize, Deserialize)]
1649#[serde(default)]
1650pub struct Upload {
1651 pub max_concurrent_requests: usize,
1655 pub timeout: u64,
1657 pub max_age: i64,
1661
1662 pub credentials: Option<UploadCredentials>,
1666}
1667
1668impl Default for Upload {
1669 fn default() -> Self {
1670 Self {
1671 max_concurrent_requests: 100,
1672 timeout: 5 * 60, max_age: 60 * 60, credentials: None,
1675 }
1676 }
1677}
1678
1679#[derive(Clone, Serialize, Deserialize)]
1681pub struct UploadCredentials {
1682 #[cfg(feature = "processing")]
1684 pub signing_key: SecretKey,
1685
1686 pub verification_key: PublicKey,
1688}
1689
1690impl fmt::Debug for UploadCredentials {
1691 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1692 let Self {
1693 #[cfg(feature = "processing")]
1694 signing_key: _,
1695 verification_key,
1696 } = self;
1697 let mut b = f.debug_struct("UploadCredentials");
1698 #[cfg(feature = "processing")]
1699 b.field("signing_key", &"[redacted]");
1700 b.field("verification_key", verification_key).finish()
1701 }
1702}
1703
1704#[derive(Serialize, Deserialize, Debug, Default, Clone)]
1706#[serde(default)]
1707#[allow(missing_docs)]
1708pub struct ConfigValues {
1709 pub relay: Relay,
1710 pub http: Http,
1711 pub cache: Cache,
1712 pub spool: Spool,
1713 pub limits: Limits,
1714 pub logging: relay_log::LogConfig,
1715 pub routing: Routing,
1716 pub metrics: Metrics,
1717 pub sentry: relay_log::SentryConfig,
1718 pub processing: Processing,
1719 pub outcomes: Outcomes,
1720 pub aggregator: AggregatorServiceConfig,
1721 pub secondary_aggregators: Vec<ScopedAggregatorConfig>,
1722 pub auth: AuthConfig,
1723 pub geoip: GeoIpConfig,
1724 pub normalization: Normalization,
1725 pub health: Health,
1726 pub cogs: Cogs,
1727 pub upload: Upload,
1728}
1729
1730impl ConfigObject for ConfigValues {
1731 fn format() -> ConfigFormat {
1732 ConfigFormat::Yaml
1733 }
1734
1735 fn name() -> &'static str {
1736 "config"
1737 }
1738}
1739
1740#[derive(Default, Clone)]
1741struct ConfigInner {
1742 values: ConfigValues,
1744 credentials: Option<Credentials>,
1748}
1749
1750impl ConfigInner {
1751 fn apply_overrides(&mut self, overrides: &OverridableConfig) -> anyhow::Result<()> {
1752 if let Some(log_level) = &overrides.log_level {
1753 self.values.logging.level = log_level.parse()?;
1754 }
1755
1756 if let Some(log_format) = &overrides.log_format {
1757 self.values.logging.format = log_format.parse()?;
1758 }
1759
1760 let relay = &mut self.values.relay;
1761 if let Some(mode) = &overrides.mode {
1762 relay.mode = mode
1763 .parse::<RelayMode>()
1764 .with_context(|| ConfigError::field("mode"))?;
1765 }
1766 if let Some(deployment) = &overrides.instance {
1767 relay.instance = deployment
1768 .parse::<RelayInstance>()
1769 .with_context(|| ConfigError::field("deployment"))?;
1770 }
1771 if let Some(upstream) = &overrides.upstream {
1772 relay.upstream = upstream
1773 .parse::<UpstreamDescriptor>()
1774 .with_context(|| ConfigError::field("upstream"))?;
1775 } else if let Some(upstream_dsn) = &overrides.upstream_dsn {
1776 relay.upstream = upstream_dsn
1777 .parse::<Dsn>()
1778 .map(|dsn| UpstreamDescriptor::from_dsn(&dsn))
1779 .with_context(|| ConfigError::field("upstream_dsn"))?;
1780 }
1781 if let Some(host) = &overrides.host {
1782 relay.host = host
1783 .parse::<IpAddr>()
1784 .with_context(|| ConfigError::field("host"))?;
1785 }
1786 if let Some(port) = &overrides.port {
1787 relay.port = port
1788 .as_str()
1789 .parse()
1790 .with_context(|| ConfigError::field("port"))?;
1791 }
1792
1793 let processing = &mut self.values.processing;
1794 if let Some(enabled) = &overrides.processing {
1795 match enabled.to_lowercase().as_str() {
1796 "true" | "1" => processing.enabled = true,
1797 "false" | "0" | "" => processing.enabled = false,
1798 _ => return Err(ConfigError::field("processing").into()),
1799 }
1800 }
1801 if let Some(redis) = overrides.redis_url.clone() {
1802 processing.redis = Some(RedisConfigs::Unified(RedisConfig::single(redis)))
1803 }
1804 if let Some(kafka_url) = overrides.kafka_url.clone() {
1805 let existing = processing
1806 .kafka_config
1807 .iter_mut()
1808 .find(|e| e.name == "bootstrap.servers");
1809
1810 if let Some(config_param) = existing {
1811 config_param.value = kafka_url;
1812 } else {
1813 self.values.processing.kafka_config.push(KafkaConfigParam {
1814 name: "bootstrap.servers".to_owned(),
1815 value: kafka_url,
1816 })
1817 }
1818 }
1819
1820 if overrides.outcome_source.is_some() {
1821 self.values.outcomes.source = overrides.outcome_source.clone();
1822 }
1823
1824 if let Some(shutdown_timeout) = &overrides.shutdown_timeout
1825 && let Ok(shutdown_timeout) = shutdown_timeout.parse::<u64>()
1826 {
1827 self.values.limits.shutdown_timeout = shutdown_timeout;
1828 }
1829
1830 if let Some(server_name) = overrides.server_name.clone() {
1831 self.values.sentry.server_name = Some(server_name.into());
1832 }
1833
1834 let id = if let Some(id) = &overrides.id {
1835 let id = Uuid::parse_str(id).with_context(|| ConfigError::field("id"))?;
1836 Some(id)
1837 } else {
1838 None
1839 };
1840 let public_key = if let Some(public_key) = &overrides.public_key {
1841 let public_key = public_key
1842 .parse::<PublicKey>()
1843 .with_context(|| ConfigError::field("public_key"))?;
1844 Some(public_key)
1845 } else {
1846 None
1847 };
1848
1849 let secret_key = if let Some(secret_key) = &overrides.secret_key {
1850 let secret_key = secret_key
1851 .parse::<SecretKey>()
1852 .with_context(|| ConfigError::field("secret_key"))?;
1853 Some(secret_key)
1854 } else {
1855 None
1856 };
1857
1858 if let Some(credentials) = &mut self.credentials {
1859 if let Some(id) = id {
1861 credentials.id = id;
1862 }
1863 if let Some(public_key) = public_key {
1864 credentials.public_key = public_key;
1865 }
1866 if let Some(secret_key) = secret_key {
1867 credentials.secret_key = secret_key
1868 }
1869 } else {
1870 match (id, public_key, secret_key) {
1872 (Some(id), Some(public_key), Some(secret_key)) => {
1873 self.credentials = Some(Credentials {
1874 secret_key,
1875 public_key,
1876 id,
1877 })
1878 }
1879 (None, None, None) => {
1880 }
1883 _ => {
1884 return Err(ConfigError::field("incomplete credentials").into());
1885 }
1886 }
1887 }
1888
1889 Ok(())
1890 }
1891
1892 fn reload_with(&mut self, other: &Self) -> bool {
1896 let mut changed = false;
1897
1898 if self.values.health != other.values.health {
1899 relay_log::debug!("updating health");
1900 self.values.health = other.values.health.clone();
1901 changed = true;
1902 }
1903
1904 if self.values.processing.kafka_config != other.values.processing.kafka_config
1914 || self.values.processing.secondary_kafka_configs
1915 != other.values.processing.secondary_kafka_configs
1916 {
1917 relay_log::debug!("updating kafka config");
1918 self.values.processing.kafka_config = other.values.processing.kafka_config.clone();
1919 self.values.processing.secondary_kafka_configs =
1920 other.values.processing.secondary_kafka_configs.clone();
1921 changed = true;
1922 }
1923
1924 if self.values.spool.envelopes.max_unspool_envelopes_per_second
1925 != other
1926 .values
1927 .spool
1928 .envelopes
1929 .max_unspool_envelopes_per_second
1930 {
1931 relay_log::debug!("updating spool.envelopes.max_unspool_envelopes_per_second");
1932 self.values.spool.envelopes.max_unspool_envelopes_per_second = other
1933 .values
1934 .spool
1935 .envelopes
1936 .max_unspool_envelopes_per_second;
1937 changed = true;
1938 }
1939
1940 changed
1941 }
1942}
1943
1944pub struct Config {
1946 inner_access: Mutex<()>,
1955 inner: ArcSwap<ConfigInner>,
1957 overrides: Vec<OverridableConfig>,
1962 path: PathBuf,
1966}
1967
1968impl fmt::Debug for Config {
1969 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1970 let inner = self.inner.load();
1971
1972 f.debug_struct("Config")
1973 .field("path", &self.path)
1974 .field("values", &inner.values)
1976 .finish()
1977 }
1978}
1979
1980impl Config {
1981 pub fn from_path<P: AsRef<Path>>(path: P) -> anyhow::Result<Config> {
1983 let path = env::current_dir()
1984 .map(|x| x.join(path.as_ref()))
1985 .unwrap_or_else(|_| path.as_ref().to_path_buf());
1986
1987 let inner = ConfigInner {
1988 values: ConfigValues::load(&path)?,
1989 credentials: match Credentials::path(&path).exists() {
1990 true => Some(Credentials::load(&path)?),
1991 false => None,
1992 },
1993 };
1994
1995 let config = Config {
1996 inner_access: Mutex::new(()),
1997 inner: ArcSwap::from_pointee(inner),
1998 overrides: Vec::new(),
1999 path: path.clone(),
2000 };
2001
2002 if cfg!(not(feature = "processing")) && config.current().processing_enabled() {
2003 return Err(ConfigError::file(ConfigErrorKind::ProcessingNotAvailable, &path).into());
2004 }
2005
2006 Ok(config)
2007 }
2008
2009 pub fn from_json_value(value: serde_json::Value) -> anyhow::Result<Config> {
2013 Ok(Config {
2014 inner_access: Mutex::new(()),
2015 inner: ArcSwap::from_pointee(ConfigInner {
2016 values: serde_json::from_value(value)
2017 .with_context(|| ConfigError::new(ConfigErrorKind::BadJson))?,
2018 credentials: None,
2019 }),
2020 overrides: Vec::new(),
2021 path: PathBuf::new(),
2022 })
2023 }
2024
2025 pub fn apply_override(&mut self, overrides: OverridableConfig) -> anyhow::Result<&mut Self> {
2030 crate::utils::try_rcu(&self.inner, |inner| {
2033 let mut new = ConfigInner::clone(inner);
2034 new.apply_overrides(&overrides)?;
2035 Ok::<_, anyhow::Error>(Arc::new(new))
2036 })?;
2037
2038 self.overrides.push(overrides);
2040
2041 Ok(self)
2042 }
2043
2044 pub fn config_exists<P: AsRef<Path>>(path: P) -> bool {
2046 fs::metadata(ConfigValues::path(path.as_ref())).is_ok()
2047 }
2048
2049 pub fn path(&self) -> &Path {
2051 &self.path
2052 }
2053
2054 pub fn to_yaml_string(&self) -> anyhow::Result<String> {
2056 serde_yaml::to_string(&self.inner.load().values)
2057 .with_context(|| ConfigError::new(ConfigErrorKind::CouldNotWriteFile))
2058 }
2059
2060 pub fn replace_credentials(
2064 &mut self,
2065 credentials: Option<Credentials>,
2066 ) -> anyhow::Result<bool> {
2067 if self.inner.load().credentials == credentials {
2068 return Ok(false);
2069 }
2070
2071 if !self.path.is_empty() {
2072 match &credentials {
2073 Some(creds) => {
2074 creds.save(&self.path)?;
2075 }
2076 None => {
2077 let path = Credentials::path(&self.path);
2078 if fs::metadata(&path).is_ok() {
2079 fs::remove_file(&path).with_context(|| {
2080 ConfigError::file(ConfigErrorKind::CouldNotWriteFile, &path)
2081 })?;
2082 }
2083 }
2084 }
2085 }
2086
2087 self.inner.rcu(|inner| {
2092 let mut inner = ConfigInner::clone(inner);
2093 inner.credentials = credentials.clone();
2094 Arc::new(inner)
2095 });
2096
2097 Ok(true)
2098 }
2099
2100 pub fn reload(&self) -> anyhow::Result<bool> {
2110 if self.path.is_empty() {
2111 return Ok(false);
2112 }
2113
2114 let _access = self
2115 .inner_access
2116 .lock()
2117 .unwrap_or_else(PoisonError::into_inner);
2118
2119 let mut new_config = Self::from_path(&self.path)?;
2120 for overrides in &self.overrides {
2121 new_config.apply_override(overrides.clone())?;
2122 }
2123 let new_config = new_config.current();
2124
2125 let mut changed = false;
2126
2127 self.inner.rcu(|inner| {
2129 let mut new_inner = ConfigInner::clone(inner);
2130 changed = new_inner.reload_with(&new_config.inner);
2131 match changed {
2132 true => Arc::new(new_inner),
2133 false => Arc::clone(inner),
2134 }
2135 });
2136
2137 Ok(changed)
2138 }
2139
2140 pub fn current(&self) -> ConfigSnapshot {
2145 let inner = self.inner.load();
2146 ConfigSnapshot { inner }
2147 }
2148}
2149
2150impl Default for Config {
2151 fn default() -> Self {
2152 Self {
2153 inner_access: Mutex::new(()),
2154 inner: ArcSwap::from_pointee(Default::default()),
2155 overrides: Vec::new(),
2156 path: PathBuf::new(),
2157 }
2158 }
2159}
2160
2161pub struct ConfigSnapshot {
2169 inner: arc_swap::Guard<Arc<ConfigInner>>,
2170}
2171
2172impl ConfigSnapshot {
2173 pub fn has_credentials(&self) -> bool {
2175 self.inner.credentials.is_some()
2176 }
2177
2178 pub fn credentials(&self) -> Option<&Credentials> {
2180 self.inner.credentials.as_ref()
2181 }
2182
2183 pub fn secret_key(&self) -> Option<&SecretKey> {
2185 self.inner.credentials.as_ref().map(|x| &x.secret_key)
2186 }
2187
2188 pub fn public_key(&self) -> Option<&PublicKey> {
2190 self.inner.credentials.as_ref().map(|x| &x.public_key)
2191 }
2192
2193 pub fn relay_id(&self) -> Option<&RelayId> {
2195 self.inner.credentials.as_ref().map(|x| &x.id)
2196 }
2197
2198 pub fn relay_mode(&self) -> RelayMode {
2200 self.inner.values.relay.mode
2201 }
2202
2203 pub fn relay_instance(&self) -> RelayInstance {
2205 self.inner.values.relay.instance
2206 }
2207
2208 pub fn upstream(&self) -> &UpstreamDescriptor {
2210 &self.inner.values.relay.upstream
2211 }
2212
2213 pub fn advertised_upstream(&self) -> Option<&UpstreamDescriptor> {
2215 self.inner.values.relay.advertised_upstream.as_ref()
2216 }
2217
2218 pub fn http_host_header(&self) -> Option<&str> {
2220 self.inner.values.http.host_header.as_deref()
2221 }
2222
2223 pub fn listen_addr(&self) -> SocketAddr {
2225 (self.inner.values.relay.host, self.inner.values.relay.port).into()
2226 }
2227
2228 pub fn listen_addr_internal(&self) -> Option<SocketAddr> {
2236 match (
2237 self.inner.values.relay.internal_host,
2238 self.inner.values.relay.internal_port,
2239 ) {
2240 (Some(host), None) => Some((host, self.inner.values.relay.port).into()),
2241 (None, Some(port)) => Some((self.inner.values.relay.host, port).into()),
2242 (Some(host), Some(port)) => Some((host, port).into()),
2243 (None, None) => None,
2244 }
2245 }
2246
2247 pub fn override_project_ids(&self) -> bool {
2251 self.inner.values.relay.override_project_ids
2252 }
2253
2254 pub fn config_reload_interval(&self) -> Option<Duration> {
2258 let interval = self.inner.values.relay.config_reload_interval?;
2259 Some(match interval {
2260 0 => Duration::from_millis(50),
2263 secs => Duration::from_secs(secs),
2264 })
2265 }
2266
2267 pub fn requires_auth(&self) -> bool {
2271 match self.inner.values.auth.ready {
2272 ReadinessCondition::Authenticated => self.relay_mode() == RelayMode::Managed,
2273 ReadinessCondition::Always => false,
2274 }
2275 }
2276
2277 pub fn http_auth_interval(&self) -> Option<Duration> {
2281 if self.processing_enabled() {
2282 return None;
2283 }
2284
2285 match self.inner.values.http.auth_interval {
2286 None | Some(0) => None,
2287 Some(secs) => Some(Duration::from_secs(secs)),
2288 }
2289 }
2290
2291 pub fn http_outage_grace_period(&self) -> Duration {
2294 Duration::from_secs(self.inner.values.http.outage_grace_period)
2295 }
2296
2297 pub fn http_retry_delay(&self) -> Duration {
2302 Duration::from_secs(self.inner.values.http.retry_delay)
2303 }
2304
2305 pub fn http_project_failure_interval(&self) -> Duration {
2307 Duration::from_secs(self.inner.values.http.project_failure_interval)
2308 }
2309
2310 pub fn http_encoding(&self) -> HttpEncoding {
2312 self.inner.values.http.encoding
2313 }
2314
2315 pub fn http_global_metrics(&self) -> bool {
2317 self.inner.values.http.global_metrics
2318 }
2319
2320 pub fn http_forward(&self) -> bool {
2325 self.inner.values.http.forward && !self.processing_enabled()
2326 }
2327
2328 pub fn emit_outcomes(&self) -> EmitOutcomes {
2333 if self.processing_enabled() {
2334 return EmitOutcomes::AsOutcomes;
2335 }
2336 self.inner.values.outcomes.emit_outcomes
2337 }
2338
2339 pub fn outcome_source(&self) -> Option<&str> {
2341 self.inner.values.outcomes.source.as_deref()
2342 }
2343
2344 pub fn logging(&self) -> &relay_log::LogConfig {
2346 &self.inner.values.logging
2347 }
2348
2349 pub fn sentry(&self) -> &relay_log::SentryConfig {
2351 &self.inner.values.sentry
2352 }
2353
2354 pub fn statsd_addr(&self) -> Option<&str> {
2356 self.inner.values.metrics.statsd.as_deref()
2357 }
2358
2359 pub fn statsd_buffer_size(&self) -> Option<usize> {
2361 self.inner.values.metrics.statsd_buffer_size
2362 }
2363
2364 pub fn metrics_prefix(&self) -> &str {
2366 &self.inner.values.metrics.prefix
2367 }
2368
2369 pub fn metrics_default_tags(&self) -> &BTreeMap<String, String> {
2371 &self.inner.values.metrics.default_tags
2372 }
2373
2374 pub fn metrics_hostname_tag(&self) -> Option<&str> {
2376 self.inner.values.metrics.hostname_tag.as_deref()
2377 }
2378
2379 pub fn metrics_periodic_interval(&self) -> Option<Duration> {
2383 match self.inner.values.metrics.periodic_secs {
2384 0 => None,
2385 secs => Some(Duration::from_secs(secs)),
2386 }
2387 }
2388
2389 pub fn http_timeout(&self) -> Duration {
2391 Duration::from_secs(self.inner.values.http.timeout.into())
2392 }
2393
2394 pub fn http_connection_timeout(&self) -> Duration {
2396 Duration::from_secs(self.inner.values.http.connection_timeout.into())
2397 }
2398
2399 pub fn http_max_retry_interval(&self) -> Duration {
2401 Duration::from_secs(self.inner.values.http.max_retry_interval.into())
2402 }
2403
2404 pub fn http_dns_cache(&self) -> bool {
2406 self.inner.values.http.dns_cache
2407 }
2408
2409 pub fn project_cache_expiry(&self) -> Duration {
2411 Duration::from_secs(self.inner.values.cache.project_expiry.into())
2412 }
2413
2414 pub fn request_full_project_config(&self) -> bool {
2416 self.inner.values.cache.project_request_full_config
2417 }
2418
2419 pub fn relay_cache_expiry(&self) -> Duration {
2421 Duration::from_secs(self.inner.values.cache.relay_expiry.into())
2422 }
2423
2424 pub fn cache_miss_expiry(&self) -> Duration {
2426 Duration::from_secs(self.inner.values.cache.miss_expiry.into())
2427 }
2428
2429 pub fn project_grace_period(&self) -> Duration {
2431 Duration::from_secs(self.inner.values.cache.project_grace_period.into())
2432 }
2433
2434 pub fn project_refresh_interval(&self) -> Option<Duration> {
2438 self.inner
2439 .values
2440 .cache
2441 .project_refresh_interval
2442 .map(Into::into)
2443 .map(Duration::from_secs)
2444 }
2445
2446 pub fn query_batch_interval(&self) -> Duration {
2449 Duration::from_millis(self.inner.values.cache.batch_interval.into())
2450 }
2451
2452 pub fn downstream_relays_batch_interval(&self) -> Duration {
2454 Duration::from_millis(
2455 self.inner
2456 .values
2457 .cache
2458 .downstream_relays_batch_interval
2459 .into(),
2460 )
2461 }
2462
2463 pub fn local_cache_interval(&self) -> Duration {
2465 Duration::from_secs(self.inner.values.cache.file_interval.into())
2466 }
2467
2468 pub fn global_config_fetch_interval(&self) -> Duration {
2471 Duration::from_secs(self.inner.values.cache.global_config_fetch_interval.into())
2472 }
2473
2474 pub fn spool_envelopes_path(&self, partition_id: u8) -> Option<PathBuf> {
2479 let mut path = self
2480 .inner
2481 .values
2482 .spool
2483 .envelopes
2484 .path
2485 .as_ref()
2486 .map(|path| path.to_owned())?;
2487
2488 if partition_id == 0 {
2489 return Some(path);
2490 }
2491
2492 let file_name = path.file_name().and_then(|f| f.to_str())?;
2493 let new_file_name = format!("{file_name}.{partition_id}");
2494 path.set_file_name(new_file_name);
2495
2496 Some(path)
2497 }
2498
2499 pub fn spool_envelopes_max_disk_size(&self) -> usize {
2501 self.inner.values.spool.envelopes.max_disk_size.as_bytes()
2502 }
2503
2504 pub fn spool_envelopes_batch_size_bytes(&self) -> usize {
2507 self.inner
2508 .values
2509 .spool
2510 .envelopes
2511 .batch_size_bytes
2512 .as_bytes()
2513 }
2514
2515 pub fn spool_envelopes_flush_timeout(&self) -> Option<Duration> {
2517 self.inner
2518 .values
2519 .spool
2520 .envelopes
2521 .flush_timeout_secs
2522 .map(Duration::from_secs)
2523 }
2524
2525 pub fn spool_max_unspool_envelopes_per_second(&self) -> Option<NonZeroU32> {
2527 self.inner
2528 .values
2529 .spool
2530 .envelopes
2531 .max_unspool_envelopes_per_second
2532 }
2533
2534 pub fn spool_envelopes_max_age(&self) -> Duration {
2536 Duration::from_secs(self.inner.values.spool.envelopes.max_envelope_delay_secs)
2537 }
2538
2539 pub fn spool_disk_usage_refresh_frequency_ms(&self) -> Duration {
2541 Duration::from_millis(
2542 self.inner
2543 .values
2544 .spool
2545 .envelopes
2546 .disk_usage_refresh_frequency_ms,
2547 )
2548 }
2549
2550 pub fn spool_max_backpressure_memory_percent(&self) -> f32 {
2552 self.inner
2553 .values
2554 .spool
2555 .envelopes
2556 .max_backpressure_memory_percent
2557 }
2558
2559 pub fn spool_partitions(&self) -> NonZeroU8 {
2561 self.inner.values.spool.envelopes.partitions
2562 }
2563
2564 pub fn spool_partitioning(&self) -> EnvelopeSpoolPartitioning {
2566 self.inner.values.spool.envelopes.partitioning
2567 }
2568
2569 pub fn spool_ephemeral(&self) -> bool {
2571 self.inner.values.spool.envelopes.ephemeral
2572 }
2573
2574 pub fn max_event_size(&self) -> usize {
2576 self.inner.values.limits.max_event_size.as_bytes()
2577 }
2578
2579 pub fn max_attachment_size(&self) -> usize {
2581 self.inner.values.limits.max_attachment_size.as_bytes()
2582 }
2583
2584 pub fn max_attachment_count(&self) -> usize {
2586 self.inner.values.limits.max_attachment_count
2587 }
2588
2589 pub fn max_attachments_size(&self) -> usize {
2592 self.inner.values.limits.max_attachments_size.as_bytes()
2593 }
2594
2595 pub fn max_upload_size(&self) -> usize {
2597 self.inner.values.limits.max_upload_size.as_bytes()
2598 }
2599
2600 pub fn max_client_reports_count(&self) -> usize {
2602 self.inner.values.limits.max_client_reports_count
2603 }
2604
2605 pub fn max_client_reports_size(&self) -> usize {
2607 self.inner.values.limits.max_client_reports_size.as_bytes()
2608 }
2609
2610 pub fn max_check_in_size(&self) -> usize {
2612 self.inner.values.limits.max_check_in_size.as_bytes()
2613 }
2614
2615 pub fn max_log_size(&self) -> usize {
2617 self.inner.values.limits.max_log_size.as_bytes()
2618 }
2619
2620 pub fn max_expanded_log_operations(&self) -> usize {
2622 self.inner.values.limits.max_expanded_log_operations
2623 }
2624
2625 pub fn max_expanded_span_operations(&self) -> usize {
2627 self.inner.values.limits.max_expanded_span_operations
2628 }
2629
2630 pub fn max_expanded_trace_metric_operations(&self) -> usize {
2632 self.inner
2633 .values
2634 .limits
2635 .max_expanded_trace_metric_operations
2636 }
2637
2638 pub fn max_span_size(&self) -> usize {
2640 self.inner.values.limits.max_span_size.as_bytes()
2641 }
2642
2643 pub fn max_standalone_span_count(&self) -> usize {
2645 self.inner.values.limits.max_standalone_span_count
2646 }
2647
2648 pub fn max_container_size(&self) -> usize {
2650 self.inner.values.limits.max_container_size.as_bytes()
2651 }
2652
2653 pub fn max_envelope_size(&self) -> usize {
2657 self.inner.values.limits.max_envelope_size.as_bytes()
2658 }
2659
2660 pub fn max_session_count(&self) -> usize {
2662 self.inner.values.limits.max_session_count
2663 }
2664
2665 pub fn max_sessions_size(&self) -> usize {
2667 self.inner.values.limits.max_sessions_size.as_bytes()
2668 }
2669
2670 pub fn max_metric_buckets_size(&self) -> usize {
2672 self.inner.values.limits.max_metric_buckets_size.as_bytes()
2673 }
2674
2675 pub fn max_api_payload_size(&self) -> usize {
2677 self.inner.values.limits.max_api_payload_size.as_bytes()
2678 }
2679
2680 pub fn max_api_file_upload_size(&self) -> usize {
2682 self.inner.values.limits.max_api_file_upload_size.as_bytes()
2683 }
2684
2685 pub fn max_api_chunk_upload_size(&self) -> usize {
2687 self.inner
2688 .values
2689 .limits
2690 .max_api_chunk_upload_size
2691 .as_bytes()
2692 }
2693
2694 pub fn max_profile_size(&self) -> usize {
2696 self.inner.values.limits.max_profile_size.as_bytes()
2697 }
2698
2699 pub fn max_trace_metric_size(&self) -> usize {
2701 self.inner.values.limits.max_trace_metric_size.as_bytes()
2702 }
2703
2704 pub fn max_replay_compressed_size(&self) -> usize {
2706 self.inner
2707 .values
2708 .limits
2709 .max_replay_compressed_size
2710 .as_bytes()
2711 }
2712
2713 pub fn max_replay_uncompressed_size(&self) -> usize {
2715 self.inner
2716 .values
2717 .limits
2718 .max_replay_uncompressed_size
2719 .as_bytes()
2720 }
2721
2722 pub fn max_replay_message_size(&self) -> usize {
2728 self.inner.values.limits.max_replay_message_size.as_bytes()
2729 }
2730
2731 pub fn max_concurrent_requests(&self) -> usize {
2733 self.inner.values.limits.max_concurrent_requests
2734 }
2735
2736 pub fn max_concurrent_queries(&self) -> usize {
2738 self.inner.values.limits.max_concurrent_queries
2739 }
2740
2741 pub fn max_removed_attribute_key_size(&self) -> usize {
2743 self.inner
2744 .values
2745 .limits
2746 .max_removed_attribute_key_size
2747 .as_bytes()
2748 }
2749
2750 pub fn query_timeout(&self) -> Duration {
2752 Duration::from_secs(self.inner.values.limits.query_timeout)
2753 }
2754
2755 pub fn shutdown_timeout(&self) -> Duration {
2758 Duration::from_secs(self.inner.values.limits.shutdown_timeout)
2759 }
2760
2761 pub fn keepalive_timeout(&self) -> Duration {
2765 Duration::from_secs(self.inner.values.limits.keepalive_timeout)
2766 }
2767
2768 pub fn idle_timeout(&self) -> Option<Duration> {
2770 self.inner
2771 .values
2772 .limits
2773 .idle_timeout
2774 .map(Duration::from_secs)
2775 }
2776
2777 pub fn max_connections(&self) -> Option<usize> {
2779 self.inner.values.limits.max_connections
2780 }
2781
2782 pub fn tcp_listen_backlog(&self) -> u32 {
2784 self.inner.values.limits.tcp_listen_backlog
2785 }
2786
2787 pub fn cpu_concurrency(&self) -> usize {
2789 self.inner.values.limits.max_thread_count
2790 }
2791
2792 pub fn pool_concurrency(&self) -> usize {
2794 self.inner.values.limits.max_pool_concurrency
2795 }
2796
2797 pub fn query_batch_size(&self) -> usize {
2799 self.inner.values.cache.batch_size
2800 }
2801
2802 pub fn processing_enabled(&self) -> bool {
2804 self.inner.values.processing.enabled
2805 }
2806
2807 pub fn normalization_level(&self) -> NormalizationLevel {
2809 self.inner.values.normalization.level
2810 }
2811
2812 pub fn geoip_path(&self) -> Option<&Path> {
2814 self.inner.values.geoip.path.as_deref().or(self
2815 .inner
2816 .values
2817 .processing
2818 .geoip_path
2819 .as_deref())
2820 }
2821
2822 pub fn max_secs_in_future(&self) -> i64 {
2826 self.inner.values.processing.max_secs_in_future.into()
2827 }
2828
2829 pub fn max_session_secs_in_past(&self) -> i64 {
2831 self.inner.values.processing.max_session_secs_in_past.into()
2832 }
2833
2834 pub fn kafka_configs(
2836 &self,
2837 topic: KafkaTopic,
2838 ) -> Result<KafkaTopicConfig<'_>, KafkaConfigError> {
2839 self.inner
2840 .values
2841 .processing
2842 .topics
2843 .get(topic)
2844 .kafka_configs(
2845 &self.inner.values.processing.kafka_config,
2846 &self.inner.values.processing.secondary_kafka_configs,
2847 )
2848 }
2849
2850 pub fn kafka_validate_topics(&self) -> bool {
2852 self.inner.values.processing.kafka_validate_topics
2853 }
2854
2855 pub fn reloading_producer_jitter(&self) -> Duration {
2857 Duration::from_secs(self.inner.values.processing.reloading_producer_jitter_secs)
2858 }
2859
2860 pub fn kafka_config_blob(&self) -> Result<Vec<u8>, KafkaConfigError> {
2869 let mut clusters = BTreeMap::new();
2872 let mut topics = BTreeMap::new();
2873
2874 let default_cluster = default_cluster_name(&self.inner.values.processing);
2878
2879 for topic in KafkaTopic::iter() {
2880 let topic_config = self.kafka_configs(*topic)?;
2881
2882 for params in topic_config.topics() {
2883 let cluster_name = params.config_name.unwrap_or(&default_cluster);
2884
2885 let config: BTreeMap<_, _> = params
2886 .params
2887 .iter()
2888 .map(|param| (param.name.as_str(), param.value.as_str()))
2889 .collect();
2890
2891 clusters.insert(
2892 cluster_name.to_owned(),
2893 serde_json::json!({ "config": config }),
2894 );
2895
2896 let entry = serde_json::json!({ "physical_topic": { "cluster": cluster_name } });
2902 match topics.entry(params.topic_name.clone()) {
2903 Entry::Vacant(vacant) => {
2904 vacant.insert(entry);
2905 }
2906 Entry::Occupied(occupied) if *occupied.get() != entry => {
2907 return Err(KafkaConfigError::AmbiguousTopic(params.topic_name.clone()));
2908 }
2909 Entry::Occupied(_) => {}
2910 }
2911 }
2912 }
2913
2914 Ok(serde_json::to_vec(&serde_json::json!({
2916 "clusters": clusters,
2917 "topics": topics,
2918 }))
2919 .expect("kafka config blob is serializable"))
2920 }
2921
2922 pub fn unused_topic_assignments(&self) -> &relay_kafka::Unused {
2924 &self.inner.values.processing.topics.unused
2925 }
2926
2927 pub fn objectstore(&self) -> &ObjectstoreServiceConfig {
2929 &self.inner.values.processing.objectstore
2930 }
2931
2932 pub fn upload(&self) -> &Upload {
2934 &self.inner.values.upload
2935 }
2936
2937 #[cfg(feature = "processing")]
2939 pub fn upload_signing_key(&self) -> Option<&SecretKey> {
2940 self.upload()
2941 .credentials
2942 .as_ref()
2943 .map(|c| &c.signing_key)
2944 .or(self.credentials().map(|c| &c.secret_key))
2945 }
2946
2947 #[cfg(feature = "processing")]
2949 pub fn upload_verification_key(&self) -> Option<&PublicKey> {
2950 self.upload()
2951 .credentials
2952 .as_ref()
2953 .map(|c| &c.verification_key)
2954 .or(self.credentials().map(|c| &c.public_key))
2955 }
2956
2957 pub fn redis(&self) -> Option<RedisConfigsRef<'_>> {
2959 let redis_configs = self.inner.values.processing.redis.as_ref()?;
2960
2961 Some(build_redis_configs(
2962 redis_configs,
2963 self.cpu_concurrency() as u32,
2964 self.pool_concurrency() as u32,
2965 ))
2966 }
2967
2968 pub fn attachment_chunk_size(&self) -> usize {
2970 self.inner
2971 .values
2972 .processing
2973 .attachment_chunk_size
2974 .as_bytes()
2975 }
2976
2977 pub fn metrics_max_batch_size_bytes(&self) -> usize {
2979 self.inner.values.aggregator.max_flush_bytes
2980 }
2981
2982 pub fn projectconfig_cache_prefix(&self) -> &str {
2985 &self.inner.values.processing.projectconfig_cache_prefix
2986 }
2987
2988 pub fn max_rate_limit(&self) -> Option<u64> {
2990 self.inner.values.processing.max_rate_limit.map(u32::into)
2991 }
2992
2993 pub fn quota_cache_ratio(&self) -> Option<f32> {
2995 self.inner.values.processing.quota_cache_ratio
2996 }
2997
2998 pub fn quota_cache_max(&self) -> Option<f32> {
3000 self.inner.values.processing.quota_cache_max
3001 }
3002
3003 pub fn health_refresh_interval(&self) -> Duration {
3005 Duration::from_millis(self.inner.values.health.refresh_interval_ms)
3006 }
3007
3008 pub fn health_max_memory_watermark_bytes(&self) -> u64 {
3010 self.inner
3011 .values
3012 .health
3013 .max_memory_bytes
3014 .as_ref()
3015 .map_or(u64::MAX, |b| b.as_bytes() as u64)
3016 }
3017
3018 pub fn health_max_memory_watermark_percent(&self) -> f32 {
3020 self.inner.values.health.max_memory_percent
3021 }
3022
3023 pub fn health_probe_timeout(&self) -> Duration {
3025 Duration::from_millis(self.inner.values.health.probe_timeout_ms)
3026 }
3027
3028 pub fn memory_stat_refresh_frequency_ms(&self) -> u64 {
3030 self.inner.values.health.memory_stat_refresh_frequency_ms
3031 }
3032
3033 pub fn cogs_max_queue_size(&self) -> u64 {
3035 self.inner.values.cogs.max_queue_size
3036 }
3037
3038 pub fn cogs_relay_resource_id(&self) -> &str {
3040 &self.inner.values.cogs.relay_resource_id
3041 }
3042
3043 pub fn default_aggregator_config(&self) -> &AggregatorServiceConfig {
3045 &self.inner.values.aggregator
3046 }
3047
3048 pub fn secondary_aggregator_configs(&self) -> &Vec<ScopedAggregatorConfig> {
3050 &self.inner.values.secondary_aggregators
3051 }
3052
3053 pub fn aggregator_config_for(&self, namespace: MetricNamespace) -> &AggregatorServiceConfig {
3055 for entry in &self.inner.values.secondary_aggregators {
3056 if entry.condition.matches(Some(namespace)) {
3057 return &entry.config;
3058 }
3059 }
3060 &self.inner.values.aggregator
3061 }
3062
3063 pub fn static_relays(&self) -> &HashMap<RelayId, RelayInfo> {
3065 &self.inner.values.auth.static_relays
3066 }
3067
3068 pub fn signature_max_age(&self) -> Duration {
3070 Duration::from_secs(self.inner.values.auth.signature_max_age)
3071 }
3072
3073 pub fn accept_unknown_items(&self) -> bool {
3075 let forward = self.inner.values.routing.accept_unknown_items;
3076 forward.unwrap_or_else(|| !self.processing_enabled())
3077 }
3078}
3079
3080impl fmt::Debug for ConfigSnapshot {
3081 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3082 f.debug_struct("ConfigSnapshot")
3083 .field("values", &self.inner.values)
3084 .finish()
3085 }
3086}
3087
3088#[cfg(test)]
3089mod tests {
3090 use super::*;
3091
3092 #[cfg(feature = "processing")]
3093 #[test]
3094 fn test_upload_secret_key_from_file() {
3095 let path = env::temp_dir().join(Uuid::new_v4().to_string());
3096 fs::create_dir(&path).unwrap();
3097 fs::write(
3098 path.join("my_secret.txt"),
3099 "U3LSQM5NorvgnoYHW_aZpc_43nuuh3lhs3zjjcBwaks",
3100 )
3101 .unwrap();
3102 fs::write(
3103 ConfigValues::path(&path),
3104 r#"
3105 upload:
3106 credentials:
3107 signing_key: ${file:my_secret.txt}
3108 verification_key: "VNS8haF0VTnuMMDR2t-f7AgnmUcXmcdzV3SVksSk34s""#,
3109 )
3110 .unwrap();
3111
3112 let config = Config::from_path(&path).unwrap().current();
3113
3114 fs::remove_dir_all(path).unwrap();
3115
3116 let signing_key = &config.upload().credentials.as_ref().unwrap().signing_key;
3117 assert_eq!(
3118 signing_key.to_string(),
3119 "U3LSQM5NorvgnoYHW_aZpc_43nuuh3lhs3zjjcBwaks"
3120 );
3121 }
3122
3123 #[test]
3124 fn test_emit_outcomes() {
3125 for (serialized, deserialized) in &[
3126 ("true", EmitOutcomes::AsOutcomes),
3127 ("false", EmitOutcomes::None),
3128 ("\"as_client_reports\"", EmitOutcomes::AsClientReports),
3129 ] {
3130 let value: EmitOutcomes = serde_json::from_str(serialized).unwrap();
3131 assert_eq!(value, *deserialized);
3132 assert_eq!(serde_json::to_string(&value).unwrap(), *serialized);
3133 }
3134 }
3135
3136 #[test]
3137 fn test_emit_outcomes_invalid() {
3138 assert!(serde_json::from_str::<EmitOutcomes>("asdf").is_err());
3139 }
3140
3141 #[cfg(feature = "processing")]
3143 fn processing_config(yaml: &str) -> Config {
3144 let path = env::temp_dir().join(Uuid::new_v4().to_string());
3145 fs::create_dir(&path).unwrap();
3146 fs::write(ConfigValues::path(&path), yaml).unwrap();
3147 Config::from_path(&path).unwrap()
3148 }
3149
3150 #[cfg(feature = "processing")]
3152 #[test]
3153 fn test_kafka_config_blob_round_trips() {
3154 use sentry_arroyo::backends::kafka::config_blob::{ConfigBlob, ProducerSelector};
3155
3156 let config = processing_config(
3158 r#"
3159processing:
3160 enabled: true
3161 kafka_config:
3162 - {name: "bootstrap.servers", value: "primary:9092"}
3163 - {name: "compression.type", value: "lz4"}
3164 secondary_kafka_configs:
3165 default:
3166 - {name: "bootstrap.servers", value: "impostor:9092"}
3167 topics:
3168 ingest-events: ["events-1", "events-2"]
3169 ingest-metrics:
3170 name: "metrics-topic"
3171 config: "default"
3172"#,
3173 );
3174 let config = config.current();
3175
3176 let blob = config.kafka_config_blob().unwrap();
3177 let parsed = ConfigBlob::parse(&blob).unwrap();
3178
3179 let resolve = |topic: &str| {
3180 let resolved = parsed
3181 .producer_config(&ProducerSelector::new(topic, "relay"))
3182 .unwrap_or_else(|_| panic!("{topic} is missing from the blob"));
3183
3184 assert_eq!(resolved.physical_topic.as_str(), topic);
3186
3187 resolved
3188 .config
3189 .config_map()
3190 .iter()
3191 .filter(|(key, _)| key.as_str() != "statistics.interval.ms")
3193 .map(|(key, value)| (key.clone(), value.clone()))
3194 .collect::<BTreeMap<_, _>>()
3195 };
3196
3197 for (topic, physical) in [
3198 (KafkaTopic::Events, ["events-1", "events-2"].as_slice()),
3199 (KafkaTopic::MetricsSessions, ["metrics-topic"].as_slice()),
3200 ] {
3201 let params = config.kafka_configs(topic).unwrap();
3202
3203 for (shard, physical) in params.topics().iter().zip(physical) {
3204 let expected: BTreeMap<_, _> = shard
3205 .params
3206 .iter()
3207 .map(|param| (param.name.clone(), param.value.clone()))
3208 .collect();
3209
3210 assert_eq!(
3211 resolve(physical),
3212 expected,
3213 "{physical} resolved differently"
3214 );
3215 }
3216 }
3217
3218 assert_eq!(
3220 resolve("events-1").get("bootstrap.servers").unwrap(),
3221 "primary:9092"
3222 );
3223 assert_eq!(
3224 resolve("metrics-topic").get("bootstrap.servers").unwrap(),
3225 "impostor:9092"
3226 );
3227 }
3228
3229 #[cfg(feature = "processing")]
3231 #[test]
3232 fn test_topic_can_move_between_clusters() {
3233 use sentry_arroyo::backends::kafka::config_blob::{ConfigBlob, ProducerSelector};
3234
3235 let resolve = |yaml: &str| {
3236 let config = processing_config(yaml);
3237 let blob = config.current().kafka_config_blob().unwrap();
3238 ConfigBlob::parse(&blob)
3239 .unwrap()
3240 .producer_config(&ProducerSelector::new("events-topic", "relay"))
3241 .unwrap()
3242 .config
3243 .get_config_value("bootstrap.servers")
3244 .unwrap()
3245 .clone()
3246 };
3247
3248 let before = resolve(
3249 r#"
3250processing:
3251 enabled: true
3252 kafka_config:
3253 - {name: "bootstrap.servers", value: "primary:9092"}
3254 secondary_kafka_configs:
3255 other:
3256 - {name: "bootstrap.servers", value: "other:9092"}
3257 topics:
3258 ingest-events: "events-topic"
3259"#,
3260 );
3261
3262 let after = resolve(
3264 r#"
3265processing:
3266 enabled: true
3267 kafka_config:
3268 - {name: "bootstrap.servers", value: "primary:9092"}
3269 secondary_kafka_configs:
3270 other:
3271 - {name: "bootstrap.servers", value: "other:9092"}
3272 topics:
3273 ingest-events:
3274 name: "events-topic"
3275 config: "other"
3276"#,
3277 );
3278
3279 assert_eq!(before, "primary:9092");
3280 assert_eq!(after, "other:9092");
3281 }
3282
3283 #[cfg(feature = "processing")]
3286 #[test]
3287 fn test_kafka_config_blob_rejects_topic_on_two_clusters() {
3288 let config = processing_config(
3289 r#"
3290processing:
3291 enabled: true
3292 kafka_config:
3293 - {name: "bootstrap.servers", value: "primary:9092"}
3294 secondary_kafka_configs:
3295 other:
3296 - {name: "bootstrap.servers", value: "other:9092"}
3297 topics:
3298 outcomes:
3299 name: "outcomes-topic"
3300 config: "other"
3301 outcomes-billing: "outcomes-topic"
3302"#,
3303 );
3304
3305 assert!(matches!(
3306 config.current().kafka_config_blob(),
3307 Err(KafkaConfigError::AmbiguousTopic(topic)) if topic == "outcomes-topic"
3308 ));
3309 }
3310
3311 #[test]
3314 fn test_reload_takes_kafka_config_but_not_topics() {
3315 let mut current = ConfigValues::default();
3316 current.processing.kafka_config = vec![KafkaConfigParam {
3317 name: "bootstrap.servers".to_owned(),
3318 value: "old:9092".to_owned(),
3319 }];
3320 current.processing.topics.events = "old-topic".to_owned().into();
3321
3322 let mut other = ConfigValues::default();
3323 other.processing.kafka_config = vec![KafkaConfigParam {
3324 name: "bootstrap.servers".to_owned(),
3325 value: "new:9092".to_owned(),
3326 }];
3327 other.processing.topics.events = "new-topic".to_owned().into();
3328
3329 let mut current = ConfigInner {
3330 values: current,
3331 ..Default::default()
3332 };
3333 let other = ConfigInner {
3334 values: other,
3335 ..Default::default()
3336 };
3337
3338 assert!(current.reload_with(&other));
3339 assert_eq!(current.values.processing.kafka_config[0].value, "new:9092");
3340
3341 let assignment = format!("{:?}", current.values.processing.topics.events);
3342 assert!(
3343 assignment.contains("old-topic"),
3344 "topic assignment was reloaded, but the producers cannot pick it up: {assignment}"
3345 );
3346 }
3347}