use std::borrow::Cow;
use std::collections::HashMap;
#[cfg(test)]
use std::time::Duration;
use crate::detect::Confidence;
use crate::score::alumet::AlumetConfig;
use crate::score::carbon::DEFAULT_EMBODIED_CARBON_PER_REQUEST_GCO2;
use crate::score::cloud_energy::config::CloudEnergyConfig;
use crate::score::kepler::KeplerConfig;
use crate::score::redfish::RedfishConfig;
#[cfg(test)]
use crate::score::redfish::RedfishEndpoint;
use crate::score::scaphandre::ScaphandreConfig;
#[derive(Debug, Clone, Default)]
pub struct Config {
pub thresholds: ThresholdsConfig,
pub detection: DetectionConfig,
pub green: GreenConfig,
pub daemon: DaemonConfig,
pub reporting: ReportingConfig,
}
#[derive(Debug, Clone, Default)]
pub struct ReportingConfig {
pub intent: Option<String>,
pub confidentiality_level: Option<String>,
pub org_config_path: Option<String>,
pub disclose_output_path: Option<String>,
pub disclose_period: Option<String>,
pub sigstore: SigstoreConfig,
}
#[derive(Debug, Clone)]
pub struct SigstoreConfig {
pub rekor_url: String,
pub fulcio_url: String,
}
impl Default for SigstoreConfig {
fn default() -> Self {
Self {
rekor_url: DEFAULT_REKOR_URL.to_string(),
fulcio_url: DEFAULT_FULCIO_URL.to_string(),
}
}
}
pub const DEFAULT_REKOR_URL: &str = "https://rekor.sigstore.dev";
pub const DEFAULT_FULCIO_URL: &str = "https://fulcio.sigstore.dev";
const RESERVED_DISCLOSE_OUTPUT_PATH_VERSION: &str = "0.8.0";
#[derive(Debug, Clone)]
pub struct DaemonArchiveConfig {
pub path: String,
pub max_size_mb: u64,
pub max_files: u32,
}
impl Default for DaemonArchiveConfig {
fn default() -> Self {
Self {
path: String::new(),
max_size_mb: 100,
max_files: 12,
}
}
}
#[derive(Debug, Clone)]
pub struct ThresholdsConfig {
pub n_plus_one_sql_critical_max: u32,
pub n_plus_one_http_warning_max: u32,
pub n_plus_one_messaging_warning_max: u32,
pub io_waste_ratio_max: f64,
}
#[derive(Debug, Clone)]
pub struct DetectionConfig {
pub n_plus_one_threshold: u32,
pub window_duration_ms: u64,
pub slow_query_threshold_ms: u64,
pub slow_query_min_occurrences: u32,
pub max_fanout: u32,
pub chatty_service_min_calls: u32,
pub pool_saturation_concurrent_threshold: u32,
pub serialized_min_sequential: u32,
pub sanitizer_aware_classification: crate::detect::sanitizer_aware::SanitizerAwareMode,
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] pub struct GreenConfig {
pub enabled: bool,
pub default_region: Option<String>,
pub service_regions: HashMap<String, String>,
pub embodied_carbon_per_request_gco2: f64,
pub use_hourly_profiles: bool,
pub scaphandre: Option<ScaphandreConfig>,
pub kepler: Option<KeplerConfig>,
pub alumet: Option<AlumetConfig>,
pub redfish: Option<RedfishConfig>,
pub cloud_energy: Option<CloudEnergyConfig>,
pub broker_static: Option<crate::score::broker_static::StaticBrokerConfig>,
pub per_operation_coefficients: bool,
#[deprecated(
since = "0.9.25",
note = "the transport term is always shown; this value has no effect"
)]
pub include_network_transport: bool,
#[deprecated(
since = "0.9.25",
note = "the transport coefficient is fixed; this value has no effect"
)]
pub network_energy_per_byte_kwh: f64,
pub hourly_profiles_file: Option<String>,
pub custom_hourly_profiles:
Option<std::sync::Arc<HashMap<String, crate::score::carbon::HourlyProfile>>>,
pub calibration_file: Option<String>,
pub calibration: Option<crate::calibrate::CalibrationData>,
pub electricity_maps: Option<crate::score::electricity_maps::ElectricityMapsConfig>,
}
#[derive(Debug, Clone)]
pub struct DaemonConfig {
pub listen_addr: String,
pub listen_port: u16,
pub listen_port_grpc: u16,
pub json_socket: String,
pub max_active_traces: usize,
pub trace_ttl_ms: u64,
pub sampling_rate: f64,
pub max_events_per_trace: usize,
pub max_payload_size: usize,
pub environment: DaemonEnvironment,
pub max_retained_findings: usize,
pub ingest_queue_capacity: usize,
pub analysis_queue_capacity: usize,
pub memory_high_water_pct: u8,
pub api_enabled: bool,
pub tls: DaemonTlsConfig,
pub ack: DaemonAckConfig,
pub cors: DaemonCorsConfig,
pub correlation: crate::detect::correlate_cross::CorrelationConfig,
pub archive: Option<DaemonArchiveConfig>,
}
#[derive(Debug, Clone, Default)]
pub struct DaemonTlsConfig {
pub cert_path: Option<String>,
pub key_path: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DaemonAckConfig {
pub enabled: bool,
pub storage_path: Option<String>,
pub api_key: Option<String>,
pub toml_path: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct DaemonCorsConfig {
pub allowed_origins: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DaemonEnvironment {
#[default]
Staging,
Production,
}
impl DaemonEnvironment {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Staging => "staging",
Self::Production => "production",
}
}
}
impl Default for ThresholdsConfig {
fn default() -> Self {
Self {
n_plus_one_sql_critical_max: 0,
n_plus_one_http_warning_max: 3,
n_plus_one_messaging_warning_max: 3,
io_waste_ratio_max: 0.30,
}
}
}
impl Default for DetectionConfig {
fn default() -> Self {
Self {
n_plus_one_threshold: 5,
window_duration_ms: 500,
slow_query_threshold_ms: 500,
slow_query_min_occurrences: 3,
max_fanout: 20,
chatty_service_min_calls: 15,
pool_saturation_concurrent_threshold: 10,
serialized_min_sequential: 3,
sanitizer_aware_classification:
crate::detect::sanitizer_aware::SanitizerAwareMode::default(),
}
}
}
impl Default for GreenConfig {
#[allow(deprecated)]
fn default() -> Self {
Self {
include_network_transport: true,
network_energy_per_byte_kwh: crate::score::carbon::DEFAULT_NETWORK_ENERGY_PER_BYTE_KWH,
enabled: true,
default_region: None,
service_regions: HashMap::new(),
embodied_carbon_per_request_gco2: DEFAULT_EMBODIED_CARBON_PER_REQUEST_GCO2,
use_hourly_profiles: true,
scaphandre: None,
kepler: None,
alumet: None,
redfish: None,
cloud_energy: None,
broker_static: None,
per_operation_coefficients: true,
hourly_profiles_file: None,
custom_hourly_profiles: None,
calibration_file: None,
calibration: None,
electricity_maps: None,
}
}
}
impl Default for DaemonConfig {
fn default() -> Self {
Self {
listen_addr: "127.0.0.1".to_string(),
listen_port: 4318,
listen_port_grpc: 4317,
json_socket: "/tmp/perf-sentinel.sock".to_string(),
max_active_traces: 10_000,
trace_ttl_ms: 30_000,
sampling_rate: 1.0,
max_events_per_trace: 1_000,
max_payload_size: 16 * 1024 * 1024,
environment: DaemonEnvironment::Staging,
max_retained_findings: 10_000,
ingest_queue_capacity: 1024,
analysis_queue_capacity: 1024,
memory_high_water_pct: 0,
api_enabled: true,
tls: DaemonTlsConfig::default(),
ack: DaemonAckConfig::default(),
cors: DaemonCorsConfig::default(),
correlation: crate::detect::correlate_cross::CorrelationConfig::default(),
archive: None,
}
}
}
impl Default for DaemonAckConfig {
fn default() -> Self {
Self {
enabled: true,
storage_path: None,
api_key: None,
toml_path: None,
}
}
}
impl Config {
#[must_use]
pub const fn confidence(&self) -> Confidence {
match self.daemon.environment {
DaemonEnvironment::Staging => Confidence::DaemonStaging,
DaemonEnvironment::Production => Confidence::DaemonProduction,
}
}
#[must_use]
pub fn effective_embodied_per_request_gco2(&self) -> f64 {
let declared = self.green.embodied_carbon_per_request_gco2;
if declared > 0.0 {
declared
} else {
DEFAULT_EMBODIED_CARBON_PER_REQUEST_GCO2
}
}
#[must_use]
#[allow(deprecated)] pub fn carbon_context(&self) -> crate::score::carbon::CarbonContext {
let scoring_config = self.green.enabled.then(|| self.scoring_config());
crate::score::carbon::CarbonContext {
include_network_transport: true,
default_region: self.green.default_region.clone(),
service_regions: self.green.service_regions.clone(),
embodied_per_request_gco2: self.effective_embodied_per_request_gco2(),
use_hourly_profiles: self.green.use_hourly_profiles,
energy_snapshot: None,
per_operation_coefficients: self.green.per_operation_coefficients,
network_energy_per_byte_kwh: crate::score::carbon::DEFAULT_NETWORK_ENERGY_PER_BYTE_KWH,
custom_hourly_profiles: self.green.custom_hourly_profiles.clone(),
calibration: self.green.calibration.clone(),
real_time_intensity: None, scoring_config,
db_energy: None,
broker_energy: None,
}
}
#[must_use]
pub fn scoring_config(&self) -> crate::score::carbon::ScoringConfig {
let mut scoring_config = self.green.electricity_maps.as_ref().map_or_else(
|| crate::score::carbon::ScoringConfig {
electricity_maps: Some(false),
..Default::default()
},
crate::score::carbon::ScoringConfig::from_electricity_maps,
);
scoring_config.embodied_per_request_gco2 = Some(self.effective_embodied_per_request_gco2());
scoring_config.network_energy_per_byte_kwh =
Some(crate::score::carbon::DEFAULT_NETWORK_ENERGY_PER_BYTE_KWH);
scoring_config.per_operation_coefficients = Some(self.green.per_operation_coefficients);
scoring_config.use_hourly_profiles = Some(self.green.use_hourly_profiles);
scoring_config
}
}
mod raw;
mod toml_paths;
mod validate;
use raw::{
RawConfig, parse_daemon_environment, parse_kepler_metric_kind, validate_alumet_raw,
validate_broker_static_raw,
};
use toml_paths::normalize_toml_path_strings;
pub(crate) use validate::has_control_char;
#[cfg(test)]
use raw::{
AlumetDatabaseSection, AlumetSection, CloudSection, ElectricityMapsSection, KeplerSection,
RedfishSection, ScaphandreSection, convert_alumet_section_with_env,
convert_cloud_section_with_env, convert_electricity_maps_section_with_env,
convert_kepler_section_with_env, convert_redfish_section_with_env,
convert_scaphandre_section_with_env,
};
#[cfg(test)]
use toml_paths::{TOML_PATH_STRING_KEYS, find_basic_string_end};
#[cfg(test)]
use validate::validate_http_authority;
const REMOVED_LEGACY_TOP_LEVEL_KEYS: &[(&str, &str)] = &[
(
"n_plus_one_threshold",
"[detection] n_plus_one_min_occurrences",
),
("window_duration_ms", "[detection] window_duration_ms"),
("listen_addr", "[daemon] listen_address"),
("listen_port", "[daemon] listen_port_http"),
("max_active_traces", "[daemon] max_active_traces"),
("trace_ttl_ms", "[daemon] trace_ttl_ms"),
("max_events_per_trace", "[daemon] max_events_per_trace"),
("max_payload_size", "[daemon] max_payload_size"),
];
fn reject_legacy_top_level_keys(content: &str) -> Result<(), ConfigError> {
let value: toml::Value = toml::from_str(content).map_err(ConfigError::Parse)?;
reject_legacy_top_level_value(&value).map_err(ConfigError::Validation)
}
fn reject_legacy_top_level_value(value: &toml::Value) -> Result<(), String> {
let toml::Value::Table(table) = value else {
return Ok(());
};
for (legacy, replacement) in REMOVED_LEGACY_TOP_LEVEL_KEYS {
if table.contains_key(*legacy) {
return Err(format!(
"config: top-level '{legacy}' was removed in 0.6.0; \
use '{replacement}' instead. \
See the 0.6.0 migration notes for the full list of renamed keys."
));
}
}
Ok(())
}
pub fn load_from_str(content: &str) -> Result<Config, ConfigError> {
let normalized = normalize_toml_path_strings(content);
reject_legacy_top_level_keys(normalized.as_ref())?;
let raw: RawConfig = match toml::from_str(normalized.as_ref()) {
Ok(raw) => raw,
Err(norm_err) => {
if matches!(normalized, Cow::Owned(_)) {
tracing::debug!(
normalized_error = %norm_err,
"path normalization produced invalid TOML; retrying with original input"
);
toml::from_str(content).map_err(ConfigError::Parse)?
} else {
return Err(ConfigError::Parse(norm_err));
}
}
};
validate_raw_config(raw)
}
pub fn load_from_fragments(fragments: &[(&str, &str)]) -> Result<Config, ConfigError> {
let mut merged = toml::Value::Table(toml::Table::new());
let mut origins = HashMap::new();
for (name, content) in fragments {
let normalized = normalize_toml_path_strings(content);
let value = match toml::from_str(normalized.as_ref()) {
Ok(value) => value,
Err(normalized_error) if matches!(normalized, Cow::Owned(_)) => {
tracing::debug!(
%normalized_error,
fragment = *name,
"path normalization produced invalid TOML; retrying with original input"
);
toml::from_str(content).map_err(|source| ConfigError::FragmentParse {
name: (*name).to_string(),
source,
})?
}
Err(source) => {
return Err(ConfigError::FragmentParse {
name: (*name).to_string(),
source,
});
}
};
reject_legacy_top_level_value(&value).map_err(|message| {
ConfigError::FragmentValidation {
name: (*name).to_string(),
message,
}
})?;
merge_toml_value(&mut merged, value, "", name, &mut origins);
}
let raw: RawConfig = serde_path_to_error::deserialize(merged).map_err(|error| {
let path = error.path().to_string();
let name = origin_for_path(&origins, &path)
.or_else(|| unique_origin_for_subtree(&origins, &path))
.unwrap_or("merged configuration")
.to_string();
ConfigError::FragmentParse {
name,
source: error.into_inner(),
}
})?;
validate_raw_config(raw).map_err(|error| match error {
ConfigError::Validation(message) => ConfigError::FragmentValidation {
name: origin_for_validation(&origins, &message)
.unwrap_or("merged configuration")
.to_string(),
message,
},
other => other,
})
}
fn merge_toml_value(
base: &mut toml::Value,
overlay: toml::Value,
path: &str,
fragment: &str,
origins: &mut HashMap<String, String>,
) {
match (base, overlay) {
(toml::Value::Table(base), toml::Value::Table(overlay)) => {
for (key, value) in overlay {
let child = if path.is_empty() {
key.clone()
} else {
format!("{path}.{key}")
};
if let Some(existing) = base.get_mut(&key) {
merge_toml_value(existing, value, &child, fragment, origins);
} else {
record_toml_origins(&value, &child, fragment, origins);
base.insert(key, value);
}
}
}
(base, overlay) => {
if matches!(base, toml::Value::Table(_)) {
let prefix = format!("{path}.");
origins.retain(|key, _| key != path && !key.starts_with(&prefix));
} else {
origins.remove(path);
}
record_toml_origins(&overlay, path, fragment, origins);
*base = overlay;
}
}
}
fn record_toml_origins(
value: &toml::Value,
path: &str,
fragment: &str,
origins: &mut HashMap<String, String>,
) {
if let toml::Value::Table(table) = value {
for (key, value) in table {
let child = if path.is_empty() {
key.clone()
} else {
format!("{path}.{key}")
};
record_toml_origins(value, &child, fragment, origins);
}
} else if !path.is_empty() {
origins.insert(path.to_string(), fragment.to_string());
}
}
fn origin_for_path<'a>(origins: &'a HashMap<String, String>, path: &str) -> Option<&'a str> {
origins
.iter()
.filter(|(candidate, _)| {
path == candidate.as_str()
|| path
.strip_prefix(candidate.as_str())
.is_some_and(|suffix| suffix.starts_with('.') || suffix.starts_with('['))
})
.max_by_key(|(candidate, _)| candidate.len())
.map(|(_, fragment)| fragment.as_str())
}
fn origin_for_sectioned_message<'a>(
origins: &'a HashMap<String, String>,
section: &str,
detail: &str,
) -> Option<&'a str> {
let mut matched = None;
for field in detail.split(|character: char| {
!(character.is_ascii_lowercase() || character.is_ascii_digit() || character == '_')
}) {
if field.is_empty() {
continue;
}
if let Some(fragment) = unique_origin_for_subtree(origins, &format!("{section}.{field}")) {
if matched.is_some_and(|existing| existing != fragment) {
return None;
}
matched = Some(fragment);
}
}
matched.or_else(|| unique_origin_for_subtree(origins, section))
}
fn origin_for_validation<'a>(
origins: &'a HashMap<String, String>,
message: &str,
) -> Option<&'a str> {
if let Some(rest) = message.strip_prefix('[')
&& let Some((section, detail)) = rest.split_once(']')
{
return origin_for_sectioned_message(origins, section, detail);
}
let field = first_config_identifier(message)?;
let field = match field {
"n_plus_one_threshold" => "n_plus_one_min_occurrences",
other => other,
};
let suffix = format!(".{field}");
let mut matching = origins
.iter()
.filter(|(path, _)| path == &field || path.ends_with(&suffix))
.map(|(_, fragment)| fragment.as_str());
let first = matching.next()?;
matching.all(|fragment| fragment == first).then_some(first)
}
fn unique_origin_for_subtree<'a>(
origins: &'a HashMap<String, String>,
path: &str,
) -> Option<&'a str> {
let prefix = format!("{path}.");
let mut leaves = origins
.iter()
.filter(|(candidate, _)| candidate.as_str() == path || candidate.starts_with(&prefix))
.map(|(_, fragment)| fragment.as_str());
let first = leaves.next()?;
leaves.all(|fragment| fragment == first).then_some(first)
}
fn first_config_identifier(input: &str) -> Option<&str> {
let identifier = input
.trim_start()
.split(|character: char| {
!(character.is_ascii_lowercase() || character.is_ascii_digit() || character == '_')
})
.next()?;
(!identifier.is_empty()).then_some(identifier)
}
fn validate_raw_config(raw: RawConfig) -> Result<Config, ConfigError> {
if let Some(env_str) = raw.daemon.environment.as_deref()
&& parse_daemon_environment(env_str).is_none()
{
return Err(ConfigError::Validation(format!(
"[daemon] environment '{env_str}' is invalid; \
expected 'staging' or 'production' (case-insensitive)"
)));
}
parse_kepler_metric_kind(raw.green.kepler.metric_kind.as_deref())
.map_err(ConfigError::Validation)?;
validate_alumet_raw(&raw.green.alumet).map_err(ConfigError::Validation)?;
validate_broker_static_raw(&raw.green.broker_static).map_err(ConfigError::Validation)?;
let config = Config::from(raw);
config.validate().map_err(ConfigError::Validation)?;
config.warn_listen_addr_if_non_loopback();
config.warn_reporting_advisory();
Ok(config)
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ConfigError {
#[error("config parse error: {0}")]
Parse(#[from] toml::de::Error),
#[error("in {name}: {source}")]
FragmentParse {
name: String,
#[source]
source: toml::de::Error,
},
#[error("in {name}: {message}")]
FragmentValidation { name: String, message: String },
#[error("config validation error: {0}")]
Validation(String),
}
#[cfg(test)]
mod tests;