use anyhow::Result;
use async_trait::async_trait;
use crate::event::{Alert, DetectedService, ServiceKind, ServiceMetric};
use crate::ssh::probe::ProbeOutput;
use crate::ssh::session::SshSession;
pub mod docker;
pub mod nginx;
pub mod nodejs;
pub mod postgresql;
pub mod redis;
#[async_trait]
pub trait ServiceProvider: Send + Sync {
fn kind(&self) -> ServiceKind;
fn detect(&self, probe_output: &ProbeOutput) -> bool;
fn quick_metrics(&self, _probe_output: &ProbeOutput) -> Vec<ServiceMetric> {
Vec::new()
}
async fn collect_metrics(&self, session: &SshSession) -> Result<DetectedService>;
fn extract_version(&self, _probe_output: &ProbeOutput) -> Option<String> {
None
}
}
pub struct ServiceRegistry {
providers: Vec<Box<dyn ServiceProvider>>,
}
impl ServiceRegistry {
pub fn new() -> Self {
let providers: Vec<Box<dyn ServiceProvider>> = vec![
Box::new(docker::DockerProvider),
Box::new(nginx::NginxProvider),
Box::new(postgresql::PostgreSQLProvider),
Box::new(redis::RedisProvider),
Box::new(nodejs::NodeJSProvider),
];
Self { providers }
}
pub fn detect_services(&self, probe_output: &ProbeOutput) -> Vec<ServiceKind> {
self.providers
.iter()
.filter(|p| p.detect(probe_output))
.map(|p| p.kind())
.collect()
}
pub fn get_provider(&self, kind: &ServiceKind) -> Option<&dyn ServiceProvider> {
self.providers
.iter()
.find(|p| &p.kind() == kind)
.map(|boxed| &**boxed)
}
pub async fn collect_all_metrics(
&self,
session: &SshSession,
probe_output: &ProbeOutput,
) -> Vec<DetectedService> {
let detected_kinds = self.detect_services(probe_output);
let mut services = Vec::new();
for kind in detected_kinds {
if let Some(provider) = self.get_provider(&kind) {
match provider.collect_metrics(session).await {
Ok(service) => services.push(service),
Err(e) => {
tracing::debug!(
service = ?kind,
error = %e,
"failed to collect metrics for service"
);
}
}
}
}
services
}
}
impl Default for ServiceRegistry {
fn default() -> Self {
Self::new()
}
}
pub fn metric_int(name: impl Into<String>, value: i64, unit: impl Into<String>) -> ServiceMetric {
ServiceMetric {
name: name.into(),
value: crate::event::MetricValue::Integer(value),
unit: unit.into(),
threshold: None,
}
}
pub fn metric_float(name: impl Into<String>, value: f64, unit: impl Into<String>) -> ServiceMetric {
ServiceMetric {
name: name.into(),
value: crate::event::MetricValue::Float(value),
unit: unit.into(),
threshold: None,
}
}
pub fn metric_string(name: impl Into<String>, value: impl Into<String>) -> ServiceMetric {
ServiceMetric {
name: name.into(),
value: crate::event::MetricValue::String(value.into()),
unit: String::new(),
threshold: None,
}
}
pub fn alert(
severity: crate::event::AlertSeverity,
service: ServiceKind,
message: impl Into<String>,
suggested_action: Option<String>,
) -> Alert {
Alert {
severity,
message: message.into(),
service,
suggested_action,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_registry_creation() {
let registry = ServiceRegistry::new();
assert_eq!(registry.providers.len(), 5); }
#[test]
fn test_registry_get_provider() {
let registry = ServiceRegistry::new();
assert!(registry.get_provider(&ServiceKind::Docker).is_some());
assert!(registry.get_provider(&ServiceKind::Nginx).is_some());
assert!(registry.get_provider(&ServiceKind::PostgreSQL).is_some());
assert!(registry.get_provider(&ServiceKind::Redis).is_some());
assert!(registry.get_provider(&ServiceKind::NodeJS).is_some());
}
#[test]
fn test_metric_helpers() {
let m_int = metric_int("count", 42, "");
assert_eq!(m_int.name, "count");
matches!(m_int.value, crate::event::MetricValue::Integer(42));
let m_float = metric_float("percent", 73.5, "%");
assert_eq!(m_float.unit, "%");
let m_string = metric_string("status", "healthy");
matches!(m_string.value, crate::event::MetricValue::String(_));
}
}