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
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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
28#[serde(rename_all = "lowercase")]
29pub enum LogLevel {
30 Trace,
32 Debug,
34 #[default]
36 Info,
37 Warn,
39 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#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct LogConfig {
58 pub level: LogLevel,
60 pub json: bool,
62 pub color: bool,
64 pub file: Option<PathBuf>,
66 pub max_file_size: Option<u64>,
68 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), max_files: Some(5),
81 }
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct ShutdownConfig {
88 pub graceful: u64,
90 pub force: u64,
92 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, kill: 15_000, }
103 }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct PerformanceConfig {
109 pub worker_threads: usize,
111 pub thread_pinning: bool,
113 pub memory_pool_size: usize,
115 pub numa_aware: bool,
117 pub lock_free: bool,
119}
120
121impl Default for PerformanceConfig {
122 fn default() -> Self {
123 Self {
124 worker_threads: 0, thread_pinning: false,
126 memory_pool_size: 1024 * 1024, numa_aware: false,
128 lock_free: true,
129 }
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct MonitoringConfig {
136 pub enable_metrics: bool,
138 pub metrics_interval_ms: u64,
140 pub track_resources: bool,
142 pub health_checks: bool,
144 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, track_resources: true,
154 health_checks: true,
155 health_check_interval_ms: 5000, }
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct Config {
163 pub name: String,
165 pub logging: LogConfig,
167 pub shutdown: ShutdownConfig,
169 pub performance: PerformanceConfig,
171 pub monitoring: MonitoringConfig,
173 pub work_dir: Option<PathBuf>,
175 pub pid_file: Option<PathBuf>,
177 pub hot_reload: bool,
179}
180
181impl Default for Config {
182 fn default() -> Self {
183 Self {
184 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 pub fn new() -> Result<Self> {
204 let config = Self::default();
205 config.validate()?;
206 Ok(config)
207 }
208
209 pub fn load() -> Result<Self> {
219 Self::load_from_file(crate::DEFAULT_CONFIG_FILE)
220 }
221
222 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
228 let path = path.as_ref();
229
230 let base = Figment::from(Serialized::defaults(Self::default()));
232 let figment = base.merge(Env::prefixed("DAEMON_").split("_"));
233
234 let figment = if path.exists() {
236 #[cfg(feature = "toml")]
238 {
239 if path.extension().and_then(|s| s.to_str()) == Some("toml") {
240 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 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 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 #[must_use]
290 pub const fn shutdown_timeout(&self) -> Duration {
291 Duration::from_millis(self.shutdown.graceful)
292 }
293
294 #[must_use]
296 pub const fn force_shutdown_timeout(&self) -> Duration {
297 Duration::from_millis(self.shutdown.force)
298 }
299
300 #[must_use]
302 pub const fn kill_timeout(&self) -> Duration {
303 Duration::from_millis(self.shutdown.kill)
304 }
305
306 #[must_use]
308 pub const fn metrics_interval(&self) -> Duration {
309 Duration::from_millis(self.monitoring.metrics_interval_ms)
310 }
311
312 #[must_use]
314 pub const fn health_check_interval(&self) -> Duration {
315 Duration::from_millis(self.monitoring.health_check_interval_ms)
316 }
317
318 pub fn validate(&self) -> Result<()> {
324 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 if self.performance.memory_pool_size == 0 {
343 return Err(Error::config("Memory pool size must be greater than 0"));
344 }
345
346 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 if self.name.is_empty() {
361 return Err(Error::config("Daemon name cannot be empty"));
362 }
363
364 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 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 #[must_use]
424 pub const fn is_json_logging(&self) -> bool {
425 self.logging.json
426 }
427
428 #[must_use]
430 pub const fn is_colored_logging(&self) -> bool {
431 self.logging.color && !self.logging.json
432 }
433
434 #[must_use]
436 pub fn builder() -> ConfigBuilder {
437 ConfigBuilder::new()
438 }
439}
440
441#[derive(Debug, Clone)]
443pub struct ConfigBuilder {
444 config: Config,
445}
446
447impl ConfigBuilder {
448 pub fn new() -> Self {
450 Self {
451 config: Config::default(),
452 }
453 }
454
455 pub fn name<S: Into<String>>(mut self, name: S) -> Self {
457 self.config.name = name.into();
458 self
459 }
460
461 pub const fn log_level(mut self, level: LogLevel) -> Self {
463 self.config.logging.level = level;
464 self
465 }
466
467 pub const fn json_logging(mut self, enabled: bool) -> Self {
469 self.config.logging.json = enabled;
470 self
471 }
472
473 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 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 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 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 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 pub const fn hot_reload(mut self, enabled: bool) -> Self {
520 self.config.hot_reload = enabled;
521 self
522 }
523
524 pub const fn worker_threads(mut self, threads: usize) -> Self {
526 self.config.performance.worker_threads = threads;
527 self
528 }
529
530 pub const fn enable_metrics(mut self, enabled: bool) -> Self {
532 self.config.monitoring.enable_metrics = enabled;
533 self
534 }
535
536 pub const fn memory_pool_size(mut self, size: usize) -> Self {
538 self.config.performance.memory_pool_size = size;
539 self
540 }
541
542 pub const fn lock_free(mut self, enabled: bool) -> Self {
544 self.config.performance.lock_free = enabled;
545 self
546 }
547
548 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#[cfg(feature = "config-watch")]
563impl Config {
564 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 match res {
586 Ok(_event) => {
587 let result = Self::load_from_file(arc_path.as_ref());
588 cb(result);
589 }
590 Err(e) => {
591 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() .kill_timeout(Duration::from_secs(30))
632 .unwrap() .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}