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