1use std::collections::HashMap;
2use std::collections::btree_map::Entry;
3use std::fs::File;
4use std::io::BufReader;
5use std::path::Path;
6
7use relay_base_schema::metrics::MetricNamespace;
8use relay_event_normalization::{
9 AiOperationTypeMap, MeasurementsConfig, ModelCosts, SpanOpDefaults,
10};
11use relay_filter::GenericFiltersConfig;
12use relay_quotas::Quota;
13use serde::{Deserialize, Serialize, de};
14use serde_json::Value;
15
16use crate::{ErrorBoundary, MetricExtractionGroup, MetricExtractionGroups, defaults};
17
18#[derive(Default, Clone, Debug, Serialize, Deserialize)]
23#[serde(default, rename_all = "camelCase")]
24pub struct GlobalConfig {
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub measurements: Option<MeasurementsConfig>,
28 #[serde(skip_serializing_if = "Vec::is_empty")]
30 pub quotas: Vec<Quota>,
31 #[serde(skip_serializing_if = "is_err_or_empty")]
36 pub filters: ErrorBoundary<GenericFiltersConfig>,
37 #[serde(
39 deserialize_with = "default_on_error",
40 skip_serializing_if = "is_default"
41 )]
42 pub options: Options,
43
44 #[serde(skip_serializing_if = "is_ok_and_empty")]
49 pub metric_extraction: ErrorBoundary<MetricExtractionGroups>,
50
51 #[serde(skip_serializing_if = "is_model_costs_empty")]
53 pub ai_model_costs: ErrorBoundary<ModelCosts>,
54
55 #[serde(skip_serializing_if = "is_ai_operation_type_map_empty")]
57 pub ai_operation_type_map: ErrorBoundary<AiOperationTypeMap>,
58
59 #[serde(
61 deserialize_with = "default_on_error",
62 skip_serializing_if = "is_default"
63 )]
64 pub span_op_defaults: SpanOpDefaults,
65}
66
67impl GlobalConfig {
68 pub fn load(folder_path: &Path) -> anyhow::Result<Option<Self>> {
73 let path = folder_path.join("global_config.json");
74
75 if path.exists() {
76 let file = BufReader::new(File::open(path)?);
77 Ok(Some(serde_json::from_reader(file)?))
78 } else {
79 Ok(None)
80 }
81 }
82
83 pub fn filters(&self) -> Option<&GenericFiltersConfig> {
85 match &self.filters {
86 ErrorBoundary::Err(_) => None,
87 ErrorBoundary::Ok(f) => Some(f),
88 }
89 }
90
91 pub fn normalize(&mut self) {
95 if let ErrorBoundary::Ok(config) = &mut self.metric_extraction {
96 for (group_name, metrics, tags) in defaults::hardcoded_span_metrics() {
97 if let Entry::Vacant(entry) = config.groups.entry(group_name) {
100 entry.insert(MetricExtractionGroup {
101 is_enabled: false, metrics,
103 tags,
104 });
105 }
106 }
107 }
108 }
109}
110
111fn is_err_or_empty(filters_config: &ErrorBoundary<GenericFiltersConfig>) -> bool {
112 match filters_config {
113 ErrorBoundary::Err(_) => true,
114 ErrorBoundary::Ok(config) => config.version == 0 && config.filters.is_empty(),
115 }
116}
117
118#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)]
120#[serde(default)]
121pub struct Options {
122 #[serde(
124 rename = "relay.cardinality-limiter.mode",
125 deserialize_with = "default_on_error",
126 skip_serializing_if = "is_default"
127 )]
128 pub cardinality_limiter_mode: CardinalityLimiterMode,
129
130 #[serde(
135 rename = "relay.cardinality-limiter.error-sample-rate",
136 deserialize_with = "default_on_error",
137 skip_serializing_if = "is_default"
138 )]
139 pub cardinality_limiter_error_sample_rate: f32,
140
141 #[serde(
143 rename = "relay.metric-bucket-set-encodings",
144 deserialize_with = "de_metric_bucket_encodings",
145 skip_serializing_if = "is_default"
146 )]
147 pub metric_bucket_set_encodings: BucketEncodings,
148 #[serde(
150 rename = "relay.metric-bucket-distribution-encodings",
151 deserialize_with = "de_metric_bucket_encodings",
152 skip_serializing_if = "is_default"
153 )]
154 pub metric_bucket_dist_encodings: BucketEncodings,
155
156 #[serde(
165 rename = "relay.span-extraction.sample-rate",
166 deserialize_with = "default_on_error",
167 skip_serializing_if = "is_default"
168 )]
169 pub span_extraction_sample_rate: Option<f32>,
170
171 #[serde(
175 rename = "relay.span-normalization.allowed_hosts",
176 deserialize_with = "default_on_error",
177 skip_serializing_if = "Vec::is_empty"
178 )]
179 pub http_span_allowed_hosts: Vec<String>,
180
181 #[serde(
183 rename = "replay.relay-snuba-publishing-disabled.sample-rate",
184 deserialize_with = "default_on_error",
185 skip_serializing_if = "is_default"
186 )]
187 pub replay_relay_snuba_publish_disabled_sample_rate: f32,
188
189 #[serde(flatten)]
191 other: HashMap<String, Value>,
192}
193
194#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
196#[serde(rename_all = "lowercase")]
197pub enum CardinalityLimiterMode {
198 #[default]
200 #[serde(alias = "")]
203 Enabled,
204 Passive,
206 Disabled,
208}
209
210#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
212#[serde(default)]
213pub struct BucketEncodings {
214 transactions: BucketEncoding,
215 spans: BucketEncoding,
216 profiles: BucketEncoding,
217 custom: BucketEncoding,
218}
219
220impl BucketEncodings {
221 pub fn for_namespace(&self, namespace: MetricNamespace) -> BucketEncoding {
223 match namespace {
224 MetricNamespace::Transactions => self.transactions,
225 MetricNamespace::Spans => self.spans,
226 MetricNamespace::Custom => self.custom,
227 MetricNamespace::Sessions => BucketEncoding::Legacy,
231 _ => BucketEncoding::Legacy,
232 }
233 }
234}
235
236fn de_metric_bucket_encodings<'de, D>(deserializer: D) -> Result<BucketEncodings, D::Error>
240where
241 D: serde::de::Deserializer<'de>,
242{
243 struct Visitor;
244
245 impl<'de> de::Visitor<'de> for Visitor {
246 type Value = BucketEncodings;
247
248 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
249 formatter.write_str("metric bucket encodings")
250 }
251
252 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
253 where
254 E: de::Error,
255 {
256 let encoding = BucketEncoding::deserialize(de::value::StrDeserializer::new(v))?;
257 Ok(BucketEncodings {
258 transactions: encoding,
259 spans: encoding,
260 profiles: encoding,
261 custom: encoding,
262 })
263 }
264
265 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
266 where
267 A: de::MapAccess<'de>,
268 {
269 BucketEncodings::deserialize(de::value::MapAccessDeserializer::new(map))
270 }
271 }
272
273 match deserializer.deserialize_any(Visitor) {
274 Ok(value) => Ok(value),
275 Err(error) => {
276 relay_log::error!(
277 error = %error,
278 "Error deserializing metric bucket encodings",
279 );
280 Ok(BucketEncodings::default())
281 }
282 }
283}
284
285#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
287#[serde(rename_all = "lowercase")]
288pub enum BucketEncoding {
289 #[default]
293 Legacy,
294 Array,
299 Base64,
303 Zstd,
307}
308
309fn is_default<T: Default + PartialEq>(t: &T) -> bool {
311 t == &T::default()
312}
313
314fn default_on_error<'de, D, T>(deserializer: D) -> Result<T, D::Error>
315where
316 D: serde::de::Deserializer<'de>,
317 T: Default + serde::de::DeserializeOwned,
318{
319 match T::deserialize(deserializer) {
320 Ok(value) => Ok(value),
321 Err(error) => {
322 relay_log::error!(
323 error = %error,
324 "Error deserializing global config option: {}",
325 std::any::type_name::<T>(),
326 );
327 Ok(T::default())
328 }
329 }
330}
331
332fn is_ok_and_empty(value: &ErrorBoundary<MetricExtractionGroups>) -> bool {
333 matches!(
334 value,
335 &ErrorBoundary::Ok(MetricExtractionGroups { ref groups }) if groups.is_empty()
336 )
337}
338
339fn is_model_costs_empty(value: &ErrorBoundary<ModelCosts>) -> bool {
340 matches!(value, ErrorBoundary::Ok(model_costs) if model_costs.is_empty())
341}
342
343fn is_ai_operation_type_map_empty(value: &ErrorBoundary<AiOperationTypeMap>) -> bool {
344 matches!(value, ErrorBoundary::Ok(ai_operation_type_map) if ai_operation_type_map.is_empty())
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 #[test]
352 fn test_global_config_roundtrip() {
353 let json = r#"{
354 "measurements": {
355 "builtinMeasurements": [
356 {
357 "name": "foo",
358 "unit": "none"
359 },
360 {
361 "name": "bar",
362 "unit": "none"
363 },
364 {
365 "name": "baz",
366 "unit": "none"
367 }
368 ],
369 "maxCustomMeasurements": 5
370 },
371 "quotas": [
372 {
373 "id": "foo",
374 "categories": [
375 "metric_bucket"
376 ],
377 "scope": "organization",
378 "limit": 0,
379 "namespace": null
380 },
381 {
382 "id": "bar",
383 "categories": [
384 "metric_bucket"
385 ],
386 "scope": "organization",
387 "limit": 0,
388 "namespace": null
389 }
390 ],
391 "filters": {
392 "version": 1,
393 "filters": [
394 {
395 "id": "myError",
396 "isEnabled": true,
397 "condition": {
398 "op": "eq",
399 "name": "event.exceptions",
400 "value": "myError"
401 }
402 }
403 ]
404 }
405}"#;
406
407 let deserialized = serde_json::from_str::<GlobalConfig>(json).unwrap();
408 let serialized = serde_json::to_string_pretty(&deserialized).unwrap();
409 assert_eq!(json, serialized.as_str());
410 }
411
412 #[test]
413 fn test_global_config_invalid_value_is_default() {
414 let options: Options = serde_json::from_str(
415 r#"{
416 "relay.cardinality-limiter.mode": "passive"
417 }"#,
418 )
419 .unwrap();
420
421 let expected = Options {
422 cardinality_limiter_mode: CardinalityLimiterMode::Passive,
423 ..Default::default()
424 };
425
426 assert_eq!(options, expected);
427 }
428
429 #[test]
430 fn test_cardinality_limiter_mode_de_serialize() {
431 let m: CardinalityLimiterMode = serde_json::from_str("\"\"").unwrap();
432 assert_eq!(m, CardinalityLimiterMode::Enabled);
433 let m: CardinalityLimiterMode = serde_json::from_str("\"enabled\"").unwrap();
434 assert_eq!(m, CardinalityLimiterMode::Enabled);
435 let m: CardinalityLimiterMode = serde_json::from_str("\"disabled\"").unwrap();
436 assert_eq!(m, CardinalityLimiterMode::Disabled);
437 let m: CardinalityLimiterMode = serde_json::from_str("\"passive\"").unwrap();
438 assert_eq!(m, CardinalityLimiterMode::Passive);
439
440 let m = serde_json::to_string(&CardinalityLimiterMode::Enabled).unwrap();
441 assert_eq!(m, "\"enabled\"");
442 }
443
444 #[test]
445 fn test_minimal_serialization() {
446 let config = r#"{"options":{"foo":"bar"}}"#;
447 let deserialized: GlobalConfig = serde_json::from_str(config).unwrap();
448 let serialized = serde_json::to_string(&deserialized).unwrap();
449 assert_eq!(config, &serialized);
450 }
451
452 #[test]
453 fn test_metric_bucket_encodings_de_from_str() {
454 let o: Options = serde_json::from_str(
455 r#"{
456 "relay.metric-bucket-set-encodings": "legacy",
457 "relay.metric-bucket-distribution-encodings": "zstd"
458 }"#,
459 )
460 .unwrap();
461
462 assert_eq!(
463 o.metric_bucket_set_encodings,
464 BucketEncodings {
465 transactions: BucketEncoding::Legacy,
466 spans: BucketEncoding::Legacy,
467 profiles: BucketEncoding::Legacy,
468 custom: BucketEncoding::Legacy,
469 }
470 );
471 assert_eq!(
472 o.metric_bucket_dist_encodings,
473 BucketEncodings {
474 transactions: BucketEncoding::Zstd,
475 spans: BucketEncoding::Zstd,
476 profiles: BucketEncoding::Zstd,
477 custom: BucketEncoding::Zstd,
478 }
479 );
480 }
481
482 #[test]
483 fn test_metric_bucket_encodings_de_from_obj() {
484 let original = BucketEncodings {
485 transactions: BucketEncoding::Base64,
486 spans: BucketEncoding::Zstd,
487 profiles: BucketEncoding::Base64,
488 custom: BucketEncoding::Zstd,
489 };
490 let s = serde_json::to_string(&original).unwrap();
491 let s = format!(
492 r#"{{
493 "relay.metric-bucket-set-encodings": {s},
494 "relay.metric-bucket-distribution-encodings": {s}
495 }}"#
496 );
497
498 let o: Options = serde_json::from_str(&s).unwrap();
499 assert_eq!(o.metric_bucket_set_encodings, original);
500 assert_eq!(o.metric_bucket_dist_encodings, original);
501 }
502}