1use 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#[derive(Clone, Serialize, Deserialize)]
14pub struct ServiceInfo {
15 pub name: String,
17 pub service_type: ServiceType,
19 pub domain_name: String,
20 pub port_info: String,
21 pub status: ServiceState,
23 pub description: Option<String>,
25 #[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 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 pub fn set_counters(&mut self, counters: Arc<ServiceCounters>) {
113 self.counters = Some(counters);
114 }
115
116 pub fn counters(&self) -> Option<&Arc<ServiceCounters>> {
118 self.counters.as_ref()
119 }
120
121 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 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 pub fn is_running(&self) -> bool {
146 matches!(self.status, ServiceState::Running(_))
147 }
148
149 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
158impl From<&ServiceInfo> for ProtoServiceStatus {
160 fn from(service_info: &ServiceInfo) -> Self {
161 let is_healthy = matches!(service_info.status, ServiceState::Running(_));
162
163 let port = service_info.port_info.parse::<u32>().unwrap_or(0);
165
166 let url = service_info.url();
168
169 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
202impl 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}