Skip to main content

qlog_dancer/
request_stub.rs

1// Copyright (C) 2025, Cloudflare, Inc.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8//     * Redistributions of source code must retain the above copyright notice,
9//       this list of conditions and the following disclaimer.
10//
11//     * Redistributions in binary form must reproduce the above copyright
12//       notice, this list of conditions and the following disclaimer in the
13//       documentation and/or other materials provided with the distribution.
14//
15// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
16// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
19// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27use qlog::events::http3::HttpHeader;
28use std::borrow::Cow;
29use std::fmt::Display;
30
31use crate::datastore::H2StreamReset;
32use crate::datastore::QuicStreamReset;
33use crate::datastore::QuicStreamStopSending;
34use crate::datastore::RequestActor;
35use crate::datastore::RequestAtClientDeltas;
36use crate::datastore::RequestAtServerDeltas;
37use netlog::http::headers_to_map;
38
39pub const CLIENT_CONTENT_LENGTH: &str = "Request Content-Length";
40pub const CLIENT_TRANSFERRED: &str = "Request Transferred";
41pub const UPLOAD_TIME: &str = "Upload duration (ms)";
42pub const UPLOAD_RATE: &str = "Upload rate (Mbps)";
43pub const SERVER_RX_HDR_TX_HDR: &str = "Server Rx Hdr, Tx Hdr";
44pub const SERVER_TX_HDR_TX_FIRST_HDR: &str = "Server Tx Hdr, Tx First Hdr";
45pub const SERVER_TX_HDR_TX_LAST_HDR: &str = "Server Tx Hdr, Tx Last Hdr";
46pub const SERVER_TX_FIRST_DATA_TX_LAST_DATA: &str =
47    "Server Tx First Data, Tx Last Data (Download time)";
48
49pub const CLIENT_PRI: &str = "Client Priority Header";
50pub const SERVER_PRI: &str = "Server Priority Header";
51
52const MAX_PATH_LENGTH: usize = 80;
53
54#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
55pub struct NaOption<T> {
56    pub inner: Option<T>,
57}
58
59impl<T> NaOption<T> {
60    pub fn new(value: Option<T>) -> Self {
61        Self { inner: value }
62    }
63}
64
65impl<T: std::fmt::Display> Display for NaOption<T> {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self.inner.as_ref() {
68            Some(v) => write!(f, "{}", v),
69            None => write!(f, "n/a"),
70        }
71    }
72}
73
74#[derive(Debug, Default)]
75pub struct HttpRequestStub {
76    pub request_actor: RequestActor,
77    pub stream_id: u64,
78    pub host: NaOption<String>,
79    pub method: NaOption<String>,
80    pub path: NaOption<String>,
81    pub status: NaOption<String>,
82    pub client_content_length: NaOption<String>,
83    pub server_content_length: NaOption<String>,
84    pub client_pri_hdr: NaOption<String>,
85    pub server_pri_hdr: NaOption<String>,
86    pub priority_updates: Vec<String>,
87
88    pub time_discovery: Option<f64>,
89    pub time_first_headers_rx: Option<f64>,
90    pub time_first_headers_tx: Option<f64>,
91
92    pub time_first_data_rx: Option<f64>,
93    pub time_first_data_tx: Option<f64>,
94
95    pub time_last_data_rx: Option<f64>,
96    pub time_last_data_tx: Option<f64>,
97
98    pub time_fin_rx: Option<f64>,
99
100    // TODO, Option<u64>
101    pub time_data_rx_set: Vec<(f64, u64)>,
102    pub time_data_tx_set: Vec<(f64, u64)>,
103
104    pub client_transferred_bytes: NaOption<u64>,
105    pub server_transferred_bytes: NaOption<u64>,
106
107    pub avg_upload_rate: NaOption<f64>,
108    pub avg_download_rate_d2d: NaOption<f64>,
109    pub avg_download_rate_h2d: NaOption<f64>,
110
111    pub at_client_deltas: Option<RequestAtClientDeltas>,
112    pub at_server_deltas: Option<RequestAtServerDeltas>,
113
114    pub quic_stream_stop_sending_sent: Option<QuicStreamStopSending>,
115    pub quic_stream_stop_sending_received: Option<QuicStreamStopSending>,
116
117    pub quic_stream_reset_sent: Option<QuicStreamReset>,
118    pub quic_stream_reset_received: Option<QuicStreamReset>,
119
120    pub h2_stream_reset_sent: Option<H2StreamReset>,
121    pub h2_stream_reset_receive: Option<H2StreamReset>,
122}
123
124struct ClientDeltaStrings {
125    pub discovery_tx_hdr: String,
126    pub tx_hdr_rx_hdr: String,
127    pub tx_hdr_rx_first_data: String,
128    pub tx_hdr_rx_last_data: String,
129    pub download_time_d2d: String,
130    pub download_time_h2d: String,
131    pub upload_time: String,
132}
133
134impl Default for ClientDeltaStrings {
135    fn default() -> Self {
136        let na = NaOption::<u8>::new(None);
137
138        Self {
139            discovery_tx_hdr: na.to_string(),
140            tx_hdr_rx_hdr: na.to_string(),
141            tx_hdr_rx_first_data: na.to_string(),
142            tx_hdr_rx_last_data: na.to_string(),
143            download_time_d2d: na.to_string(),
144            download_time_h2d: na.to_string(),
145            upload_time: na.to_string(),
146        }
147    }
148}
149
150impl HttpRequestStub {
151    fn client_deltas(&self) -> ClientDeltaStrings {
152        match self.at_client_deltas {
153            Some(d) => ClientDeltaStrings {
154                discovery_tx_hdr: d.discover_tx_hdr.to_string(),
155                tx_hdr_rx_hdr: d.tx_hdr_rx_hdr.to_string(),
156                tx_hdr_rx_first_data: d.tx_hdr_rx_first_data.to_string(),
157                tx_hdr_rx_last_data: d.tx_hdr_rx_last_data.to_string(),
158                download_time_d2d: d.rx_first_data_rx_last_data.to_string(),
159                download_time_h2d: d.tx_hdr_rx_last_data.to_string(),
160                upload_time: d.tx_first_data_tx_last_data.to_string(),
161            },
162
163            None => ClientDeltaStrings::default(),
164        }
165    }
166
167    fn server_deltas(&self) -> (String, String, String, String) {
168        match self.at_server_deltas {
169            Some(d) => (
170                d.rx_hdr_tx_hdr.to_string(),
171                d.rx_hdr_tx_first_data.to_string(),
172                d.rx_hdr_tx_last_data.to_string(),
173                d.tx_first_data_tx_last_data.to_string(),
174            ),
175
176            None => {
177                let na = NaOption::<u8>::new(None);
178                (
179                    na.to_string(),
180                    na.to_string(),
181                    na.to_string(),
182                    na.to_string(),
183                )
184            },
185        }
186    }
187}
188
189impl tabled::Tabled for HttpRequestStub {
190    const LENGTH: usize = 28;
191
192    fn fields(&self) -> Vec<Cow<'_, str>> {
193        // truncate long paths for
194        let mut path = self.path.to_string();
195        if path.len() > MAX_PATH_LENGTH {
196            path.truncate(MAX_PATH_LENGTH);
197            path.push_str("<snip>");
198        }
199
200        let req_start = match self.request_actor {
201            RequestActor::Client => self.time_first_headers_tx,
202            RequestActor::Server => self.time_first_headers_rx,
203        };
204        let req_start = NaOption::new(req_start);
205
206        // This one is tricky, we might never receive anything, so do a few
207        // checks.
208        let req_end = match self.request_actor {
209            RequestActor::Client =>
210                if self.time_last_data_rx.is_some() {
211                    self.time_last_data_rx
212                } else {
213                    self.time_first_headers_rx
214                },
215            RequestActor::Server =>
216                if self.time_last_data_tx.is_some() {
217                    self.time_first_data_tx
218                } else {
219                    self.time_first_headers_tx
220                },
221        };
222
223        let req_end = NaOption::new(req_end);
224
225        let client_deltas = self.client_deltas();
226
227        let (
228            server_rx_hdr_tx_hdr,
229            server_rx_hdr_tx_first_data,
230            server_rx_hdr_tx_last_data,
231            server_tx_first_data_tx_last_data,
232        ) = self.server_deltas();
233
234        let rst_stream_sent =
235            match (&self.quic_stream_reset_sent, &self.h2_stream_reset_sent) {
236                (Some(v), None) => v.to_string(),
237                (None, Some(v)) => v.to_string(),
238                _ => NaOption::<u8>::new(None).to_string(),
239            };
240
241        let rst_stream_received = match (
242            &self.quic_stream_reset_received,
243            &self.h2_stream_reset_receive,
244        ) {
245            (Some(v), None) => v.to_string(),
246            (None, Some(v)) => v.to_string(),
247            _ => NaOption::<u8>::new(None).to_string(),
248        };
249
250        let stop_sending_sent = match &self.quic_stream_stop_sending_sent {
251            Some(v) => v.to_string(),
252            None => NaOption::<u8>::new(None).to_string(),
253        };
254
255        vec![
256            self.stream_id.to_string().into(),
257            self.method.to_string().into(),
258            self.host.to_string().into(),
259            path.into(),
260            self.status.to_string().into(),
261            self.server_content_length.to_string().into(),
262            self.server_transferred_bytes.to_string().into(),
263            NaOption::new(self.time_discovery).to_string().into(),
264            req_start.to_string().into(),
265            req_end.to_string().into(),
266            client_deltas.discovery_tx_hdr.into(),
267            client_deltas.download_time_h2d.into(),
268            self.avg_download_rate_h2d.to_string().into(),
269            client_deltas.download_time_d2d.into(),
270            self.avg_download_rate_d2d.to_string().into(),
271            client_deltas.tx_hdr_rx_hdr.into(),
272            client_deltas.tx_hdr_rx_first_data.into(),
273            client_deltas.tx_hdr_rx_last_data.into(),
274            self.client_content_length.to_string().into(),
275            self.client_transferred_bytes.to_string().into(),
276            client_deltas.upload_time.into(),
277            self.avg_upload_rate.to_string().into(),
278            server_rx_hdr_tx_hdr.into(),
279            server_rx_hdr_tx_first_data.into(),
280            server_rx_hdr_tx_last_data.into(),
281            server_tx_first_data_tx_last_data.into(),
282            self.client_pri_hdr.to_string().into(),
283            self.server_pri_hdr.to_string().into(),
284            rst_stream_sent.into(),
285            rst_stream_received.into(),
286            stop_sending_sent.into(),
287        ]
288    }
289
290    fn headers() -> Vec<Cow<'static, str>> {
291        vec![
292            "ID".into(),
293            "Method".into(),
294            "Host".into(),
295            "Path".into(),
296            "Status".into(),
297            "Response Content-Length (bytes)".into(),
298            "Response Transferred (bytes)".into(),
299            "Request Discovered Time".into(),
300            "Request Start Time".into(),
301            "Request End Time".into(),
302            "Stalled duration (ms)".into(),
303            "Download duration (h2d) (ms)".into(),
304            "Download rate (h2d) (Mbps)".into(),
305            "Download duration (d2d) (ms)".into(),
306            "Download rate (d2d) (Mbps)".into(),
307            "Client Tx Hdr, Rx Hdr".into(),
308            "Client Tx Hdr, Rx First Data".into(),
309            "Client Tx Hdr, Rx Last Data".into(),
310            CLIENT_CONTENT_LENGTH.into(),
311            CLIENT_TRANSFERRED.into(),
312            UPLOAD_TIME.into(),
313            UPLOAD_RATE.into(),
314            SERVER_RX_HDR_TX_HDR.into(),
315            SERVER_TX_HDR_TX_FIRST_HDR.into(),
316            SERVER_TX_HDR_TX_LAST_HDR.into(),
317            SERVER_TX_FIRST_DATA_TX_LAST_DATA.into(),
318            CLIENT_PRI.into(),
319            SERVER_PRI.into(),
320            "Reset Stream Sent".into(),
321            "Reset Stream Received".into(),
322            "Stop Sending Sent".into(),
323        ]
324    }
325}
326
327pub fn find_header_value(hdrs: &[HttpHeader], name: &str) -> Option<String> {
328    hdrs.iter()
329        .find(|&h| h.name.as_deref() == Some(name))
330        .and_then(|h| h.value.clone())
331}
332
333impl HttpRequestStub {
334    pub fn set_request_info_from_netlog(&mut self, hdrs: &[String]) {
335        // TODO: case-sensitivity for HTTP/1.1
336        let headers = headers_to_map(hdrs);
337
338        self.method = NaOption::new(headers.get(":method").cloned());
339        self.host = NaOption::new(headers.get(":authority").cloned());
340        self.path = NaOption::new(headers.get(":path").cloned());
341        self.client_pri_hdr = NaOption::new(headers.get("priority").cloned());
342        self.client_content_length =
343            NaOption::new(headers.get("content-length").cloned());
344    }
345
346    pub fn set_response_info_from_netlog(&mut self, hdrs: &[String]) {
347        // TODO: case-sensitivity for HTTP/1.1
348        let headers = headers_to_map(hdrs);
349
350        self.status = NaOption::new(headers.get(":status").cloned());
351        self.server_pri_hdr = NaOption::new(headers.get("priority").cloned());
352        self.server_content_length =
353            NaOption::new(headers.get("content-length").cloned());
354    }
355
356    pub fn set_request_info_from_qlog(&mut self, hdrs: &[HttpHeader]) {
357        self.method = NaOption::new(find_header_value(hdrs, ":method"));
358        self.host = NaOption::new(find_header_value(hdrs, ":authority"));
359        self.path = NaOption::new(find_header_value(hdrs, ":path"));
360        self.client_pri_hdr = NaOption::new(find_header_value(hdrs, "priority"));
361        self.client_content_length =
362            NaOption::new(find_header_value(hdrs, "content-lengt"));
363    }
364
365    pub fn set_response_info_from_qlog(&mut self, hdrs: &[HttpHeader]) {
366        self.status = NaOption::new(find_header_value(hdrs, ":status"));
367        self.server_pri_hdr = NaOption::new(find_header_value(hdrs, "priority"));
368        self.server_content_length =
369            NaOption::new(find_header_value(hdrs, "content-length"));
370    }
371
372    // input times in milliseconds
373    fn maybe_megabits_per_second(
374        start: Option<f64>, end: Option<f64>, bytes: Option<u64>,
375    ) -> NaOption<f64> {
376        match (end, start, bytes) {
377            (Some(end), Some(start), Some(bytes)) => {
378                let mut total_time = end - start;
379                // there might be only one frame, or it was sent super quick,
380                // clamp to 1ms
381                if total_time == 0.0 {
382                    total_time = 1.0
383                };
384
385                // convert to seconds
386                total_time /= 1000.0;
387
388                let megabits = (bytes * 8) as f64 / 1000000.0;
389                let avg_rate = megabits / total_time;
390
391                NaOption::new(Some(avg_rate))
392            },
393
394            _ => NaOption::<f64>::new(None),
395        }
396    }
397
398    pub fn calculate_upload_download_rate(&mut self) {
399        match self.request_actor {
400            RequestActor::Client => {
401                self.avg_upload_rate = Self::maybe_megabits_per_second(
402                    self.time_first_data_tx,
403                    self.time_last_data_tx,
404                    self.client_transferred_bytes.inner,
405                );
406
407                self.avg_download_rate_d2d = Self::maybe_megabits_per_second(
408                    self.time_first_data_rx,
409                    self.time_last_data_rx,
410                    self.server_transferred_bytes.inner,
411                );
412
413                self.avg_download_rate_h2d = Self::maybe_megabits_per_second(
414                    self.time_first_headers_tx,
415                    self.time_last_data_rx,
416                    self.server_transferred_bytes.inner,
417                );
418            },
419
420            RequestActor::Server => {
421                self.avg_upload_rate = Self::maybe_megabits_per_second(
422                    self.time_first_data_rx,
423                    self.time_last_data_rx,
424                    self.client_transferred_bytes.inner,
425                );
426
427                self.avg_download_rate_d2d = Self::maybe_megabits_per_second(
428                    self.time_first_data_tx,
429                    self.time_last_data_tx,
430                    self.server_transferred_bytes.inner,
431                );
432            },
433        }
434    }
435
436    fn maybe_time_delta(start: Option<f64>, end: Option<f64>) -> NaOption<f64> {
437        match (start, end) {
438            (Some(start), Some(end)) => NaOption::new(Some(end - start)),
439
440            _ => NaOption::<f64>::new(None),
441        }
442    }
443
444    pub fn calculate_deltas(&mut self) {
445        match self.request_actor {
446            RequestActor::Client => {
447                let discover_tx_hdr = Self::maybe_time_delta(
448                    self.time_discovery,
449                    self.time_first_headers_tx,
450                );
451
452                let tx_hdr_rx_hdr = Self::maybe_time_delta(
453                    self.time_first_headers_tx,
454                    self.time_first_headers_rx,
455                );
456
457                let tx_hdr_rx_first_data = Self::maybe_time_delta(
458                    self.time_first_headers_tx,
459                    self.time_first_data_rx,
460                );
461
462                let tx_hdr_rx_last_data = Self::maybe_time_delta(
463                    self.time_first_headers_tx,
464                    self.time_last_data_rx,
465                );
466
467                let tx_first_data_tx_last_data = Self::maybe_time_delta(
468                    self.time_first_data_tx,
469                    self.time_last_data_tx,
470                );
471
472                let rx_first_data_rx_last_data = Self::maybe_time_delta(
473                    self.time_first_data_rx,
474                    self.time_last_data_rx,
475                );
476
477                let rx_hdr_rx_last_data = Self::maybe_time_delta(
478                    self.time_first_headers_rx,
479                    self.time_last_data_rx,
480                );
481
482                self.at_client_deltas = Some(RequestAtClientDeltas {
483                    discover_tx_hdr,
484                    tx_hdr_rx_hdr,
485                    tx_hdr_rx_first_data,
486                    tx_hdr_rx_last_data,
487                    tx_first_data_tx_last_data,
488                    rx_first_data_rx_last_data,
489                    rx_hdr_rx_last_data,
490                });
491            },
492
493            RequestActor::Server => {
494                let rx_hdr_tx_hdr = match (
495                    self.time_first_headers_tx,
496                    self.time_first_headers_rx,
497                ) {
498                    (Some(end), Some(start)) => NaOption::new(Some(end - start)),
499
500                    _ => NaOption::<f64>::new(None),
501                };
502
503                let rx_hdr_tx_first_data =
504                    match (self.time_first_data_tx, self.time_first_headers_rx) {
505                        (Some(end), Some(start)) =>
506                            NaOption::new(Some(end - start)),
507
508                        _ => NaOption::<f64>::new(None),
509                    };
510
511                let rx_hdr_tx_last_data =
512                    match (self.time_last_data_tx, self.time_first_headers_rx) {
513                        (Some(end), Some(start)) =>
514                            NaOption::new(Some(end - start)),
515
516                        _ => NaOption::<f64>::new(None),
517                    };
518
519                let tx_first_data_tx_last_data =
520                    match (self.time_last_data_tx, self.time_first_data_tx) {
521                        (Some(end), Some(start)) =>
522                            NaOption::new(Some(end - start)),
523
524                        _ => NaOption::<f64>::new(None),
525                    };
526
527                self.at_server_deltas = Some(RequestAtServerDeltas {
528                    rx_hdr_tx_hdr,
529                    rx_hdr_tx_first_data,
530                    rx_hdr_tx_last_data,
531                    tx_first_data_tx_last_data,
532                });
533            },
534        }
535    }
536}