Skip to main content

qlog_dancer/reports/
text.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
27//! Reporting (tables etc.)
28
29use qlog::events::quic::QuicFrame;
30use tabled::settings::location::ByColumnName;
31use tabled::settings::object::Segment;
32use tabled::settings::Alignment;
33use tabled::settings::Disable;
34use tabled::settings::Modify;
35use tabled::settings::Style;
36use tabled::Table;
37
38use crate::config::AppConfig;
39use crate::datastore::Datastore;
40use crate::datastore::PrintStatsConfig;
41use crate::request_stub::*;
42use crate::stringify_last;
43use crate::LogFileData;
44
45pub fn request_timing_table(
46    lf: &LogFileData, config: &AppConfig,
47) -> Option<Table> {
48    let mut table = Table::new(lf.datastore.http_requests.values());
49    table.with(Modify::new(Segment::all()).with(Alignment::right()));
50
51    if config.report_omit_upload {
52        table
53            .with(Disable::column(ByColumnName::new(CLIENT_CONTENT_LENGTH)))
54            .with(Disable::column(ByColumnName::new(CLIENT_TRANSFERRED)))
55            .with(Disable::column(ByColumnName::new(UPLOAD_TIME)))
56            .with(Disable::column(ByColumnName::new(UPLOAD_RATE)));
57    }
58
59    match lf.datastore.vantage_point {
60        crate::datastore::VantagePoint::Client => {
61            table
62                .with(Disable::column(ByColumnName::new(SERVER_RX_HDR_TX_HDR)))
63                .with(Disable::column(ByColumnName::new(
64                    SERVER_TX_HDR_TX_FIRST_HDR,
65                )))
66                .with(Disable::column(ByColumnName::new(
67                    SERVER_TX_HDR_TX_LAST_HDR,
68                )))
69                .with(Disable::column(ByColumnName::new(
70                    SERVER_TX_FIRST_DATA_TX_LAST_DATA,
71                )));
72        },
73
74        crate::datastore::VantagePoint::Server => {
75            // TODO:
76        },
77    }
78
79    if config.report_omit_priorities {
80        table
81            .with(Disable::column(ByColumnName::new(CLIENT_PRI)))
82            .with(Disable::column(ByColumnName::new(SERVER_PRI)));
83    }
84
85    if config.report_text_csv {
86        let style = Style::empty().vertical(',');
87
88        table.with(style);
89    }
90
91    Some(table)
92}
93
94pub fn print_stats(data_store: &Datastore, config: &PrintStatsConfig) {
95    if config.rx_flow_control {
96        print_rx_max_data_frames(data_store);
97        print_rx_max_stream_data_frames(data_store);
98    }
99
100    if config.tx_flow_control {
101        print_tx_max_data_frames(data_store);
102        print_tx_max_stream_data_frames(data_store);
103    }
104
105    if config.reset_streams {
106        print_tx_reset_stream_frames(data_store);
107        print_rx_reset_stream_frames(data_store);
108    }
109
110    if config.tx_stream_frames {
111        print_tx_stream_frames(data_store);
112    }
113
114    if config.stream_buffering {
115        print_local_stream_buffer_reads(data_store);
116        print_local_stream_buffer_writes(data_store);
117        print_local_stream_buffer_dropped(data_store);
118    }
119
120    if config.packet_stats {
121        print_sent_packet_stats(data_store);
122    }
123}
124
125fn print_sent_packet_stats(data_store: &Datastore) {
126    println!("### sent packets ###");
127    for (pkt_space, pkts) in &data_store.packet_sent {
128        println!("\t# packet space={:?}", pkt_space);
129
130        for (pkt_num, pkt_info) in pkts {
131            let (length, payload_length) = match &pkt_info.raw {
132                Some(raw) => (raw.length, raw.payload_length),
133
134                None => (None, None),
135            };
136
137            println!(
138                "\tpkt_num={}, acked=TODO-unknown, length={:?}, payload_length={:?}, ",
139                pkt_num, length, payload_length
140            );
141        }
142        println!();
143    }
144}
145
146fn print_rx_max_data_frames(data_store: &Datastore) {
147    println!("### received MAX_DATA frames ###");
148    if data_store
149        .received_stream_max_data_tracker
150        .per_stream
151        .is_empty()
152    {
153        println!("    None")
154    } else {
155        println!(
156            "    first={:?}, last={:?}",
157            data_store.received_max_data.first().unwrap(),
158            data_store.received_max_data.last().unwrap()
159        );
160    }
161}
162
163fn print_tx_max_data_frames(data_store: &Datastore) {
164    println!("### sent MAX_DATA frames ###");
165    println!(
166        "   total_count={}, first={:?}, last={:?}",
167        data_store.sent_max_data.len(),
168        data_store.sent_max_data.first().unwrap(),
169        data_store.sent_max_data.last().unwrap()
170    );
171}
172
173fn print_rx_max_stream_data_frames(data_store: &Datastore) {
174    println!("### received MAX_STREAM_DATA frames ###");
175    if data_store
176        .received_stream_max_data_tracker
177        .per_stream
178        .is_empty()
179    {
180        println!("    None")
181    } else {
182        for entry in &data_store.received_stream_max_data_tracker.per_stream {
183            println!(
184                "    stream={}, total_count={}, first={:?}, last={}",
185                entry.0,
186                entry.1.len(),
187                entry.1.first(),
188                stringify_last(entry.1)
189            );
190        }
191    }
192}
193
194fn print_tx_max_stream_data_frames(data_store: &Datastore) {
195    println!("### sent MAX_STREAM_DATA frames ###");
196    if data_store
197        .sent_stream_max_data_tracker
198        .per_stream
199        .is_empty()
200    {
201        println!("    None")
202    } else {
203        for entry in &data_store.sent_stream_max_data_tracker.per_stream {
204            println!(
205                "    stream={}, total_count={}, first={:?}, last={:?}",
206                entry.0,
207                entry.1.len(),
208                entry.1.first().unwrap(),
209                entry.1.last().unwrap()
210            );
211        }
212    }
213}
214
215fn print_tx_reset_stream_frames(data_store: &Datastore) {
216    println!("### sent RESET_STREAM frames ###");
217    if data_store.sent_reset_stream.is_empty() {
218        println!("    None")
219    } else {
220        for entry in &data_store.sent_reset_stream {
221            println!(
222                "    stream={}, total_count={}, first={:?}, last={:?}",
223                entry.0,
224                entry.1.len(),
225                entry.1.first(),
226                stringify_last(entry.1)
227            );
228        }
229    }
230}
231
232fn print_rx_reset_stream_frames(data_store: &Datastore) {
233    println!("### received RESET_STREAM frames ###");
234    if data_store.received_reset_stream.is_empty() {
235        println!("    None")
236    } else {
237        for entry in &data_store.received_reset_stream {
238            println!(
239                "    stream={}, total_count={}, first={:?}, last={:?}",
240                entry.0,
241                entry.1.len(),
242                entry.1.first(),
243                stringify_last(entry.1)
244            );
245        }
246    }
247}
248
249fn print_local_stream_buffer_reads(data_store: &Datastore) {
250    println!("### local stream buffer reads ###");
251    if data_store.stream_buffer_reads_tracker.per_stream.is_empty() {
252        println!("    None")
253    } else {
254        for entry in &data_store.stream_buffer_reads_tracker.per_stream {
255            println!(
256                "    stream={}, total_count={}, first=(offset={}, length={}), last=(offset={}, length={}), total_length={}",
257                entry.0,
258                entry.1.len(),
259                entry.1.first().unwrap().1.offset,
260                entry.1.first().unwrap().1.length,
261                entry.1.last().unwrap().1.offset,
262                entry.1.last().unwrap().1.length,
263                entry.1.last().unwrap().1.offset + entry.1.last().unwrap().1.length,
264            );
265        }
266    }
267}
268
269fn print_local_stream_buffer_writes(data_store: &Datastore) {
270    println!("### local stream buffer writes ###");
271    if data_store
272        .stream_buffer_writes_tracker
273        .per_stream
274        .is_empty()
275    {
276        println!("    None")
277    } else {
278        for entry in &data_store.stream_buffer_writes_tracker.per_stream {
279            println!(
280                "    stream={}, total_count={}, first=(offset={}, length={}), last=(offset={}, length={}), total_length={}",
281                entry.0,
282                entry.1.len(),
283                entry.1.first().unwrap().1.offset,
284                entry.1.first().unwrap().1.length,
285                entry.1.last().unwrap().1.offset,
286                entry.1.last().unwrap().1.length,
287                entry.1.last().unwrap().1.offset + entry.1.last().unwrap().1.length,
288            );
289        }
290    }
291}
292
293fn print_local_stream_buffer_dropped(data_store: &Datastore) {
294    println!("### local stream buffer dropped ###");
295    if data_store
296        .stream_buffer_dropped_tracker
297        .per_stream
298        .is_empty()
299    {
300        println!("    None")
301    } else {
302        for entry in &data_store.stream_buffer_dropped_tracker.per_stream {
303            println!(
304                "    stream={}, total_count={}, first=(offset={}, length={}), last=(offset={}, length={}), total_length={}",
305                entry.0,
306                entry.1.len(),
307                entry.1.first().unwrap().1.offset,
308                entry.1.first().unwrap().1.length,
309                entry.1.last().unwrap().1.offset,
310                entry.1.last().unwrap().1.length,
311                entry.1.last().unwrap().1.offset + entry.1.last().unwrap().1.length,
312            );
313        }
314    }
315}
316
317fn print_tx_stream_frames(data_store: &Datastore) {
318    println!("### sent STREAM frames ###");
319    if data_store.sent_stream_frames.is_empty() {
320        println!("    None")
321    } else {
322        for entry in &data_store.sent_stream_frames {
323            let total = match entry.1.last() {
324                Some((_, QuicFrame::Stream { offset, raw, .. })) => {
325                    let offset = offset.unwrap_or_default();
326                    let length = raw
327                        .clone()
328                        .unwrap_or_default()
329                        .payload_length
330                        .unwrap_or_default();
331                    format!("{}", offset + length)
332                },
333
334                _ => "n/a".to_string(),
335            };
336
337            println!(
338                "    stream={}, total_count={}, first={:?}, last={:?}, total_length={}",
339                entry.0,
340                entry.1.len(),
341                entry.1.first(),
342                stringify_last(entry.1),
343                total
344            );
345        }
346    }
347}
348
349pub fn print_flow_control(data: &[LogFileData]) {
350    // TODO make this a proper table
351    println!("================");
352    println!("flow control stuff");
353    println!("================");
354    for lf in data {
355        println!(
356            "Session={}, host={}",
357            lf.datastore.session_id.unwrap_or(-1),
358            lf.datastore
359                .host
360                .clone()
361                .unwrap_or("ERROR UNKNOWN".to_string())
362        );
363
364        println!("  Initial Client connection window, Initial Client Bidi Local Stream Window");
365        println!(
366            "  {},{}",
367            lf.datastore.client_quic_tps.initial_max_data.unwrap_or(0),
368            lf.datastore
369                .client_quic_tps
370                .initial_max_stream_data_bidi_local
371                .unwrap_or(0)
372        );
373
374        for (stream_id, points) in
375            &lf.datastore.netlog_quic_client_side_window_updates
376        {
377            println!("  Stream {} flow control updates", stream_id);
378            println!("    Time, Value");
379
380            for (time, val) in points {
381                println!("    {},{}", time, val);
382            }
383        }
384    }
385}
386
387pub fn print_packet_loss(data: &[LogFileData]) {
388    // TODO make this a proper table
389    println!("================");
390    println!("QUIC packet loss");
391    println!("================");
392    for lf in data {
393        let is_received_some = lf
394            .datastore
395            .netlog_ack_sent_missing_packet
396            .contains_key(&crate::PacketType::Handshake) ||
397            lf.datastore
398                .netlog_ack_sent_missing_packet
399                .contains_key(&crate::PacketType::Initial) ||
400            lf.datastore
401                .netlog_ack_sent_missing_packet
402                .contains_key(&crate::PacketType::OneRtt) ||
403            lf.datastore
404                .netlog_ack_sent_missing_packet
405                .contains_key(&crate::PacketType::ZeroRtt) ||
406            lf.datastore
407                .netlog_ack_sent_missing_packet
408                .contains_key(&crate::PacketType::Retry) ||
409            lf.datastore
410                .netlog_ack_sent_missing_packet
411                .contains_key(&crate::PacketType::VersionNegotiation);
412
413        if !lf.datastore.netlog_ack_received_missing_packet.is_empty() ||
414            is_received_some
415        {
416            println!(
417                "Session={}, host={}",
418                lf.datastore.session_id.unwrap_or(-1),
419                lf.datastore
420                    .host
421                    .clone()
422                    .unwrap_or("ERROR UNKNOWN".to_string())
423            );
424        }
425
426        if !lf.datastore.netlog_ack_received_missing_packet.is_empty() {
427            println!("  Packets sent and lost");
428            println!("    Packet Type, Packet tx count, Packet lost count, Packet loss %, Packets lost");
429
430            for (pkt_type, pkt_nums) in
431                &lf.datastore.netlog_ack_received_missing_packet
432            {
433                if let Some(pkts) = lf.datastore.packet_received.get(pkt_type) {
434                    let total_pkts_tx = pkts.len();
435                    let pkt_loss = pkt_nums.len() as f64 / total_pkts_tx as f64;
436
437                    if !pkt_nums.is_empty() {
438                        println!(
439                            "    {:?}, {}, {}, {:.2}, {:?}",
440                            pkt_type,
441                            total_pkts_tx,
442                            pkt_nums.len(),
443                            pkt_loss * 100f64,
444                            pkt_nums
445                        );
446                    }
447                }
448            }
449
450            println!();
451        }
452
453        if is_received_some {
454            println!("  Packet lost and not received");
455            println!("    Packet Type, Packet rx count, Packet lost count, Packet loss %, Packets lost");
456
457            for (pkt_type, pkt_nums) in
458                &lf.datastore.netlog_ack_sent_missing_packet
459            {
460                if let Some(pkts) = lf.datastore.packet_received.get(pkt_type) {
461                    let total_pkts_tx = pkts.len();
462                    let pkt_loss = pkt_nums.len() as f64 / total_pkts_tx as f64;
463
464                    if !pkt_nums.is_empty() {
465                        println!(
466                            "    {:?}, {}, {}, {:.2}, {:?}",
467                            pkt_type,
468                            total_pkts_tx,
469                            pkt_nums.len(),
470                            pkt_loss * 100f64,
471                            pkt_nums
472                        );
473                    }
474                }
475            }
476
477            println!();
478        }
479    }
480
481    println!();
482}