isahc/
metrics.rs

1//! Request and response metrics tracking.
2
3use crossbeam_utils::atomic::AtomicCell;
4use std::{fmt, sync::Arc, time::Duration};
5
6/// An object that holds status updates and progress statistics on a particular
7/// request. A [`Metrics`] can be shared between threads, which allows an agent
8/// thread to post updates to the object while consumers can read from the
9/// object simultaneously.
10///
11/// Reading stats is not always guaranteed to be up-to-date.
12#[derive(Clone)]
13pub struct Metrics {
14    pub(crate) inner: Arc<Inner>,
15}
16
17#[derive(Default)]
18pub(crate) struct Inner {
19    pub(crate) upload_progress: AtomicCell<f64>,
20    pub(crate) upload_total: AtomicCell<f64>,
21    pub(crate) download_progress: AtomicCell<f64>,
22    pub(crate) download_total: AtomicCell<f64>,
23
24    pub(crate) upload_speed: AtomicCell<f64>,
25    pub(crate) download_speed: AtomicCell<f64>,
26
27    // An overview of the six time values (taken from the curl documentation):
28    //
29    // curl_easy_perform()
30    //     |
31    //     |--NAMELOOKUP
32    //     |--|--CONNECT
33    //     |--|--|--APPCONNECT
34    //     |--|--|--|--PRETRANSFER
35    //     |--|--|--|--|--STARTTRANSFER
36    //     |--|--|--|--|--|--TOTAL
37    //     |--|--|--|--|--|--REDIRECT
38    //
39    // The numbers we expose in the API are a little more "high-level" than the
40    // ones written here.
41    pub(crate) namelookup_time: AtomicCell<f64>,
42    pub(crate) connect_time: AtomicCell<f64>,
43    pub(crate) appconnect_time: AtomicCell<f64>,
44    pub(crate) pretransfer_time: AtomicCell<f64>,
45    pub(crate) starttransfer_time: AtomicCell<f64>,
46    pub(crate) total_time: AtomicCell<f64>,
47    pub(crate) redirect_time: AtomicCell<f64>,
48}
49
50impl Metrics {
51    pub(crate) fn new() -> Self {
52        Self {
53            inner: Arc::default(),
54        }
55    }
56
57    /// Number of bytes uploaded / estimated total.
58    pub fn upload_progress(&self) -> (u64, u64) {
59        (
60            self.inner.upload_progress.load() as u64,
61            self.inner.upload_total.load() as u64,
62        )
63    }
64
65    /// Average upload speed so far in bytes/second.
66    pub fn upload_speed(&self) -> f64 {
67        self.inner.upload_speed.load()
68    }
69
70    /// Number of bytes downloaded / estimated total.
71    pub fn download_progress(&self) -> (u64, u64) {
72        (
73            self.inner.download_progress.load() as u64,
74            self.inner.download_total.load() as u64,
75        )
76    }
77
78    /// Average download speed so far in bytes/second.
79    pub fn download_speed(&self) -> f64 {
80        self.inner.download_speed.load()
81    }
82
83    /// Get the total time from the start of the request until DNS name
84    /// resolving was completed.
85    ///
86    /// When a redirect is followed, the time from each request is added
87    /// together.
88    pub fn name_lookup_time(&self) -> Duration {
89        Duration::from_secs_f64(self.inner.namelookup_time.load())
90    }
91
92    /// Get the amount of time taken to establish a connection to the server
93    /// (not including TLS connection time).
94    ///
95    /// When a redirect is followed, the time from each request is added
96    /// together.
97    pub fn connect_time(&self) -> Duration {
98        Duration::from_secs_f64(
99            (self.inner.connect_time.load() - self.inner.namelookup_time.load()).max(0f64),
100        )
101    }
102
103    /// Get the amount of time spent on TLS handshakes.
104    ///
105    /// When a redirect is followed, the time from each request is added
106    /// together.
107    pub fn secure_connect_time(&self) -> Duration {
108        let app_connect_time = self.inner.appconnect_time.load();
109
110        if app_connect_time > 0f64 {
111            Duration::from_secs_f64(app_connect_time - self.inner.connect_time.load())
112        } else {
113            Duration::new(0, 0)
114        }
115    }
116
117    /// Get the time it took from the start of the request until the first
118    /// byte is either sent or received.
119    ///
120    /// When a redirect is followed, the time from each request is added
121    /// together.
122    pub fn transfer_start_time(&self) -> Duration {
123        Duration::from_secs_f64(self.inner.starttransfer_time.load())
124    }
125
126    /// Get the amount of time spent performing the actual request transfer. The
127    /// "transfer" includes both sending the request and receiving the response.
128    ///
129    /// When a redirect is followed, the time from each request is added
130    /// together.
131    pub fn transfer_time(&self) -> Duration {
132        Duration::from_secs_f64(
133            (self.inner.total_time.load() - self.inner.starttransfer_time.load()).max(0f64),
134        )
135    }
136
137    /// Get the total time for the entire request. This will continuously
138    /// increase until the entire response body is consumed and completed.
139    ///
140    /// When a redirect is followed, the time from each request is added
141    /// together.
142    pub fn total_time(&self) -> Duration {
143        Duration::from_secs_f64(self.inner.total_time.load())
144    }
145
146    /// If automatic redirect following is enabled, gets the total time taken
147    /// for all redirection steps including name lookup, connect, pretransfer
148    /// and transfer before final transaction was started.
149    pub fn redirect_time(&self) -> Duration {
150        Duration::from_secs_f64(self.inner.redirect_time.load())
151    }
152}
153
154impl fmt::Debug for Metrics {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        f.debug_struct("Metrics")
157            .field("upload_progress", &self.upload_progress())
158            .field("upload_speed", &self.upload_speed())
159            .field("download_progress", &self.download_progress())
160            .field("download_speed", &self.download_speed())
161            .field("name_lookup_time", &self.name_lookup_time())
162            .field("connect_time", &self.connect_time())
163            .field("secure_connect_time", &self.secure_connect_time())
164            .field("transfer_start_time", &self.transfer_start_time())
165            .field("transfer_time", &self.transfer_time())
166            .field("total_time", &self.total_time())
167            .field("redirect_time", &self.redirect_time())
168            .finish()
169    }
170}