1use std::collections::HashMap;
2use std::fs::File;
3use std::io::BufReader;
4use std::path::Path;
5
6use relay_base_schema::metrics::MetricNamespace;
7use relay_event_normalization::{MeasurementsConfig, ModelMetadata, SpanOpDefaults};
8use relay_filter::GenericFiltersConfig;
9use relay_quotas::Quota;
10use serde::{Deserialize, Serialize, de};
11use serde_json::Value;
12
13use crate::ErrorBoundary;
14
15#[derive(Default, Clone, Debug, Serialize, Deserialize)]
20#[serde(default, rename_all = "camelCase")]
21pub struct GlobalConfig {
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub measurements: Option<MeasurementsConfig>,
25 #[serde(skip_serializing_if = "Vec::is_empty")]
27 pub quotas: Vec<Quota>,
28 #[serde(skip_serializing_if = "is_err_or_empty")]
33 pub filters: ErrorBoundary<GenericFiltersConfig>,
34 #[serde(
36 deserialize_with = "default_on_error",
37 skip_serializing_if = "is_default"
38 )]
39 pub options: Options,
40
41 #[serde(skip_serializing_if = "is_model_metadata_empty")]
43 pub ai_model_metadata: ErrorBoundary<ModelMetadata>,
44
45 #[serde(
47 deserialize_with = "default_on_error",
48 skip_serializing_if = "is_default"
49 )]
50 pub span_op_defaults: SpanOpDefaults,
51}
52
53impl GlobalConfig {
54 pub fn load(folder_path: &Path) -> anyhow::Result<Option<Self>> {
59 let path = folder_path.join("global_config.json");
60
61 if path.exists() {
62 let file = BufReader::new(File::open(path)?);
63 Ok(Some(serde_json::from_reader(file)?))
64 } else {
65 Ok(None)
66 }
67 }
68
69 pub fn filters(&self) -> Option<&GenericFiltersConfig> {
71 match &self.filters {
72 ErrorBoundary::Err(_) => None,
73 ErrorBoundary::Ok(f) => Some(f),
74 }
75 }
76
77 pub fn ai_model_metadata(&self) -> Option<&ModelMetadata> {
79 self.ai_model_metadata
80 .as_ref()
81 .ok()
82 .filter(|m| m.is_enabled())
83 }
84}
85
86fn is_err_or_empty(filters_config: &ErrorBoundary<GenericFiltersConfig>) -> bool {
87 match filters_config {
88 ErrorBoundary::Err(_) => true,
89 ErrorBoundary::Ok(config) => config.version == 0 && config.filters.is_empty(),
90 }
91}
92
93fn default_killswitched() -> bool {
95 relay_log::info!("using default for endpoint fetch config");
96 bool::default()
97}
98
99#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)]
101#[serde(default)]
102pub struct Options {
103 #[serde(
105 rename = "relay.metric-bucket-set-encodings",
106 deserialize_with = "de_metric_bucket_encodings",
107 skip_serializing_if = "is_default"
108 )]
109 pub metric_bucket_set_encodings: BucketEncodings,
110 #[serde(
112 rename = "relay.metric-bucket-distribution-encodings",
113 deserialize_with = "de_metric_bucket_encodings",
114 skip_serializing_if = "is_default"
115 )]
116 pub metric_bucket_dist_encodings: BucketEncodings,
117
118 #[serde(
122 rename = "relay.span-normalization.allowed_hosts",
123 deserialize_with = "default_on_error",
124 skip_serializing_if = "Vec::is_empty"
125 )]
126 pub http_span_allowed_hosts: Vec<String>,
127
128 #[serde(
135 rename = "relay.sessions-eap.rollout-rate",
136 deserialize_with = "default_on_error",
137 skip_serializing_if = "is_default"
138 )]
139 pub sessions_eap_rollout_rate: f32,
140
141 #[serde(
143 default = "default_killswitched",
144 rename = "relay.endpoint-fetch-config.enabled",
145 deserialize_with = "default_on_error",
146 skip_serializing_if = "is_default"
147 )]
148 pub endpoint_fetch_config_enabled: bool,
149
150 #[serde(
156 rename = "relay.attachment-inline.limit",
157 deserialize_with = "default_on_error",
158 skip_serializing_if = "is_default"
159 )]
160 pub attachment_inline_limit: usize,
161
162 #[serde(
166 rename = "relay.upload-max-chunk.size",
167 deserialize_with = "default_on_error",
168 skip_serializing_if = "is_default"
169 )]
170 pub upload_max_chunk_size: usize,
171
172 #[serde(flatten)]
174 other: HashMap<String, Value>,
175}
176
177#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
179#[serde(default)]
180pub struct BucketEncodings {
181 spans: BucketEncoding,
182 transactions: BucketEncoding,
183 profiles: BucketEncoding,
184}
185
186impl BucketEncodings {
187 pub fn for_namespace(&self, namespace: MetricNamespace) -> BucketEncoding {
189 match namespace {
190 MetricNamespace::Spans => self.spans,
191 MetricNamespace::Transactions => self.transactions,
192 MetricNamespace::Sessions => BucketEncoding::Legacy,
196 _ => BucketEncoding::Legacy,
197 }
198 }
199}
200
201fn de_metric_bucket_encodings<'de, D>(deserializer: D) -> Result<BucketEncodings, D::Error>
205where
206 D: serde::de::Deserializer<'de>,
207{
208 struct Visitor;
209
210 impl<'de> de::Visitor<'de> for Visitor {
211 type Value = BucketEncodings;
212
213 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
214 formatter.write_str("metric bucket encodings")
215 }
216
217 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
218 where
219 E: de::Error,
220 {
221 let encoding = BucketEncoding::deserialize(de::value::StrDeserializer::new(v))?;
222 Ok(BucketEncodings {
223 spans: encoding,
224 transactions: encoding,
225 profiles: encoding,
226 })
227 }
228
229 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
230 where
231 A: de::MapAccess<'de>,
232 {
233 BucketEncodings::deserialize(de::value::MapAccessDeserializer::new(map))
234 }
235 }
236
237 match deserializer.deserialize_any(Visitor) {
238 Ok(value) => Ok(value),
239 Err(error) => {
240 relay_log::error!(
241 error = %error,
242 "Error deserializing metric bucket encodings",
243 );
244 Ok(BucketEncodings::default())
245 }
246 }
247}
248
249#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
251#[serde(rename_all = "lowercase")]
252pub enum BucketEncoding {
253 #[default]
257 Legacy,
258 Array,
263 Base64,
267 Zstd,
271}
272
273fn is_default<T: Default + PartialEq>(t: &T) -> bool {
275 t == &T::default()
276}
277
278fn default_on_error<'de, D, T>(deserializer: D) -> Result<T, D::Error>
279where
280 D: serde::de::Deserializer<'de>,
281 T: Default + serde::de::DeserializeOwned,
282{
283 match T::deserialize(deserializer) {
284 Ok(value) => Ok(value),
285 Err(error) => {
286 relay_log::error!(
287 error = %error,
288 "Error deserializing global config option: {}",
289 std::any::type_name::<T>(),
290 );
291 Ok(T::default())
292 }
293 }
294}
295
296fn is_model_metadata_empty(value: &ErrorBoundary<ModelMetadata>) -> bool {
297 matches!(value, ErrorBoundary::Ok(metadata) if metadata.is_empty())
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_global_config_roundtrip() {
306 let json = r#"{
307 "measurements": {
308 "builtinMeasurements": [
309 {
310 "name": "foo",
311 "unit": "none"
312 },
313 {
314 "name": "bar",
315 "unit": "none"
316 },
317 {
318 "name": "baz",
319 "unit": "none"
320 }
321 ],
322 "maxCustomMeasurements": 5
323 },
324 "quotas": [
325 {
326 "id": "foo",
327 "categories": [
328 "metric_bucket"
329 ],
330 "scope": "organization",
331 "limit": 0,
332 "namespace": null
333 },
334 {
335 "id": "bar",
336 "categories": [
337 "metric_bucket"
338 ],
339 "scope": "organization",
340 "limit": 0,
341 "namespace": null
342 }
343 ],
344 "filters": {
345 "version": 1,
346 "filters": [
347 {
348 "id": "myError",
349 "isEnabled": true,
350 "condition": {
351 "op": "eq",
352 "name": "event.exceptions",
353 "value": "myError"
354 }
355 }
356 ]
357 }
358}"#;
359
360 let deserialized = serde_json::from_str::<GlobalConfig>(json).unwrap();
361 let serialized = serde_json::to_string_pretty(&deserialized).unwrap();
362 assert_eq!(json, serialized.as_str());
363 }
364
365 #[test]
366 fn test_minimal_serialization() {
367 let config = r#"{"options":{"foo":"bar"}}"#;
368 let deserialized: GlobalConfig = serde_json::from_str(config).unwrap();
369 let serialized = serde_json::to_string(&deserialized).unwrap();
370 assert_eq!(config, &serialized);
371 }
372
373 #[test]
374 fn test_metric_bucket_encodings_de_from_str() {
375 let o: Options = serde_json::from_str(
376 r#"{
377 "relay.metric-bucket-set-encodings": "legacy",
378 "relay.metric-bucket-distribution-encodings": "zstd"
379 }"#,
380 )
381 .unwrap();
382
383 assert_eq!(
384 o.metric_bucket_set_encodings,
385 BucketEncodings {
386 spans: BucketEncoding::Legacy,
387 transactions: BucketEncoding::Legacy,
388 profiles: BucketEncoding::Legacy,
389 }
390 );
391 assert_eq!(
392 o.metric_bucket_dist_encodings,
393 BucketEncodings {
394 spans: BucketEncoding::Zstd,
395 transactions: BucketEncoding::Zstd,
396 profiles: BucketEncoding::Zstd,
397 }
398 );
399 }
400
401 #[test]
402 fn test_metric_bucket_encodings_de_from_obj() {
403 let original = BucketEncodings {
404 spans: BucketEncoding::Zstd,
405 transactions: BucketEncoding::Zstd,
406 profiles: BucketEncoding::Base64,
407 };
408 let s = serde_json::to_string(&original).unwrap();
409 let s = format!(
410 r#"{{
411 "relay.metric-bucket-set-encodings": {s},
412 "relay.metric-bucket-distribution-encodings": {s}
413 }}"#
414 );
415
416 let o: Options = serde_json::from_str(&s).unwrap();
417 assert_eq!(o.metric_bucket_set_encodings, original);
418 assert_eq!(o.metric_bucket_dist_encodings, original);
419 }
420}