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