use std::collections::BTreeMap;
use serde_json::Value;
use zenkey::{SliceToken, SubjectKind};
use crate::judge::common::{EXPANSION_CAP, FINDING_CAP};
use crate::model::examples::Examples;
use crate::report::{CheckId, DoctorFinding, DoctorSeverity};
pub type ProducerId = (String, String);
#[derive(Debug)]
pub struct KeyKind {
pub producer: ProducerId,
pub declared: SubjectKind,
pub judged: u64,
pub tag_mismatches: u64,
pub value_mismatches: u64,
pub decreases: u64,
pub undecoded: u64,
examples: Examples<String>,
last: Option<f64>,
generation: u64,
}
#[derive(Debug, Default)]
pub struct KindObservation {
keys: BTreeMap<String, KeyKind>,
cycles: BTreeMap<ProducerId, u64>,
}
fn payload_tag(doc: &Value) -> Option<SubjectKind> {
doc.get("type")
.and_then(Value::as_str)
.and_then(SubjectKind::from_payload_tag)
}
fn leaf(doc: &Value) -> &Value {
match doc.get("value") {
Some(v) if doc.is_object() => v,
_ => doc,
}
}
fn stated_bounds(v: &Value) -> Option<Vec<f64>> {
let items = v.get("buckets")?.as_array()?;
let mut out = Vec::with_capacity(items.len());
for item in items {
let bound = match item {
Value::Number(n) => n.as_f64()?,
Value::Object(o) => match o.get("le")? {
Value::Number(n) => n.as_f64()?,
Value::String(s) if s == "+Inf" || s == "inf" || s == "Infinity" => f64::INFINITY,
_ => return None,
},
Value::String(s) if s == "+Inf" || s == "inf" || s == "Infinity" => f64::INFINITY,
_ => return None,
};
out.push(bound);
}
if out.last().is_some_and(|b| b.is_infinite()) {
out.pop();
}
Some(out)
}
fn bounds_disagree(declared: &[f64], stated: &[f64]) -> Option<String> {
if let Some(i) = declared
.iter()
.zip(stated)
.position(|(d, s)| d.to_bits() != s.to_bits())
{
return Some(format!(
"histogram bound {} is {}, registry declares {}",
i + 1,
stated[i],
declared[i]
));
}
(declared.len() != stated.len()).then(|| {
format!(
"histogram states {} bound(s), registry declares {}",
stated.len(),
declared.len()
)
})
}
fn describe(v: &Value) -> String {
match v {
Value::Null => "null".into(),
Value::Bool(b) => format!("boolean {b}"),
Value::Number(n) => format!("number {n}"),
Value::String(s) if s.len() > 24 => format!("string {:?}…", &s[..24]),
Value::String(s) => format!("string {s:?}"),
Value::Array(a) => format!("array of {}", a.len()),
Value::Object(o) => format!("object with {} field(s)", o.len()),
}
}
impl KindObservation {
pub fn new() -> KindObservation {
KindObservation::default()
}
pub fn alive_cycled(&mut self, origin: &str, producer: &str) {
*self
.cycles
.entry((origin.to_string(), producer.to_string()))
.or_default() += 1;
}
pub fn observe(
&mut self,
key: &str,
origin: &str,
producer: &str,
declared: SubjectKind,
doc: Option<&Value>,
) {
self.observe_declared(key, origin, producer, declared, None, doc);
}
pub fn observe_declared(
&mut self,
key: &str,
origin: &str,
producer: &str,
declared: SubjectKind,
declared_buckets: Option<&[f64]>,
doc: Option<&Value>,
) {
let producer_id = (origin.to_string(), producer.to_string());
let generation = self.cycles.get(&producer_id).copied().unwrap_or(0);
let entry = self.keys.entry(key.to_string()).or_insert_with(|| KeyKind {
producer: producer_id,
declared,
judged: 0,
tag_mismatches: 0,
value_mismatches: 0,
decreases: 0,
undecoded: 0,
examples: Examples::new(EXPANSION_CAP),
last: None,
generation,
});
let Some(doc) = doc else {
entry.undecoded += 1;
return;
};
entry.judged += 1;
if let Some(tag) = payload_tag(doc)
&& tag != declared
{
entry.tag_mismatches += 1;
entry.examples.push_with(|| {
format!(
"payload tags itself `{}`, registry declares `{}`",
tag.payload_tag(),
declared.token()
)
});
return;
}
let v = leaf(doc);
match declared {
SubjectKind::Gauge => {
if v.as_f64().is_none() {
entry.value_mismatches += 1;
entry
.examples
.push_with(|| format!("gauge value is {}", describe(v)));
}
}
SubjectKind::Bool => {
if !v.is_boolean() {
entry.value_mismatches += 1;
entry
.examples
.push_with(|| format!("bool value is {}", describe(v)));
}
}
SubjectKind::Text => {
if !v.is_string() {
entry.value_mismatches += 1;
entry
.examples
.push_with(|| format!("text value is {}", describe(v)));
}
}
SubjectKind::Histogram => {
if !v.is_object() {
entry.value_mismatches += 1;
entry
.examples
.push_with(|| format!("histogram value is {}", describe(v)));
return;
}
if let (Some(declared), Some(stated)) = (declared_buckets, stated_bounds(v))
&& let Some(why) = bounds_disagree(declared, &stated)
{
entry.value_mismatches += 1;
entry.examples.push_with(|| why);
}
}
SubjectKind::Counter => {
let Some(n) = v.as_f64() else {
entry.value_mismatches += 1;
entry
.examples
.push_with(|| format!("counter value is {}", describe(v)));
return;
};
if n < 0.0 {
entry.value_mismatches += 1;
entry
.examples
.push_with(|| format!("counter value is negative ({n})"));
}
if entry.generation != generation {
entry.generation = generation;
entry.last = Some(n);
return;
}
if let Some(prev) = entry.last
&& n < prev
{
entry.decreases += 1;
entry.examples.push_with(|| {
format!("counter decreased {prev} → {n} with no `alive` cycle in between")
});
}
entry.last = Some(n);
}
}
}
}
impl KeyKind {
pub fn examples(&self) -> &[String] {
self.examples.as_slice()
}
}
impl KindObservation {
pub fn iter(&self) -> impl Iterator<Item = (&str, &KeyKind)> {
self.keys.iter().map(|(k, v)| (k.as_str(), v))
}
pub fn keys_seen(&self) -> usize {
self.keys.len()
}
}
pub fn judge_kind(observation: &KindObservation, window_s: f64) -> Vec<DoctorFinding> {
let mut findings = Vec::new();
let mut bad: Examples<DoctorFinding> = Examples::new(FINDING_CAP);
let mut unjudged: Examples<DoctorFinding> = Examples::new(FINDING_CAP);
for (key, k) in observation.iter() {
let mismatches = k.tag_mismatches + k.value_mismatches + k.decreases;
if mismatches > 0 {
bad.push_with(|| {
let mut parts = Vec::new();
if k.tag_mismatches > 0 {
parts.push(format!("{} tag disagreement(s)", k.tag_mismatches));
}
if k.decreases > 0 {
parts.push(format!("{} decrease(s)", k.decreases));
}
if k.value_mismatches > 0 {
parts.push(format!("{} value(s) not of that kind", k.value_mismatches));
}
let examples = k.examples.as_slice().join("; ");
DoctorFinding {
severity: DoctorSeverity::Error,
check: CheckId::KindMismatch,
subject: key.to_string(),
evidence: format!(
"declared `{}`, and {} of {} sample(s) from origin {} in {window_s:.0}s \
disagree: {} — e.g. {examples}",
k.declared.token(),
mismatches,
k.judged,
k.producer.0,
parts.join(", "),
),
citation: Some("RFC 08 §2".into()),
}
});
}
if k.undecoded > 0 {
unjudged.push_with(|| DoctorFinding {
severity: DoctorSeverity::Warning,
check: CheckId::KindMismatch,
subject: key.to_string(),
evidence: format!(
"kind not judged: {} payload(s) from origin {} in {window_s:.0}s could \
not be decoded, so the declared `{}` is unobservable for them",
k.undecoded,
k.producer.0,
k.declared.token(),
),
citation: Some("RFC 13 §3".into()),
});
}
}
for (ex, tail) in [
(bad, "more key(s) with the same finding"),
(unjudged, "more key(s) whose kind was not judged"),
] {
let more = ex.more(tail);
findings.extend(ex.into_vec());
if let Some(evidence) = more {
findings.push(DoctorFinding {
severity: DoctorSeverity::Info,
check: CheckId::KindMismatch,
subject: "fleet".into(),
evidence,
citation: None,
});
}
}
findings
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
const KEY: &str = "v1/h-aaaaaaaaaaaa/telemetry/sysinfo/memory/oom_kills_total";
fn observe(obs: &mut KindObservation, declared: SubjectKind, doc: Value) {
obs.observe(KEY, "h-aaaaaaaaaaaa", "sysinfo", declared, Some(&doc));
}
#[test]
fn a_histogram_is_judged_by_its_shape_and_its_stated_bounds() {
let key = "v1/h-aaaaaaaaaaaa/telemetry/sysinfo/system/runqlat";
let declared = [0.001, 0.01, 0.1];
let judge = |doc: Value| {
let mut obs = KindObservation::new();
obs.observe_declared(
key,
"h-aaaaaaaaaaaa",
"sysinfo",
SubjectKind::Histogram,
Some(&declared),
Some(&doc),
);
judge_kind(&obs, 10.0)
.into_iter()
.filter(|f| f.check == CheckId::KindMismatch)
.map(|f| f.evidence)
.collect::<Vec<_>>()
};
assert!(judge(json!({"type": "histogram", "value": {"buckets": [0.001, 0.01, 0.1], "counts": [1, 2, 3, 4]}})).is_empty());
assert!(
judge(json!({"buckets": [{"le": 0.001}, {"le": 0.01}, {"le": 0.1}, {"le": "+Inf"}]}))
.is_empty()
);
assert!(judge(json!({"count": 4, "sum": 0.3})).is_empty());
let f = judge(json!({"buckets": [0.001, 0.05, 0.1]}));
assert_eq!(f.len(), 1, "{f:?}");
assert!(
f[0].contains("histogram bound 2 is 0.05, registry declares 0.01"),
"{}",
f[0]
);
let f = judge(json!({"buckets": [0.001, 0.01]}));
assert!(
f[0].contains("states 2 bound(s), registry declares 3"),
"{}",
f[0]
);
let f = judge(json!(12.5));
assert!(f[0].contains("histogram value is number 12.5"), "{}", f[0]);
let f = judge(json!({"type": "gauge", "value": 1.0}));
assert!(
f[0].contains("payload tags itself `gauge`, registry declares `histogram`"),
"{}",
f[0]
);
}
fn mismatches(obs: &KindObservation) -> Vec<DoctorFinding> {
judge_kind(obs, 10.0)
.into_iter()
.filter(|f| f.severity == DoctorSeverity::Error)
.collect()
}
#[test]
fn a_decreasing_counter_is_a_finding_and_a_rising_one_is_nothing() {
let mut obs = KindObservation::new();
for n in [10, 20, 30] {
observe(&mut obs, SubjectKind::Counter, json!(n));
}
assert!(
judge_kind(&obs, 10.0).is_empty(),
"no Established(yes) exists"
);
observe(&mut obs, SubjectKind::Counter, json!(5));
let f = mismatches(&obs);
assert_eq!(f.len(), 1, "{f:?}");
assert_eq!(f[0].check, CheckId::KindMismatch);
assert_eq!(f[0].subject, KEY);
assert!(
f[0].evidence.contains("h-aaaaaaaaaaaa"),
"{}",
f[0].evidence
);
assert!(f[0].evidence.contains("10s"), "the window is stated");
assert!(f[0].evidence.contains("30 → 5"), "{}", f[0].evidence);
assert_eq!(f[0].citation.as_deref(), Some("RFC 08 §2"));
}
#[test]
fn a_reset_across_an_alive_cycle_is_not_a_finding() {
let mut obs = KindObservation::new();
let other = "v1/h-aaaaaaaaaaaa/telemetry/sysinfo/memory/page_faults_total";
observe(&mut obs, SubjectKind::Counter, json!(10));
obs.observe(
other,
"h-aaaaaaaaaaaa",
"sysinfo",
SubjectKind::Counter,
Some(&json!(7)),
);
obs.alive_cycled("h-aaaaaaaaaaaa", "sysinfo");
observe(&mut obs, SubjectKind::Counter, json!(0));
obs.observe(
other,
"h-aaaaaaaaaaaa",
"sysinfo",
SubjectKind::Counter,
Some(&json!(0)),
);
assert!(mismatches(&obs).is_empty(), "{:?}", judge_kind(&obs, 10.0));
observe(&mut obs, SubjectKind::Counter, json!(3));
observe(&mut obs, SubjectKind::Counter, json!(1));
assert_eq!(mismatches(&obs).len(), 1);
let mut obs = KindObservation::new();
observe(&mut obs, SubjectKind::Counter, json!(10));
obs.alive_cycled("h-aaaaaaaaaaaa", "netring");
observe(&mut obs, SubjectKind::Counter, json!(0));
assert_eq!(mismatches(&obs).len(), 1);
}
#[test]
fn a_disagreeing_tag_is_a_finding_and_an_agreeing_or_foreign_one_is_not() {
let mut obs = KindObservation::new();
observe(
&mut obs,
SubjectKind::Counter,
json!({"type": "gauge", "value": 1}),
);
let f = mismatches(&obs);
assert_eq!(f.len(), 1, "{f:?}");
assert!(
f[0].evidence.contains("tags itself `gauge`"),
"{}",
f[0].evidence
);
let mut obs = KindObservation::new();
observe(
&mut obs,
SubjectKind::Bool,
json!({"type": "boolean", "value": true}),
);
observe(
&mut obs,
SubjectKind::Bool,
json!({"type": "summary", "value": true}),
);
assert!(mismatches(&obs).is_empty());
let mut obs = KindObservation::new();
observe(
&mut obs,
SubjectKind::Bool,
json!({"type": "histogram", "value": {}}),
);
assert_eq!(mismatches(&obs).len(), 1);
}
#[test]
fn a_value_not_of_the_declared_kind_is_a_finding() {
let mut obs = KindObservation::new();
observe(&mut obs, SubjectKind::Text, json!(3));
observe(&mut obs, SubjectKind::Text, json!({"value": "ok"}));
assert_eq!(mismatches(&obs).len(), 1);
let mut obs = KindObservation::new();
observe(&mut obs, SubjectKind::Bool, json!({"value": "true"}));
assert_eq!(mismatches(&obs).len(), 1);
let mut obs = KindObservation::new();
observe(&mut obs, SubjectKind::Counter, json!(-1));
assert_eq!(mismatches(&obs).len(), 1);
let mut obs = KindObservation::new();
observe(&mut obs, SubjectKind::Gauge, json!(-1.5));
observe(&mut obs, SubjectKind::Gauge, json!({"value": 2}));
assert!(mismatches(&obs).is_empty(), "a gauge is any number");
}
#[test]
fn an_undecodable_payload_is_reported_unobservable_not_passed() {
let mut obs = KindObservation::new();
obs.observe(KEY, "h-aaaaaaaaaaaa", "sysinfo", SubjectKind::Counter, None);
obs.observe(KEY, "h-aaaaaaaaaaaa", "sysinfo", SubjectKind::Counter, None);
let f = judge_kind(&obs, 10.0);
assert_eq!(f.len(), 1, "{f:?}");
assert_eq!(f[0].severity, DoctorSeverity::Warning);
assert_eq!(f[0].check, CheckId::KindMismatch);
assert!(
f[0].evidence.contains("kind not judged: 2 payload(s)"),
"{}",
f[0].evidence
);
assert!(f[0].evidence.contains("10s"));
}
#[test]
fn the_cap_bites_with_a_counted_remainder() {
let mut obs = KindObservation::new();
for i in 0..(FINDING_CAP + 3) {
let key = format!("v1/h-aaaaaaaaaaaa/telemetry/sysinfo/k{i}");
obs.observe(
&key,
"h-aaaaaaaaaaaa",
"sysinfo",
SubjectKind::Text,
Some(&json!(1)),
);
}
let f = judge_kind(&obs, 10.0);
assert_eq!(f.len(), FINDING_CAP + 1, "{f:?}");
let tail = f.last().unwrap();
assert_eq!(tail.severity, DoctorSeverity::Info);
assert!(tail.evidence.contains("… and 3 more"), "{}", tail.evidence);
}
}