1#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum LogLevel {
29 Trace,
31 Debug,
33 Info,
35 Warn,
37 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#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct LogConfig {
62 pub level: LogLevel,
64 pub json: bool,
66 pub color: bool,
68 pub file: Option<PathBuf>,
70 pub max_file_size: Option<u64>,
72 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), max_files: Some(5),
85 }
86 }
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct ShutdownConfig {
92 pub graceful: u64,
94 pub force: u64,
96 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, kill: 15_000, }
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct PerformanceConfig {
113 pub worker_threads: usize,
115 pub thread_pinning: bool,
117 pub memory_pool_size: usize,
119 pub numa_aware: bool,
121 pub lock_free: bool,
123}
124
125impl Default for PerformanceConfig {
126 fn default() -> Self {
127 Self {
128 worker_threads: 0, thread_pinning: false,
130 memory_pool_size: 1024 * 1024, numa_aware: false,
132 lock_free: true,
133 }
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct MonitoringConfig {
140 pub enable_metrics: bool,
142 pub metrics_interval_ms: u64,
144 pub track_resources: bool,
146 pub health_checks: bool,
148 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, track_resources: true,
158 health_checks: true,
159 health_check_interval_ms: 5000, }
161 }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct Config {
167 pub name: String,
169 pub logging: LogConfig,
171 pub shutdown: ShutdownConfig,
173 pub performance: PerformanceConfig,
175 pub monitoring: MonitoringConfig,
177 pub work_dir: Option<PathBuf>,
179 pub pid_file: Option<PathBuf>,
181 pub hot_reload: bool,
183}
184
185impl Default for Config {
186 fn default() -> Self {
187 Self {
188 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 pub fn new() -> Result<Self> {
208 Ok(Self::default())
209 }
210
211 pub fn load() -> Result<Self> {
221 Self::load_from_file(crate::DEFAULT_CONFIG_FILE)
222 }
223
224 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
230 let path = path.as_ref();
231
232 let base = Figment::from(Serialized::defaults(Self::default()));
234 let figment = base.merge(Env::prefixed("DAEMON_").split("_"));
235
236 let figment = if path.exists() {
238 #[cfg(feature = "toml")]
240 {
241 if path.extension().and_then(|s| s.to_str()) == Some("toml") {
242 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 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 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 #[must_use]
292 pub const fn shutdown_timeout(&self) -> Duration {
293 Duration::from_millis(self.shutdown.graceful)
294 }
295
296 #[must_use]
298 pub const fn force_shutdown_timeout(&self) -> Duration {
299 Duration::from_millis(self.shutdown.force)
300 }
301
302 #[must_use]
304 pub const fn kill_timeout(&self) -> Duration {
305 Duration::from_millis(self.shutdown.kill)
306 }
307
308 #[must_use]
310 pub const fn metrics_interval(&self) -> Duration {
311 Duration::from_millis(self.monitoring.metrics_interval_ms)
312 }
313
314 #[must_use]
316 pub const fn health_check_interval(&self) -> Duration {
317 Duration::from_millis(self.monitoring.health_check_interval_ms)
318 }
319
320 pub fn validate(&self) -> Result<()> {
326 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 if self.performance.memory_pool_size == 0 {
345 return Err(Error::config("Memory pool size must be greater than 0"));
346 }
347
348 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 if self.name.is_empty() {
363 return Err(Error::config("Daemon name cannot be empty"));
364 }
365
366 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 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 #[must_use]
405 pub const fn is_json_logging(&self) -> bool {
406 self.logging.json
407 }
408
409 #[must_use]
411 pub const fn is_colored_logging(&self) -> bool {
412 self.logging.color && !self.logging.json
413 }
414
415 #[must_use]
417 pub fn builder() -> ConfigBuilder {
418 ConfigBuilder::new()
419 }
420}
421
422#[derive(Debug, Clone)]
424pub struct ConfigBuilder {
425 config: Config,
426}
427
428impl ConfigBuilder {
429 pub fn new() -> Self {
431 Self {
432 config: Config::default(),
433 }
434 }
435
436 pub fn name<S: Into<String>>(mut self, name: S) -> Self {
438 self.config.name = name.into();
439 self
440 }
441
442 pub const fn log_level(mut self, level: LogLevel) -> Self {
444 self.config.logging.level = level;
445 self
446 }
447
448 pub const fn json_logging(mut self, enabled: bool) -> Self {
450 self.config.logging.json = enabled;
451 self
452 }
453
454 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 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 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 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 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 pub const fn hot_reload(mut self, enabled: bool) -> Self {
501 self.config.hot_reload = enabled;
502 self
503 }
504
505 pub const fn worker_threads(mut self, threads: usize) -> Self {
507 self.config.performance.worker_threads = threads;
508 self
509 }
510
511 pub const fn enable_metrics(mut self, enabled: bool) -> Self {
513 self.config.monitoring.enable_metrics = enabled;
514 self
515 }
516
517 pub const fn memory_pool_size(mut self, size: usize) -> Self {
519 self.config.performance.memory_pool_size = size;
520 self
521 }
522
523 pub const fn lock_free(mut self, enabled: bool) -> Self {
525 self.config.performance.lock_free = enabled;
526 self
527 }
528
529 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#[cfg(feature = "config-watch")]
544impl Config {
545 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 match res {
567 Ok(_event) => {
568 let result = Self::load_from_file(arc_path.as_ref());
569 cb(result);
570 }
571 Err(e) => {
572 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() .kill_timeout(Duration::from_secs(30))
613 .unwrap() .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}