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