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 heck::ToTrainCase;
20use http::HeaderMap;
21use http::HeaderValue;
22use http::StatusCode;
23use nu_ansi_term::Color::{LightCyan, LightGreen, LightRed};
24use std::fmt;
25use std::time::Duration;
26use unicode_truncate::Alignment;
27use unicode_truncate::UnicodeTruncateStr;
28
29pub static ALPN_HTTP2: &str = "h2";
30pub static ALPN_HTTP1: &str = "http/1.1";
31
32fn format_duration(duration: Duration) -> String {
33    if duration > Duration::from_secs(1) {
34        return format!("{:.2}s", duration.as_secs_f64());
35    }
36    if duration > Duration::from_millis(1) {
37        return format!("{}ms", duration.as_millis());
38    }
39    format!("{}µs", duration.as_micros())
40}
41
42struct Timeline {
43    name: String,
44    duration: Duration,
45}
46
47#[derive(Default, Debug)]
48pub struct HttpStat {
49    pub dns_lookup: Option<Duration>,
50    pub tcp_connect: Option<Duration>,
51    pub tls_handshake: Option<Duration>,
52    pub server_processing: Option<Duration>,
53    pub content_transfer: Option<Duration>,
54    pub total: Option<Duration>,
55    pub addr: Option<String>,
56    pub status: Option<StatusCode>,
57    pub tls: Option<String>,
58    pub alpn: Option<String>,
59    pub cert_not_before: Option<String>,
60    pub cert_not_after: Option<String>,
61    pub cert_cipher: Option<String>,
62    pub cert_domains: Option<Vec<String>>,
63    pub body: Option<Bytes>,
64    pub headers: Option<HeaderMap<HeaderValue>>,
65    pub error: Option<String>,
66}
67
68impl fmt::Display for HttpStat {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        if let Some(addr) = &self.addr {
71            write!(
72                f,
73                "{} {}\n\n",
74                LightGreen.paint("Connected to"),
75                LightCyan.paint(addr)
76            )?;
77        }
78        if let Some(error) = &self.error {
79            writeln!(f, "Error: {}", LightRed.paint(error))?;
80        }
81        if let Some(status) = &self.status {
82            let alpn = self.alpn.clone().unwrap_or_else(|| ALPN_HTTP1.to_string());
83            let status_code = status.as_u16();
84            let status = if status_code < 400 {
85                LightGreen.paint(status.to_string())
86            } else {
87                LightRed.paint(status.to_string())
88            };
89            writeln!(f, "{} {}", LightCyan.paint(alpn.to_uppercase()), status)?;
90        }
91        if let Some(tls) = &self.tls {
92            writeln!(f, "{}: {}", "tls".to_train_case(), LightCyan.paint(tls))?;
93            writeln!(
94                f,
95                "{}: {}",
96                "cipher".to_train_case(),
97                LightCyan.paint(self.cert_cipher.clone().unwrap_or_default())
98            )?;
99            writeln!(
100                f,
101                "{}: {}",
102                "not before".to_train_case(),
103                LightCyan.paint(self.cert_not_before.clone().unwrap_or_default())
104            )?;
105            writeln!(
106                f,
107                "{}: {}",
108                "not after".to_train_case(),
109                LightCyan.paint(self.cert_not_after.clone().unwrap_or_default())
110            )?;
111            writeln!(f)?;
112        }
113
114        if let Some(headers) = &self.headers {
115            for (key, value) in headers.iter() {
116                writeln!(
117                    f,
118                    "{}: {}",
119                    key.to_string().to_train_case(),
120                    LightCyan.paint(value.to_str().unwrap_or_default())
121                )?;
122            }
123            writeln!(f)?;
124        }
125
126        if let Some(body) = &self.body {
127            let status = self.status.unwrap_or(StatusCode::OK).as_u16();
128            if status >= 400 {
129                let body = std::str::from_utf8(self.body.as_ref().unwrap()).unwrap_or_default();
130                writeln!(f, "Body: {}", LightRed.paint(body))?;
131            } else {
132                let text = format!("Body discarded {} bytes", body.len());
133                writeln!(f, "{}", LightCyan.paint(text))?;
134            }
135        }
136
137        let width = 20;
138
139        let mut timelines = vec![];
140        if let Some(value) = self.dns_lookup {
141            timelines.push(Timeline {
142                name: "DNS Lookup".to_string(),
143                duration: value,
144            });
145        }
146
147        if let Some(value) = self.tcp_connect {
148            timelines.push(Timeline {
149                name: "TCP Connect".to_string(),
150                duration: value,
151            });
152        }
153
154        if let Some(value) = self.tls_handshake {
155            timelines.push(Timeline {
156                name: "TLS Handshake".to_string(),
157                duration: value,
158            });
159        }
160
161        if let Some(value) = self.server_processing {
162            timelines.push(Timeline {
163                name: "Server Processing".to_string(),
164                duration: value,
165            });
166        }
167
168        if let Some(value) = self.content_transfer {
169            timelines.push(Timeline {
170                name: "Content Transfer".to_string(),
171                duration: value,
172            });
173        }
174
175        // print name
176        write!(f, " ")?;
177        for (i, timeline) in timelines.iter().enumerate() {
178            write!(
179                f,
180                "{}",
181                timeline.name.unicode_pad(width, Alignment::Center, true)
182            )?;
183            if i < timelines.len() - 1 {
184                write!(f, " ")?;
185            }
186        }
187        writeln!(f)?;
188
189        // print duration
190        write!(f, "[")?;
191        for (i, timeline) in timelines.iter().enumerate() {
192            write!(
193                f,
194                "{}",
195                LightCyan.paint(
196                    format_duration(timeline.duration)
197                        .unicode_pad(width, Alignment::Center, true)
198                        .to_string(),
199                )
200            )?;
201            if i < timelines.len() - 1 {
202                write!(f, "|")?;
203            }
204        }
205        writeln!(f, "]")?;
206
207        // print | line
208        write!(f, " ")?;
209        for _ in 0..timelines.len() {
210            write!(f, "{}", " ".repeat(width))?;
211            write!(f, "|")?;
212        }
213        writeln!(f)?;
214
215        write!(f, "{}", " ".repeat(width * timelines.len()))?;
216        write!(
217            f,
218            "total:{}",
219            LightCyan.paint(format_duration(self.total.unwrap_or_default()))
220        )?;
221
222        Ok(())
223    }
224}