grafatui 0.1.12

A Grafana-like TUI for Prometheus
use std::collections::BTreeSet;
use std::fmt;

use super::{AnnotationSnapshot, AnnotationTarget};

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum AnnotationTargetWarning {
    MissingPanel { title: String },
    AmbiguousPanel { title: String, matches: usize },
}

impl fmt::Display for AnnotationTargetWarning {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::MissingPanel { title } => {
                write!(
                    formatter,
                    "target \"{title}\" matches no graph/timeseries panel"
                )
            }
            Self::AmbiguousPanel { title, matches } => write!(
                formatter,
                "target \"{title}\" matches {matches} graph/timeseries panels; applied to all"
            ),
        }
    }
}

pub(crate) fn target_warnings(
    snapshot: &AnnotationSnapshot,
    eligible_panel_titles: &[String],
) -> Vec<AnnotationTargetWarning> {
    let requested = snapshot
        .events()
        .iter()
        .filter_map(|event| match &event.target {
            AnnotationTarget::All => None,
            AnnotationTarget::PanelTitles(titles) => Some(titles.iter()),
        })
        .flatten()
        .cloned()
        .collect::<BTreeSet<_>>();

    requested
        .into_iter()
        .filter_map(|title| {
            let matches = eligible_panel_titles
                .iter()
                .filter(|candidate| candidate.as_str() == title.as_str())
                .count();
            match matches {
                0 => Some(AnnotationTargetWarning::MissingPanel { title }),
                1 => None,
                matches => Some(AnnotationTargetWarning::AmbiguousPanel { title, matches }),
            }
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use crate::annotations::{
        AnnotationSnapshot, AnnotationTarget, AnnotationTargetWarning, target_warnings,
    };

    fn targeted_event(text: &str, titles: &[&str]) -> crate::annotations::AnnotationEvent {
        let mut event = crate::annotations::test_event_at(10.0, text);
        event.target = AnnotationTarget::PanelTitles(
            titles.iter().map(|title| (*title).to_string()).collect(),
        );
        event
    }

    #[test]
    fn warns_once_for_missing_and_ambiguous_titles() {
        let snapshot = AnnotationSnapshot::new(vec![
            targeted_event("first", &["CPU", "Missing"]),
            targeted_event("second", &["CPU", "Missing"]),
        ]);
        let panels = vec!["CPU".to_string(), "CPU".to_string(), "Memory".to_string()];

        assert_eq!(
            target_warnings(&snapshot, &panels),
            vec![
                AnnotationTargetWarning::AmbiguousPanel {
                    title: "CPU".to_string(),
                    matches: 2,
                },
                AnnotationTargetWarning::MissingPanel {
                    title: "Missing".to_string(),
                },
            ]
        );
    }

    #[test]
    fn all_targets_do_not_produce_warnings() {
        let snapshot =
            AnnotationSnapshot::new(vec![crate::annotations::test_event_at(10.0, "all")]);

        assert!(target_warnings(&snapshot, &["CPU".to_string()]).is_empty());
    }

    #[test]
    fn formats_target_warnings_for_the_footer() {
        assert_eq!(
            AnnotationTargetWarning::AmbiguousPanel {
                title: "CPU".to_string(),
                matches: 2,
            }
            .to_string(),
            "target \"CPU\" matches 2 graph/timeseries panels; applied to all"
        );
        assert_eq!(
            AnnotationTargetWarning::MissingPanel {
                title: "Missing".to_string(),
            }
            .to_string(),
            "target \"Missing\" matches no graph/timeseries panel"
        );
    }
}