nntp_proxy/metrics/
user_stats.rs1use crate::metrics::types::{CommandCount, ErrorCount};
4use crate::types::{BytesPerSecondRate, BytesReceived, BytesSent, TotalConnections};
5
6#[allow(clippy::cast_precision_loss)] const fn count_as_f64_for_rate(value: u64) -> f64 {
8 value as f64
11}
12
13#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
15pub struct UserStats {
16 pub username: String,
17 #[serde(skip, default)]
18 pub active_connections: usize,
19 pub total_connections: TotalConnections,
20 pub bytes_sent: BytesSent,
21 pub bytes_received: BytesReceived,
22 pub total_commands: CommandCount,
23 pub errors: ErrorCount,
24 #[serde(skip, default)]
26 pub bytes_sent_per_sec: BytesPerSecondRate,
27 #[serde(skip, default)]
28 pub bytes_received_per_sec: BytesPerSecondRate,
29}
30
31impl UserStats {
32 #[must_use]
34 pub fn new(username: impl Into<String>) -> Self {
35 Self {
36 username: username.into(),
37 ..Default::default()
38 }
39 }
40
41 #[must_use]
43 #[inline]
44 pub const fn total_bytes(&self) -> u64 {
45 self.bytes_sent
46 .as_u64()
47 .saturating_add(self.bytes_received.as_u64())
48 }
49
50 #[must_use]
52 #[inline]
53 pub const fn total_bytes_per_sec(&self) -> u64 {
54 self.bytes_sent_per_sec
55 .get()
56 .saturating_add(self.bytes_received_per_sec.get())
57 }
58
59 #[must_use]
61 pub fn error_rate_percent(&self) -> f64 {
62 let total_cmds = self.total_commands.get();
63 if total_cmds > 0 {
64 (count_as_f64_for_rate(self.errors.get()) / count_as_f64_for_rate(total_cmds)) * 100.0
65 } else {
66 0.0
67 }
68 }
69
70 #[must_use]
72 #[inline]
73 pub const fn has_activity(&self) -> bool {
74 self.total_commands.get() > 0 || self.total_connections.get() > 0
75 }
76
77 #[must_use]
79 #[inline]
80 pub const fn is_connected(&self) -> bool {
81 self.active_connections > 0
82 }
83}
84
85#[cfg(test)]
86#[allow(clippy::float_cmp)] mod tests {
88 use super::*;
89
90 #[test]
91 fn test_user_stats_new() {
92 let stats = UserStats::new("testuser");
93 assert_eq!(stats.username, "testuser");
94 assert_eq!(stats.active_connections, 0);
95 assert_eq!(stats.total_connections.get(), 0);
96 assert_eq!(stats.bytes_sent.as_u64(), 0);
97 assert_eq!(stats.bytes_received.as_u64(), 0);
98 assert_eq!(stats.total_commands.get(), 0);
99 assert_eq!(stats.errors.get(), 0);
100 }
101
102 #[test]
103 fn test_user_stats_new_with_string() {
104 let username = String::from("alice");
105 let stats = UserStats::new(username);
106 assert_eq!(stats.username, "alice");
107 }
108
109 #[test]
110 fn test_total_bytes() {
111 let stats = UserStats {
112 bytes_sent: BytesSent::new(1000),
113 bytes_received: BytesReceived::new(2000),
114 ..Default::default()
115 };
116 assert_eq!(stats.total_bytes(), 3000);
117 }
118
119 #[test]
120 fn test_total_bytes_zero() {
121 let stats = UserStats::default();
122 assert_eq!(stats.total_bytes(), 0);
123 }
124
125 #[test]
126 fn test_total_bytes_saturating() {
127 let stats = UserStats {
128 bytes_sent: BytesSent::new(u64::MAX),
129 bytes_received: BytesReceived::new(1),
130 ..Default::default()
131 };
132 assert_eq!(stats.total_bytes(), u64::MAX);
134 }
135
136 #[test]
137 fn test_total_bytes_per_sec() {
138 let stats = UserStats {
139 bytes_sent_per_sec: BytesPerSecondRate::new(100),
140 bytes_received_per_sec: BytesPerSecondRate::new(200),
141 ..Default::default()
142 };
143 assert_eq!(stats.total_bytes_per_sec(), 300);
144 }
145
146 #[test]
147 fn test_total_bytes_per_sec_zero() {
148 let stats = UserStats::default();
149 assert_eq!(stats.total_bytes_per_sec(), 0);
150 }
151
152 #[test]
153 fn test_total_bytes_per_sec_saturating() {
154 let stats = UserStats {
155 bytes_sent_per_sec: BytesPerSecondRate::new(u64::MAX),
156 bytes_received_per_sec: BytesPerSecondRate::new(1),
157 ..Default::default()
158 };
159 assert_eq!(stats.total_bytes_per_sec(), u64::MAX);
160 }
161
162 #[test]
163 fn test_error_rate_percent() {
164 let stats = UserStats {
165 total_commands: CommandCount::new(100),
166 errors: ErrorCount::new(5),
167 ..Default::default()
168 };
169 assert!((stats.error_rate_percent() - 5.0).abs() < 0.01);
170 }
171
172 #[test]
173 fn test_error_rate_percent_zero_commands() {
174 let stats = UserStats {
175 total_commands: CommandCount::new(0),
176 errors: ErrorCount::new(10),
177 ..Default::default()
178 };
179 assert_eq!(stats.error_rate_percent(), 0.0);
180 }
181
182 #[test]
183 fn test_error_rate_percent_no_errors() {
184 let stats = UserStats {
185 total_commands: CommandCount::new(100),
186 errors: ErrorCount::new(0),
187 ..Default::default()
188 };
189 assert_eq!(stats.error_rate_percent(), 0.0);
190 }
191
192 #[test]
193 fn test_error_rate_percent_high() {
194 let stats = UserStats {
195 total_commands: CommandCount::new(10),
196 errors: ErrorCount::new(3),
197 ..Default::default()
198 };
199 assert!((stats.error_rate_percent() - 30.0).abs() < 0.01);
200 }
201
202 #[test]
203 fn test_has_activity_with_commands() {
204 let stats = UserStats {
205 total_commands: CommandCount::new(1),
206 ..Default::default()
207 };
208 assert!(stats.has_activity());
209 }
210
211 #[test]
212 fn test_has_activity_with_connections() {
213 let stats = UserStats {
214 total_connections: TotalConnections::new(1),
215 ..Default::default()
216 };
217 assert!(stats.has_activity());
218 }
219
220 #[test]
221 fn test_has_activity_with_both() {
222 let stats = UserStats {
223 total_commands: CommandCount::new(5),
224 total_connections: TotalConnections::new(2),
225 ..Default::default()
226 };
227 assert!(stats.has_activity());
228 }
229
230 #[test]
231 fn test_has_activity_none() {
232 let stats = UserStats::default();
233 assert!(!stats.has_activity());
234 }
235
236 #[test]
237 fn test_is_connected_active() {
238 let stats = UserStats {
239 active_connections: 1,
240 ..Default::default()
241 };
242 assert!(stats.is_connected());
243 }
244
245 #[test]
246 fn test_is_connected_multiple() {
247 let stats = UserStats {
248 active_connections: 5,
249 ..Default::default()
250 };
251 assert!(stats.is_connected());
252 }
253
254 #[test]
255 fn test_is_connected_none() {
256 let stats = UserStats::default();
257 assert!(!stats.is_connected());
258 }
259
260 #[test]
261 fn test_user_stats_default() {
262 let stats = UserStats::default();
263 assert_eq!(stats.username, "");
264 assert_eq!(stats.active_connections, 0);
265 assert!(!stats.has_activity());
266 assert!(!stats.is_connected());
267 }
268
269 #[test]
270 fn test_user_stats_clone() {
271 let stats = UserStats {
272 username: "bob".to_string(),
273 active_connections: 2,
274 total_connections: TotalConnections::new(10),
275 bytes_sent: BytesSent::new(1000),
276 bytes_received: BytesReceived::new(2000),
277 total_commands: CommandCount::new(50),
278 errors: ErrorCount::new(2),
279 bytes_sent_per_sec: BytesPerSecondRate::new(100),
280 bytes_received_per_sec: BytesPerSecondRate::new(200),
281 };
282
283 let cloned = stats.clone();
284 assert_eq!(stats.username, cloned.username);
285 assert_eq!(stats.active_connections, cloned.active_connections);
286 assert_eq!(stats.total_bytes(), cloned.total_bytes());
287 }
288}