#[cfg(any(feature = "toml", feature = "serde_json"))]
use figment::providers::Format;
use figment::providers::{Env, Serialized};
use figment::{Figment, Provider};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::error::{Error, Result};
#[cfg(feature = "toml")]
use std::fs;
#[cfg(feature = "config-watch")]
use {
notify::{Event, RecommendedWatcher, RecursiveMode, Result as NotifyResult, Watcher},
std::sync::Arc,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Trace,
Debug,
#[default]
Info,
Warn,
Error,
}
impl From<LogLevel> for tracing::Level {
fn from(level: LogLevel) -> Self {
match level {
LogLevel::Trace => Self::TRACE,
LogLevel::Debug => Self::DEBUG,
LogLevel::Info => Self::INFO,
LogLevel::Warn => Self::WARN,
LogLevel::Error => Self::ERROR,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogConfig {
pub level: LogLevel,
pub json: bool,
pub color: bool,
pub file: Option<PathBuf>,
pub max_file_size: Option<u64>,
pub max_files: Option<u32>,
}
impl Default for LogConfig {
fn default() -> Self {
Self {
level: LogLevel::Info,
json: false,
color: true,
file: None,
max_file_size: Some(100 * 1024 * 1024), max_files: Some(5),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShutdownConfig {
pub graceful: u64,
pub force: u64,
pub kill: u64,
}
impl Default for ShutdownConfig {
fn default() -> Self {
Self {
graceful: crate::DEFAULT_SHUTDOWN_TIMEOUT_MS,
force: 10_000, kill: 15_000, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceConfig {
pub worker_threads: usize,
pub thread_pinning: bool,
pub memory_pool_size: usize,
pub numa_aware: bool,
pub lock_free: bool,
}
impl Default for PerformanceConfig {
fn default() -> Self {
Self {
worker_threads: 0, thread_pinning: false,
memory_pool_size: 1024 * 1024, numa_aware: false,
lock_free: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitoringConfig {
pub enable_metrics: bool,
pub metrics_interval_ms: u64,
pub track_resources: bool,
pub health_checks: bool,
pub health_check_interval_ms: u64,
}
impl Default for MonitoringConfig {
fn default() -> Self {
Self {
enable_metrics: true,
metrics_interval_ms: 1000, track_resources: true,
health_checks: true,
health_check_interval_ms: 5000, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub name: String,
pub logging: LogConfig,
pub shutdown: ShutdownConfig,
pub performance: PerformanceConfig,
pub monitoring: MonitoringConfig,
pub work_dir: Option<PathBuf>,
pub pid_file: Option<PathBuf>,
pub hot_reload: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
name: String::from("proc-daemon"),
logging: LogConfig::default(),
shutdown: ShutdownConfig::default(),
performance: PerformanceConfig::default(),
monitoring: MonitoringConfig::default(),
work_dir: None,
pid_file: None,
hot_reload: false,
}
}
}
impl Config {
pub fn new() -> Result<Self> {
let config = Self::default();
config.validate()?;
Ok(config)
}
pub fn load() -> Result<Self> {
Self::load_from_file(crate::DEFAULT_CONFIG_FILE)
}
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let base = Figment::from(Serialized::defaults(Self::default()));
let figment = base.merge(Env::prefixed("DAEMON_").split("_"));
let figment = if path.exists() {
#[cfg(feature = "toml")]
{
if path.extension().and_then(|s| s.to_str()) == Some("toml") {
if let Ok(bytes) = fs::read(path) {
if let Ok(s) = std::str::from_utf8(&bytes) {
if let Ok(file_cfg) = toml::from_str::<Self>(s) {
return Figment::from(Serialized::defaults(Self::default()))
.merge(Serialized::from(file_cfg, "file"))
.merge(Env::prefixed("DAEMON_").split("_"))
.extract()
.map_err(Error::from);
}
}
}
}
}
let result = figment;
#[cfg(feature = "toml")]
let result = result.merge(figment::providers::Toml::file(path));
#[cfg(feature = "serde_json")]
let result = if path.extension().and_then(|s| s.to_str()) == Some("json") {
result.merge(figment::providers::Json::file(path))
} else {
result
};
result
} else {
figment
};
figment.extract().map_err(Error::from)
}
pub fn load_with_provider<P: Provider>(provider: P) -> Result<Self> {
Figment::from(Serialized::defaults(Self::default()))
.merge(Env::prefixed("DAEMON_").split("_"))
.merge(provider)
.extract()
.map_err(Error::from)
}
#[must_use]
pub const fn shutdown_timeout(&self) -> Duration {
Duration::from_millis(self.shutdown.graceful)
}
#[must_use]
pub const fn force_shutdown_timeout(&self) -> Duration {
Duration::from_millis(self.shutdown.force)
}
#[must_use]
pub const fn kill_timeout(&self) -> Duration {
Duration::from_millis(self.shutdown.kill)
}
#[must_use]
pub const fn metrics_interval(&self) -> Duration {
Duration::from_millis(self.monitoring.metrics_interval_ms)
}
#[must_use]
pub const fn health_check_interval(&self) -> Duration {
Duration::from_millis(self.monitoring.health_check_interval_ms)
}
pub fn validate(&self) -> Result<()> {
if self.shutdown.graceful == 0 {
return Err(Error::config("Shutdown timeout must be greater than 0"));
}
if self.shutdown.force <= self.shutdown.graceful {
return Err(Error::config(
"Force timeout must be greater than graceful timeout",
));
}
if self.shutdown.kill <= self.shutdown.force {
return Err(Error::config(
"Kill timeout must be greater than force timeout",
));
}
if self.performance.memory_pool_size == 0 {
return Err(Error::config("Memory pool size must be greater than 0"));
}
if self.monitoring.enable_metrics && self.monitoring.metrics_interval_ms == 0 {
return Err(Error::config(
"Metrics interval must be greater than 0 when metrics are enabled",
));
}
if self.monitoring.health_checks && self.monitoring.health_check_interval_ms == 0 {
return Err(Error::config(
"Health check interval must be greater than 0 when health checks are enabled",
));
}
if self.name.is_empty() {
return Err(Error::config("Daemon name cannot be empty"));
}
if let Some(ref pid_file) = self.pid_file {
if let Some(parent) = pid_file.parent() {
if !parent.exists() {
return Err(Error::config(format!(
"PID file directory does not exist: {}",
parent.display()
)));
}
}
}
if let Some(ref work_dir) = self.work_dir {
if !work_dir.exists() {
return Err(Error::config(format!(
"Working directory does not exist: {}",
work_dir.display()
)));
}
if !work_dir.is_dir() {
return Err(Error::config(format!(
"Working directory is not a directory: {}",
work_dir.display()
)));
}
}
if let Some(ref log_file) = self.logging.file {
if let Some(parent) = log_file.parent() {
if !parent.exists() {
return Err(Error::config(format!(
"Log file directory does not exist: {}",
parent.display()
)));
}
}
}
if let Some(max_size) = self.logging.max_file_size {
if max_size == 0 {
return Err(Error::config("Log max_file_size must be greater than 0"));
}
}
Ok(())
}
pub fn worker_threads(&self) -> usize {
if self.performance.worker_threads == 0 {
std::thread::available_parallelism().map_or(4, std::num::NonZeroUsize::get)
} else {
self.performance.worker_threads
}
}
#[must_use]
pub const fn is_json_logging(&self) -> bool {
self.logging.json
}
#[must_use]
pub const fn is_colored_logging(&self) -> bool {
self.logging.color && !self.logging.json
}
#[must_use]
pub fn builder() -> ConfigBuilder {
ConfigBuilder::new()
}
}
#[derive(Debug, Clone)]
pub struct ConfigBuilder {
config: Config,
}
impl ConfigBuilder {
pub fn new() -> Self {
Self {
config: Config::default(),
}
}
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.config.name = name.into();
self
}
pub const fn log_level(mut self, level: LogLevel) -> Self {
self.config.logging.level = level;
self
}
pub const fn json_logging(mut self, enabled: bool) -> Self {
self.config.logging.json = enabled;
self
}
pub fn shutdown_timeout(mut self, timeout: Duration) -> Result<Self> {
self.config.shutdown.graceful = u64::try_from(timeout.as_millis())
.map_err(|_| Error::config("Shutdown timeout too large"))?;
Ok(self)
}
pub fn force_shutdown_timeout(mut self, timeout: Duration) -> Result<Self> {
self.config.shutdown.force = u64::try_from(timeout.as_millis())
.map_err(|_| Error::config("Force shutdown timeout too large"))?;
Ok(self)
}
pub fn kill_timeout(mut self, timeout: Duration) -> Result<Self> {
self.config.shutdown.kill = u64::try_from(timeout.as_millis())
.map_err(|_| Error::config("Kill timeout too large"))?;
Ok(self)
}
pub fn work_dir<P: Into<PathBuf>>(mut self, dir: P) -> Self {
self.config.work_dir = Some(dir.into());
self
}
pub fn pid_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.config.pid_file = Some(path.into());
self
}
pub const fn hot_reload(mut self, enabled: bool) -> Self {
self.config.hot_reload = enabled;
self
}
pub const fn worker_threads(mut self, threads: usize) -> Self {
self.config.performance.worker_threads = threads;
self
}
pub const fn enable_metrics(mut self, enabled: bool) -> Self {
self.config.monitoring.enable_metrics = enabled;
self
}
pub const fn memory_pool_size(mut self, size: usize) -> Self {
self.config.performance.memory_pool_size = size;
self
}
pub const fn lock_free(mut self, enabled: bool) -> Self {
self.config.performance.lock_free = enabled;
self
}
pub fn build(self) -> Result<Config> {
self.config.validate()?;
Ok(self.config)
}
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "config-watch")]
impl Config {
pub fn watch_file<F, P>(path: P, on_change: F) -> Result<RecommendedWatcher>
where
F: Fn(Result<Self>) + Send + Sync + 'static,
P: AsRef<Path>,
{
let path_buf: Arc<PathBuf> = Arc::new(path.as_ref().to_path_buf());
let cb: Arc<dyn Fn(Result<Self>) + Send + Sync> = Arc::new(on_change);
let mut watcher = notify::recommended_watcher({
let cb = Arc::clone(&cb);
let arc_path = Arc::clone(&path_buf);
move |res: NotifyResult<Event>| {
match res {
Ok(_event) => {
let result = Self::load_from_file(arc_path.as_ref());
cb(result);
}
Err(e) => {
cb(Err(Error::runtime_with_source("Config watcher error", e)));
}
}
}
})
.map_err(|e| Error::runtime_with_source("Failed to create config watcher", e))?;
watcher
.watch(path_buf.as_ref(), RecursiveMode::NonRecursive)
.map_err(|e| Error::runtime_with_source("Failed to watch config path", e))?;
Ok(watcher)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.name, "proc-daemon");
assert_eq!(config.logging.level, LogLevel::Info);
assert!(!config.logging.json);
assert!(config.logging.color);
}
#[test]
fn test_config_builder() {
let config = Config::builder()
.name("test-daemon")
.log_level(LogLevel::Debug)
.json_logging(true)
.shutdown_timeout(Duration::from_secs(10))
.unwrap()
.force_shutdown_timeout(Duration::from_secs(20))
.unwrap() .kill_timeout(Duration::from_secs(30))
.unwrap() .worker_threads(4)
.build()
.unwrap();
assert_eq!(config.name, "test-daemon");
assert_eq!(config.logging.level, LogLevel::Debug);
assert!(config.logging.json);
assert_eq!(config.shutdown.graceful, 10_000);
assert_eq!(config.shutdown.force, 20_000);
assert_eq!(config.shutdown.kill, 30_000);
assert_eq!(config.performance.worker_threads, 4);
}
#[test]
fn test_config_validation() {
let mut config = Config::default();
config.shutdown.graceful = 0;
assert!(config.validate().is_err());
config.shutdown.graceful = 5000;
config.shutdown.force = 3000;
assert!(config.validate().is_err());
config.shutdown.force = 10_000;
config.shutdown.kill = 8_000;
assert!(config.validate().is_err());
config.shutdown.kill = 15_000;
assert!(config.validate().is_ok());
}
#[test]
fn test_log_level_conversion() {
assert_eq!(tracing::Level::from(LogLevel::Info), tracing::Level::INFO);
assert_eq!(tracing::Level::from(LogLevel::Error), tracing::Level::ERROR);
}
#[test]
fn test_duration_helpers() {
let config = Config::default();
assert_eq!(config.shutdown_timeout(), Duration::from_secs(5));
assert_eq!(config.force_shutdown_timeout(), Duration::from_secs(10));
assert_eq!(config.kill_timeout(), Duration::from_secs(15));
}
}