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, ModelCosts, SpanOpDefaults};
8use relay_filter::GenericFiltersConfig;
9use relay_quotas::Quota;
10use serde::{Deserialize, Serialize, de};
11use serde_json::Value;
12
13use crate::{ErrorBoundary, MetricExtractionGroups};
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_ok_and_empty")]
46 pub metric_extraction: ErrorBoundary<MetricExtractionGroups>,
47
48 #[serde(skip_serializing_if = "is_model_costs_empty")]
50 pub ai_model_costs: ErrorBoundary<ModelCosts>,
51
52 #[serde(
54 deserialize_with = "default_on_error",
55 skip_serializing_if = "is_default"
56 )]
57 pub span_op_defaults: SpanOpDefaults,
58}
59
60impl GlobalConfig {
61 pub fn load(folder_path: &Path) -> anyhow::Result<Option<Self>> {
66 let path = folder_path.join("global_config.json");
67
68 if path.exists() {
69 let file = BufReader::new(File::open(path)?);
70 Ok(Some(serde_json::from_reader(file)?))
71 } else {
72 Ok(None)
73 }
74 }
75
76 pub fn filters(&self) -> Option<&GenericFiltersConfig> {
78 match &self.filters {
79 ErrorBoundary::Err(_) => None,
80 ErrorBoundary::Ok(f) => Some(f),
81 }
82 }
83}
84
85fn is_err_or_empty(filters_config: &ErrorBoundary<GenericFiltersConfig>) -> bool {
86 match filters_config {
87 ErrorBoundary::Err(_) => true,
88 ErrorBoundary::Ok(config) => config.version == 0 && config.filters.is_empty(),
89 }
90}
91
92#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)]
94#[serde(default)]
95pub struct Options {
96 #[serde(
98 rename = "relay.cardinality-limiter.mode",
99 deserialize_with = "default_on_error",
100 skip_serializing_if = "is_default"
101 )]
102 pub cardinality_limiter_mode: CardinalityLimiterMode,
103
104 #[serde(
109 rename = "relay.cardinality-limiter.error-sample-rate",
110 deserialize_with = "default_on_error",
111 skip_serializing_if = "is_default"
112 )]
113 pub cardinality_limiter_error_sample_rate: f32,
114
115 #[serde(
117 rename = "relay.metric-bucket-set-encodings",
118 deserialize_with = "de_metric_bucket_encodings",
119 skip_serializing_if = "is_default"
120 )]
121 pub metric_bucket_set_encodings: BucketEncodings,
122 #[serde(
124 rename = "relay.metric-bucket-distribution-encodings",
125 deserialize_with = "de_metric_bucket_encodings",
126 skip_serializing_if = "is_default"
127 )]
128 pub metric_bucket_dist_encodings: BucketEncodings,
129
130 #[serde(
134 rename = "relay.span-normalization.allowed_hosts",
135 deserialize_with = "default_on_error",
136 skip_serializing_if = "Vec::is_empty"
137 )]
138 pub http_span_allowed_hosts: Vec<String>,
139
140 #[serde(
145 rename = "relay.objectstore-attachments.sample-rate",
146 deserialize_with = "default_on_error",
147 skip_serializing_if = "is_default"
148 )]
149 pub objectstore_attachments_sample_rate: f32,
150
151 #[serde(
158 rename = "relay.sessions-eap.rollout-rate",
159 deserialize_with = "default_on_error",
160 skip_serializing_if = "is_default"
161 )]
162 pub sessions_eap_rollout_rate: f32,
163
164 #[serde(
168 rename = "relay.eap-outcomes.rollout-rate",
169 deserialize_with = "default_on_error",
170 skip_serializing_if = "is_default"
171 )]
172 pub eap_outcomes_rollout_rate: f32,
173
174 #[serde(
178 rename = "relay.eap-span-outcomes.rollout-rate",
179 deserialize_with = "default_on_error",
180 skip_serializing_if = "is_default"
181 )]
182 pub eap_span_outcomes_rollout_rate: f32,
183
184 #[serde(flatten)]
186 other: HashMap<String, Value>,
187}
188
189#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
191#[serde(rename_all = "lowercase")]
192pub enum CardinalityLimiterMode {
193 #[default]
195 #[serde(alias = "")]
198 Enabled,
199 Passive,
201 Disabled,
203}
204
205#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
207#[serde(default)]
208pub struct BucketEncodings {
209 transactions: BucketEncoding,
210 spans: BucketEncoding,
211 profiles: BucketEncoding,
212 custom: BucketEncoding,
213}
214
215impl BucketEncodings {
216 pub fn for_namespace(&self, namespace: MetricNamespace) -> BucketEncoding {
218 match namespace {
219 MetricNamespace::Transactions => self.transactions,
220 MetricNamespace::Spans => self.spans,
221 MetricNamespace::Custom => self.custom,
222 MetricNamespace::Sessions => BucketEncoding::Legacy,
226 _ => BucketEncoding::Legacy,
227 }
228 }
229}
230
231fn de_metric_bucket_encodings<'de, D>(deserializer: D) -> Result<BucketEncodings, D::Error>
235where
236 D: serde::de::Deserializer<'de>,
237{
238 struct Visitor;
239
240 impl<'de> de::Visitor<'de> for Visitor {
241 type Value = BucketEncodings;
242
243 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
244 formatter.write_str("metric bucket encodings")
245 }
246
247 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
248 where
249 E: de::Error,
250 {
251 let encoding = BucketEncoding::deserialize(de::value::StrDeserializer::new(v))?;
252 Ok(BucketEncodings {
253 transactions: encoding,
254 spans: encoding,
255 profiles: encoding,
256 custom: encoding,
257 })
258 }
259
260 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
261 where
262 A: de::MapAccess<'de>,
263 {
264 BucketEncodings::deserialize(de::value::MapAccessDeserializer::new(map))
265 }
266 }
267
268 match deserializer.deserialize_any(Visitor) {
269 Ok(value) => Ok(value),
270 Err(error) => {
271 relay_log::error!(
272 error = %error,
273 "Error deserializing metric bucket encodings",
274 );
275 Ok(BucketEncodings::default())
276 }
277 }
278}
279
280#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
282#[serde(rename_all = "lowercase")]
283pub enum BucketEncoding {
284 #[default]
288 Legacy,
289 Array,
294 Base64,
298 Zstd,
302}
303
304fn is_default<T: Default + PartialEq>(t: &T) -> bool {
306 t == &T::default()
307}
308
309fn default_on_error<'de, D, T>(deserializer: D) -> Result<T, D::Error>
310where
311 D: serde::de::Deserializer<'de>,
312 T: Default + serde::de::DeserializeOwned,
313{
314 match T::deserialize(deserializer) {
315 Ok(value) => Ok(value),
316 Err(error) => {
317 relay_log::error!(
318 error = %error,
319 "Error deserializing global config option: {}",
320 std::any::type_name::<T>(),
321 );
322 Ok(T::default())
323 }
324 }
325}
326
327fn is_ok_and_empty(value: &ErrorBoundary<MetricExtractionGroups>) -> bool {
328 matches!(
329 value,
330 &ErrorBoundary::Ok(MetricExtractionGroups { ref groups }) if groups.is_empty()
331 )
332}
333
334fn is_model_costs_empty(value: &ErrorBoundary<ModelCosts>) -> bool {
335 matches!(value, ErrorBoundary::Ok(model_costs) if model_costs.is_empty())
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn test_global_config_roundtrip() {
344 let json = r#"{
345 "measurements": {
346 "builtinMeasurements": [
347 {
348 "name": "foo",
349 "unit": "none"
350 },
351 {
352 "name": "bar",
353 "unit": "none"
354 },
355 {
356 "name": "baz",
357 "unit": "none"
358 }
359 ],
360 "maxCustomMeasurements": 5
361 },
362 "quotas": [
363 {
364 "id": "foo",
365 "categories": [
366 "metric_bucket"
367 ],
368 "scope": "organization",
369 "limit": 0,
370 "namespace": null
371 },
372 {
373 "id": "bar",
374 "categories": [
375 "metric_bucket"
376 ],
377 "scope": "organization",
378 "limit": 0,
379 "namespace": null
380 }
381 ],
382 "filters": {
383 "version": 1,
384 "filters": [
385 {
386 "id": "myError",
387 "isEnabled": true,
388 "condition": {
389 "op": "eq",
390 "name": "event.exceptions",
391 "value": "myError"
392 }
393 }
394 ]
395 }
396}"#;
397
398 let deserialized = serde_json::from_str::<GlobalConfig>(json).unwrap();
399 let serialized = serde_json::to_string_pretty(&deserialized).unwrap();
400 assert_eq!(json, serialized.as_str());
401 }
402
403 #[test]
404 fn test_global_config_invalid_value_is_default() {
405 let options: Options = serde_json::from_str(
406 r#"{
407 "relay.cardinality-limiter.mode": "passive"
408 }"#,
409 )
410 .unwrap();
411
412 let expected = Options {
413 cardinality_limiter_mode: CardinalityLimiterMode::Passive,
414 ..Default::default()
415 };
416
417 assert_eq!(options, expected);
418 }
419
420 #[test]
421 fn test_cardinality_limiter_mode_de_serialize() {
422 let m: CardinalityLimiterMode = serde_json::from_str("\"\"").unwrap();
423 assert_eq!(m, CardinalityLimiterMode::Enabled);
424 let m: CardinalityLimiterMode = serde_json::from_str("\"enabled\"").unwrap();
425 assert_eq!(m, CardinalityLimiterMode::Enabled);
426 let m: CardinalityLimiterMode = serde_json::from_str("\"disabled\"").unwrap();
427 assert_eq!(m, CardinalityLimiterMode::Disabled);
428 let m: CardinalityLimiterMode = serde_json::from_str("\"passive\"").unwrap();
429 assert_eq!(m, CardinalityLimiterMode::Passive);
430
431 let m = serde_json::to_string(&CardinalityLimiterMode::Enabled).unwrap();
432 assert_eq!(m, "\"enabled\"");
433 }
434
435 #[test]
436 fn test_minimal_serialization() {
437 let config = r#"{"options":{"foo":"bar"}}"#;
438 let deserialized: GlobalConfig = serde_json::from_str(config).unwrap();
439 let serialized = serde_json::to_string(&deserialized).unwrap();
440 assert_eq!(config, &serialized);
441 }
442
443 #[test]
444 fn test_metric_bucket_encodings_de_from_str() {
445 let o: Options = serde_json::from_str(
446 r#"{
447 "relay.metric-bucket-set-encodings": "legacy",
448 "relay.metric-bucket-distribution-encodings": "zstd"
449 }"#,
450 )
451 .unwrap();
452
453 assert_eq!(
454 o.metric_bucket_set_encodings,
455 BucketEncodings {
456 transactions: BucketEncoding::Legacy,
457 spans: BucketEncoding::Legacy,
458 profiles: BucketEncoding::Legacy,
459 custom: BucketEncoding::Legacy,
460 }
461 );
462 assert_eq!(
463 o.metric_bucket_dist_encodings,
464 BucketEncodings {
465 transactions: BucketEncoding::Zstd,
466 spans: BucketEncoding::Zstd,
467 profiles: BucketEncoding::Zstd,
468 custom: BucketEncoding::Zstd,
469 }
470 );
471 }
472
473 #[test]
474 fn test_metric_bucket_encodings_de_from_obj() {
475 let original = BucketEncodings {
476 transactions: BucketEncoding::Base64,
477 spans: BucketEncoding::Zstd,
478 profiles: BucketEncoding::Base64,
479 custom: BucketEncoding::Zstd,
480 };
481 let s = serde_json::to_string(&original).unwrap();
482 let s = format!(
483 r#"{{
484 "relay.metric-bucket-set-encodings": {s},
485 "relay.metric-bucket-distribution-encodings": {s}
486 }}"#
487 );
488
489 let o: Options = serde_json::from_str(&s).unwrap();
490 assert_eq!(o.metric_bucket_set_encodings, original);
491 assert_eq!(o.metric_bucket_dist_encodings, original);
492 }
493}