use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::models::validation::{ConfigValidator, Diagnostic, Severity, YamlType};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrometheusGlobal {
#[serde(default)]
pub scrape_interval: Option<String>,
#[serde(default)]
pub scrape_timeout: Option<String>,
#[serde(default)]
pub evaluation_interval: Option<String>,
#[serde(default)]
pub external_labels: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StaticConfig {
pub targets: Vec<String>,
#[serde(default)]
pub labels: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScrapeConfig {
pub job_name: String,
#[serde(default)]
pub static_configs: Vec<StaticConfig>,
#[serde(default)]
pub scrape_interval: Option<String>,
#[serde(default)]
pub scrape_timeout: Option<String>,
#[serde(default)]
pub metrics_path: Option<String>,
#[serde(default)]
pub scheme: Option<String>,
#[serde(default)]
pub honor_labels: Option<bool>,
#[serde(default)]
pub honor_timestamps: Option<bool>,
#[serde(default)]
pub params: Option<serde_json::Value>,
#[serde(default)]
pub basic_auth: Option<serde_json::Value>,
#[serde(default)]
pub bearer_token: Option<String>,
#[serde(default)]
pub bearer_token_file: Option<String>,
#[serde(default)]
pub tls_config: Option<serde_json::Value>,
#[serde(default)]
pub relabel_configs: Vec<serde_json::Value>,
#[serde(default)]
pub metric_relabel_configs: Vec<serde_json::Value>,
#[serde(default)]
pub kubernetes_sd_configs: Vec<serde_json::Value>,
#[serde(default)]
pub consul_sd_configs: Vec<serde_json::Value>,
#[serde(default)]
pub dns_sd_configs: Vec<serde_json::Value>,
#[serde(default)]
pub file_sd_configs: Vec<serde_json::Value>,
#[serde(default)]
pub ec2_sd_configs: Vec<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertingAlertmanager {
#[serde(default)]
pub static_configs: Vec<StaticConfig>,
#[serde(default)]
pub scheme: Option<String>,
#[serde(default)]
pub path_prefix: Option<String>,
#[serde(default)]
pub timeout: Option<String>,
#[serde(default)]
pub tls_config: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertingConfig {
#[serde(default)]
pub alertmanagers: Vec<AlertingAlertmanager>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrometheusConfig {
#[serde(default)]
pub global: Option<PrometheusGlobal>,
#[serde(default)]
pub scrape_configs: Vec<ScrapeConfig>,
#[serde(default)]
pub rule_files: Vec<String>,
#[serde(default)]
pub alerting: Option<AlertingConfig>,
#[serde(default)]
pub remote_write: Vec<serde_json::Value>,
#[serde(default)]
pub remote_read: Vec<serde_json::Value>,
#[serde(default)]
pub storage: Option<serde_json::Value>,
}
impl PrometheusConfig {
pub fn from_value(data: serde_json::Value) -> Result<Self, String> {
serde_json::from_value(data)
.map_err(|e| format!("Failed to parse Prometheus config: {e}"))
}
}
fn parse_duration_secs(s: &str) -> Option<u64> {
let s = s.trim();
if let Some(rest) = s.strip_suffix('s') {
return rest.parse().ok();
}
if let Some(rest) = s.strip_suffix('m') {
return rest.parse::<u64>().ok().map(|m| m * 60);
}
if let Some(rest) = s.strip_suffix('h') {
return rest.parse::<u64>().ok().map(|h| h * 3600);
}
if let Some(rest) = s.strip_suffix('d') {
return rest.parse::<u64>().ok().map(|d| d * 86400);
}
None
}
impl ConfigValidator for PrometheusConfig {
fn yaml_type(&self) -> YamlType { YamlType::Prometheus }
fn validate_structure(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
if self.scrape_configs.is_empty() {
diags.push(Diagnostic {
severity: Severity::Error,
message: "No scrape_configs defined — Prometheus won't scrape any targets".into(),
path: Some("scrape_configs".into()),
});
}
let mut job_names: HashMap<&str, usize> = HashMap::new();
for sc in &self.scrape_configs {
*job_names.entry(&sc.job_name).or_insert(0) += 1;
}
for (name, count) in &job_names {
if *count > 1 {
diags.push(Diagnostic {
severity: Severity::Error,
message: format!("Duplicate job_name '{}' found {} times", name, count),
path: Some("scrape_configs > job_name".into()),
});
}
}
diags
}
fn validate_semantics(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
if let Some(global) = &self.global {
if let Some(interval) = &global.scrape_interval
&& let Some(secs) = parse_duration_secs(interval)
&& secs < 5 {
diags.push(Diagnostic {
severity: Severity::Warning,
message: format!("global.scrape_interval='{}' is very aggressive — may overload targets", interval),
path: Some("global > scrape_interval".into()),
});
}
if let Some(timeout) = &global.scrape_timeout
&& let (Some(t_secs), Some(i_secs)) = (
parse_duration_secs(timeout),
global.scrape_interval.as_ref().and_then(|i| parse_duration_secs(i)),
)
&& t_secs > i_secs {
diags.push(Diagnostic {
severity: Severity::Warning,
message: format!("global.scrape_timeout ({}) > scrape_interval ({}) — scrapes may overlap", timeout, global.scrape_interval.as_deref().unwrap_or("?")),
path: Some("global > scrape_timeout".into()),
});
}
}
for sc in &self.scrape_configs {
if sc.static_configs.is_empty()
&& sc.kubernetes_sd_configs.is_empty()
&& sc.consul_sd_configs.is_empty()
&& sc.dns_sd_configs.is_empty()
&& sc.file_sd_configs.is_empty()
&& sc.ec2_sd_configs.is_empty()
{
diags.push(Diagnostic {
severity: Severity::Warning,
message: format!("Job '{}': no targets or service discovery configured", sc.job_name),
path: Some(format!("scrape_configs > {}", sc.job_name)),
});
}
}
if self.alerting.is_none() && !self.rule_files.is_empty() {
diags.push(Diagnostic {
severity: Severity::Warning,
message: "rule_files defined but no alerting config — alerts won't be delivered".into(),
path: Some("alerting".into()),
});
}
diags
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertmanagerGlobal {
#[serde(default)]
pub smtp_smarthost: Option<String>,
#[serde(default)]
pub smtp_from: Option<String>,
#[serde(default)]
pub smtp_auth_username: Option<String>,
#[serde(default)]
pub smtp_require_tls: Option<bool>,
#[serde(default)]
pub slack_api_url: Option<String>,
#[serde(default)]
pub pagerduty_url: Option<String>,
#[serde(default)]
pub resolve_timeout: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertmanagerRoute {
#[serde(default)]
pub receiver: Option<String>,
#[serde(default)]
pub group_by: Vec<String>,
#[serde(default)]
pub group_wait: Option<String>,
#[serde(default)]
pub group_interval: Option<String>,
#[serde(default)]
pub repeat_interval: Option<String>,
#[serde(default, rename = "match")]
pub match_labels: Option<HashMap<String, String>>,
#[serde(default)]
pub match_re: Option<HashMap<String, String>>,
#[serde(default)]
pub matchers: Vec<String>,
#[serde(default)]
pub routes: Vec<AlertmanagerRoute>,
#[serde(default, rename = "continue")]
pub continue_matching: Option<bool>,
#[serde(default)]
pub mute_time_intervals: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertmanagerReceiver {
pub name: String,
#[serde(default)]
pub email_configs: Vec<serde_json::Value>,
#[serde(default)]
pub slack_configs: Vec<serde_json::Value>,
#[serde(default)]
pub pagerduty_configs: Vec<serde_json::Value>,
#[serde(default)]
pub webhook_configs: Vec<serde_json::Value>,
#[serde(default)]
pub opsgenie_configs: Vec<serde_json::Value>,
#[serde(default)]
pub victorops_configs: Vec<serde_json::Value>,
#[serde(default)]
pub pushover_configs: Vec<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertmanagerInhibitRule {
#[serde(default)]
pub source_match: Option<HashMap<String, String>>,
#[serde(default)]
pub source_match_re: Option<HashMap<String, String>>,
#[serde(default)]
pub source_matchers: Vec<String>,
#[serde(default)]
pub target_match: Option<HashMap<String, String>>,
#[serde(default)]
pub target_match_re: Option<HashMap<String, String>>,
#[serde(default)]
pub target_matchers: Vec<String>,
#[serde(default)]
pub equal: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertmanagerConfig {
#[serde(default)]
pub global: Option<AlertmanagerGlobal>,
pub route: AlertmanagerRoute,
#[serde(default)]
pub receivers: Vec<AlertmanagerReceiver>,
#[serde(default)]
pub inhibit_rules: Vec<AlertmanagerInhibitRule>,
#[serde(default)]
pub templates: Vec<String>,
#[serde(default)]
pub time_intervals: Vec<serde_json::Value>,
#[serde(default)]
pub mute_time_intervals: Vec<serde_json::Value>,
}
impl AlertmanagerConfig {
pub fn from_value(data: serde_json::Value) -> Result<Self, String> {
serde_json::from_value(data)
.map_err(|e| format!("Failed to parse Alertmanager config: {e}"))
}
}
impl ConfigValidator for AlertmanagerConfig {
fn yaml_type(&self) -> YamlType { YamlType::Alertmanager }
fn validate_structure(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
if self.route.receiver.is_none() {
diags.push(Diagnostic {
severity: Severity::Error,
message: "Root route must have a 'receiver'".into(),
path: Some("route > receiver".into()),
});
}
if self.receivers.is_empty() {
diags.push(Diagnostic {
severity: Severity::Error,
message: "No receivers defined".into(),
path: Some("receivers".into()),
});
}
let receiver_names: Vec<&str> = self.receivers.iter().map(|r| r.name.as_str()).collect();
if let Some(root_receiver) = &self.route.receiver
&& !receiver_names.contains(&root_receiver.as_str()) {
diags.push(Diagnostic {
severity: Severity::Error,
message: format!("Root route receiver '{}' is not defined", root_receiver),
path: Some("route > receiver".into()),
});
}
check_route_receivers(&self.route, &receiver_names, &mut diags);
diags
}
fn validate_semantics(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
for r in &self.receivers {
if r.email_configs.is_empty()
&& r.slack_configs.is_empty()
&& r.pagerduty_configs.is_empty()
&& r.webhook_configs.is_empty()
&& r.opsgenie_configs.is_empty()
&& r.victorops_configs.is_empty()
&& r.pushover_configs.is_empty()
{
diags.push(Diagnostic {
severity: Severity::Warning,
message: format!("Receiver '{}' has no notification channels configured — alerts will be silently dropped", r.name),
path: Some(format!("receivers > {}", r.name)),
});
}
}
if self.route.group_by.is_empty() {
diags.push(Diagnostic {
severity: Severity::Info,
message: "Root route has no group_by — all alerts will be grouped together".into(),
path: Some("route > group_by".into()),
});
}
diags
}
}
fn check_route_receivers(route: &AlertmanagerRoute, receivers: &[&str], diags: &mut Vec<Diagnostic>) {
for sub in &route.routes {
if let Some(recv) = &sub.receiver
&& !receivers.contains(&recv.as_str()) {
diags.push(Diagnostic {
severity: Severity::Error,
message: format!("Sub-route receiver '{}' is not defined", recv),
path: Some("route > routes > receiver".into()),
});
}
check_route_receivers(sub, receivers, diags);
}
}