| 63 |
63 |
|
pub label: String,
|
| 64 |
64 |
|
/// The most recent health snapshot. `None` before the first check.
|
| 65 |
65 |
|
pub health: Option<HealthView>,
|
|
66 |
+ |
/// Whether the target configures an HTTP health check at all.
|
|
67 |
+ |
///
|
|
68 |
+ |
/// Distinct from `health` being `None`, which only says no snapshot has been
|
|
69 |
+ |
/// stored. Without this the two collapse and a target that will never have a
|
|
70 |
+ |
/// health endpoint reports `health: pending` forever: the desktop apps and the
|
|
71 |
+ |
/// SDK are test-only targets, so af, bb, go and sk sat permanently pending and
|
|
72 |
+ |
/// held the whole source there with them. A row that cannot ever go green is
|
|
73 |
+ |
/// worse than no row, because a surface where most rows are unsatisfiable is
|
|
74 |
+ |
/// one nobody reads.
|
|
75 |
+ |
pub health_configured: bool,
|
| 66 |
76 |
|
/// Uptime percentage over the last 24 hours, if any checks fell in the window.
|
| 67 |
77 |
|
pub uptime_24h: Option<f64>,
|
| 68 |
78 |
|
/// Mean response time over the last 24 hours of operational checks, in ms.
|
| 217 |
227 |
|
}
|
| 218 |
228 |
|
|
| 219 |
229 |
|
fn target_node(target: &TargetView) -> Node {
|
| 220 |
|
- |
let mut conditions = vec![health_condition(target.health.as_ref())];
|
|
230 |
+ |
// Only speak about health where health is watched. A stored snapshot still
|
|
231 |
+ |
// reports even if the config was since removed, so history stays visible.
|
|
232 |
+ |
let mut conditions = Vec::new();
|
|
233 |
+ |
if target.health_configured || target.health.is_some() {
|
|
234 |
+ |
conditions.push(health_condition(target.health.as_ref()));
|
|
235 |
+ |
}
|
| 221 |
236 |
|
if let Some(incident) = &target.incident {
|
| 222 |
237 |
|
conditions.push(incident_condition(incident));
|
| 223 |
238 |
|
}
|
| 756 |
771 |
|
TargetView {
|
| 757 |
772 |
|
name: name.into(),
|
| 758 |
773 |
|
label: name.to_uppercase(),
|
|
774 |
+ |
health_configured: true,
|
| 759 |
775 |
|
health: Some(HealthView {
|
| 760 |
776 |
|
status: HealthStatus::Operational,
|
| 761 |
777 |
|
checked_at: checked_at(),
|
| 1525 |
1541 |
|
assert_eq!(p.validate(), Ok(()));
|
| 1526 |
1542 |
|
}
|
| 1527 |
1543 |
|
|
|
1544 |
+ |
#[test]
|
|
1545 |
+ |
fn a_target_that_does_not_watch_health_says_nothing_about_health() {
|
|
1546 |
+ |
// The test-only targets (the desktop apps, the SDK) have no HTTP endpoint
|
|
1547 |
+ |
// and never will. Emitting `health: pending` for them is a row that can
|
|
1548 |
+ |
// never go green, which held af/bb/go/sk pending forever and the whole
|
|
1549 |
+ |
// source with them.
|
|
1550 |
+ |
let mut t = healthy("af");
|
|
1551 |
+ |
t.health_configured = false;
|
|
1552 |
+ |
t.health = None;
|
|
1553 |
+ |
let p = payload(&[t], now());
|
|
1554 |
+ |
let n = node(&p, "target:af");
|
|
1555 |
+ |
assert!(
|
|
1556 |
+ |
n.conditions.iter().all(|c| c.condition_type != "health"),
|
|
1557 |
+ |
"unconfigured health must emit no condition, got {:?}",
|
|
1558 |
+ |
n.conditions
|
|
1559 |
+ |
);
|
|
1560 |
+ |
assert_eq!(p.validate(), Ok(()));
|
|
1561 |
+ |
}
|
|
1562 |
+ |
|
|
1563 |
+ |
#[test]
|
|
1564 |
+ |
fn a_stored_snapshot_still_reports_after_the_config_is_removed() {
|
|
1565 |
+ |
// Config is truth for whether to watch, but evidence already collected
|
|
1566 |
+ |
// should not vanish: dropping the block should not silently erase the
|
|
1567 |
+ |
// last thing PoM knew about that target's health.
|
|
1568 |
+ |
let mut t = healthy("mnw");
|
|
1569 |
+ |
t.health_configured = false;
|
|
1570 |
+ |
let p = payload(&[t], now());
|
|
1571 |
+ |
let n = node(&p, "target:mnw");
|
|
1572 |
+ |
assert!(n.conditions.iter().any(|c| c.condition_type == "health"));
|
|
1573 |
+ |
assert_eq!(p.validate(), Ok(()));
|
|
1574 |
+ |
}
|
|
1575 |
+ |
|
|
1576 |
+ |
#[test]
|
|
1577 |
+ |
fn a_watched_target_with_no_snapshot_yet_is_still_pending() {
|
|
1578 |
+ |
// The case the flag must not break: health IS configured, no check has
|
|
1579 |
+ |
// run, so "evidence of nothing" is the honest answer.
|
|
1580 |
+ |
let mut t = healthy("mnw");
|
|
1581 |
+ |
t.health = None;
|
|
1582 |
+ |
let p = payload(&[t], now());
|
|
1583 |
+ |
let n = node(&p, "target:mnw");
|
|
1584 |
+ |
let h = n
|
|
1585 |
+ |
.conditions
|
|
1586 |
+ |
.iter()
|
|
1587 |
+ |
.find(|c| c.condition_type == "health")
|
|
1588 |
+ |
.expect("configured health must still emit a condition");
|
|
1589 |
+ |
assert_eq!(h.status, Status::Pending);
|
|
1590 |
+ |
assert_eq!(p.validate(), Ok(()));
|
|
1591 |
+ |
}
|
|
1592 |
+ |
|
| 1528 |
1593 |
|
#[test]
|
| 1529 |
1594 |
|
fn render_is_a_pure_function_of_state_and_clock() {
|
| 1530 |
1595 |
|
let a = payload(&[healthy("mnw")], now());
|