Skip to main content

nntp_proxy/types/
metrics.rs

1//! Type-safe metrics and measurement types
2//!
3//! Uses phantom types to provide zero-cost newtype wrappers with compile-time direction tracking.
4//! All byte counter types share the same implementation via `ByteCounter<D>` where `D` is a
5//! direction marker type (`ClientToBackend`, `BackendToClient`, etc.).
6
7use std::fmt;
8use std::marker::PhantomData;
9use std::ops::{Add, AddAssign};
10
11// ============================================================================
12// Direction Marker Types (zero-sized)
13// ============================================================================
14
15/// Marker: Client → Backend traffic
16#[derive(
17    Debug,
18    Clone,
19    Copy,
20    PartialEq,
21    Eq,
22    PartialOrd,
23    Ord,
24    Default,
25    serde::Serialize,
26    serde::Deserialize,
27)]
28pub struct ClientToBackend;
29
30/// Marker: Backend → Client traffic
31#[derive(
32    Debug,
33    Clone,
34    Copy,
35    PartialEq,
36    Eq,
37    PartialOrd,
38    Ord,
39    Default,
40    serde::Serialize,
41    serde::Deserialize,
42)]
43pub struct BackendToClient;
44
45/// Marker: Generic client traffic
46#[derive(
47    Debug,
48    Clone,
49    Copy,
50    PartialEq,
51    Eq,
52    PartialOrd,
53    Ord,
54    Default,
55    serde::Serialize,
56    serde::Deserialize,
57)]
58pub struct Client;
59
60/// Marker: Bytes sent
61#[derive(
62    Debug,
63    Clone,
64    Copy,
65    PartialEq,
66    Eq,
67    PartialOrd,
68    Ord,
69    Default,
70    serde::Serialize,
71    serde::Deserialize,
72)]
73pub struct Sent;
74
75/// Marker: Bytes received
76#[derive(
77    Debug,
78    Clone,
79    Copy,
80    PartialEq,
81    Eq,
82    PartialOrd,
83    Ord,
84    Default,
85    serde::Serialize,
86    serde::Deserialize,
87)]
88pub struct Received;
89
90// ============================================================================
91// Generic ByteCounter with Phantom Type Direction
92// ============================================================================
93
94/// Generic byte counter with compile-time direction tracking via phantom types
95///
96/// This zero-cost abstraction provides type-safe byte counting where the direction
97/// is encoded in the type system. The `PhantomData<D>` has zero size at runtime.
98#[repr(transparent)]
99#[derive(
100    Debug,
101    Clone,
102    Copy,
103    PartialEq,
104    Eq,
105    PartialOrd,
106    Ord,
107    Default,
108    serde::Serialize,
109    serde::Deserialize,
110)]
111pub struct ByteCounter<D> {
112    bytes: u64,
113    _direction: PhantomData<D>,
114}
115
116impl<D> ByteCounter<D> {
117    pub const ZERO: Self = Self {
118        bytes: 0,
119        _direction: PhantomData,
120    };
121
122    #[must_use]
123    pub const fn new(bytes: u64) -> Self {
124        Self {
125            bytes,
126            _direction: PhantomData,
127        }
128    }
129
130    #[must_use]
131    pub const fn zero() -> Self {
132        Self::ZERO
133    }
134
135    #[must_use]
136    pub const fn as_u64(&self) -> u64 {
137        self.bytes
138    }
139
140    #[must_use]
141    #[inline]
142    pub const fn add(self, bytes: usize) -> Self {
143        Self {
144            bytes: self.bytes + bytes as u64,
145            _direction: PhantomData,
146        }
147    }
148
149    #[must_use]
150    #[inline]
151    pub const fn add_u64(self, bytes: u64) -> Self {
152        Self {
153            bytes: self.bytes + bytes,
154            _direction: PhantomData,
155        }
156    }
157
158    #[must_use]
159    #[inline]
160    #[allow(clippy::needless_pass_by_value)] // Value semantics keep fluent byte-counter arithmetic ergonomic.
161    pub const fn saturating_sub(self, other: Self) -> Self {
162        // Byte counters are cheap value types and are used in fluent arithmetic
163        // chains, so by-value subtraction keeps the API ergonomic.
164        Self {
165            bytes: self.bytes.saturating_sub(other.bytes),
166            _direction: PhantomData,
167        }
168    }
169}
170
171impl<D> From<u64> for ByteCounter<D> {
172    #[inline]
173    fn from(bytes: u64) -> Self {
174        Self::new(bytes)
175    }
176}
177
178impl<D> From<ByteCounter<D>> for u64 {
179    #[inline]
180    fn from(counter: ByteCounter<D>) -> Self {
181        counter.bytes
182    }
183}
184
185impl<D> Add for ByteCounter<D> {
186    type Output = Self;
187    #[inline]
188    fn add(self, other: Self) -> Self {
189        Self {
190            bytes: self.bytes + other.bytes,
191            _direction: PhantomData,
192        }
193    }
194}
195
196impl<D> AddAssign for ByteCounter<D> {
197    #[inline]
198    fn add_assign(&mut self, other: Self) {
199        self.bytes += other.bytes;
200    }
201}
202
203impl<D> fmt::Display for ByteCounter<D> {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        write!(f, "{} bytes", self.bytes)
206    }
207}
208
209// ============================================================================
210// Type Aliases for Direction-Specific Counters
211// ============================================================================
212
213/// Client traffic metrics (Client ↔ Proxy)
214pub type ClientBytes = ByteCounter<Client>;
215
216/// Client → Backend traffic (request bytes)
217pub type ClientToBackendBytes = ByteCounter<ClientToBackend>;
218
219/// Backend → Client traffic (response bytes)
220pub type BackendToClientBytes = ByteCounter<BackendToClient>;
221
222/// Bytes sent by a backend or user
223pub type BytesSent = ByteCounter<Sent>;
224
225/// Bytes received by a backend or user
226pub type BytesReceived = ByteCounter<Received>;
227
228/// Transfer statistics for a session
229#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
230pub struct TransferMetrics {
231    pub client_to_backend: ClientToBackendBytes,
232    pub backend_to_client: BackendToClientBytes,
233}
234
235impl TransferMetrics {
236    #[must_use]
237    pub const fn zero() -> Self {
238        Self {
239            client_to_backend: ClientToBackendBytes::ZERO,
240            backend_to_client: BackendToClientBytes::ZERO,
241        }
242    }
243
244    #[must_use]
245    pub const fn new(client_to_backend: u64, backend_to_client: u64) -> Self {
246        Self {
247            client_to_backend: ClientToBackendBytes::new(client_to_backend),
248            backend_to_client: BackendToClientBytes::new(backend_to_client),
249        }
250    }
251
252    #[must_use]
253    #[inline]
254    pub const fn total(&self) -> u64 {
255        self.client_to_backend.as_u64() + self.backend_to_client.as_u64()
256    }
257
258    #[must_use]
259    #[inline]
260    pub const fn as_tuple(&self) -> (u64, u64) {
261        (
262            self.client_to_backend.as_u64(),
263            self.backend_to_client.as_u64(),
264        )
265    }
266
267    /// Saturating subtraction of two transfer metrics
268    #[must_use]
269    pub const fn saturating_sub(self, other: Self) -> Self {
270        Self {
271            client_to_backend: self
272                .client_to_backend
273                .saturating_sub(other.client_to_backend),
274            backend_to_client: self
275                .backend_to_client
276                .saturating_sub(other.backend_to_client),
277        }
278    }
279}
280
281impl From<(u64, u64)> for TransferMetrics {
282    fn from((c2b, b2c): (u64, u64)) -> Self {
283        Self::new(c2b, b2c)
284    }
285}
286
287impl fmt::Display for TransferMetrics {
288    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289        write!(
290            f,
291            "sent: {}, received: {}",
292            self.client_to_backend, self.backend_to_client
293        )
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use proptest::prelude::*;
301
302    // ============================================================================
303    // Critical Property Tests - Arithmetic Operations
304    // ============================================================================
305
306    proptest! {
307        /// Property: ByteCounter arithmetic works correctly across all direction types
308        #[test]
309        fn prop_byte_counter_arithmetic(initial in 0u64..1_000_000, increment in 0usize..1_000_000, b in 0u64..1_000_000) {
310            // Test add() method
311            let c2b = ClientToBackendBytes::new(initial).add(increment);
312            let b2c = BackendToClientBytes::new(initial).add(increment);
313            prop_assert_eq!(c2b.as_u64(), initial + increment as u64);
314            prop_assert_eq!(b2c.as_u64(), initial + increment as u64);
315
316            // Test Add trait
317            let sum1 = ClientToBackendBytes::new(initial) + ClientToBackendBytes::new(b);
318            let sum2 = BackendToClientBytes::new(initial) + BackendToClientBytes::new(b);
319            prop_assert_eq!(sum1.as_u64(), initial + b);
320            prop_assert_eq!(sum2.as_u64(), initial + b);
321
322            // Test saturating_sub
323            prop_assert_eq!(
324                ClientToBackendBytes::new(initial).saturating_sub(ClientToBackendBytes::new(b)).as_u64(),
325                initial.saturating_sub(b)
326            );
327            prop_assert_eq!(
328                BackendToClientBytes::new(initial).saturating_sub(BackendToClientBytes::new(b)).as_u64(),
329                initial.saturating_sub(b)
330            );
331        }
332    }
333
334    #[test]
335    fn test_phantom_type_zero_size() {
336        use std::mem::size_of;
337        // Verify PhantomData has zero runtime cost
338        assert_eq!(size_of::<ClientToBackendBytes>(), size_of::<u64>());
339        assert_eq!(size_of::<BackendToClientBytes>(), size_of::<u64>());
340        assert_eq!(size_of::<BytesSent>(), size_of::<u64>());
341    }
342
343    // ============================================================================
344    // TransferMetrics Tests
345    // ============================================================================
346
347    #[test]
348    fn test_transfer_metrics_total() {
349        let metrics = TransferMetrics::new(100, 200);
350        assert_eq!(metrics.total(), 300);
351        assert_eq!(metrics.as_tuple(), (100, 200));
352    }
353
354    proptest! {
355        /// Property: total() equals sum of components
356        #[test]
357        fn prop_transfer_metrics_total(c2b in 0u64..1_000_000, b2c in 0u64..1_000_000) {
358            let metrics = TransferMetrics::new(c2b, b2c);
359            prop_assert_eq!(metrics.total(), c2b + b2c);
360        }
361
362        /// Property: saturating_sub works on TransferMetrics
363        #[test]
364        fn prop_transfer_metrics_saturating_sub(a1 in 0u64..1_000_000, a2 in 0u64..1_000_000, b1 in 0u64..1_000_000, b2 in 0u64..1_000_000) {
365            let metrics1 = TransferMetrics::new(a1, a2);
366            let metrics2 = TransferMetrics::new(b1, b2);
367            let diff = metrics1.saturating_sub(metrics2);
368
369            prop_assert_eq!(diff.client_to_backend.as_u64(), a1.saturating_sub(b1));
370            prop_assert_eq!(diff.backend_to_client.as_u64(), a2.saturating_sub(b2));
371        }
372    }
373
374    // ========================================================================
375    // define_counter! macro tests — Display with unit and empty unit
376    // ========================================================================
377
378    #[test]
379    fn test_counter_display_with_unit() {
380        let c = TotalConnections::new(42);
381        assert_eq!(format!("{c}"), "42 connections");
382    }
383
384    #[test]
385    fn test_counter_display_with_unit_zero() {
386        let c = TotalConnections::new(0);
387        assert_eq!(format!("{c}"), "0 connections");
388    }
389
390    #[test]
391    fn test_counter_display_bytes_per_second() {
392        let c = BytesPerSecondRate::new(1500);
393        assert_eq!(format!("{c}"), "1500 B/s");
394    }
395
396    #[test]
397    fn test_counter_display_article_bytes() {
398        let c = ArticleBytesTotal::new(999_999);
399        assert_eq!(format!("{c}"), "999999 bytes");
400    }
401
402    #[test]
403    fn test_counter_display_empty_unit() {
404        // TimingMeasurementCount uses empty unit — should NOT have trailing space
405        let c = TimingMeasurementCount::new(7);
406        assert_eq!(format!("{c}"), "7");
407    }
408
409    #[test]
410    fn test_counter_display_empty_unit_zero() {
411        let c = TimingMeasurementCount::new(0);
412        assert_eq!(format!("{c}"), "0");
413    }
414
415    #[test]
416    fn test_counter_display_empty_unit_large() {
417        let c = TimingMeasurementCount::new(u64::MAX);
418        assert_eq!(format!("{c}"), format!("{}", u64::MAX));
419    }
420
421    #[test]
422    fn test_timing_measurement_count_new_and_get() {
423        let c = TimingMeasurementCount::new(123);
424        assert_eq!(c.get(), 123);
425    }
426
427    #[test]
428    fn test_timing_measurement_count_zero_constant() {
429        assert_eq!(TimingMeasurementCount::ZERO.get(), 0);
430    }
431
432    #[test]
433    fn test_timing_measurement_count_from_u64() {
434        let c = TimingMeasurementCount::from(55u64);
435        assert_eq!(c.get(), 55);
436    }
437
438    #[test]
439    fn test_timing_measurement_count_default() {
440        let c = TimingMeasurementCount::default();
441        assert_eq!(c.get(), 0);
442    }
443
444    #[test]
445    fn test_timing_measurement_count_eq() {
446        assert_eq!(
447            TimingMeasurementCount::new(10),
448            TimingMeasurementCount::new(10)
449        );
450        assert_ne!(
451            TimingMeasurementCount::new(10),
452            TimingMeasurementCount::new(11)
453        );
454    }
455
456    #[test]
457    fn test_timing_measurement_count_ord() {
458        assert!(TimingMeasurementCount::new(5) < TimingMeasurementCount::new(10));
459        assert!(TimingMeasurementCount::new(10) > TimingMeasurementCount::new(5));
460    }
461
462    #[test]
463    fn test_timing_measurement_count_clone_copy() {
464        let a = TimingMeasurementCount::new(42);
465        let b = a; // Copy
466        assert_eq!(a, b);
467    }
468
469    #[test]
470    fn test_timing_measurement_count_debug() {
471        let c = TimingMeasurementCount::new(99);
472        let dbg = format!("{c:?}");
473        assert!(dbg.contains("99"));
474    }
475
476    #[test]
477    fn test_counter_repr_transparent() {
478        use std::mem::size_of;
479        // All counter types should be exactly u64-sized due to #[repr(transparent)]
480        assert_eq!(size_of::<TotalConnections>(), size_of::<u64>());
481        assert_eq!(size_of::<BytesPerSecondRate>(), size_of::<u64>());
482        assert_eq!(size_of::<ArticleBytesTotal>(), size_of::<u64>());
483        assert_eq!(size_of::<TimingMeasurementCount>(), size_of::<u64>());
484    }
485}
486
487// ============================================================================
488// Macro for Display-Oriented Counter Types (with unit strings)
489// ============================================================================
490
491/// Define a counter type with a unit string for display formatting.
492///
493/// Unlike `metrics::types::counter_type!` which is for internal counting operations
494/// with `increment()` and `saturating_sub()`, this macro creates types focused on
495/// display with a unit suffix (e.g., "42 connections").
496macro_rules! define_counter {
497    ($name:ident, $unit:expr) => {
498        #[repr(transparent)]
499        #[derive(
500            Debug,
501            Clone,
502            Copy,
503            PartialEq,
504            Eq,
505            PartialOrd,
506            Ord,
507            Default,
508            serde::Serialize,
509            serde::Deserialize,
510        )]
511        pub struct $name(u64);
512
513        impl $name {
514            pub const ZERO: Self = Self(0);
515
516            #[must_use]
517            pub const fn new(value: u64) -> Self {
518                Self(value)
519            }
520
521            #[must_use]
522            pub const fn get(&self) -> u64 {
523                self.0
524            }
525        }
526
527        impl From<u64> for $name {
528            #[inline]
529            fn from(value: u64) -> Self {
530                Self(value)
531            }
532        }
533
534        impl fmt::Display for $name {
535            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
536                let unit: &str = $unit;
537                if unit.is_empty() {
538                    write!(f, "{}", self.0)
539                } else {
540                    write!(f, "{} {}", self.0, unit)
541                }
542            }
543        }
544    };
545}
546
547// ============================================================================
548// Specific Counter Types
549// ============================================================================
550
551define_counter!(TotalConnections, "connections");
552define_counter!(BytesPerSecondRate, "B/s");
553define_counter!(ArticleBytesTotal, "bytes");
554
555define_counter!(TimingMeasurementCount, "");