appcore_supervisor/
supervisor_diagnostics.rs1use super::*;
14
15impl Supervisor {
16 pub fn diagnose(&self) -> SupervisorDiagnosis {
18 let validation = self.validate();
19 SupervisorDiagnosis {
20 graph_valid: validation.is_ok(),
21 issues: validation
22 .err()
23 .map(|error| vec![error.to_string()])
24 .unwrap_or_default(),
25 services: self.snapshots(),
26 watchdog: self.watchdog_snapshot(now_ms()),
27 restart_executor: self.inner.restart_executor.snapshot(),
28 }
29 }
30
31 pub fn snapshots(&self) -> Vec<crate::ServiceSnapshot> {
33 let Ok(services) = self.inner.services.read() else {
34 return Vec::new();
35 };
36 let Ok(records) = self.inner.records.lock() else {
37 return Vec::new();
38 };
39 services
40 .iter()
41 .map(|(name, service)| service_snapshot(name, service, records.get(name)))
42 .collect()
43 }
44
45 pub fn events(&self) -> Vec<SupervisorEvent> {
47 self.inner
48 .events
49 .lock()
50 .map(|events| events.iter().cloned().collect())
51 .unwrap_or_default()
52 }
53
54 pub fn is_healthy(&self, timestamp_ms: u64) -> bool {
56 self.validate().is_ok()
57 && self.watchdog_snapshot(timestamp_ms).is_healthy()
58 && self.inner.restart_executor.snapshot().healthy
59 && self.critical_services_healthy()
60 }
61
62 pub(super) fn critical_services_healthy(&self) -> bool {
63 self.snapshots().iter().all(|service| {
64 !service.enabled
65 || !service.critical
66 || matches!(
67 service.health,
68 ServiceHealth::Ready | ServiceHealth::Healthy
69 ) && !service.quarantined
70 })
71 }
72
73 pub(super) fn watchdog_snapshot(&self, timestamp_ms: u64) -> WatchdogSnapshot {
74 let watchdog = &self.inner.watchdog;
75 WatchdogSnapshot {
76 state: watchdog.state(),
77 last_reconcile_at_ms: watchdog.last_reconcile_at_ms(),
78 last_progress_at_ms: watchdog.last_progress_at_ms(),
79 reconcile_sequence: watchdog.reconcile_sequence(),
80 stalled_for_ms: watchdog.stalled_for_ms(timestamp_ms),
81 critical_services_healthy: self.critical_services_healthy(),
82 enabled: watchdog.config().enabled,
83 stall_timeout_ms: watchdog.config().stall_timeout_ms,
84 }
85 }
86
87 pub(super) fn record_stopped(&self, name: &str, timestamp_ms: u64) -> SupervisorResult<()> {
88 self.update_record(name, ServiceHealth::Unknown, ServiceRuntimeState::Stopped)?;
89 self.emit(
90 name,
91 SupervisorEventKind::ServiceStopped,
92 timestamp_ms,
93 self.restart_attempt(name),
94 ("Running", "Stopped"),
95 "lifecycle_stop",
96 );
97 Ok(())
98 }
99
100 pub(super) fn record_stop_failure(
101 &self,
102 service: &Arc<dyn ManagedService>,
103 timestamp_ms: u64,
104 ) -> SupervisorResult<()> {
105 let name = service.descriptor().name();
106 if service.runtime_state() == ServiceRuntimeState::Orphaned {
107 return self.quarantine_orphan(
108 &RestartCompletion {
109 service_id: name.to_string(),
110 attempt: self.restart_attempt(name),
111 outcome: RestartOutcome::Orphaned,
112 },
113 timestamp_ms,
114 );
115 }
116 self.update_record(name, ServiceHealth::Failed, service.runtime_state())?;
117 Ok(())
118 }
119}
120
121fn service_snapshot(
122 name: &str,
123 service: &Arc<dyn ManagedService>,
124 record: Option<&RuntimeRecord>,
125) -> crate::ServiceSnapshot {
126 let activation = service.descriptor().activation();
127 let runtime_state = record
128 .map(|record| record.runtime_state)
129 .unwrap_or_else(|| service.runtime_state());
130 crate::ServiceSnapshot {
131 name: name.to_string(),
132 health: record
133 .and_then(|record| record.health)
134 .unwrap_or_else(|| service.health()),
135 dependencies: service
136 .descriptor()
137 .dependencies()
138 .iter()
139 .map(|dependency| dependency.service_id().to_string())
140 .collect(),
141 dependency_requirements: service
142 .descriptor()
143 .dependencies()
144 .iter()
145 .map(|dependency| format!("{:?}", dependency.requirement()))
146 .collect(),
147 activation,
148 enabled: activation.is_enabled(),
149 configured: activation.is_configured(),
150 running: runtime_state == ServiceRuntimeState::Running,
151 runtime_state,
152 restart_state: record
153 .map(|record| record.restart_state)
154 .unwrap_or(RestartState::None),
155 restart_count: record.map(|record| record.restart_count).unwrap_or(0),
156 operator_required: record
157 .map(|record| record.operator_required)
158 .unwrap_or(false),
159 quarantined: record.map(|record| record.quarantined).unwrap_or(false),
160 critical: service.descriptor().is_critical(),
161 }
162}