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