1use anyhow::{anyhow, Result};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::fs;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17use std::time::Duration;
18use tokio::sync::{broadcast, RwLock};
19use tracing::info;
20
21use crate::{CompressionType, StreamBackendType, StreamConfig, StreamPerformanceConfig};
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub enum ConfigSource {
26 File { path: PathBuf },
28 Environment { prefix: String },
30 Remote { url: String, key: String },
32 Memory { data: HashMap<String, String> },
34}
35
36pub struct ConfigManager {
38 current_config: Arc<RwLock<StreamConfig>>,
40 sources: Vec<ConfigSource>,
42 change_notifier: broadcast::Sender<ConfigChangeEvent>,
44 secret_manager: Arc<SecretManager>,
46 environment: Environment,
48 performance_profiles: HashMap<String, PerformanceProfile>,
50 tls_manager: Arc<TlsManager>,
52}
53
54#[derive(Debug, Clone)]
56pub enum ConfigChangeEvent {
57 Reloaded {
59 old_config: Box<StreamConfig>,
60 new_config: Box<StreamConfig>,
61 },
62 ValidationFailed { reason: String },
64 SecretRotated { secret_name: String },
66 TlsCertificateUpdated { cert_type: String },
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub enum Environment {
73 Development,
74 Testing,
75 Staging,
76 Production,
77}
78
79impl Environment {
80 pub fn detect() -> Self {
82 if let Ok(env) = std::env::var("OXIRS_ENV") {
83 match env.to_lowercase().as_str() {
84 "dev" | "development" => Environment::Development,
85 "test" | "testing" => Environment::Testing,
86 "staging" | "stage" => Environment::Staging,
87 "prod" | "production" => Environment::Production,
88 _ => Environment::Development,
89 }
90 } else if let Ok(env) = std::env::var("RUST_ENV") {
91 match env.to_lowercase().as_str() {
92 "production" => Environment::Production,
93 _ => Environment::Development,
94 }
95 } else {
96 Environment::Development
97 }
98 }
99
100 pub fn get_defaults(&self) -> ConfigDefaults {
102 match self {
103 Environment::Development => ConfigDefaults {
104 log_level: "debug".to_string(),
105 enable_debug_endpoints: true,
106 connection_timeout_secs: 60,
107 max_connections: 10,
108 enable_compression: false,
109 enable_metrics: true,
110 enable_profiling: true,
111 },
112 Environment::Testing => ConfigDefaults {
113 log_level: "info".to_string(),
114 enable_debug_endpoints: true,
115 connection_timeout_secs: 30,
116 max_connections: 5,
117 enable_compression: false,
118 enable_metrics: true,
119 enable_profiling: false,
120 },
121 Environment::Staging => ConfigDefaults {
122 log_level: "info".to_string(),
123 enable_debug_endpoints: false,
124 connection_timeout_secs: 30,
125 max_connections: 50,
126 enable_compression: true,
127 enable_metrics: true,
128 enable_profiling: false,
129 },
130 Environment::Production => ConfigDefaults {
131 log_level: "warn".to_string(),
132 enable_debug_endpoints: false,
133 connection_timeout_secs: 30,
134 max_connections: 100,
135 enable_compression: true,
136 enable_metrics: true,
137 enable_profiling: false,
138 },
139 }
140 }
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct ConfigDefaults {
146 pub log_level: String,
147 pub enable_debug_endpoints: bool,
148 pub connection_timeout_secs: u64,
149 pub max_connections: usize,
150 pub enable_compression: bool,
151 pub enable_metrics: bool,
152 pub enable_profiling: bool,
153}
154
155pub struct SecretManager {
157 backend: SecretBackend,
159 cache: Arc<RwLock<HashMap<String, CachedSecret>>>,
161 rotation_interval: Duration,
163}
164
165#[derive(Debug, Clone)]
167pub enum SecretBackend {
168 Environment { prefix: String },
170 File { directory: PathBuf },
172 Vault {
174 url: String,
175 token: String,
176 mount_path: String,
177 },
178 AwsSecretsManager { region: String },
180 Memory {
182 secrets: Arc<RwLock<HashMap<String, String>>>,
183 },
184}
185
186#[derive(Debug, Clone)]
188struct CachedSecret {
189 value: String,
190 cached_at: std::time::Instant,
191 expires_at: Option<std::time::Instant>,
192 version: u64,
193}
194
195impl SecretManager {
196 pub fn new(backend: SecretBackend, rotation_interval: Duration) -> Self {
198 Self {
199 backend,
200 cache: Arc::new(RwLock::new(HashMap::new())),
201 rotation_interval,
202 }
203 }
204
205 pub async fn get_secret(&self, name: &str) -> Result<String> {
207 {
209 let cache = self.cache.read().await;
210 if let Some(cached) = cache.get(name) {
211 if cached
212 .expires_at
213 .map_or(true, |exp| exp > std::time::Instant::now())
214 {
215 return Ok(cached.value.clone());
216 }
217 }
218 }
219
220 let value = match &self.backend {
222 SecretBackend::Environment { prefix } => {
223 let key = format!("{prefix}_{}", name.to_uppercase());
224 std::env::var(key).map_err(|_| anyhow!("Secret {name} not found in environment"))
225 }
226 SecretBackend::File { directory } => {
227 let path = directory.join(name);
228 fs::read_to_string(&path)
229 .map_err(|e| anyhow!("Failed to read secret from {path:?}: {e}"))
230 .map(|s| s.trim().to_string())
231 }
232 SecretBackend::Memory { secrets } => {
233 let secrets = secrets.read().await;
234 secrets
235 .get(name)
236 .cloned()
237 .ok_or_else(|| anyhow!("Secret {name} not found"))
238 }
239 _ => {
240 return Err(anyhow!(
243 "Secret backend requires an external service connection (Vault/AWS) \
244 that is not available in this configuration"
245 ));
246 }
247 }?;
248
249 let cached = CachedSecret {
251 value: value.clone(),
252 cached_at: std::time::Instant::now(),
253 expires_at: Some(std::time::Instant::now() + self.rotation_interval),
254 version: 1,
255 };
256
257 self.cache.write().await.insert(name.to_string(), cached);
258 Ok(value)
259 }
260
261 pub async fn set_secret(&self, name: &str, value: &str) -> Result<()> {
263 match &self.backend {
264 SecretBackend::Memory { secrets } => {
265 secrets
266 .write()
267 .await
268 .insert(name.to_string(), value.to_string());
269 self.cache.write().await.remove(name);
270 Ok(())
271 }
272 _ => Err(anyhow!("Set secret only supported for memory backend")),
273 }
274 }
275
276 pub async fn rotate_secrets(&self) -> Result<()> {
278 self.cache.write().await.clear();
279 info!("Rotated all cached secrets");
280 Ok(())
281 }
282}
283
284pub struct TlsManager {
286 certs: Arc<RwLock<HashMap<String, TlsCertificate>>>,
288 cert_paths: HashMap<String, CertPaths>,
290 auto_reload: bool,
292}
293
294#[derive(Debug, Clone)]
296pub struct TlsCertificate {
297 pub cert_pem: Vec<u8>,
298 pub key_pem: Vec<u8>,
299 pub ca_pem: Option<Vec<u8>>,
300 pub loaded_at: std::time::Instant,
301 pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
302}
303
304#[derive(Debug, Clone)]
306struct CertPaths {
307 cert_path: PathBuf,
308 key_path: PathBuf,
309 ca_path: Option<PathBuf>,
310}
311
312impl TlsManager {
313 pub fn new(auto_reload: bool) -> Self {
315 Self {
316 certs: Arc::new(RwLock::new(HashMap::new())),
317 cert_paths: HashMap::new(),
318 auto_reload,
319 }
320 }
321
322 fn parse_certificate_expiration(cert_pem: &[u8]) -> Option<chrono::DateTime<chrono::Utc>> {
324 let cert_str = String::from_utf8_lossy(cert_pem);
327
328 if cert_str.contains("-----BEGIN CERTIFICATE-----") {
331 Some(chrono::Utc::now() + chrono::Duration::days(365))
334 } else {
335 None
336 }
337 }
338
339 pub async fn load_certificate(
341 &self,
342 name: &str,
343 cert_path: &Path,
344 key_path: &Path,
345 ca_path: Option<&Path>,
346 ) -> Result<()> {
347 let cert_pem = fs::read(cert_path)
348 .map_err(|e| anyhow!("Failed to read certificate {}: {}", cert_path.display(), e))?;
349
350 let key_pem = fs::read(key_path)
351 .map_err(|e| anyhow!("Failed to read key {}: {}", key_path.display(), e))?;
352
353 let ca_pem = if let Some(ca) = ca_path {
354 Some(fs::read(ca).map_err(|e| anyhow!("Failed to read CA {}: {}", ca.display(), e))?)
355 } else {
356 None
357 };
358
359 let expires_at = Self::parse_certificate_expiration(&cert_pem);
360
361 let cert = TlsCertificate {
362 cert_pem,
363 key_pem,
364 ca_pem,
365 loaded_at: std::time::Instant::now(),
366 expires_at,
367 };
368
369 self.certs.write().await.insert(name.to_string(), cert);
370
371 info!("Loaded TLS certificate: {}", name);
372 Ok(())
373 }
374
375 pub async fn get_certificate(&self, name: &str) -> Result<TlsCertificate> {
377 self.certs
378 .read()
379 .await
380 .get(name)
381 .cloned()
382 .ok_or_else(|| anyhow!("Certificate {} not found", name))
383 }
384
385 pub async fn check_expiration(&self) -> Vec<(String, chrono::DateTime<chrono::Utc>)> {
387 let certs = self.certs.read().await;
388 let mut expiring = Vec::new();
389
390 for (name, cert) in certs.iter() {
391 if let Some(expires) = cert.expires_at {
392 let days_until = (expires - chrono::Utc::now()).num_days();
393 if days_until < 30 {
394 expiring.push((name.clone(), expires));
395 }
396 }
397 }
398
399 expiring
400 }
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct PerformanceProfile {
406 pub name: String,
407 pub description: String,
408 pub settings: StreamPerformanceConfig,
409 pub recommended_for: Vec<String>,
410}
411
412impl ConfigManager {
413 pub async fn new(sources: Vec<ConfigSource>) -> Result<Self> {
415 let environment = Environment::detect();
416 let (tx, _) = broadcast::channel(100);
417
418 let secret_backend = SecretBackend::Environment {
420 prefix: "OXIRS_SECRET".to_string(),
421 };
422 let secret_manager = Arc::new(SecretManager::new(
423 secret_backend,
424 Duration::from_secs(3600),
425 ));
426
427 let tls_manager = Arc::new(TlsManager::new(true));
429
430 let initial_config = StreamConfig::default();
432
433 let mut manager = Self {
434 current_config: Arc::new(RwLock::new(initial_config)),
435 sources,
436 change_notifier: tx,
437 secret_manager,
438 environment,
439 performance_profiles: Self::create_default_profiles(),
440 tls_manager,
441 };
442
443 manager.reload().await?;
445
446 Ok(manager)
447 }
448
449 fn create_default_profiles() -> HashMap<String, PerformanceProfile> {
451 let mut profiles = HashMap::new();
452
453 profiles.insert(
455 "low-latency".to_string(),
456 PerformanceProfile {
457 name: "low-latency".to_string(),
458 description: "Optimized for minimal latency".to_string(),
459 settings: StreamPerformanceConfig {
460 enable_batching: false,
461 enable_pipelining: true,
462 buffer_size: 4096,
463 prefetch_count: 10,
464 enable_zero_copy: true,
465 enable_simd: true,
466 parallel_processing: true,
467 worker_threads: Some(4),
468 },
469 recommended_for: vec!["real-time".to_string(), "trading".to_string()],
470 },
471 );
472
473 profiles.insert(
475 "high-throughput".to_string(),
476 PerformanceProfile {
477 name: "high-throughput".to_string(),
478 description: "Optimized for maximum throughput".to_string(),
479 settings: StreamPerformanceConfig {
480 enable_batching: true,
481 enable_pipelining: true,
482 buffer_size: 65536,
483 prefetch_count: 1000,
484 enable_zero_copy: true,
485 enable_simd: true,
486 parallel_processing: true,
487 worker_threads: None, },
489 recommended_for: vec!["batch-processing".to_string(), "etl".to_string()],
490 },
491 );
492
493 profiles.insert(
495 "balanced".to_string(),
496 PerformanceProfile {
497 name: "balanced".to_string(),
498 description: "Balanced between latency and throughput".to_string(),
499 settings: StreamPerformanceConfig::default(),
500 recommended_for: vec!["general".to_string(), "web-services".to_string()],
501 },
502 );
503
504 profiles.insert(
506 "resource-constrained".to_string(),
507 PerformanceProfile {
508 name: "resource-constrained".to_string(),
509 description: "Optimized for limited resources".to_string(),
510 settings: StreamPerformanceConfig {
511 enable_batching: true,
512 enable_pipelining: false,
513 buffer_size: 2048,
514 prefetch_count: 10,
515 enable_zero_copy: false,
516 enable_simd: false,
517 parallel_processing: false,
518 worker_threads: Some(2),
519 },
520 recommended_for: vec!["edge".to_string(), "iot".to_string()],
521 },
522 );
523
524 profiles
525 }
526
527 pub async fn reload(&mut self) -> Result<()> {
529 let old_config = self.current_config.read().await.clone();
530 let mut new_config = old_config.clone();
531
532 let defaults = self.environment.get_defaults();
534 new_config.max_connections = defaults.max_connections;
535 new_config.connection_timeout = Duration::from_secs(defaults.connection_timeout_secs);
536 new_config.enable_compression = defaults.enable_compression;
537 new_config.monitoring.enable_metrics = defaults.enable_metrics;
538 new_config.monitoring.enable_profiling = defaults.enable_profiling;
539
540 for source in &self.sources {
542 match source {
543 ConfigSource::File { path } => {
544 if let Ok(content) = fs::read_to_string(path) {
545 if let Ok(file_config) = toml::from_str::<StreamConfig>(&content) {
546 new_config = self.merge_configs(new_config, file_config);
547 }
548 }
549 }
550 ConfigSource::Environment { prefix } => {
551 new_config = self.load_from_env(new_config, prefix).await?;
552 }
553 ConfigSource::Memory { data } => {
554 new_config = self.apply_overrides(new_config, data.clone());
555 }
556 _ => {
557 }
559 }
560 }
561
562 new_config = self.apply_secrets(new_config).await?;
564
565 self.validate_config(&new_config)?;
567
568 *self.current_config.write().await = new_config.clone();
570
571 let _ = self.change_notifier.send(ConfigChangeEvent::Reloaded {
573 old_config: Box::new(old_config),
574 new_config: Box::new(new_config),
575 });
576
577 info!("Configuration reloaded successfully");
578 Ok(())
579 }
580
581 async fn load_from_env(&self, mut config: StreamConfig, prefix: &str) -> Result<StreamConfig> {
583 if let Ok(backend) = std::env::var(format!("{prefix}_BACKEND")) {
585 config.backend = match backend.as_str() {
586 "kafka" => {
587 let brokers: Vec<String> = std::env::var(format!("{prefix}_KAFKA_BROKERS"))
590 .unwrap_or_else(|_| "localhost:9092".to_string())
591 .split(',')
592 .map(|s| s.to_string())
593 .collect();
594 StreamBackendType::Kafka {
595 brokers,
596 security_protocol: std::env::var(format!("{prefix}_KAFKA_SECURITY")).ok(),
597 sasl_config: None,
598 }
599 }
600 "memory" => StreamBackendType::Memory {
601 max_size: Some(10000),
602 persistence: false,
603 },
604 _ => config.backend,
605 };
606 }
607
608 if let Ok(max_conn) = std::env::var(format!("{prefix}_MAX_CONNECTIONS")) {
610 if let Ok(val) = max_conn.parse() {
611 config.max_connections = val;
612 }
613 }
614
615 if let Ok(compression) = std::env::var(format!("{prefix}_COMPRESSION")) {
617 config.compression_type = match compression.as_str() {
618 "gzip" => CompressionType::Gzip,
619 "snappy" => CompressionType::Snappy,
620 "lz4" => CompressionType::Lz4,
621 "zstd" => CompressionType::Zstd,
622 _ => CompressionType::None,
623 };
624 config.enable_compression = compression != "none";
625 }
626
627 Ok(config)
628 }
629
630 async fn apply_secrets(&self, mut config: StreamConfig) -> Result<StreamConfig> {
632 if let StreamBackendType::Kafka {
634 brokers,
635 security_protocol,
636 sasl_config: _,
637 } = &config.backend
638 {
639 if security_protocol.as_deref() == Some("SASL_SSL") {
640 if let Ok(username) = self.secret_manager.get_secret("kafka_username").await {
641 if let Ok(password) = self.secret_manager.get_secret("kafka_password").await {
642 {
643 config.backend = StreamBackendType::Kafka {
644 brokers: brokers.clone(),
645 security_protocol: security_protocol.clone(),
646 sasl_config: Some(crate::SaslConfig {
647 mechanism: crate::SaslMechanism::ScramSha256,
648 username,
649 password,
650 }),
651 };
652 }
653 }
654 }
655 }
656 }
657
658 if config.security.enable_tls {
660 if let Ok(_cert) = self.tls_manager.get_certificate("client").await {
661 config.security.client_cert_path = Some("/tmp/client.crt".to_string());
663 config.security.client_key_path = Some("/tmp/client.key".to_string());
664 }
665 }
666
667 Ok(config)
668 }
669
670 fn merge_configs(&self, _base: StreamConfig, override_config: StreamConfig) -> StreamConfig {
672 override_config
675 }
676
677 fn apply_overrides(
679 &self,
680 mut config: StreamConfig,
681 overrides: HashMap<String, String>,
682 ) -> StreamConfig {
683 for (key, value) in overrides {
684 match key.as_str() {
685 "topic" => config.topic = value,
686 "batch_size" => {
687 if let Ok(size) = value.parse() {
688 config.batch_size = size;
689 }
690 }
691 "max_connections" => {
692 if let Ok(max) = value.parse() {
693 config.max_connections = max;
694 }
695 }
696 _ => {}
697 }
698 }
699 config
700 }
701
702 fn validate_config(&self, config: &StreamConfig) -> Result<()> {
704 if config.max_connections == 0 {
706 return Err(anyhow!("max_connections must be greater than 0"));
707 }
708
709 if config.batch_size == 0 {
711 return Err(anyhow!("batch_size must be greater than 0"));
712 }
713
714 if config.topic.is_empty() {
716 return Err(anyhow!("topic name cannot be empty"));
717 }
718
719 match &config.backend {
721 StreamBackendType::Kafka { brokers, .. } if brokers.is_empty() => {
722 return Err(anyhow!("Kafka brokers list cannot be empty"));
723 }
724 _ => {}
725 }
726
727 Ok(())
728 }
729
730 pub async fn get_config(&self) -> StreamConfig {
732 self.current_config.read().await.clone()
733 }
734
735 pub fn subscribe(&self) -> broadcast::Receiver<ConfigChangeEvent> {
737 self.change_notifier.subscribe()
738 }
739
740 pub async fn apply_performance_profile(&mut self, profile_name: &str) -> Result<()> {
742 let profile = self
743 .performance_profiles
744 .get(profile_name)
745 .ok_or_else(|| anyhow!("Performance profile {} not found", profile_name))?
746 .clone();
747
748 let mut config = self.current_config.write().await;
749 config.performance = profile.settings;
750
751 info!("Applied performance profile: {}", profile_name);
752 Ok(())
753 }
754
755 pub fn get_performance_profiles(&self) -> Vec<&PerformanceProfile> {
757 self.performance_profiles.values().collect()
758 }
759
760 pub async fn update_value(&mut self, key: &str, value: String) -> Result<()> {
762 let mut overrides = HashMap::new();
763 overrides.insert(key.to_string(), value);
764
765 let current = self.current_config.read().await.clone();
766 let updated = self.apply_overrides(current, overrides);
767
768 self.validate_config(&updated)?;
769 *self.current_config.write().await = updated;
770
771 info!("Updated configuration key: {}", key);
772 Ok(())
773 }
774
775 pub fn secret_manager(&self) -> &Arc<SecretManager> {
777 &self.secret_manager
778 }
779
780 pub fn tls_manager(&self) -> &Arc<TlsManager> {
782 &self.tls_manager
783 }
784}
785
786pub struct ConfigBuilder {
788 sources: Vec<ConfigSource>,
789 environment: Option<Environment>,
790}
791
792impl ConfigBuilder {
793 pub fn new() -> Self {
795 Self {
796 sources: Vec::new(),
797 environment: None,
798 }
799 }
800
801 pub fn with_file(mut self, path: impl Into<PathBuf>) -> Self {
803 self.sources.push(ConfigSource::File { path: path.into() });
804 self
805 }
806
807 pub fn with_env(mut self, prefix: impl Into<String>) -> Self {
809 self.sources.push(ConfigSource::Environment {
810 prefix: prefix.into(),
811 });
812 self
813 }
814
815 pub fn with_overrides(mut self, overrides: HashMap<String, String>) -> Self {
817 self.sources.push(ConfigSource::Memory { data: overrides });
818 self
819 }
820
821 pub fn with_environment(mut self, env: Environment) -> Self {
823 self.environment = Some(env);
824 self
825 }
826
827 pub async fn build(self) -> Result<ConfigManager> {
829 ConfigManager::new(self.sources).await
830 }
831}
832
833impl Default for ConfigBuilder {
834 fn default() -> Self {
835 Self::new()
836 }
837}
838
839#[cfg(test)]
840mod tests {
841 use super::*;
842
843 #[tokio::test]
844 async fn test_environment_detection() {
845 let env = Environment::detect();
846 let defaults = env.get_defaults();
847 assert!(defaults.max_connections > 0);
848 }
849
850 #[tokio::test]
851 async fn test_config_builder() {
852 let mut overrides = HashMap::new();
853 overrides.insert("topic".to_string(), "test-topic".to_string());
854
855 let manager = ConfigBuilder::new()
856 .with_env("OXIRS")
857 .with_overrides(overrides)
858 .build()
859 .await
860 .unwrap();
861
862 let config = manager.get_config().await;
863 assert_eq!(config.topic, "test-topic");
864 }
865
866 #[tokio::test]
867 async fn test_secret_manager() {
868 let backend = SecretBackend::Memory {
869 secrets: Arc::new(RwLock::new(HashMap::new())),
870 };
871 let manager = SecretManager::new(backend, Duration::from_secs(60));
872
873 manager.set_secret("test_key", "test_value").await.unwrap();
875 let value = manager.get_secret("test_key").await.unwrap();
876 assert_eq!(value, "test_value");
877
878 let value2 = manager.get_secret("test_key").await.unwrap();
880 assert_eq!(value2, "test_value");
881 }
882
883 #[tokio::test]
884 async fn test_performance_profiles() {
885 let manager = ConfigBuilder::new().build().await.unwrap();
886 let profiles = manager.get_performance_profiles();
887
888 assert!(profiles.len() >= 4);
889 assert!(profiles.iter().any(|p| p.name == "low-latency"));
890 assert!(profiles.iter().any(|p| p.name == "high-throughput"));
891 }
892
893 #[tokio::test]
894 async fn test_config_validation() {
895 let manager = ConfigBuilder::new().build().await.unwrap();
896
897 let invalid_config = StreamConfig {
899 max_connections: 0,
900 ..Default::default()
901 };
902
903 assert!(manager.validate_config(&invalid_config).is_err());
904 }
905}