diff options
Diffstat (limited to 'third_party/rust/qlog')
-rw-r--r-- | third_party/rust/qlog/.cargo-checksum.json | 1 | ||||
-rw-r--r-- | third_party/rust/qlog/Cargo.toml | 36 | ||||
-rw-r--r-- | third_party/rust/qlog/README.md | 304 | ||||
-rw-r--r-- | third_party/rust/qlog/src/event.rs | 988 | ||||
-rw-r--r-- | third_party/rust/qlog/src/lib.rs | 2970 |
5 files changed, 4299 insertions, 0 deletions
diff --git a/third_party/rust/qlog/.cargo-checksum.json b/third_party/rust/qlog/.cargo-checksum.json new file mode 100644 index 0000000000..f0db87c5fe --- /dev/null +++ b/third_party/rust/qlog/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"3e92e34343c4777cb0249185b1f693a2e21321b090136be6d5287cadf353db1c","README.md":"c74fdbabf7262695e42a9af0b8c0c2f089c188dde8b13bc71847551f126397b4","src/event.rs":"866b2cb41af83e1552dd10ae8e07a9ae40b65bd8ca63c10537b38e9be0abfe3c","src/lib.rs":"c557479cf4c1f6c44c3fd708338eebe5fb2e01f106259db58578a9a1b55ef358"},"package":"8777d5490145d6907198d48b3a907447689ce80e071b3d8a16a9d9fb3df02bc1"}
\ No newline at end of file diff --git a/third_party/rust/qlog/Cargo.toml b/third_party/rust/qlog/Cargo.toml new file mode 100644 index 0000000000..a1304cd9ab --- /dev/null +++ b/third_party/rust/qlog/Cargo.toml @@ -0,0 +1,36 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies +# +# If you believe there's an error in this file please file an +# issue against the rust-lang/cargo repository. If you're +# editing this file be aware that the upstream Cargo.toml +# will likely look very different (and much more reasonable) + +[package] +edition = "2018" +name = "qlog" +version = "0.4.0" +authors = ["Lucas Pardue <lucaspardue.24.7@gmail.com>"] +description = "qlog data model for QUIC and HTTP/3" +readme = "README.md" +keywords = ["qlog", "quic", "http3"] +categories = ["network-programming"] +license = "BSD-2-Clause" +repository = "https://github.com/cloudflare/quiche" +[dependencies.serde] +version = "1" +features = ["derive"] + +[dependencies.serde_derive] +version = "1.0" + +[dependencies.serde_json] +version = "1.0" +features = ["preserve_order"] + +[dependencies.serde_with] +version = "1.3.1" diff --git a/third_party/rust/qlog/README.md b/third_party/rust/qlog/README.md new file mode 100644 index 0000000000..cc420a17d7 --- /dev/null +++ b/third_party/rust/qlog/README.md @@ -0,0 +1,304 @@ +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. + +Buffered Mode +--------------- + +Create the trace: + +```rust +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. + +```rust +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: + +```rust +serde_json::to_string_pretty(&trace).unwrap(); +``` + +would generate the following: + +``` +{ + "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": "0xff00001b", + "scil": "8", + "dcil": "8", + "scid": "7e37e4dcc6682da8", + "dcid": "36ce104eee50101c" + }, + "frames": [ + { + "frame_type": "crypto", + "offset": "0", + "length": "100", + } + ] + } + ] + ] +} +``` + +Streaming Mode +--------------- + +Create the trace: + +```rust +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: + +```rust +let mut file = std::fs::File::create("foo.qlog").unwrap(); +``` + +Create a [`QlogStreamer`] and start serialization to foo.qlog +using [`start_log()`]: + +```rust +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()`]: + +```rust +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: + +```rust +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()`]. + +```rust +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()`]: + +```rust +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
\ No newline at end of file diff --git a/third_party/rust/qlog/src/event.rs b/third_party/rust/qlog/src/event.rs new file mode 100644 index 0000000000..5748bf82d5 --- /dev/null +++ b/third_party/rust/qlog/src/event.rs @@ -0,0 +1,988 @@ +// Copyright (C) 2020, 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. + +//! Module-defined Event and functions to work with qlog data structures. +//! +//! The Qlog data structures are very flexible, which makes working with them +//! liable to simple semantic errors. This module provides some helper functions +//! that focus on creating valid types, while also reducing some of the +//! verbosity of using the raw data structures. + +use super::*; + +/// A representation of qlog events that couple EventCategory, EventType and +/// EventData. +/// +/// Functions are provided to help construct valid events. Most events consist +/// of several optional fields, so minimal versions of these functions are +/// provided, which accept only mandatory qlog parameters. Minimal functions are +/// identified by a `_min` suffix. +#[derive(Clone)] +pub struct Event { + pub category: EventCategory, + pub ty: EventType, + pub data: EventData, +} + +#[allow(clippy::too_many_arguments)] +impl Event { + // Connectivity events. + + /// Returns: + /// * `EventCategory`=`Connectivity` + /// * `EventType`=`ConnectivityEventType::ServerListening` + /// * `EventData`=`ServerListening`. + pub fn server_listening( + 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>, + ) -> Self { + Event { + category: EventCategory::Connectivity, + ty: EventType::ConnectivityEventType( + ConnectivityEventType::ServerListening, + ), + data: EventData::ServerListening { + ip_v4, + ip_v6, + port_v4, + port_v6, + quic_versions, + alpn_values, + stateless_reset_required, + }, + } + } + + pub fn server_listening_min(port_v4: u64, port_v6: u64) -> Self { + Event::server_listening(None, None, port_v4, port_v6, None, None, None) + } + + /// Returns: + /// * `EventCategory`=`Connectivity` + /// * `EventType`=`ConnectivityEventType::ConnectionStarted` + /// * `EventData`=`ConnectionStarted`. + pub fn connection_started( + 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>, + ) -> Self { + Event { + category: EventCategory::Connectivity, + ty: EventType::ConnectivityEventType( + ConnectivityEventType::ConnectionStarted, + ), + data: EventData::ConnectionStarted { + ip_version, + src_ip, + dst_ip, + protocol, + src_port, + dst_port, + quic_version, + src_cid, + dst_cid, + }, + } + } + + pub fn connection_started_min( + ip_version: String, src_ip: String, dst_ip: String, src_port: u64, + dst_port: u64, + ) -> Self { + Event::connection_started( + ip_version, src_ip, dst_ip, None, src_port, dst_port, None, None, + None, + ) + } + + /// Returns: + /// * `EventCategory`=`Connectivity` + /// * `EventType`=`ConnectivityEventType::ConnectionIdUpdated` + /// * `EventData`=`ConnectionIdUpdated`. + pub fn connection_id_updated( + src_old: Option<String>, src_new: Option<String>, + dst_old: Option<String>, dst_new: Option<String>, + ) -> Self { + Event { + category: EventCategory::Connectivity, + ty: EventType::ConnectivityEventType( + ConnectivityEventType::ConnectionIdUpdated, + ), + data: EventData::ConnectionIdUpdated { + src_old, + src_new, + dst_old, + dst_new, + }, + } + } + + pub fn connection_id_updated_min() -> Self { + Event::connection_id_updated(None, None, None, None) + } + + /// Returns: + /// * `EventCategory`=`Connectivity` + /// * `EventType`=`ConnectivityEventType::SpinBitUpdated` + /// * `EventData`=`SpinBitUpdated`. + pub fn spinbit_updated(state: bool) -> Self { + Event { + category: EventCategory::Connectivity, + ty: EventType::ConnectivityEventType( + ConnectivityEventType::SpinBitUpdated, + ), + data: EventData::SpinBitUpdated { state }, + } + } + + /// Returns: + /// * `EventCategory`=`Connectivity` + /// * `EventType`=`ConnectivityEventType::ConnectionState` + /// * `EventData`=`ConnectionState`. + pub fn connection_state_updated( + old: Option<ConnectionState>, new: ConnectionState, + ) -> Self { + Event { + category: EventCategory::Connectivity, + ty: EventType::ConnectivityEventType( + ConnectivityEventType::ConnectionStateUpdated, + ), + data: EventData::ConnectionStateUpdated { old, new }, + } + } + + pub fn connection_state_updated_min(new: ConnectionState) -> Self { + Event::connection_state_updated(None, new) + } + + // Transport events. + + /// Returns: + /// * `EventCategory`=`Transport` + /// * `EventType`=`TransportEventType::ParametersSet` + /// * `EventData`=`ParametersSet`. + pub fn transport_parameters_set( + 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>, + ) -> Self { + Event { + category: EventCategory::Transport, + ty: EventType::TransportEventType(TransportEventType::ParametersSet), + data: EventData::TransportParametersSet { + owner, + + resumption_allowed, + early_data_enabled, + alpn, + version, + tls_cipher, + + original_connection_id, + stateless_reset_token, + disable_active_migration, + + idle_timeout, + max_packet_size, + ack_delay_exponent, + max_ack_delay, + active_connection_id_limit, + + initial_max_data, + initial_max_stream_data_bidi_local, + initial_max_stream_data_bidi_remote, + initial_max_stream_data_uni, + initial_max_streams_bidi, + initial_max_streams_uni, + + preferred_address, + }, + } + } + + pub fn transport_parameters_set_min() -> Self { + Event::transport_parameters_set( + None, None, None, None, None, None, None, None, None, None, None, + None, None, None, None, None, None, None, None, None, None, + ) + } + + /// Returns: + /// * `EventCategory`=`Transport` + /// * `EventType`=`TransportEventType::DatagramsReceived` + /// * `EventData`=`DatagramsReceived`. + pub fn datagrams_received( + count: Option<u64>, byte_length: Option<u64>, + ) -> Self { + Event { + category: EventCategory::Transport, + ty: EventType::TransportEventType( + TransportEventType::DatagramsReceived, + ), + data: EventData::DatagramsReceived { count, byte_length }, + } + } + + pub fn datagrams_received_min() -> Self { + Event::datagrams_received(None, None) + } + + /// Returns: + /// * `EventCategory`=`Transport` + /// * `EventType`=`TransportEventType::DatagramsSent` + /// * `EventData`=`DatagramsSent`. + pub fn datagrams_sent(count: Option<u64>, byte_length: Option<u64>) -> Self { + Event { + category: EventCategory::Transport, + ty: EventType::TransportEventType(TransportEventType::DatagramsSent), + data: EventData::DatagramsSent { count, byte_length }, + } + } + + pub fn datagrams_sent_min() -> Self { + Event::datagrams_sent(None, None) + } + + /// Returns: + /// * `EventCategory`=`Transport` + /// * `EventType`=`TransportEventType::DatagramDropped` + /// * `EventData`=`DatagramDropped`. + pub fn datagram_dropped(byte_length: Option<u64>) -> Self { + Event { + category: EventCategory::Transport, + ty: EventType::TransportEventType( + TransportEventType::DatagramDropped, + ), + data: EventData::DatagramDropped { byte_length }, + } + } + + pub fn datagram_dropped_min() -> Self { + Event::datagram_dropped(None) + } + + /// Returns: + /// * `EventCategory`=`Transport` + /// * `EventType`=`TransportEventType::PacketReceived` + /// * `EventData`=`PacketReceived`. + pub fn packet_received( + packet_type: PacketType, header: PacketHeader, + frames: Option<Vec<QuicFrame>>, is_coalesced: Option<bool>, + raw_encrypted: Option<String>, raw_decrypted: Option<String>, + ) -> Self { + Event { + category: EventCategory::Transport, + ty: EventType::TransportEventType(TransportEventType::PacketReceived), + data: EventData::PacketReceived { + packet_type, + header, + frames, + + is_coalesced, + + raw_encrypted, + raw_decrypted, + }, + } + } + + pub fn packet_received_min( + packet_type: PacketType, header: PacketHeader, + ) -> Self { + Event::packet_received(packet_type, header, None, None, None, None) + } + + /// Returns: + /// * `EventCategory`=`Transport` + /// * `EventType`=`TransportEventType::PacketSent` + /// * `EventData`=`PacketSent`. + pub fn packet_sent( + packet_type: PacketType, header: PacketHeader, + frames: Option<Vec<QuicFrame>>, is_coalesced: Option<bool>, + raw_encrypted: Option<String>, raw_decrypted: Option<String>, + ) -> Self { + Event { + category: EventCategory::Transport, + ty: EventType::TransportEventType(TransportEventType::PacketSent), + data: EventData::PacketSent { + packet_type, + header, + frames, + + is_coalesced, + + raw_encrypted, + raw_decrypted, + }, + } + } + + pub fn packet_sent_min( + packet_type: PacketType, header: PacketHeader, + frames: Option<Vec<QuicFrame>>, + ) -> Self { + Event::packet_sent(packet_type, header, frames, None, None, None) + } + + /// Returns: + /// * `EventCategory`=`Transport` + /// * `EventType`=`TransportEventType::PacketDropped` + /// * `EventData`=`PacketDropped`. + pub fn packet_dropped( + packet_type: Option<PacketType>, packet_size: Option<u64>, + raw: Option<String>, + ) -> Self { + Event { + category: EventCategory::Transport, + ty: EventType::TransportEventType(TransportEventType::PacketDropped), + data: EventData::PacketDropped { + packet_type, + packet_size, + raw, + }, + } + } + + pub fn packet_dropped_min() -> Self { + Event::packet_dropped(None, None, None) + } + + /// Returns: + /// * `EventCategory`=`Transport` + /// * `EventType`=`TransportEventType::PacketBuffered` + /// * `EventData`=`PacketBuffered`. + pub fn packet_buffered( + packet_type: PacketType, packet_number: String, + ) -> Self { + Event { + category: EventCategory::Transport, + ty: EventType::TransportEventType(TransportEventType::PacketBuffered), + data: EventData::PacketBuffered { + packet_type, + packet_number, + }, + } + } + + /// Returns: + /// * `EventCategory`=`Transport` + /// * `EventType`=`TransportEventType::StreamStateUpdated` + /// * `EventData`=`StreamStateUpdated`. + pub fn stream_state_updated( + stream_id: String, stream_type: Option<StreamType>, + old: Option<StreamState>, new: StreamState, + stream_side: Option<StreamSide>, + ) -> Self { + Event { + category: EventCategory::Connectivity, + ty: EventType::TransportEventType( + TransportEventType::StreamStateUpdated, + ), + data: EventData::StreamStateUpdated { + stream_id, + stream_type, + old, + new, + stream_side, + }, + } + } + + pub fn stream_state_updated_min(stream_id: String, new: StreamState) -> Self { + Event::stream_state_updated(stream_id, None, None, new, None) + } + + /// Returns: + /// * `EventCategory`=`Transport` + /// * `EventType`=`TransportEventType::FramesProcessed` + /// * `EventData`=`FramesProcessed`. + pub fn frames_processed(frames: Vec<QuicFrame>) -> Self { + Event { + category: EventCategory::Transport, + ty: EventType::TransportEventType( + TransportEventType::FramesProcessed, + ), + data: EventData::FramesProcessed { frames }, + } + } + + // Recovery events. + + /// Returns: + /// * `EventCategory`=`Recovery` + /// * `EventType`=`RecoveryEventType::ParametersSet` + /// * `EventData`=`RecoveryParametersSet`. + pub fn recovery_parameters_set( + 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>, + ) -> Self { + Event { + category: EventCategory::Recovery, + ty: EventType::RecoveryEventType(RecoveryEventType::ParametersSet), + data: EventData::RecoveryParametersSet { + reordering_threshold, + time_threshold, + timer_granularity, + initial_rtt, + max_datagram_size, + initial_congestion_window, + minimum_congestion_window, + loss_reduction_factor, + persistent_congestion_threshold, + }, + } + } + + pub fn recovery_parameters_set_min() -> Self { + Event::recovery_parameters_set( + None, None, None, None, None, None, None, None, None, + ) + } + + /// Returns: + /// * `EventCategory`=`Recovery` + /// * `EventType`=`RecoveryEventType::MetricsUpdated` + /// * `EventData`=`MetricsUpdated`. + pub fn metrics_updated( + 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>, + packets_in_flight: Option<u64>, in_recovery: Option<bool>, + pacing_rate: Option<u64>, + ) -> Self { + Event { + category: EventCategory::Recovery, + ty: EventType::RecoveryEventType(RecoveryEventType::MetricsUpdated), + data: EventData::MetricsUpdated { + min_rtt, + smoothed_rtt, + latest_rtt, + rtt_variance, + max_ack_delay, + pto_count, + congestion_window, + bytes_in_flight, + ssthresh, + packets_in_flight, + in_recovery, + pacing_rate, + }, + } + } + + pub fn metrics_updated_min() -> Self { + Event::metrics_updated( + None, None, None, None, None, None, None, None, None, None, None, + None, + ) + } + + /// Returns: + /// * `EventCategory`=`Recovery` + /// * `EventType`=`RecoveryEventType::CongestionStateUpdated` + /// * `EventData`=`CongestionStateUpdated`. + pub fn congestion_state_updated(old: Option<String>, new: String) -> Self { + Event { + category: EventCategory::Recovery, + ty: EventType::RecoveryEventType( + RecoveryEventType::CongestionStateUpdated, + ), + data: EventData::CongestionStateUpdated { old, new }, + } + } + + pub fn congestion_state_updated_min(new: String) -> Self { + Event::congestion_state_updated(None, new) + } + + /// Returns: + /// * `EventCategory`=`Recovery` + /// * `EventType`=`RecoveryEventType::LossTimerSet` + /// * `EventData`=`LossTimerSet`. + pub fn loss_timer_set( + timer_type: Option<TimerType>, timeout: Option<String>, + ) -> Self { + Event { + category: EventCategory::Recovery, + ty: EventType::RecoveryEventType(RecoveryEventType::LossTimerSet), + data: EventData::LossTimerSet { + timer_type, + timeout, + }, + } + } + + pub fn loss_timer_set_min() -> Self { + Event::loss_timer_set(None, None) + } + + /// Returns: + /// * `EventCategory`=`Recovery` + /// * `EventType`=`RecoveryEventType::PacketLost` + /// * `EventData`=`PacketLost`. + pub fn packet_lost( + packet_type: PacketType, packet_number: String, + header: Option<PacketHeader>, frames: Vec<QuicFrame>, + ) -> Self { + Event { + category: EventCategory::Recovery, + ty: EventType::RecoveryEventType(RecoveryEventType::PacketLost), + data: EventData::PacketLost { + packet_type, + packet_number, + header, + frames, + }, + } + } + + pub fn packet_lost_min( + packet_type: PacketType, packet_number: String, frames: Vec<QuicFrame>, + ) -> Self { + Event::packet_lost(packet_type, packet_number, None, frames) + } + + /// Returns: + /// * `EventCategory`=`Recovery` + /// * `EventType`=`RecoveryEventType::MarkedForRetransmit` + /// * `EventData`=`MarkedForRetransmit`. + pub fn marked_for_retransmit(frames: Vec<QuicFrame>) -> Self { + Event { + category: EventCategory::Recovery, + ty: EventType::RecoveryEventType( + RecoveryEventType::MarkedForRetransmit, + ), + data: EventData::MarkedForRetransmit { frames }, + } + } + + // HTTP/3 events. + + /// Returns: + /// * `EventCategory`=`Http` + /// * `EventType`=`Http3EventType::ParametersSet` + /// * `EventData`=`H3ParametersSet`. + pub fn h3_parameters_set( + 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>, + ) -> Self { + Event { + category: EventCategory::Http, + ty: EventType::Http3EventType(Http3EventType::ParametersSet), + data: EventData::H3ParametersSet { + owner, + max_header_list_size, + max_table_capacity, + blocked_streams_count, + push_allowed, + waits_for_settings, + }, + } + } + + pub fn h3_parameters_set_min() -> Self { + Event::h3_parameters_set(None, None, None, None, None, None) + } + + /// Returns: + /// * `EventCategory`=`Http` + /// * `EventType`=`Http3EventType::StreamTypeSet` + /// * `EventData`=`H3StreamTypeSet`. + pub fn h3_stream_type_set( + stream_id: String, owner: Option<H3Owner>, old: Option<H3StreamType>, + new: H3StreamType, + ) -> Self { + Event { + category: EventCategory::Http, + ty: EventType::Http3EventType(Http3EventType::StreamTypeSet), + data: EventData::H3StreamTypeSet { + stream_id, + owner, + old, + new, + }, + } + } + + pub fn h3_stream_type_set_min(stream_id: String, new: H3StreamType) -> Self { + Event::h3_stream_type_set(stream_id, None, None, new) + } + + /// Returns: + /// * `EventCategory`=`Http` + /// * `EventType`=`Http3EventType::FrameCreated` + /// * `EventData`=`H3FrameCreated`. + pub fn h3_frame_created( + stream_id: String, frame: Http3Frame, byte_length: Option<String>, + raw: Option<String>, + ) -> Self { + Event { + category: EventCategory::Http, + ty: EventType::Http3EventType(Http3EventType::FrameCreated), + data: EventData::H3FrameCreated { + stream_id, + frame, + byte_length, + raw, + }, + } + } + + pub fn h3_frame_created_min(stream_id: String, frame: Http3Frame) -> Self { + Event::h3_frame_created(stream_id, frame, None, None) + } + + /// Returns: + /// * `EventCategory`=`Http` + /// * `EventType`=`Http3EventType::FrameParsed` + /// * `EventData`=`H3FrameParsed`. + pub fn h3_frame_parsed( + stream_id: String, frame: Http3Frame, byte_length: Option<String>, + raw: Option<String>, + ) -> Self { + Event { + category: EventCategory::Http, + ty: EventType::Http3EventType(Http3EventType::FrameParsed), + data: EventData::H3FrameParsed { + stream_id, + frame, + byte_length, + raw, + }, + } + } + + pub fn h3_frame_parsed_min(stream_id: String, frame: Http3Frame) -> Self { + Event::h3_frame_parsed(stream_id, frame, None, None) + } + + /// Returns: + /// * `EventCategory`=`Http` + /// * `EventType`=`Http3EventType::DataMoved` + /// * `EventData`=`H3DataMoved`. + pub fn h3_data_moved( + stream_id: String, offset: Option<String>, length: Option<u64>, + from: Option<H3DataRecipient>, to: Option<H3DataRecipient>, + raw: Option<String>, + ) -> Self { + Event { + category: EventCategory::Http, + ty: EventType::Http3EventType(Http3EventType::DataMoved), + data: EventData::H3DataMoved { + stream_id, + offset, + length, + from, + to, + raw, + }, + } + } + + pub fn h3_data_moved_min(stream_id: String) -> Self { + Event::h3_data_moved(stream_id, None, None, None, None, None) + } + + /// Returns: + /// * `EventCategory`=`Http` + /// * `EventType`=`Http3EventType::PushResolved` + /// * `EventData`=`H3PushResolved`. + pub fn h3_push_resolved( + push_id: Option<String>, stream_id: Option<String>, + decision: Option<H3PushDecision>, + ) -> Self { + Event { + category: EventCategory::Http, + ty: EventType::Http3EventType(Http3EventType::PushResolved), + data: EventData::H3PushResolved { + push_id, + stream_id, + decision, + }, + } + } + + pub fn h3_push_resolved_min() -> Self { + Event::h3_push_resolved(None, None, None) + } + + // QPACK events. + + /// Returns: + /// * `EventCategory`=`Qpack` + /// * `EventType`=`QpackEventType::StateUpdated` + /// * `EventData`=`QpackStateUpdated`. + pub fn qpack_state_updated( + owner: Option<QpackOwner>, dynamic_table_capacity: Option<u64>, + dynamic_table_size: Option<u64>, known_received_count: Option<u64>, + current_insert_count: Option<u64>, + ) -> Self { + Event { + category: EventCategory::Qpack, + ty: EventType::QpackEventType(QpackEventType::StateUpdated), + data: EventData::QpackStateUpdated { + owner, + dynamic_table_capacity, + dynamic_table_size, + known_received_count, + current_insert_count, + }, + } + } + + pub fn qpack_state_updated_min() -> Self { + Event::qpack_state_updated(None, None, None, None, None) + } + + /// Returns: + /// * `EventCategory`=`Qpack` + /// * `EventType`=`QpackEventType::StreamStateUpdated` + /// * `EventData`=`QpackStreamStateUpdated`. + pub fn qpack_stream_state_updated( + stream_id: String, state: QpackStreamState, + ) -> Self { + Event { + category: EventCategory::Qpack, + ty: EventType::QpackEventType(QpackEventType::StreamStateUpdated), + data: EventData::QpackStreamStateUpdated { stream_id, state }, + } + } + + /// Returns: + /// * `EventCategory`=`Qpack` + /// * `EventType`=`QpackEventType::DynamicTableUpdated` + /// * `EventData`=`QpackDynamicTableUpdated`. + pub fn qpack_dynamic_table_updated( + update_type: QpackUpdateType, entries: Vec<QpackDynamicTableEntry>, + ) -> Self { + Event { + category: EventCategory::Qpack, + ty: EventType::QpackEventType(QpackEventType::DynamicTableUpdated), + data: EventData::QpackDynamicTableUpdated { + update_type, + entries, + }, + } + } + + /// Returns: + /// * `EventCategory`=`Qpack` + /// * `EventType`=`QpackEventType::HeadersEncoded` + /// * `EventData`=`QpackHeadersEncoded`. + pub fn qpack_headers_encoded( + stream_id: Option<String>, headers: Option<HttpHeader>, + block_prefix: QpackHeaderBlockPrefix, + header_block: Vec<QpackHeaderBlockRepresentation>, raw: Option<String>, + ) -> Self { + Event { + category: EventCategory::Qpack, + ty: EventType::QpackEventType(QpackEventType::HeadersEncoded), + data: EventData::QpackHeadersEncoded { + stream_id, + headers, + block_prefix, + header_block, + raw, + }, + } + } + + pub fn qpack_headers_encoded_min( + block_prefix: QpackHeaderBlockPrefix, + header_block: Vec<QpackHeaderBlockRepresentation>, + ) -> Self { + Event::qpack_headers_encoded(None, None, block_prefix, header_block, None) + } + + /// Returns: + /// * `EventCategory`=`Qpack` + /// * `EventType`=`QpackEventType::HeadersDecoded` + /// * `EventData`=`QpackHeadersDecoded`. + pub fn qpack_headers_decoded( + stream_id: Option<String>, headers: Option<HttpHeader>, + block_prefix: QpackHeaderBlockPrefix, + header_block: Vec<QpackHeaderBlockRepresentation>, raw: Option<String>, + ) -> Self { + Event { + category: EventCategory::Qpack, + ty: EventType::QpackEventType(QpackEventType::HeadersDecoded), + data: EventData::QpackHeadersDecoded { + stream_id, + headers, + block_prefix, + header_block, + raw, + }, + } + } + + pub fn qpack_headers_decoded_min( + block_prefix: QpackHeaderBlockPrefix, + header_block: Vec<QpackHeaderBlockRepresentation>, + ) -> Self { + Event::qpack_headers_decoded(None, None, block_prefix, header_block, None) + } + + /// Returns: + /// * `EventCategory`=`Qpack` + /// * `EventType`=`QpackEventType::InstructionSent` + /// * `EventData`=`QpackInstructionSent`. + pub fn qpack_instruction_sent( + instruction: QPackInstruction, byte_length: Option<String>, + raw: Option<String>, + ) -> Self { + Event { + category: EventCategory::Qpack, + ty: EventType::QpackEventType(QpackEventType::InstructionSent), + data: EventData::QpackInstructionSent { + instruction, + byte_length, + raw, + }, + } + } + + pub fn qpack_instruction_sent_min(instruction: QPackInstruction) -> Self { + Event::qpack_instruction_sent(instruction, None, None) + } + + /// Returns: + /// * `EventCategory`=`Qpack` + /// * `EventType`=`QpackEventType::InstructionReceived` + /// * `EventData`=`QpackInstructionReceived`. + pub fn qpack_instruction_received( + instruction: QPackInstruction, byte_length: Option<String>, + raw: Option<String>, + ) -> Self { + Event { + category: EventCategory::Qpack, + ty: EventType::QpackEventType(QpackEventType::InstructionReceived), + data: EventData::QpackInstructionReceived { + instruction, + byte_length, + raw, + }, + } + } + + pub fn qpack_instruction_received_min(instruction: QPackInstruction) -> Self { + Event::qpack_instruction_received(instruction, None, None) + } + + /// Checks if the the combination of `EventCategory`, `EventType` and + /// `EventData` is valid. + pub fn is_valid(&self) -> bool { + match (&self.category, &self.ty) { + ( + EventCategory::Connectivity, + EventType::ConnectivityEventType(_), + ) => matches!(&self.data, + EventData::ServerListening { .. } | + EventData::ConnectionStarted { .. } | + EventData::ConnectionIdUpdated { .. } | + EventData::SpinBitUpdated { .. } | + EventData::ConnectionStateUpdated { .. }), + + (EventCategory::Transport, EventType::TransportEventType(_)) => + matches!(&self.data, + EventData::TransportParametersSet { .. } | + EventData::DatagramsReceived { .. } | + EventData::DatagramsSent { .. } | + EventData::DatagramDropped { .. } | + EventData::PacketReceived { .. } | + EventData::PacketSent { .. } | + EventData::PacketDropped { .. } | + EventData::PacketBuffered { .. } | + EventData::StreamStateUpdated { .. } | + EventData::FramesProcessed { .. }), + + (EventCategory::Security, EventType::SecurityEventType(_)) => + matches!(&self.data, + EventData::KeyUpdated { .. } | + EventData::KeyRetired { .. }), + + (EventCategory::Recovery, EventType::RecoveryEventType(_)) => + matches!(&self.data, + EventData::RecoveryParametersSet { .. } | + EventData::MetricsUpdated { .. } | + EventData::CongestionStateUpdated { .. } | + EventData::LossTimerSet { .. } | + EventData::PacketLost { .. } | + EventData::MarkedForRetransmit { .. }), + + (EventCategory::Http, EventType::Http3EventType(_)) => + matches!(&self.data, + EventData::H3ParametersSet { .. } | + EventData::H3StreamTypeSet { .. } | + EventData::H3FrameCreated { .. } | + EventData::H3FrameParsed { .. } | + EventData::H3DataMoved { .. } | + EventData::H3PushResolved { .. }), + + (EventCategory::Qpack, EventType::QpackEventType(_)) => + matches!(&self.data, + EventData::QpackStateUpdated { .. } | + EventData::QpackStreamStateUpdated { .. } | + EventData::QpackDynamicTableUpdated { .. } | + EventData::QpackHeadersEncoded { .. } | + EventData::QpackHeadersDecoded { .. } | + EventData::QpackInstructionSent { .. } | + EventData::QpackInstructionReceived { .. }), + + // TODO: in qlog-01 there is no sane default category defined for + // GenericEventType + (_, EventType::GenericEventType(_)) => matches!(&self.data, + EventData::ConnectionError { .. } | + EventData::ApplicationError { .. } | + EventData::InternalError { .. } | + EventData::InternalWarning { .. } | + EventData::Message { .. } | + EventData::Marker { .. }), + + _ => false, + } + } +} diff --git a/third_party/rust/qlog/src/lib.rs b/third_party/rust/qlog/src/lib.rs new file mode 100644 index 0000000000..d4a3bb45d0 --- /dev/null +++ b/third_party/rust/qlog/src/lib.rs @@ -0,0 +1,2970 @@ +// 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. + /// + /// 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> { + if self.state != StreamerState::Ready { + return Err(Error::InvalidState); + } + + let event_time = if cfg!(test) { + std::time::Duration::from_secs(0) + } else { + self.start_time.elapsed() + }; + + 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, + byte_length: Option<String>, + + raw: Option<String>, + }, + + H3FrameParsed { + stream_id: String, + frame: Http3Frame, + byte_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, + byte_length: Option<String>, + + raw: Option<String>, + }, + + QpackInstructionReceived { + instruction: QPackInstruction, + byte_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, .. } => + if let Some(f) = frames { + Some(f.len()) + } else { + None + }, + + 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 = match version { + Some(v) => Some(format!("{:x?}", v)), + + None => None, + }; + + 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, +} + +#[derive(Serialize, Clone)] +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, + { + match data { + Some(d) => Some(format!("{}", HexSlice::new(d))), + + None => None, + } + } +} + +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, + }); + + 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}]}]]}]}"#; + + let written_string = std::str::from_utf8(w.as_ref().get_ref()).unwrap(); + + assert_eq!(log_string, written_string); + } +} + +pub mod event; |