http_stat/
stats.rs

1// See the License for the specific language governing permissions and
2// limitations under the License.
3
4// Copyright 2025 Tree xie.
5//
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18use bytes::Bytes;
19use bytesize::ByteSize;
20use heck::ToTrainCase;
21use http::HeaderMap;
22use http::HeaderValue;
23use http::StatusCode;
24use nu_ansi_term::Color::{LightCyan, LightGreen, LightRed};
25use std::fmt;
26use std::time::Duration;
27use unicode_truncate::Alignment;
28use unicode_truncate::UnicodeTruncateStr;
29
30pub static ALPN_HTTP2: &str = "h2";
31pub static ALPN_HTTP1: &str = "http/1.1";
32pub static ALPN_HTTP3: &str = "h3";
33
34fn format_duration(duration: Duration) -> String {
35    if duration > Duration::from_secs(1) {
36        return format!("{:.2}s", duration.as_secs_f64());
37    }
38    if duration > Duration::from_millis(1) {
39        return format!("{}ms", duration.as_millis());
40    }
41    format!("{}µs", duration.as_micros())
42}
43
44struct Timeline {
45    name: String,
46    duration: Duration,
47}
48
49/// Statistics and information collected during an HTTP request.
50///
51/// This struct contains timing information for each phase of the request,
52/// connection details, TLS information, and response data.
53///
54/// # Fields
55///
56/// * `dns_lookup` - Time taken for DNS resolution
57/// * `quic_connect` - Time taken to establish QUIC connection (for HTTP/3)
58/// * `tcp_connect` - Time taken to establish TCP connection
59/// * `tls_handshake` - Time taken for TLS handshake (for HTTPS)
60/// * `server_processing` - Time taken for server to process the request
61/// * `content_transfer` - Time taken to transfer the response body
62/// * `total` - Total time taken for the entire request
63/// * `addr` - Resolved IP address and port
64/// * `status` - HTTP response status code
65/// * `tls` - TLS protocol version used
66/// * `alpn` - Application-Layer Protocol Negotiation (ALPN) protocol selected
67/// * `cert_not_before` - Certificate validity start time
68/// * `cert_not_after` - Certificate validity end time
69/// * `cert_cipher` - TLS cipher suite used
70/// * `cert_domains` - List of domains in the certificate's Subject Alternative Names
71/// * `body` - Response body content
72/// * `headers` - Response headers
73/// * `error` - Any error that occurred during the request
74#[derive(Default, Debug)]
75pub struct HttpStat {
76    pub dns_lookup: Option<Duration>,
77    pub quic_connect: Option<Duration>,
78    pub tcp_connect: Option<Duration>,
79    pub tls_handshake: Option<Duration>,
80    pub server_processing: Option<Duration>,
81    pub content_transfer: Option<Duration>,
82    pub total: Option<Duration>,
83    pub addr: Option<String>,
84    pub status: Option<StatusCode>,
85    pub tls: Option<String>,
86    pub alpn: Option<String>,
87    pub subject: Option<String>,
88    pub issuer: Option<String>,
89    pub cert_not_before: Option<String>,
90    pub cert_not_after: Option<String>,
91    pub cert_cipher: Option<String>,
92    pub cert_domains: Option<Vec<String>>,
93    pub body: Option<Bytes>,
94    pub body_size: Option<usize>,
95    pub headers: Option<HeaderMap<HeaderValue>>,
96    pub error: Option<String>,
97    pub silent: bool,
98}
99
100impl fmt::Display for HttpStat {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        if let Some(addr) = &self.addr {
103            let mut text = format!(
104                "{} {}",
105                LightGreen.paint("Connected to"),
106                LightCyan.paint(addr)
107            );
108            if self.silent {
109                if let Some(status) = &self.status {
110                    let alpn = self.alpn.clone().unwrap_or_else(|| ALPN_HTTP1.to_string());
111                    let status_code = status.as_u16();
112                    let status = if status_code < 400 {
113                        LightGreen.paint(status.to_string())
114                    } else {
115                        LightRed.paint(status.to_string())
116                    };
117                    text = format!(
118                        "{text} --> {} {}",
119                        LightCyan.paint(alpn.to_uppercase()),
120                        status
121                    );
122                } else {
123                    text = format!("{text} --> {}", LightRed.paint("FAIL"));
124                }
125            }
126            writeln!(f, "{}", text)?;
127        }
128        if let Some(error) = &self.error {
129            writeln!(f, "Error: {}", LightRed.paint(error))?;
130        }
131        if self.silent {
132            return Ok(());
133        }
134        if let Some(status) = &self.status {
135            let alpn = self.alpn.clone().unwrap_or_else(|| ALPN_HTTP1.to_string());
136            let status_code = status.as_u16();
137            let status = if status_code < 400 {
138                LightGreen.paint(status.to_string())
139            } else {
140                LightRed.paint(status.to_string())
141            };
142            writeln!(f, "{} {}", LightCyan.paint(alpn.to_uppercase()), status)?;
143        }
144
145        if let Some(tls) = &self.tls {
146            writeln!(f, "{}: {}", "tls".to_train_case(), LightCyan.paint(tls))?;
147            writeln!(
148                f,
149                "{}: {}",
150                "cipher".to_train_case(),
151                LightCyan.paint(self.cert_cipher.clone().unwrap_or_default())
152            )?;
153            writeln!(
154                f,
155                "{}: {}",
156                "not before".to_train_case(),
157                LightCyan.paint(self.cert_not_before.clone().unwrap_or_default())
158            )?;
159            writeln!(
160                f,
161                "{}: {}",
162                "not after".to_train_case(),
163                LightCyan.paint(self.cert_not_after.clone().unwrap_or_default())
164            )?;
165            writeln!(f)?;
166        }
167
168        if let Some(headers) = &self.headers {
169            for (key, value) in headers.iter() {
170                writeln!(
171                    f,
172                    "{}: {}",
173                    key.to_string().to_train_case(),
174                    LightCyan.paint(value.to_str().unwrap_or_default())
175                )?;
176            }
177            writeln!(f)?;
178        }
179
180        if let Some(body) = &self.body {
181            let status = self.status.unwrap_or(StatusCode::OK).as_u16();
182            if status >= 400 {
183                let body = std::str::from_utf8(body.as_ref()).unwrap_or_default();
184                writeln!(f, "Body: {}", LightRed.paint(body))?;
185            } else {
186                let text = format!(
187                    "Body discarded {}",
188                    ByteSize(self.body_size.unwrap_or(0) as u64)
189                );
190                writeln!(f, "{}", LightCyan.paint(text))?;
191            }
192        }
193
194        let width = 20;
195
196        let mut timelines = vec![];
197        if let Some(value) = self.dns_lookup {
198            timelines.push(Timeline {
199                name: "DNS Lookup".to_string(),
200                duration: value,
201            });
202        }
203
204        if let Some(value) = self.tcp_connect {
205            timelines.push(Timeline {
206                name: "TCP Connect".to_string(),
207                duration: value,
208            });
209        }
210
211        if let Some(value) = self.tls_handshake {
212            timelines.push(Timeline {
213                name: "TLS Handshake".to_string(),
214                duration: value,
215            });
216        }
217        if let Some(value) = self.quic_connect {
218            timelines.push(Timeline {
219                name: "QUIC Connect".to_string(),
220                duration: value,
221            });
222        }
223
224        if let Some(value) = self.server_processing {
225            timelines.push(Timeline {
226                name: "Server Processing".to_string(),
227                duration: value,
228            });
229        }
230
231        if let Some(value) = self.content_transfer {
232            timelines.push(Timeline {
233                name: "Content Transfer".to_string(),
234                duration: value,
235            });
236        }
237
238        // print name
239        write!(f, " ")?;
240        for (i, timeline) in timelines.iter().enumerate() {
241            write!(
242                f,
243                "{}",
244                timeline.name.unicode_pad(width, Alignment::Center, true)
245            )?;
246            if i < timelines.len() - 1 {
247                write!(f, " ")?;
248            }
249        }
250        writeln!(f)?;
251
252        // print duration
253        write!(f, "[")?;
254        for (i, timeline) in timelines.iter().enumerate() {
255            write!(
256                f,
257                "{}",
258                LightCyan.paint(
259                    format_duration(timeline.duration)
260                        .unicode_pad(width, Alignment::Center, true)
261                        .to_string(),
262                )
263            )?;
264            if i < timelines.len() - 1 {
265                write!(f, "|")?;
266            }
267        }
268        writeln!(f, "]")?;
269
270        // print | line
271        write!(f, " ")?;
272        for _ in 0..timelines.len() {
273            write!(f, "{}", " ".repeat(width))?;
274            write!(f, "|")?;
275        }
276        writeln!(f)?;
277
278        write!(f, "{}", " ".repeat(width * timelines.len()))?;
279        write!(
280            f,
281            "total:{}",
282            LightCyan.paint(format_duration(self.total.unwrap_or_default()))
283        )?;
284
285        Ok(())
286    }
287}