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::{MeasurementsConfig, ModelCosts, SpanOpDefaults};
9use relay_filter::GenericFiltersConfig;
10use relay_quotas::Quota;
11use serde::{Deserialize, Serialize, de};
12use serde_json::Value;
13
14use crate::{ErrorBoundary, MetricExtractionGroup, MetricExtractionGroups, defaults};
15
16#[derive(Default, Clone, Debug, Serialize, Deserialize)]
21#[serde(default, rename_all = "camelCase")]
22pub struct GlobalConfig {
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub measurements: Option<MeasurementsConfig>,
26 #[serde(skip_serializing_if = "Vec::is_empty")]
28 pub quotas: Vec<Quota>,
29 #[serde(skip_serializing_if = "is_err_or_empty")]
34 pub filters: ErrorBoundary<GenericFiltersConfig>,
35 #[serde(
37 deserialize_with = "default_on_error",
38 skip_serializing_if = "is_default"
39 )]
40 pub options: Options,
41
42 #[serde(skip_serializing_if = "is_ok_and_empty")]
47 pub metric_extraction: ErrorBoundary<MetricExtractionGroups>,
48
49 #[serde(skip_serializing_if = "is_missing")]
51 pub ai_model_costs: ErrorBoundary<ModelCosts>,
52
53 #[serde(
55 deserialize_with = "default_on_error",
56 skip_serializing_if = "is_default"
57 )]
58 pub span_op_defaults: SpanOpDefaults,
59}
60
61impl GlobalConfig {
62 pub fn load(folder_path: &Path) -> anyhow::Result<Option<Self>> {
67 let path = folder_path.join("global_config.json");
68
69 if path.exists() {
70 let file = BufReader::new(File::open(path)?);
71 Ok(Some(serde_json::from_reader(file)?))
72 } else {
73 Ok(None)
74 }
75 }
76
77 pub fn filters(&self) -> Option<&GenericFiltersConfig> {
79 match &self.filters {
80 ErrorBoundary::Err(_) => None,
81 ErrorBoundary::Ok(f) => Some(f),
82 }
83 }
84
85 pub fn normalize(&mut self) {
89 if let ErrorBoundary::Ok(config) = &mut self.metric_extraction {
90 for (group_name, metrics, tags) in defaults::hardcoded_span_metrics() {
91 if let Entry::Vacant(entry) = config.groups.entry(group_name) {
94 entry.insert(MetricExtractionGroup {
95 is_enabled: false, metrics,
97 tags,
98 });
99 }
100 }
101 }
102 }
103}
104
105fn is_err_or_empty(filters_config: &ErrorBoundary<GenericFiltersConfig>) -> bool {
106 match filters_config {
107 ErrorBoundary::Err(_) => true,
108 ErrorBoundary::Ok(config) => config.version == 0 && config.filters.is_empty(),
109 }
110}
111
112#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)]
114#[serde(default)]
115pub struct Options {
116 #[serde(
118 rename = "relay.cardinality-limiter.mode",
119 deserialize_with = "default_on_error",
120 skip_serializing_if = "is_default"
121 )]
122 pub cardinality_limiter_mode: CardinalityLimiterMode,
123
124 #[serde(
129 rename = "relay.cardinality-limiter.error-sample-rate",
130 deserialize_with = "default_on_error",
131 skip_serializing_if = "is_default"
132 )]
133 pub cardinality_limiter_error_sample_rate: f32,
134
135 #[serde(
137 rename = "relay.metric-bucket-set-encodings",
138 deserialize_with = "de_metric_bucket_encodings",
139 skip_serializing_if = "is_default"
140 )]
141 pub metric_bucket_set_encodings: BucketEncodings,
142 #[serde(
144 rename = "relay.metric-bucket-distribution-encodings",
145 deserialize_with = "de_metric_bucket_encodings",
146 skip_serializing_if = "is_default"
147 )]
148 pub metric_bucket_dist_encodings: BucketEncodings,
149
150 #[serde(
155 rename = "relay.metric-stats.rollout-rate",
156 deserialize_with = "default_on_error",
157 skip_serializing_if = "is_default"
158 )]
159 pub metric_stats_rollout_rate: f32,
160
161 #[serde(
171 rename = "relay.span-extraction.sample-rate",
172 deserialize_with = "default_on_error",
173 skip_serializing_if = "is_default"
174 )]
175 pub span_extraction_sample_rate: Option<f32>,
176
177 #[serde(
186 rename = "relay.ourlogs-ingestion.sample-rate",
187 deserialize_with = "default_on_error",
188 skip_serializing_if = "is_default"
189 )]
190 pub ourlogs_ingestion_sample_rate: Option<f32>,
191
192 #[serde(
196 rename = "relay.span-normalization.allowed_hosts",
197 deserialize_with = "default_on_error",
198 skip_serializing_if = "Vec::is_empty"
199 )]
200 pub http_span_allowed_hosts: Vec<String>,
201
202 #[serde(
204 rename = "relay.drop-transaction-attachments",
205 deserialize_with = "default_on_error",
206 skip_serializing_if = "is_default"
207 )]
208 pub drop_transaction_attachments: bool,
209
210 #[doc(hidden)]
212 #[serde(
213 rename = "profiling.profile_metrics.unsampled_profiles.platforms",
214 deserialize_with = "default_on_error",
215 skip_serializing_if = "Vec::is_empty"
216 )]
217 pub deprecated1: Vec<String>,
218
219 #[doc(hidden)]
221 #[serde(
222 rename = "profiling.profile_metrics.unsampled_profiles.sample_rate",
223 deserialize_with = "default_on_error",
224 skip_serializing_if = "is_default"
225 )]
226 pub deprecated2: f32,
227
228 #[serde(
234 rename = "relay.spans-ignore-trace-id-partitioning",
235 skip_serializing_if = "is_default"
236 )]
237 pub spans_ignore_trace_id_partitioning: bool,
238
239 #[serde(flatten)]
241 other: HashMap<String, Value>,
242}
243
244#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
246#[serde(rename_all = "lowercase")]
247pub enum CardinalityLimiterMode {
248 #[default]
250 #[serde(alias = "")]
253 Enabled,
254 Passive,
256 Disabled,
258}
259
260#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
262#[serde(default)]
263pub struct BucketEncodings {
264 transactions: BucketEncoding,
265 spans: BucketEncoding,
266 profiles: BucketEncoding,
267 custom: BucketEncoding,
268 metric_stats: BucketEncoding,
269}
270
271impl BucketEncodings {
272 pub fn for_namespace(&self, namespace: MetricNamespace) -> BucketEncoding {
274 match namespace {
275 MetricNamespace::Transactions => self.transactions,
276 MetricNamespace::Spans => self.spans,
277 MetricNamespace::Custom => self.custom,
278 MetricNamespace::Stats => self.metric_stats,
279 MetricNamespace::Sessions => BucketEncoding::Legacy,
283 _ => BucketEncoding::Legacy,
284 }
285 }
286}
287
288fn de_metric_bucket_encodings<'de, D>(deserializer: D) -> Result<BucketEncodings, D::Error>
292where
293 D: serde::de::Deserializer<'de>,
294{
295 struct Visitor;
296
297 impl<'de> de::Visitor<'de> for Visitor {
298 type Value = BucketEncodings;
299
300 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
301 formatter.write_str("metric bucket encodings")
302 }
303
304 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
305 where
306 E: de::Error,
307 {
308 let encoding = BucketEncoding::deserialize(de::value::StrDeserializer::new(v))?;
309 Ok(BucketEncodings {
310 transactions: encoding,
311 spans: encoding,
312 profiles: encoding,
313 custom: encoding,
314 metric_stats: encoding,
315 })
316 }
317
318 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
319 where
320 A: de::MapAccess<'de>,
321 {
322 BucketEncodings::deserialize(de::value::MapAccessDeserializer::new(map))
323 }
324 }
325
326 match deserializer.deserialize_any(Visitor) {
327 Ok(value) => Ok(value),
328 Err(error) => {
329 relay_log::error!(
330 error = %error,
331 "Error deserializing metric bucket encodings",
332 );
333 Ok(BucketEncodings::default())
334 }
335 }
336}
337
338#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
340#[serde(rename_all = "lowercase")]
341pub enum BucketEncoding {
342 #[default]
346 Legacy,
347 Array,
352 Base64,
356 Zstd,
360}
361
362fn is_default<T: Default + PartialEq>(t: &T) -> bool {
364 t == &T::default()
365}
366
367fn default_on_error<'de, D, T>(deserializer: D) -> Result<T, D::Error>
368where
369 D: serde::de::Deserializer<'de>,
370 T: Default + serde::de::DeserializeOwned,
371{
372 match T::deserialize(deserializer) {
373 Ok(value) => Ok(value),
374 Err(error) => {
375 relay_log::error!(
376 error = %error,
377 "Error deserializing global config option: {}",
378 std::any::type_name::<T>(),
379 );
380 Ok(T::default())
381 }
382 }
383}
384
385fn is_ok_and_empty(value: &ErrorBoundary<MetricExtractionGroups>) -> bool {
386 matches!(
387 value,
388 &ErrorBoundary::Ok(MetricExtractionGroups { ref groups }) if groups.is_empty()
389 )
390}
391
392fn is_missing(value: &ErrorBoundary<ModelCosts>) -> bool {
393 matches!(
394 value,
395 &ErrorBoundary::Ok(ModelCosts{ version, ref costs }) if version == 0 && costs.is_empty()
396 )
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn test_global_config_roundtrip() {
405 let json = r#"{
406 "measurements": {
407 "builtinMeasurements": [
408 {
409 "name": "foo",
410 "unit": "none"
411 },
412 {
413 "name": "bar",
414 "unit": "none"
415 },
416 {
417 "name": "baz",
418 "unit": "none"
419 }
420 ],
421 "maxCustomMeasurements": 5
422 },
423 "quotas": [
424 {
425 "id": "foo",
426 "categories": [
427 "metric_bucket"
428 ],
429 "scope": "organization",
430 "limit": 0,
431 "namespace": null
432 },
433 {
434 "id": "bar",
435 "categories": [
436 "metric_bucket"
437 ],
438 "scope": "organization",
439 "limit": 0,
440 "namespace": null
441 }
442 ],
443 "filters": {
444 "version": 1,
445 "filters": [
446 {
447 "id": "myError",
448 "isEnabled": true,
449 "condition": {
450 "op": "eq",
451 "name": "event.exceptions",
452 "value": "myError"
453 }
454 }
455 ]
456 }
457}"#;
458
459 let deserialized = serde_json::from_str::<GlobalConfig>(json).unwrap();
460 let serialized = serde_json::to_string_pretty(&deserialized).unwrap();
461 assert_eq!(json, serialized.as_str());
462 }
463
464 #[test]
465 fn test_global_config_invalid_value_is_default() {
466 let options: Options = serde_json::from_str(
467 r#"{
468 "relay.cardinality-limiter.mode": "passive"
469 }"#,
470 )
471 .unwrap();
472
473 let expected = Options {
474 cardinality_limiter_mode: CardinalityLimiterMode::Passive,
475 ..Default::default()
476 };
477
478 assert_eq!(options, expected);
479 }
480
481 #[test]
482 fn test_cardinality_limiter_mode_de_serialize() {
483 let m: CardinalityLimiterMode = serde_json::from_str("\"\"").unwrap();
484 assert_eq!(m, CardinalityLimiterMode::Enabled);
485 let m: CardinalityLimiterMode = serde_json::from_str("\"enabled\"").unwrap();
486 assert_eq!(m, CardinalityLimiterMode::Enabled);
487 let m: CardinalityLimiterMode = serde_json::from_str("\"disabled\"").unwrap();
488 assert_eq!(m, CardinalityLimiterMode::Disabled);
489 let m: CardinalityLimiterMode = serde_json::from_str("\"passive\"").unwrap();
490 assert_eq!(m, CardinalityLimiterMode::Passive);
491
492 let m = serde_json::to_string(&CardinalityLimiterMode::Enabled).unwrap();
493 assert_eq!(m, "\"enabled\"");
494 }
495
496 #[test]
497 fn test_minimal_serialization() {
498 let config = r#"{"options":{"foo":"bar"}}"#;
499 let deserialized: GlobalConfig = serde_json::from_str(config).unwrap();
500 let serialized = serde_json::to_string(&deserialized).unwrap();
501 assert_eq!(config, &serialized);
502 }
503
504 #[test]
505 fn test_metric_bucket_encodings_de_from_str() {
506 let o: Options = serde_json::from_str(
507 r#"{
508 "relay.metric-bucket-set-encodings": "legacy",
509 "relay.metric-bucket-distribution-encodings": "zstd"
510 }"#,
511 )
512 .unwrap();
513
514 assert_eq!(
515 o.metric_bucket_set_encodings,
516 BucketEncodings {
517 transactions: BucketEncoding::Legacy,
518 spans: BucketEncoding::Legacy,
519 profiles: BucketEncoding::Legacy,
520 custom: BucketEncoding::Legacy,
521 metric_stats: BucketEncoding::Legacy,
522 }
523 );
524 assert_eq!(
525 o.metric_bucket_dist_encodings,
526 BucketEncodings {
527 transactions: BucketEncoding::Zstd,
528 spans: BucketEncoding::Zstd,
529 profiles: BucketEncoding::Zstd,
530 custom: BucketEncoding::Zstd,
531 metric_stats: BucketEncoding::Zstd,
532 }
533 );
534 }
535
536 #[test]
537 fn test_metric_bucket_encodings_de_from_obj() {
538 let original = BucketEncodings {
539 transactions: BucketEncoding::Base64,
540 spans: BucketEncoding::Zstd,
541 profiles: BucketEncoding::Base64,
542 custom: BucketEncoding::Zstd,
543 metric_stats: BucketEncoding::Base64,
544 };
545 let s = serde_json::to_string(&original).unwrap();
546 let s = format!(
547 r#"{{
548 "relay.metric-bucket-set-encodings": {s},
549 "relay.metric-bucket-distribution-encodings": {s}
550 }}"#
551 );
552
553 let o: Options = serde_json::from_str(&s).unwrap();
554 assert_eq!(o.metric_bucket_set_encodings, original);
555 assert_eq!(o.metric_bucket_dist_encodings, original);
556 }
557}