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::io::Write;
27use std::time::Duration;
28use tempfile::NamedTempFile;
29use unicode_truncate::Alignment;
30use unicode_truncate::UnicodeTruncateStr;
31
32pub static ALPN_HTTP2: &str = "h2";
33pub static ALPN_HTTP1: &str = "http/1.1";
34pub static ALPN_HTTP3: &str = "h3";
35
36fn format_duration(duration: Duration) -> String {
37    if duration > Duration::from_secs(1) {
38        return format!("{:.2}s", duration.as_secs_f64());
39    }
40    if duration > Duration::from_millis(1) {
41        return format!("{}ms", duration.as_millis());
42    }
43    format!("{}µs", duration.as_micros())
44}
45
46struct Timeline {
47    name: String,
48    duration: Duration,
49}
50
51/// Statistics and information collected during an HTTP request.
52///
53/// This struct contains timing information for each phase of the request,
54/// connection details, TLS information, and response data.
55///
56/// # Fields
57///
58/// * `dns_lookup` - Time taken for DNS resolution
59/// * `quic_connect` - Time taken to establish QUIC connection (for HTTP/3)
60/// * `tcp_connect` - Time taken to establish TCP connection
61/// * `tls_handshake` - Time taken for TLS handshake (for HTTPS)
62/// * `server_processing` - Time taken for server to process the request
63/// * `content_transfer` - Time taken to transfer the response body
64/// * `total` - Total time taken for the entire request
65/// * `addr` - Resolved IP address and port
66/// * `status` - HTTP response status code
67/// * `tls` - TLS protocol version used
68/// * `alpn` - Application-Layer Protocol Negotiation (ALPN) protocol selected
69/// * `cert_not_before` - Certificate validity start time
70/// * `cert_not_after` - Certificate validity end time
71/// * `cert_cipher` - TLS cipher suite used
72/// * `cert_domains` - List of domains in the certificate's Subject Alternative Names
73/// * `body` - Response body content
74/// * `headers` - Response headers
75/// * `error` - Any error that occurred during the request
76#[derive(Default, Debug, Clone)]
77pub struct HttpStat {
78    pub is_grpc: bool,
79    pub request_headers: HeaderMap<HeaderValue>,
80    pub dns_lookup: Option<Duration>,
81    pub quic_connect: Option<Duration>,
82    pub tcp_connect: Option<Duration>,
83    pub tls_handshake: Option<Duration>,
84    pub server_processing: Option<Duration>,
85    pub content_transfer: Option<Duration>,
86    pub total: Option<Duration>,
87    pub addr: Option<String>,
88    pub grpc_status: Option<String>,
89    pub status: Option<StatusCode>,
90    pub tls: Option<String>,
91    pub alpn: Option<String>,
92    pub subject: Option<String>,
93    pub issuer: Option<String>,
94    pub cert_not_before: Option<String>,
95    pub cert_not_after: Option<String>,
96    pub cert_cipher: Option<String>,
97    pub cert_domains: Option<Vec<String>>,
98    pub certificates: Option<Vec<Certificate>>,
99    pub body: Option<Bytes>,
100    pub body_size: Option<usize>,
101    pub headers: Option<HeaderMap<HeaderValue>>,
102    pub error: Option<String>,
103    pub silent: bool,
104    pub verbose: bool,
105    pub pretty: bool,
106}
107
108#[derive(Debug, Clone)]
109pub struct Certificate {
110    pub subject: String,
111    pub issuer: String,
112    pub not_before: String,
113    pub not_after: String,
114}
115
116impl HttpStat {
117    pub fn is_success(&self) -> bool {
118        if self.error.is_some() {
119            return false;
120        }
121        if self.is_grpc {
122            if let Some(grpc_status) = &self.grpc_status {
123                return grpc_status == "0";
124            }
125            return false;
126        }
127        let Some(status) = &self.status else {
128            return false;
129        };
130        if status.as_u16() >= 400 {
131            return false;
132        }
133        true
134    }
135}
136
137impl fmt::Display for HttpStat {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        if let Some(addr) = &self.addr {
140            let mut text = format!(
141                "{} {}",
142                LightGreen.paint("Connected to"),
143                LightCyan.paint(addr)
144            );
145            if self.silent {
146                if let Some(status) = &self.status {
147                    let alpn = self.alpn.clone().unwrap_or_else(|| ALPN_HTTP1.to_string());
148                    let status_code = status.as_u16();
149                    let status = if status_code < 400 {
150                        LightGreen.paint(status.to_string())
151                    } else {
152                        LightRed.paint(status.to_string())
153                    };
154                    text = format!(
155                        "{text} --> {} {}",
156                        LightCyan.paint(alpn.to_uppercase()),
157                        status
158                    );
159                } else {
160                    text = format!("{text} --> {}", LightRed.paint("FAIL"));
161                }
162                text = format!("{text} {}", format_duration(self.total.unwrap_or_default()));
163            }
164            writeln!(f, "{text}")?;
165        }
166        if let Some(error) = &self.error {
167            writeln!(f, "Error: {}", LightRed.paint(error))?;
168        }
169        if self.silent {
170            return Ok(());
171        }
172        if self.verbose {
173            for (key, value) in self.request_headers.iter() {
174                writeln!(
175                    f,
176                    "{}: {}",
177                    key.to_string().to_train_case(),
178                    LightCyan.paint(value.to_str().unwrap_or_default())
179                )?;
180            }
181            writeln!(f)?;
182        }
183
184        if let Some(status) = &self.status {
185            let alpn = self.alpn.clone().unwrap_or_else(|| ALPN_HTTP1.to_string());
186            let status_code = status.as_u16();
187            let status = if status_code < 400 {
188                LightGreen.paint(status.to_string())
189            } else {
190                LightRed.paint(status.to_string())
191            };
192            writeln!(f, "{} {}", LightCyan.paint(alpn.to_uppercase()), status)?;
193        }
194        if self.is_grpc {
195            if self.is_success() {
196                writeln!(f, "{}", LightGreen.paint("GRPC OK"))?;
197            }
198            writeln!(f)?;
199        }
200
201        if let Some(tls) = &self.tls {
202            writeln!(f)?;
203            writeln!(f, "Tls: {}", LightCyan.paint(tls))?;
204            writeln!(
205                f,
206                "Cipher: {}",
207                LightCyan.paint(self.cert_cipher.clone().unwrap_or_default())
208            )?;
209            writeln!(
210                f,
211                "Not Before: {}",
212                LightCyan.paint(self.cert_not_before.clone().unwrap_or_default())
213            )?;
214            writeln!(
215                f,
216                "Not After: {}",
217                LightCyan.paint(self.cert_not_after.clone().unwrap_or_default())
218            )?;
219            if self.verbose {
220                writeln!(
221                    f,
222                    "Subject: {}",
223                    LightCyan.paint(self.subject.clone().unwrap_or_default())
224                )?;
225                writeln!(
226                    f,
227                    "Issuer: {}",
228                    LightCyan.paint(self.issuer.clone().unwrap_or_default())
229                )?;
230                writeln!(
231                    f,
232                    "Certificate Domains: {}",
233                    LightCyan.paint(self.cert_domains.clone().unwrap_or_default().join(", "))
234                )?;
235            }
236            writeln!(f)?;
237
238            if self.verbose {
239                if let Some(certificates) = &self.certificates {
240                    writeln!(f, "Certificate Chain")?;
241                    for (index, cert) in certificates.iter().enumerate() {
242                        writeln!(
243                            f,
244                            " {index} Subject: {}",
245                            LightCyan.paint(cert.subject.clone())
246                        )?;
247                        writeln!(f, "   Issuer: {}", LightCyan.paint(cert.issuer.clone()))?;
248                        writeln!(
249                            f,
250                            "   Not Before: {}",
251                            LightCyan.paint(cert.not_before.clone())
252                        )?;
253                        writeln!(
254                            f,
255                            "   Not After: {}",
256                            LightCyan.paint(cert.not_after.clone())
257                        )?;
258                        writeln!(f)?;
259                    }
260                }
261            }
262        }
263
264        let mut is_text = false;
265        let mut is_json = false;
266        if let Some(headers) = &self.headers {
267            for (key, value) in headers.iter() {
268                let value = value.to_str().unwrap_or_default();
269                if key.to_string().to_lowercase() == "content-type" {
270                    if value.contains("text/") || value.contains("application/json") {
271                        is_text = true;
272                    }
273                    if value.contains("application/json") {
274                        is_json = true;
275                    }
276                }
277                writeln!(
278                    f,
279                    "{}: {}",
280                    key.to_string().to_train_case(),
281                    LightCyan.paint(value)
282                )?;
283            }
284            writeln!(f)?;
285        }
286
287        let width = 20;
288
289        let mut timelines = vec![];
290        if let Some(value) = self.dns_lookup {
291            timelines.push(Timeline {
292                name: "DNS Lookup".to_string(),
293                duration: value,
294            });
295        }
296
297        if let Some(value) = self.tcp_connect {
298            timelines.push(Timeline {
299                name: "TCP Connect".to_string(),
300                duration: value,
301            });
302        }
303
304        if let Some(value) = self.tls_handshake {
305            timelines.push(Timeline {
306                name: "TLS Handshake".to_string(),
307                duration: value,
308            });
309        }
310        if let Some(value) = self.quic_connect {
311            timelines.push(Timeline {
312                name: "QUIC Connect".to_string(),
313                duration: value,
314            });
315        }
316
317        if let Some(value) = self.server_processing {
318            timelines.push(Timeline {
319                name: "Server Processing".to_string(),
320                duration: value,
321            });
322        }
323
324        if let Some(value) = self.content_transfer {
325            timelines.push(Timeline {
326                name: "Content Transfer".to_string(),
327                duration: value,
328            });
329        }
330
331        if !timelines.is_empty() {
332            // print name
333            write!(f, " ")?;
334            for (i, timeline) in timelines.iter().enumerate() {
335                write!(
336                    f,
337                    "{}",
338                    timeline.name.unicode_pad(width, Alignment::Center, true)
339                )?;
340                if i < timelines.len() - 1 {
341                    write!(f, " ")?;
342                }
343            }
344            writeln!(f)?;
345
346            // print duration
347            write!(f, "[")?;
348            for (i, timeline) in timelines.iter().enumerate() {
349                write!(
350                    f,
351                    "{}",
352                    LightCyan.paint(
353                        format_duration(timeline.duration)
354                            .unicode_pad(width, Alignment::Center, true)
355                            .to_string(),
356                    )
357                )?;
358                if i < timelines.len() - 1 {
359                    write!(f, "|")?;
360                }
361            }
362            writeln!(f, "]")?;
363        }
364
365        // print | line
366        write!(f, " ")?;
367        for _ in 0..timelines.len() {
368            write!(f, "{}", " ".repeat(width))?;
369            write!(f, "|")?;
370        }
371        writeln!(f)?;
372        write!(f, "{}", " ".repeat(width * timelines.len()))?;
373        write!(
374            f,
375            "Total:{}\n\n",
376            LightCyan.paint(format_duration(self.total.unwrap_or_default()))
377        )?;
378
379        if let Some(body) = &self.body {
380            let status = self.status.unwrap_or(StatusCode::OK).as_u16();
381            let mut body = std::str::from_utf8(body.as_ref())
382                .unwrap_or_default()
383                .to_string();
384            if self.pretty && is_json {
385                if let Ok(json_body) = serde_json::from_str::<serde_json::Value>(&body) {
386                    if let Ok(value) = serde_json::to_string_pretty(&json_body) {
387                        body = value;
388                    }
389                }
390            }
391            if self.verbose || (is_text && body.len() < 1024) {
392                let text = format!(
393                    "Body size: {}",
394                    ByteSize(self.body_size.unwrap_or(0) as u64)
395                );
396                writeln!(f, "{}\n", LightCyan.paint(text))?;
397                if status >= 400 {
398                    writeln!(f, "{}", LightRed.paint(body))?;
399                } else {
400                    writeln!(f, "{body}")?;
401                }
402            } else {
403                let mut save_tips = "".to_string();
404                if let Ok(mut file) = NamedTempFile::new() {
405                    if let Ok(()) = file.write_all(body.as_bytes()) {
406                        save_tips = format!("saved to: {}", file.path().display());
407                        let _ = file.keep();
408                    }
409                }
410                let text = format!(
411                    "Body discarded {}",
412                    ByteSize(self.body_size.unwrap_or(0) as u64)
413                );
414                writeln!(f, "{} {}", LightCyan.paint(text), save_tips)?;
415            }
416        }
417
418        Ok(())
419    }
420}