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;
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_model_metadata_empty")]
43 pub ai_model_metadata: ErrorBoundary<ModelMetadata>,
44
45 #[serde(
47 deserialize_with = "default_on_error",
48 skip_serializing_if = "is_default"
49 )]
50 pub span_op_defaults: SpanOpDefaults,
51}
52
53impl GlobalConfig {
54 pub fn load(folder_path: &Path) -> anyhow::Result<Option<Self>> {
59 let path = folder_path.join("global_config.json");
60
61 if path.exists() {
62 let file = BufReader::new(File::open(path)?);
63 Ok(Some(serde_json::from_reader(file)?))
64 } else {
65 Ok(None)
66 }
67 }
68
69 pub fn filters(&self) -> Option<&GenericFiltersConfig> {
71 match &self.filters {
72 ErrorBoundary::Err(_) => None,
73 ErrorBoundary::Ok(f) => Some(f),
74 }
75 }
76
77 pub fn ai_model_metadata(&self) -> Option<&ModelMetadata> {
79 self.ai_model_metadata
80 .as_ref()
81 .ok()
82 .filter(|m| m.is_enabled())
83 }
84}
85
86fn is_err_or_empty(filters_config: &ErrorBoundary<GenericFiltersConfig>) -> bool {
87 match filters_config {
88 ErrorBoundary::Err(_) => true,
89 ErrorBoundary::Ok(config) => config.version == 0 && config.filters.is_empty(),
90 }
91}
92
93fn default_killswitched() -> bool {
95 relay_log::info!("using default for endpoint fetch config");
96 bool::default()
97}
98
99#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)]
101#[serde(default)]
102pub struct Options {
103 #[serde(
105 rename = "relay.metric-bucket-set-encodings",
106 deserialize_with = "de_metric_bucket_encodings",
107 skip_serializing_if = "is_default"
108 )]
109 pub metric_bucket_set_encodings: BucketEncodings,
110 #[serde(
112 rename = "relay.metric-bucket-distribution-encodings",
113 deserialize_with = "de_metric_bucket_encodings",
114 skip_serializing_if = "is_default"
115 )]
116 pub metric_bucket_dist_encodings: BucketEncodings,
117
118 #[serde(
122 rename = "relay.span-normalization.allowed_hosts",
123 deserialize_with = "default_on_error",
124 skip_serializing_if = "Vec::is_empty"
125 )]
126 pub http_span_allowed_hosts: Vec<String>,
127
128 #[serde(
133 rename = "relay.objectstore-attachments.sample-rate",
134 deserialize_with = "default_on_error",
135 skip_serializing_if = "is_default"
136 )]
137 pub objectstore_attachments_sample_rate: f32,
138
139 #[serde(
146 rename = "relay.sessions-eap.rollout-rate",
147 deserialize_with = "default_on_error",
148 skip_serializing_if = "is_default"
149 )]
150 pub sessions_eap_rollout_rate: f32,
151
152 #[serde(
154 default = "default_killswitched",
155 rename = "relay.endpoint-fetch-config.enabled",
156 deserialize_with = "default_on_error",
157 skip_serializing_if = "is_default"
158 )]
159 pub endpoint_fetch_config_enabled: bool,
160
161 #[serde(
167 rename = "relay.attachment-inline.limit",
168 deserialize_with = "default_on_error",
169 skip_serializing_if = "is_default"
170 )]
171 pub attachment_inline_limit: usize,
172
173 #[serde(
177 rename = "relay.upload-chunk.size",
178 deserialize_with = "default_on_error",
179 skip_serializing_if = "is_default"
180 )]
181 pub upload_chunk_size: usize,
182
183 #[serde(flatten)]
185 other: HashMap<String, Value>,
186}
187
188#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
190#[serde(default)]
191pub struct BucketEncodings {
192 spans: BucketEncoding,
193 transactions: BucketEncoding,
194 profiles: BucketEncoding,
195}
196
197impl BucketEncodings {
198 pub fn for_namespace(&self, namespace: MetricNamespace) -> BucketEncoding {
200 match namespace {
201 MetricNamespace::Spans => self.spans,
202 MetricNamespace::Transactions => self.transactions,
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 })
238 }
239
240 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
241 where
242 A: de::MapAccess<'de>,
243 {
244 BucketEncodings::deserialize(de::value::MapAccessDeserializer::new(map))
245 }
246 }
247
248 match deserializer.deserialize_any(Visitor) {
249 Ok(value) => Ok(value),
250 Err(error) => {
251 relay_log::error!(
252 error = %error,
253 "Error deserializing metric bucket encodings",
254 );
255 Ok(BucketEncodings::default())
256 }
257 }
258}
259
260#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
262#[serde(rename_all = "lowercase")]
263pub enum BucketEncoding {
264 #[default]
268 Legacy,
269 Array,
274 Base64,
278 Zstd,
282}
283
284fn is_default<T: Default + PartialEq>(t: &T) -> bool {
286 t == &T::default()
287}
288
289fn default_on_error<'de, D, T>(deserializer: D) -> Result<T, D::Error>
290where
291 D: serde::de::Deserializer<'de>,
292 T: Default + serde::de::DeserializeOwned,
293{
294 match T::deserialize(deserializer) {
295 Ok(value) => Ok(value),
296 Err(error) => {
297 relay_log::error!(
298 error = %error,
299 "Error deserializing global config option: {}",
300 std::any::type_name::<T>(),
301 );
302 Ok(T::default())
303 }
304 }
305}
306
307fn is_model_metadata_empty(value: &ErrorBoundary<ModelMetadata>) -> bool {
308 matches!(value, ErrorBoundary::Ok(metadata) if metadata.is_empty())
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn test_global_config_roundtrip() {
317 let json = r#"{
318 "measurements": {
319 "builtinMeasurements": [
320 {
321 "name": "foo",
322 "unit": "none"
323 },
324 {
325 "name": "bar",
326 "unit": "none"
327 },
328 {
329 "name": "baz",
330 "unit": "none"
331 }
332 ],
333 "maxCustomMeasurements": 5
334 },
335 "quotas": [
336 {
337 "id": "foo",
338 "categories": [
339 "metric_bucket"
340 ],
341 "scope": "organization",
342 "limit": 0,
343 "namespace": null
344 },
345 {
346 "id": "bar",
347 "categories": [
348 "metric_bucket"
349 ],
350 "scope": "organization",
351 "limit": 0,
352 "namespace": null
353 }
354 ],
355 "filters": {
356 "version": 1,
357 "filters": [
358 {
359 "id": "myError",
360 "isEnabled": true,
361 "condition": {
362 "op": "eq",
363 "name": "event.exceptions",
364 "value": "myError"
365 }
366 }
367 ]
368 }
369}"#;
370
371 let deserialized = serde_json::from_str::<GlobalConfig>(json).unwrap();
372 let serialized = serde_json::to_string_pretty(&deserialized).unwrap();
373 assert_eq!(json, serialized.as_str());
374 }
375
376 #[test]
377 fn test_minimal_serialization() {
378 let config = r#"{"options":{"foo":"bar"}}"#;
379 let deserialized: GlobalConfig = serde_json::from_str(config).unwrap();
380 let serialized = serde_json::to_string(&deserialized).unwrap();
381 assert_eq!(config, &serialized);
382 }
383
384 #[test]
385 fn test_metric_bucket_encodings_de_from_str() {
386 let o: Options = serde_json::from_str(
387 r#"{
388 "relay.metric-bucket-set-encodings": "legacy",
389 "relay.metric-bucket-distribution-encodings": "zstd"
390 }"#,
391 )
392 .unwrap();
393
394 assert_eq!(
395 o.metric_bucket_set_encodings,
396 BucketEncodings {
397 spans: BucketEncoding::Legacy,
398 transactions: BucketEncoding::Legacy,
399 profiles: BucketEncoding::Legacy,
400 }
401 );
402 assert_eq!(
403 o.metric_bucket_dist_encodings,
404 BucketEncodings {
405 spans: BucketEncoding::Zstd,
406 transactions: BucketEncoding::Zstd,
407 profiles: BucketEncoding::Zstd,
408 }
409 );
410 }
411
412 #[test]
413 fn test_metric_bucket_encodings_de_from_obj() {
414 let original = BucketEncodings {
415 spans: BucketEncoding::Zstd,
416 transactions: BucketEncoding::Zstd,
417 profiles: BucketEncoding::Base64,
418 };
419 let s = serde_json::to_string(&original).unwrap();
420 let s = format!(
421 r#"{{
422 "relay.metric-bucket-set-encodings": {s},
423 "relay.metric-bucket-distribution-encodings": {s}
424 }}"#
425 );
426
427 let o: Options = serde_json::from_str(&s).unwrap();
428 assert_eq!(o.metric_bucket_set_encodings, original);
429 assert_eq!(o.metric_bucket_dist_encodings, original);
430 }
431}