use config_rs::{builder::BuilderState, ConfigBuilder, ConfigError};
use envconfig::Envconfig;
use tracing::debug;
use crate::config::log::{LogFormat, LogLevel};
#[derive(Default, Envconfig)]
pub struct EnvVarOverrides {
#[envconfig(from = "LOG_LEVEL")]
pub log_level: Option<LogLevel>,
#[envconfig(from = "LOG_FORMAT")]
pub log_format: Option<LogFormat>,
#[envconfig(from = "LOG_FILTER")]
pub log_filter: Option<String>,
#[envconfig(from = "LOG_INTERNALS")]
pub log_internals: Option<bool>,
#[envconfig(from = "LABORATORY_ENABLED")]
pub laboratory_enabled: Option<bool>,
#[envconfig(from = "WEBSOCKET_ENABLED")]
pub websocket_enabled: Option<bool>,
#[envconfig(from = "SUBSCRIPTIONS_ENABLED")]
pub subscriptions_enabled: Option<bool>,
#[envconfig(from = "PORT")]
pub http_port: Option<u64>,
#[envconfig(from = "HOST")]
pub http_host: Option<String>,
#[envconfig(from = "ROUTER_HTTP_WORKERS")]
pub http_workers: Option<usize>,
#[envconfig(from = "SUPERGRAPH_FILE_PATH")]
pub supergraph_file_path: Option<String>,
#[envconfig(from = "HIVE_CDN_ENDPOINT")]
pub hive_console_cdn_endpoint: Option<String>,
#[envconfig(from = "HIVE_CDN_KEY")]
pub hive_console_cdn_key: Option<String>,
#[envconfig(from = "HIVE_CDN_POLL_INTERVAL")]
pub hive_console_cdn_poll_interval: Option<String>,
#[envconfig(from = "APOLLO_KEY")]
pub apollo_key: Option<String>,
#[envconfig(from = "APOLLO_GRAPH_REF")]
pub apollo_graph_ref: Option<String>,
#[envconfig(from = "APOLLO_UPLINK_ENDPOINTS")]
pub apollo_uplink_endpoints: Option<String>,
#[envconfig(from = "HIVE_ACCESS_TOKEN")]
pub hive_access_token: Option<String>,
#[envconfig(from = "HIVE_TARGET")]
pub hive_target: Option<String>,
#[envconfig(from = "HIVE_TRACING_ENABLED")]
pub hive_tracing_enabled: Option<bool>,
#[envconfig(from = "HIVE_USAGE_REPORTING_ENABLED")]
pub hive_usage_reporting_enabled: Option<bool>,
#[envconfig(from = "TELEMETRY_TRACING_SAMPLING_RATE")]
pub tracing_sampling_rate: Option<f64>,
#[envconfig(from = "QUERY_PLANNER_EXPERIMENTAL_ABSTRACT_TYPE_FOLDING")]
pub query_planner_experimental_abstract_type_folding: Option<bool>,
#[envconfig(from = "DISABLE_SUBGRAPH_ERROR_MASKING")]
pub disable_subgraph_error_masking: Option<bool>,
}
#[derive(Debug, thiserror::Error)]
pub enum EnvVarOverridesError {
#[error("Failed to override configuration: {0}")]
FailedToOverrideConfig(#[from] ConfigError),
#[error("Cannot override supergraph source due to conflict: SUPERGRAPH_FILE_PATH, HIVE_CDN_ENDPOINT and APOLLO_KEY cannot be used together")]
ConflictingSupergraphSource,
#[error("Missing required environment variable: {0}")]
MissingRequiredEnvVar(&'static str),
}
const CONFIG_LOGGING_TARGET: &str = "router::config";
impl EnvVarOverrides {
pub fn apply_overrides<T: BuilderState>(
mut self,
mut config: ConfigBuilder<T>,
) -> Result<ConfigBuilder<T>, EnvVarOverridesError> {
if let Some(log_level) = self.log_level.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = ?log_level, "overriding 'log.level'");
config = config.set_override("log.level", log_level.as_str())?;
}
if let Some(log_format) = self.log_format.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = ?log_format, "overriding 'log.format'");
config = config.set_override("log.format", log_format.as_str())?;
}
if let Some(log_filter) = self.log_filter.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = ?log_filter, "overriding 'log.filter'");
config = config.set_override("log.filter", log_filter)?;
}
if let Some(log_internals) = self.log_internals.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = log_internals, "overriding 'log.log_internals'");
config = config.set_override("log.log_internals", log_internals)?;
}
if let Some(http_port) = self.http_port.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = http_port, "overriding 'http.port'");
config = config.set_override("http.port", http_port)?;
}
if let Some(http_host) = self.http_host.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = http_host, "overriding 'http.host'");
config = config.set_override("http.host", http_host)?;
}
if let Some(http_workers) = self.http_workers.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = http_workers, "overriding 'http.workers'");
config = config.set_override("http.workers", http_workers as u64)?;
}
let configured_supergraph_sources = [
self.supergraph_file_path.is_some(),
self.hive_console_cdn_endpoint.is_some(),
self.apollo_key.is_some(),
]
.into_iter()
.filter(|configured| *configured)
.count();
if configured_supergraph_sources > 1 {
return Err(EnvVarOverridesError::ConflictingSupergraphSource);
}
if let Some(supergraph_file_path) = self.supergraph_file_path.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = supergraph_file_path, "overriding 'supergraph.path'");
config = config.set_override("supergraph.source", "file")?;
config = config.set_override("supergraph.path", supergraph_file_path)?;
}
if let Some(hive_console_cdn_endpoint) = self.hive_console_cdn_endpoint.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = hive_console_cdn_endpoint, "overriding 'hive_console_cdn_endpoint'");
config = config.set_override("supergraph.source", "hive")?;
if hive_console_cdn_endpoint.contains(",") {
let endpoints: Vec<String> = hive_console_cdn_endpoint
.split(',')
.map(|s| s.trim().to_string())
.collect();
config = config.set_override("supergraph.endpoint", endpoints)?;
} else {
config = config.set_override("supergraph.endpoint", hive_console_cdn_endpoint)?;
}
if let Some(hive_console_cdn_key) = self.hive_console_cdn_key.take() {
debug!(target: CONFIG_LOGGING_TARGET, "overriding 'hive_console_cdn_key'");
config = config.set_override("supergraph.key", hive_console_cdn_key)?;
} else {
return Err(EnvVarOverridesError::MissingRequiredEnvVar("HIVE_CDN_KEY"));
}
if let Some(hive_console_cdn_poll_interval) = self.hive_console_cdn_poll_interval.take()
{
debug!(target: CONFIG_LOGGING_TARGET, value = hive_console_cdn_poll_interval, "overriding 'hive_console_cdn_poll_interval'");
config = config
.set_override("supergraph.poll_interval", hive_console_cdn_poll_interval)?;
}
}
if let Some(apollo_key) = self.apollo_key.take() {
debug!(target: CONFIG_LOGGING_TARGET, "overriding 'apollo_key'");
config = config.set_override("supergraph.source", "apollo_graphos")?;
config = config.set_override("supergraph.key", apollo_key)?;
if let Some(apollo_graph_ref) = self.apollo_graph_ref.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = apollo_graph_ref, "overriding 'apollo_graph_ref'");
config = config.set_override("supergraph.graph_ref", apollo_graph_ref)?;
} else {
return Err(EnvVarOverridesError::MissingRequiredEnvVar(
"APOLLO_GRAPH_REF",
));
}
if let Some(apollo_uplink_endpoints) = self.apollo_uplink_endpoints.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = apollo_uplink_endpoints, "overriding 'apollo_uplink_endpoints'");
let endpoints: Vec<String> = apollo_uplink_endpoints
.split(',')
.map(|s| s.trim().to_string())
.collect();
config = config.set_override("supergraph.endpoint", endpoints)?;
}
}
if let Some(enabled) = self.hive_tracing_enabled.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = enabled, "overriding 'hive_tracing_enabled'");
config = config.set_override("telemetry.hive.tracing.enabled", enabled)?;
}
if let Some(enabled) = self.hive_usage_reporting_enabled.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = enabled, "overriding 'hive_usage_reporting_enabled'");
config = config.set_override("telemetry.hive.usage_reporting.enabled", enabled)?;
}
if let Some(hive_access_token) = self.hive_access_token.take() {
debug!(target: CONFIG_LOGGING_TARGET, "overriding 'hive_access_token'");
config = config.set_override("telemetry.hive.token", hive_access_token)?;
}
if let Some(hive_target) = self.hive_target.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = hive_target, "overriding 'hive_target'");
config = config.set_override("telemetry.hive.target", hive_target)?;
}
if let Some(tracing_sampling_rate) = self.tracing_sampling_rate.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = tracing_sampling_rate, "overriding 'tracing_sampling_rate'");
config =
config.set_override("telemetry.tracing.collect.sampling", tracing_sampling_rate)?;
}
if let Some(laboratory_enabled) = self.laboratory_enabled.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = laboratory_enabled, "overriding 'laboratory_enabled'");
config = config.set_override("laboratory.enabled", laboratory_enabled)?;
}
if let Some(websocket_enabled) = self.websocket_enabled.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = websocket_enabled, "overriding 'websocket_enabled'");
config = config.set_override("websocket.enabled", websocket_enabled)?;
}
if let Some(subscriptions_enabled) = self.subscriptions_enabled.take() {
debug!(target: CONFIG_LOGGING_TARGET, value = subscriptions_enabled, "overriding 'subscriptions_enabled'");
config = config.set_override("subscriptions.enabled", subscriptions_enabled)?;
}
if let Some(experimental_abstract_type_folding) =
self.query_planner_experimental_abstract_type_folding.take()
{
debug!(target: CONFIG_LOGGING_TARGET, value = experimental_abstract_type_folding, "overriding 'experimental_abstract_type_folding'");
config = config.set_override(
"query_planner.experimental_abstract_type_folding",
experimental_abstract_type_folding,
)?;
}
if let Some(disable_subgraph_error_masking) = self.disable_subgraph_error_masking.take() {
debug!(
"[config-override] 'disable_subgraph_error_masking' = {}",
disable_subgraph_error_masking
);
config =
config.set_override("error_masking.enabled", !disable_subgraph_error_masking)?;
}
Ok(config)
}
}
#[cfg(test)]
mod tests {
use config_rs::{Config, File, FileFormat};
use crate::config::HiveRouterConfig;
use super::*;
fn config_from_overrides(overrides: EnvVarOverrides) -> HiveRouterConfig {
overrides
.apply_overrides(Config::builder())
.unwrap()
.build()
.unwrap()
.try_deserialize::<HiveRouterConfig>()
.unwrap()
}
#[test]
fn tracing_sampling_rate_override_sets_tracing_collect_sampling() {
let config = config_from_overrides(EnvVarOverrides {
tracing_sampling_rate: Some(0.25),
..Default::default()
});
assert_eq!(config.telemetry.tracing.collect.sampling, 0.25);
}
#[test]
fn tracing_sampling_rate_override_wins_over_config_file_value() {
let config = EnvVarOverrides {
tracing_sampling_rate: Some(0.1),
..Default::default()
}
.apply_overrides(Config::builder().add_source(File::from_str(
r#"
telemetry:
tracing:
collect:
sampling: 0.75
"#,
FileFormat::Yaml,
)))
.unwrap()
.build()
.unwrap()
.try_deserialize::<HiveRouterConfig>()
.unwrap();
assert_eq!(config.telemetry.tracing.collect.sampling, 0.1);
}
#[test]
fn query_planner_experimental_abstract_type_folding_override_sets_config() {
let config = config_from_overrides(EnvVarOverrides {
query_planner_experimental_abstract_type_folding: Some(true),
..Default::default()
});
assert!(config.query_planner.experimental_abstract_type_folding);
}
#[test]
fn query_planner_experimental_abstract_type_folding_override_wins_over_config_file_value() {
let config = EnvVarOverrides {
query_planner_experimental_abstract_type_folding: Some(true),
..Default::default()
}
.apply_overrides(Config::builder().add_source(File::from_str(
r#"
query_planner:
experimental_abstract_type_folding: false
"#,
FileFormat::Yaml,
)))
.unwrap()
.build()
.unwrap()
.try_deserialize::<HiveRouterConfig>()
.unwrap();
assert!(config.query_planner.experimental_abstract_type_folding);
}
}