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, ModelMetadata, 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_metadata_empty")]
50 pub ai_model_metadata: ErrorBoundary<ModelMetadata>,
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 pub fn ai_model_metadata(&self) -> Option<&ModelMetadata> {
86 self.ai_model_metadata
87 .as_ref()
88 .ok()
89 .filter(|m| m.is_enabled())
90 }
91}
92
93fn is_err_or_empty(filters_config: &ErrorBoundary<GenericFiltersConfig>) -> bool {
94 match filters_config {
95 ErrorBoundary::Err(_) => true,
96 ErrorBoundary::Ok(config) => config.version == 0 && config.filters.is_empty(),
97 }
98}
99
100#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)]
102#[serde(default)]
103pub struct Options {
104 #[serde(
106 rename = "relay.metric-bucket-set-encodings",
107 deserialize_with = "de_metric_bucket_encodings",
108 skip_serializing_if = "is_default"
109 )]
110 pub metric_bucket_set_encodings: BucketEncodings,
111 #[serde(
113 rename = "relay.metric-bucket-distribution-encodings",
114 deserialize_with = "de_metric_bucket_encodings",
115 skip_serializing_if = "is_default"
116 )]
117 pub metric_bucket_dist_encodings: BucketEncodings,
118
119 #[serde(
123 rename = "relay.span-normalization.allowed_hosts",
124 deserialize_with = "default_on_error",
125 skip_serializing_if = "Vec::is_empty"
126 )]
127 pub http_span_allowed_hosts: Vec<String>,
128
129 #[serde(
134 rename = "relay.objectstore-attachments.sample-rate",
135 deserialize_with = "default_on_error",
136 skip_serializing_if = "is_default"
137 )]
138 pub objectstore_attachments_sample_rate: f32,
139
140 #[serde(
147 rename = "relay.sessions-eap.rollout-rate",
148 deserialize_with = "default_on_error",
149 skip_serializing_if = "is_default"
150 )]
151 pub sessions_eap_rollout_rate: f32,
152
153 #[serde(
157 rename = "relay.eap-outcomes.rollout-rate",
158 deserialize_with = "default_on_error",
159 skip_serializing_if = "is_default"
160 )]
161 pub eap_outcomes_rollout_rate: f32,
162
163 #[serde(
167 rename = "relay.eap-span-outcomes.rollout-rate",
168 deserialize_with = "default_on_error",
169 skip_serializing_if = "is_default"
170 )]
171 pub eap_span_outcomes_rollout_rate: f32,
172
173 #[serde(
175 rename = "relay.endpoint-fetch-config.enabled",
176 deserialize_with = "default_on_error",
177 skip_serializing_if = "is_default"
178 )]
179 pub endpoint_fetch_config_enabled: bool,
180
181 #[serde(flatten)]
183 other: HashMap<String, Value>,
184}
185
186#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
188#[serde(default)]
189pub struct BucketEncodings {
190 spans: BucketEncoding,
191 transactions: BucketEncoding,
192 profiles: BucketEncoding,
193 custom: BucketEncoding,
194}
195
196impl BucketEncodings {
197 pub fn for_namespace(&self, namespace: MetricNamespace) -> BucketEncoding {
199 match namespace {
200 MetricNamespace::Spans => self.spans,
201 MetricNamespace::Transactions => self.transactions,
202 MetricNamespace::Custom => self.custom,
203 MetricNamespace::Sessions => BucketEncoding::Legacy,
207 _ => BucketEncoding::Legacy,
208 }
209 }
210}
211
212fn de_metric_bucket_encodings<'de, D>(deserializer: D) -> Result<BucketEncodings, D::Error>
216where
217 D: serde::de::Deserializer<'de>,
218{
219 struct Visitor;
220
221 impl<'de> de::Visitor<'de> for Visitor {
222 type Value = BucketEncodings;
223
224 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
225 formatter.write_str("metric bucket encodings")
226 }
227
228 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
229 where
230 E: de::Error,
231 {
232 let encoding = BucketEncoding::deserialize(de::value::StrDeserializer::new(v))?;
233 Ok(BucketEncodings {
234 spans: encoding,
235 transactions: encoding,
236 profiles: encoding,
237 custom: encoding,
238 })
239 }
240
241 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
242 where
243 A: de::MapAccess<'de>,
244 {
245 BucketEncodings::deserialize(de::value::MapAccessDeserializer::new(map))
246 }
247 }
248
249 match deserializer.deserialize_any(Visitor) {
250 Ok(value) => Ok(value),
251 Err(error) => {
252 relay_log::error!(
253 error = %error,
254 "Error deserializing metric bucket encodings",
255 );
256 Ok(BucketEncodings::default())
257 }
258 }
259}
260
261#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
263#[serde(rename_all = "lowercase")]
264pub enum BucketEncoding {
265 #[default]
269 Legacy,
270 Array,
275 Base64,
279 Zstd,
283}
284
285fn is_default<T: Default + PartialEq>(t: &T) -> bool {
287 t == &T::default()
288}
289
290fn default_on_error<'de, D, T>(deserializer: D) -> Result<T, D::Error>
291where
292 D: serde::de::Deserializer<'de>,
293 T: Default + serde::de::DeserializeOwned,
294{
295 match T::deserialize(deserializer) {
296 Ok(value) => Ok(value),
297 Err(error) => {
298 relay_log::error!(
299 error = %error,
300 "Error deserializing global config option: {}",
301 std::any::type_name::<T>(),
302 );
303 Ok(T::default())
304 }
305 }
306}
307
308fn is_ok_and_empty(value: &ErrorBoundary<MetricExtractionGroups>) -> bool {
309 matches!(
310 value,
311 &ErrorBoundary::Ok(MetricExtractionGroups { ref groups }) if groups.is_empty()
312 )
313}
314
315fn is_model_metadata_empty(value: &ErrorBoundary<ModelMetadata>) -> bool {
316 matches!(value, ErrorBoundary::Ok(metadata) if metadata.is_empty())
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn test_global_config_roundtrip() {
325 let json = r#"{
326 "measurements": {
327 "builtinMeasurements": [
328 {
329 "name": "foo",
330 "unit": "none"
331 },
332 {
333 "name": "bar",
334 "unit": "none"
335 },
336 {
337 "name": "baz",
338 "unit": "none"
339 }
340 ],
341 "maxCustomMeasurements": 5
342 },
343 "quotas": [
344 {
345 "id": "foo",
346 "categories": [
347 "metric_bucket"
348 ],
349 "scope": "organization",
350 "limit": 0,
351 "namespace": null
352 },
353 {
354 "id": "bar",
355 "categories": [
356 "metric_bucket"
357 ],
358 "scope": "organization",
359 "limit": 0,
360 "namespace": null
361 }
362 ],
363 "filters": {
364 "version": 1,
365 "filters": [
366 {
367 "id": "myError",
368 "isEnabled": true,
369 "condition": {
370 "op": "eq",
371 "name": "event.exceptions",
372 "value": "myError"
373 }
374 }
375 ]
376 }
377}"#;
378
379 let deserialized = serde_json::from_str::<GlobalConfig>(json).unwrap();
380 let serialized = serde_json::to_string_pretty(&deserialized).unwrap();
381 assert_eq!(json, serialized.as_str());
382 }
383
384 #[test]
385 fn test_minimal_serialization() {
386 let config = r#"{"options":{"foo":"bar"}}"#;
387 let deserialized: GlobalConfig = serde_json::from_str(config).unwrap();
388 let serialized = serde_json::to_string(&deserialized).unwrap();
389 assert_eq!(config, &serialized);
390 }
391
392 #[test]
393 fn test_metric_bucket_encodings_de_from_str() {
394 let o: Options = serde_json::from_str(
395 r#"{
396 "relay.metric-bucket-set-encodings": "legacy",
397 "relay.metric-bucket-distribution-encodings": "zstd"
398 }"#,
399 )
400 .unwrap();
401
402 assert_eq!(
403 o.metric_bucket_set_encodings,
404 BucketEncodings {
405 spans: BucketEncoding::Legacy,
406 transactions: BucketEncoding::Legacy,
407 profiles: BucketEncoding::Legacy,
408 custom: BucketEncoding::Legacy,
409 }
410 );
411 assert_eq!(
412 o.metric_bucket_dist_encodings,
413 BucketEncodings {
414 spans: BucketEncoding::Zstd,
415 transactions: BucketEncoding::Zstd,
416 profiles: BucketEncoding::Zstd,
417 custom: BucketEncoding::Zstd,
418 }
419 );
420 }
421
422 #[test]
423 fn test_metric_bucket_encodings_de_from_obj() {
424 let original = BucketEncodings {
425 spans: BucketEncoding::Zstd,
426 transactions: BucketEncoding::Zstd,
427 profiles: BucketEncoding::Base64,
428 custom: BucketEncoding::Zstd,
429 };
430 let s = serde_json::to_string(&original).unwrap();
431 let s = format!(
432 r#"{{
433 "relay.metric-bucket-set-encodings": {s},
434 "relay.metric-bucket-distribution-encodings": {s}
435 }}"#
436 );
437
438 let o: Options = serde_json::from_str(&s).unwrap();
439 assert_eq!(o.metric_bucket_set_encodings, original);
440 assert_eq!(o.metric_bucket_dist_encodings, original);
441 }
442}