Skip to main content

netlog/
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//! The netlog crate is a reverse-engineered deserializer for the Chrome
28//! [netlog] format. It supports QUIC and HTTP(/2 and /3) events.
29//!
30//! # Overview
31//!
32//! Chromium-based browsers allow users to enable detailed logging, netlog,
33//! which is useful for debugging interoperability or performance issues. A
34//! netlog file uses a kind of line-delimited JSON format. The first line
35//! contains "constants", which are specific to the version of the software used
36//! to generate the log. These constants are used for a form of compressed
37//! encoding for the netlog events that appear on each subsequent newline.
38//!
39//! This crate supports parsing a netlog file and converting a subset of netlog
40//! events into Rust structures, via Serde.
41//!
42//! # Example usage
43//!
44//! Assuming a netlog file name of `chrome-net-export-log-error.json`, the first
45//! task is to create a `BufReader` for the file and initialize the netlog
46//! constants.
47//!
48//! ```no_run
49//! use netlog::read_netlog_constants;
50//! use std::fs::File;
51//! use std::io::BufReader;
52//!
53//! let mut reader =
54//!     BufReader::new(File::open("chrome-net-export-log-error.json").unwrap());
55//!
56//! let constants = read_netlog_constants(&mut reader).unwrap();
57//! ```
58//!
59//! Then move on to parsing the netlog file until the end.
60//!
61//! ```no_run
62//! # use std::io::BufReader;
63//! # use std::fs::File;
64//! # use netlog::read_netlog_constants;
65//! use netlog::read_netlog_record;
66//! use netlog::EventHeader;
67//! use netlog::h2::Http2SessionEvent;
68//! use netlog::quic::QuicSessionEvent;
69//! # let mut reader =
70//! #    BufReader::new(File::open("chrome-net-export-log-error.json").unwrap());
71//! # let constants = read_netlog_constants(&mut reader).unwrap();
72//! // The second line of a netlog is `"events" [`, which can be skipped over.
73//! read_netlog_record(&mut reader);
74//!
75//! while let Some(record) = read_netlog_record(&mut reader) {
76//!     let res: Result<EventHeader, serde_json::Error> =
77//!         serde_json::from_slice(&record);
78//!
79//!     match res {
80//!         Ok(mut event_hdr) => {
81//!             event_hdr.populate_strings(&constants);
82//!             event_hdr.time_num = event_hdr.time.parse::<u64>().unwrap();
83//!
84//!             // Netlogs can hold many different sessions.
85//!             // Application might want to track these separately
86//!             if event_hdr.phase_string == "PHASE_BEGIN" {
87//!                 match event_hdr.ty_string.as_str() {
88//!                     "HTTP2_SESSION" => {
89//!                         let ev: Http2SessionEvent =
90//!                             serde_json::from_slice(&record).unwrap();
91//!                         // Handle new session event ...
92//!                     },
93//!                     "QUIC_SESSION" => {
94//!                         let ev: QuicSessionEvent =
95//!                             serde_json::from_slice(&record).unwrap();
96//!                         // Handle new session event ...
97//!                     },
98//!
99//!                     // Ignore others
100//!                     _ => (),
101//!                 }
102//!             }
103//!
104//!             // Try to parse other events.
105//!             if let Some(ev) = netlog::parse_event(&event_hdr, &record) {
106//!                 // Handle parsed event.
107//!             }
108//!         },
109//!
110//!         Err(e) => {
111//!             println!("Error deserializing: {}", e);
112//!             println!("input value {}", String::from_utf8_lossy(&record));
113//!         },
114//!     }
115//! }
116//! ```
117//!
118//! [netlog]:
119//! (https://www.chromium.org/developers/design-documents/network-stack/netlog/)
120use std::io::BufRead;
121
122use serde::Deserialize;
123
124use crate::constants::Constants;
125use crate::constants::ConstantsLine;
126
127#[derive(Deserialize, Debug, Default)]
128pub struct EventSource {
129    #[serde(skip)]
130    pub start_time_int: u64,
131
132    pub id: i64,
133    pub start_time: String,
134    #[serde(rename = "type")]
135    pub ty: i64,
136}
137
138#[derive(Deserialize, Debug, Default)]
139pub struct EventHeader {
140    #[serde(skip)]
141    pub ty_string: String,
142    #[serde(skip)]
143    pub phase_string: String,
144    #[serde(skip)]
145    pub time_num: u64,
146
147    pub phase: i64,
148    pub source: EventSource,
149    pub time: String,
150    #[serde(rename = "type")]
151    pub ty: i64,
152}
153
154impl EventHeader {
155    /// Populate the event details based on the provided netlog file constants.
156    pub fn populate_strings(&mut self, constants: &constants::Constants) {
157        self.ty_string = constants.log_event_types_id_keyed[&self.ty].clone();
158        self.phase_string =
159            constants.log_event_phase_id_keyed[&self.phase].clone();
160    }
161}
162
163#[derive(Deserialize, Debug, Default)]
164pub struct SourceDependency {
165    pub id: i64,
166    #[serde(rename = "type")]
167    pub ty: i64,
168}
169
170/// The core netlog event type with several domain-specific variants.
171#[derive(Debug)]
172pub enum Event {
173    Http(http::Event),
174    H2(h2::Event),
175    H3(h3::Event),
176    Quic(quic::Event),
177}
178
179/// Read the netlog constants from a netlog file accessed by a BufRead.
180pub fn read_netlog_constants<R: BufRead>(
181    reader: &mut R,
182) -> Result<Constants, serde_json::Error> {
183    let mut buf = Vec::<u8>::new();
184
185    // Read the constants line and replace the trailing comma (,) with a brace
186    // (}) to close the object and make it parseable.
187    let len = reader.read_until(b'\n', &mut buf).unwrap();
188    buf[len - 2] = b'}';
189
190    let res: Result<ConstantsLine, serde_json::Error> =
191        serde_json::from_slice(&buf);
192
193    match res {
194        Ok(mut line) => {
195            line.constants.populate_id_keyed();
196
197            Ok(line.constants)
198        },
199
200        Err(e) => {
201            log::error!("Error deserializing constants: {}", e);
202
203            Err(e)
204        },
205    }
206}
207
208/// Reads a single record from a netlog file accessed by a BufRead.
209pub fn read_netlog_record<R: BufRead>(reader: &mut R) -> Option<Vec<u8>> {
210    let mut buf = Vec::<u8>::new();
211    let size = reader.read_until(b'\n', &mut buf).unwrap();
212
213    if size <= 1 {
214        return None;
215    }
216
217    // After netlog events, line holds polledData struct. Ignore it and return
218    if buf[0] != b'{' {
219        return None;
220    }
221
222    // Remove trailing comma and newline
223    buf.truncate(buf.len() - 2);
224
225    // Last line of events closes array. Lets ignore it.
226    if buf[buf.len() - 1] == b']' {
227        buf.truncate(buf.len() - 1);
228    }
229
230    log::trace!(
231        "read record={}",
232        String::from_utf8(buf.clone()).expect("from_utf8 failed")
233    );
234
235    Some(buf)
236}
237
238/// Parses the provided `event` based on the event type provided in `event_hdr`.
239pub fn parse_event(event_hdr: &EventHeader, event: &[u8]) -> Option<Event> {
240    if event_hdr.ty_string.starts_with("HTTP_") {
241        return http::parse_event(event_hdr, event);
242    } else if event_hdr.ty_string.starts_with("HTTP2_") {
243        return h2::parse_event(event_hdr, event);
244    } else if event_hdr.ty_string.starts_with("HTTP3_") {
245        return h3::parse_event(event_hdr, event);
246    } else if event_hdr.ty_string.starts_with("QUIC") {
247        return quic::parse_event(event_hdr, event);
248    }
249
250    None
251}
252
253pub mod constants;
254pub mod h2;
255pub mod h3;
256pub mod http;
257pub mod quic;