| // Copyright (C) 2019, Cloudflare, Inc. |
| // All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are |
| // met: |
| // |
| // * Redistributions of source code must retain the above copyright notice, |
| // this list of conditions and the following disclaimer. |
| // |
| // * Redistributions in binary form must reproduce the above copyright |
| // notice, this list of conditions and the following disclaimer in the |
| // documentation and/or other materials provided with the distribution. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
| // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
| // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
| // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
| // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| //! The qlog crate is an implementation of the [qlog main schema] and [qlog QUIC |
| //! and HTTP/3 events] that attempts to closely follow the format of the qlog |
| //! [TypeScript schema]. This is just a data model and no support is provided |
| //! for logging IO, applications can decide themselves the most appropriate |
| //! method. |
| //! |
| //! The crate uses Serde for conversion between Rust and JSON. |
| //! |
| //! [qlog main schema]: https://tools.ietf.org/html/draft-marx-qlog-main-schema |
| //! [qlog QUIC and HTTP/3 events]: |
| //! https://quiclog.github.io/internet-drafts/draft-marx-qlog-event-definitions-quic-h3 |
| //! [TypeScript schema]: |
| //! https://github.com/quiclog/qlog/blob/master/TypeScript/draft-01/QLog.ts |
| //! |
| //! Overview |
| //! --------------- |
| //! qlog is a hierarchical logging format, with a rough structure of: |
| //! |
| //! * Log |
| //! * Trace(s) |
| //! * Event(s) |
| //! |
| //! In practice, a single QUIC connection maps to a single Trace file with one |
| //! or more Events. Applications can decide whether to combine Traces from |
| //! different connections into the same Log. |
| //! |
| //! ## Traces |
| //! |
| //! A [`Trace`] contains metadata such as the [`VantagePoint`] of capture and |
| //! the [`Configuration`] of the `Trace`. |
| //! |
| //! A very important part of the `Trace` is the definition of `event_fields`. A |
| //! qlog Event is a vector of [`EventField`]; this provides great flexibility to |
| //! log events with any number of `EventFields` in any order. The `event_fields` |
| //! property describes the format of event logging and it is important that |
| //! events comply with that format. Failing to do so it going to cause problems |
| //! for qlog analysis tools. For information is available at |
| //! https://tools.ietf.org/html/draft-marx-qlog-main-schema-01#section-3.3.4 |
| //! |
| //! In order to make using qlog a bit easier, this crate expects a qlog Event to |
| //! consist of the following EventFields in the following order: |
| //! [`EventField::RelativeTime`], [`EventField::Category`], |
| //! [`EventField::Event`] and [`EventField::Data`]. A set of methods are |
| //! provided to assist in creating a Trace and appending events to it in this |
| //! format. |
| //! |
| //! ## Writing out logs |
| //! As events occur during the connection, the application appends them to the |
| //! trace. The qlog crate supports two modes of writing logs: the buffered mode |
| //! stores everything in memory and requires the application to serialize and |
| //! write the output, the streaming mode progressively writes serialized JSON |
| //! output to a writer designated by the application. |
| //! |
| //! ### Creating a Trace |
| //! |
| //! A typical application needs a single qlog [`Trace`] that it appends QUIC |
| //! and/or HTTP/3 events to: |
| //! |
| //! ``` |
| //! let mut trace = qlog::Trace::new( |
| //! qlog::VantagePoint { |
| //! name: Some("Example client".to_string()), |
| //! ty: qlog::VantagePointType::Client, |
| //! flow: None, |
| //! }, |
| //! Some("Example qlog trace".to_string()), |
| //! Some("Example qlog trace description".to_string()), |
| //! Some(qlog::Configuration { |
| //! time_offset: Some("0".to_string()), |
| //! time_units: Some(qlog::TimeUnits::Ms), |
| //! original_uris: None, |
| //! }), |
| //! None, |
| //! ); |
| //! ``` |
| //! |
| //! ## Adding events |
| //! |
| //! Qlog Events are added to [`qlog::Trace.events`]. |
| //! |
| //! It is recommended to use the provided utility methods to append semantically |
| //! valid events to a trace. However, there is nothing preventing you from |
| //! creating the events manually. |
| //! |
| //! The following example demonstrates how to log a QUIC packet |
| //! containing a single Crypto frame. It uses the [`QuicFrame::crypto()`], |
| //! [`packet_sent_min()`] and [`push_event()`] methods to create and log a |
| //! PacketSent event and its EventData. |
| //! |
| //! ``` |
| //! # let mut trace = qlog::Trace::new ( |
| //! # qlog::VantagePoint { |
| //! # name: Some("Example client".to_string()), |
| //! # ty: qlog::VantagePointType::Client, |
| //! # flow: None, |
| //! # }, |
| //! # Some("Example qlog trace".to_string()), |
| //! # Some("Example qlog trace description".to_string()), |
| //! # Some(qlog::Configuration { |
| //! # time_offset: Some("0".to_string()), |
| //! # time_units: Some(qlog::TimeUnits::Ms), |
| //! # original_uris: None, |
| //! # }), |
| //! # None |
| //! # ); |
| //! |
| //! let scid = [0x7e, 0x37, 0xe4, 0xdc, 0xc6, 0x68, 0x2d, 0xa8]; |
| //! let dcid = [0x36, 0xce, 0x10, 0x4e, 0xee, 0x50, 0x10, 0x1c]; |
| //! |
| //! let pkt_hdr = qlog::PacketHeader::new( |
| //! 0, |
| //! Some(1251), |
| //! Some(1224), |
| //! Some(0xff00001b), |
| //! Some(b"7e37e4dcc6682da8"), |
| //! Some(&dcid), |
| //! ); |
| //! |
| //! let frames = |
| //! vec![qlog::QuicFrame::crypto("0".to_string(), "1000".to_string())]; |
| //! |
| //! let event = qlog::event::Event::packet_sent_min( |
| //! qlog::PacketType::Initial, |
| //! pkt_hdr, |
| //! Some(frames), |
| //! ); |
| //! |
| //! trace.push_event(std::time::Duration::new(0, 0), event); |
| //! ``` |
| //! |
| //! ### Serializing |
| //! |
| //! The qlog crate has only been tested with `serde_json`, however |
| //! other serializer targets might work. |
| //! |
| //! For example, serializing the trace created above: |
| //! |
| //! ``` |
| //! # let mut trace = qlog::Trace::new ( |
| //! # qlog::VantagePoint { |
| //! # name: Some("Example client".to_string()), |
| //! # ty: qlog::VantagePointType::Client, |
| //! # flow: None, |
| //! # }, |
| //! # Some("Example qlog trace".to_string()), |
| //! # Some("Example qlog trace description".to_string()), |
| //! # Some(qlog::Configuration { |
| //! # time_offset: Some("0".to_string()), |
| //! # time_units: Some(qlog::TimeUnits::Ms), |
| //! # original_uris: None, |
| //! # }), |
| //! # None |
| //! # ); |
| //! serde_json::to_string_pretty(&trace).unwrap(); |
| //! ``` |
| //! |
| //! which would generate the following: |
| //! |
| //! ```ignore |
| //! { |
| //! "vantage_point": { |
| //! "name": "Example client", |
| //! "type": "client" |
| //! }, |
| //! "title": "Example qlog trace", |
| //! "description": "Example qlog trace description", |
| //! "configuration": { |
| //! "time_units": "ms", |
| //! "time_offset": "0" |
| //! }, |
| //! "event_fields": [ |
| //! "relative_time", |
| //! "category", |
| //! "event", |
| //! "data" |
| //! ], |
| //! "events": [ |
| //! [ |
| //! "0", |
| //! "transport", |
| //! "packet_sent", |
| //! { |
| //! "packet_type": "initial", |
| //! "header": { |
| //! "packet_number": "0", |
| //! "packet_size": 1251, |
| //! "payload_length": 1224, |
| //! "version": "ff00001b", |
| //! "scil": "8", |
| //! "dcil": "8", |
| //! "scid": "7e37e4dcc6682da8", |
| //! "dcid": "36ce104eee50101c" |
| //! }, |
| //! "frames": [ |
| //! { |
| //! "frame_type": "crypto", |
| //! "offset": "0", |
| //! "length": "100", |
| //! } |
| //! ] |
| //! } |
| //! ] |
| //! ] |
| //! } |
| //! ``` |
| //! |
| //! Streaming Mode |
| //! -------------- |
| //! |
| //! Create the trace: |
| //! |
| //! ``` |
| //! let mut trace = qlog::Trace::new( |
| //! qlog::VantagePoint { |
| //! name: Some("Example client".to_string()), |
| //! ty: qlog::VantagePointType::Client, |
| //! flow: None, |
| //! }, |
| //! Some("Example qlog trace".to_string()), |
| //! Some("Example qlog trace description".to_string()), |
| //! Some(qlog::Configuration { |
| //! time_offset: Some("0".to_string()), |
| //! time_units: Some(qlog::TimeUnits::Ms), |
| //! original_uris: None, |
| //! }), |
| //! None, |
| //! ); |
| //! ``` |
| //! Create an object with the [`Write`] trait: |
| //! |
| //! ``` |
| //! let mut file = std::fs::File::create("foo.qlog").unwrap(); |
| //! ``` |
| //! |
| //! Create a [`QlogStreamer`] and start serialization to foo.qlog |
| //! using [`start_log()`]: |
| //! |
| //! ``` |
| //! # let mut trace = qlog::Trace::new( |
| //! # qlog::VantagePoint { |
| //! # name: Some("Example client".to_string()), |
| //! # ty: qlog::VantagePointType::Client, |
| //! # flow: None, |
| //! # }, |
| //! # Some("Example qlog trace".to_string()), |
| //! # Some("Example qlog trace description".to_string()), |
| //! # Some(qlog::Configuration { |
| //! # time_offset: Some("0".to_string()), |
| //! # time_units: Some(qlog::TimeUnits::Ms), |
| //! # original_uris: None, |
| //! # }), |
| //! # None, |
| //! # ); |
| //! # let mut file = std::fs::File::create("foo.qlog").unwrap(); |
| //! let mut streamer = qlog::QlogStreamer::new( |
| //! qlog::QLOG_VERSION.to_string(), |
| //! Some("Example qlog".to_string()), |
| //! Some("Example qlog description".to_string()), |
| //! None, |
| //! std::time::Instant::now(), |
| //! trace, |
| //! Box::new(file), |
| //! ); |
| //! |
| //! streamer.start_log().ok(); |
| //! ``` |
| //! |
| //! ### Adding simple events |
| //! |
| //! Once logging has started you can stream events. Simple events |
| //! can be written in one step using [`add_event()`]: |
| //! |
| //! ``` |
| //! # let mut trace = qlog::Trace::new( |
| //! # qlog::VantagePoint { |
| //! # name: Some("Example client".to_string()), |
| //! # ty: qlog::VantagePointType::Client, |
| //! # flow: None, |
| //! # }, |
| //! # Some("Example qlog trace".to_string()), |
| //! # Some("Example qlog trace description".to_string()), |
| //! # Some(qlog::Configuration { |
| //! # time_offset: Some("0".to_string()), |
| //! # time_units: Some(qlog::TimeUnits::Ms), |
| //! # original_uris: None, |
| //! # }), |
| //! # None, |
| //! # ); |
| //! # let mut file = std::fs::File::create("foo.qlog").unwrap(); |
| //! # let mut streamer = qlog::QlogStreamer::new( |
| //! # qlog::QLOG_VERSION.to_string(), |
| //! # Some("Example qlog".to_string()), |
| //! # Some("Example qlog description".to_string()), |
| //! # None, |
| //! # std::time::Instant::now(), |
| //! # trace, |
| //! # Box::new(file), |
| //! # ); |
| //! let event = qlog::event::Event::metrics_updated_min(); |
| //! streamer.add_event(event).ok(); |
| //! ``` |
| //! |
| //! ### Adding events with frames |
| //! Some events contain optional arrays of QUIC frames. If the |
| //! event has `Some(Vec<QuicFrame>)`, even if it is empty, the |
| //! streamer enters a frame serializing mode that must be |
| //! finalized before other events can be logged. |
| //! |
| //! In this example, a `PacketSent` event is created with an |
| //! empty frame array and frames are written out later: |
| //! |
| //! ``` |
| //! # let mut trace = qlog::Trace::new( |
| //! # qlog::VantagePoint { |
| //! # name: Some("Example client".to_string()), |
| //! # ty: qlog::VantagePointType::Client, |
| //! # flow: None, |
| //! # }, |
| //! # Some("Example qlog trace".to_string()), |
| //! # Some("Example qlog trace description".to_string()), |
| //! # Some(qlog::Configuration { |
| //! # time_offset: Some("0".to_string()), |
| //! # time_units: Some(qlog::TimeUnits::Ms), |
| //! # original_uris: None, |
| //! # }), |
| //! # None, |
| //! # ); |
| //! # let mut file = std::fs::File::create("foo.qlog").unwrap(); |
| //! # let mut streamer = qlog::QlogStreamer::new( |
| //! # qlog::QLOG_VERSION.to_string(), |
| //! # Some("Example qlog".to_string()), |
| //! # Some("Example qlog description".to_string()), |
| //! # None, |
| //! # std::time::Instant::now(), |
| //! # trace, |
| //! # Box::new(file), |
| //! # ); |
| //! let qlog_pkt_hdr = qlog::PacketHeader::with_type( |
| //! qlog::PacketType::OneRtt, |
| //! 0, |
| //! Some(1251), |
| //! Some(1224), |
| //! Some(0xff00001b), |
| //! Some(b"7e37e4dcc6682da8"), |
| //! Some(b"36ce104eee50101c"), |
| //! ); |
| //! |
| //! let event = qlog::event::Event::packet_sent_min( |
| //! qlog::PacketType::OneRtt, |
| //! qlog_pkt_hdr, |
| //! Some(Vec::new()), |
| //! ); |
| //! |
| //! streamer.add_event(event).ok(); |
| //! ``` |
| //! |
| //! In this example, the frames contained in the QUIC packet |
| //! are PING and PADDING. Each frame is written using the |
| //! [`add_frame()`] method. Frame writing is concluded with |
| //! [`finish_frames()`]. |
| //! |
| //! ``` |
| //! # let mut trace = qlog::Trace::new( |
| //! # qlog::VantagePoint { |
| //! # name: Some("Example client".to_string()), |
| //! # ty: qlog::VantagePointType::Client, |
| //! # flow: None, |
| //! # }, |
| //! # Some("Example qlog trace".to_string()), |
| //! # Some("Example qlog trace description".to_string()), |
| //! # Some(qlog::Configuration { |
| //! # time_offset: Some("0".to_string()), |
| //! # time_units: Some(qlog::TimeUnits::Ms), |
| //! # original_uris: None, |
| //! # }), |
| //! # None, |
| //! # ); |
| //! # let mut file = std::fs::File::create("foo.qlog").unwrap(); |
| //! # let mut streamer = qlog::QlogStreamer::new( |
| //! # qlog::QLOG_VERSION.to_string(), |
| //! # Some("Example qlog".to_string()), |
| //! # Some("Example qlog description".to_string()), |
| //! # None, |
| //! # std::time::Instant::now(), |
| //! # trace, |
| //! # Box::new(file), |
| //! # ); |
| //! |
| //! let ping = qlog::QuicFrame::ping(); |
| //! let padding = qlog::QuicFrame::padding(); |
| //! |
| //! streamer.add_frame(ping, false).ok(); |
| //! streamer.add_frame(padding, false).ok(); |
| //! |
| //! streamer.finish_frames().ok(); |
| //! ``` |
| //! |
| //! Once all events have have been written, the log |
| //! can be finalized with [`finish_log()`]: |
| //! |
| //! ``` |
| //! # let mut trace = qlog::Trace::new( |
| //! # qlog::VantagePoint { |
| //! # name: Some("Example client".to_string()), |
| //! # ty: qlog::VantagePointType::Client, |
| //! # flow: None, |
| //! # }, |
| //! # Some("Example qlog trace".to_string()), |
| //! # Some("Example qlog trace description".to_string()), |
| //! # Some(qlog::Configuration { |
| //! # time_offset: Some("0".to_string()), |
| //! # time_units: Some(qlog::TimeUnits::Ms), |
| //! # original_uris: None, |
| //! # }), |
| //! # None, |
| //! # ); |
| //! # let mut file = std::fs::File::create("foo.qlog").unwrap(); |
| //! # let mut streamer = qlog::QlogStreamer::new( |
| //! # qlog::QLOG_VERSION.to_string(), |
| //! # Some("Example qlog".to_string()), |
| //! # Some("Example qlog description".to_string()), |
| //! # None, |
| //! # std::time::Instant::now(), |
| //! # trace, |
| //! # Box::new(file), |
| //! # ); |
| //! streamer.finish_log().ok(); |
| //! ``` |
| //! |
| //! ### Serializing |
| //! |
| //! Serialization to JSON occurs as methods on the [`QlogStreamer`] |
| //! are called. No additional steps are required. |
| //! |
| //! [`Trace`]: struct.Trace.html |
| //! [`VantagePoint`]: struct.VantagePoint.html |
| //! [`Configuration`]: struct.Configuration.html |
| //! [`EventField`]: enum.EventField.html |
| //! [`EventField::RelativeTime`]: enum.EventField.html#variant.RelativeTime |
| //! [`EventField::Category`]: enum.EventField.html#variant.Category |
| //! [`EventField::Type`]: enum.EventField.html#variant.Type |
| //! [`EventField::Data`]: enum.EventField.html#variant.Data |
| //! [`qlog::Trace.events`]: struct.Trace.html#structfield.events |
| //! [`push_event()`]: struct.Trace.html#method.push_event |
| //! [`packet_sent_min()`]: event/struct.Event.html#method.packet_sent_min |
| //! [`QuicFrame::crypto()`]: enum.QuicFrame.html#variant.Crypto |
| //! [`QlogStreamer`]: struct.QlogStreamer.html |
| //! [`Write`]: https://doc.rust-lang.org/std/io/trait.Write.html |
| //! [`start_log()`]: struct.QlogStreamer.html#method.start_log |
| //! [`add_event()`]: struct.QlogStreamer.html#method.add_event |
| //! [`add_frame()`]: struct.QlogStreamer.html#method.add_frame |
| //! [`finish_frames()`]: struct.QlogStreamer.html#method.finish_frames |
| //! [`finish_log()`]: struct.QlogStreamer.html#method.finish_log |
| |
| use serde::Serialize; |
| |
| /// A quiche qlog error. |
| #[derive(Debug)] |
| pub enum Error { |
| /// There is no more work to do. |
| Done, |
| |
| /// The operation cannot be completed because it was attempted |
| /// in an invalid state. |
| InvalidState, |
| |
| /// I/O error. |
| IoError(std::io::Error), |
| } |
| |
| impl std::fmt::Display for Error { |
| fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { |
| write!(f, "{:?}", self) |
| } |
| } |
| |
| impl std::error::Error for Error { |
| fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { |
| None |
| } |
| } |
| |
| impl std::convert::From<std::io::Error> for Error { |
| fn from(err: std::io::Error) -> Self { |
| Error::IoError(err) |
| } |
| } |
| |
| pub const QLOG_VERSION: &str = "draft-02-wip"; |
| |
| /// A specialized [`Result`] type for quiche qlog operations. |
| /// |
| /// This type is used throughout the public API for any operation that |
| /// can produce an error. |
| /// |
| /// [`Result`]: https://doc.rust-lang.org/std/result/enum.Result.html |
| pub type Result<T> = std::result::Result<T, Error>; |
| |
| #[serde_with::skip_serializing_none] |
| #[derive(Serialize, Clone)] |
| pub struct Qlog { |
| pub qlog_version: String, |
| pub title: Option<String>, |
| pub description: Option<String>, |
| pub summary: Option<String>, |
| |
| pub traces: Vec<Trace>, |
| } |
| |
| impl Default for Qlog { |
| fn default() -> Self { |
| Qlog { |
| qlog_version: QLOG_VERSION.to_string(), |
| title: Some("Default qlog title".to_string()), |
| description: Some("Default qlog description".to_string()), |
| summary: Some("Default qlog title".to_string()), |
| traces: Vec::new(), |
| } |
| } |
| } |
| |
| #[derive(PartialEq)] |
| pub enum StreamerState { |
| Initial, |
| Ready, |
| WritingFrames, |
| Finished, |
| } |
| |
| /// A helper object specialized for streaming JSON-serialized qlog to a |
| /// [`Write`] trait. |
| /// |
| /// The object is responsible for the `Qlog` object that contains the provided |
| /// `Trace`. |
| /// |
| /// Serialization is progressively driven by method calls; once log streaming is |
| /// started, `event::Events` can be written using `add_event()`. Some events |
| /// can contain an array of `QuicFrame`s, when writing such an event, the |
| /// streamer enters a frame-serialization mode where frames are be progressively |
| /// written using `add_frame()`. This mode is concluded using |
| /// `finished_frames()`. While serializing frames, any attempts to log |
| /// additional events are ignored. |
| /// |
| /// [`Write`]: https://doc.rust-lang.org/std/io/trait.Write.html |
| pub struct QlogStreamer { |
| start_time: std::time::Instant, |
| writer: Box<dyn std::io::Write + Send + Sync>, |
| qlog: Qlog, |
| state: StreamerState, |
| first_event: bool, |
| first_frame: bool, |
| } |
| |
| impl QlogStreamer { |
| /// Creates a QlogStreamer object. |
| /// |
| /// It owns a `Qlog` object that contains the provided `Trace`, which must |
| /// have the following ordered-set of names EventFields: |
| /// |
| /// ["relative_time", "category", "event".to_string(), "data"] |
| /// |
| /// All serialization will be written to the provided `Write`. |
| pub fn new( |
| qlog_version: String, title: Option<String>, description: Option<String>, |
| summary: Option<String>, start_time: std::time::Instant, trace: Trace, |
| writer: Box<dyn std::io::Write + Send + Sync>, |
| ) -> Self { |
| let qlog = Qlog { |
| qlog_version, |
| title, |
| description, |
| summary, |
| traces: vec![trace], |
| }; |
| |
| QlogStreamer { |
| start_time, |
| writer, |
| qlog, |
| state: StreamerState::Initial, |
| first_event: true, |
| first_frame: false, |
| } |
| } |
| |
| /// Starts qlog streaming serialization. |
| /// |
| /// This writes out the JSON-serialized form of all information up to qlog |
| /// `Trace`'s array of `EventField`s. EventFields are separately appended |
| /// using functions that accept and `event::Event`. |
| pub fn start_log(&mut self) -> Result<()> { |
| if self.state != StreamerState::Initial { |
| return Err(Error::Done); |
| } |
| |
| // A qlog contains a trace holding a vector of events that we want to |
| // serialize in a streaming manner. So at the start of serialization, |
| // take off all closing delimiters, and leave us in a state to accept |
| // new events. |
| match serde_json::to_string(&self.qlog) { |
| Ok(mut out) => { |
| out.truncate(out.len() - 4); |
| |
| self.writer.as_mut().write_all(out.as_bytes())?; |
| |
| self.state = StreamerState::Ready; |
| |
| self.first_event = self.qlog.traces[0].events.is_empty(); |
| }, |
| |
| _ => return Err(Error::Done), |
| } |
| |
| Ok(()) |
| } |
| |
| /// Finishes qlog streaming serialization. |
| /// |
| /// The JSON-serialized output has remaining close delimiters added. |
| /// After this is called, no more serialization will occur. |
| pub fn finish_log(&mut self) -> Result<()> { |
| if self.state == StreamerState::Initial || |
| self.state == StreamerState::Finished |
| { |
| return Err(Error::InvalidState); |
| } |
| |
| self.writer.as_mut().write_all(b"]}]}")?; |
| |
| self.state = StreamerState::Finished; |
| |
| self.writer.as_mut().flush()?; |
| |
| Ok(()) |
| } |
| |
| /// Writes a JSON-serialized `EventField`s at `std::time::Instant::now()`. |
| /// |
| /// Some qlog events can contain `QuicFrames`. If this is detected `true` is |
| /// returned and the streamer enters a frame-serialization mode that is only |
| /// concluded by `finish_frames()`. In this mode, attempts to log additional |
| /// events are ignored. |
| /// |
| /// If the event contains no array of `QuicFrames` return `false`. |
| pub fn add_event(&mut self, event: event::Event) -> Result<bool> { |
| let now = std::time::Instant::now(); |
| |
| self.add_event_with_instant(event, now) |
| } |
| |
| /// Writes a JSON-serialized `EventField`s with the provided instant. |
| /// |
| /// Some qlog events can contain `QuicFrames`. If this is detected `true` is |
| /// returned and the streamer enters a frame-serialization mode that is only |
| /// concluded by `finish_frames()`. In this mode, attempts to log additional |
| /// events are ignored. |
| /// |
| /// If the event contains no array of `QuicFrames` return `false`. |
| pub fn add_event_with_instant( |
| &mut self, event: event::Event, now: std::time::Instant, |
| ) -> Result<bool> { |
| if self.state != StreamerState::Ready { |
| return Err(Error::InvalidState); |
| } |
| |
| let event_time = if cfg!(test) { |
| std::time::Duration::from_secs(0) |
| } else { |
| now.duration_since(self.start_time) |
| }; |
| |
| let rel = match &self.qlog.traces[0].configuration { |
| Some(conf) => match conf.time_units { |
| Some(TimeUnits::Ms) => event_time.as_millis().to_string(), |
| |
| Some(TimeUnits::Us) => event_time.as_micros().to_string(), |
| |
| None => String::from(""), |
| }, |
| |
| None => String::from(""), |
| }; |
| |
| let (ev_data, contains_frames) = match serde_json::to_string(&event.data) |
| { |
| Ok(mut ev_data_out) => |
| if let Some(f) = event.data.contains_quic_frames() { |
| ev_data_out.truncate(ev_data_out.len() - 2); |
| |
| if f == 0 { |
| self.first_frame = true; |
| } |
| |
| (ev_data_out, true) |
| } else { |
| (ev_data_out, false) |
| }, |
| |
| _ => return Err(Error::Done), |
| }; |
| |
| let maybe_comma = if self.first_event { |
| self.first_event = false; |
| "" |
| } else { |
| "," |
| }; |
| |
| let maybe_terminate = if contains_frames { "" } else { "]" }; |
| |
| let ev_time = serde_json::to_string(&EventField::RelativeTime(rel)).ok(); |
| let ev_cat = |
| serde_json::to_string(&EventField::Category(event.category)).ok(); |
| let ev_ty = serde_json::to_string(&EventField::Event(event.ty)).ok(); |
| |
| if let (Some(ev_time), Some(ev_cat), Some(ev_ty)) = |
| (ev_time, ev_cat, ev_ty) |
| { |
| let out = format!( |
| "{}[{},{},{},{}{}", |
| maybe_comma, ev_time, ev_cat, ev_ty, ev_data, maybe_terminate |
| ); |
| |
| self.writer.as_mut().write_all(out.as_bytes())?; |
| |
| if contains_frames { |
| self.state = StreamerState::WritingFrames |
| } else { |
| self.state = StreamerState::Ready |
| }; |
| |
| return Ok(contains_frames); |
| } |
| |
| Err(Error::Done) |
| } |
| |
| /// Writes a JSON-serialized `QuicFrame`. |
| /// |
| /// Only valid while in the frame-serialization mode. |
| pub fn add_frame(&mut self, frame: QuicFrame, last: bool) -> Result<()> { |
| if self.state != StreamerState::WritingFrames { |
| return Err(Error::InvalidState); |
| } |
| |
| match serde_json::to_string(&frame) { |
| Ok(mut out) => { |
| if !self.first_frame { |
| out.insert(0, ','); |
| } else { |
| self.first_frame = false; |
| } |
| |
| self.writer.as_mut().write_all(out.as_bytes())?; |
| |
| if last { |
| self.finish_frames()?; |
| } |
| }, |
| |
| _ => return Err(Error::Done), |
| } |
| |
| Ok(()) |
| } |
| |
| /// Concludes `QuicFrame` streaming serialization. |
| /// |
| /// Only valid while in the frame-serialization mode. |
| pub fn finish_frames(&mut self) -> Result<()> { |
| if self.state != StreamerState::WritingFrames { |
| return Err(Error::InvalidState); |
| } |
| |
| self.writer.as_mut().write_all(b"]}]")?; |
| self.state = StreamerState::Ready; |
| |
| Ok(()) |
| } |
| |
| /// Returns the writer. |
| #[allow(clippy::borrowed_box)] |
| pub fn writer(&self) -> &Box<dyn std::io::Write + Send + Sync> { |
| &self.writer |
| } |
| } |
| |
| #[serde_with::skip_serializing_none] |
| #[derive(Serialize, Clone)] |
| pub struct Trace { |
| pub vantage_point: VantagePoint, |
| pub title: Option<String>, |
| pub description: Option<String>, |
| |
| pub configuration: Option<Configuration>, |
| |
| pub common_fields: Option<CommonFields>, |
| pub event_fields: Vec<String>, |
| |
| pub events: Vec<Vec<EventField>>, |
| } |
| |
| /// Helper functions for using a qlog trace. |
| impl Trace { |
| /// Creates a new qlog trace with the hard-coded event_fields |
| /// ["relative_time", "category", "event", "data"] |
| pub fn new( |
| vantage_point: VantagePoint, title: Option<String>, |
| description: Option<String>, configuration: Option<Configuration>, |
| common_fields: Option<CommonFields>, |
| ) -> Self { |
| Trace { |
| vantage_point, |
| title, |
| description, |
| configuration, |
| common_fields, |
| event_fields: vec![ |
| "relative_time".to_string(), |
| "category".to_string(), |
| "event".to_string(), |
| "data".to_string(), |
| ], |
| events: Vec::new(), |
| } |
| } |
| |
| pub fn push_event( |
| &mut self, relative_time: std::time::Duration, event: crate::event::Event, |
| ) { |
| let rel = match &self.configuration { |
| Some(conf) => match conf.time_units { |
| Some(TimeUnits::Ms) => relative_time.as_millis().to_string(), |
| |
| Some(TimeUnits::Us) => relative_time.as_micros().to_string(), |
| |
| None => String::from(""), |
| }, |
| |
| None => String::from(""), |
| }; |
| |
| self.events.push(vec![ |
| EventField::RelativeTime(rel), |
| EventField::Category(event.category), |
| EventField::Event(event.ty), |
| EventField::Data(event.data), |
| ]); |
| } |
| } |
| |
| #[serde_with::skip_serializing_none] |
| #[derive(Serialize, Clone)] |
| pub struct VantagePoint { |
| pub name: Option<String>, |
| |
| #[serde(rename = "type")] |
| pub ty: VantagePointType, |
| |
| pub flow: Option<VantagePointType>, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum VantagePointType { |
| Client, |
| Server, |
| Network, |
| Unknown, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum TimeUnits { |
| Ms, |
| Us, |
| } |
| |
| #[serde_with::skip_serializing_none] |
| #[derive(Serialize, Clone)] |
| pub struct Configuration { |
| pub time_units: Option<TimeUnits>, |
| pub time_offset: Option<String>, |
| |
| pub original_uris: Option<Vec<String>>, |
| /* TODO |
| * additionalUserSpecifiedProperty */ |
| } |
| |
| impl Default for Configuration { |
| fn default() -> Self { |
| Configuration { |
| time_units: Some(TimeUnits::Ms), |
| time_offset: Some("0".to_string()), |
| original_uris: None, |
| } |
| } |
| } |
| |
| #[serde_with::skip_serializing_none] |
| #[derive(Serialize, Clone, Default)] |
| pub struct CommonFields { |
| pub group_id: Option<String>, |
| pub protocol_type: Option<String>, |
| |
| pub reference_time: Option<String>, |
| /* TODO |
| * additionalUserSpecifiedProperty */ |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(untagged)] |
| pub enum EventType { |
| ConnectivityEventType(ConnectivityEventType), |
| |
| TransportEventType(TransportEventType), |
| |
| SecurityEventType(SecurityEventType), |
| |
| RecoveryEventType(RecoveryEventType), |
| |
| Http3EventType(Http3EventType), |
| |
| QpackEventType(QpackEventType), |
| |
| GenericEventType(GenericEventType), |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(untagged)] |
| #[allow(clippy::large_enum_variant)] |
| pub enum EventField { |
| RelativeTime(String), |
| |
| Category(EventCategory), |
| |
| Event(EventType), |
| |
| Data(EventData), |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum EventCategory { |
| Connectivity, |
| Security, |
| Transport, |
| Recovery, |
| Http, |
| Qpack, |
| |
| Error, |
| Warning, |
| Info, |
| Debug, |
| Verbose, |
| Simulation, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum ConnectivityEventType { |
| ServerListening, |
| ConnectionStarted, |
| ConnectionIdUpdated, |
| SpinBitUpdated, |
| ConnectionStateUpdated, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum TransportEventType { |
| ParametersSet, |
| |
| DatagramsSent, |
| DatagramsReceived, |
| DatagramDropped, |
| |
| PacketSent, |
| PacketReceived, |
| PacketDropped, |
| PacketBuffered, |
| |
| FramesProcessed, |
| |
| StreamStateUpdated, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum TransportEventTrigger { |
| Line, |
| Retransmit, |
| KeysUnavailable, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum SecurityEventType { |
| KeyUpdated, |
| KeyRetired, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum SecurityEventTrigger { |
| Tls, |
| Implicit, |
| RemoteUpdate, |
| LocalUpdate, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum RecoveryEventType { |
| ParametersSet, |
| MetricsUpdated, |
| CongestionStateUpdated, |
| LossTimerSet, |
| LossTimerTriggered, |
| PacketLost, |
| MarkedForRetransmit, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum RecoveryEventTrigger { |
| AckReceived, |
| PacketSent, |
| Alarm, |
| Unknown, |
| } |
| |
| // ================================================================== // |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum KeyType { |
| ServerInitialSecret, |
| ClientInitialSecret, |
| |
| ServerHandshakeSecret, |
| ClientHandshakeSecret, |
| |
| Server0RttSecret, |
| Client0RttSecret, |
| |
| Server1RttSecret, |
| Client1RttSecret, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum ConnectionState { |
| Attempted, |
| Reset, |
| Handshake, |
| Active, |
| Keepalive, |
| Draining, |
| Closed, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum TransportOwner { |
| Local, |
| Remote, |
| } |
| |
| #[derive(Serialize, Clone)] |
| pub struct PreferredAddress { |
| pub ip_v4: String, |
| pub ip_v6: String, |
| |
| pub port_v4: u64, |
| pub port_v6: u64, |
| |
| pub connection_id: String, |
| pub stateless_reset_token: String, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum StreamSide { |
| Sending, |
| Receiving, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum StreamState { |
| // bidirectional stream states, draft-23 3.4. |
| Idle, |
| Open, |
| HalfClosedLocal, |
| HalfClosedRemote, |
| Closed, |
| |
| // sending-side stream states, draft-23 3.1. |
| Ready, |
| Send, |
| DataSent, |
| ResetSent, |
| ResetReceived, |
| |
| // receive-side stream states, draft-23 3.2. |
| Receive, |
| SizeKnown, |
| DataRead, |
| ResetRead, |
| |
| // both-side states |
| DataReceived, |
| |
| // qlog-defined |
| Destroyed, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum TimerType { |
| Ack, |
| Pto, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum H3Owner { |
| Local, |
| Remote, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum H3StreamType { |
| Data, |
| Control, |
| Push, |
| Reserved, |
| QpackEncode, |
| QpackDecode, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum H3DataRecipient { |
| Application, |
| Transport, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum H3PushDecision { |
| Claimed, |
| Abandoned, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum QpackOwner { |
| Local, |
| Remote, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum QpackStreamState { |
| Blocked, |
| Unblocked, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum QpackUpdateType { |
| Added, |
| Evicted, |
| } |
| |
| #[derive(Serialize, Clone)] |
| pub struct QpackDynamicTableEntry { |
| pub index: u64, |
| pub name: Option<String>, |
| pub value: Option<String>, |
| } |
| |
| #[derive(Serialize, Clone)] |
| pub struct QpackHeaderBlockPrefix { |
| pub required_insert_count: u64, |
| pub sign_bit: bool, |
| pub delta_base: u64, |
| } |
| |
| #[serde_with::skip_serializing_none] |
| #[derive(Serialize, Clone)] |
| #[serde(untagged)] |
| #[allow(clippy::large_enum_variant)] |
| pub enum EventData { |
| // ================================================================== // |
| // CONNECTIVITY |
| ServerListening { |
| ip_v4: Option<String>, |
| ip_v6: Option<String>, |
| port_v4: u64, |
| port_v6: u64, |
| |
| quic_versions: Option<Vec<String>>, |
| alpn_values: Option<Vec<String>>, |
| |
| stateless_reset_required: Option<bool>, |
| }, |
| |
| ConnectionStarted { |
| ip_version: String, |
| src_ip: String, |
| dst_ip: String, |
| |
| protocol: Option<String>, |
| src_port: u64, |
| dst_port: u64, |
| |
| quic_version: Option<String>, |
| src_cid: Option<String>, |
| dst_cid: Option<String>, |
| }, |
| |
| ConnectionIdUpdated { |
| src_old: Option<String>, |
| src_new: Option<String>, |
| |
| dst_old: Option<String>, |
| dst_new: Option<String>, |
| }, |
| |
| SpinBitUpdated { |
| state: bool, |
| }, |
| |
| ConnectionStateUpdated { |
| old: Option<ConnectionState>, |
| new: ConnectionState, |
| }, |
| |
| // ================================================================== // |
| // SECURITY |
| KeyUpdated { |
| key_type: KeyType, |
| old: Option<String>, |
| new: String, |
| generation: Option<u64>, |
| }, |
| |
| KeyRetired { |
| key_type: KeyType, |
| key: Option<String>, |
| generation: Option<u64>, |
| }, |
| |
| // ================================================================== // |
| // TRANSPORT |
| TransportParametersSet { |
| owner: Option<TransportOwner>, |
| |
| resumption_allowed: Option<bool>, |
| early_data_enabled: Option<bool>, |
| alpn: Option<String>, |
| version: Option<String>, |
| tls_cipher: Option<String>, |
| |
| original_connection_id: Option<String>, |
| stateless_reset_token: Option<String>, |
| disable_active_migration: Option<bool>, |
| |
| idle_timeout: Option<u64>, |
| max_packet_size: Option<u64>, |
| ack_delay_exponent: Option<u64>, |
| max_ack_delay: Option<u64>, |
| active_connection_id_limit: Option<u64>, |
| |
| initial_max_data: Option<String>, |
| initial_max_stream_data_bidi_local: Option<String>, |
| initial_max_stream_data_bidi_remote: Option<String>, |
| initial_max_stream_data_uni: Option<String>, |
| initial_max_streams_bidi: Option<String>, |
| initial_max_streams_uni: Option<String>, |
| |
| preferred_address: Option<PreferredAddress>, |
| }, |
| |
| DatagramsReceived { |
| count: Option<u64>, |
| byte_length: Option<u64>, |
| }, |
| |
| DatagramsSent { |
| count: Option<u64>, |
| byte_length: Option<u64>, |
| }, |
| |
| DatagramDropped { |
| byte_length: Option<u64>, |
| }, |
| |
| PacketReceived { |
| packet_type: PacketType, |
| header: PacketHeader, |
| // `frames` is defined here in the QLog schema specification. However, |
| // our streaming serializer requires serde to put the object at the end, |
| // so we define it there and depend on serde's preserve_order feature. |
| is_coalesced: Option<bool>, |
| |
| raw_encrypted: Option<String>, |
| raw_decrypted: Option<String>, |
| frames: Option<Vec<QuicFrame>>, |
| }, |
| |
| PacketSent { |
| packet_type: PacketType, |
| header: PacketHeader, |
| // `frames` is defined here in the QLog schema specification. However, |
| // our streaming serializer requires serde to put the object at the end, |
| // so we define it there and depend on serde's preserve_order feature. |
| is_coalesced: Option<bool>, |
| |
| raw_encrypted: Option<String>, |
| raw_decrypted: Option<String>, |
| frames: Option<Vec<QuicFrame>>, |
| }, |
| |
| PacketDropped { |
| packet_type: Option<PacketType>, |
| packet_size: Option<u64>, |
| |
| raw: Option<String>, |
| }, |
| |
| PacketBuffered { |
| packet_type: PacketType, |
| packet_number: String, |
| }, |
| |
| StreamStateUpdated { |
| stream_id: String, |
| stream_type: Option<StreamType>, |
| |
| old: Option<StreamState>, |
| new: StreamState, |
| |
| stream_side: Option<StreamSide>, |
| }, |
| |
| FramesProcessed { |
| frames: Vec<QuicFrame>, |
| }, |
| |
| // ================================================================== // |
| // RECOVERY |
| RecoveryParametersSet { |
| reordering_threshold: Option<u64>, |
| time_threshold: Option<u64>, |
| timer_granularity: Option<u64>, |
| initial_rtt: Option<u64>, |
| |
| max_datagram_size: Option<u64>, |
| initial_congestion_window: Option<u64>, |
| minimum_congestion_window: Option<u64>, |
| loss_reduction_factor: Option<u64>, |
| persistent_congestion_threshold: Option<u64>, |
| }, |
| |
| MetricsUpdated { |
| min_rtt: Option<u64>, |
| smoothed_rtt: Option<u64>, |
| latest_rtt: Option<u64>, |
| rtt_variance: Option<u64>, |
| |
| max_ack_delay: Option<u64>, |
| pto_count: Option<u64>, |
| |
| congestion_window: Option<u64>, |
| bytes_in_flight: Option<u64>, |
| |
| ssthresh: Option<u64>, |
| |
| // qlog defined |
| packets_in_flight: Option<u64>, |
| in_recovery: Option<bool>, |
| |
| pacing_rate: Option<u64>, |
| }, |
| |
| CongestionStateUpdated { |
| old: Option<String>, |
| new: String, |
| }, |
| |
| LossTimerSet { |
| timer_type: Option<TimerType>, |
| timeout: Option<String>, |
| }, |
| |
| PacketLost { |
| packet_type: PacketType, |
| packet_number: String, |
| |
| header: Option<PacketHeader>, |
| frames: Vec<QuicFrame>, |
| }, |
| |
| MarkedForRetransmit { |
| frames: Vec<QuicFrame>, |
| }, |
| |
| // ================================================================== // |
| // HTTP/3 |
| H3ParametersSet { |
| owner: Option<H3Owner>, |
| |
| max_header_list_size: Option<u64>, |
| max_table_capacity: Option<u64>, |
| blocked_streams_count: Option<u64>, |
| |
| push_allowed: Option<bool>, |
| |
| waits_for_settings: Option<bool>, |
| }, |
| |
| H3StreamTypeSet { |
| stream_id: String, |
| owner: Option<H3Owner>, |
| |
| old: Option<H3StreamType>, |
| new: H3StreamType, |
| }, |
| |
| H3FrameCreated { |
| stream_id: String, |
| frame: Http3Frame, |
| length: Option<String>, |
| |
| raw: Option<String>, |
| }, |
| |
| H3FrameParsed { |
| stream_id: String, |
| frame: Http3Frame, |
| length: Option<String>, |
| |
| raw: Option<String>, |
| }, |
| |
| H3DataMoved { |
| stream_id: String, |
| offset: Option<String>, |
| length: Option<u64>, |
| |
| from: Option<H3DataRecipient>, |
| to: Option<H3DataRecipient>, |
| |
| raw: Option<String>, |
| }, |
| |
| H3PushResolved { |
| push_id: Option<String>, |
| stream_id: Option<String>, |
| |
| decision: Option<H3PushDecision>, |
| }, |
| |
| // ================================================================== // |
| // QPACK |
| QpackStateUpdated { |
| owner: Option<QpackOwner>, |
| |
| dynamic_table_capacity: Option<u64>, |
| dynamic_table_size: Option<u64>, |
| |
| known_received_count: Option<u64>, |
| current_insert_count: Option<u64>, |
| }, |
| |
| QpackStreamStateUpdated { |
| stream_id: String, |
| |
| state: QpackStreamState, |
| }, |
| |
| QpackDynamicTableUpdated { |
| update_type: QpackUpdateType, |
| |
| entries: Vec<QpackDynamicTableEntry>, |
| }, |
| |
| QpackHeadersEncoded { |
| stream_id: Option<String>, |
| |
| headers: Option<HttpHeader>, |
| |
| block_prefix: QpackHeaderBlockPrefix, |
| header_block: Vec<QpackHeaderBlockRepresentation>, |
| |
| raw: Option<String>, |
| }, |
| |
| QpackHeadersDecoded { |
| stream_id: Option<String>, |
| |
| headers: Option<HttpHeader>, |
| |
| block_prefix: QpackHeaderBlockPrefix, |
| header_block: Vec<QpackHeaderBlockRepresentation>, |
| |
| raw: Option<String>, |
| }, |
| |
| QpackInstructionSent { |
| instruction: QPackInstruction, |
| length: Option<String>, |
| |
| raw: Option<String>, |
| }, |
| |
| QpackInstructionReceived { |
| instruction: QPackInstruction, |
| length: Option<String>, |
| |
| raw: Option<String>, |
| }, |
| |
| // ================================================================== // |
| // Generic |
| ConnectionError { |
| code: Option<ConnectionErrorCode>, |
| description: Option<String>, |
| }, |
| |
| ApplicationError { |
| code: Option<ApplicationErrorCode>, |
| description: Option<String>, |
| }, |
| |
| InternalError { |
| code: Option<u64>, |
| description: Option<String>, |
| }, |
| |
| InternalWarning { |
| code: Option<u64>, |
| description: Option<String>, |
| }, |
| |
| Message { |
| message: String, |
| }, |
| |
| Marker { |
| marker_type: String, |
| message: Option<String>, |
| }, |
| } |
| |
| impl EventData { |
| /// Returns size of `EventData` array of `QuicFrame`s if it exists. |
| pub fn contains_quic_frames(&self) -> Option<usize> { |
| // For some EventData variants, the frame array is optional |
| // but for others it is mandatory. |
| match self { |
| EventData::PacketSent { frames, .. } | |
| EventData::PacketReceived { frames, .. } => |
| frames.as_ref().map(|f| f.len()), |
| |
| EventData::PacketLost { frames, .. } | |
| EventData::MarkedForRetransmit { frames } | |
| EventData::FramesProcessed { frames } => Some(frames.len()), |
| |
| _ => None, |
| } |
| } |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum PacketType { |
| Initial, |
| Handshake, |
| |
| #[serde(rename = "0RTT")] |
| ZeroRtt, |
| |
| #[serde(rename = "1RTT")] |
| OneRtt, |
| |
| Retry, |
| VersionNegotiation, |
| Unknown, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum Http3EventType { |
| ParametersSet, |
| StreamTypeSet, |
| FrameCreated, |
| FrameParsed, |
| DataMoved, |
| PushResolved, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum QpackEventType { |
| StateUpdated, |
| StreamStateUpdated, |
| DynamicTableUpdated, |
| HeadersEncoded, |
| HeadersDecoded, |
| InstructionSent, |
| InstructionReceived, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum QuicFrameTypeName { |
| Padding, |
| Ping, |
| Ack, |
| ResetStream, |
| StopSending, |
| Crypto, |
| NewToken, |
| Stream, |
| MaxData, |
| MaxStreamData, |
| MaxStreams, |
| DataBlocked, |
| StreamDataBlocked, |
| StreamsBlocked, |
| NewConnectionId, |
| RetireConnectionId, |
| PathChallenge, |
| PathResponse, |
| ConnectionClose, |
| ApplicationClose, |
| HandshakeDone, |
| Datagram, |
| Unknown, |
| } |
| |
| // TODO: search for pub enum Error { to see how best to encode errors in qlog. |
| #[serde_with::skip_serializing_none] |
| #[derive(Clone, Serialize)] |
| pub struct PacketHeader { |
| pub packet_number: String, |
| pub packet_size: Option<u64>, |
| pub payload_length: Option<u64>, |
| pub version: Option<String>, |
| pub scil: Option<String>, |
| pub dcil: Option<String>, |
| pub scid: Option<String>, |
| pub dcid: Option<String>, |
| } |
| |
| impl PacketHeader { |
| /// Creates a new PacketHeader. |
| pub fn new( |
| packet_number: u64, packet_size: Option<u64>, |
| payload_length: Option<u64>, version: Option<u32>, scid: Option<&[u8]>, |
| dcid: Option<&[u8]>, |
| ) -> Self { |
| let (scil, scid) = match scid { |
| Some(cid) => ( |
| Some(cid.len().to_string()), |
| Some(format!("{}", HexSlice::new(&cid))), |
| ), |
| |
| None => (None, None), |
| }; |
| |
| let (dcil, dcid) = match dcid { |
| Some(cid) => ( |
| Some(cid.len().to_string()), |
| Some(format!("{}", HexSlice::new(&cid))), |
| ), |
| |
| None => (None, None), |
| }; |
| |
| let version = version.map(|v| format!("{:x?}", v)); |
| |
| PacketHeader { |
| packet_number: packet_number.to_string(), |
| packet_size, |
| payload_length, |
| version, |
| scil, |
| dcil, |
| scid, |
| dcid, |
| } |
| } |
| |
| /// Creates a new PacketHeader. |
| /// |
| /// Once a QUIC connection has formed, version, dcid and scid are stable, so |
| /// there are space benefits to not logging them in every packet, especially |
| /// PacketType::OneRtt. |
| pub fn with_type( |
| ty: PacketType, packet_number: u64, packet_size: Option<u64>, |
| payload_length: Option<u64>, version: Option<u32>, scid: Option<&[u8]>, |
| dcid: Option<&[u8]>, |
| ) -> Self { |
| match ty { |
| PacketType::OneRtt => PacketHeader::new( |
| packet_number, |
| packet_size, |
| payload_length, |
| None, |
| None, |
| None, |
| ), |
| |
| _ => PacketHeader::new( |
| packet_number, |
| packet_size, |
| payload_length, |
| version, |
| scid, |
| dcid, |
| ), |
| } |
| } |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum StreamType { |
| Bidirectional, |
| Unidirectional, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum ErrorSpace { |
| TransportError, |
| ApplicationError, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum GenericEventType { |
| ConnectionError, |
| ApplicationError, |
| InternalError, |
| InternalWarning, |
| |
| Message, |
| Marker, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(untagged)] |
| pub enum ConnectionErrorCode { |
| TransportError(TransportError), |
| CryptoError(CryptoError), |
| Value(u64), |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(untagged)] |
| pub enum ApplicationErrorCode { |
| ApplicationError(ApplicationError), |
| Value(u64), |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum TransportError { |
| NoError, |
| InternalError, |
| ServerBusy, |
| FlowControlError, |
| StreamLimitError, |
| StreamStateError, |
| FinalSizeError, |
| FrameEncodingError, |
| TransportParameterError, |
| ProtocolViolation, |
| InvalidMigration, |
| CryptoBufferExceeded, |
| Unknown, |
| } |
| |
| // TODO |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum CryptoError { |
| Prefix, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum ApplicationError { |
| HttpNoError, |
| HttpGeneralProtocolError, |
| HttpInternalError, |
| HttpRequestCancelled, |
| HttpIncompleteRequest, |
| HttpConnectError, |
| HttpFrameError, |
| HttpExcessiveLoad, |
| HttpVersionFallback, |
| HttpIdError, |
| HttpStreamCreationError, |
| HttpClosedCriticalStream, |
| HttpEarlyResponse, |
| HttpMissingSettings, |
| HttpUnexpectedFrame, |
| HttpRequestRejection, |
| HttpSettingsError, |
| Unknown, |
| } |
| |
| #[serde_with::skip_serializing_none] |
| #[derive(Serialize, Clone)] |
| #[serde(untagged)] |
| pub enum QuicFrame { |
| Padding { |
| frame_type: QuicFrameTypeName, |
| }, |
| |
| Ping { |
| frame_type: QuicFrameTypeName, |
| }, |
| |
| Ack { |
| frame_type: QuicFrameTypeName, |
| ack_delay: Option<String>, |
| acked_ranges: Option<Vec<(u64, u64)>>, |
| |
| ect1: Option<String>, |
| |
| ect0: Option<String>, |
| |
| ce: Option<String>, |
| }, |
| |
| ResetStream { |
| frame_type: QuicFrameTypeName, |
| stream_id: String, |
| error_code: u64, |
| final_size: String, |
| }, |
| |
| StopSending { |
| frame_type: QuicFrameTypeName, |
| stream_id: String, |
| error_code: u64, |
| }, |
| |
| Crypto { |
| frame_type: QuicFrameTypeName, |
| offset: String, |
| length: String, |
| }, |
| |
| NewToken { |
| frame_type: QuicFrameTypeName, |
| length: String, |
| token: String, |
| }, |
| |
| Stream { |
| frame_type: QuicFrameTypeName, |
| stream_id: String, |
| offset: String, |
| length: String, |
| fin: bool, |
| |
| raw: Option<String>, |
| }, |
| |
| MaxData { |
| frame_type: QuicFrameTypeName, |
| maximum: String, |
| }, |
| |
| MaxStreamData { |
| frame_type: QuicFrameTypeName, |
| stream_id: String, |
| maximum: String, |
| }, |
| |
| MaxStreams { |
| frame_type: QuicFrameTypeName, |
| stream_type: StreamType, |
| maximum: String, |
| }, |
| |
| DataBlocked { |
| frame_type: QuicFrameTypeName, |
| limit: String, |
| }, |
| |
| StreamDataBlocked { |
| frame_type: QuicFrameTypeName, |
| stream_id: String, |
| limit: String, |
| }, |
| |
| StreamsBlocked { |
| frame_type: QuicFrameTypeName, |
| stream_type: StreamType, |
| limit: String, |
| }, |
| |
| NewConnectionId { |
| frame_type: QuicFrameTypeName, |
| sequence_number: String, |
| retire_prior_to: String, |
| length: u64, |
| connection_id: String, |
| reset_token: String, |
| }, |
| |
| RetireConnectionId { |
| frame_type: QuicFrameTypeName, |
| sequence_number: String, |
| }, |
| |
| PathChallenge { |
| frame_type: QuicFrameTypeName, |
| |
| data: Option<String>, |
| }, |
| |
| PathResponse { |
| frame_type: QuicFrameTypeName, |
| |
| data: Option<String>, |
| }, |
| |
| ConnectionClose { |
| frame_type: QuicFrameTypeName, |
| error_space: ErrorSpace, |
| error_code: u64, |
| raw_error_code: u64, |
| reason: String, |
| |
| trigger_frame_type: Option<String>, |
| }, |
| |
| HandshakeDone { |
| frame_type: QuicFrameTypeName, |
| }, |
| |
| Datagram { |
| frame_type: QuicFrameTypeName, |
| length: String, |
| |
| raw: Option<String>, |
| }, |
| |
| Unknown { |
| frame_type: QuicFrameTypeName, |
| raw_frame_type: u64, |
| }, |
| } |
| |
| impl QuicFrame { |
| pub fn padding() -> Self { |
| QuicFrame::Padding { |
| frame_type: QuicFrameTypeName::Padding, |
| } |
| } |
| |
| pub fn ping() -> Self { |
| QuicFrame::Ping { |
| frame_type: QuicFrameTypeName::Ping, |
| } |
| } |
| |
| pub fn ack( |
| ack_delay: Option<String>, acked_ranges: Option<Vec<(u64, u64)>>, |
| ect1: Option<String>, ect0: Option<String>, ce: Option<String>, |
| ) -> Self { |
| QuicFrame::Ack { |
| frame_type: QuicFrameTypeName::Ack, |
| ack_delay, |
| acked_ranges, |
| ect1, |
| ect0, |
| ce, |
| } |
| } |
| |
| pub fn reset_stream( |
| stream_id: String, error_code: u64, final_size: String, |
| ) -> Self { |
| QuicFrame::ResetStream { |
| frame_type: QuicFrameTypeName::ResetStream, |
| stream_id, |
| error_code, |
| final_size, |
| } |
| } |
| |
| pub fn stop_sending(stream_id: String, error_code: u64) -> Self { |
| QuicFrame::StopSending { |
| frame_type: QuicFrameTypeName::StopSending, |
| stream_id, |
| error_code, |
| } |
| } |
| |
| pub fn crypto(offset: String, length: String) -> Self { |
| QuicFrame::Crypto { |
| frame_type: QuicFrameTypeName::Crypto, |
| offset, |
| length, |
| } |
| } |
| |
| pub fn new_token(length: String, token: String) -> Self { |
| QuicFrame::NewToken { |
| frame_type: QuicFrameTypeName::NewToken, |
| length, |
| token, |
| } |
| } |
| |
| pub fn stream( |
| stream_id: String, offset: String, length: String, fin: bool, |
| raw: Option<String>, |
| ) -> Self { |
| QuicFrame::Stream { |
| frame_type: QuicFrameTypeName::Stream, |
| stream_id, |
| offset, |
| length, |
| fin, |
| raw, |
| } |
| } |
| |
| pub fn max_data(maximum: String) -> Self { |
| QuicFrame::MaxData { |
| frame_type: QuicFrameTypeName::MaxData, |
| maximum, |
| } |
| } |
| |
| pub fn max_stream_data(stream_id: String, maximum: String) -> Self { |
| QuicFrame::MaxStreamData { |
| frame_type: QuicFrameTypeName::MaxStreamData, |
| stream_id, |
| maximum, |
| } |
| } |
| |
| pub fn max_streams(stream_type: StreamType, maximum: String) -> Self { |
| QuicFrame::MaxStreams { |
| frame_type: QuicFrameTypeName::MaxStreams, |
| stream_type, |
| maximum, |
| } |
| } |
| |
| pub fn data_blocked(limit: String) -> Self { |
| QuicFrame::DataBlocked { |
| frame_type: QuicFrameTypeName::DataBlocked, |
| limit, |
| } |
| } |
| |
| pub fn stream_data_blocked(stream_id: String, limit: String) -> Self { |
| QuicFrame::StreamDataBlocked { |
| frame_type: QuicFrameTypeName::StreamDataBlocked, |
| stream_id, |
| limit, |
| } |
| } |
| |
| pub fn streams_blocked(stream_type: StreamType, limit: String) -> Self { |
| QuicFrame::StreamsBlocked { |
| frame_type: QuicFrameTypeName::StreamsBlocked, |
| stream_type, |
| limit, |
| } |
| } |
| |
| pub fn new_connection_id( |
| sequence_number: String, retire_prior_to: String, length: u64, |
| connection_id: String, reset_token: String, |
| ) -> Self { |
| QuicFrame::NewConnectionId { |
| frame_type: QuicFrameTypeName::NewConnectionId, |
| sequence_number, |
| retire_prior_to, |
| length, |
| connection_id, |
| reset_token, |
| } |
| } |
| |
| pub fn retire_connection_id(sequence_number: String) -> Self { |
| QuicFrame::RetireConnectionId { |
| frame_type: QuicFrameTypeName::RetireConnectionId, |
| sequence_number, |
| } |
| } |
| |
| pub fn path_challenge(data: Option<String>) -> Self { |
| QuicFrame::PathChallenge { |
| frame_type: QuicFrameTypeName::PathChallenge, |
| data, |
| } |
| } |
| |
| pub fn path_response(data: Option<String>) -> Self { |
| QuicFrame::PathResponse { |
| frame_type: QuicFrameTypeName::PathResponse, |
| data, |
| } |
| } |
| |
| pub fn connection_close( |
| error_space: ErrorSpace, error_code: u64, raw_error_code: u64, |
| reason: String, trigger_frame_type: Option<String>, |
| ) -> Self { |
| QuicFrame::ConnectionClose { |
| frame_type: QuicFrameTypeName::ConnectionClose, |
| error_space, |
| error_code, |
| raw_error_code, |
| reason, |
| trigger_frame_type, |
| } |
| } |
| |
| pub fn handshake_done() -> Self { |
| QuicFrame::HandshakeDone { |
| frame_type: QuicFrameTypeName::HandshakeDone, |
| } |
| } |
| |
| pub fn datagram(length: String, raw: Option<String>) -> Self { |
| QuicFrame::Datagram { |
| frame_type: QuicFrameTypeName::Datagram, |
| length, |
| raw, |
| } |
| } |
| |
| pub fn unknown(raw_frame_type: u64) -> Self { |
| QuicFrame::Unknown { |
| frame_type: QuicFrameTypeName::Unknown, |
| raw_frame_type, |
| } |
| } |
| } |
| |
| // ================================================================== // |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum Http3FrameTypeName { |
| Data, |
| Headers, |
| CancelPush, |
| Settings, |
| PushPromise, |
| Goaway, |
| MaxPushId, |
| DuplicatePush, |
| Reserved, |
| Unknown, |
| } |
| |
| #[derive(Serialize, Clone)] |
| pub struct HttpHeader { |
| pub name: String, |
| pub value: String, |
| } |
| |
| #[derive(Serialize, Clone)] |
| pub struct Setting { |
| pub name: String, |
| pub value: String, |
| } |
| |
| #[serde_with::skip_serializing_none] |
| #[derive(Serialize, Clone)] |
| #[serde(untagged)] |
| pub enum Http3Frame { |
| Data { |
| frame_type: Http3FrameTypeName, |
| |
| raw: Option<String>, |
| }, |
| |
| Headers { |
| frame_type: Http3FrameTypeName, |
| headers: Vec<HttpHeader>, |
| }, |
| |
| CancelPush { |
| frame_type: Http3FrameTypeName, |
| push_id: String, |
| }, |
| |
| Settings { |
| frame_type: Http3FrameTypeName, |
| settings: Vec<Setting>, |
| }, |
| |
| PushPromise { |
| frame_type: Http3FrameTypeName, |
| push_id: String, |
| headers: Vec<HttpHeader>, |
| }, |
| |
| Goaway { |
| frame_type: Http3FrameTypeName, |
| stream_id: String, |
| }, |
| |
| MaxPushId { |
| frame_type: Http3FrameTypeName, |
| push_id: String, |
| }, |
| |
| DuplicatePush { |
| frame_type: Http3FrameTypeName, |
| push_id: String, |
| }, |
| |
| Reserved { |
| frame_type: Http3FrameTypeName, |
| }, |
| |
| Unknown { |
| frame_type: Http3FrameTypeName, |
| }, |
| } |
| |
| impl Http3Frame { |
| pub fn data(raw: Option<String>) -> Self { |
| Http3Frame::Data { |
| frame_type: Http3FrameTypeName::Data, |
| raw, |
| } |
| } |
| |
| pub fn headers(headers: Vec<HttpHeader>) -> Self { |
| Http3Frame::Headers { |
| frame_type: Http3FrameTypeName::Headers, |
| headers, |
| } |
| } |
| |
| pub fn cancel_push(push_id: String) -> Self { |
| Http3Frame::CancelPush { |
| frame_type: Http3FrameTypeName::CancelPush, |
| push_id, |
| } |
| } |
| |
| pub fn settings(settings: Vec<Setting>) -> Self { |
| Http3Frame::Settings { |
| frame_type: Http3FrameTypeName::Settings, |
| settings, |
| } |
| } |
| |
| pub fn push_promise(push_id: String, headers: Vec<HttpHeader>) -> Self { |
| Http3Frame::PushPromise { |
| frame_type: Http3FrameTypeName::PushPromise, |
| push_id, |
| headers, |
| } |
| } |
| |
| pub fn goaway(stream_id: String) -> Self { |
| Http3Frame::Goaway { |
| frame_type: Http3FrameTypeName::Goaway, |
| stream_id, |
| } |
| } |
| |
| pub fn max_push_id(push_id: String) -> Self { |
| Http3Frame::MaxPushId { |
| frame_type: Http3FrameTypeName::MaxPushId, |
| push_id, |
| } |
| } |
| |
| pub fn duplicate_push(push_id: String) -> Self { |
| Http3Frame::DuplicatePush { |
| frame_type: Http3FrameTypeName::DuplicatePush, |
| push_id, |
| } |
| } |
| |
| pub fn reserved() -> Self { |
| Http3Frame::Reserved { |
| frame_type: Http3FrameTypeName::Reserved, |
| } |
| } |
| |
| pub fn unknown() -> Self { |
| Http3Frame::Unknown { |
| frame_type: Http3FrameTypeName::Unknown, |
| } |
| } |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum QpackInstructionTypeName { |
| SetDynamicTableCapacityInstruction, |
| InsertWithNameReferenceInstruction, |
| InsertWithoutNameReferenceInstruction, |
| DuplicateInstruction, |
| HeaderAcknowledgementInstruction, |
| StreamCancellationInstruction, |
| InsertCountIncrementInstruction, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum QpackTableType { |
| Static, |
| Dynamic, |
| } |
| |
| #[derive(Serialize, Clone)] |
| pub enum QPackInstruction { |
| SetDynamicTableCapacityInstruction { |
| instruction_type: QpackInstructionTypeName, |
| |
| capacity: u64, |
| }, |
| |
| InsertWithNameReferenceInstruction { |
| instruction_type: QpackInstructionTypeName, |
| |
| table_type: QpackTableType, |
| |
| name_index: u64, |
| |
| huffman_encoded_value: bool, |
| value_length: u64, |
| value: String, |
| }, |
| |
| InsertWithoutNameReferenceInstruction { |
| instruction_type: QpackInstructionTypeName, |
| |
| huffman_encoded_name: bool, |
| name_length: u64, |
| name: String, |
| |
| huffman_encoded_value: bool, |
| value_length: u64, |
| value: String, |
| }, |
| |
| DuplicateInstruction { |
| instruction_type: QpackInstructionTypeName, |
| |
| index: u64, |
| }, |
| |
| HeaderAcknowledgementInstruction { |
| instruction_type: QpackInstructionTypeName, |
| |
| stream_id: String, |
| }, |
| |
| StreamCancellationInstruction { |
| instruction_type: QpackInstructionTypeName, |
| |
| stream_id: String, |
| }, |
| |
| InsertCountIncrementInstruction { |
| instruction_type: QpackInstructionTypeName, |
| |
| increment: u64, |
| }, |
| } |
| |
| #[derive(Serialize, Clone)] |
| #[serde(rename_all = "snake_case")] |
| pub enum QpackHeaderBlockRepresentationTypeName { |
| IndexedHeaderField, |
| LiteralHeaderFieldWithName, |
| LiteralHeaderFieldWithoutName, |
| } |
| |
| #[derive(Serialize, Clone)] |
| pub enum QpackHeaderBlockRepresentation { |
| IndexedHeaderField { |
| header_field_type: QpackHeaderBlockRepresentationTypeName, |
| |
| table_type: QpackTableType, |
| index: u64, |
| |
| is_post_base: Option<bool>, |
| }, |
| |
| LiteralHeaderFieldWithName { |
| header_field_type: QpackHeaderBlockRepresentationTypeName, |
| |
| preserve_literal: bool, |
| table_type: QpackTableType, |
| name_index: u64, |
| |
| huffman_encoded_value: bool, |
| value_length: u64, |
| value: String, |
| |
| is_post_base: Option<bool>, |
| }, |
| |
| LiteralHeaderFieldWithoutName { |
| header_field_type: QpackHeaderBlockRepresentationTypeName, |
| |
| preserve_literal: bool, |
| table_type: QpackTableType, |
| name_index: u64, |
| |
| huffman_encoded_name: bool, |
| name_length: u64, |
| name: String, |
| |
| huffman_encoded_value: bool, |
| value_length: u64, |
| value: String, |
| |
| is_post_base: Option<bool>, |
| }, |
| } |
| |
| pub struct HexSlice<'a>(&'a [u8]); |
| |
| impl<'a> HexSlice<'a> { |
| pub fn new<T>(data: &'a T) -> HexSlice<'a> |
| where |
| T: ?Sized + AsRef<[u8]> + 'a, |
| { |
| HexSlice(data.as_ref()) |
| } |
| |
| pub fn maybe_string<T>(data: Option<&'a T>) -> Option<String> |
| where |
| T: ?Sized + AsRef<[u8]> + 'a, |
| { |
| data.map(|d| format!("{}", HexSlice::new(d))) |
| } |
| } |
| |
| impl<'a> std::fmt::Display for HexSlice<'a> { |
| fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { |
| for byte in self.0 { |
| write!(f, "{:02x}", byte)?; |
| } |
| Ok(()) |
| } |
| } |
| |
| #[doc(hidden)] |
| pub mod testing { |
| use super::*; |
| |
| pub fn make_pkt_hdr() -> PacketHeader { |
| let scid = [0x7e, 0x37, 0xe4, 0xdc, 0xc6, 0x68, 0x2d, 0xa8]; |
| let dcid = [0x36, 0xce, 0x10, 0x4e, 0xee, 0x50, 0x10, 0x1c]; |
| |
| PacketHeader::new( |
| 0, |
| Some(1251), |
| Some(1224), |
| Some(0xff00_0018), |
| Some(&scid), |
| Some(&dcid), |
| ) |
| } |
| |
| pub fn make_trace() -> Trace { |
| Trace::new( |
| VantagePoint { |
| name: None, |
| ty: VantagePointType::Server, |
| flow: None, |
| }, |
| Some("Quiche qlog trace".to_string()), |
| Some("Quiche qlog trace description".to_string()), |
| Some(Configuration { |
| time_offset: Some("0".to_string()), |
| time_units: Some(TimeUnits::Ms), |
| original_uris: None, |
| }), |
| None, |
| ) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use testing::*; |
| |
| #[test] |
| fn packet_header() { |
| let pkt_hdr = make_pkt_hdr(); |
| |
| let log_string = r#"{ |
| "packet_number": "0", |
| "packet_size": 1251, |
| "payload_length": 1224, |
| "version": "ff000018", |
| "scil": "8", |
| "dcil": "8", |
| "scid": "7e37e4dcc6682da8", |
| "dcid": "36ce104eee50101c" |
| }"#; |
| |
| assert_eq!(serde_json::to_string_pretty(&pkt_hdr).unwrap(), log_string); |
| } |
| |
| #[test] |
| fn packet_sent_event_no_frames() { |
| let log_string = r#"{ |
| "packet_type": "initial", |
| "header": { |
| "packet_number": "0", |
| "packet_size": 1251, |
| "payload_length": 1224, |
| "version": "ff00001b", |
| "scil": "8", |
| "dcil": "8", |
| "scid": "7e37e4dcc6682da8", |
| "dcid": "36ce104eee50101c" |
| } |
| }"#; |
| |
| let scid = [0x7e, 0x37, 0xe4, 0xdc, 0xc6, 0x68, 0x2d, 0xa8]; |
| let dcid = [0x36, 0xce, 0x10, 0x4e, 0xee, 0x50, 0x10, 0x1c]; |
| let pkt_hdr = PacketHeader::new( |
| 0, |
| Some(1251), |
| Some(1224), |
| Some(0xff00001b), |
| Some(&scid), |
| Some(&dcid), |
| ); |
| |
| let pkt_sent_evt = EventData::PacketSent { |
| raw_encrypted: None, |
| raw_decrypted: None, |
| packet_type: PacketType::Initial, |
| header: pkt_hdr.clone(), |
| frames: None, |
| is_coalesced: None, |
| }; |
| |
| assert_eq!( |
| serde_json::to_string_pretty(&pkt_sent_evt).unwrap(), |
| log_string |
| ); |
| } |
| |
| #[test] |
| fn packet_sent_event_some_frames() { |
| let log_string = r#"{ |
| "packet_type": "initial", |
| "header": { |
| "packet_number": "0", |
| "packet_size": 1251, |
| "payload_length": 1224, |
| "version": "ff000018", |
| "scil": "8", |
| "dcil": "8", |
| "scid": "7e37e4dcc6682da8", |
| "dcid": "36ce104eee50101c" |
| }, |
| "frames": [ |
| { |
| "frame_type": "padding" |
| }, |
| { |
| "frame_type": "ping" |
| }, |
| { |
| "frame_type": "stream", |
| "stream_id": "0", |
| "offset": "0", |
| "length": "100", |
| "fin": true |
| } |
| ] |
| }"#; |
| |
| let pkt_hdr = make_pkt_hdr(); |
| |
| let mut frames = Vec::new(); |
| frames.push(QuicFrame::padding()); |
| |
| frames.push(QuicFrame::ping()); |
| |
| frames.push(QuicFrame::stream( |
| "0".to_string(), |
| "0".to_string(), |
| "100".to_string(), |
| true, |
| None, |
| )); |
| |
| let pkt_sent_evt = EventData::PacketSent { |
| raw_encrypted: None, |
| raw_decrypted: None, |
| packet_type: PacketType::Initial, |
| header: pkt_hdr.clone(), |
| frames: Some(frames), |
| is_coalesced: None, |
| }; |
| |
| assert_eq!( |
| serde_json::to_string_pretty(&pkt_sent_evt).unwrap(), |
| log_string |
| ); |
| } |
| |
| #[test] |
| fn trace_no_events() { |
| let log_string = r#"{ |
| "vantage_point": { |
| "type": "server" |
| }, |
| "title": "Quiche qlog trace", |
| "description": "Quiche qlog trace description", |
| "configuration": { |
| "time_units": "ms", |
| "time_offset": "0" |
| }, |
| "event_fields": [ |
| "relative_time", |
| "category", |
| "event", |
| "data" |
| ], |
| "events": [] |
| }"#; |
| |
| let trace = make_trace(); |
| |
| assert_eq!(serde_json::to_string_pretty(&trace).unwrap(), log_string); |
| } |
| |
| #[test] |
| fn trace_single_transport_event() { |
| let log_string = r#"{ |
| "vantage_point": { |
| "type": "server" |
| }, |
| "title": "Quiche qlog trace", |
| "description": "Quiche qlog trace description", |
| "configuration": { |
| "time_units": "ms", |
| "time_offset": "0" |
| }, |
| "event_fields": [ |
| "relative_time", |
| "category", |
| "event", |
| "data" |
| ], |
| "events": [ |
| [ |
| "0", |
| "transport", |
| "packet_sent", |
| { |
| "packet_type": "initial", |
| "header": { |
| "packet_number": "0", |
| "packet_size": 1251, |
| "payload_length": 1224, |
| "version": "ff000018", |
| "scil": "8", |
| "dcil": "8", |
| "scid": "7e37e4dcc6682da8", |
| "dcid": "36ce104eee50101c" |
| }, |
| "frames": [ |
| { |
| "frame_type": "stream", |
| "stream_id": "0", |
| "offset": "0", |
| "length": "100", |
| "fin": true |
| } |
| ] |
| } |
| ] |
| ] |
| }"#; |
| |
| let mut trace = make_trace(); |
| |
| let pkt_hdr = make_pkt_hdr(); |
| |
| let frames = vec![QuicFrame::stream( |
| "0".to_string(), |
| "0".to_string(), |
| "100".to_string(), |
| true, |
| None, |
| )]; |
| let event = event::Event::packet_sent_min( |
| PacketType::Initial, |
| pkt_hdr, |
| Some(frames), |
| ); |
| |
| trace.push_event(std::time::Duration::new(0, 0), event); |
| |
| assert_eq!(serde_json::to_string_pretty(&trace).unwrap(), log_string); |
| } |
| |
| #[test] |
| fn test_event_validity() { |
| // Test a single event in each category |
| |
| let ev = event::Event::server_listening_min(443, 443); |
| assert!(ev.is_valid()); |
| |
| let ev = event::Event::transport_parameters_set_min(); |
| assert!(ev.is_valid()); |
| |
| let ev = event::Event::recovery_parameters_set_min(); |
| assert!(ev.is_valid()); |
| |
| let ev = event::Event::h3_parameters_set_min(); |
| assert!(ev.is_valid()); |
| |
| let ev = event::Event::qpack_state_updated_min(); |
| assert!(ev.is_valid()); |
| |
| let ev = event::Event { |
| category: EventCategory::Error, |
| ty: EventType::GenericEventType(GenericEventType::ConnectionError), |
| data: EventData::ConnectionError { |
| code: None, |
| description: None, |
| }, |
| }; |
| |
| assert!(ev.is_valid()); |
| } |
| |
| #[test] |
| fn bogus_event_validity() { |
| // Test a single event in each category |
| |
| let mut ev = event::Event::server_listening_min(443, 443); |
| ev.category = EventCategory::Simulation; |
| assert!(!ev.is_valid()); |
| |
| let mut ev = event::Event::transport_parameters_set_min(); |
| ev.category = EventCategory::Simulation; |
| assert!(!ev.is_valid()); |
| |
| let mut ev = event::Event::recovery_parameters_set_min(); |
| ev.category = EventCategory::Simulation; |
| assert!(!ev.is_valid()); |
| |
| let mut ev = event::Event::h3_parameters_set_min(); |
| ev.category = EventCategory::Simulation; |
| assert!(!ev.is_valid()); |
| |
| let mut ev = event::Event::qpack_state_updated_min(); |
| ev.category = EventCategory::Simulation; |
| assert!(!ev.is_valid()); |
| |
| let ev = event::Event { |
| category: EventCategory::Error, |
| ty: EventType::GenericEventType(GenericEventType::ConnectionError), |
| data: EventData::FramesProcessed { frames: Vec::new() }, |
| }; |
| |
| assert!(!ev.is_valid()); |
| } |
| |
| #[test] |
| fn serialization_states() { |
| let v: Vec<u8> = Vec::new(); |
| let buff = std::io::Cursor::new(v); |
| let writer = Box::new(buff); |
| |
| let mut trace = make_trace(); |
| let pkt_hdr = make_pkt_hdr(); |
| |
| let frame1 = QuicFrame::stream( |
| "40".to_string(), |
| "40".to_string(), |
| "400".to_string(), |
| true, |
| None, |
| ); |
| |
| let event1 = event::Event::packet_sent_min( |
| PacketType::Handshake, |
| pkt_hdr.clone(), |
| Some(vec![frame1]), |
| ); |
| |
| trace.push_event(std::time::Duration::new(0, 0), event1); |
| |
| let frame2 = QuicFrame::stream( |
| "0".to_string(), |
| "0".to_string(), |
| "100".to_string(), |
| true, |
| None, |
| ); |
| |
| let frame3 = QuicFrame::stream( |
| "0".to_string(), |
| "0".to_string(), |
| "100".to_string(), |
| true, |
| None, |
| ); |
| |
| let event2 = event::Event::packet_sent_min( |
| PacketType::Initial, |
| pkt_hdr.clone(), |
| Some(Vec::new()), |
| ); |
| |
| let event3 = event::Event::packet_sent( |
| PacketType::Initial, |
| pkt_hdr, |
| Some(Vec::new()), |
| None, |
| Some("encrypted_foo".to_string()), |
| Some("decrypted_foo".to_string()), |
| ); |
| |
| let mut s = QlogStreamer::new( |
| "version".to_string(), |
| Some("title".to_string()), |
| Some("description".to_string()), |
| None, |
| std::time::Instant::now(), |
| trace, |
| writer, |
| ); |
| |
| // Before the log is started all other operations should fail. |
| assert!(match s.add_event(event2.clone()) { |
| Err(Error::InvalidState) => true, |
| _ => false, |
| }); |
| assert!(match s.add_frame(frame2.clone(), false) { |
| Err(Error::InvalidState) => true, |
| _ => false, |
| }); |
| assert!(match s.finish_frames() { |
| Err(Error::InvalidState) => true, |
| _ => false, |
| }); |
| assert!(match s.finish_log() { |
| Err(Error::InvalidState) => true, |
| _ => false, |
| }); |
| |
| // Once a log is started, can't write frames before an event. |
| assert!(match s.start_log() { |
| Ok(()) => true, |
| _ => false, |
| }); |
| assert!(match s.add_frame(frame2.clone(), true) { |
| Err(Error::InvalidState) => true, |
| _ => false, |
| }); |
| assert!(match s.finish_frames() { |
| Err(Error::InvalidState) => true, |
| _ => false, |
| }); |
| |
| // Some events hold frames; can't write any more events until frame |
| // writing is concluded. |
| assert!(match s.add_event(event2.clone()) { |
| Ok(true) => true, |
| _ => false, |
| }); |
| assert!(match s.add_event(event2.clone()) { |
| Err(Error::InvalidState) => true, |
| _ => false, |
| }); |
| |
| // While writing frames, can't write events. |
| assert!(match s.add_frame(frame2.clone(), false) { |
| Ok(()) => true, |
| _ => false, |
| }); |
| |
| assert!(match s.add_event(event2.clone()) { |
| Err(Error::InvalidState) => true, |
| _ => false, |
| }); |
| assert!(match s.finish_frames() { |
| Ok(()) => true, |
| _ => false, |
| }); |
| |
| // Adding an event that includes both frames and raw data should |
| // be allowed. |
| assert!(match s.add_event(event3.clone()) { |
| Ok(true) => true, |
| _ => false, |
| }); |
| assert!(match s.add_frame(frame3.clone(), false) { |
| Ok(()) => true, |
| _ => false, |
| }); |
| assert!(match s.finish_frames() { |
| Ok(()) => true, |
| _ => false, |
| }); |
| |
| // Adding an event with an external time should work too. |
| // For tests, it will resolve to 0 but we care about proving the API |
| // here, not timing specifics. |
| let now = std::time::Instant::now(); |
| |
| assert!(match s.add_event_with_instant(event3.clone(), now) { |
| Ok(true) => true, |
| _ => false, |
| }); |
| assert!(match s.add_frame(frame3.clone(), false) { |
| Ok(()) => true, |
| _ => false, |
| }); |
| assert!(match s.finish_frames() { |
| Ok(()) => true, |
| _ => false, |
| }); |
| |
| assert!(match s.finish_log() { |
| Ok(()) => true, |
| _ => false, |
| }); |
| |
| let r = s.writer(); |
| let w: &Box<std::io::Cursor<Vec<u8>>> = unsafe { std::mem::transmute(r) }; |
| |
| let log_string = r#"{"qlog_version":"version","title":"title","description":"description","traces":[{"vantage_point":{"type":"server"},"title":"Quiche qlog trace","description":"Quiche qlog trace description","configuration":{"time_units":"ms","time_offset":"0"},"event_fields":["relative_time","category","event","data"],"events":[["0","transport","packet_sent",{"packet_type":"handshake","header":{"packet_number":"0","packet_size":1251,"payload_length":1224,"version":"ff000018","scil":"8","dcil":"8","scid":"7e37e4dcc6682da8","dcid":"36ce104eee50101c"},"frames":[{"frame_type":"stream","stream_id":"40","offset":"40","length":"400","fin":true}]}],["0","transport","packet_sent",{"packet_type":"initial","header":{"packet_number":"0","packet_size":1251,"payload_length":1224,"version":"ff000018","scil":"8","dcil":"8","scid":"7e37e4dcc6682da8","dcid":"36ce104eee50101c"},"frames":[{"frame_type":"stream","stream_id":"0","offset":"0","length":"100","fin":true}]}],["0","transport","packet_sent",{"packet_type":"initial","header":{"packet_number":"0","packet_size":1251,"payload_length":1224,"version":"ff000018","scil":"8","dcil":"8","scid":"7e37e4dcc6682da8","dcid":"36ce104eee50101c"},"raw_encrypted":"encrypted_foo","raw_decrypted":"decrypted_foo","frames":[{"frame_type":"stream","stream_id":"0","offset":"0","length":"100","fin":true}]}],["0","transport","packet_sent",{"packet_type":"initial","header":{"packet_number":"0","packet_size":1251,"payload_length":1224,"version":"ff000018","scil":"8","dcil":"8","scid":"7e37e4dcc6682da8","dcid":"36ce104eee50101c"},"raw_encrypted":"encrypted_foo","raw_decrypted":"decrypted_foo","frames":[{"frame_type":"stream","stream_id":"0","offset":"0","length":"100","fin":true}]}]]}]}"#; |
| |
| let written_string = std::str::from_utf8(w.as_ref().get_ref()).unwrap(); |
| |
| assert_eq!(log_string, written_string); |
| } |
| } |
| |
| pub mod event; |