adaptive_pipeline/infrastructure/config/
config_service.rs

1// /////////////////////////////////////////////////////////////////////////////
2// Adaptive Pipeline
3// Copyright (c) 2025 Michael Gardner, A Bit of Help, Inc.
4// SPDX-License-Identifier: BSD-3-Clause
5// See LICENSE file in the project root.
6// /////////////////////////////////////////////////////////////////////////////
7
8//! # Configuration Service Implementation
9//!
10//! This module provides configuration management services for the adaptive
11//! pipeline system. It handles loading, parsing, validation, and management
12//! of configuration settings for observability, logging, metrics, and system
13//! behavior.
14//!
15//! ## Overview
16//!
17//! The configuration service implementation provides:
18//!
19//! - **Configuration Loading**: Loads configuration from files and environment
20//!   variables
21//! - **Validation**: Validates configuration settings and provides defaults
22//! - **Hot Reloading**: Supports dynamic configuration updates without restart
23//! - **Environment Integration**: Integrates with environment-specific settings
24//! - **Type Safety**: Strongly typed configuration structures with validation
25//!
26//! ## Architecture
27//!
28//! The configuration service follows these design principles:
29//!
30//! - **Layered Configuration**: Supports multiple configuration sources with
31//!   precedence
32//! - **Type Safety**: Uses Rust's type system for configuration validation
33//! - **Default Values**: Provides sensible defaults for all configuration
34//!   options
35//! - **Environment Awareness**: Adapts configuration based on deployment
36//!   environment
37//!
38//! ## Configuration Categories
39//!
40//! ### Observability Configuration
41//!
42//! Controls system monitoring and observability features:
43//! - **Structured Logging**: Enable/disable structured logging output
44//! - **Performance Tracing**: Control performance tracing and profiling
45//! - **Health Checks**: Configure health check endpoints and intervals
46//! - **Metrics Export**: Control metrics collection and export settings
47//! - **Trace Sampling**: Configure distributed tracing sample rates
48//!
49//! ### Logging Configuration
50//!
51//! Manages application logging behavior:
52//! - **Log Level**: Set minimum log level (debug, info, warn, error)
53//! - **Output Format**: Configure log output format (JSON, plain text)
54//! - **File Rotation**: Configure log file rotation and retention
55//! - **Filtering**: Set up log filtering rules and patterns
56//!
57//! ### Metrics Configuration
58//!
59//! Controls metrics collection and export:
60//! - **Collection Interval**: How frequently to collect metrics
61//! - **Export Endpoints**: Where to export metrics (Prometheus, etc.)
62//! - **Retention Policy**: How long to retain metric data
63//! - **Aggregation**: Configure metric aggregation strategies
64//!
65//! ## Usage Examples
66//!
67//! ### Loading Configuration
68
69//!
70//! ### Environment-Specific Configuration
71
72//!
73//! ### Configuration Validation
74
75//!
76//! ## Configuration Sources
77//!
78//! ### File-Based Configuration
79//!
80//! Supports multiple configuration file formats:
81//! - **TOML**: Primary configuration format (recommended)
82//! - **JSON**: Alternative JSON format support
83//! - **YAML**: YAML format for complex configurations
84//!
85//! ### Environment Variables
86//!
87//! Environment variable overrides with prefixes:
88//! - **ADAPIPE_LOG_LEVEL**: Override logging level
89//! - **ADAPIPE_METRICS_ENABLED**: Enable/disable metrics
90//! - **ADAPIPE_TRACE_SAMPLE_RATE**: Set tracing sample rate
91//!
92//! ### Default Configuration
93//!
94//! Provides sensible defaults for all settings:
95//! - **Development**: Verbose logging, detailed tracing
96//! - **Production**: Optimized for performance and stability
97//! - **Testing**: Minimal overhead, focused on test execution
98//!
99//! ## Configuration Validation
100//!
101//! ### Type Safety
102//!
103//! - **Compile-Time Validation**: Rust's type system prevents invalid
104//!   configurations
105//! - **Runtime Validation**: Additional validation for business rules
106//! - **Default Values**: Automatic fallback to safe defaults
107//!
108//! ### Validation Rules
109//!
110//! - **Range Validation**: Numeric values within acceptable ranges
111//! - **Format Validation**: String values match expected formats
112//! - **Dependency Validation**: Ensure dependent settings are compatible
113//! - **Resource Validation**: Validate resource limits and availability
114//!
115//! ## Hot Reloading
116//!
117//! ### Dynamic Updates
118//!
119//! - **File Watching**: Automatically detect configuration file changes
120//! - **Graceful Updates**: Apply changes without service interruption
121//! - **Rollback Support**: Automatic rollback on invalid configurations
122//! - **Notification**: Notify services of configuration changes
123//!
124//! ### Safety Mechanisms
125//!
126//! - **Validation**: New configurations are validated before application
127//! - **Atomic Updates**: Configuration changes are applied atomically
128//! - **Backup**: Previous configurations are backed up for rollback
129//! - **Monitoring**: Configuration changes are logged and monitored
130//!
131//! ## Performance Considerations
132//!
133//! ### Efficient Loading
134//!
135//! - **Lazy Loading**: Load configuration only when needed
136//! - **Caching**: Cache parsed configuration to avoid repeated parsing
137//! - **Minimal Overhead**: Optimized for fast startup and low memory usage
138//!
139//! ### Memory Management
140//!
141//! - **Shared Configuration**: Share configuration across components
142//! - **Copy-on-Write**: Efficient updates with copy-on-write semantics
143//! - **Garbage Collection**: Automatic cleanup of old configurations
144//!
145//! ## Security Considerations
146//!
147//! ### Sensitive Data
148//!
149//! - **No Secrets**: Configuration files should not contain secrets
150//! - **Environment Variables**: Use environment variables for sensitive data
151//! - **Access Control**: Restrict access to configuration files
152//! - **Audit Logging**: Log configuration access and changes
153//!
154//! ## Integration
155//!
156//! The configuration service integrates with:
157//!
158//! - **Logging System**: Configures logging behavior and output
159//! - **Metrics System**: Controls metrics collection and export
160//! - **Health Checks**: Configures health check endpoints and behavior
161//! - **Tracing System**: Controls distributed tracing and sampling
162//!
163//! ## Future Enhancements
164//!
165//! Planned enhancements include:
166//!
167//! - **Configuration UI**: Web-based configuration management interface
168//! - **Schema Validation**: JSON Schema validation for configuration files
169//! - **Configuration Templates**: Template-based configuration generation
170//! - **Remote Configuration**: Support for remote configuration stores
171
172use serde::{Deserialize, Serialize};
173use std::path::Path;
174use tokio::fs;
175use tracing::{debug, warn};
176
177use adaptive_pipeline_domain::error::PipelineError;
178
179/// Configuration service for reading observability settings
180///
181/// This struct provides comprehensive configuration management for the adaptive
182/// pipeline system, handling observability, logging, metrics, health checks,
183/// tracing, and alerting configurations.
184///
185/// # Configuration Structure
186///
187/// The configuration is organized into logical sections:
188/// - **Observability**: General observability feature toggles
189/// - **Logging**: Application logging configuration
190/// - **Metrics**: Metrics collection and export settings
191/// - **Health Checks**: Health monitoring configuration
192/// - **Tracing**: Distributed tracing settings
193/// - **Alerts**: Alerting and notification configuration
194///
195/// # Examples
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ObservabilityConfig {
198    pub observability: ObservabilitySettings,
199    pub logging: LoggingSettings,
200    pub metrics: MetricsSettings,
201    pub health_checks: HealthCheckSettings,
202    pub tracing: TracingSettings,
203    pub alerts: AlertSettings,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct ObservabilitySettings {
208    pub enable_structured_logging: bool,
209    pub enable_performance_tracing: bool,
210    pub enable_health_checks: bool,
211    pub metrics_export_interval_secs: u64,
212    pub trace_sample_rate: f64,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct LoggingSettings {
217    pub level: String,
218    pub format: String,
219    pub enable_file_logging: bool,
220    pub log_file_path: String,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct MetricsSettings {
225    pub port: u16,
226    pub enable_custom_metrics: bool,
227    pub retention_hours: u32,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct HealthCheckSettings {
232    pub interval_secs: u64,
233    pub memory_threshold_mb: u64,
234    pub error_rate_threshold_percent: f64,
235    pub throughput_threshold_mbps: f64,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct TracingSettings {
240    pub enable_distributed_tracing: bool,
241    pub jaeger_endpoint: String,
242    pub service_name: String,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct AlertSettings {
247    pub enable_alerts: bool,
248    pub webhook_url: String,
249    pub error_rate_alert_threshold: f64,
250    pub memory_usage_alert_threshold: f64,
251    pub disk_usage_alert_threshold: f64,
252}
253
254impl Default for ObservabilityConfig {
255    fn default() -> Self {
256        Self {
257            observability: ObservabilitySettings {
258                enable_structured_logging: true,
259                enable_performance_tracing: true,
260                enable_health_checks: true,
261                metrics_export_interval_secs: 30,
262                trace_sample_rate: 1.0,
263            },
264            logging: LoggingSettings {
265                level: "info".to_string(),
266                format: "pretty".to_string(),
267                enable_file_logging: false,
268                log_file_path: "logs/adaptive_pipeline.log".to_string(),
269            },
270            metrics: MetricsSettings {
271                port: 9090,
272                enable_custom_metrics: true,
273                retention_hours: 24,
274            },
275            health_checks: HealthCheckSettings {
276                interval_secs: 30,
277                memory_threshold_mb: 1000,
278                error_rate_threshold_percent: 5.0,
279                throughput_threshold_mbps: 1.0,
280            },
281            tracing: TracingSettings {
282                enable_distributed_tracing: false,
283                jaeger_endpoint: "http://localhost:14268/api/traces".to_string(),
284                service_name: "adaptive_pipeline".to_string(),
285            },
286            alerts: AlertSettings {
287                enable_alerts: false,
288                webhook_url: String::new(),
289                error_rate_alert_threshold: 10.0,
290                memory_usage_alert_threshold: 80.0,
291                disk_usage_alert_threshold: 90.0,
292            },
293        }
294    }
295}
296
297/// Configuration service for loading observability settings
298pub struct ConfigService;
299
300impl ConfigService {
301    /// Load observability configuration from file
302    pub async fn load_observability_config<P: AsRef<Path>>(
303        config_path: P,
304    ) -> Result<ObservabilityConfig, PipelineError> {
305        let config_path = config_path.as_ref();
306
307        if !config_path.exists() {
308            warn!(
309                "Observability config file not found at {:?}, using defaults",
310                config_path
311            );
312            return Ok(ObservabilityConfig::default());
313        }
314
315        let config_content = fs::read_to_string(config_path).await.map_err(|e| {
316            PipelineError::invalid_config(format!("Failed to read config file {:?}: {}", config_path, e))
317        })?;
318
319        let config: ObservabilityConfig = toml::from_str(&config_content).map_err(|e| {
320            PipelineError::invalid_config(format!("Failed to parse config file {:?}: {}", config_path, e))
321        })?;
322
323        debug!(
324            "Loaded observability config from {:?}: metrics port {}, structured logging {}",
325            config_path, config.metrics.port, config.observability.enable_structured_logging
326        );
327
328        Ok(config)
329    }
330
331    /// Load observability config from default location
332    pub async fn load_default_observability_config() -> Result<ObservabilityConfig, PipelineError> {
333        // Try to find observability.toml in current directory or parent directories
334        let mut current_dir = std::env::current_dir()
335            .map_err(|e| PipelineError::invalid_config(format!("Failed to get current directory: {}", e)))?;
336
337        // Look for observability.toml in current directory and up to 3 parent
338        // directories
339        for _ in 0..4 {
340            let config_path = current_dir.join("observability.toml");
341            if config_path.exists() {
342                debug!("Found observability config at: {:?}", config_path);
343                return Self::load_observability_config(config_path).await;
344            }
345
346            if let Some(parent) = current_dir.parent() {
347                current_dir = parent.to_path_buf();
348            } else {
349                break;
350            }
351        }
352
353        warn!("No observability.toml found, using default configuration");
354        Ok(ObservabilityConfig::default())
355    }
356
357    /// Get metrics port from configuration
358    pub async fn get_metrics_port() -> u16 {
359        match Self::load_default_observability_config().await {
360            Ok(config) => config.metrics.port,
361            Err(_) => 9090, // fallback to default
362        }
363    }
364
365    /// Get alert thresholds from configuration
366    pub async fn get_alert_thresholds() -> (f64, f64) {
367        match Self::load_default_observability_config().await {
368            Ok(config) => (
369                config.health_checks.error_rate_threshold_percent,
370                config.health_checks.throughput_threshold_mbps,
371            ),
372            Err(_) => (5.0, 1.0), // fallback defaults
373        }
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380    use tempfile::NamedTempFile;
381    use tokio::io::AsyncWriteExt;
382
383    #[tokio::test]
384    async fn test_load_default_config() {
385        let config = ConfigService::load_default_observability_config().await.unwrap();
386        assert_eq!(config.metrics.port, 9091); // Should find the actual
387                                               // observability.toml
388    }
389
390    #[tokio::test]
391    async fn test_load_config_from_file() {
392        let temp_file = NamedTempFile::new().unwrap();
393        let config_content = r#"
394[observability]
395enable_structured_logging = true
396enable_performance_tracing = true
397enable_health_checks = true
398metrics_export_interval_secs = 30
399trace_sample_rate = 1.0
400
401[logging]
402level = "debug"
403format = "json"
404enable_file_logging = true
405log_file_path = "test.log"
406
407[metrics]
408port = 8080
409enable_custom_metrics = true
410retention_hours = 48
411
412[health_checks]
413interval_secs = 60
414memory_threshold_mb = 2000
415error_rate_threshold_percent = 10.0
416throughput_threshold_mbps = 5.0
417
418[tracing]
419enable_distributed_tracing = true
420jaeger_endpoint = "http://test:14268/api/traces"
421service_name = "test_service"
422
423[alerts]
424enable_alerts = true
425webhook_url = "http://example.com/webhook"
426error_rate_alert_threshold = 15.0
427memory_usage_alert_threshold = 85.0
428disk_usage_alert_threshold = 95.0
429"#;
430
431        let mut file = tokio::fs::File::create(temp_file.path()).await.unwrap();
432        file.write_all(config_content.as_bytes()).await.unwrap();
433        file.flush().await.unwrap();
434        drop(file);
435
436        let config = ConfigService::load_observability_config(temp_file.path())
437            .await
438            .unwrap();
439
440        assert_eq!(config.metrics.port, 8080);
441        assert_eq!(config.logging.level, "debug");
442        assert_eq!(config.logging.format, "json");
443        assert!(config.logging.enable_file_logging);
444        assert_eq!(config.health_checks.memory_threshold_mb, 2000);
445        assert!(config.tracing.enable_distributed_tracing);
446        assert!(config.alerts.enable_alerts);
447    }
448
449    #[tokio::test]
450    async fn test_get_metrics_port() {
451        let port = ConfigService::get_metrics_port().await;
452        assert!(port > 0);
453    }
454}