drive_abci/logging/
destination.rs

1use super::error::Error;
2use crate::logging::config::LogConfig;
3use file_rotate::suffix::{AppendTimestamp, FileLimit};
4use file_rotate::{ContentLimit, FileRotate, TimeFrequency};
5use reopen::Reopen;
6use std::fmt::{Debug, Display};
7use std::fs::{File, OpenOptions};
8use std::io::Write;
9use std::os::unix::prelude::*;
10use std::path::{Path, PathBuf};
11#[cfg(test)]
12use std::str::from_utf8;
13use tracing_subscriber::fmt::writer::TestWriter;
14
15use serde::de::Visitor;
16use serde::{de, Deserialize, Deserializer, Serialize};
17use std::sync::{Arc, Mutex};
18use std::{fmt, fs, path};
19
20/// Log destination configuration that will be converted to LogDestinationWriter
21#[derive(Default, Serialize, Clone, Debug)]
22pub enum LogDestination {
23    /// Standard error
24    StdErr,
25    #[default]
26    /// Standard out
27    StdOut,
28    /// Test output (shown on failure unless --nocapture)
29    TestWriter,
30    /// File
31    File(PathBuf),
32    /// Blob of bytes for testing
33    #[cfg(test)]
34    Bytes,
35}
36
37impl Display for LogDestination {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            LogDestination::StdErr => write!(f, "stderr"),
41            LogDestination::StdOut => write!(f, "stdout"),
42            LogDestination::TestWriter => write!(f, "testwriter"),
43            LogDestination::File(path) => write!(f, "{}", path.to_string_lossy()),
44            #[cfg(test)]
45            LogDestination::Bytes => write!(f, "bytes"),
46        }
47    }
48}
49
50/// Creates log destination from string
51impl From<&str> for LogDestination {
52    fn from(value: &str) -> Self {
53        match value {
54            "stdout" => LogDestination::StdOut,
55            "stderr" => LogDestination::StdErr,
56            "testwriter" => LogDestination::TestWriter,
57            #[cfg(test)]
58            "bytes" => LogDestination::Bytes,
59            file_path => LogDestination::File(PathBuf::from(file_path)),
60        }
61    }
62}
63
64struct LogDestinationVisitor;
65
66impl Visitor<'_> for LogDestinationVisitor {
67    type Value = LogDestination;
68
69    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
70        formatter.write_str("stdout, stderr, or absolute path to a file log destination")
71    }
72
73    fn visit_str<E>(self, value: &str) -> Result<LogDestination, E>
74    where
75        E: de::Error,
76    {
77        let destination = LogDestination::from(value);
78
79        Ok(destination)
80    }
81}
82
83impl<'de> Deserialize<'de> for LogDestination {
84    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
85    where
86        D: Deserializer<'de>,
87    {
88        deserializer.deserialize_str(LogDestinationVisitor)
89    }
90}
91
92/// Writer wraps Arc<Mutex<...>> data structure to implement std::io::Write on it.
93///
94/// Implementation of std::io::Write is required by [tracing_subscriber] crate.
95pub(super) struct Writer<T>(Arc<Mutex<T>>)
96where
97    T: Write;
98
99impl<T> Write for Writer<T>
100where
101    T: Write,
102{
103    delegate::delegate! {
104        to self.0.lock().expect("logging mutex poisoned") {
105            #[inline]
106            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize>;
107
108            #[inline]
109            fn flush(&mut self) -> std::io::Result<()> ;
110
111            #[inline]
112            fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize>;
113
114            #[inline]
115            fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()>;
116
117            #[inline]
118            fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()>;
119        }
120    }
121}
122
123impl<T> Writer<T>
124where
125    T: Write,
126{
127    /// Create writer
128    pub(super) fn new(write: Arc<Mutex<T>>) -> Self {
129        Self(write)
130    }
131}
132
133impl<T: Write> From<T> for Writer<T> {
134    fn from(value: T) -> Self {
135        Self(Arc::new(Mutex::new(value)))
136    }
137}
138
139impl<T: Write> Clone for Writer<T> {
140    fn clone(&self) -> Self {
141        Self(self.0.clone())
142    }
143}
144
145/// Log destination represents actual destination (implementing std::io::Write) where logs are sent
146pub(super) enum LogDestinationWriter {
147    /// Standard error
148    StdErr,
149    /// Standard out
150    StdOut,
151    /// Test output (shown on failure unless --nocapture)
152    TestWriter,
153    /// File
154    File(Writer<Reopen<File>>),
155    /// Rotated file
156    RotationWriter(Writer<FileRotate<AppendTimestamp>>),
157    #[cfg(test)]
158    // Just some bytes, for testing
159    Bytes(Writer<Vec<u8>>),
160}
161
162impl LogDestinationWriter {
163    /// Convert this log destination to std::io::Write implementation
164    pub fn to_write(&self) -> Box<dyn Write> {
165        match self {
166            LogDestinationWriter::StdErr => Box::new(std::io::stderr()) as Box<dyn Write>,
167            LogDestinationWriter::StdOut => Box::new(std::io::stdout()) as Box<dyn Write>,
168            LogDestinationWriter::TestWriter => Box::new(TestWriter::new()) as Box<dyn Write>,
169            LogDestinationWriter::File(f) => Box::new(f.clone()) as Box<dyn Write>,
170            LogDestinationWriter::RotationWriter(w) => Box::new(w.clone()) as Box<dyn Write>,
171            #[cfg(test)]
172            LogDestinationWriter::Bytes(w) => Box::new(w.clone()) as Box<dyn Write>,
173        }
174    }
175
176    /// Return human-readable name of selected log destination
177    pub fn name(&self) -> String {
178        let s = match self {
179            LogDestinationWriter::StdOut => "stdout",
180            LogDestinationWriter::StdErr => "stderr",
181            LogDestinationWriter::TestWriter => "testwriter",
182            LogDestinationWriter::File(_) => "file",
183            LogDestinationWriter::RotationWriter(_) => "RotationWriter",
184            #[cfg(test)]
185            LogDestinationWriter::Bytes(_) => "ByteBuffer",
186        };
187
188        String::from(s)
189    }
190
191    /// Rotate log file
192    pub fn rotate(&self) -> Result<(), Error> {
193        match self {
194            LogDestinationWriter::RotationWriter(ref writer) => {
195                let mut file_rotate_guard = writer.0.lock().expect("logging lock poisoned");
196
197                file_rotate_guard.rotate().map_err(Error::FileRotate)?;
198            }
199            LogDestinationWriter::File(ref f) => {
200                let mut file_reopen_guard = f.0.lock().expect("logging lock poisoned");
201
202                file_reopen_guard
203                    .flush()
204                    .map_err(Error::FileRotate)
205                    .map(|_| {
206                        file_reopen_guard.handle().reopen();
207                    })?
208            }
209            _ => {}
210        };
211
212        Ok(())
213    }
214
215    /// Reads data written into destination
216    ///
217    /// Only [LogDestinationWriter::Bytes] and [LogDestinationWriter::RotationWriter] are supported.
218    ///
219    /// Contract: LogDestinationWriter::RotationWriter was rotated
220    #[cfg(test)]
221    pub fn read_as_string(&self) -> String {
222        match self {
223            LogDestinationWriter::Bytes(b) => {
224                let guard = b.0.lock().unwrap();
225                let b = guard.clone();
226
227                from_utf8(b.as_slice()).unwrap().to_string()
228            }
229            LogDestinationWriter::RotationWriter(w) => {
230                let paths = w.0.lock().unwrap().log_paths();
231                let path = paths.first().expect("exactly one path excepted");
232                fs::read_to_string(path).unwrap()
233            }
234            _ => todo!(),
235        }
236    }
237}
238
239impl TryFrom<&LogConfig> for FileRotate<AppendTimestamp> {
240    type Error = Error;
241    /// Configure new FileRotate based on log configuration.
242    ///
243    /// In future, we might allow more detailed configuration, like log rotation frequency, compression, etc.
244    fn try_from(config: &LogConfig) -> Result<Self, Self::Error> {
245        let suffix_scheme = AppendTimestamp::default(FileLimit::MaxFiles(config.max_files));
246        let content_limit = ContentLimit::Time(TimeFrequency::Daily);
247        let compression = file_rotate::compression::Compression::OnRotate(2);
248        // Only owner can see logs
249        let mode = Some(0o600);
250        let path = PathBuf::from(&config.destination.to_string());
251
252        let f = FileRotate::new(path, suffix_scheme, content_limit, compression, mode);
253
254        Ok(f)
255    }
256}
257
258impl TryFrom<&LogConfig> for Reopen<File> {
259    type Error = Error;
260    /// Configure new File based on log configuration.
261    fn try_from(config: &LogConfig) -> Result<Self, Self::Error> {
262        // Only owner can see logs
263        let mode = 0o600;
264        let path = PathBuf::from(&config.destination.to_string());
265
266        let opened_path = path.clone();
267        let open_fn = move || {
268            OpenOptions::new()
269                .create(true)
270                .append(true)
271                .mode(mode)
272                .open(&opened_path)
273        };
274
275        Reopen::new(Box::new(open_fn)).map_err(|e| Error::FileCreate(path, e))
276    }
277}
278
279impl TryFrom<&LogConfig> for LogDestinationWriter {
280    type Error = Error;
281
282    fn try_from(value: &LogConfig) -> Result<Self, Self::Error> {
283        let destination = match &value.destination {
284            LogDestination::StdOut => LogDestinationWriter::StdOut,
285            LogDestination::StdErr => LogDestinationWriter::StdErr,
286            LogDestination::TestWriter => LogDestinationWriter::TestWriter,
287            #[cfg(test)]
288            LogDestination::Bytes => LogDestinationWriter::Bytes(Vec::<u8>::new().into()),
289            LogDestination::File(path_string) => {
290                let path = PathBuf::from(path_string);
291
292                validate_log_path(path)?;
293
294                if value.max_files > 0 {
295                    let file: FileRotate<AppendTimestamp> = FileRotate::try_from(value)?;
296                    LogDestinationWriter::RotationWriter(file.into())
297                } else {
298                    let file: Reopen<File> = value.try_into()?;
299                    LogDestinationWriter::File(file.into())
300                }
301            }
302        };
303
304        Ok(destination)
305    }
306}
307
308impl Debug for LogDestinationWriter {
309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310        f.write_str(&self.name())
311    }
312}
313
314/// Whenever we want to write to log destination, we delegate to the Writer implementation
315impl Write for LogDestinationWriter {
316    delegate::delegate! {
317        to self.to_write() {
318            #[inline]
319            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> ;
320
321            #[inline]
322            fn flush(&mut self) -> std::io::Result<()> ;
323
324            #[inline]
325            fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> ;
326
327            #[inline]
328            fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> ;
329
330            #[inline]
331            fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()> ;
332        }
333    }
334}
335
336/// Verify log file path.
337///
338/// Ensure that the log file path is correct, that is:
339/// - it points to a file, not a directory
340/// - if the log file exists, it is writable for the current user
341/// - parent directory of the file exists and is writable for current user
342/// - path is absolute
343fn validate_log_path<T: AsRef<Path>>(log_file_path: T) -> Result<(), Error> {
344    let log_file_path = log_file_path.as_ref();
345
346    if !log_file_path.is_absolute() {
347        return Err(Error::FilePath(
348            log_file_path.to_owned(),
349            "log file path must be absolute".to_string(),
350        ));
351    }
352
353    if log_file_path.exists() {
354        // Make sure log file is writable
355        if log_file_path.is_dir() {
356            return Err(Error::FilePath(
357                log_file_path.to_owned(),
358                "log file path must point to file".to_string(),
359            ));
360        }
361
362        let md = fs::metadata(log_file_path).map_err(|e| {
363            Error::FilePath(
364                log_file_path.to_owned(),
365                format!("cannot read log file metadata: {}", e),
366            )
367        })?;
368
369        if md.permissions().readonly() {
370            return Err(Error::FilePath(
371                log_file_path.to_owned(),
372                "log file is readonly".to_string(),
373            ));
374        }
375    } else if log_file_path.ends_with(String::from(path::MAIN_SEPARATOR)) {
376        // If file doesn't exist we need to do at least simple validation
377        return Err(Error::FilePath(
378            log_file_path.to_owned(),
379            "log file path must point to file".to_string(),
380        ));
381    }
382
383    // Make sure parent directly is writable so log rotation can work
384    let parent_dir = log_file_path
385        .parent()
386        .expect("absolute log file path will always have parent");
387
388    let md = fs::metadata(parent_dir).map_err(|e| {
389        Error::FilePath(
390            log_file_path.to_owned(),
391            format!("cannot read parent directory: {}", e),
392        )
393    })?;
394
395    let permissions = md.permissions();
396    if permissions.readonly() {
397        return Err(Error::FilePath(
398            log_file_path.to_owned(),
399            "parent directory is readonly".to_string(),
400        ));
401    }
402
403    Ok(())
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    use std::fs;
411    use std::fs::OpenOptions;
412    use std::path::Path;
413    use tempfile::tempdir;
414
415    #[test]
416    fn test_validate_log_path_file_exists_but_readonly() {
417        let dir = tempdir().unwrap();
418        let file_path = dir.path().join("log.txt");
419        OpenOptions::new()
420            .write(true)
421            .create(true)
422            .open(&file_path)
423            .unwrap();
424        let mut perms = fs::metadata(&file_path).unwrap().permissions();
425        perms.set_mode(0o444);
426        fs::set_permissions(&file_path, perms).unwrap();
427
428        assert!(
429            matches!(validate_log_path(&file_path), Err(Error::FilePath(_, message)) if message == "log file is readonly")
430        );
431    }
432
433    #[test]
434    fn test_validate_log_path_parent_directory_not_writable() {
435        let dir = tempdir().unwrap();
436        let file_path = dir.path().join("log.txt");
437        let mut perms = fs::metadata(dir.path()).unwrap().permissions();
438        perms.set_mode(0o555);
439        fs::set_permissions(dir.path(), perms).unwrap();
440
441        assert!(
442            matches!(validate_log_path(file_path), Err(Error::FilePath(_, message)) if message == "parent directory is readonly")
443        );
444    }
445
446    #[test]
447    fn test_validate_log_path_points_to_directory() {
448        let dir = tempdir().unwrap();
449
450        assert!(
451            matches!(validate_log_path(dir.path()), Err(Error::FilePath(_, message)) if message == "log file path must point to file")
452        );
453    }
454
455    #[test]
456    fn test_validate_log_path_not_absolute() {
457        let relative_path = Path::new("log.txt");
458
459        assert!(
460            matches!(validate_log_path(relative_path), Err(Error::FilePath(_, message)) if message == "log file path must be absolute")
461        );
462    }
463
464    #[test]
465    fn test_validate_log_path_file_exists_and_writable() {
466        let dir = tempdir().unwrap();
467        let file_path = dir.path().join("log.txt");
468        OpenOptions::new()
469            .write(true)
470            .create(true)
471            .open(&file_path)
472            .unwrap();
473
474        assert!(validate_log_path(&file_path).is_ok());
475    }
476}