proc_daemon/
config.rs

1//! Configuration management for the proc-daemon framework.
2//!
3//! This module provides a flexible configuration system that can load settings
4//! from multiple sources with clear precedence rules. Built on top of figment
5//! for maximum flexibility and performance.
6
7#[cfg(any(feature = "toml", feature = "serde_json"))]
8use figment::providers::Format;
9use figment::providers::{Env, Serialized};
10use figment::{Figment, Provider};
11use serde::{Deserialize, Serialize};
12use std::path::{Path, PathBuf};
13use std::time::Duration;
14
15use crate::error::{Error, Result};
16
17use std::fs;
18
19#[cfg(feature = "config-watch")]
20use {
21    notify::{Event, RecommendedWatcher, RecursiveMode, Result as NotifyResult, Watcher},
22    std::sync::Arc,
23};
24
25/// Log level configuration.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum LogLevel {
29    /// Trace level logging (most verbose)
30    Trace,
31    /// Debug level logging
32    Debug,
33    /// Info level logging (default)
34    Info,
35    /// Warning level logging
36    Warn,
37    /// Error level logging
38    Error,
39}
40
41impl Default for LogLevel {
42    fn default() -> Self {
43        Self::Info
44    }
45}
46
47impl From<LogLevel> for tracing::Level {
48    fn from(level: LogLevel) -> Self {
49        match level {
50            LogLevel::Trace => Self::TRACE,
51            LogLevel::Debug => Self::DEBUG,
52            LogLevel::Info => Self::INFO,
53            LogLevel::Warn => Self::WARN,
54            LogLevel::Error => Self::ERROR,
55        }
56    }
57}
58
59/// Logging configuration.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct LogConfig {
62    /// Logging level
63    pub level: LogLevel,
64    /// Enable JSON formatted logs
65    pub json: bool,
66    /// Enable colored output (ignored for JSON logs)
67    pub color: bool,
68    /// Log file path (optional)
69    pub file: Option<PathBuf>,
70    /// Maximum log file size in bytes before rotation
71    pub max_file_size: Option<u64>,
72    /// Number of rotated log files to keep
73    pub max_files: Option<u32>,
74}
75
76impl Default for LogConfig {
77    fn default() -> Self {
78        Self {
79            level: LogLevel::Info,
80            json: false,
81            color: true,
82            file: None,
83            max_file_size: Some(100 * 1024 * 1024), // 100MB
84            max_files: Some(5),
85        }
86    }
87}
88
89/// Shutdown configuration.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct ShutdownConfig {
92    /// Graceful shutdown timeout in milliseconds
93    pub graceful: u64,
94    /// Force shutdown timeout in milliseconds
95    pub force: u64,
96    /// Kill processes timeout in milliseconds
97    pub kill: u64,
98}
99
100impl Default for ShutdownConfig {
101    fn default() -> Self {
102        Self {
103            graceful: crate::DEFAULT_SHUTDOWN_TIMEOUT_MS,
104            force: 10_000, // 10 seconds
105            kill: 15_000,  // 15 seconds
106        }
107    }
108}
109
110/// Performance tuning configuration.
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct PerformanceConfig {
113    /// Number of worker threads (0 = auto-detect)
114    pub worker_threads: usize,
115    /// Enable thread pinning to CPU cores
116    pub thread_pinning: bool,
117    /// Memory pool initial size in bytes
118    pub memory_pool_size: usize,
119    /// Enable NUMA awareness
120    pub numa_aware: bool,
121    /// Enable lock-free optimizations
122    pub lock_free: bool,
123}
124
125impl Default for PerformanceConfig {
126    fn default() -> Self {
127        Self {
128            worker_threads: 0, // Auto-detect
129            thread_pinning: false,
130            memory_pool_size: 1024 * 1024, // 1MB
131            numa_aware: false,
132            lock_free: true,
133        }
134    }
135}
136
137/// Monitoring configuration.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct MonitoringConfig {
140    /// Enable metrics collection
141    pub enable_metrics: bool,
142    /// Metrics collection interval in milliseconds
143    pub metrics_interval_ms: u64,
144    /// Enable resource usage tracking
145    pub track_resources: bool,
146    /// Enable health checks
147    pub health_checks: bool,
148    /// Health check interval in milliseconds
149    pub health_check_interval_ms: u64,
150}
151
152impl Default for MonitoringConfig {
153    fn default() -> Self {
154        Self {
155            enable_metrics: true,
156            metrics_interval_ms: 1000, // 1 second
157            track_resources: true,
158            health_checks: true,
159            health_check_interval_ms: 5000, // 5 seconds
160        }
161    }
162}
163
164/// Main daemon configuration.
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct Config {
167    /// Daemon name/identifier
168    pub name: String,
169    /// Logging configuration
170    pub logging: LogConfig,
171    /// Shutdown configuration
172    pub shutdown: ShutdownConfig,
173    /// Performance configuration
174    pub performance: PerformanceConfig,
175    /// Monitoring configuration
176    pub monitoring: MonitoringConfig,
177    /// Working directory
178    pub work_dir: Option<PathBuf>,
179    /// PID file location
180    pub pid_file: Option<PathBuf>,
181    /// Enable configuration hot-reloading
182    pub hot_reload: bool,
183}
184
185impl Default for Config {
186    fn default() -> Self {
187        Self {
188            // Use a static string to avoid allocation
189            name: String::from("proc-daemon"),
190            logging: LogConfig::default(),
191            shutdown: ShutdownConfig::default(),
192            performance: PerformanceConfig::default(),
193            monitoring: MonitoringConfig::default(),
194            work_dir: None,
195            pid_file: None,
196            hot_reload: false,
197        }
198    }
199}
200
201impl Config {
202    /// Create a new config with defaults.
203    ///
204    /// # Errors
205    ///
206    /// Will return an error if the default configuration validation fails.
207    pub fn new() -> Result<Self> {
208        Ok(Self::default())
209    }
210
211    /// Load configuration from multiple sources with precedence:
212    /// 1. Default values
213    /// 2. Configuration file (if exists)
214    /// 3. Environment variables
215    /// 4. Provided overrides
216    ///
217    /// # Errors
218    ///
219    /// Will return an error if the configuration file cannot be read or contains invalid configuration data.
220    pub fn load() -> Result<Self> {
221        Self::load_from_file(crate::DEFAULT_CONFIG_FILE)
222    }
223
224    /// Load config from a file.
225    ///
226    /// # Errors
227    ///
228    /// Will return an error if the file cannot be read or contains invalid configuration data.
229    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
230        let path = path.as_ref();
231
232        // Base figment configuration
233        let base = Figment::from(Serialized::defaults(Self::default()));
234        let figment = base.merge(Env::prefixed("DAEMON_").split("_"));
235
236        // Add config file if it exists
237        let figment = if path.exists() {
238            // Try fast-path buffered TOML parsing while minimizing allocations when enabled
239            #[cfg(feature = "toml")]
240            {
241                if path.extension().and_then(|s| s.to_str()) == Some("toml") {
242                    // Attempt a single buffered read, avoid intermediate String by parsing from &str
243                    if let Ok(bytes) = fs::read(path) {
244                        if let Ok(s) = std::str::from_utf8(&bytes) {
245                            if let Ok(file_cfg) = toml::from_str::<Self>(s) {
246                                return Figment::from(Serialized::defaults(Self::default()))
247                                    .merge(Serialized::from(file_cfg, "file"))
248                                    .merge(Env::prefixed("DAEMON_").split("_"))
249                                    .extract()
250                                    .map_err(Error::from);
251                            }
252                        }
253                    }
254                }
255            }
256
257            // Provider-based path (original behavior)
258            let result = figment;
259            #[cfg(feature = "toml")]
260            let result = result.merge(figment::providers::Toml::file(path));
261
262            #[cfg(feature = "serde_json")]
263            let result = if path.extension().and_then(|s| s.to_str()) == Some("json") {
264                result.merge(figment::providers::Json::file(path))
265            } else {
266                result
267            };
268
269            result
270        } else {
271            figment
272        };
273
274        figment.extract().map_err(Error::from)
275    }
276
277    /// Load config using a configuration provider.
278    ///
279    /// # Errors
280    ///
281    /// Will return an error if the provider fails to load a valid configuration.
282    pub fn load_with_provider<P: Provider>(provider: P) -> Result<Self> {
283        Figment::from(Serialized::defaults(Self::default()))
284            .merge(Env::prefixed("DAEMON_").split("_"))
285            .merge(provider)
286            .extract()
287            .map_err(Error::from)
288    }
289
290    /// Get the shutdown timeout as a Duration.
291    #[must_use]
292    pub const fn shutdown_timeout(&self) -> Duration {
293        Duration::from_millis(self.shutdown.graceful)
294    }
295
296    /// Get the force shutdown timeout as a Duration.
297    #[must_use]
298    pub const fn force_shutdown_timeout(&self) -> Duration {
299        Duration::from_millis(self.shutdown.force)
300    }
301
302    /// Get the kill timeout as a Duration.
303    #[must_use]
304    pub const fn kill_timeout(&self) -> Duration {
305        Duration::from_millis(self.shutdown.kill)
306    }
307
308    /// Get the metrics interval as a Duration.
309    #[must_use]
310    pub const fn metrics_interval(&self) -> Duration {
311        Duration::from_millis(self.monitoring.metrics_interval_ms)
312    }
313
314    /// Get the health check interval as a Duration.
315    #[must_use]
316    pub const fn health_check_interval(&self) -> Duration {
317        Duration::from_millis(self.monitoring.health_check_interval_ms)
318    }
319
320    /// Validate the configuration.
321    ///
322    /// # Errors
323    ///
324    /// Will return an error if any configuration values are invalid or missing required fields.
325    pub fn validate(&self) -> Result<()> {
326        // Validate timeouts
327        if self.shutdown.graceful == 0 {
328            return Err(Error::config("Shutdown timeout must be greater than 0"));
329        }
330
331        if self.shutdown.force <= self.shutdown.graceful {
332            return Err(Error::config(
333                "Force timeout must be greater than graceful timeout",
334            ));
335        }
336
337        if self.shutdown.kill <= self.shutdown.force {
338            return Err(Error::config(
339                "Kill timeout must be greater than force timeout",
340            ));
341        }
342
343        // Validate performance settings
344        if self.performance.memory_pool_size == 0 {
345            return Err(Error::config("Memory pool size must be greater than 0"));
346        }
347
348        // Validate monitoring settings
349        if self.monitoring.enable_metrics && self.monitoring.metrics_interval_ms == 0 {
350            return Err(Error::config(
351                "Metrics interval must be greater than 0 when metrics are enabled",
352            ));
353        }
354
355        if self.monitoring.health_checks && self.monitoring.health_check_interval_ms == 0 {
356            return Err(Error::config(
357                "Health check interval must be greater than 0 when health checks are enabled",
358            ));
359        }
360
361        // Validate name
362        if self.name.is_empty() {
363            return Err(Error::config("Daemon name cannot be empty"));
364        }
365
366        // Validate file paths
367        if let Some(ref pid_file) = self.pid_file {
368            if let Some(parent) = pid_file.parent() {
369                if !parent.exists() {
370                    return Err(Error::config(format!(
371                        "PID file directory does not exist: {}",
372                        parent.display()
373                    )));
374                }
375            }
376        }
377
378        if let Some(ref log_file) = self.logging.file {
379            if let Some(parent) = log_file.parent() {
380                if !parent.exists() {
381                    return Err(Error::config(format!(
382                        "Log file directory does not exist: {}",
383                        parent.display()
384                    )));
385                }
386            }
387        }
388
389        Ok(())
390    }
391
392    /// Get the number of worker threads to use.
393    pub fn worker_threads(&self) -> usize {
394        if self.performance.worker_threads == 0 {
395            std::thread::available_parallelism()
396                .map(std::num::NonZeroUsize::get)
397                .unwrap_or(4)
398        } else {
399            self.performance.worker_threads
400        }
401    }
402
403    /// Check if JSON logging is enabled.
404    #[must_use]
405    pub const fn is_json_logging(&self) -> bool {
406        self.logging.json
407    }
408
409    /// Check if colored logging is enabled.
410    #[must_use]
411    pub const fn is_colored_logging(&self) -> bool {
412        self.logging.color && !self.logging.json
413    }
414
415    /// Create a builder for this configuration.
416    #[must_use]
417    pub fn builder() -> ConfigBuilder {
418        ConfigBuilder::new()
419    }
420}
421
422/// Builder for creating configurations programmatically.
423#[derive(Debug, Clone)]
424pub struct ConfigBuilder {
425    config: Config,
426}
427
428impl ConfigBuilder {
429    /// Create a new configuration builder.
430    pub fn new() -> Self {
431        Self {
432            config: Config::default(),
433        }
434    }
435
436    /// Set the daemon name.
437    pub fn name<S: Into<String>>(mut self, name: S) -> Self {
438        self.config.name = name.into();
439        self
440    }
441
442    /// Set the log level.
443    pub const fn log_level(mut self, level: LogLevel) -> Self {
444        self.config.logging.level = level;
445        self
446    }
447
448    /// Enable JSON logging.
449    pub const fn json_logging(mut self, enabled: bool) -> Self {
450        self.config.logging.json = enabled;
451        self
452    }
453
454    /// Set the shutdown timeout
455    ///
456    /// # Errors
457    ///
458    /// Will return an error if the duration exceeds `u64::MAX` milliseconds
459    pub fn shutdown_timeout(mut self, timeout: Duration) -> Result<Self> {
460        self.config.shutdown.graceful = u64::try_from(timeout.as_millis())
461            .map_err(|_| Error::config("Shutdown timeout too large"))?;
462        Ok(self)
463    }
464
465    /// Set the force shutdown timeout
466    ///
467    /// # Errors
468    ///
469    /// Will return an error if the duration exceeds `u64::MAX` milliseconds
470    pub fn force_shutdown_timeout(mut self, timeout: Duration) -> Result<Self> {
471        self.config.shutdown.force = u64::try_from(timeout.as_millis())
472            .map_err(|_| Error::config("Force shutdown timeout too large"))?;
473        Ok(self)
474    }
475
476    /// Set the kill timeout
477    ///
478    /// # Errors
479    ///
480    /// Will return an error if the duration exceeds `u64::MAX` milliseconds
481    pub fn kill_timeout(mut self, timeout: Duration) -> Result<Self> {
482        self.config.shutdown.kill = u64::try_from(timeout.as_millis())
483            .map_err(|_| Error::config("Kill timeout too large"))?;
484        Ok(self)
485    }
486
487    /// Set the working directory.
488    pub fn work_dir<P: Into<PathBuf>>(mut self, dir: P) -> Self {
489        self.config.work_dir = Some(dir.into());
490        self
491    }
492
493    /// Set the PID file location.
494    pub fn pid_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
495        self.config.pid_file = Some(path.into());
496        self
497    }
498
499    /// Enable hot-reloading of configuration.
500    pub const fn hot_reload(mut self, enabled: bool) -> Self {
501        self.config.hot_reload = enabled;
502        self
503    }
504
505    /// Set the number of worker threads.
506    pub const fn worker_threads(mut self, threads: usize) -> Self {
507        self.config.performance.worker_threads = threads;
508        self
509    }
510
511    /// Enable metrics collection.
512    pub const fn enable_metrics(mut self, enabled: bool) -> Self {
513        self.config.monitoring.enable_metrics = enabled;
514        self
515    }
516
517    /// Set the memory pool size.
518    pub const fn memory_pool_size(mut self, size: usize) -> Self {
519        self.config.performance.memory_pool_size = size;
520        self
521    }
522
523    /// Enable lock-free optimizations.
524    pub const fn lock_free(mut self, enabled: bool) -> Self {
525        self.config.performance.lock_free = enabled;
526        self
527    }
528
529    /// Build the configuration.
530    pub fn build(self) -> Result<Config> {
531        self.config.validate()?;
532        Ok(self.config)
533    }
534}
535
536impl Default for ConfigBuilder {
537    fn default() -> Self {
538        Self::new()
539    }
540}
541
542// Optional config file watcher using notify
543#[cfg(feature = "config-watch")]
544impl Config {
545    /// Start watching a configuration file and invoke a callback on changes.
546    /// This uses the same loading path as `load_from_file`, so it will pick up
547    /// any enabled fast-paths (e.g., `mmap-config`).
548    ///
549    /// The returned watcher must be kept alive for notifications to continue.
550    ///
551    /// # Errors
552    /// Returns an error if the watcher cannot be created or the path cannot be watched.
553    pub fn watch_file<F, P>(path: P, on_change: F) -> Result<RecommendedWatcher>
554    where
555        F: Fn(Result<Self>) + Send + Sync + 'static,
556        P: AsRef<Path>,
557    {
558        let path_buf: Arc<PathBuf> = Arc::new(path.as_ref().to_path_buf());
559        let cb: Arc<dyn Fn(Result<Self>) + Send + Sync> = Arc::new(on_change);
560
561        let mut watcher = notify::recommended_watcher({
562            let cb = Arc::clone(&cb);
563            let arc_path = Arc::clone(&path_buf);
564            move |res: NotifyResult<Event>| {
565                // On any filesystem event, attempt reload and notify
566                match res {
567                    Ok(_event) => {
568                        let result = Self::load_from_file(arc_path.as_ref());
569                        cb(result);
570                    }
571                    Err(e) => {
572                        // Surface error to callback as a runtime error
573                        cb(Err(Error::runtime_with_source("Config watcher error", e)));
574                    }
575                }
576            }
577        })
578        .map_err(|e| Error::runtime_with_source("Failed to create config watcher", e))?;
579
580        watcher
581            .watch(path_buf.as_ref(), RecursiveMode::NonRecursive)
582            .map_err(|e| Error::runtime_with_source("Failed to watch config path", e))?;
583
584        Ok(watcher)
585    }
586}
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591    use std::time::Duration;
592
593    #[test]
594    fn test_default_config() {
595        let config = Config::default();
596        assert_eq!(config.name, "proc-daemon");
597        assert_eq!(config.logging.level, LogLevel::Info);
598        assert!(!config.logging.json);
599        assert!(config.logging.color);
600    }
601
602    #[test]
603    fn test_config_builder() {
604        let config = Config::builder()
605            .name("test-daemon")
606            .log_level(LogLevel::Debug)
607            .json_logging(true)
608            .shutdown_timeout(Duration::from_secs(10))
609            .unwrap()
610            .force_shutdown_timeout(Duration::from_secs(20))
611            .unwrap() // Force timeout > shutdown timeout
612            .kill_timeout(Duration::from_secs(30))
613            .unwrap() // Kill timeout > force timeout
614            .worker_threads(4)
615            .build()
616            .unwrap();
617
618        assert_eq!(config.name, "test-daemon");
619        assert_eq!(config.logging.level, LogLevel::Debug);
620        assert!(config.logging.json);
621        assert_eq!(config.shutdown.graceful, 10_000);
622        assert_eq!(config.shutdown.force, 20_000);
623        assert_eq!(config.shutdown.kill, 30_000);
624        assert_eq!(config.performance.worker_threads, 4);
625    }
626
627    #[test]
628    fn test_config_validation() {
629        let mut config = Config::default();
630        config.shutdown.graceful = 0;
631        assert!(config.validate().is_err());
632
633        config.shutdown.graceful = 5000;
634        config.shutdown.force = 3000;
635        assert!(config.validate().is_err());
636
637        config.shutdown.force = 10_000;
638        config.shutdown.kill = 8_000;
639        assert!(config.validate().is_err());
640
641        config.shutdown.kill = 15_000;
642        assert!(config.validate().is_ok());
643    }
644
645    #[test]
646    fn test_log_level_conversion() {
647        assert_eq!(tracing::Level::from(LogLevel::Info), tracing::Level::INFO);
648        assert_eq!(tracing::Level::from(LogLevel::Error), tracing::Level::ERROR);
649    }
650
651    #[test]
652    fn test_duration_helpers() {
653        let config = Config::default();
654        assert_eq!(config.shutdown_timeout(), Duration::from_millis(5000));
655        assert_eq!(
656            config.force_shutdown_timeout(),
657            Duration::from_millis(10_000)
658        );
659        assert_eq!(config.kill_timeout(), Duration::from_millis(15_000));
660    }
661}