feat(config): add config-rs settings support
Introduce config-rs and Serde support with a nested Settings model. Add config and serde dependencies with toml feature for file-based configuration loading. The Settings struct provides typed configuration with sensible defaults for network, output, observability, and advanced settings sections. Environment variable override via SQMA__ prefix is supported. The legacy Config remains in use; migration follows later.
This commit is contained in:
4 files changed
+327
No files matched your search
Generated
+99
@@ -94,6 +94,18 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "config"
|
||||||
|
version = "0.15.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b85f248a4de22d204ceabc6299d89d2c70fbd7f09fea53c06c852369652d8139"
|
||||||
|
dependencies = [
|
||||||
|
"pathdiff",
|
||||||
|
"serde_core",
|
||||||
|
"toml",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
@@ -284,6 +296,12 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathdiff"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.2.37"
|
version = "0.2.37"
|
||||||
@@ -389,6 +407,45 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@@ -425,6 +482,7 @@ name = "sqm-autorate-rust"
|
|||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"config",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"flume",
|
"flume",
|
||||||
"icmp-socket2",
|
"icmp-socket2",
|
||||||
@@ -434,6 +492,7 @@ dependencies = [
|
|||||||
"netlink-socket2",
|
"netlink-socket2",
|
||||||
"rust-uci",
|
"rust-uci",
|
||||||
"rustix",
|
"rustix",
|
||||||
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -479,6 +538,37 @@ dependencies = [
|
|||||||
"syn 3.0.2",
|
"syn 3.0.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "1.1.4+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "1.1.1+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.1.3+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
||||||
|
dependencies = [
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
@@ -544,3 +634,12 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
@@ -26,6 +26,8 @@ rustix = { version = "1.1", features = ["fs", "system", "thread", "time"] }
|
|||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
flume = { version = "0.12.0", default-features = false, features = ["eventual-fairness", "select"] }
|
flume = { version = "0.12.0", default-features = false, features = ["eventual-fairness", "select"] }
|
||||||
|
config = { version = "0.15.25", default-features = false }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
[dependencies.log]
|
[dependencies.log]
|
||||||
features = ["std"]
|
features = ["std"]
|
||||||
@@ -37,4 +39,6 @@ netlink-socket2 = { version = "0.3.3" }
|
|||||||
rust-uci = { git = "https://github.com/namib-project/rust-uci.git", rev = "3e7b6153b6df92b45428c32bb6461756563deecb", optional = true }
|
rust-uci = { git = "https://github.com/namib-project/rust-uci.git", rev = "3e7b6153b6df92b45428c32bb6461756563deecb", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
default = ["toml"]
|
||||||
|
toml = ["config/toml"]
|
||||||
uci = ["dep:rust-uci"]
|
uci = ["dep:rust-uci"]
|
||||||
@@ -17,6 +17,7 @@ mod pinger_icmp_ts;
|
|||||||
mod platform;
|
mod platform;
|
||||||
mod ratecontroller;
|
mod ratecontroller;
|
||||||
mod reflector_selector;
|
mod reflector_selector;
|
||||||
|
mod settings;
|
||||||
mod time;
|
mod time;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use anyhow::{Context, bail};
|
||||||
|
use config::{Config, ConfigError, Environment};
|
||||||
|
use log::Level;
|
||||||
|
use serde::{Deserialize, Deserializer, de};
|
||||||
|
use std::fmt::{self, Display};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum MeasurementType {
|
||||||
|
Icmp = 1,
|
||||||
|
IcmpTimestamps,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for MeasurementType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
MeasurementType::Icmp => write!(f, "icmp"),
|
||||||
|
MeasurementType::IcmpTimestamps => write!(f, "icmp-timestamps"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for MeasurementType {
|
||||||
|
type Err = ConfigError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"icmp" => Ok(MeasurementType::Icmp),
|
||||||
|
"icmp-timestamps" | "icmp_timestamps" => Ok(MeasurementType::IcmpTimestamps),
|
||||||
|
_ => Err(ConfigError::Message(format!(
|
||||||
|
"invalid measurement type: {s}"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum ObservabilityProtocol {
|
||||||
|
Udp,
|
||||||
|
Tcp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ObservabilityProtocol {
|
||||||
|
type Err = ConfigError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"udp" => Ok(ObservabilityProtocol::Udp),
|
||||||
|
"tcp" => Ok(ObservabilityProtocol::Tcp),
|
||||||
|
_ => Err(ConfigError::Message(format!(
|
||||||
|
"invalid observability protocol: {s}",
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub(crate) struct Settings {
|
||||||
|
pub(crate) network: NetworkSettings,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) output: OutputSettings,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) observability: ObservabilitySettings,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) advanced_settings: AdvancedSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings {
|
||||||
|
pub(crate) fn load() -> Result<Self, ConfigError> {
|
||||||
|
let builder = Config::builder();
|
||||||
|
|
||||||
|
#[cfg(feature = "toml")]
|
||||||
|
let builder = builder.add_source(
|
||||||
|
config::File::with_name("/etc/sqm-autorate-rust/config.toml").required(false),
|
||||||
|
);
|
||||||
|
|
||||||
|
let builder = builder.add_source(Environment::with_prefix("SQMA").separator("__"));
|
||||||
|
let built_config = builder.build()?;
|
||||||
|
|
||||||
|
built_config.try_deserialize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub(crate) struct NetworkSettings {
|
||||||
|
pub(crate) download_interface: String,
|
||||||
|
pub(crate) upload_interface: String,
|
||||||
|
pub(crate) download_base_kbits: f64,
|
||||||
|
#[serde(default = "default_min_percent")]
|
||||||
|
pub(crate) download_min_percent: f64,
|
||||||
|
pub(crate) upload_base_kbits: f64,
|
||||||
|
#[serde(default = "default_min_percent")]
|
||||||
|
pub(crate) upload_min_percent: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkSettings {
|
||||||
|
pub(crate) fn download_min_kbits(&self) -> f64 {
|
||||||
|
(self.download_base_kbits * self.download_min_percent / 100.0).floor()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn upload_min_kbits(&self) -> f64 {
|
||||||
|
(self.upload_base_kbits * self.upload_min_percent / 100.0).floor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) struct OutputSettings {
|
||||||
|
#[serde(deserialize_with = "deserialize_from_str")]
|
||||||
|
pub(crate) log_level: Level,
|
||||||
|
pub(crate) speed_hist_file: String,
|
||||||
|
pub(crate) stats_file: String,
|
||||||
|
pub(crate) suppress_statistics: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OutputSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
log_level: Level::Error,
|
||||||
|
speed_hist_file: "/tmp/sqm-speedhist.csv".to_owned(),
|
||||||
|
stats_file: "/tmp/sqm-autorate-rust.csv".to_owned(),
|
||||||
|
suppress_statistics: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) struct ObservabilitySettings {
|
||||||
|
pub(crate) enabled: bool,
|
||||||
|
#[serde(deserialize_with = "deserialize_from_str")]
|
||||||
|
pub(crate) protocol: ObservabilityProtocol,
|
||||||
|
pub(crate) host: Option<String>,
|
||||||
|
pub(crate) port: u16,
|
||||||
|
pub(crate) batch_size: usize,
|
||||||
|
pub(crate) batch_timeout_ms: u64,
|
||||||
|
pub(crate) export_ping_metrics: bool,
|
||||||
|
pub(crate) export_rate_metrics: bool,
|
||||||
|
pub(crate) export_baseline_metrics: bool,
|
||||||
|
pub(crate) export_events: bool,
|
||||||
|
pub(crate) host_tag: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ObservabilitySettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
protocol: ObservabilityProtocol::Udp,
|
||||||
|
host: None,
|
||||||
|
port: 8089,
|
||||||
|
batch_size: 25,
|
||||||
|
batch_timeout_ms: 100,
|
||||||
|
export_ping_metrics: false,
|
||||||
|
export_rate_metrics: true,
|
||||||
|
export_baseline_metrics: false,
|
||||||
|
export_events: true,
|
||||||
|
host_tag: default_host_tag(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) struct AdvancedSettings {
|
||||||
|
pub(crate) download_delay_ms: f64,
|
||||||
|
pub(crate) high_load_level: f64,
|
||||||
|
pub(crate) min_change_interval: f64,
|
||||||
|
#[serde(deserialize_with = "deserialize_from_str")]
|
||||||
|
pub(crate) measurement_type: MeasurementType,
|
||||||
|
pub(crate) num_reflectors: u8,
|
||||||
|
pub(crate) peer_reselection_time: u64,
|
||||||
|
pub(crate) reflector_list_file: String,
|
||||||
|
pub(crate) speed_hist_size: u32,
|
||||||
|
pub(crate) tick_interval: f64,
|
||||||
|
pub(crate) upload_delay_ms: f64,
|
||||||
|
pub(crate) dry_run: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AdvancedSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
download_delay_ms: 15.0,
|
||||||
|
high_load_level: 0.8,
|
||||||
|
min_change_interval: 0.5,
|
||||||
|
measurement_type: MeasurementType::IcmpTimestamps,
|
||||||
|
num_reflectors: 5,
|
||||||
|
peer_reselection_time: 15,
|
||||||
|
reflector_list_file: "/etc/sqm-autorate/reflectors-icmp.csv".to_owned(),
|
||||||
|
speed_hist_size: 100,
|
||||||
|
tick_interval: 0.5,
|
||||||
|
upload_delay_ms: 15.0,
|
||||||
|
dry_run: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_min_percent() -> f64 {
|
||||||
|
20.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_host_tag() -> String {
|
||||||
|
rustix::system::uname()
|
||||||
|
.nodename()
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_from_str<'de, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
T: FromStr,
|
||||||
|
T::Err: Display,
|
||||||
|
{
|
||||||
|
let value = String::deserialize(deserializer)?;
|
||||||
|
value.parse().map_err(de::Error::custom)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user