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