use std::borrow::Cow;
use std::collections::HashMap;
#[cfg(test)]
use std::time::Duration;
use crate::detect::Confidence;
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 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 redfish: Option<RedfishConfig>,
pub cloud_energy: Option<CloudEnergyConfig>,
pub per_operation_coefficients: bool,
pub include_network_transport: bool,
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,
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 {
fn default() -> Self {
Self {
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,
redfish: None,
cloud_energy: None,
per_operation_coefficients: true,
include_network_transport: false,
network_energy_per_byte_kwh: crate::score::carbon::DEFAULT_NETWORK_ENERGY_PER_BYTE_KWH,
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 carbon_context(&self) -> crate::score::carbon::CarbonContext {
let scoring_config = self
.green
.electricity_maps
.as_ref()
.map(crate::score::carbon::ScoringConfig::from_electricity_maps);
crate::score::carbon::CarbonContext {
default_region: self.green.default_region.clone(),
service_regions: self.green.service_regions.clone(),
embodied_per_request_gco2: self.green.embodied_carbon_per_request_gco2,
use_hourly_profiles: self.green.use_hourly_profiles,
energy_snapshot: None,
per_operation_coefficients: self.green.per_operation_coefficients,
include_network_transport: self.green.include_network_transport,
network_energy_per_byte_kwh: self.green.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,
}
}
}
mod raw;
mod toml_paths;
mod validate;
use raw::{RawConfig, parse_daemon_environment, parse_kepler_metric_kind};
use toml_paths::normalize_toml_path_strings;
pub(crate) use validate::has_control_char;
#[cfg(test)]
use raw::{
CloudSection, ElectricityMapsSection, KeplerSection, RedfishSection, ScaphandreSection,
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)?;
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(ConfigError::Validation(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));
}
}
};
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)?;
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("config validation error: {0}")]
Validation(String),
}
#[cfg(test)]
mod tests;