Skip to main content

qlog/
lib.rs

1// Copyright (C) 2019, Cloudflare, Inc.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8//     * Redistributions of source code must retain the above copyright notice,
9//       this list of conditions and the following disclaimer.
10//
11//     * Redistributions in binary form must reproduce the above copyright
12//       notice, this list of conditions and the following disclaimer in the
13//       documentation and/or other materials provided with the distribution.
14//
15// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
16// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
19// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27//! The qlog crate is an implementation of the qlog [main logging schema],
28//! [QUIC event definitions], and [HTTP/3 and QPACK event definitions].
29//! The crate provides a qlog data model that can be used for traces with
30//! events. It supports serialization and deserialization but defers logging IO
31//! choices to applications.
32//!
33//! Serialization operates in either a [buffered mode] or a [streaming mode].
34//!
35//! The crate uses Serde for conversion between Rust and JSON.
36//!
37//! [main logging schema]: https://datatracker.ietf.org/doc/html/draft-ietf-quic-qlog-main-schema
38//! [QUIC event definitions]:
39//! https://datatracker.ietf.org/doc/html/draft-ietf-quic-qlog-quic-events.html
40//! [HTTP/3 and QPACK event definitions]:
41//! https://datatracker.ietf.org/doc/html/draft-ietf-quic-qlog-h3-events.html
42//! [buffered mode]: #buffered-traces-with-standard-json
43//! [streaming mode]: #streaming-traces-with-json-seq
44//!
45//! Overview
46//! ---------------
47//! qlog is a hierarchical logging format, with a rough structure of:
48//!
49//! * Log
50//!   * Trace(s)
51//!     * Event(s)
52//!
53//! In practice, a single QUIC connection maps to a single Trace file with one
54//! or more Events. Applications can decide whether to combine Traces from
55//! different connections into the same Log.
56//!
57//! ## Buffered Traces with standard JSON
58//!
59//! A [`Trace`] is a single JSON object. It contains metadata such as the
60//! [`VantagePoint`] of capture and the [`Configuration`], and protocol event
61//! data in the [`Event`] array.
62//!
63//! JSON Traces allow applications to appends events to them before eventually
64//! being serialized as a complete JSON object.
65//!
66//! ### Creating a Trace
67//!
68//! ```
69//! let mut trace = qlog::Trace::new(
70//! #    Some("Example qlog trace".to_string()),
71//! #    Some("Example qlog trace description".to_string()),
72//! #    None,
73//! #    Some(qlog::VantagePoint {
74//! #        name: Some("Example client".to_string()),
75//! #        ty: qlog::VantagePointType::Client,
76//! #        flow: None,
77//! #    }),
78//! #    vec![qlog::events::QUIC_URI.to_string(), qlog::events::HTTP3_URI.to_string()],
79//! # );
80//! ```
81//!
82//! ### Adding events to a Trace
83//!
84//! Qlog [`Event`] objects are added to [`qlog::Trace.events`].
85//!
86//! The following example demonstrates how to log a qlog QUIC `packet_sent`
87//! event containing a single Crypto frame. It constructs the necessary elements
88//! of the [`Event`], then appends it to the trace with [`push_event()`].
89//!
90//! ```
91//! # let mut trace = qlog::Trace::new(
92//! #    Some("Example qlog trace".to_string()),
93//! #    Some("Example qlog trace description".to_string()),
94//! #    None,
95//! #    Some(qlog::VantagePoint {
96//! #        name: Some("Example client".to_string()),
97//! #        ty: qlog::VantagePointType::Client,
98//! #        flow: None,
99//! #    }),
100//! #    vec![qlog::events::QUIC_URI.to_string(), qlog::events::HTTP3_URI.to_string()],
101//! # );
102//!
103//! let scid = [0x7e, 0x37, 0xe4, 0xdc, 0xc6, 0x68, 0x2d, 0xa8];
104//! let dcid = [0x36, 0xce, 0x10, 0x4e, 0xee, 0x50, 0x10, 0x1c];
105//!
106//! let pkt_hdr = qlog::events::quic::PacketHeader::new(
107//!     qlog::events::quic::PacketType::Initial,
108//!     Some(0),          // packet_number
109//!     None,             // token
110//!     None,             // length
111//!     Some(0x00000001), // version
112//!     Some(&scid),
113//!     Some(&dcid),
114//! );
115//!
116//! let frames = vec![qlog::events::quic::QuicFrame::Crypto {
117//!     offset: 0,
118//!     raw: None,
119//! }];
120//!
121//! let raw = qlog::events::RawInfo {
122//!     length: Some(1251),
123//!     payload_length: Some(1224),
124//!     data: None,
125//! };
126//!
127//! let event_data =
128//!     qlog::events::EventData::QuicPacketSent(qlog::events::quic::PacketSent {
129//!         header: pkt_hdr,
130//!         frames: Some(frames.into()),
131//!         stateless_reset_token: None,
132//!         supported_versions: None,
133//!         raw: Some(raw),
134//!         datagram_id: None,
135//!         is_mtu_probe_packet: None,
136//!         send_at_time: None,
137//!         trigger: None,
138//!     });
139//!
140//! trace.push_event(qlog::events::Event::with_time(0.0, event_data));
141//! ```
142//!
143//! ### Serializing
144//!
145//! The qlog crate has only been tested with `serde_json`, however
146//! other serializer targets might work.
147//!
148//! For example, serializing the trace created above:
149//!
150//! ```
151//! # let mut trace = qlog::Trace::new(
152//! #    Some("Example qlog trace".to_string()),
153//! #    Some("Example qlog trace description".to_string()),
154//! #    None,
155//! #    Some(qlog::VantagePoint {
156//! #        name: Some("Example client".to_string()),
157//! #        ty: qlog::VantagePointType::Client,
158//! #        flow: None,
159//! #    }),
160//! #    vec![qlog::events::QUIC_URI.to_string(), qlog::events::HTTP3_URI.to_string()],
161//! # );
162//! serde_json::to_string_pretty(&trace).unwrap();
163//! ```
164//!
165//! which would generate the following:
166//!
167//! ```ignore
168//! {
169//!   "vantage_point": {
170//!     "name": "Example client",
171//!     "type": "client"
172//!   },
173//!   "title": "Example qlog trace",
174//!   "description": "Example qlog trace description",
175//!   "configuration": {
176//!     "time_offset": 0.0
177//!   },
178//!   "events": [
179//!     {
180//!       "time": 0.0,
181//!       "name": "quic:packet_sent",
182//!       "data": {
183//!         "header": {
184//!           "packet_type": "initial",
185//!           "packet_number": 0,
186//!           "version": "1",
187//!           "scil": 8,
188//!           "dcil": 8,
189//!           "scid": "7e37e4dcc6682da8",
190//!           "dcid": "36ce104eee50101c"
191//!         },
192//!         "raw": {
193//!           "length": 1251,
194//!           "payload_length": 1224
195//!         },
196//!         "frames": [
197//!           {
198//!             "frame_type": "crypto",
199//!             "offset": 0,
200//!             "length": 0
201//!           }
202//!         ]
203//!       }
204//!     }
205//!   ]
206//! }
207//! ```
208//!
209//! ## Streaming Traces with JSON-SEQ
210//!
211//! To help support streaming serialization of qlogs,
212//! draft-ietf-quic-qlog-main-schema-01 introduced support for RFC 7464 JSON
213//! Text Sequences (JSON-SEQ). The qlog crate supports this format and provides
214//! utilities that aid streaming.
215//!
216//! A [`TraceSeq`] contains metadata such as the [`VantagePoint`] of capture and
217//! the [`Configuration`]. However, protocol event data is handled as separate
218//! lines containing a record separator character, a serialized [`Event`], and a
219//! newline.
220//!
221//! ### Creating a TraceSeq
222//!
223//! ```
224//! let mut trace = qlog::TraceSeq::new(
225//!     Some("Example qlog trace".to_string()),
226//!     Some("Example qlog trace description".to_string()),
227//!     None,
228//!     Some(qlog::VantagePoint {
229//!         name: Some("Example client".to_string()),
230//!         ty: qlog::VantagePointType::Client,
231//!         flow: None,
232//!     }),
233//!     vec![
234//!         qlog::events::QUIC_URI.to_string(),
235//!         qlog::events::HTTP3_URI.to_string(),
236//!     ],
237//! );
238//! ```
239//!
240//! Create an object with the [`Write`] trait:
241//!
242//! ```
243//! let mut file = std::fs::File::create("foo.sqlog").unwrap();
244//! ```
245//!
246//! Create a [`QlogStreamer`] and start serialization to foo.sqlog
247//! using [`start_log()`]:
248//!
249//! ```
250//! # let mut trace = qlog::TraceSeq::new(
251//! #    Some("Example qlog trace".to_string()),
252//! #    Some("Example qlog trace description".to_string()),
253//! #    None,
254//! #    Some(qlog::VantagePoint {
255//! #        name: Some("Example client".to_string()),
256//! #        ty: qlog::VantagePointType::Client,
257//! #        flow: None,
258//! #    }),
259//! #    vec![qlog::events::QUIC_URI.to_string(), qlog::events::HTTP3_URI.to_string()],
260//! # );
261//! # let mut file = std::fs::File::create("foo.sqlog").unwrap();
262//! let mut streamer = qlog::streamer::QlogStreamer::new(
263//!     Some("Example qlog".to_string()),
264//!     Some("Example qlog description".to_string()),
265//!     std::time::Instant::now(),
266//!     trace,
267//!     qlog::events::EventImportance::Base,
268//!     Box::new(file),
269//! );
270//!
271//! streamer.start_log().ok();
272//! ```
273//!
274//! ### Adding events
275//!
276//! Once logging has started you can stream events. Events
277//! are written in one step using one of [`add_event()`],
278//! [`add_event_with_instant()`], [`add_event_now()`],
279//! [`add_event_data_with_instant()`], or [`add_event_data_now()`] :
280//!
281//! ```
282//! # let mut trace = qlog::TraceSeq::new(
283//! #    Some("Example qlog trace".to_string()),
284//! #    Some("Example qlog trace description".to_string()),
285//! #    None,
286//! #    Some(qlog::VantagePoint {
287//! #        name: Some("Example client".to_string()),
288//! #        ty: qlog::VantagePointType::Client,
289//! #        flow: None,
290//! #    }),
291//! #    vec![qlog::events::QUIC_URI.to_string(), qlog::events::HTTP3_URI.to_string()],
292//! # );
293//! # let mut file = std::fs::File::create("foo.qlog").unwrap();
294//! # let mut streamer = qlog::streamer::QlogStreamer::new(
295//! #     Some("Example qlog".to_string()),
296//! #     Some("Example qlog description".to_string()),
297//! #     std::time::Instant::now(),
298//! #     trace,
299//! #     qlog::events::EventImportance::Base,
300//! #     Box::new(file),
301//! # );
302//!
303//! let scid = [0x7e, 0x37, 0xe4, 0xdc, 0xc6, 0x68, 0x2d, 0xa8];
304//! let dcid = [0x36, 0xce, 0x10, 0x4e, 0xee, 0x50, 0x10, 0x1c];
305//!
306//! let pkt_hdr = qlog::events::quic::PacketHeader::with_type(
307//!     qlog::events::quic::PacketType::OneRtt,
308//!     Some(0),
309//!     Some(0x00000001),
310//!     Some(&scid),
311//!     Some(&dcid),
312//! );
313//!
314//! let ping = qlog::events::quic::QuicFrame::Ping {
315//!     raw: None,
316//! };
317//!
318//! let raw = qlog::events::RawInfo {
319//!             length: None,
320//!             payload_length:
321//!             Some(1234), data: None
322//!           };
323//! let padding = qlog::events::quic::QuicFrame::Padding {
324//!     raw: Some(Box::new(raw)),
325//! };
326//!
327//! let event_data =
328//!     qlog::events::EventData::QuicPacketSent(qlog::events::quic::PacketSent {
329//!         header: pkt_hdr,
330//!         frames: Some(vec![ping, padding].into()),
331//!         stateless_reset_token: None,
332//!         supported_versions: None,
333//!         raw: None,
334//!         datagram_id: None,
335//!         is_mtu_probe_packet: None,
336//!         send_at_time: None,
337//!         trigger: None,
338//!     });
339//!
340//! let event = qlog::events::Event::with_time(0.0, event_data);
341//!
342//! streamer.add_event(event).ok();
343//! ```
344//!
345//! Once all events have been written, the log
346//! can be finalized with [`finish_log()`]:
347//!
348//! ```
349//! # let mut trace = qlog::TraceSeq::new(
350//! #    Some("Example qlog trace".to_string()),
351//! #    Some("Example qlog trace description".to_string()),
352//! #    None,
353//! #    Some(qlog::VantagePoint {
354//! #        name: Some("Example client".to_string()),
355//! #        ty: qlog::VantagePointType::Client,
356//! #        flow: None,
357//! #    }),
358//! #    vec![qlog::events::QUIC_URI.to_string(), qlog::events::HTTP3_URI.to_string()],
359//! # );
360//! # let mut file = std::fs::File::create("foo.qlog").unwrap();
361//! # let mut streamer = qlog::streamer::QlogStreamer::new(
362//! #     Some("Example qlog".to_string()),
363//! #     Some("Example qlog description".to_string()),
364//! #     std::time::Instant::now(),
365//! #     trace,
366//! #     qlog::events::EventImportance::Base,
367//! #     Box::new(file),
368//! # );
369//! streamer.finish_log().ok();
370//! ```
371//!
372//! ### Serializing
373//!
374//! Serialization to JSON occurs as methods on the [`QlogStreamer`]
375//! are called. No additional steps are required.
376//!
377//! [`Trace`]: struct.Trace.html
378//! [`TraceSeq`]: struct.TraceSeq.html
379//! [`VantagePoint`]: struct.VantagePoint.html
380//! [`Configuration`]: struct.Configuration.html
381//! [`qlog::Trace.events`]: struct.Trace.html#structfield.events
382//! [`push_event()`]: struct.Trace.html#method.push_event
383//! [`QlogStreamer`]: struct.QlogStreamer.html
384//! [`Write`]: https://doc.rust-lang.org/std/io/trait.Write.html
385//! [`start_log()`]: streamer/struct.QlogStreamer.html#method.start_log
386//! [`add_event()`]: streamer/struct.QlogStreamer.html#method.add_event
387//! [`add_event_with_instant()`]: streamer/struct.QlogStreamer.html#method.add_event_with_instant
388//! [`add_event_now()`]: streamer/struct.QlogStreamer.html#method.add_event_now
389//! [`add_event_data_with_instant()`]: streamer/struct.QlogStreamer.html#method.add_event_data_with_instant
390//! [`add_event_data_now()`]: streamer/struct.QlogStreamer.html#method.add_event_data_now
391//! [`finish_log()`]: streamer/struct.QlogStreamer.html#method.finish_log
392
393use std::time::SystemTime;
394
395use crate::events::quic::PacketHeader;
396use crate::events::Event;
397
398use serde::Deserialize;
399use serde::Serialize;
400
401/// A quiche qlog error.
402#[derive(Debug)]
403pub enum Error {
404    /// There is no more work to do.
405    Done,
406
407    /// The operation cannot be completed because it was attempted
408    /// in an invalid state.
409    InvalidState,
410
411    // Invalid Qlog format
412    InvalidFormat,
413
414    /// I/O error.
415    IoError(std::io::Error),
416}
417
418impl std::fmt::Display for Error {
419    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
420        write!(f, "{self:?}")
421    }
422}
423
424impl std::error::Error for Error {
425    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
426        None
427    }
428}
429
430impl std::convert::From<std::io::Error> for Error {
431    fn from(err: std::io::Error) -> Self {
432        Error::IoError(err)
433    }
434}
435
436pub const QLOGFILE_URI: &str = "urn:ietf:params:qlog:file:contained";
437pub const QLOGFILESEQ_URI: &str = "urn:ietf:params:qlog:file:sequential";
438
439pub type Bytes = String;
440pub type StatelessResetToken = Bytes;
441
442/// A specialized [`Result`] type for quiche qlog operations.
443///
444/// This type is used throughout the public API for any operation that
445/// can produce an error.
446///
447/// [`Result`]: https://doc.rust-lang.org/std/result/enum.Result.html
448pub type Result<T> = std::result::Result<T, Error>;
449
450#[serde_with::skip_serializing_none]
451#[derive(Serialize, Deserialize, Clone)]
452pub struct Qlog {
453    pub file_schema: String,
454    pub serialization_format: String,
455    pub title: Option<String>,
456    pub description: Option<String>,
457
458    pub traces: Vec<Trace>,
459}
460#[serde_with::skip_serializing_none]
461#[derive(Serialize, Deserialize, Clone, Debug)]
462pub struct QlogSeq {
463    pub file_schema: String,
464    pub serialization_format: String,
465    pub title: Option<String>,
466    pub description: Option<String>,
467
468    pub trace: TraceSeq,
469}
470
471#[derive(Clone, Copy)]
472pub enum ImportanceLogLevel {
473    Core  = 0,
474    Base  = 1,
475    Extra = 2,
476}
477
478// We now commence data definitions heavily styled on the QLOG
479// schema definition. Data is serialized using serde.
480#[serde_with::skip_serializing_none]
481#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
482pub struct Trace {
483    pub title: Option<String>,
484    pub description: Option<String>,
485    pub common_fields: Option<CommonFields>,
486    pub vantage_point: Option<VantagePoint>,
487    pub event_schemas: Vec<String>,
488
489    pub events: Vec<Event>,
490}
491
492/// Helper functions for using a qlog [Trace].
493impl Trace {
494    /// Creates a new qlog [Trace]
495    pub fn new(
496        title: Option<String>, description: Option<String>,
497        common_fields: Option<CommonFields>, vantage_point: Option<VantagePoint>,
498        event_schemas: Vec<String>,
499    ) -> Self {
500        Trace {
501            title,
502            description,
503            common_fields,
504            vantage_point,
505            event_schemas,
506            events: Vec::new(),
507        }
508    }
509
510    /// Append an [Event] to a [Trace]
511    pub fn push_event(&mut self, event: Event) {
512        self.events.push(event);
513    }
514}
515
516#[serde_with::skip_serializing_none]
517#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
518pub struct TraceSeq {
519    pub title: Option<String>,
520    pub description: Option<String>,
521    pub common_fields: Option<CommonFields>,
522    pub vantage_point: Option<VantagePoint>,
523    pub event_schemas: Vec<String>,
524}
525
526/// Helper functions for using a qlog [TraceSeq].
527impl TraceSeq {
528    /// Creates a new qlog [TraceSeq]
529    pub fn new(
530        title: Option<String>, description: Option<String>,
531        common_fields: Option<CommonFields>, vantage_point: Option<VantagePoint>,
532        event_schemas: Vec<String>,
533    ) -> Self {
534        TraceSeq {
535            title,
536            description,
537            common_fields,
538            vantage_point,
539            event_schemas,
540        }
541    }
542}
543
544#[serde_with::skip_serializing_none]
545#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
546pub struct VantagePoint {
547    pub name: Option<String>,
548
549    #[serde(rename = "type")]
550    pub ty: VantagePointType,
551
552    pub flow: Option<VantagePointType>,
553}
554
555#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
556#[serde(rename_all = "snake_case")]
557pub enum VantagePointType {
558    Client,
559    Server,
560    Network,
561    #[default]
562    Unknown,
563}
564
565#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, Default)]
566#[serde(rename_all = "snake_case")]
567pub enum TimeFormat {
568    #[default]
569    RelativeToEpoch,
570    RelativeToPreviousEvent,
571}
572
573#[serde_with::skip_serializing_none]
574#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Debug)]
575#[serde(rename_all = "snake_case")]
576pub struct ReferenceTime {
577    pub clock_type: String,
578    pub epoch: String,
579    pub wall_clock_time: Option<String>,
580}
581
582impl ReferenceTime {
583    /// Create a new `ReferenceTime` instance that uses a monotonic clock.
584    ///
585    /// If `wall_clock_time` is specified, it will be added as the optional
586    /// `wall_clock_time` field of `ReferenceTime`.
587    pub fn new_monotonic(wall_clock_time: Option<SystemTime>) -> Self {
588        let wall_clock_time =
589            wall_clock_time.map(|t| humantime::format_rfc3339(t).to_string());
590        ReferenceTime {
591            clock_type: "monotonic".to_string(),
592            // per draft-ietf-quic-qlog-main-schema-13 epoch must be "unknown"
593            // for monotonic clocks
594            epoch: "unknown".to_string(),
595            wall_clock_time,
596        }
597    }
598}
599
600#[serde_with::skip_serializing_none]
601#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Debug)]
602pub struct CommonFields {
603    pub tuple: Option<String>,
604    pub group_id: Option<String>,
605    pub protocol_types: Option<Vec<String>>,
606
607    pub reference_time: ReferenceTime,
608    pub time_format: Option<TimeFormat>,
609}
610
611#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
612#[serde(rename_all = "snake_case")]
613pub enum TokenType {
614    Retry,
615    Resumption,
616}
617
618#[serde_with::skip_serializing_none]
619#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
620pub struct Token {
621    #[serde(rename(serialize = "type"))]
622    pub ty: Option<TokenType>,
623
624    pub details: Option<String>,
625
626    pub raw: Option<events::RawInfo>,
627}
628
629pub struct HexSlice<'a>(&'a [u8]);
630
631impl<'a> HexSlice<'a> {
632    pub fn new<T>(data: &'a T) -> HexSlice<'a>
633    where
634        T: ?Sized + AsRef<[u8]> + 'a,
635    {
636        HexSlice(data.as_ref())
637    }
638
639    pub fn maybe_string<T>(data: Option<&'a T>) -> Option<String>
640    where
641        T: ?Sized + AsRef<[u8]> + 'a,
642    {
643        data.map(|d| format!("{}", HexSlice::new(d)))
644    }
645}
646
647impl std::fmt::Display for HexSlice<'_> {
648    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
649        for byte in self.0 {
650            write!(f, "{byte:02x}")?;
651        }
652        Ok(())
653    }
654}
655
656pub mod events;
657pub mod reader;
658pub mod streamer;
659#[doc(hidden)]
660pub mod testing;
661
662#[cfg(test)]
663mod tests {
664    use std::time::Duration;
665    use std::time::UNIX_EPOCH;
666
667    use super::ReferenceTime;
668
669    #[test]
670    fn reference_time_new_monotonic_serialization() {
671        // 2024-01-15T10:30:00Z = 1705314600 seconds after UNIX epoch
672        let t = UNIX_EPOCH + Duration::from_secs(1_705_314_600);
673        let rt = ReferenceTime::new_monotonic(Some(t));
674        let map: serde_json::Map<String, serde_json::Value> =
675            serde_json::from_str(&serde_json::to_string(&rt).unwrap()).unwrap();
676
677        assert_eq!(map["clock_type"], "monotonic");
678        assert_eq!(map["epoch"], "unknown");
679        assert_eq!(map["wall_clock_time"], "2024-01-15T10:30:00Z");
680
681        let rt = ReferenceTime::new_monotonic(None);
682        let map: serde_json::Map<String, serde_json::Value> =
683            serde_json::from_str(&serde_json::to_string(&rt).unwrap()).unwrap();
684
685        assert_eq!(map["clock_type"], "monotonic");
686        assert_eq!(map["epoch"], "unknown");
687        assert!(!map.contains_key("wall_clock_time"));
688    }
689}