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