Skip to main content

qlog_dancer/
lib.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#![allow(clippy::collapsible_match)]
28
29use std::collections::BTreeMap;
30use std::error::Error;
31use std::fs::File;
32use std::io::BufReader;
33use std::path::Path;
34
35use config::AppConfig;
36use datastore::Datastore;
37use datastore::NetlogSession;
38use log::debug;
39use log::error;
40
41use qlog::reader::QlogSeqReader;
42use qlog::Qlog;
43
44use serde::ser::Serialize;
45
46use crate::wirefilter::filter_sqlog_events;
47
48pub type QlogPointu64 = (f64, u64);
49pub type QlogPointRtt = (f64, f32);
50
51#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Copy, Clone)]
52pub enum PacketType {
53    Initial,
54    Handshake,
55    ZeroRtt,
56    OneRtt,
57    Retry,
58    VersionNegotiation,
59    Unknown,
60}
61
62impl PacketType {
63    pub fn from_qlog_packet_type(ty: &qlog::events::quic::PacketType) -> Self {
64        match ty {
65            qlog::events::quic::PacketType::Initial => PacketType::Initial,
66            qlog::events::quic::PacketType::Handshake => PacketType::Handshake,
67            qlog::events::quic::PacketType::ZeroRtt => PacketType::ZeroRtt,
68            qlog::events::quic::PacketType::OneRtt => PacketType::OneRtt,
69            qlog::events::quic::PacketType::Retry => PacketType::Retry,
70            qlog::events::quic::PacketType::VersionNegotiation =>
71                PacketType::VersionNegotiation,
72            qlog::events::quic::PacketType::StatelessReset => PacketType::Unknown,
73            qlog::events::quic::PacketType::Unknown => PacketType::Unknown,
74        }
75    }
76
77    pub fn from_netlog_packet_header(
78        header_format: &str, long_header_type: &Option<String>,
79    ) -> Self {
80        match header_format {
81            "IETF_QUIC_LONG_HEADER_PACKET" => match long_header_type {
82                Some(v) => match v.as_str() {
83                    "INITIAL" => PacketType::Initial,
84                    "HANDSHAKE" => PacketType::Handshake,
85                    _ => PacketType::Unknown,
86                },
87
88                None => PacketType::Unknown,
89            },
90
91            "IETF_QUIC_SHORT_HEADER_PACKET" => PacketType::OneRtt,
92
93            _ => PacketType::Unknown,
94        }
95    }
96
97    pub fn from_netlog_encryption_level(encryption_level: &str) -> Self {
98        match encryption_level {
99            "ENCRYPTION_INITIAL" => PacketType::Initial,
100
101            "ENCRYPTION_HANDSHAKE" => PacketType::Handshake,
102
103            "ENCRYPTION_ZERO_RTT" => PacketType::ZeroRtt,
104
105            "ENCRYPTION_FORWARD_SECURE" => PacketType::OneRtt,
106
107            _ => PacketType::Unknown,
108        }
109    }
110}
111
112#[derive(Debug)]
113pub enum SerializationFormat {
114    QlogJson,
115    QlogJsonSeq,
116    NetlogJson,
117    Unknown,
118}
119
120impl SerializationFormat {
121    pub fn from_file_extension(extension: &str) -> Self {
122        match extension {
123            "qlog" => SerializationFormat::QlogJson,
124            "sqlog" | "gz" | "zst" => SerializationFormat::QlogJsonSeq,
125            "json" => Self::NetlogJson,
126            _ => SerializationFormat::Unknown,
127        }
128    }
129
130    pub fn from_filename(filename: &str) -> Self {
131        // Check for compound extensions first (order matters!)
132        if filename.ends_with(".sqlog.gz") || filename.ends_with(".sqlog.zst") {
133            return SerializationFormat::QlogJsonSeq;
134        }
135        if filename.ends_with(".sqlog") {
136            return SerializationFormat::QlogJsonSeq;
137        }
138        if filename.ends_with(".qlog") {
139            return SerializationFormat::QlogJson;
140        }
141        // Fall back to extension-based detection
142        if let Some(ext) = filename.rsplit('.').next() {
143            return Self::from_file_extension(ext);
144        }
145        SerializationFormat::Unknown
146    }
147}
148
149pub struct VantagePointTypeShim {
150    pub inner: qlog::VantagePointType,
151}
152
153impl Default for VantagePointTypeShim {
154    fn default() -> Self {
155        VantagePointTypeShim {
156            inner: qlog::VantagePointType::Unknown,
157        }
158    }
159}
160
161impl std::fmt::Display for VantagePointTypeShim {
162    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
163        write!(f, "{:?}", self.inner)
164    }
165}
166
167pub struct LogFileDetails {
168    pub file_schema: String,
169    pub serialization_format: String,
170    pub qlog_vantage_point_type: VantagePointTypeShim,
171    pub sessions: BTreeMap<i64, NetlogSession>,
172}
173
174pub enum RawLogEvents {
175    QlogJson { events: Vec<qlog::events::Event> },
176    QlogJsonSeq { events: Vec<qlog::reader::Event> },
177    Netlog,
178}
179
180pub struct LogFileData {
181    pub datastore: Datastore,
182    pub raw: RawLogEvents,
183}
184
185pub struct LogFileParseResult {
186    pub details: LogFileDetails,
187    pub data: Vec<LogFileData>,
188}
189
190pub fn parse_log_file(
191    config: &AppConfig,
192) -> Result<LogFileParseResult, Box<dyn Error>> {
193    match config.log_format {
194        SerializationFormat::QlogJson => {
195            println!("parsing qlog as JSON...");
196            let mark = std::time::Instant::now();
197            let qlog = read_qlog_from_file(config.file.clone())?;
198            let vp = qlog.traces[0].vantage_point.clone().unwrap_or_default().ty;
199
200            let details = LogFileDetails {
201                file_schema: qlog.file_schema.clone(),
202                serialization_format: qlog.serialization_format.clone(),
203                qlog_vantage_point_type: VantagePointTypeShim { inner: vp },
204                sessions: BTreeMap::new(),
205            };
206            debug!("\tcomplete in {:?}", std::time::Instant::now() - mark);
207
208            println!("populating datastore...");
209            let mark = std::time::Instant::now();
210            // TODO: support more than one trace in a file
211            let datastore = Datastore::with_qlog_events(
212                &qlog.traces[0].events,
213                &details.qlog_vantage_point_type.inner,
214                !config.ignore_acks,
215            );
216            debug!("\tcomplete in {:?}", std::time::Instant::now() - mark);
217
218            let raw = RawLogEvents::QlogJson {
219                events: qlog.traces[0].events.clone(),
220            };
221
222            Ok(LogFileParseResult {
223                details,
224                data: vec![LogFileData { datastore, raw }],
225            })
226        },
227
228        SerializationFormat::QlogJsonSeq => {
229            println!("parsing qlog as JSON-SEQ...");
230            let mark = std::time::Instant::now();
231
232            let (qlog_reader, details) = qlog_seq_reader(config)?;
233
234            debug!("\tcomplete in {:?}", std::time::Instant::now() - mark);
235
236            println!("populating datastore...");
237            let mark = std::time::Instant::now();
238            let events: Vec<qlog::reader::Event> =
239                qlog_reader.into_iter().collect();
240            let datastore: Datastore = Datastore::with_sqlog_reader_events(
241                &events,
242                &details.qlog_vantage_point_type.inner,
243                !config.ignore_acks,
244            );
245            debug!("\tcomplete in {:?}", std::time::Instant::now() - mark);
246
247            let events = if let Some(filter) = &config.qlog_wirefilter {
248                filter_sqlog_events(events, filter)
249            } else {
250                events
251            };
252
253            let raw = RawLogEvents::QlogJsonSeq { events };
254
255            Ok(LogFileParseResult {
256                details,
257                data: vec![LogFileData { datastore, raw }],
258            })
259        },
260
261        SerializationFormat::NetlogJson => {
262            println!("setting up parsing file as netlog...");
263            let mark = std::time::Instant::now();
264
265            let file = std::fs::File::open(config.file.clone())?;
266            let mut reader = BufReader::new(file);
267
268            let constants = netlog_with_reader(&mut reader).unwrap();
269
270            // trace!("{:?}", constants);
271            debug!("\tcomplete in {:?}", std::time::Instant::now() - mark);
272
273            println!("parsing file ...");
274            println!("Filtering on hostnames: {:#?}", config.netlog_filter);
275            let mark = std::time::Instant::now();
276
277            let (data, sessions) = datastore::with_netlog_reader(
278                &mut reader,
279                config.netlog_filter.clone(),
280                &constants,
281            );
282            debug!("\tcomplete in {:?}", std::time::Instant::now() - mark);
283
284            let details = LogFileDetails {
285                file_schema: constants.log_format_version.to_string(),
286                serialization_format: "Chrome netlog".to_string(),
287                qlog_vantage_point_type: VantagePointTypeShim {
288                    inner: qlog::VantagePointType::Client,
289                },
290                sessions,
291            };
292
293            Ok(LogFileParseResult { details, data })
294        },
295
296        _ => {
297            error!("Unknown log file format for {}", config.filename);
298            Err("total fail".into())
299        },
300    }
301}
302
303pub fn read_qlog_from_file<P: AsRef<Path>>(
304    path: P,
305) -> Result<Qlog, Box<dyn Error>> {
306    let file = File::open(path)?;
307    let reader = BufReader::new(file);
308
309    let qlog = serde_json::from_reader(reader)?;
310
311    Ok(qlog)
312}
313
314pub fn qlog_seq_reader(
315    config: &AppConfig,
316) -> Result<(QlogSeqReader<'static>, LogFileDetails), Box<dyn Error>> {
317    let qlog_reader =
318        QlogSeqReader::with_file(config.file.clone()).map_err(|e| {
319            std::io::Error::other(format!("problem reading file! {}", e))
320        })?;
321
322    let vp = qlog_reader
323        .qlog
324        .trace
325        .vantage_point
326        .clone()
327        .unwrap_or_default()
328        .ty;
329    let log_file_details = LogFileDetails {
330        file_schema: qlog_reader.qlog.file_schema.clone(),
331        serialization_format: qlog_reader.qlog.serialization_format.clone(),
332        qlog_vantage_point_type: VantagePointTypeShim { inner: vp },
333        sessions: BTreeMap::new(),
334    };
335
336    Ok((qlog_reader, log_file_details))
337}
338
339pub fn netlog_with_reader<R: std::io::BufRead>(
340    reader: &mut R,
341) -> Result<netlog::constants::Constants, Box<dyn Error>> {
342    // Netlog format is sort of newline-delimited. It starts off creating a JSON
343    // object, within that is an object containing constants, followed by an
344    // array of line-delimited events. This franken-JSON needs a bit of molding
345    // to fit serde parsing.
346    let mut buf = Vec::<u8>::new();
347
348    // read the constants line
349    let len = reader.read_until(b'\n', &mut buf).unwrap();
350
351    // replace the trailing comma (,) with a brace (}) to close the object and
352    // make it parseable.
353    buf[len - 2] = b'}';
354
355    let res: Result<netlog::constants::ConstantsLine, serde_json::Error> =
356        serde_json::from_slice(&buf);
357
358    match res {
359        Ok(mut line) => {
360            line.constants.populate_id_keyed();
361
362            Ok(line.constants)
363        },
364
365        Err(e) => {
366            error!("Error deserializing: {}", e);
367
368            // Just swallow the failure and move on
369
370            Err(e.into())
371        },
372    }
373}
374
375pub fn stringify_last<T>(src: &[T]) -> String
376where
377    T: std::fmt::Debug,
378{
379    if src.len() == 1 {
380        "n/a".to_string()
381    } else {
382        format!("{:?}", src.last().unwrap())
383    }
384}
385
386// slight hack: duplicate the previous point so
387// that no misleading line interpolation occurs
388fn push_interp<X: Clone, Y: Clone>(collection: &mut Vec<(X, Y)>, value: (X, Y)) {
389    let prev = collection.last().cloned();
390    let new_time = value.0.clone();
391
392    if let Some((_, y)) = prev {
393        collection.push((new_time, y));
394    }
395
396    collection.push(value)
397}
398
399fn create_file_recursive(filename: &str) -> std::io::Result<File> {
400    let path = std::path::Path::new(filename);
401    if let Some(dir) = path.parent() {
402        std::fs::create_dir_all(dir)?;
403    }
404
405    File::create(filename)
406}
407
408pub fn category_and_type_from_name(name: &str) -> (String, String) {
409    let mut category = "".to_string();
410    let mut ty = "".to_string();
411
412    let split: Vec<&str> = name.split(':').collect();
413    if let Some(cat) = split.first() {
414        category = cat.to_string();
415    }
416    if let Some(t) = split.get(1) {
417        ty = t.to_string();
418    }
419
420    (category, ty)
421}
422pub fn category_and_type_from_event<T: Serialize>(ev: &T) -> (String, String) {
423    let name = serde_json::to_value(ev).unwrap()["name"]
424        .to_string()
425        .replace("\"", "");
426    category_and_type_from_name(&name)
427}
428
429pub mod config;
430pub mod datastore;
431pub mod plots;
432pub mod reports;
433pub mod request_stub;
434pub mod seriesstore;
435pub mod trackers;
436#[cfg(target_arch = "wasm32")]
437pub mod web;
438pub mod wirefilter;