Skip to main content

qlog_dancer/
config.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 std::collections::HashSet;
28
29use clap::Arg;
30use clap::ArgAction;
31use clap::Command;
32
33use crate::datastore::PrintStatsConfig;
34use crate::plots::colors::PlotColors;
35use crate::plots::colors::DARK_MODE;
36use crate::plots::colors::LIGHT_MODE;
37use crate::plots::stream_sparks::SparkPlotsParams;
38use crate::plots::ClampParams;
39use crate::SerializationFormat;
40
41#[derive(Debug)]
42pub struct AppConfig {
43    pub file: String,
44    pub filename: String,
45
46    pub charts_dir: String,
47    pub plot_conn_overview: bool,
48    pub plot_pkt_sent: bool,
49    pub plot_pkt_received: bool,
50    pub plot_conn_flow_control: bool,
51    pub plot_sparks: bool,
52    pub plot_multiplex: bool,
53    pub plot_pending: bool,
54    pub sparks_layout: SparkPlotsParams,
55
56    pub report_text: bool,
57    pub report_omit_upload: bool,
58    pub report_omit_priorities: bool,
59    pub report_text_csv: bool,
60    pub report_html: bool,
61
62    pub dark_mode: bool,
63
64    pub start: Option<f64>,
65    pub end: Option<f64>,
66
67    pub stream_y_max: Option<u64>,
68    pub cwnd_y_max: Option<u64>,
69
70    pub netlog_filter: HashSet<String>,
71    pub qlog_wirefilter: Option<String>,
72
73    pub stats_config: PrintStatsConfig,
74    pub ignore_acks: bool,
75
76    pub log_format: SerializationFormat,
77}
78
79impl AppConfig {
80    pub fn colors(dark_mode: bool) -> PlotColors {
81        if dark_mode {
82            DARK_MODE
83        } else {
84            LIGHT_MODE
85        }
86    }
87
88    pub fn from_clap() -> std::result::Result<Self, String> {
89        let mut matches = Command::new("qlog-dancer")
90        .version("v0.1.0")
91        .about("dances with qlog (and more!)")
92        .arg(
93            Arg::new("LOG FILE")
94                .help("Sets the input log file to use")
95                .required(true)
96                .index(1),
97        )
98        .arg(
99            Arg::new("charts")
100                .long("charts")
101                .help("Type of charts to plot")
102                .value_parser([
103                    "all",
104                    "overview",
105                    "pkt-rx",
106                    "sparks",
107                    "multiplex",
108                    "pending",
109                    "conn-flow",
110                    "none",
111                ])
112                .default_value("none"),
113        )
114        .arg(
115            Arg::new("charts_directory")
116                .long("charts-directory")
117                .help("Sets the output directory for charts"),
118        )
119        .arg(
120            Arg::new("sparks_layout")
121                .long("sparks-layout")
122                .help("Layout of spark charts")
123                .value_parser(["grid", "vert"])
124                .default_value("vert"),
125        )
126        .arg(
127            Arg::new("start")
128                .short('s')
129                .help("Relative start time for plots")
130        )
131        .arg(
132            Arg::new("end")
133                .short('e')
134                .help("Relative end time for plots")
135        )
136        .arg(
137            Arg::new("stream_y_axis_max")
138                .long("stream-y-axis_max")
139                .help("Maximum value of Y axis on stream-related charts")
140        )
141        .arg(
142            Arg::new("cwnd_y_axis_max")
143                .long("cwnd-y-axis-max")
144                .help("Maximum value of Y axis on cwnd-related charts")
145        )
146        .arg(
147            Arg::new("netlog_filter")
148                .long("netlog-filter")
149                .help("A comma-seperated list of hostnames to filter in to netlog analysis. By default, all hostname are analysed.")
150        )
151        .arg(
152            Arg::new("qlog_wirefilter")
153                .long("qlog-wirefilter")
154                .help("A Wirefilter expression for Wireshark-like matching of qlog events.")
155        )
156        .arg(
157            Arg::new("dark_mode")
158                .long("dark_mode")
159                .help("Generate outputs in a dark mode style")
160                .action(ArgAction::SetTrue),
161        )
162        .arg(
163            Arg::new("print_stats")
164                .long("print-stats")
165                .help("Print stats about qlog events")
166                .value_parser(["all", "stream", "packets", "none"])
167                .default_value("none"),
168        )
169        .arg(
170            Arg::new("report_text")
171                .long("report-text")
172                .help("Generate log reports in text format")
173                .action(ArgAction::SetTrue),
174        )
175        .arg(
176            Arg::new("report_html")
177                .long("report-html")
178                .help("Generate log reports in HTML format")
179                .action(ArgAction::SetTrue),
180        )
181        .arg(
182            Arg::new("report_omit_upload")
183                .long("report-omit-upload")
184                .help("Omit printing columns about HTTP uploads")
185                .action(ArgAction::SetTrue),
186        )
187        .arg(
188            Arg::new("report_omit_priorities")
189                .long("report-omit-priorities")
190                .help("Omit printing columns about HTTP priorities")
191                .action(ArgAction::SetTrue),
192        )
193        .arg(
194            Arg::new("report_text_csv")
195                .long("report-text-csv")
196                .help("Tables printed in a CSV-compatible format")
197                .action(ArgAction::SetTrue),
198        )
199        .get_matches();
200
201        let file = matches.remove_one::<String>("LOG FILE").unwrap();
202        let filename = std::path::Path::new(&file)
203            .file_name()
204            .unwrap()
205            .to_str()
206            .unwrap();
207
208        let charts_dir = matches
209            .remove_one::<String>("charts_directory")
210            .unwrap_or(format!("{filename}-charts"));
211
212        let start = matches.remove_one::<f64>("start");
213        let end = matches.remove_one::<f64>("end");
214
215        if end.is_some() && end < start {
216            return Err("End time cannot be earlier than start time.".into());
217        }
218
219        if let Some(start) = start {
220            if start < 0.0 {
221                return Err("Start time cannot be less than 0.0.".into());
222            }
223        }
224
225        if let Some(end) = end {
226            if end < 0.0 {
227                return Err("End time cannot be less than 0.0.".into());
228            }
229        }
230
231        let stream_y_max =
232            matches.remove_one::<i64>("stream_y_axis_max").unwrap_or(-1);
233        let stream_y_max = if stream_y_max == -1 {
234            None
235        } else {
236            Some(stream_y_max as u64)
237        };
238
239        let cwnd_y_max =
240            matches.remove_one::<i64>("cwnd_y_axis_max").unwrap_or(-1);
241        let cwnd_y_max = if cwnd_y_max == -1 {
242            None
243        } else {
244            Some(cwnd_y_max as u64)
245        };
246
247        let dark_mode = matches.get_flag("dark_mode");
248
249        let charts = matches.remove_one::<String>("charts").unwrap();
250        let (
251            plot_conn_overview,
252            plot_pkt_sent,
253            plot_pkt_received,
254            plot_sparks,
255            plot_multiplex,
256            plot_pending,
257            plot_conn_flow_control,
258        ) = match charts.as_str() {
259            "all" => (true, true, true, true, true, true, true),
260
261            "overview" => (true, true, false, false, false, false, false),
262
263            "pkt-rx" => (false, false, true, false, false, false, false),
264
265            "sparks" => (false, false, false, true, false, false, false),
266
267            "multiplex" => (false, false, false, false, true, false, false),
268
269            "pending" => (false, false, false, false, false, true, false),
270
271            "conn-flow" => (false, false, false, false, false, false, true),
272
273            "none" => (false, false, false, false, false, false, false),
274
275            _ => unreachable!(),
276        };
277
278        let sparks_layout =
279            matches.remove_one::<String>("sparks_layout").unwrap();
280        let sparks_layout = if sparks_layout == "grid" {
281            SparkPlotsParams {
282                clamp: ClampParams {
283                    start,
284                    end,
285                    stream_y_max,
286                },
287                colors: Self::colors(dark_mode),
288                ..Default::default()
289            }
290        } else {
291            SparkPlotsParams {
292                clamp: ClampParams {
293                    start,
294                    end,
295                    stream_y_max,
296                },
297                colors: Self::colors(dark_mode),
298                sparks_per_row: 1,
299                captions_on_top: false,
300                spark_dimension_x: 600,
301                caption_area_width: 600,
302                label_area_height: 1,
303                ..Default::default()
304            }
305        };
306
307        let netlog_filter = match matches.remove_one::<String>("netlog_filter") {
308            Some(filter_string) => filter_string
309                .split(',')
310                .map(|v| v.to_string())
311                .collect::<HashSet<String>>(),
312
313            None => HashSet::new(),
314        };
315
316        let qlog_wirefilter = matches.remove_one::<String>("qlog_wirefilter");
317
318        let report_text = matches.get_flag("report_text");
319        let report_omit_upload = matches.get_flag("report_omit_upload");
320        let report_omit_priorities = matches.get_flag("report_omit_priorities");
321        let report_text_csv = matches.get_flag("report_text_csv");
322        let report_html = matches.get_flag("report_html");
323
324        let print_stats = matches.remove_one::<String>("print_stats").unwrap();
325        let stats_config = match print_stats.as_str() {
326            "all" => PrintStatsConfig {
327                rx_flow_control: true,
328                tx_flow_control: true,
329                reset_streams: true,
330                stream_buffering: true,
331                tx_stream_frames: true,
332                packet_stats: true,
333            },
334
335            "stream" => PrintStatsConfig {
336                rx_flow_control: true,
337                tx_flow_control: true,
338                reset_streams: true,
339                stream_buffering: true,
340                tx_stream_frames: true,
341                packet_stats: false,
342            },
343
344            "packets" => PrintStatsConfig {
345                rx_flow_control: false,
346                tx_flow_control: false,
347                reset_streams: false,
348                stream_buffering: false,
349                tx_stream_frames: false,
350                packet_stats: true,
351            },
352
353            "none" => PrintStatsConfig {
354                rx_flow_control: false,
355                tx_flow_control: false,
356                reset_streams: false,
357                stream_buffering: false,
358                tx_stream_frames: false,
359                packet_stats: false,
360            },
361
362            _ => unreachable!(),
363        };
364
365        let ignore_acks = false;
366
367        let log_format = SerializationFormat::from_filename(&file);
368
369        let config = Self {
370            file: file.to_string(),
371            filename: filename.to_string(),
372            charts_dir: charts_dir.to_string(),
373            plot_conn_overview,
374            plot_pkt_sent,
375            plot_pkt_received,
376            plot_conn_flow_control,
377            plot_sparks,
378            plot_multiplex,
379            plot_pending,
380            sparks_layout,
381            report_text,
382            report_omit_upload,
383            report_omit_priorities,
384            report_text_csv,
385            report_html,
386            dark_mode,
387            start,
388            end,
389            stream_y_max,
390            cwnd_y_max,
391            netlog_filter,
392            qlog_wirefilter,
393            stats_config,
394            ignore_acks,
395            log_format,
396        };
397
398        Ok(config)
399    }
400}