darpan 0.2.2

Linux developer service monitoring utility with auto-detection, real-time health checks, and interactive TUI for databases, APIs, Docker containers, and more
Documentation
use crate::models::health::{HealthCheckConfig, HealthResult};
use chrono::{DateTime, Utc, Duration};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Service {
    /// Unique identifier (auto-generated)
    pub id: String,
    
    /// Display name
    pub name: String,
    
    /// Service type
    pub service_type: ServiceType,
    
    /// How this service was detected
    pub source: DetectionSource,
    
    /// Host (usually localhost)
    pub host: String,
    
    /// Primary port
    pub port: u16,
    
    /// Additional ports used by this service
    pub additional_ports: Vec<u16>,
    
    /// Process ID (if applicable)
    pub pid: Option<u32>,
    
    /// Process name
    pub process_name: Option<String>,
    
    /// Full command line
    pub command_line: Option<String>,
    
    /// Docker container ID (for log streaming)
    pub container_id: Option<String>,
    
    /// Systemd unit name (for journalctl logs)
    pub systemd_unit: Option<String>,
    
    /// Custom log file path
    pub log_file_path: Option<String>,
    
    /// Health check configuration
    pub health_check: HealthCheckConfig,
    
    /// Project path
    pub project_path: String,
    
    /// Tags for categorization
    pub tags: Vec<String>,
    
    /// Service IDs this depends on
    pub dependencies: Vec<String>,
    
    /// Current health status
    #[serde(skip)]
    pub health_status: Option<HealthResult>,
    
    /// When the service was first detected
    pub first_seen: DateTime<Utc>,
    
    /// When the service was last seen healthy
    pub last_seen: DateTime<Utc>,
}

impl Service {
    pub fn new(
        name: String,
        service_type: ServiceType,
        host: String,
        port: u16,
        source: DetectionSource,
        project_path: String,
    ) -> Self {
        let now = Utc::now();
        let id = format!("{}:{}", name.to_lowercase().replace(' ', "-"), port);
        
        Self {
            id,
            name,
            service_type,
            source,
            host,
            port,
            additional_ports: Vec::new(),
            pid: None,
            process_name: None,
            command_line: None,
            container_id: None,
            systemd_unit: None,
            log_file_path: None,
            health_check: HealthCheckConfig::default(),
            project_path,
            tags: Vec::new(),
            dependencies: Vec::new(),
            health_status: None,
            first_seen: now,
            last_seen: now,
        }
    }

    pub fn is_healthy(&self) -> bool {
        self.health_status
            .as_ref()
            .map(|h| matches!(h.status, crate::models::HealthStatus::Healthy))
            .unwrap_or(false)
    }

    pub fn uptime(&self) -> Duration {
        Utc::now() - self.first_seen
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServiceType {
    HttpServer,
    Database { database: DatabaseType },
    MessageQueue { message_queue: QueueType },
    Cache { cache: CacheType },
    Search { search: SearchType },
    DockerContainer,
    Custom,
}

// Custom serialization/deserialization to handle both formats:
// - Simple: "http_server"
// - Complex: { database: "postgres" }
impl Serialize for ServiceType {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        use serde::ser::SerializeMap;
        match self {
            ServiceType::HttpServer => serializer.serialize_str("http_server"),
            ServiceType::DockerContainer => serializer.serialize_str("docker_container"),
            ServiceType::Custom => serializer.serialize_str("custom"),
            ServiceType::Database { database } => {
                let mut map = serializer.serialize_map(Some(1))?;
                map.serialize_entry("database", &database)?;
                map.end()
            }
            ServiceType::MessageQueue { message_queue } => {
                let mut map = serializer.serialize_map(Some(1))?;
                map.serialize_entry("message_queue", &message_queue)?;
                map.end()
            }
            ServiceType::Cache { cache } => {
                let mut map = serializer.serialize_map(Some(1))?;
                map.serialize_entry("cache", &cache)?;
                map.end()
            }
            ServiceType::Search { search } => {
                let mut map = serializer.serialize_map(Some(1))?;
                map.serialize_entry("search", &search)?;
                map.end()
            }
        }
    }
}

impl<'de> Deserialize<'de> for ServiceType {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        use serde::de::{self, MapAccess, Visitor};
        use std::fmt;

        struct ServiceTypeVisitor;

        impl<'de> Visitor<'de> for ServiceTypeVisitor {
            type Value = ServiceType;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("a service type string or map")
            }

            fn visit_str<E>(self, value: &str) -> Result<ServiceType, E>
            where
                E: de::Error,
            {
                match value {
                    "http_server" => Ok(ServiceType::HttpServer),
                    "docker_container" => Ok(ServiceType::DockerContainer),
                    "custom" => Ok(ServiceType::Custom),
                    _ => Err(de::Error::unknown_variant(
                        value,
                        &["http_server", "docker_container", "custom", "database", "message_queue", "cache", "search"],
                    )),
                }
            }

            fn visit_map<M>(self, mut map: M) -> Result<ServiceType, M::Error>
            where
                M: MapAccess<'de>,
            {
                if let Some(key) = map.next_key::<String>()? {
                    match key.as_str() {
                        "database" => {
                            let database = map.next_value()?;
                            Ok(ServiceType::Database { database })
                        }
                        "message_queue" => {
                            let message_queue = map.next_value()?;
                            Ok(ServiceType::MessageQueue { message_queue })
                        }
                        "cache" => {
                            let cache = map.next_value()?;
                            Ok(ServiceType::Cache { cache })
                        }
                        "search" => {
                            let search = map.next_value()?;
                            Ok(ServiceType::Search { search })
                        }
                        _ => Err(de::Error::unknown_field(
                            &key,
                            &["database", "message_queue", "cache", "search"],
                        )),
                    }
                } else {
                    Err(de::Error::custom("expected a map with one entry"))
                }
            }
        }

        deserializer.deserialize_any(ServiceTypeVisitor)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum DatabaseType {
    Postgres,
    MySQL,
    MongoDB,
    SQLite,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum QueueType {
    RabbitMQ,
    Kafka,
    Redis,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum CacheType {
    Redis,
    Memcached,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SearchType {
    Elasticsearch,
    Meilisearch,
    Typesense,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DetectionSource {
    ExplicitConfig,
    DockerCompose,
    PortScan,
    ProcessScan,
    AutoDetected,
}