Skip to main content

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