use crate::models::health::{HealthCheckConfig, HealthResult};
use chrono::{DateTime, Utc, Duration};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Service {
pub id: String,
pub name: String,
pub service_type: ServiceType,
pub source: DetectionSource,
pub host: String,
pub port: u16,
pub additional_ports: Vec<u16>,
pub pid: Option<u32>,
pub process_name: Option<String>,
pub command_line: Option<String>,
pub container_id: Option<String>,
pub systemd_unit: Option<String>,
pub log_file_path: Option<String>,
pub health_check: HealthCheckConfig,
pub project_path: String,
pub tags: Vec<String>,
pub dependencies: Vec<String>,
#[serde(skip)]
pub health_status: Option<HealthResult>,
pub first_seen: DateTime<Utc>,
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,
}
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,
}