Skip to content

Commit 3deccff

Browse files
author
Erik Bremen
committed
qlog,quiche,tokio-quiche: add QlogSink abstraction for custom qlog writing
Add a QlogSink trait that allows overriding qlog behavior. Previously QlogStreamer was instantiated with a writer object. Instead, it is instantiated with a QlogSink object which allows more flexibility. A QlogWriterSink implementation provides backwards compatibility. Implementations have new methods for overriding qlog behavior: - Connection::set_qlog_sink and set_qlog_sink_with_level allow setting a custom qlog sink. The existing set_qlog and set_qlog_with_level exist for backwards compatibility and call these new functions with a QlogWriterSink object. - A new ConnectionHook::create_qlog_sink hook can be implemented to install a sink at connection accept/connect time. If a sink is returned it overrides any qlog_dir behavior. Add QlogStreamer::should_log(EventType) that allows high volume events to be skipped without construction. Modified both the recv and write path to make use of this. Remove QlogStreamer::writer. A QlogStreamer is no longer guaranteed to be backed by a Writer. The underlying QlogSink is exposed via QlogStreamer::sink(), where it's used by tests to access the underlying test sink.
1 parent f0c7193 commit 3deccff

19 files changed

Lines changed: 1245 additions & 105 deletions

File tree

apps/src/bin/quiche-server.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,8 +381,10 @@ fn main() {
381381
let id = format!("{:?}", scid);
382382
let writer = make_qlog_writer(&dir, "server", &id);
383383

384-
conn.set_qlog(
385-
std::boxed::Box::new(writer),
384+
conn.set_qlog_sink(
385+
std::boxed::Box::new(quiche::QlogWriterSink::new(
386+
writer,
387+
)),
386388
"quiche-server qlog".to_string(),
387389
format!("{} id={}", "quiche-server qlog", id),
388390
);

apps/src/client.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,8 @@ pub fn connect(
205205
let id = format!("{scid:?}");
206206
let writer = make_qlog_writer(&dir, "client", &id);
207207

208-
conn.set_qlog(
209-
std::boxed::Box::new(writer),
208+
conn.set_qlog_sink(
209+
std::boxed::Box::new(quiche::QlogWriterSink::new(writer)),
210210
"quiche-client qlog".to_string(),
211211
format!("{} id={}", "quiche-client qlog", id),
212212
);

qlog/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,11 @@ impl std::fmt::Display for HexSlice<'_> {
671671

672672
pub mod events;
673673
pub mod reader;
674+
pub mod sink;
674675
pub mod streamer;
676+
pub use sink::QlogEvent;
677+
pub use sink::QlogSink;
678+
pub use sink::QlogWriterSink;
675679
#[doc(hidden)]
676680
pub mod testing;
677681
pub mod writer;

qlog/src/sink.rs

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
// Copyright (C) 2026, Cloudflare, Inc.
2+
// All rights reserved.
3+
//
4+
// Redistribution and use in source and binary forms, with or without
5+
// modification, are permitted provided that the following conditions are
6+
// met:
7+
//
8+
// * Redistributions of source code must retain the above copyright notice,
9+
// this list of conditions and the following disclaimer.
10+
//
11+
// * Redistributions in binary form must reproduce the above copyright
12+
// notice, this list of conditions and the following disclaimer in the
13+
// documentation and/or other materials provided with the distribution.
14+
//
15+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
16+
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17+
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18+
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
19+
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20+
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21+
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22+
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23+
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24+
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25+
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26+
27+
use std::io::Write;
28+
29+
use crate::events;
30+
use crate::Error;
31+
use crate::QlogSeq;
32+
use crate::Result;
33+
34+
/// An event that can be delivered to a [`QlogSink`].
35+
pub enum QlogEvent {
36+
/// A native qlog event.
37+
Event(events::Event),
38+
39+
/// An extended JSON event.
40+
JsonEvent(events::JsonEvent),
41+
}
42+
43+
impl From<events::Event> for QlogEvent {
44+
fn from(event: events::Event) -> Self {
45+
Self::Event(event)
46+
}
47+
}
48+
49+
impl From<events::JsonEvent> for QlogEvent {
50+
fn from(event: events::JsonEvent) -> Self {
51+
Self::JsonEvent(event)
52+
}
53+
}
54+
55+
/// A destination for sequential qlog events.
56+
pub trait QlogSink: Send + Sync {
57+
/// Start a qlog stream by writing or otherwise recording the stream header.
58+
fn start_log(&mut self, qlog: &QlogSeq) -> Result<()>;
59+
60+
/// Add a native qlog event to the stream.
61+
fn add_event(&mut self, event: events::Event) -> Result<()>;
62+
63+
/// Add a pretty-printed native qlog event to the stream.
64+
fn add_event_pretty(&mut self, event: events::Event) -> Result<()> {
65+
self.add_event(event)
66+
}
67+
68+
/// Add an extended JSON qlog event to the stream.
69+
fn add_json_event(&mut self, event: events::JsonEvent) -> Result<()>;
70+
71+
/// Add a pretty-printed extended JSON qlog event to the stream.
72+
fn add_json_event_pretty(&mut self, event: events::JsonEvent) -> Result<()> {
73+
self.add_json_event(event)
74+
}
75+
76+
/// Finish the stream.
77+
fn finish_log(&mut self) -> Result<()>;
78+
79+
/// Returns whether this sink wants events of `event_type`.
80+
fn should_log(&self, _event_type: events::EventType) -> bool {
81+
true
82+
}
83+
}
84+
85+
/// A [`QlogSink`] that writes JSON-SEQ qlog records to a [`Write`].
86+
pub struct QlogWriterSink<W: Write + Send + Sync> {
87+
writer: W,
88+
}
89+
90+
impl<W: Write + Send + Sync> QlogWriterSink<W> {
91+
/// Creates a new writer-backed sink.
92+
pub fn new(writer: W) -> Self {
93+
Self { writer }
94+
}
95+
}
96+
97+
impl<W: Write + Send + Sync> QlogSink for QlogWriterSink<W> {
98+
fn start_log(&mut self, qlog: &QlogSeq) -> Result<()> {
99+
self.writer.write_all(b"\x1e")?;
100+
serde_json::to_writer(&mut self.writer, qlog).map_err(|_| Error::Done)?;
101+
self.writer.write_all(b"\n")?;
102+
103+
Ok(())
104+
}
105+
106+
fn add_event(&mut self, event: events::Event) -> Result<()> {
107+
self.writer.write_all(b"\x1e")?;
108+
serde_json::to_writer(&mut self.writer, &event)
109+
.map_err(|_| Error::Done)?;
110+
self.writer.write_all(b"\n")?;
111+
112+
Ok(())
113+
}
114+
115+
fn add_event_pretty(&mut self, event: events::Event) -> Result<()> {
116+
self.writer.write_all(b"\x1e")?;
117+
serde_json::to_writer_pretty(&mut self.writer, &event)
118+
.map_err(|_| Error::Done)?;
119+
self.writer.write_all(b"\n")?;
120+
121+
Ok(())
122+
}
123+
124+
fn add_json_event(&mut self, event: events::JsonEvent) -> Result<()> {
125+
self.writer.write_all(b"\x1e")?;
126+
serde_json::to_writer(&mut self.writer, &event)
127+
.map_err(|_| Error::Done)?;
128+
self.writer.write_all(b"\n")?;
129+
130+
Ok(())
131+
}
132+
133+
fn add_json_event_pretty(&mut self, event: events::JsonEvent) -> Result<()> {
134+
self.writer.write_all(b"\x1e")?;
135+
serde_json::to_writer_pretty(&mut self.writer, &event)
136+
.map_err(|_| Error::Done)?;
137+
self.writer.write_all(b"\n")?;
138+
139+
Ok(())
140+
}
141+
142+
fn finish_log(&mut self) -> Result<()> {
143+
self.writer.flush()?;
144+
145+
Ok(())
146+
}
147+
}
148+
149+
#[cfg(test)]
150+
mod tests {
151+
use std::io;
152+
use std::io::Write;
153+
use std::sync::Arc;
154+
use std::sync::Mutex;
155+
156+
use crate::events::quic;
157+
use crate::events::quic::QuicEventType;
158+
use crate::events::Event;
159+
use crate::events::EventData;
160+
use crate::events::EventImportance;
161+
use crate::events::EventType;
162+
use crate::events::JsonEvent;
163+
use crate::events::RawInfo;
164+
use crate::sink::QlogSink;
165+
use crate::sink::QlogWriterSink;
166+
use crate::testing;
167+
use crate::QlogSeq;
168+
use crate::QLOGFILESEQ_URI;
169+
170+
#[derive(Clone, Default)]
171+
struct SharedWriter {
172+
bytes: Arc<Mutex<Vec<u8>>>,
173+
}
174+
175+
impl SharedWriter {
176+
fn bytes(&self) -> Vec<u8> {
177+
self.bytes.lock().unwrap().clone()
178+
}
179+
}
180+
181+
impl Write for SharedWriter {
182+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
183+
self.bytes.lock().unwrap().extend_from_slice(buf);
184+
Ok(buf.len())
185+
}
186+
187+
fn flush(&mut self) -> io::Result<()> {
188+
Ok(())
189+
}
190+
}
191+
192+
fn make_qlog() -> QlogSeq {
193+
QlogSeq {
194+
file_schema: QLOGFILESEQ_URI.to_string(),
195+
serialization_format: "JSON-SEQ".to_string(),
196+
title: Some("title".to_string()),
197+
description: Some("description".to_string()),
198+
trace: testing::make_trace_seq(),
199+
}
200+
}
201+
202+
fn make_event() -> Event {
203+
let event_data = EventData::QuicPacketSent(quic::PacketSent {
204+
header: testing::make_pkt_hdr(quic::PacketType::Handshake),
205+
raw: Some(RawInfo {
206+
length: Some(1251),
207+
payload_length: Some(1224),
208+
data: None,
209+
}),
210+
..Default::default()
211+
});
212+
213+
Event::with_time(0.0, event_data)
214+
}
215+
216+
#[test]
217+
fn writer_sink_writes_json_seq_header_and_native_event() {
218+
let writer = SharedWriter::default();
219+
let bytes = writer.clone();
220+
let mut sink = QlogWriterSink::new(writer);
221+
222+
sink.start_log(&make_qlog()).unwrap();
223+
sink.add_event(make_event()).unwrap();
224+
sink.finish_log().unwrap();
225+
226+
let written = String::from_utf8(bytes.bytes()).unwrap();
227+
let expected = r#"{"file_schema":"urn:ietf:params:qlog:file:sequential","serialization_format":"JSON-SEQ","title":"title","description":"description","trace":{"title":"Quiche qlog trace","description":"Quiche qlog trace description","vantage_point":{"type":"server"},"event_schemas":[]}}
228+
{"time":0.0,"name":"quic:packet_sent","data":{"header":{"packet_type":"handshake","packet_number":0,"version":"1","scil":8,"dcil":8,"scid":"7e37e4dcc6682da8","dcid":"36ce104eee50101c"},"raw":{"length":1251,"payload_length":1224}}}
229+
"#;
230+
231+
pretty_assertions::assert_eq!(expected, written);
232+
}
233+
234+
#[test]
235+
fn writer_sink_writes_json_event() {
236+
let writer = SharedWriter::default();
237+
let bytes = writer.clone();
238+
let mut sink = QlogWriterSink::new(writer);
239+
240+
let event = JsonEvent {
241+
time: 0.0,
242+
importance: EventImportance::Core,
243+
name: "jsonevent:sample".to_string(),
244+
data: serde_json::json!({"foo":"bar"}),
245+
};
246+
247+
sink.start_log(&make_qlog()).unwrap();
248+
sink.add_json_event(event).unwrap();
249+
sink.finish_log().unwrap();
250+
251+
let written = String::from_utf8(bytes.bytes()).unwrap();
252+
assert!(written.contains(r#""name":"jsonevent:sample""#));
253+
assert!(written.contains(r#""foo":"bar""#));
254+
}
255+
256+
#[test]
257+
fn writer_sink_defaults_to_logging_all_event_types() {
258+
let writer = SharedWriter::default();
259+
let sink = QlogWriterSink::new(writer);
260+
261+
assert!(
262+
sink.should_log(EventType::QuicEventType(QuicEventType::PacketSent))
263+
);
264+
}
265+
}

0 commit comments

Comments
 (0)