Skip to main content

netpulse/
config.rs

1// src/config.rs — Configuration management
2//
3// NetPulseConfig can be loaded from an optional `netpulse.toml` file.
4// CLI flags ALWAYS take precedence over config file values.
5// If no config file is present, sensible defaults are used.
6
7use crate::error::NetPulseError;
8use serde::{Deserialize, Serialize};
9use std::path::Path;
10
11/// Top-level configuration for netpulse.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct NetPulseConfig {
14    /// Targets to monitor (IP addresses or host:port for TCP mode)
15    #[serde(default)]
16    pub targets: Vec<String>,
17
18    /// How often to send probes per target (milliseconds)
19    #[serde(default = "default_interval_ms")]
20    pub interval_ms: u64,
21
22    /// Probe timeout (milliseconds)
23    #[serde(default = "default_timeout_ms")]
24    pub timeout_ms: u64,
25
26    /// Number of probe samples to keep in the ring buffer per target
27    #[serde(default = "default_window_size")]
28    pub window_size: usize,
29
30    /// How often to emit a stats summary (in probe cycles)
31    #[serde(default = "default_report_every")]
32    pub report_every: u64,
33
34    /// Probe type: "icmp" or "tcp"
35    #[serde(default = "default_probe_type")]
36    pub probe_type: ProbeType,
37
38    /// Default TCP port for TCP probing
39    #[serde(default = "default_tcp_port")]
40    pub tcp_port: u16,
41}
42
43/// Which probe type to use.
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45#[serde(rename_all = "lowercase")]
46pub enum ProbeType {
47    Icmp,
48    Tcp,
49    Udp,
50}
51
52impl std::fmt::Display for ProbeType {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            ProbeType::Icmp => write!(f, "icmp"),
56            ProbeType::Tcp => write!(f, "tcp"),
57            ProbeType::Udp => write!(f, "udp"),
58        }
59    }
60}
61
62impl std::str::FromStr for ProbeType {
63    type Err = NetPulseError;
64
65    fn from_str(s: &str) -> Result<Self, Self::Err> {
66        match s.to_lowercase().as_str() {
67            "icmp" => Ok(ProbeType::Icmp),
68            "tcp" => Ok(ProbeType::Tcp),
69            "udp" => Ok(ProbeType::Udp),
70            _ => Err(NetPulseError::ConfigError(format!(
71                "unknown probe type '{}', must be 'icmp', 'tcp', or 'udp'",
72                s
73            ))),
74        }
75    }
76}
77
78fn default_interval_ms() -> u64 {
79    1000
80}
81fn default_timeout_ms() -> u64 {
82    3000
83}
84fn default_window_size() -> usize {
85    300
86} // 5 minutes at 1Hz
87fn default_report_every() -> u64 {
88    10
89} // emit summary every 10 probes
90fn default_probe_type() -> ProbeType {
91    ProbeType::Icmp
92}
93fn default_tcp_port() -> u16 {
94    80
95}
96
97impl Default for NetPulseConfig {
98    fn default() -> Self {
99        Self {
100            targets: vec![],
101            interval_ms: default_interval_ms(),
102            timeout_ms: default_timeout_ms(),
103            window_size: default_window_size(),
104            report_every: default_report_every(),
105            probe_type: default_probe_type(),
106            tcp_port: default_tcp_port(),
107        }
108    }
109}
110
111impl NetPulseConfig {
112    /// Load config from a TOML file, returning defaults if file doesn't exist.
113    pub fn from_file(path: &Path) -> Result<Self, NetPulseError> {
114        if !path.exists() {
115            return Ok(Self::default());
116        }
117
118        let content = std::fs::read_to_string(path)
119            .map_err(|e| NetPulseError::ConfigError(format!("cannot read config file: {}", e)))?;
120
121        toml::from_str(&content)
122            .map_err(|e| NetPulseError::ConfigError(format!("invalid TOML: {}", e)))
123    }
124}