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}