Skip to main content

zenith_foundation/
config.rs

1//! Zenith 配置系统
2//!
3//! 基于 TOML 的统一配置管理,支持:
4//! - 从文件加载配置
5//! - 序列化/反序列化
6//! - 运行时热更新
7//! - 配置校验
8
9use serde::{Deserialize, Serialize};
10use std::fs;
11use std::path::Path;
12use std::str::FromStr;
13use thiserror::Error;
14
15/// 配置加载与校验过程中可能发生的错误。
16#[derive(Debug, Error)]
17pub enum ConfigError {
18    /// 读取配置文件时的 I/O 错误。
19    #[error("IO error: {0}")]
20    Io(#[from] std::io::Error),
21
22    /// TOML 反序列化错误。
23    #[error("TOML parse error: {0}")]
24    Toml(#[from] toml::de::Error),
25
26    /// TOML 序列化错误。
27    #[error("TOML serialize error: {0}")]
28    Serialize(#[from] toml::ser::Error),
29
30    /// 配置值未通过业务校验。
31    #[error("Validation error: {0}")]
32    Validation(String),
33
34    /// 指定的配置文件未找到。
35    #[error("Config not found: {0}")]
36    NotFound(String),
37}
38
39/// Zenith 框架的顶层配置根,聚合全部子配置段。
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[derive(Default)]
42pub struct ZenithConfig {
43    /// 服务器监听与连接相关配置。
44    pub server: ServerConfig,
45    /// 运行时线程模型与加速特性配置。
46    pub runtime: RuntimeConfig,
47    /// 缓存分片与淘汰策略配置。
48    pub cache: CacheConfig,
49    /// 安全(TLS、常量时间、eBPF 完整性)配置。
50    pub security: SecurityConfig,
51    /// 可观测性(指标、追踪、日志)配置。
52    pub observability: ObservabilityConfig,
53}
54
55
56impl ZenithConfig {
57    /// 从指定文件路径加载并校验配置。
58    ///
59    /// # Errors
60    /// - 文件不存在(`ErrorKind::NotFound`)→ [`ConfigError::NotFound`]
61    /// - 其他文件读取失败 → [`ConfigError::Io`]
62    /// - TOML 解析失败 → [`ConfigError::Toml`]
63    /// - 业务校验失败 → [`ConfigError::Validation`]
64    pub fn from_file(path: &Path) -> Result<Self, ConfigError> {
65        // 文件不存在单独映射为 NotFound,与权限不足等其他 I/O 错误区分
66        let content = fs::read_to_string(path).map_err(|e| {
67            if e.kind() == std::io::ErrorKind::NotFound {
68                ConfigError::NotFound(path.display().to_string())
69            } else {
70                ConfigError::Io(e)
71            }
72        })?;
73        let config: ZenithConfig = toml::from_str(&content)?;
74        config.validate()?;
75        Ok(config)
76    }
77
78    /// 将当前配置序列化为美观格式的 TOML 字符串。
79    ///
80    /// # Errors
81    /// 当 TOML 序列化失败时返回 [`ConfigError::Serialize`]。
82    pub fn to_toml(&self) -> Result<String, ConfigError> {
83        toml::to_string_pretty(self).map_err(ConfigError::from)
84    }
85
86    /// 将当前配置写入指定文件路径(覆盖写入)。
87    ///
88    /// # Errors
89    /// 当序列化或文件写入失败时返回 [`ConfigError`]。
90    pub fn save_to_file(&self, path: &Path) -> Result<(), ConfigError> {
91        let content = self.to_toml()?;
92        fs::write(path, content)?;
93        Ok(())
94    }
95
96    /// 递归校验全部子配置段的业务约束。
97    ///
98    /// # Errors
99    /// 当任一子配置段未通过校验时返回对应的 [`ConfigError::Validation`]。
100    pub fn validate(&self) -> Result<(), ConfigError> {
101        self.server.validate()?;
102        self.runtime.validate()?;
103        self.cache.validate()?;
104        self.security.validate()?;
105        self.observability.validate()?;
106        Ok(())
107    }
108}
109
110/// 通过 [`std::str::FromStr`] 从 TOML 字符串加载配置,避免与标准 trait 命名冲突。
111impl FromStr for ZenithConfig {
112    type Err = ConfigError;
113
114    fn from_str(s: &str) -> Result<Self, ConfigError> {
115        let config: ZenithConfig = toml::from_str(s)?;
116        config.validate()?;
117        Ok(config)
118    }
119}
120
121/// 服务器监听与连接生命周期配置。
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct ServerConfig {
124    /// 监听地址(`ip:port`)。
125    pub listen_addr: String,
126    /// 最大并发连接数上限。
127    pub max_connections: u32,
128    /// 连接建立后的空闲超时(毫秒)。
129    pub connection_timeout_ms: u64,
130    /// Keepalive 探测间隔(毫秒)。
131    pub keepalive_interval_ms: u64,
132    /// `/metrics` 端点认证令牌(Bearer Token)。
133    ///
134    /// - `None`:`/metrics` 端点不存在(返回 404),防止未显式配置时暴露观测数据。
135    /// - `Some(token)`:请求须携带 `Authorization: Bearer {token}` 头(恒定时间比较),
136    ///   不匹配返回 401。
137    pub metrics_auth_token: Option<String>,
138}
139
140impl Default for ServerConfig {
141    fn default() -> Self {
142        Self {
143            listen_addr: "0.0.0.0:8080".to_string(),
144            max_connections: 65536,
145            connection_timeout_ms: 5000,
146            keepalive_interval_ms: 30000,
147            metrics_auth_token: None,
148        }
149    }
150}
151
152impl ServerConfig {
153    /// 校验服务器配置的业务约束。
154    ///
155    /// # Errors
156    /// 当 `max_connections`、`connection_timeout_ms` 或 `keepalive_interval_ms` 为 0 时返回错误。
157    pub fn validate(&self) -> Result<(), ConfigError> {
158        if self.max_connections == 0 {
159            return Err(ConfigError::Validation("max_connections must be > 0".into()));
160        }
161        if self.connection_timeout_ms == 0 {
162            return Err(ConfigError::Validation("connection_timeout_ms must be > 0".into()));
163        }
164        if self.keepalive_interval_ms == 0 {
165            return Err(ConfigError::Validation("keepalive_interval_ms must be > 0".into()));
166        }
167        Ok(())
168    }
169}
170
171/// 运行时线程模型与内核加速特性配置。
172///
173/// 全标量字段(`usize` × 4 + `bool` × 3),按 AGENT.md §6.1.3 实现 `Copy`,
174/// 避免热路径上的 `Clone` 开销。
175#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
176pub struct RuntimeConfig {
177    /// 业务工作线程数。
178    pub worker_threads: usize,
179    /// I/O 轮询线程数。
180    pub io_threads: usize,
181    /// 定时器轮询线程数。
182    pub time_threads: usize,
183    /// 阻塞任务线程数。
184    pub blocking_threads: usize,
185    /// 是否启用 eBPF 加速。
186    pub enable_ebpf: bool,
187    /// 是否启用 XDP/AF_XDP 零拷贝。
188    pub enable_xsk: bool,
189    /// 是否启用 NUMA 感知内存分配。
190    pub numa_aware: bool,
191}
192
193impl Default for RuntimeConfig {
194    fn default() -> Self {
195        Self {
196            worker_threads: 4,
197            io_threads: 2,
198            time_threads: 1,
199            blocking_threads: 4,
200            enable_ebpf: true,
201            enable_xsk: true,
202            numa_aware: true,
203        }
204    }
205}
206
207impl RuntimeConfig {
208    /// 校验运行时配置的业务约束。
209    ///
210    /// # Errors
211    /// 当 `worker_threads`、`io_threads`、`time_threads` 或 `blocking_threads` 为 0 时返回错误。
212    pub fn validate(&self) -> Result<(), ConfigError> {
213        if self.worker_threads == 0 {
214            return Err(ConfigError::Validation("worker_threads must be > 0".into()));
215        }
216        if self.io_threads == 0 {
217            return Err(ConfigError::Validation("io_threads must be > 0".into()));
218        }
219        if self.time_threads == 0 {
220            return Err(ConfigError::Validation("time_threads must be > 0".into()));
221        }
222        if self.blocking_threads == 0 {
223            return Err(ConfigError::Validation("blocking_threads must be > 0".into()));
224        }
225        Ok(())
226    }
227}
228
229/// 缓存分片与淘汰策略配置。
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct CacheConfig {
232    /// 分片数量(必须为 2 的幂,用于位掩码取模)。
233    pub shard_count: usize,
234    /// 每个分片的容量上限(条目数)。
235    pub per_shard_capacity: usize,
236    /// 单个缓存条目的最大字节数。
237    pub max_entry_size: usize,
238    /// 淘汰策略名称(白名单:`"lru"` / `"fifo"` / `"random"`)。
239    pub eviction_policy: String,
240    /// 默认 TTL(秒)。
241    pub ttl_seconds: u64,
242}
243
244impl Default for CacheConfig {
245    fn default() -> Self {
246        Self {
247            shard_count: 16,
248            per_shard_capacity: 65536,
249            max_entry_size: 1048576,
250            eviction_policy: "lru".to_string(),
251            ttl_seconds: 3600,
252        }
253    }
254}
255
256impl CacheConfig {
257    /// 校验缓存配置的业务约束。
258    ///
259    /// # Errors
260    /// 以下任一不满足时返回 [`ConfigError::Validation`]:
261    /// - `shard_count` 非 2 的幂
262    /// - `per_shard_capacity` 为 0
263    /// - `max_entry_size` 为 0
264    /// - `eviction_policy` 不在白名单 `"lru" | "fifo" | "random"` 内
265    /// - `ttl_seconds` 为 0
266    pub fn validate(&self) -> Result<(), ConfigError> {
267        if self.shard_count == 0 || !self.shard_count.is_power_of_two() {
268            return Err(ConfigError::Validation("shard_count must be a power of 2".into()));
269        }
270        if self.per_shard_capacity == 0 {
271            return Err(ConfigError::Validation("per_shard_capacity must be > 0".into()));
272        }
273        if self.max_entry_size == 0 {
274            return Err(ConfigError::Validation("max_entry_size must be > 0".into()));
275        }
276        // 淘汰策略收敛到受支持的白名单,杜绝拼写错误导致的静默行为分歧
277        if !matches!(self.eviction_policy.as_str(), "lru" | "fifo" | "random") {
278            return Err(ConfigError::Validation(
279                "eviction_policy must be one of \"lru\" | \"fifo\" | \"random\"".into(),
280            ));
281        }
282        if self.ttl_seconds == 0 {
283            return Err(ConfigError::Validation("ttl_seconds must be > 0".into()));
284        }
285        Ok(())
286    }
287}
288
289/// 安全相关配置(TLS、常量时间比较、eBPF 完整性)。
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct SecurityConfig {
292    /// 是否启用 TLS 终止。
293    pub enable_tls: bool,
294    /// TLS 证书文件路径(启用 TLS 时必填)。
295    pub cert_path: Option<String>,
296    /// TLS 私钥文件路径(启用 TLS 时必填)。
297    pub key_path: Option<String>,
298    /// 是否启用常量时间比较(防时序攻击)。
299    pub enable_constant_time: bool,
300    /// 是否启用 eBPF 自身完整性校验。
301    pub enable_ebpf_integrity: bool,
302    /// 令牌最大生命周期(毫秒)。
303    pub max_token_lifetime_ms: u64,
304}
305
306impl Default for SecurityConfig {
307    fn default() -> Self {
308        Self {
309            enable_tls: false,
310            cert_path: None,
311            key_path: None,
312            enable_constant_time: true,
313            enable_ebpf_integrity: true,
314            max_token_lifetime_ms: 3600000,
315        }
316    }
317}
318
319impl SecurityConfig {
320    /// 校验安全配置的业务约束。
321    ///
322    /// # Errors
323    /// - 当启用 TLS 但未提供证书或私钥路径时返回错误
324    /// - 当 `max_token_lifetime_ms` 为 0 时返回错误
325    pub fn validate(&self) -> Result<(), ConfigError> {
326        if self.enable_tls
327            && (self.cert_path.is_none() || self.key_path.is_none()) {
328                return Err(ConfigError::Validation("TLS requires cert_path and key_path".into()));
329        }
330        if self.max_token_lifetime_ms == 0 {
331            return Err(ConfigError::Validation("max_token_lifetime_ms must be > 0".into()));
332        }
333        Ok(())
334    }
335}
336
337/// 可观测性配置(指标、追踪、日志)。
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct ObservabilityConfig {
340    /// 是否启用 Prometheus 指标导出。
341    pub enable_metrics: bool,
342    /// 指标导出监听地址。
343    pub metrics_addr: String,
344    /// 是否启用分布式追踪。
345    pub enable_tracing: bool,
346    /// 追踪级别(如 `info`、`debug`)。
347    pub tracing_level: String,
348    /// 是否启用结构化日志。
349    pub enable_logging: bool,
350    /// 日志级别(如 `info`、`debug`)。
351    pub log_level: String,
352}
353
354impl Default for ObservabilityConfig {
355    fn default() -> Self {
356        Self {
357            enable_metrics: true,
358            metrics_addr: "0.0.0.0:9090".to_string(),
359            enable_tracing: false,
360            tracing_level: "info".to_string(),
361            enable_logging: true,
362            log_level: "info".to_string(),
363        }
364    }
365}
366
367impl ObservabilityConfig {
368    /// 校验可观测性配置的业务约束。
369    ///
370    /// # Errors
371    /// 当 `log_level` 或 `tracing_level` 不在
372    /// `"trace" | "debug" | "info" | "warn" | "error"` 白名单内时返回
373    /// [`ConfigError::Validation`]。
374    pub fn validate(&self) -> Result<(), ConfigError> {
375        // 级别收敛到 tracing/log 生态通用白名单,拒绝拼写错误或未知级别
376        const LEVELS: [&str; 5] = ["trace", "debug", "info", "warn", "error"];
377        if !LEVELS.contains(&self.log_level.as_str()) {
378            return Err(ConfigError::Validation(
379                "log_level must be one of \"trace\" | \"debug\" | \"info\" | \"warn\" | \"error\""
380                    .into(),
381            ));
382        }
383        if !LEVELS.contains(&self.tracing_level.as_str()) {
384            return Err(ConfigError::Validation(
385                "tracing_level must be one of \"trace\" | \"debug\" | \"info\" | \"warn\" | \"error\""
386                    .into(),
387            ));
388        }
389        Ok(())
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    #[test]
398    fn test_default_config() {
399        let config = ZenithConfig::default();
400        assert!(config.validate().is_ok());
401        assert_eq!(config.server.listen_addr, "0.0.0.0:8080");
402        assert!(config.runtime.enable_ebpf);
403    }
404
405    #[test]
406    fn test_metrics_auth_token_default_none() {
407        let config = ZenithConfig::default();
408        assert!(config.server.metrics_auth_token.is_none());
409    }
410
411    #[test]
412    fn test_metrics_auth_token_from_toml() {
413        let toml_str = r#"
414[server]
415listen_addr = "0.0.0.0:8080"
416max_connections = 100
417connection_timeout_ms = 5000
418keepalive_interval_ms = 30000
419metrics_auth_token = "s3cr3t-t0ken"
420
421[runtime]
422worker_threads = 4
423io_threads = 2
424time_threads = 1
425blocking_threads = 4
426enable_ebpf = false
427enable_xsk = false
428numa_aware = false
429
430[cache]
431shard_count = 16
432per_shard_capacity = 4096
433max_entry_size = 1048576
434eviction_policy = "lru"
435ttl_seconds = 300
436
437[security]
438enable_tls = false
439enable_constant_time = true
440enable_ebpf_integrity = true
441max_token_lifetime_ms = 3600000
442
443[observability]
444enable_metrics = true
445metrics_addr = "0.0.0.0:9091"
446enable_tracing = false
447tracing_level = "info"
448enable_logging = true
449log_level = "info"
450"#;
451        let config = ZenithConfig::from_str(toml_str).unwrap();
452        assert_eq!(config.server.metrics_auth_token.as_deref(), Some("s3cr3t-t0ken"));
453    }
454
455    #[test]
456    fn test_config_roundtrip() {
457        let config = ZenithConfig::default();
458        let toml_str = config.to_toml().unwrap();
459        let parsed = ZenithConfig::from_str(&toml_str).unwrap();
460        assert_eq!(parsed.server.listen_addr, config.server.listen_addr);
461        assert_eq!(parsed.runtime.worker_threads, config.runtime.worker_threads);
462    }
463
464    #[test]
465    fn test_config_validation() {
466        let mut config = ZenithConfig::default();
467        config.server.max_connections = 0;
468        assert!(config.validate().is_err());
469    }
470
471    #[test]
472    fn test_config_tls_validation() {
473        let mut config = ZenithConfig::default();
474        config.security.enable_tls = true;
475        assert!(config.validate().is_err());
476
477        config.security.cert_path = Some("/path/to/cert".to_string());
478        config.security.key_path = Some("/path/to/key".to_string());
479        assert!(config.validate().is_ok());
480    }
481
482    #[test]
483    fn test_config_cache_validation() {
484        let mut config = ZenithConfig::default();
485        config.cache.shard_count = 3;
486        assert!(config.validate().is_err());
487
488        config.cache.shard_count = 16;
489        assert!(config.validate().is_ok());
490    }
491
492    #[test]
493    fn test_config_custom_values() {
494        let toml_str = r#"
495[server]
496listen_addr = "127.0.0.1:9090"
497max_connections = 10000
498connection_timeout_ms = 3000
499keepalive_interval_ms = 15000
500
501[runtime]
502worker_threads = 8
503io_threads = 4
504time_threads = 2
505blocking_threads = 8
506enable_ebpf = true
507enable_xsk = true
508numa_aware = true
509
510[cache]
511shard_count = 32
512per_shard_capacity = 131072
513max_entry_size = 2097152
514eviction_policy = "fifo"
515ttl_seconds = 7200
516
517[security]
518enable_tls = false
519enable_constant_time = true
520enable_ebpf_integrity = true
521max_token_lifetime_ms = 7200000
522
523[observability]
524enable_metrics = true
525metrics_addr = "0.0.0.0:8081"
526enable_tracing = true
527tracing_level = "debug"
528enable_logging = true
529log_level = "debug"
530"#;
531        let config = ZenithConfig::from_str(toml_str).unwrap();
532        assert_eq!(config.server.listen_addr, "127.0.0.1:9090");
533        assert_eq!(config.runtime.worker_threads, 8);
534        assert_eq!(config.cache.shard_count, 32);
535        assert_eq!(config.observability.metrics_addr, "0.0.0.0:8081");
536    }
537
538    #[test]
539    fn test_from_file_not_found_maps_to_not_found() {
540        // 不存在的配置文件必须映射为 ConfigError::NotFound(而非笼统的 Io)
541        let path = Path::new("zzz_zenith_definitely_missing_config_x7q9.toml");
542        let result = ZenithConfig::from_file(path);
543        match result {
544            Err(ConfigError::NotFound(msg)) => {
545                assert!(msg.contains("zzz_zenith_definitely_missing_config_x7q9.toml"));
546            }
547            other => panic!("expected ConfigError::NotFound, got {other:?}"),
548        }
549        // 现有文件路径不受影响(默认配置仍可通过往返加载)
550        let config = ZenithConfig::default();
551        assert!(config.validate().is_ok());
552    }
553
554    fn default_cache() -> CacheConfig {
555        CacheConfig::default()
556    }
557
558    #[test]
559    fn test_cache_validate_eviction_policy_whitelist() {
560        // 白名单内策略全部通过
561        for policy in ["lru", "fifo", "random"] {
562            let mut cache = default_cache();
563            cache.eviction_policy = policy.to_string();
564            assert!(cache.validate().is_ok(), "policy {policy} should pass");
565        }
566        // 白名单外(含旧文档示例 "lfu")必须拒绝
567        for policy in ["lfu", "LRU", "", "clock"] {
568            let mut cache = default_cache();
569            cache.eviction_policy = policy.to_string();
570            assert!(
571                matches!(cache.validate(), Err(ConfigError::Validation(_))),
572                "policy {policy} should be rejected"
573            );
574        }
575    }
576
577    #[test]
578    fn test_cache_validate_max_entry_size_positive() {
579        let mut cache = default_cache();
580        cache.max_entry_size = 0;
581        assert!(matches!(cache.validate(), Err(ConfigError::Validation(_))));
582
583        let mut cache = default_cache();
584        cache.max_entry_size = 1;
585        assert!(cache.validate().is_ok());
586    }
587
588    #[test]
589    fn test_observability_validate_level_whitelist() {
590        // 白名单内级别全部通过
591        for level in ["trace", "debug", "info", "warn", "error"] {
592            let obs = ObservabilityConfig {
593                log_level: level.to_string(),
594                tracing_level: level.to_string(),
595                ..Default::default()
596            };
597            assert!(obs.validate().is_ok(), "level {level} should pass");
598        }
599        // log_level 非法
600        let obs = ObservabilityConfig {
601            log_level: "verbose".to_string(),
602            ..Default::default()
603        };
604        assert!(matches!(obs.validate(), Err(ConfigError::Validation(_))));
605        // tracing_level 非法(区分大小写)
606        let obs = ObservabilityConfig {
607            tracing_level: "TRACE".to_string(),
608            ..Default::default()
609        };
610        assert!(matches!(obs.validate(), Err(ConfigError::Validation(_))));
611        // 非法级别会从 ZenithConfig::validate 递归传播
612        let mut config = ZenithConfig::default();
613        config.observability.log_level = "nope".to_string();
614        assert!(matches!(config.validate(), Err(ConfigError::Validation(_))));
615    }
616}