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(
194 rename = "relay.objectstore-attachments.sample-rate",
195 deserialize_with = "default_on_error",
196 skip_serializing_if = "is_default"
197 )]
198 pub objectstore_attachments_sample_rate: f32,
199
200 #[serde(flatten)]
202 other: HashMap<String, Value>,
203}
204
205#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
207#[serde(rename_all = "lowercase")]
208pub enum CardinalityLimiterMode {
209 #[default]
211 #[serde(alias = "")]
214 Enabled,
215 Passive,
217 Disabled,
219}
220
221#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
223#[serde(default)]
224pub struct BucketEncodings {
225 transactions: BucketEncoding,
226 spans: BucketEncoding,
227 profiles: BucketEncoding,
228 custom: BucketEncoding,
229}
230
231impl BucketEncodings {
232 pub fn for_namespace(&self, namespace: MetricNamespace) -> BucketEncoding {
234 match namespace {
235 MetricNamespace::Transactions => self.transactions,
236 MetricNamespace::Spans => self.spans,
237 MetricNamespace::Custom => self.custom,
238 MetricNamespace::Sessions => BucketEncoding::Legacy,
242 _ => BucketEncoding::Legacy,
243 }
244 }
245}
246
247fn de_metric_bucket_encodings<'de, D>(deserializer: D) -> Result<BucketEncodings, D::Error>
251where
252 D: serde::de::Deserializer<'de>,
253{
254 struct Visitor;
255
256 impl<'de> de::Visitor<'de> for Visitor {
257 type Value = BucketEncodings;
258
259 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
260 formatter.write_str("metric bucket encodings")
261 }
262
263 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
264 where
265 E: de::Error,
266 {
267 let encoding = BucketEncoding::deserialize(de::value::StrDeserializer::new(v))?;
268 Ok(BucketEncodings {
269 transactions: encoding,
270 spans: encoding,
271 profiles: encoding,
272 custom: encoding,
273 })
274 }
275
276 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
277 where
278 A: de::MapAccess<'de>,
279 {
280 BucketEncodings::deserialize(de::value::MapAccessDeserializer::new(map))
281 }
282 }
283
284 match deserializer.deserialize_any(Visitor) {
285 Ok(value) => Ok(value),
286 Err(error) => {
287 relay_log::error!(
288 error = %error,
289 "Error deserializing metric bucket encodings",
290 );
291 Ok(BucketEncodings::default())
292 }
293 }
294}
295
296#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
298#[serde(rename_all = "lowercase")]
299pub enum BucketEncoding {
300 #[default]
304 Legacy,
305 Array,
310 Base64,
314 Zstd,
318}
319
320fn is_default<T: Default + PartialEq>(t: &T) -> bool {
322 t == &T::default()
323}
324
325fn default_on_error<'de, D, T>(deserializer: D) -> Result<T, D::Error>
326where
327 D: serde::de::Deserializer<'de>,
328 T: Default + serde::de::DeserializeOwned,
329{
330 match T::deserialize(deserializer) {
331 Ok(value) => Ok(value),
332 Err(error) => {
333 relay_log::error!(
334 error = %error,
335 "Error deserializing global config option: {}",
336 std::any::type_name::<T>(),
337 );
338 Ok(T::default())
339 }
340 }
341}
342
343fn is_ok_and_empty(value: &ErrorBoundary<MetricExtractionGroups>) -> bool {
344 matches!(
345 value,
346 &ErrorBoundary::Ok(MetricExtractionGroups { ref groups }) if groups.is_empty()
347 )
348}
349
350fn is_model_costs_empty(value: &ErrorBoundary<ModelCosts>) -> bool {
351 matches!(value, ErrorBoundary::Ok(model_costs) if model_costs.is_empty())
352}
353
354fn is_ai_operation_type_map_empty(value: &ErrorBoundary<AiOperationTypeMap>) -> bool {
355 matches!(value, ErrorBoundary::Ok(ai_operation_type_map) if ai_operation_type_map.is_empty())
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 #[test]
363 fn test_global_config_roundtrip() {
364 let json = r#"{
365 "measurements": {
366 "builtinMeasurements": [
367 {
368 "name": "foo",
369 "unit": "none"
370 },
371 {
372 "name": "bar",
373 "unit": "none"
374 },
375 {
376 "name": "baz",
377 "unit": "none"
378 }
379 ],
380 "maxCustomMeasurements": 5
381 },
382 "quotas": [
383 {
384 "id": "foo",
385 "categories": [
386 "metric_bucket"
387 ],
388 "scope": "organization",
389 "limit": 0,
390 "namespace": null
391 },
392 {
393 "id": "bar",
394 "categories": [
395 "metric_bucket"
396 ],
397 "scope": "organization",
398 "limit": 0,
399 "namespace": null
400 }
401 ],
402 "filters": {
403 "version": 1,
404 "filters": [
405 {
406 "id": "myError",
407 "isEnabled": true,
408 "condition": {
409 "op": "eq",
410 "name": "event.exceptions",
411 "value": "myError"
412 }
413 }
414 ]
415 }
416}"#;
417
418 let deserialized = serde_json::from_str::<GlobalConfig>(json).unwrap();
419 let serialized = serde_json::to_string_pretty(&deserialized).unwrap();
420 assert_eq!(json, serialized.as_str());
421 }
422
423 #[test]
424 fn test_global_config_invalid_value_is_default() {
425 let options: Options = serde_json::from_str(
426 r#"{
427 "relay.cardinality-limiter.mode": "passive"
428 }"#,
429 )
430 .unwrap();
431
432 let expected = Options {
433 cardinality_limiter_mode: CardinalityLimiterMode::Passive,
434 ..Default::default()
435 };
436
437 assert_eq!(options, expected);
438 }
439
440 #[test]
441 fn test_cardinality_limiter_mode_de_serialize() {
442 let m: CardinalityLimiterMode = serde_json::from_str("\"\"").unwrap();
443 assert_eq!(m, CardinalityLimiterMode::Enabled);
444 let m: CardinalityLimiterMode = serde_json::from_str("\"enabled\"").unwrap();
445 assert_eq!(m, CardinalityLimiterMode::Enabled);
446 let m: CardinalityLimiterMode = serde_json::from_str("\"disabled\"").unwrap();
447 assert_eq!(m, CardinalityLimiterMode::Disabled);
448 let m: CardinalityLimiterMode = serde_json::from_str("\"passive\"").unwrap();
449 assert_eq!(m, CardinalityLimiterMode::Passive);
450
451 let m = serde_json::to_string(&CardinalityLimiterMode::Enabled).unwrap();
452 assert_eq!(m, "\"enabled\"");
453 }
454
455 #[test]
456 fn test_minimal_serialization() {
457 let config = r#"{"options":{"foo":"bar"}}"#;
458 let deserialized: GlobalConfig = serde_json::from_str(config).unwrap();
459 let serialized = serde_json::to_string(&deserialized).unwrap();
460 assert_eq!(config, &serialized);
461 }
462
463 #[test]
464 fn test_metric_bucket_encodings_de_from_str() {
465 let o: Options = serde_json::from_str(
466 r#"{
467 "relay.metric-bucket-set-encodings": "legacy",
468 "relay.metric-bucket-distribution-encodings": "zstd"
469 }"#,
470 )
471 .unwrap();
472
473 assert_eq!(
474 o.metric_bucket_set_encodings,
475 BucketEncodings {
476 transactions: BucketEncoding::Legacy,
477 spans: BucketEncoding::Legacy,
478 profiles: BucketEncoding::Legacy,
479 custom: BucketEncoding::Legacy,
480 }
481 );
482 assert_eq!(
483 o.metric_bucket_dist_encodings,
484 BucketEncodings {
485 transactions: BucketEncoding::Zstd,
486 spans: BucketEncoding::Zstd,
487 profiles: BucketEncoding::Zstd,
488 custom: BucketEncoding::Zstd,
489 }
490 );
491 }
492
493 #[test]
494 fn test_metric_bucket_encodings_de_from_obj() {
495 let original = BucketEncodings {
496 transactions: BucketEncoding::Base64,
497 spans: BucketEncoding::Zstd,
498 profiles: BucketEncoding::Base64,
499 custom: BucketEncoding::Zstd,
500 };
501 let s = serde_json::to_string(&original).unwrap();
502 let s = format!(
503 r#"{{
504 "relay.metric-bucket-set-encodings": {s},
505 "relay.metric-bucket-distribution-encodings": {s}
506 }}"#
507 );
508
509 let o: Options = serde_json::from_str(&s).unwrap();
510 assert_eq!(o.metric_bucket_set_encodings, original);
511 assert_eq!(o.metric_bucket_dist_encodings, original);
512 }
513}