use std::net::IpAddr;
use std::time::Duration;
use crate::{StorageError, storage::MetricCategory};
use thiserror::Error;
pub const MIN_INTERVAL: Duration = Duration::from_secs(1);
pub const MAX_INTERVAL: Duration = Duration::from_secs(30 * 24 * 60 * 60);
#[derive(Debug, Error)]
pub enum CollectorError {
#[error("network error: {0}")]
Network(#[from] std::io::Error),
#[error("timeout elapsed")]
Timeout,
#[error("failed to send metric: {0}")]
Storage(#[from] StorageError),
#[error("config error: {0}")]
Config(String),
#[error("scheduler error: {0}")]
Scheduler(String),
}
#[derive(Debug, Error)]
pub enum IpValidationError {
#[error("invalid IP address: {0}")]
InvalidIpAddress(String),
}
pub fn validate_ip_address(host: &str) -> Result<IpAddr, IpValidationError> {
host.parse::<IpAddr>()
.map_err(|_| IpValidationError::InvalidIpAddress(host.to_string()))
}
#[derive(Debug, Clone)]
pub enum Schedule {
Interval(Duration),
Cron(String),
}
impl Schedule {
pub fn interval(duration: Duration) -> Self {
if duration < MIN_INTERVAL {
tracing::warn!(min_interval = ?MIN_INTERVAL,
"Interval duration is less than minimum allowed. Using minimum duration."
);
Self::Interval(MIN_INTERVAL)
} else if duration > MAX_INTERVAL {
tracing::warn!(max_interval = ?MAX_INTERVAL,
"Interval duration exceeds maximum allowed. Using maximum duration."
);
Self::Interval(MAX_INTERVAL)
} else {
Self::Interval(duration)
}
}
pub fn cron(expr: impl AsRef<str>) -> Result<Self, CollectorError> {
use std::str::FromStr;
let test_expr = expr.as_ref();
cron::Schedule::from_str(test_expr)
.map_err(|e| CollectorError::Config(format!("invalid cron expression: {e}")))?;
Ok(Self::Cron(test_expr.to_string()))
}
}
impl std::fmt::Display for Schedule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Interval(d) => write!(f, "every {:?}", d),
Self::Cron(expr) => write!(f, "cron: {}", expr),
}
}
}
#[async_trait::async_trait]
pub trait Collector: Send + Sync + 'static {
fn name(&self) -> &str;
fn category(&self) -> MetricCategory;
fn schedule(&self) -> Schedule;
fn upsert_metric_series(&self) -> Result<u64, CollectorError>;
async fn collect(&self) -> Result<(), CollectorError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_schedule_interval_minimum() {
let schedule = Schedule::interval(Duration::from_millis(100));
match schedule {
Schedule::Interval(d) => assert_eq!(d, MIN_INTERVAL),
_ => panic!("expected Interval"),
}
}
#[test]
fn test_schedule_interval_maximum() {
let schedule = Schedule::interval(Duration::from_secs(31 * 24 * 60 * 60));
match schedule {
Schedule::Interval(d) => assert_eq!(d, MAX_INTERVAL),
_ => panic!("expected Interval"),
}
}
#[test]
fn test_schedule_interval_valid() {
let schedule = Schedule::interval(Duration::from_secs(30));
match schedule {
Schedule::Interval(d) => assert_eq!(d, Duration::from_secs(30)),
_ => panic!("expected Interval"),
}
}
#[test]
fn test_schedule_cron_valid() {
let schedule = Schedule::cron("0 */5 * * * *").unwrap();
match schedule {
Schedule::Cron(expr) => assert_eq!(expr, "0 */5 * * * *"),
_ => panic!("expected Cron"),
}
}
#[test]
fn test_schedule_cron_invalid() {
let result = Schedule::cron("not a cron");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("invalid cron"));
}
#[test]
fn test_schedule_display_interval() {
let schedule = Schedule::interval(Duration::from_secs(60));
let display = schedule.to_string();
assert!(display.contains("60"));
}
#[test]
fn test_schedule_display_cron() {
let schedule = Schedule::cron("0 0 * * * *").unwrap();
let display = schedule.to_string();
assert!(display.contains("0 0 * * * *"));
}
#[test]
fn test_validate_ip_address_ipv4() {
let result = validate_ip_address("127.0.0.1");
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1))
);
}
#[test]
fn test_validate_ip_address_ipv6() {
let result = validate_ip_address("::1");
assert!(result.is_ok());
assert_eq!(result.unwrap(), IpAddr::V6(std::net::Ipv6Addr::LOCALHOST));
}
#[test]
fn test_validate_ip_address_invalid() {
let result = validate_ip_address("not-an-ip");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, IpValidationError::InvalidIpAddress(_)));
assert!(err.to_string().contains("not-an-ip"));
}
#[test]
fn test_validate_ip_address_hostname() {
let result = validate_ip_address("google.com");
assert!(result.is_err());
}
#[test]
fn test_validate_ip_address_empty() {
let result = validate_ip_address("");
assert!(result.is_err());
}
}