1use 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 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 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 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}