Skip to main content

platform/monitoring/
service_info.rs

1//! Service information
2//!
3//! Defines the basic information structure for services
4
5use crate::config::ActrixConfig;
6use crate::monitoring::{ServiceCounters, ServiceState, service_type::ServiceType};
7use actrix_proto::{ResourceType, ServiceStatus as ProtoServiceStatus};
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10use url::Url;
11
12/// Basic service information
13#[derive(Clone, Serialize, Deserialize)]
14pub struct ServiceInfo {
15    /// Service name
16    pub name: String,
17    /// Service type. Turn service is a collection of STUN and TURN
18    pub service_type: ServiceType,
19    pub domain_name: String,
20    pub port_info: String,
21    /// Service status
22    pub status: ServiceState,
23    /// Service description
24    pub description: Option<String>,
25    /// Live counters for this service (not serialized).
26    #[serde(skip)]
27    counters: Option<Arc<ServiceCounters>>,
28}
29
30impl std::fmt::Debug for ServiceInfo {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        f.debug_struct("ServiceInfo")
33            .field("name", &self.name)
34            .field("service_type", &self.service_type)
35            .field("domain_name", &self.domain_name)
36            .field("port_info", &self.port_info)
37            .field("status", &self.status)
38            .field("description", &self.description)
39            .field("counters", &self.counters.as_ref().map(|_| "..."))
40            .finish()
41    }
42}
43
44impl ServiceInfo {
45    /// Create a ServiceInfo with explicit domain/port (no config needed).
46    pub fn new_raw(
47        name: impl Into<String>,
48        service_type: ServiceType,
49        domain_name: String,
50        port_info: String,
51        description: Option<String>,
52    ) -> Self {
53        Self {
54            name: name.into(),
55            service_type,
56            port_info,
57            domain_name,
58            status: ServiceState::Unknown,
59            description,
60            counters: None,
61        }
62    }
63
64    pub fn new(
65        name: impl Into<String>,
66        service_type: ServiceType,
67        description: Option<String>,
68        config: &ActrixConfig,
69    ) -> Self {
70        let (port_info, domain_name) = match service_type {
71            ServiceType::Signaling => {
72                if let Some(ref h) = config.bind.http {
73                    (
74                        h.port.to_string(),
75                        format!("{}://{}", h.ws_scheme(), h.domain_name),
76                    )
77                } else {
78                    ("0".to_string(), "ws://localhost".to_string())
79                }
80            }
81            ServiceType::Turn => (
82                config.bind.ice.port.to_string(),
83                format!("turn:{}", config.bind.ice.advertised_ip),
84            ),
85            ServiceType::Stun => (
86                config.bind.ice.port.to_string(),
87                format!("stun:{}", config.bind.ice.advertised_ip),
88            ),
89            ServiceType::Ais | ServiceType::Ks | ServiceType::Mfr => {
90                if let Some(ref h) = config.bind.http {
91                    (
92                        h.port.to_string(),
93                        format!("{}://{}", h.scheme(), h.domain_name),
94                    )
95                } else {
96                    ("0".to_string(), "http://localhost".to_string())
97                }
98            }
99        };
100        Self {
101            name: name.into(),
102            service_type,
103            port_info,
104            domain_name,
105            status: ServiceState::Unknown,
106            description,
107            counters: None,
108        }
109    }
110
111    /// Attach live counters to this service.
112    pub fn set_counters(&mut self, counters: Arc<ServiceCounters>) {
113        self.counters = Some(counters);
114    }
115
116    /// Get the live counters (if set).
117    pub fn counters(&self) -> Option<&Arc<ServiceCounters>> {
118        self.counters.as_ref()
119    }
120
121    /// Set service status to running
122    pub fn set_running(&mut self, url: Url) {
123        self.status = ServiceState::Running(url.to_string());
124        crate::recording::info!(
125            "Service '{}' is now running at {}/{}",
126            self.name,
127            self.url(),
128            self.domain_name
129        );
130    }
131
132    /// Set service status to error
133    pub fn set_error(&mut self, error: impl Into<String>) {
134        let error_msg = error.into();
135        self.status = ServiceState::Error(error_msg.clone());
136        crate::recording::error!(
137            "Service '{}' encountered error: {}/{}",
138            self.name,
139            self.url(),
140            self.domain_name
141        );
142    }
143
144    /// Check if service is running
145    pub fn is_running(&self) -> bool {
146        matches!(self.status, ServiceState::Running(_))
147    }
148
149    /// Get service status URL (if in running state)
150    pub fn url(&self) -> String {
151        match &self.status {
152            ServiceState::Running(url) => url.to_string(),
153            _ => "N/A".to_string(),
154        }
155    }
156}
157
158/// Convert ServiceInfo to proto ServiceStatus
159impl From<&ServiceInfo> for ProtoServiceStatus {
160    fn from(service_info: &ServiceInfo) -> Self {
161        let is_healthy = matches!(service_info.status, ServiceState::Running(_));
162
163        // Parse port number (extract digits from port_info)
164        let port = service_info.port_info.parse::<u32>().unwrap_or(0);
165
166        // Build URL
167        let url = service_info.url();
168
169        // Read live counters if available, otherwise return defaults.
170        let (active_connections, total_requests, failed_requests) =
171            if let Some(ctr) = &service_info.counters {
172                (
173                    ctr.active_conns.load(std::sync::atomic::Ordering::Relaxed),
174                    ctr.total_requests
175                        .load(std::sync::atomic::Ordering::Relaxed),
176                    ctr.failed_requests
177                        .load(std::sync::atomic::Ordering::Relaxed),
178                )
179            } else {
180                (0, 0, 0)
181            };
182
183        Self {
184            name: service_info.name.clone(),
185            r#type: ResourceType::from(&service_info.service_type).into(),
186            is_healthy,
187            active_connections,
188            total_requests,
189            failed_requests,
190            average_latency_ms: 0.0,
191            url: Some(url),
192            port: if port > 0 { Some(port) } else { None },
193            domain: if service_info.domain_name != "N/A" {
194                Some(service_info.domain_name.clone())
195            } else {
196                None
197            },
198        }
199    }
200}
201
202/// Convert ServiceInfo to proto ServiceStatus (owned version)
203impl From<ServiceInfo> for ProtoServiceStatus {
204    fn from(service_info: ServiceInfo) -> Self {
205        Self::from(&service_info)
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    fn config_with_ice(advertised_ip: &str, port: u16) -> ActrixConfig {
214        let mut config = ActrixConfig::default();
215        config.bind.ice.advertised_ip = advertised_ip.to_string();
216        config.bind.ice.port = port;
217        config
218    }
219
220    #[test]
221    fn stun_turn_advertise_public_ip_not_listen_ip() {
222        let config = config_with_ice("123.0.0.10", 3480);
223
224        let turn = ServiceInfo::new("TURN Server", ServiceType::Turn, None, &config);
225        assert_eq!(turn.port_info, "3480");
226        assert_eq!(turn.domain_name, "turn:123.0.0.10");
227
228        let stun = ServiceInfo::new("STUN Server", ServiceType::Stun, None, &config);
229        assert_eq!(stun.port_info, "3480");
230        assert_eq!(stun.domain_name, "stun:123.0.0.10");
231    }
232}