1use crate::metrics::{
4 BackendHealthStatus, BackendStats, CommandCount, DiskCacheStats, ErrorCount, MetricsSnapshot,
5 UserStats,
6};
7use crate::tui::app::{ThroughputPoint, ViewMode};
8use crate::tui::system_stats::SystemStats;
9use crate::types::{
10 BackendToClientBytes, BytesPerSecondRate, BytesReceived, BytesSent, ClientToBackendBytes,
11 HostName, MaxConnections, Port, ServerName, TotalConnections,
12};
13use std::time::Duration;
14
15#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
17pub struct BufferPoolStats {
18 pub available: usize,
19 pub in_use: usize,
20 pub total: usize,
21}
22
23#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
25pub struct BackendDisplay {
26 pub host: HostName,
27 pub port: Port,
28 pub name: ServerName,
29 pub max_connections: MaxConnections,
30}
31
32#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
34pub struct BackendView {
35 pub server: BackendDisplay,
36 pub stats: BackendStats,
37 pub active_connections: usize,
38 pub health_status: BackendHealthStatus,
39 pub pending_count: usize,
40 pub load_ratio: Option<f64>,
41 pub stateful_count: usize,
42 pub traffic_share: Option<f64>,
43 pub history: Vec<ThroughputPoint>,
44}
45
46impl BackendView {
47 #[must_use]
48 pub fn latest_throughput(&self) -> Option<&ThroughputPoint> {
49 self.history.last()
50 }
51}
52
53#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
55pub struct RemoteBackendView {
56 pub server: BackendDisplay,
57 pub stats: BackendStats,
58 pub active_connections: usize,
59 pub health_status: BackendHealthStatus,
60 pub pending_count: usize,
61 pub stateful_count: usize,
62 pub traffic_share: Option<f64>,
63 pub history: Vec<ThroughputPoint>,
64}
65
66impl RemoteBackendView {
67 #[must_use]
68 pub fn latest_throughput(&self) -> Option<&ThroughputPoint> {
69 self.history.last()
70 }
71}
72
73#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
75pub struct DashboardUserStats {
76 pub username: String,
77 pub active_connections: usize,
78 pub total_connections: TotalConnections,
79 pub bytes_sent: BytesSent,
80 pub bytes_received: BytesReceived,
81 pub bytes_sent_per_sec: BytesPerSecondRate,
82 pub bytes_received_per_sec: BytesPerSecondRate,
83 pub total_commands: CommandCount,
84 pub errors: ErrorCount,
85}
86
87impl DashboardUserStats {
88 #[must_use]
89 pub fn from_user_stats(user: &UserStats) -> Self {
90 Self {
91 username: user.username.clone(),
92 active_connections: user.active_connections,
93 total_connections: user.total_connections,
94 bytes_sent: user.bytes_sent,
95 bytes_received: user.bytes_received,
96 bytes_sent_per_sec: user.bytes_sent_per_sec,
97 bytes_received_per_sec: user.bytes_received_per_sec,
98 total_commands: user.total_commands,
99 errors: user.errors,
100 }
101 }
102
103 #[must_use]
104 pub const fn total_bytes(&self) -> u64 {
105 self.bytes_sent
106 .as_u64()
107 .saturating_add(self.bytes_received.as_u64())
108 }
109}
110
111#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
113pub struct DashboardMetrics {
114 pub total_connections: u64,
115 pub active_connections: usize,
116 pub stateful_sessions: usize,
117 pub client_to_backend_bytes: ClientToBackendBytes,
118 pub backend_to_client_bytes: BackendToClientBytes,
119 pub uptime: Duration,
120 pub cache_entries: u64,
121 pub cache_size_bytes: u64,
122 pub cache_hit_rate: f64,
123 pub disk_cache: Option<DiskCacheStats>,
124 pub pipeline_batches: u64,
125 pub pipeline_commands: u64,
126 pub pipeline_requests_queued: u64,
127 pub pipeline_requests_completed: u64,
128 #[serde(default)]
129 pub in_flight_requests: usize,
130}
131
132impl DashboardMetrics {
133 #[must_use]
134 pub fn from_snapshot(snapshot: &MetricsSnapshot) -> Self {
135 Self {
136 total_connections: snapshot.total_connections,
137 active_connections: snapshot.active_connections,
138 stateful_sessions: snapshot.stateful_sessions,
139 client_to_backend_bytes: snapshot.client_to_backend_bytes,
140 backend_to_client_bytes: snapshot.backend_to_client_bytes,
141 uptime: snapshot.uptime,
142 cache_entries: snapshot.cache_entries,
143 cache_size_bytes: snapshot.cache_size_bytes,
144 cache_hit_rate: snapshot.cache_hit_rate,
145 disk_cache: snapshot.disk_cache,
146 pipeline_batches: snapshot.pipeline_batches,
147 pipeline_commands: snapshot.pipeline_commands,
148 pipeline_requests_queued: snapshot.pipeline_requests_queued,
149 pipeline_requests_completed: snapshot.pipeline_requests_completed,
150 in_flight_requests: 0,
151 }
152 }
153
154 #[must_use]
155 pub fn format_uptime(&self) -> String {
156 let secs = self.uptime.as_secs();
157 let hours = secs / 3600;
158 let minutes = (secs % 3600) / 60;
159 let seconds = secs % 60;
160
161 if hours > 0 {
162 format!("{hours}h {minutes}m {seconds}s")
163 } else if minutes > 0 {
164 format!("{minutes}m {seconds}s")
165 } else {
166 format!("{seconds}s")
167 }
168 }
169
170 #[must_use]
171 pub const fn total_bytes(&self) -> u64 {
172 self.client_to_backend_bytes.as_u64() + self.backend_to_client_bytes.as_u64()
173 }
174}
175
176#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
178pub struct DashboardState {
179 pub metrics: DashboardMetrics,
180 pub backend_views: Vec<BackendView>,
181 pub top_users: Vec<DashboardUserStats>,
182 pub client_history: Vec<ThroughputPoint>,
183 pub system_stats: SystemStats,
184 pub view_mode: ViewMode,
185 pub show_details: bool,
186 pub log_lines: Vec<String>,
187 pub buffer_pool: Option<BufferPoolStats>,
188}
189
190#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
192pub struct RemoteDashboardState {
193 pub metrics: DashboardMetrics,
194 pub backend_views: Vec<RemoteBackendView>,
195 pub top_users: Vec<DashboardUserStats>,
196 pub latest_client_throughput: Option<ThroughputPoint>,
197 pub system_stats: SystemStats,
198 pub log_lines: Vec<String>,
199}
200
201impl DashboardState {
202 #[must_use]
203 fn backend_view(&self, backend_idx: usize) -> Option<&BackendView> {
204 self.backend_views.get(backend_idx)
205 }
206
207 #[must_use]
208 pub fn latest_client_throughput(&self) -> Option<&ThroughputPoint> {
209 self.client_history.last()
210 }
211
212 #[must_use]
213 pub fn latest_backend_throughput(&self, backend_idx: usize) -> Option<&ThroughputPoint> {
214 self.backend_view(backend_idx)
215 .and_then(BackendView::latest_throughput)
216 }
217
218 #[must_use]
219 pub fn throughput_history(&self, backend_idx: usize) -> Option<&[ThroughputPoint]> {
220 self.backend_view(backend_idx)
221 .map(|view| view.history.as_slice())
222 }
223
224 #[must_use]
225 pub fn backend_pending_count(&self, backend_idx: usize) -> usize {
226 self.backend_view(backend_idx)
227 .map_or(0, |view| view.pending_count)
228 }
229
230 #[must_use]
231 pub fn backend_load_ratio(&self, backend_idx: usize) -> Option<f64> {
232 self.backend_view(backend_idx)
233 .and_then(|view| view.load_ratio)
234 }
235
236 #[must_use]
237 pub fn backend_stateful_count(&self, backend_idx: usize) -> usize {
238 self.backend_view(backend_idx)
239 .map_or(0, |view| view.stateful_count)
240 }
241
242 #[must_use]
243 pub fn backend_traffic_share(&self, backend_idx: usize) -> Option<f64> {
244 self.backend_view(backend_idx)
245 .and_then(|view| view.traffic_share)
246 }
247
248 #[must_use]
249 pub fn buffer_pool(&self) -> Option<&BufferPoolStats> {
250 self.buffer_pool.as_ref()
251 }
252}
253
254impl RemoteDashboardState {
255 #[must_use]
256 fn backend_view(&self, backend_idx: usize) -> Option<&RemoteBackendView> {
257 self.backend_views.get(backend_idx)
258 }
259
260 #[must_use]
261 pub fn latest_client_throughput(&self) -> Option<&ThroughputPoint> {
262 self.latest_client_throughput.as_ref()
263 }
264
265 #[must_use]
266 pub fn backend_pending_count(&self, backend_idx: usize) -> usize {
267 self.backend_view(backend_idx)
268 .map_or(0, |view| view.pending_count)
269 }
270
271 #[must_use]
272 pub fn backend_stateful_count(&self, backend_idx: usize) -> usize {
273 self.backend_view(backend_idx)
274 .map_or(0, |view| view.stateful_count)
275 }
276
277 #[must_use]
278 pub fn backend_traffic_share(&self, backend_idx: usize) -> Option<f64> {
279 self.backend_view(backend_idx)
280 .and_then(|view| view.traffic_share)
281 }
282}
283
284impl From<BackendView> for RemoteBackendView {
285 fn from(view: BackendView) -> Self {
286 Self {
287 server: view.server,
288 stats: view.stats,
289 active_connections: view.active_connections,
290 health_status: view.health_status,
291 pending_count: view.pending_count,
292 stateful_count: view.stateful_count,
293 traffic_share: view.traffic_share,
294 history: view.history,
295 }
296 }
297}
298
299impl From<DashboardState> for RemoteDashboardState {
300 fn from(state: DashboardState) -> Self {
301 Self {
302 metrics: state.metrics,
303 backend_views: state
304 .backend_views
305 .into_iter()
306 .map(RemoteBackendView::from)
307 .collect(),
308 top_users: state.top_users,
309 latest_client_throughput: state.client_history.last().cloned(),
310 system_stats: state.system_stats,
311 log_lines: state.log_lines,
312 }
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::metrics::{BackendHealthStatus, BackendStats};
320 use crate::tui::app::{ThroughputPoint, ViewMode};
321 use crate::types::Port;
322 use crate::types::tui::{Throughput, Timestamp};
323
324 fn sample_backend_view() -> BackendView {
325 BackendView {
326 server: BackendDisplay {
327 host: HostName::try_new("backend.example.com".to_string()).unwrap(),
328 port: Port::try_new(119).unwrap(),
329 name: ServerName::try_new("Backend".to_string()).unwrap(),
330 max_connections: MaxConnections::try_new(10).unwrap(),
331 },
332 stats: BackendStats::default(),
333 active_connections: 1,
334 health_status: BackendHealthStatus::Healthy,
335 pending_count: 2,
336 load_ratio: Some(0.5),
337 stateful_count: 3,
338 traffic_share: Some(42.0),
339 history: vec![ThroughputPoint::new_backend(
340 Timestamp::now(),
341 Throughput::new(1.0),
342 Throughput::new(2.0),
343 crate::types::tui::CommandsPerSecond::new(3.0),
344 )],
345 }
346 }
347
348 #[test]
349 fn backend_accessors_handle_out_of_range_indices() {
350 let state = DashboardState {
351 metrics: DashboardMetrics::default(),
352 backend_views: vec![sample_backend_view()],
353 top_users: Vec::new(),
354 client_history: Vec::new(),
355 system_stats: SystemStats::default(),
356 view_mode: ViewMode::Normal,
357 show_details: false,
358 log_lines: Vec::new(),
359 buffer_pool: None,
360 };
361
362 assert!(state.latest_backend_throughput(1).is_none());
363 assert!(state.throughput_history(1).is_none());
364 assert_eq!(state.backend_pending_count(1), 0);
365 assert!(state.backend_load_ratio(1).is_none());
366 assert_eq!(state.backend_stateful_count(1), 0);
367 assert!(state.backend_traffic_share(1).is_none());
368 }
369
370 #[test]
371 fn remote_dashboard_state_keeps_latest_client_point_and_drops_local_only_fields() {
372 let latest_client = ThroughputPoint::new_client(
373 Timestamp::now(),
374 Throughput::new(10.0),
375 Throughput::new(20.0),
376 );
377 let state = DashboardState {
378 metrics: DashboardMetrics::default(),
379 backend_views: vec![sample_backend_view()],
380 top_users: Vec::new(),
381 client_history: vec![
382 ThroughputPoint::new_client(
383 Timestamp::now(),
384 Throughput::new(1.0),
385 Throughput::new(2.0),
386 ),
387 latest_client.clone(),
388 ],
389 system_stats: SystemStats::default(),
390 view_mode: ViewMode::LogFullscreen,
391 show_details: true,
392 log_lines: vec!["hello".to_string()],
393 buffer_pool: Some(BufferPoolStats {
394 available: 1,
395 in_use: 2,
396 total: 3,
397 }),
398 };
399
400 let remote = RemoteDashboardState::from(state);
401
402 assert_eq!(remote.backend_views.len(), 1);
403 assert_eq!(remote.log_lines, vec!["hello".to_string()]);
404 assert_eq!(
405 remote
406 .latest_client_throughput()
407 .map(|point| point.sent_per_sec().get()),
408 Some(latest_client.sent_per_sec().get())
409 );
410 }
411}