iroh_quinn_proto/config/
qlog.rs

1#[cfg(feature = "qlog")]
2use std::io;
3use std::{io::BufWriter, net::SocketAddr, path::PathBuf, time::SystemTime};
4
5use tracing::{trace, warn};
6
7use crate::{ConnectionId, Instant, Side};
8
9/// Constructs a [`QlogConfig`] for individual connections.
10///
11/// This is set via [`TransportConfig::qlog_factory`].
12///
13/// [`TransportConfig::qlog_factory]: crate::config::TransportConfig::qlog_factory
14pub trait QlogFactory: Send + Sync + 'static {
15    /// Returns a [`QlogConfig`] for a connection, if logging should be enabled.
16    ///
17    /// If `None` is returned, qlog capture is disabled for the connection.
18    fn for_connection(
19        &self,
20        side: Side,
21        remote: SocketAddr,
22        initial_dst_cid: ConnectionId,
23        now: Instant,
24    ) -> Option<QlogConfig>;
25}
26
27/// Configuration for qlog trace logging.
28///
29/// This struct is returned from [`QlogFactory::for_connection`] if qlog logging should
30/// be enabled for a connection. It allows to set metadata for the qlog trace.
31///
32/// The trace will be written to the provided writer in the [`JSON-SEQ format`] defined in the qlog spec.
33///
34/// [`JSON-SEQ format`](https://www.ietf.org/archive/id/draft-ietf-quic-qlog-main-schema-13.html#section-5)
35#[cfg(feature = "qlog")]
36pub struct QlogConfig {
37    pub(crate) writer: Box<dyn io::Write + Send + Sync>,
38    pub(crate) title: Option<String>,
39    pub(crate) description: Option<String>,
40    pub(crate) start_time: Option<Instant>,
41}
42
43#[cfg(feature = "qlog")]
44impl QlogConfig {
45    /// Creates a new [`QlogConfig`] that writes a qlog trace to the specified `writer`.
46    pub fn new(writer: Box<dyn io::Write + Send + Sync>) -> Self {
47        Self {
48            writer,
49            title: None,
50            description: None,
51            start_time: None,
52        }
53    }
54
55    /// Title to record in the qlog capture
56    pub fn title(&mut self, title: Option<String>) -> &mut Self {
57        self.title = title;
58        self
59    }
60
61    /// Description to record in the qlog capture
62    pub fn description(&mut self, description: Option<String>) -> &mut Self {
63        self.description = description;
64        self
65    }
66
67    /// Epoch qlog event times are recorded relative to
68    ///
69    /// If unset, the start of the connection is used.
70    pub fn start_time(&mut self, start_time: Instant) -> &mut Self {
71        self.start_time = Some(start_time);
72        self
73    }
74}
75
76/// Enables writing qlog traces to a directory.
77#[derive(Debug)]
78pub struct QlogFileFactory {
79    dir: Option<PathBuf>,
80    prefix: Option<String>,
81    start_instant: Option<Instant>,
82}
83
84impl QlogFileFactory {
85    /// Creates a new qlog factory that writes files into the specified directory.
86    pub fn new(dir: PathBuf) -> Self {
87        Self {
88            dir: Some(dir),
89            prefix: None,
90            start_instant: None,
91        }
92    }
93
94    /// Creates a new qlog factory that writes files into `QLOGDIR`, if set.
95    ///
96    /// If the environment variable `QLOGDIR` is set, qlog traces for all connections handled
97    /// by this endpoint will be written into that directory.
98    /// If the directory doesn't exist it will be created.
99    pub fn from_env() -> Self {
100        let dir = match std::env::var("QLOGDIR") {
101            Ok(dir) => {
102                if let Err(err) = std::fs::create_dir_all(&dir) {
103                    warn!("qlog not enabled: failed to create qlog directory at {dir}: {err}",);
104                    None
105                } else {
106                    Some(PathBuf::from(dir))
107                }
108            }
109            Err(_) => None,
110        };
111        Self {
112            dir,
113            prefix: None,
114            start_instant: None,
115        }
116    }
117
118    /// Sets a prefix to the filename of the generated files.
119    pub fn with_prefix(mut self, prefix: impl ToString) -> Self {
120        self.prefix = Some(prefix.to_string());
121        self
122    }
123
124    /// Override the instant relative to which all events are recorded.
125    ///
126    /// If not set, events will be recorded relative to the start of the connection.
127    pub fn with_start_instant(mut self, start: Instant) -> Self {
128        self.start_instant = Some(start);
129        self
130    }
131}
132
133impl QlogFactory for QlogFileFactory {
134    fn for_connection(
135        &self,
136        side: Side,
137        _remote: SocketAddr,
138        initial_dst_cid: ConnectionId,
139        now: Instant,
140    ) -> Option<QlogConfig> {
141        let dir = self.dir.as_ref()?;
142
143        let name = {
144            let timestamp = SystemTime::now()
145                .checked_sub(Instant::now().duration_since(now))?
146                .duration_since(SystemTime::UNIX_EPOCH)
147                .ok()?
148                .as_millis();
149            let prefix = self
150                .prefix
151                .as_ref()
152                .filter(|prefix| !prefix.is_empty())
153                .map(|prefix| format!("{prefix}-"))
154                .unwrap_or_default();
155            let side = format!("{side:?}").to_lowercase();
156            format!("{prefix}{timestamp}-{initial_dst_cid}-{side}.qlog")
157        };
158        let path = dir.join(name);
159        let file = std::fs::File::create(&path)
160            .inspect_err(|err| warn!("Failed to create qlog file at {}: {err}", path.display()))
161            .ok()?;
162        trace!(
163            "Initialized qlog file for connection {initial_dst_cid} at {}",
164            path.display()
165        );
166        let writer = BufWriter::new(file);
167        let mut config = QlogConfig::new(Box::new(writer));
168        if let Some(instant) = self.start_instant {
169            config.start_time(instant);
170        }
171        Some(config)
172    }
173}