use std::collections::btree_map::Entry;
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use relay_base_schema::metrics::MetricNamespace;
use relay_event_normalization::{MeasurementsConfig, ModelCosts, SpanOpDefaults};
use relay_filter::GenericFiltersConfig;
use relay_quotas::Quota;
use serde::{de, Deserialize, Serialize};
use serde_json::Value;
use crate::{defaults, ErrorBoundary, MetricExtractionGroup, MetricExtractionGroups};
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct GlobalConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub measurements: Option<MeasurementsConfig>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub quotas: Vec<Quota>,
#[serde(skip_serializing_if = "is_err_or_empty")]
pub filters: ErrorBoundary<GenericFiltersConfig>,
#[serde(
deserialize_with = "default_on_error",
skip_serializing_if = "is_default"
)]
pub options: Options,
#[serde(skip_serializing_if = "is_ok_and_empty")]
pub metric_extraction: ErrorBoundary<MetricExtractionGroups>,
#[serde(skip_serializing_if = "is_missing")]
pub ai_model_costs: ErrorBoundary<ModelCosts>,
#[serde(
deserialize_with = "default_on_error",
skip_serializing_if = "is_default"
)]
pub span_op_defaults: SpanOpDefaults,
}
impl GlobalConfig {
pub fn load(folder_path: &Path) -> anyhow::Result<Option<Self>> {
let path = folder_path.join("global_config.json");
if path.exists() {
let file = BufReader::new(File::open(path)?);
Ok(Some(serde_json::from_reader(file)?))
} else {
Ok(None)
}
}
pub fn filters(&self) -> Option<&GenericFiltersConfig> {
match &self.filters {
ErrorBoundary::Err(_) => None,
ErrorBoundary::Ok(f) => Some(f),
}
}
pub fn normalize(&mut self) {
if let ErrorBoundary::Ok(config) = &mut self.metric_extraction {
for (group_name, metrics, tags) in defaults::hardcoded_span_metrics() {
if let Entry::Vacant(entry) = config.groups.entry(group_name) {
entry.insert(MetricExtractionGroup {
is_enabled: false, metrics,
tags,
});
}
}
}
}
}
fn is_err_or_empty(filters_config: &ErrorBoundary<GenericFiltersConfig>) -> bool {
match filters_config {
ErrorBoundary::Err(_) => true,
ErrorBoundary::Ok(config) => config.version == 0 && config.filters.is_empty(),
}
}
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct Options {
#[serde(
rename = "profiling.generic_metrics.functions_ingestion.enabled",
deserialize_with = "default_on_error",
skip_serializing_if = "is_default"
)]
pub profiles_function_generic_metrics_enabled: bool,
#[serde(
rename = "relay.cardinality-limiter.mode",
deserialize_with = "default_on_error",
skip_serializing_if = "is_default"
)]
pub cardinality_limiter_mode: CardinalityLimiterMode,
#[serde(
rename = "relay.cardinality-limiter.error-sample-rate",
deserialize_with = "default_on_error",
skip_serializing_if = "is_default"
)]
pub cardinality_limiter_error_sample_rate: f32,
#[serde(
rename = "relay.metric-bucket-set-encodings",
deserialize_with = "de_metric_bucket_encodings",
skip_serializing_if = "is_default"
)]
pub metric_bucket_set_encodings: BucketEncodings,
#[serde(
rename = "relay.metric-bucket-distribution-encodings",
deserialize_with = "de_metric_bucket_encodings",
skip_serializing_if = "is_default"
)]
pub metric_bucket_dist_encodings: BucketEncodings,
#[serde(
rename = "relay.metric-stats.rollout-rate",
deserialize_with = "default_on_error",
skip_serializing_if = "is_default"
)]
pub metric_stats_rollout_rate: f32,
#[serde(
rename = "relay.span-extraction.sample-rate",
deserialize_with = "default_on_error",
skip_serializing_if = "is_default"
)]
pub span_extraction_sample_rate: Option<f32>,
#[serde(
rename = "relay.span-normalization.allowed_hosts",
deserialize_with = "default_on_error",
skip_serializing_if = "Vec::is_empty"
)]
pub http_span_allowed_hosts: Vec<String>,
#[doc(hidden)]
#[serde(
rename = "profiling.profile_metrics.unsampled_profiles.platforms",
deserialize_with = "default_on_error",
skip_serializing_if = "Vec::is_empty"
)]
pub deprecated1: Vec<String>,
#[doc(hidden)]
#[serde(
rename = "profiling.profile_metrics.unsampled_profiles.sample_rate",
deserialize_with = "default_on_error",
skip_serializing_if = "is_default"
)]
pub deprecated2: f32,
#[serde(flatten)]
other: HashMap<String, Value>,
}
#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum CardinalityLimiterMode {
#[default]
#[serde(alias = "")]
Enabled,
Passive,
Disabled,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct BucketEncodings {
transactions: BucketEncoding,
spans: BucketEncoding,
profiles: BucketEncoding,
custom: BucketEncoding,
metric_stats: BucketEncoding,
}
impl BucketEncodings {
pub fn for_namespace(&self, namespace: MetricNamespace) -> BucketEncoding {
match namespace {
MetricNamespace::Transactions => self.transactions,
MetricNamespace::Spans => self.spans,
MetricNamespace::Profiles => self.profiles,
MetricNamespace::Custom => self.custom,
MetricNamespace::Stats => self.metric_stats,
MetricNamespace::Sessions => BucketEncoding::Legacy,
_ => BucketEncoding::Legacy,
}
}
}
fn de_metric_bucket_encodings<'de, D>(deserializer: D) -> Result<BucketEncodings, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = BucketEncodings;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("metric bucket encodings")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
let encoding = BucketEncoding::deserialize(de::value::StrDeserializer::new(v))?;
Ok(BucketEncodings {
transactions: encoding,
spans: encoding,
profiles: encoding,
custom: encoding,
metric_stats: encoding,
})
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
BucketEncodings::deserialize(de::value::MapAccessDeserializer::new(map))
}
}
match deserializer.deserialize_any(Visitor) {
Ok(value) => Ok(value),
Err(error) => {
relay_log::error!(
error = %error,
"Error deserializing metric bucket encodings",
);
Ok(BucketEncodings::default())
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum BucketEncoding {
#[default]
Legacy,
Array,
Base64,
Zstd,
}
fn is_default<T: Default + PartialEq>(t: &T) -> bool {
t == &T::default()
}
fn default_on_error<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: serde::de::Deserializer<'de>,
T: Default + serde::de::DeserializeOwned,
{
match T::deserialize(deserializer) {
Ok(value) => Ok(value),
Err(error) => {
relay_log::error!(
error = %error,
"Error deserializing global config option: {}",
std::any::type_name::<T>(),
);
Ok(T::default())
}
}
}
fn is_ok_and_empty(value: &ErrorBoundary<MetricExtractionGroups>) -> bool {
matches!(
value,
&ErrorBoundary::Ok(MetricExtractionGroups { ref groups }) if groups.is_empty()
)
}
fn is_missing(value: &ErrorBoundary<ModelCosts>) -> bool {
matches!(
value,
&ErrorBoundary::Ok(ModelCosts{ version, ref costs }) if version == 0 && costs.is_empty()
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_global_config_roundtrip() {
let json = r#"{
"measurements": {
"builtinMeasurements": [
{
"name": "foo",
"unit": "none"
},
{
"name": "bar",
"unit": "none"
},
{
"name": "baz",
"unit": "none"
}
],
"maxCustomMeasurements": 5
},
"quotas": [
{
"id": "foo",
"categories": [
"metric_bucket"
],
"scope": "organization",
"limit": 0,
"namespace": null
},
{
"id": "bar",
"categories": [
"metric_bucket"
],
"scope": "organization",
"limit": 0,
"namespace": null
}
],
"filters": {
"version": 1,
"filters": [
{
"id": "myError",
"isEnabled": true,
"condition": {
"op": "eq",
"name": "event.exceptions",
"value": "myError"
}
}
]
}
}"#;
let deserialized = serde_json::from_str::<GlobalConfig>(json).unwrap();
let serialized = serde_json::to_string_pretty(&deserialized).unwrap();
assert_eq!(json, serialized.as_str());
}
#[test]
fn test_global_config_invalid_value_is_default() {
let options: Options = serde_json::from_str(
r#"{
"relay.cardinality-limiter.mode": "passive"
}"#,
)
.unwrap();
let expected = Options {
cardinality_limiter_mode: CardinalityLimiterMode::Passive,
..Default::default()
};
assert_eq!(options, expected);
}
#[test]
fn test_cardinality_limiter_mode_de_serialize() {
let m: CardinalityLimiterMode = serde_json::from_str("\"\"").unwrap();
assert_eq!(m, CardinalityLimiterMode::Enabled);
let m: CardinalityLimiterMode = serde_json::from_str("\"enabled\"").unwrap();
assert_eq!(m, CardinalityLimiterMode::Enabled);
let m: CardinalityLimiterMode = serde_json::from_str("\"disabled\"").unwrap();
assert_eq!(m, CardinalityLimiterMode::Disabled);
let m: CardinalityLimiterMode = serde_json::from_str("\"passive\"").unwrap();
assert_eq!(m, CardinalityLimiterMode::Passive);
let m = serde_json::to_string(&CardinalityLimiterMode::Enabled).unwrap();
assert_eq!(m, "\"enabled\"");
}
#[test]
fn test_minimal_serialization() {
let config = r#"{"options":{"foo":"bar"}}"#;
let deserialized: GlobalConfig = serde_json::from_str(config).unwrap();
let serialized = serde_json::to_string(&deserialized).unwrap();
assert_eq!(config, &serialized);
}
#[test]
fn test_metric_bucket_encodings_de_from_str() {
let o: Options = serde_json::from_str(
r#"{
"relay.metric-bucket-set-encodings": "legacy",
"relay.metric-bucket-distribution-encodings": "zstd"
}"#,
)
.unwrap();
assert_eq!(
o.metric_bucket_set_encodings,
BucketEncodings {
transactions: BucketEncoding::Legacy,
spans: BucketEncoding::Legacy,
profiles: BucketEncoding::Legacy,
custom: BucketEncoding::Legacy,
metric_stats: BucketEncoding::Legacy,
}
);
assert_eq!(
o.metric_bucket_dist_encodings,
BucketEncodings {
transactions: BucketEncoding::Zstd,
spans: BucketEncoding::Zstd,
profiles: BucketEncoding::Zstd,
custom: BucketEncoding::Zstd,
metric_stats: BucketEncoding::Zstd,
}
);
}
#[test]
fn test_metric_bucket_encodings_de_from_obj() {
let original = BucketEncodings {
transactions: BucketEncoding::Base64,
spans: BucketEncoding::Zstd,
profiles: BucketEncoding::Base64,
custom: BucketEncoding::Zstd,
metric_stats: BucketEncoding::Base64,
};
let s = serde_json::to_string(&original).unwrap();
let s = format!(
r#"{{
"relay.metric-bucket-set-encodings": {s},
"relay.metric-bucket-distribution-encodings": {s}
}}"#
);
let o: Options = serde_json::from_str(&s).unwrap();
assert_eq!(o.metric_bucket_set_encodings, original);
assert_eq!(o.metric_bucket_dist_encodings, original);
}
}