systemprompt_api/services/health/
monitor.rs1use anyhow::Result;
13use std::sync::Arc;
14use std::time::Duration;
15use systemprompt_database::ServiceRepository;
16use systemprompt_loader::subprocess;
17use systemprompt_traits::OwnedTask;
18use tokio::sync::watch;
19use tracing::{info, warn};
20
21#[derive(Debug)]
22pub struct ProcessMonitor {
23 repository: ServiceRepository,
24 monitor_handle: Option<OwnedTask<()>>,
25 check_interval: Duration,
26 cycles: Arc<watch::Sender<u64>>,
27}
28
29impl ProcessMonitor {
30 pub fn new(repository: ServiceRepository) -> Self {
31 Self::with_interval(repository, Duration::from_secs(30))
32 }
33
34 pub fn with_interval(repository: ServiceRepository, interval: Duration) -> Self {
35 Self {
36 repository,
37 monitor_handle: None,
38 check_interval: interval,
39 cycles: Arc::new(watch::Sender::new(0)),
40 }
41 }
42
43 #[must_use]
44 pub fn completed_cycles(&self) -> watch::Receiver<u64> {
45 self.cycles.subscribe()
46 }
47
48 pub fn start(&mut self) {
49 if self.monitor_handle.is_some() {
50 warn!("Process monitor already started");
51 return;
52 }
53
54 info!("Starting centralized process monitoring");
55
56 let repository = self.repository.clone();
57 let interval = self.check_interval;
58 let cycles = Arc::clone(&self.cycles);
59
60 let handle = OwnedTask::spawn("process_monitor", async move {
61 Self::monitor_loop(repository, interval, &cycles).await;
62 });
63
64 self.monitor_handle = Some(handle);
65 info!("Centralized process monitoring started");
66 }
67
68 pub fn stop(&mut self) {
69 if let Some(handle) = self.monitor_handle.take() {
70 info!("Stopping process monitoring");
71 handle.abort();
72 info!("Process monitoring stopped");
73 }
74 }
75
76 pub const fn is_running(&self) -> bool {
77 self.monitor_handle.is_some()
78 }
79
80 async fn monitor_loop(
81 repository: ServiceRepository,
82 check_interval: Duration,
83 cycles: &watch::Sender<u64>,
84 ) {
85 info!(
86 interval_secs = check_interval.as_secs(),
87 "Process monitor loop started"
88 );
89
90 let mut interval = tokio::time::interval(check_interval);
91
92 loop {
93 interval.tick().await;
94
95 if let Err(e) = Self::perform_monitoring_cycle(&repository).await {
96 warn!(error = %e, "Monitoring cycle failed");
97 }
98 cycles.send_modify(|completed| *completed = completed.saturating_add(1));
99 }
100 }
101
102 async fn perform_monitoring_cycle(repository: &ServiceRepository) -> Result<()> {
103 let services = repository.list_running_services_with_pid().await?;
104
105 if services.is_empty() {
106 return Ok(());
107 }
108
109 let mut healthy_count = 0;
110 let mut crashed_count = 0;
111
112 for service in services {
113 if let Some(pid) = service.pid {
114 if Self::pid_is_running(pid).await {
115 healthy_count += 1;
116 } else {
117 repository.mark_service_crashed(&service.name).await?;
118
119 crashed_count += 1;
120 warn!(
121 module = %service.module_name,
122 service = %service.name,
123 pid = pid,
124 "Detected crashed service"
125 );
126 }
127 }
128 }
129
130 if crashed_count == 0 {
131 info!(healthy = healthy_count, "All services healthy");
132 } else {
133 warn!(
134 healthy = healthy_count,
135 crashed = crashed_count,
136 "Service health check completed with failures"
137 );
138 }
139
140 Ok(())
141 }
142
143 async fn pid_is_running(pid: i32) -> bool {
144 match u32::try_from(pid) {
145 Ok(pid) => subprocess::is_running(pid).await,
146 Err(_) => false,
147 }
148 }
149
150 pub async fn health_check_all(&self) -> Result<HealthSummary> {
151 info!("Running health check on all services");
152
153 let services = self.repository.list_running_services_with_pid().await?;
154
155 let mut summary = HealthSummary::default();
156
157 for service in services {
158 if let Some(pid) = service.pid {
159 let healthy = Self::pid_is_running(pid).await;
160
161 info!(
162 module = %service.module_name,
163 service = %service.name,
164 pid = pid,
165 healthy = healthy,
166 "Service health status"
167 );
168
169 *summary
170 .modules
171 .entry(service.module_name.as_str().to_owned())
172 .or_insert_with(ModuleHealth::default) += if healthy {
173 ModuleHealth {
174 healthy: 1,
175 crashed: 0,
176 }
177 } else {
178 ModuleHealth {
179 healthy: 0,
180 crashed: 1,
181 }
182 };
183 }
184 }
185
186 let total_healthy = summary.modules.values().map(|m| m.healthy).sum::<u32>();
187 let total_crashed = summary.modules.values().map(|m| m.crashed).sum::<u32>();
188
189 if total_crashed == 0 {
190 info!(healthy = total_healthy, "All services are healthy");
191 } else {
192 warn!(
193 healthy = total_healthy,
194 total = total_healthy + total_crashed,
195 "Some services are unhealthy"
196 );
197 }
198
199 Ok(summary)
200 }
201}
202
203#[derive(Debug, Default)]
204pub struct HealthSummary {
205 pub modules: std::collections::HashMap<String, ModuleHealth>,
206}
207
208#[derive(Debug, Default, Copy, Clone)]
209pub struct ModuleHealth {
210 pub healthy: u32,
211 pub crashed: u32,
212}
213
214impl std::ops::AddAssign for ModuleHealth {
215 fn add_assign(&mut self, other: Self) {
216 self.healthy += other.healthy;
217 self.crashed += other.crashed;
218 }
219}
220
221impl HealthSummary {
222 pub fn total_healthy(&self) -> u32 {
223 self.modules.values().map(|m| m.healthy).sum()
224 }
225
226 pub fn total_crashed(&self) -> u32 {
227 self.modules.values().map(|m| m.crashed).sum()
228 }
229
230 pub fn is_all_healthy(&self) -> bool {
231 self.total_crashed() == 0
232 }
233}