1use super::types::{
4 ActiveConnections, ArticleCount, BackendHealthStatus, CommandCount, ErrorCount,
5 ErrorRatePercent, FailureCount, OverheadMillis, RecvMicros, SendMicros, TtfbMicros,
6};
7use crate::types::{
8 ArticleBytesTotal, BackendId, BytesReceived, BytesSent, TimingMeasurementCount,
9};
10
11#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13pub struct BackendStats {
14 pub backend_id: BackendId,
15 #[serde(skip, default)]
16 pub active_connections: ActiveConnections,
17 pub total_commands: CommandCount,
18 pub bytes_sent: BytesSent,
19 pub bytes_received: BytesReceived,
20 pub errors: ErrorCount,
21 pub errors_4xx: ErrorCount,
22 pub errors_5xx: ErrorCount,
23 pub article_bytes_total: ArticleBytesTotal,
24 pub article_count: ArticleCount,
25 pub ttfb_micros_total: TtfbMicros,
26 pub ttfb_count: TimingMeasurementCount,
27 pub send_micros_total: SendMicros,
28 pub recv_micros_total: RecvMicros,
29 pub connection_failures: FailureCount,
30 #[serde(skip, default)]
31 pub health_status: BackendHealthStatus,
32}
33
34impl Default for BackendStats {
35 fn default() -> Self {
36 Self {
37 backend_id: BackendId::from_index(0),
38 active_connections: ActiveConnections::default(),
39 total_commands: CommandCount::default(),
40 bytes_sent: BytesSent::ZERO,
41 bytes_received: BytesReceived::ZERO,
42 errors: ErrorCount::default(),
43 errors_4xx: ErrorCount::default(),
44 errors_5xx: ErrorCount::default(),
45 article_bytes_total: ArticleBytesTotal::ZERO,
46 article_count: ArticleCount::default(),
47 ttfb_micros_total: TtfbMicros::default(),
48 ttfb_count: TimingMeasurementCount::ZERO,
49 send_micros_total: SendMicros::default(),
50 recv_micros_total: RecvMicros::default(),
51 connection_failures: FailureCount::default(),
52 health_status: BackendHealthStatus::Healthy,
53 }
54 }
55}
56
57impl BackendStats {
58 #[must_use]
60 #[inline]
61 pub const fn average_article_size(&self) -> Option<u64> {
62 self.article_count
63 .average_bytes(self.article_bytes_total.get())
64 }
65
66 #[must_use]
68 #[inline]
69 pub fn average_ttfb_ms(&self) -> Option<f64> {
70 std::num::NonZeroU64::new(self.ttfb_count.get())
71 .map(|count| TtfbMicros::average(self.ttfb_micros_total, count).get())
72 }
73
74 #[must_use]
76 #[inline]
77 pub fn average_send_ms(&self) -> Option<f64> {
78 std::num::NonZeroU64::new(self.ttfb_count.get())
79 .map(|count| SendMicros::average(self.send_micros_total, count).get())
80 }
81
82 #[must_use]
84 #[inline]
85 pub fn average_recv_ms(&self) -> Option<f64> {
86 std::num::NonZeroU64::new(self.ttfb_count.get())
87 .map(|count| RecvMicros::average(self.recv_micros_total, count).get())
88 }
89
90 #[must_use]
92 #[inline]
93 pub fn average_overhead_ms(&self) -> Option<f64> {
94 std::num::NonZeroU64::new(self.ttfb_count.get()).map(|count| {
95 let ttfb = TtfbMicros::average(self.ttfb_micros_total, count);
96 let send = SendMicros::average(self.send_micros_total, count);
97 let recv = RecvMicros::average(self.recv_micros_total, count);
98 OverheadMillis::from_components(ttfb, send, recv).get()
99 })
100 }
101
102 #[must_use]
104 #[inline]
105 pub fn error_rate_percent(&self) -> f64 {
106 ErrorRatePercent::from_counts(self.errors, self.total_commands).get()
107 }
108
109 #[must_use]
111 #[inline]
112 pub fn has_high_error_rate(&self) -> bool {
113 ErrorRatePercent::from_counts(self.errors, self.total_commands).is_high()
114 }
115
116 #[must_use]
118 #[inline]
119 pub fn is_healthy(&self) -> bool {
120 self.health_status == BackendHealthStatus::Healthy
121 }
122
123 #[must_use]
125 #[inline]
126 pub fn is_degraded(&self) -> bool {
127 self.health_status == BackendHealthStatus::Degraded
128 }
129
130 #[must_use]
132 #[inline]
133 pub fn is_down(&self) -> bool {
134 self.health_status == BackendHealthStatus::Down
135 }
136
137 #[must_use]
139 #[inline]
140 pub const fn total_bytes(&self) -> u64 {
141 self.bytes_sent
142 .as_u64()
143 .saturating_add(self.bytes_received.as_u64())
144 }
145
146 #[must_use]
148 #[inline]
149 pub const fn has_activity(&self) -> bool {
150 self.total_commands.get() > 0
151 }
152}
153
154#[cfg(test)]
155#[allow(clippy::float_cmp)] mod tests {
157 use super::*;
158 use crate::types::BackendId;
159
160 fn create_test_stats() -> BackendStats {
161 BackendStats {
162 backend_id: BackendId::from_index(0),
163 total_commands: CommandCount::new(100),
164 errors: ErrorCount::new(5),
165 bytes_sent: BytesSent::new(1000),
166 bytes_received: BytesReceived::new(2000),
167 article_count: ArticleCount::new(10),
168 article_bytes_total: ArticleBytesTotal::new(5000),
169 ttfb_micros_total: TtfbMicros::new(10000),
170 ttfb_count: TimingMeasurementCount::new(10),
171 send_micros_total: SendMicros::new(3000),
172 recv_micros_total: RecvMicros::new(5000),
173 health_status: BackendHealthStatus::Healthy,
174 ..Default::default()
175 }
176 }
177
178 #[test]
179 fn test_backend_stats_default() {
180 let stats = BackendStats::default();
181 assert_eq!(stats.backend_id, BackendId::from_index(0));
182 assert_eq!(stats.total_commands.get(), 0);
183 assert_eq!(stats.errors.get(), 0);
184 assert_eq!(stats.bytes_sent.as_u64(), 0);
185 assert_eq!(stats.bytes_received.as_u64(), 0);
186 assert_eq!(stats.health_status, BackendHealthStatus::Healthy);
187 }
188
189 #[test]
190 fn test_average_article_size() {
191 let stats = create_test_stats();
192 let avg = stats.average_article_size();
193 assert_eq!(avg, Some(500)); }
195
196 #[test]
197 fn test_average_article_size_zero_articles() {
198 let stats = BackendStats {
199 article_count: ArticleCount::new(0),
200 article_bytes_total: ArticleBytesTotal::new(1000),
201 ..Default::default()
202 };
203 assert_eq!(stats.average_article_size(), None);
204 }
205
206 #[test]
207 fn test_average_article_size_zero_bytes() {
208 let stats = BackendStats {
209 article_count: ArticleCount::new(10),
210 article_bytes_total: ArticleBytesTotal::new(0),
211 ..Default::default()
212 };
213 assert_eq!(stats.average_article_size(), Some(0));
214 }
215
216 #[test]
217 fn test_average_ttfb_ms() {
218 let stats = create_test_stats();
219 let avg = stats.average_ttfb_ms();
220 assert!(avg.is_some());
221 assert!((avg.unwrap() - 1.0).abs() < 0.01);
223 }
224
225 #[test]
226 fn test_average_ttfb_ms_zero_count() {
227 let stats = BackendStats {
228 ttfb_micros_total: TtfbMicros::new(1000),
229 ttfb_count: TimingMeasurementCount::new(0),
230 ..Default::default()
231 };
232 assert_eq!(stats.average_ttfb_ms(), None);
233 }
234
235 #[test]
236 fn test_average_send_ms() {
237 let stats = create_test_stats();
238 let avg = stats.average_send_ms();
239 assert!(avg.is_some());
240 assert!((avg.unwrap() - 0.3).abs() < 0.01);
242 }
243
244 #[test]
245 fn test_average_send_ms_zero_count() {
246 let stats = BackendStats {
247 send_micros_total: SendMicros::new(1000),
248 ttfb_count: TimingMeasurementCount::new(0),
249 ..Default::default()
250 };
251 assert_eq!(stats.average_send_ms(), None);
252 }
253
254 #[test]
255 fn test_average_recv_ms() {
256 let stats = create_test_stats();
257 let avg = stats.average_recv_ms();
258 assert!(avg.is_some());
259 assert!((avg.unwrap() - 0.5).abs() < 0.01);
261 }
262
263 #[test]
264 fn test_average_recv_ms_zero_count() {
265 let stats = BackendStats {
266 recv_micros_total: RecvMicros::new(1000),
267 ttfb_count: TimingMeasurementCount::new(0),
268 ..Default::default()
269 };
270 assert_eq!(stats.average_recv_ms(), None);
271 }
272
273 #[test]
274 fn test_average_overhead_ms() {
275 let stats = create_test_stats();
276 let overhead = stats.average_overhead_ms();
277 assert!(overhead.is_some());
278 assert!((overhead.unwrap() - 0.2).abs() < 0.01);
280 }
281
282 #[test]
283 fn test_average_overhead_ms_zero_count() {
284 let stats = BackendStats::default();
285 assert_eq!(stats.average_overhead_ms(), None);
286 }
287
288 #[test]
289 fn test_error_rate_percent() {
290 let stats = create_test_stats();
291 let rate = stats.error_rate_percent();
292 assert!((rate - 5.0).abs() < 0.01); }
294
295 #[test]
296 fn test_error_rate_percent_zero_commands() {
297 let stats = BackendStats {
298 total_commands: CommandCount::new(0),
299 errors: ErrorCount::new(10),
300 ..Default::default()
301 };
302 assert_eq!(stats.error_rate_percent(), 0.0);
303 }
304
305 #[test]
306 fn test_error_rate_percent_no_errors() {
307 let stats = BackendStats {
308 total_commands: CommandCount::new(100),
309 errors: ErrorCount::new(0),
310 ..Default::default()
311 };
312 assert_eq!(stats.error_rate_percent(), 0.0);
313 }
314
315 #[test]
316 fn test_has_high_error_rate() {
317 let stats = BackendStats {
319 total_commands: CommandCount::new(100),
320 errors: ErrorCount::new(10),
321 ..Default::default()
322 };
323 assert!(stats.has_high_error_rate());
324
325 let stats_threshold = BackendStats {
327 total_commands: CommandCount::new(100),
328 errors: ErrorCount::new(5),
329 ..Default::default()
330 };
331 assert!(!stats_threshold.has_high_error_rate());
332
333 let stats_low = BackendStats {
335 total_commands: CommandCount::new(100),
336 errors: ErrorCount::new(3),
337 ..Default::default()
338 };
339 assert!(!stats_low.has_high_error_rate());
340 }
341
342 #[test]
343 fn test_is_healthy() {
344 let stats = BackendStats {
345 health_status: BackendHealthStatus::Healthy,
346 ..Default::default()
347 };
348 assert!(stats.is_healthy());
349 assert!(!stats.is_degraded());
350 assert!(!stats.is_down());
351 }
352
353 #[test]
354 fn test_is_degraded() {
355 let stats = BackendStats {
356 health_status: BackendHealthStatus::Degraded,
357 ..Default::default()
358 };
359 assert!(!stats.is_healthy());
360 assert!(stats.is_degraded());
361 assert!(!stats.is_down());
362 }
363
364 #[test]
365 fn test_is_down() {
366 let stats = BackendStats {
367 health_status: BackendHealthStatus::Down,
368 ..Default::default()
369 };
370 assert!(!stats.is_healthy());
371 assert!(!stats.is_degraded());
372 assert!(stats.is_down());
373 }
374
375 #[test]
376 fn test_total_bytes() {
377 let stats = create_test_stats();
378 assert_eq!(stats.total_bytes(), 3000); }
380
381 #[test]
382 fn test_total_bytes_zero() {
383 let stats = BackendStats::default();
384 assert_eq!(stats.total_bytes(), 0);
385 }
386
387 #[test]
388 fn test_total_bytes_saturating() {
389 let stats = BackendStats {
390 bytes_sent: BytesSent::new(u64::MAX),
391 bytes_received: BytesReceived::new(1),
392 ..Default::default()
393 };
394 assert_eq!(stats.total_bytes(), u64::MAX);
395 }
396
397 #[test]
398 fn test_has_activity() {
399 let stats = BackendStats {
400 total_commands: CommandCount::new(1),
401 ..Default::default()
402 };
403 assert!(stats.has_activity());
404 }
405
406 #[test]
407 fn test_has_activity_none() {
408 let stats = BackendStats::default();
409 assert!(!stats.has_activity());
410 }
411
412 #[test]
413 fn test_backend_stats_clone() {
414 let stats = create_test_stats();
415 let cloned = stats.clone();
416
417 assert_eq!(stats.backend_id, cloned.backend_id);
418 assert_eq!(stats.total_commands.get(), cloned.total_commands.get());
419 assert_eq!(stats.total_bytes(), cloned.total_bytes());
420 assert_eq!(stats.health_status, cloned.health_status);
421 }
422}