1use std::any::Any;
43use std::collections::HashMap;
44use std::sync::Arc;
45use tokio::sync::RwLock;
46use async_trait::async_trait;
47
48#[async_trait]
84pub trait ModuleService: Send + Sync {
85 fn service_id(&self) -> &'static str;
89
90 fn service_type(&self) -> &'static str;
92
93 fn module_name(&self) -> &'static str;
95
96 async fn health_check(&self) -> Result<ServiceHealth, String>;
98
99 fn as_any(&self) -> Option<&dyn Any> {
103 None
104 }
105}
106
107#[derive(Debug, Clone, serde::Serialize)]
109pub struct ServiceHealth {
110 pub status: HealthStatus,
111 pub message: Option<String>,
112 pub last_check: chrono::DateTime<chrono::Utc>,
113 pub details: HashMap<String, String>,
114}
115
116impl ServiceHealth {
117 pub fn healthy() -> Self {
118 Self {
119 status: HealthStatus::Healthy,
120 message: None,
121 last_check: chrono::Utc::now(),
122 details: HashMap::new(),
123 }
124 }
125
126 pub fn unhealthy(message: impl Into<String>) -> Self {
127 Self {
128 status: HealthStatus::Unhealthy,
129 message: Some(message.into()),
130 last_check: chrono::Utc::now(),
131 details: HashMap::new(),
132 }
133 }
134
135 pub fn degraded(message: impl Into<String>) -> Self {
136 Self {
137 status: HealthStatus::Degraded,
138 message: Some(message.into()),
139 last_check: chrono::Utc::now(),
140 details: HashMap::new(),
141 }
142 }
143
144 pub fn with_detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
145 self.details.insert(key.into(), value.into());
146 self
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
151#[serde(rename_all = "lowercase")]
152pub enum HealthStatus {
153 Healthy,
154 Degraded,
155 Unhealthy,
156}
157
158#[derive(Clone)]
170pub struct ServiceRegistry {
171 services: Arc<RwLock<HashMap<String, Arc<dyn ModuleService>>>>,
172}
173
174impl ServiceRegistry {
175 pub fn new() -> Self {
177 Self {
178 services: Arc::new(RwLock::new(HashMap::new())),
179 }
180 }
181
182 pub async fn register<S: ModuleService + 'static>(&self, service: S) {
187 let service_id = service.service_id().to_string();
188 tracing::info!(
189 service_id = %service_id,
190 module = %service.module_name(),
191 service_type = %service.service_type(),
192 "Registering service"
193 );
194 self.services.write().await.insert(service_id, Arc::new(service));
195 }
196
197 pub async fn register_arc(&self, service: Arc<dyn ModuleService>) {
199 let service_id = service.service_id().to_string();
200 tracing::info!(
201 service_id = %service_id,
202 module = %service.module_name(),
203 service_type = %service.service_type(),
204 "Registering service (Arc)"
205 );
206 self.services.write().await.insert(service_id, service);
207 }
208
209 pub async fn get(&self, service_id: &str) -> Option<Arc<dyn ModuleService>> {
213 self.services.read().await.get(service_id).cloned()
214 }
215
216 pub async fn unregister(&self, service_id: &str) -> bool {
218 self.services.write().await.remove(service_id).is_some()
219 }
220
221 pub async fn list(&self) -> Vec<String> {
223 self.services.read().await.keys().cloned().collect()
224 }
225
226 pub async fn list_by_module(&self, module_name: &str) -> Vec<Arc<dyn ModuleService>> {
228 self.services
229 .read()
230 .await
231 .values()
232 .filter(|s| s.module_name() == module_name)
233 .cloned()
234 .collect()
235 }
236
237 pub async fn list_by_type(&self, service_type: &str) -> Vec<Arc<dyn ModuleService>> {
239 self.services
240 .read()
241 .await
242 .values()
243 .filter(|s| s.service_type() == service_type)
244 .cloned()
245 .collect()
246 }
247
248 pub async fn has(&self, service_id: &str) -> bool {
250 self.services.read().await.contains_key(service_id)
251 }
252
253 pub async fn count(&self) -> usize {
255 self.services.read().await.len()
256 }
257
258 pub async fn health_check_all(&self) -> RegistryHealth {
260 let services = self.services.read().await;
261 let mut results = HashMap::new();
262 let mut healthy_count = 0;
263 let mut unhealthy_count = 0;
264
265 for (id, service) in services.iter() {
266 match service.health_check().await {
267 Ok(health) => {
268 if health.status == HealthStatus::Healthy {
269 healthy_count += 1;
270 } else {
271 unhealthy_count += 1;
272 }
273 results.insert(id.clone(), Ok(health));
274 }
275 Err(e) => {
276 unhealthy_count += 1;
277 results.insert(id.clone(), Err(e));
278 }
279 }
280 }
281
282 let overall_status = if unhealthy_count == 0 {
283 HealthStatus::Healthy
284 } else if healthy_count > 0 {
285 HealthStatus::Degraded
286 } else {
287 HealthStatus::Unhealthy
288 };
289
290 RegistryHealth {
291 status: overall_status,
292 total_services: services.len(),
293 healthy_count,
294 unhealthy_count,
295 services: results,
296 checked_at: chrono::Utc::now(),
297 }
298 }
299
300 pub async fn statistics(&self) -> RegistryStatistics {
302 let services = self.services.read().await;
303
304 let mut by_module: HashMap<String, usize> = HashMap::new();
305 let mut by_type: HashMap<String, usize> = HashMap::new();
306
307 for service in services.values() {
308 *by_module.entry(service.module_name().to_string()).or_default() += 1;
309 *by_type.entry(service.service_type().to_string()).or_default() += 1;
310 }
311
312 RegistryStatistics {
313 total_services: services.len(),
314 services_by_module: by_module,
315 services_by_type: by_type,
316 }
317 }
318}
319
320impl Default for ServiceRegistry {
321 fn default() -> Self {
322 Self::new()
323 }
324}
325
326#[derive(Debug, Clone, serde::Serialize)]
328pub struct RegistryHealth {
329 pub status: HealthStatus,
330 pub total_services: usize,
331 pub healthy_count: usize,
332 pub unhealthy_count: usize,
333 #[serde(skip_serializing_if = "HashMap::is_empty")]
334 pub services: HashMap<String, Result<ServiceHealth, String>>,
335 pub checked_at: chrono::DateTime<chrono::Utc>,
336}
337
338#[derive(Debug, Clone, serde::Serialize)]
340pub struct RegistryStatistics {
341 pub total_services: usize,
342 pub services_by_module: HashMap<String, usize>,
343 pub services_by_type: HashMap<String, usize>,
344}
345
346#[derive(Debug, Clone, serde::Serialize)]
352pub struct ServiceDescriptor {
353 pub service_id: String,
354 pub service_type: String,
355 pub module_name: String,
356 pub description: Option<String>,
357 pub version: Option<String>,
358}
359
360impl ServiceDescriptor {
361 pub fn from_service(service: &dyn ModuleService) -> Self {
362 Self {
363 service_id: service.service_id().to_string(),
364 service_type: service.service_type().to_string(),
365 module_name: service.module_name().to_string(),
366 description: None,
367 version: None,
368 }
369 }
370}
371
372#[cfg(test)]
377mod tests {
378 use super::*;
379
380 struct TestService {
381 id: &'static str,
382 healthy: bool,
383 }
384
385 #[async_trait]
386 impl ModuleService for TestService {
387 fn service_id(&self) -> &'static str {
388 self.id
389 }
390
391 fn service_type(&self) -> &'static str {
392 "test"
393 }
394
395 fn module_name(&self) -> &'static str {
396 "test_module"
397 }
398
399 async fn health_check(&self) -> Result<ServiceHealth, String> {
400 if self.healthy {
401 Ok(ServiceHealth::healthy())
402 } else {
403 Err("Service unhealthy".to_string())
404 }
405 }
406 }
407
408 #[tokio::test]
409 async fn test_register_and_get() {
410 let registry = ServiceRegistry::new();
411
412 let service = TestService {
413 id: "test.service",
414 healthy: true,
415 };
416
417 registry.register(service).await;
418
419 assert!(registry.has("test.service").await);
420 assert!(!registry.has("nonexistent").await);
421
422 let retrieved = registry.get("test.service").await;
423 assert!(retrieved.is_some());
424 assert_eq!(retrieved.unwrap().service_id(), "test.service");
425 }
426
427 #[tokio::test]
428 async fn test_list_services() {
429 let registry = ServiceRegistry::new();
430
431 registry.register(TestService { id: "test.a", healthy: true }).await;
432 registry.register(TestService { id: "test.b", healthy: true }).await;
433
434 let list = registry.list().await;
435 assert_eq!(list.len(), 2);
436 assert!(list.contains(&"test.a".to_string()));
437 assert!(list.contains(&"test.b".to_string()));
438 }
439
440 #[tokio::test]
441 async fn test_unregister() {
442 let registry = ServiceRegistry::new();
443
444 registry.register(TestService { id: "test.service", healthy: true }).await;
445 assert!(registry.has("test.service").await);
446
447 let removed = registry.unregister("test.service").await;
448 assert!(removed);
449 assert!(!registry.has("test.service").await);
450 }
451
452 #[tokio::test]
453 async fn test_health_check_all() {
454 let registry = ServiceRegistry::new();
455
456 registry.register(TestService { id: "test.healthy", healthy: true }).await;
457 registry.register(TestService { id: "test.unhealthy", healthy: false }).await;
458
459 let health = registry.health_check_all().await;
460
461 assert_eq!(health.total_services, 2);
462 assert_eq!(health.healthy_count, 1);
463 assert_eq!(health.unhealthy_count, 1);
464 assert_eq!(health.status, HealthStatus::Degraded);
465 }
466
467 #[tokio::test]
468 async fn test_statistics() {
469 let registry = ServiceRegistry::new();
470
471 registry.register(TestService { id: "test.a", healthy: true }).await;
472 registry.register(TestService { id: "test.b", healthy: true }).await;
473
474 let stats = registry.statistics().await;
475
476 assert_eq!(stats.total_services, 2);
477 assert_eq!(stats.services_by_module.get("test_module"), Some(&2));
478 assert_eq!(stats.services_by_type.get("test"), Some(&2));
479 }
480
481 #[tokio::test]
482 async fn test_list_by_module() {
483 let registry = ServiceRegistry::new();
484
485 registry.register(TestService { id: "test.a", healthy: true }).await;
486
487 let by_module = registry.list_by_module("test_module").await;
488 assert_eq!(by_module.len(), 1);
489
490 let by_other = registry.list_by_module("other_module").await;
491 assert!(by_other.is_empty());
492 }
493
494 #[test]
495 fn test_service_health_builders() {
496 let healthy = ServiceHealth::healthy();
497 assert_eq!(healthy.status, HealthStatus::Healthy);
498 assert!(healthy.message.is_none());
499
500 let unhealthy = ServiceHealth::unhealthy("Something went wrong");
501 assert_eq!(unhealthy.status, HealthStatus::Unhealthy);
502 assert_eq!(unhealthy.message, Some("Something went wrong".to_string()));
503
504 let degraded = ServiceHealth::degraded("Slow response")
505 .with_detail("latency_ms", "500");
506 assert_eq!(degraded.status, HealthStatus::Degraded);
507 assert_eq!(degraded.details.get("latency_ms"), Some(&"500".to_string()));
508 }
509}