diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1ca1d80290..e9867a3446 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,6 +4,10 @@ { "type": "cargo", "command": "build", + "args": [ + "--features", + "compression" + ], "problemMatcher": [ "$rustc" ], diff --git a/tracing-appender/Cargo.toml b/tracing-appender/Cargo.toml index 1e51039a99..4428601ba4 100644 --- a/tracing-appender/Cargo.toml +++ b/tracing-appender/Cargo.toml @@ -20,10 +20,14 @@ keywords = ["logging", "tracing", "file-appender", "non-blocking-writer"] edition = "2018" rust-version = "1.53.0" +[features] +compression_gzip = ["flate2"] + [dependencies] crossbeam-channel = "0.5.0" time = { version = "0.3", default-features = false, features = ["formatting"] } parking_lot = { optional = true, version = "0.12.0" } +flate2 = { optional = true, version = "1.0.22" } [dependencies.tracing-subscriber] path = "../tracing-subscriber" diff --git a/tracing-appender/src/lib.rs b/tracing-appender/src/lib.rs index 43f946a486..a3ccd5ff29 100644 --- a/tracing-appender/src/lib.rs +++ b/tracing-appender/src/lib.rs @@ -161,6 +161,8 @@ mod worker; pub(crate) mod sync; +mod writer; + /// Convenience function for creating a non-blocking, off-thread writer. /// /// See the [`non_blocking` module's docs][mod@non_blocking]'s for more details. diff --git a/tracing-appender/src/rolling/builder.rs b/tracing-appender/src/rolling/builder.rs new file mode 100644 index 0000000000..816a569893 --- /dev/null +++ b/tracing-appender/src/rolling/builder.rs @@ -0,0 +1,167 @@ +//! Builder struct for `RollingFileAppender` +//! +//! Gives access to setting additional options which are not avaible using standard interface. +//! Currently it is the only way to enable compression of logs. +use crate::rolling::{create_writer_file, Inner, RollingFileAppender, Rotation}; +use crate::sync::RwLock; +use std::path::Path; +use std::sync::atomic::AtomicUsize; +use time::OffsetDateTime; + +#[cfg(feature = "compression_gzip")] +use crate::rolling::compression::{CompressionConfig, CompressionOption}; + +use crate::writer::WriterChannel; + +/// A builder for configuring new [`RollingFileAppender`]s. +/// +/// Note that `log_directory` and `log_filename_prefix` are obligatory parameters and should +/// be passed into the constructor of `RollingFileAppenderBuilder`. +#[derive(Debug, Clone)] +pub struct Builder { + log_directory: String, + log_filename_prefix: String, + rotation: Option, + #[cfg(feature = "compression_gzip")] + compression: Option, +} + +impl Builder { + /// Returns a new builder for configuring a [`RollingFileAppender`], with + /// the provided log directory and filename prefix. + /// + /// Calling [`build`] on the returned builder will construct a new + /// [`RollingFileAppender`] that writes to files in the provided directory. + /// By default, log files will never be rotated and compression will not be + /// enabled. A rotation policy can be added to the builder using the + /// [`rotation`] method. When the "compression" feature flag is enabled, + /// compression can be configured using the [`compression`] method. + /// + /// # Panics + /// + /// This function panics if the provided log directory or log file prefix + /// are not valid UTF-8. + /// + /// # Examples + /// + /// Building a `RollingFileAppender` with the default configuration: + /// + /// ```rust + /// use tracing_appender::rolling::Builder; + /// let appender = Builder::new("/var/tmp", "my-app") + /// .build(); + /// ``` + /// + /// Enabling compression (needs a feature enabled `compression_gzip`): + /// + /// ```rust + /// #[cfg(feature = "compression_gzip")] + /// use tracing_appender::{ + /// rolling::{Builder, Rotation}, + /// rolling::compression::CompressionOption, + /// }; + /// #[cfg(feature = "compression_gzip")] + /// let appender = Builder::new("/var/tmp", "my-app") + /// .rotation(Rotation::DAILY) + /// .compression(CompressionOption::GzipFast) + /// .build(); + /// ``` + pub fn new(log_directory: impl AsRef, log_filename_prefix: impl AsRef) -> Self { + let log_directory = log_directory + .as_ref() + .to_str() + .expect("`log_directory` must not contain invalid UTF-8 characters") + .to_string(); + let log_filename_prefix = log_filename_prefix + .as_ref() + .to_str() + .expect("`log_directory` must not contain invalid UTF-8 characters") + .to_string(); + Builder { + log_directory, + log_filename_prefix, + rotation: None, + #[cfg(feature = "compression_gzip")] + compression: None, + } + } + + /// Configures when log files will be rotated. + /// + /// By default, no rotation will occur. + pub fn rotation(mut self, rotation: Rotation) -> Self { + self.rotation = Some(rotation); + self + } + + /// Sets compression level + #[cfg(feature = "compression_gzip")] + #[cfg_attr(docsrs, doc(cfg(feature = "compression_gzip")))] + pub fn compression(mut self, compression: CompressionOption) -> Self { + self.compression = Some(compression.into()); + self + } + + pub(crate) fn get_extension(&self) -> Option { + #[cfg(feature = "compression_gzip")] + if let Some(compression) = self.compression.clone() { + compression.extension().map(|v| v.to_string()) + } else { + None + } + + #[cfg(not(feature = "compression_gzip"))] + None + } + + /// Returns a new [`RollingFileAppender`] with the configuration defined by this builder. + pub fn build(self) -> RollingFileAppender { + let now = OffsetDateTime::now_utc(); + let rotation = self.rotation.clone().unwrap_or(Rotation::NEVER); + let extension = self.get_extension(); + let filename = rotation.join_date(self.log_filename_prefix.as_str(), &now, extension); + let next_date = rotation.next_date(&now); + + #[cfg(not(feature = "compression_gzip"))] + let writer = self.create_file_writer(filename.as_str()); + + #[cfg(feature = "compression_gzip")] + let writer = if let Some(compression) = self.compression.clone() { + RwLock::new( + WriterChannel::new_with_compression( + self.log_directory.as_str(), + &filename, + compression, + ) + .unwrap(), + ) + } else { + self.create_file_writer(filename.as_str()) + }; + + let next_date = AtomicUsize::new( + next_date + .map(|date| date.unix_timestamp() as usize) + .unwrap_or(0), + ); + + RollingFileAppender { + state: Inner { + log_directory: self.log_directory, + log_filename_prefix: self.log_filename_prefix, + next_date, + rotation, + #[cfg(feature = "compression_gzip")] + compression: self.compression, + }, + writer, + } + } + + fn create_file_writer(&self, filename: &str) -> RwLock { + RwLock::new(WriterChannel::File( + create_writer_file(self.log_directory.as_str(), filename) + .expect("failed to create appender"), + )) + } +} diff --git a/tracing-appender/src/rolling/compression.rs b/tracing-appender/src/rolling/compression.rs new file mode 100644 index 0000000000..2ba4e09d4b --- /dev/null +++ b/tracing-appender/src/rolling/compression.rs @@ -0,0 +1,220 @@ +//! Defines configuration for passing compression options +//! +//! Currently only gzip compression is implemented. +use flate2::Compression; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) enum GzipCompressionLevelLiteral { + None, + Fast, + Best, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +#[repr(u8)] +pub(crate) enum GzipCompressionLevelNumerical { + Level0 = 0, + Level1 = 1, + Level2 = 2, + Level3 = 3, + Level4 = 4, + Level5 = 5, + Level6 = 6, + Level7 = 7, + Level8 = 8, + Level9 = 9, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) enum GzipCompressionLevel { + Literal(GzipCompressionLevelLiteral), + Numerical(GzipCompressionLevelNumerical), +} + +/// Defines a conversion between `CompressionOption` and `flate2::Compression` +impl Into for GzipCompressionLevel { + fn into(self) -> Compression { + match self { + GzipCompressionLevel::Literal(lit) => match lit { + GzipCompressionLevelLiteral::None => Compression::none(), + GzipCompressionLevelLiteral::Fast => Compression::fast(), + GzipCompressionLevelLiteral::Best => Compression::best(), + }, + GzipCompressionLevel::Numerical(num) => Compression::new(num as u32), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct GzipCompression { + pub(crate) level: GzipCompressionLevel, +} + +/// Data structure to pass compression parameters +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) enum CompressionConfig { + Gzip(GzipCompression), +} + +impl CompressionConfig { + pub(crate) fn gz_compress_level(&self) -> Compression { + match self { + CompressionConfig::Gzip(gz) => gz.level.clone().into(), + } + } + + #[allow(unused)] + pub(crate) fn extension(&self) -> Option<&str> { + match self { + CompressionConfig::Gzip(_) => Some("gz"), + } + } +} + +/// Defines a compression level for gzip algorithm. +/// +/// Compression levels are defined as they are in `flate2` crate where +/// - compression level 0 (`CompressionOption::GzipNone` or `CompressionOption::GzipLevel0`) +/// - compression level 1 (`CompressionOption::GzipFast` or `CompressionOption::GzipLevel1`) +/// - compression level n (where n is between 2 and 9) +/// - compression level 9 (`CompressionOption::GzipBest` or `CompressionOption::GzipLevel9`) +/// +/// ```rust +/// # #[cfg(feature = "gzip_compression")] { +/// # fn docs() { +/// use tracing_appender::compression::CompressionOption; +/// let compression_level = CompressionOption::GzipBest; +/// # } +/// # } +/// ``` +#[derive(Debug, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub enum CompressionOption { + /// No compression (gzip compression level 0) + GzipNone, + /// Fast compression (gzip compression level 1) + GzipFast, + /// Fast compression (gzip compression level 9) + GzipBest, + /// Gzip compression level 0 + GzipLevel0, + /// Gzip compression level 1 + GzipLevel1, + /// Gzip compression level 2 + GzipLevel2, + /// Gzip compression level 3 + GzipLevel3, + /// Gzip compression level 4 + GzipLevel4, + /// Gzip compression level 5 + GzipLevel5, + /// Gzip compression level 6 + GzipLevel6, + /// Gzip compression level 7 + GzipLevel7, + /// Gzip compression level 8 + GzipLevel8, + /// Gzip compression level 9 + GzipLevel9, +} + +impl Into for CompressionOption { + fn into(self) -> CompressionConfig { + let new_gzip_literal = |level| -> CompressionConfig { + CompressionConfig::Gzip(GzipCompression { + level: GzipCompressionLevel::Literal(level), + }) + }; + + let new_gzip_numerical = |level| -> CompressionConfig { + CompressionConfig::Gzip(GzipCompression { + level: GzipCompressionLevel::Numerical(level), + }) + }; + + match self { + CompressionOption::GzipNone => new_gzip_literal(GzipCompressionLevelLiteral::None), + CompressionOption::GzipFast => new_gzip_literal(GzipCompressionLevelLiteral::Fast), + CompressionOption::GzipBest => new_gzip_literal(GzipCompressionLevelLiteral::Best), + CompressionOption::GzipLevel0 => { + new_gzip_numerical(GzipCompressionLevelNumerical::Level0) + } + CompressionOption::GzipLevel1 => { + new_gzip_numerical(GzipCompressionLevelNumerical::Level1) + } + CompressionOption::GzipLevel2 => { + new_gzip_numerical(GzipCompressionLevelNumerical::Level2) + } + CompressionOption::GzipLevel3 => { + new_gzip_numerical(GzipCompressionLevelNumerical::Level3) + } + CompressionOption::GzipLevel4 => { + new_gzip_numerical(GzipCompressionLevelNumerical::Level4) + } + CompressionOption::GzipLevel5 => { + new_gzip_numerical(GzipCompressionLevelNumerical::Level5) + } + CompressionOption::GzipLevel6 => { + new_gzip_numerical(GzipCompressionLevelNumerical::Level6) + } + CompressionOption::GzipLevel7 => { + new_gzip_numerical(GzipCompressionLevelNumerical::Level7) + } + CompressionOption::GzipLevel8 => { + new_gzip_numerical(GzipCompressionLevelNumerical::Level8) + } + CompressionOption::GzipLevel9 => { + new_gzip_numerical(GzipCompressionLevelNumerical::Level9) + } + } + } +} + +#[cfg(test)] +mod test { + use crate::rolling::compression::CompressionOption; + use crate::rolling::test::write_to_log; + use crate::rolling::{Builder, Rotation}; + use flate2::read::GzDecoder; + use std::fs; + use std::io::Read; + use std::path::Path; + + fn find_str_in_compressed_log(dir_path: &Path, expected_value: &str) -> bool { + let dir_contents = fs::read_dir(dir_path).expect("Failed to read directory"); + + for entry in dir_contents { + let path = entry.expect("Expected dir entry").path(); + let bytes = fs::read(&path).expect("Cannot read bytes from compressed log"); + let mut decoder = GzDecoder::new(&bytes[..]); + let mut s = String::new(); + decoder + .read_to_string(&mut s) + .expect("Cannot decode compressed log file"); + if s.as_str() == expected_value { + return true; + } + } + + false + } + + #[test] + fn test_compressed_appender() { + let file_prefix = "my-app-compressed-log"; + let directory = tempfile::tempdir().expect("failed to create tempdir"); + let mut appender = Builder::new(directory.path(), file_prefix) + .rotation(Rotation::DAILY) + .compression(CompressionOption::GzipFast) + .build(); + + let expected_value = "Hello"; + write_to_log(&mut appender, expected_value); + drop(appender); + assert!(find_str_in_compressed_log(directory.path(), expected_value)); + + directory + .close() + .expect("Failed to explicitly close TempDir. TempDir should delete once out of scope.") + } +} diff --git a/tracing-appender/src/rolling.rs b/tracing-appender/src/rolling/mod.rs similarity index 85% rename from tracing-appender/src/rolling.rs rename to tracing-appender/src/rolling/mod.rs index efb7cc20b0..e247afa438 100644 --- a/tracing-appender/src/rolling.rs +++ b/tracing-appender/src/rolling/mod.rs @@ -26,7 +26,12 @@ //! let file_appender = RollingFileAppender::new(Rotation::HOURLY, "/some/directory", "prefix.log"); //! # } //! ``` +mod builder; +pub use crate::rolling::builder::Builder; +#[cfg(feature = "compression_gzip")] +use crate::rolling::compression::CompressionConfig; use crate::sync::{RwLock, RwLockReadGuard}; +use crate::writer::WriterChannel; use std::{ fmt::Debug, fs::{self, File, OpenOptions}, @@ -36,6 +41,9 @@ use std::{ }; use time::{format_description, Duration, OffsetDateTime, Time}; +#[cfg(feature = "compression_gzip")] +pub mod compression; + /// A file appender with the ability to rotate log files at a fixed schedule. /// /// `RollingFileAppender` implements the [`std:io::Write` trait][write] and will @@ -83,7 +91,9 @@ use time::{format_description, Duration, OffsetDateTime, Time}; #[derive(Debug)] pub struct RollingFileAppender { state: Inner, - writer: RwLock, + writer: RwLock, + #[cfg(features = "compression")] + compression: Option, } /// A [writer] that writes to a rolling log file. @@ -93,14 +103,16 @@ pub struct RollingFileAppender { /// [writer]: std::io::Write /// [`MakeWriter`]: tracing_subscriber::fmt::writer::MakeWriter #[derive(Debug)] -pub struct RollingWriter<'a>(RwLockReadGuard<'a, File>); +pub struct RollingWriter<'a>(RwLockReadGuard<'a, WriterChannel>); #[derive(Debug)] -struct Inner { +pub(crate) struct Inner { log_directory: String, log_filename_prefix: String, rotation: Rotation, next_date: AtomicUsize, + #[cfg(feature = "compression_gzip")] + compression: Option, } // === impl RollingFileAppender === @@ -134,28 +146,9 @@ impl RollingFileAppender { directory: impl AsRef, file_name_prefix: impl AsRef, ) -> RollingFileAppender { - let now = OffsetDateTime::now_utc(); - let log_directory = directory.as_ref().to_str().unwrap(); - let log_filename_prefix = file_name_prefix.as_ref().to_str().unwrap(); - - let filename = rotation.join_date(log_filename_prefix, &now); - let next_date = rotation.next_date(&now); - let writer = RwLock::new( - create_writer(log_directory, &filename).expect("failed to create appender"), - ); - Self { - state: Inner { - log_directory: log_directory.to_string(), - log_filename_prefix: log_filename_prefix.to_string(), - next_date: AtomicUsize::new( - next_date - .map(|date| date.unix_timestamp() as usize) - .unwrap_or(0), - ), - rotation, - }, - writer, - } + Builder::new(directory, file_name_prefix) + .rotation(rotation) + .build() } } @@ -421,7 +414,12 @@ impl Rotation { } } - pub(crate) fn join_date(&self, filename: &str, date: &OffsetDateTime) -> String { + pub(crate) fn join_date( + &self, + filename: &str, + date: &OffsetDateTime, + extension: Option, + ) -> String { match *self { Rotation::MINUTELY => { let format = format_description::parse("[year]-[month]-[day]-[hour]-[minute]") @@ -430,7 +428,7 @@ impl Rotation { let date = date .format(&format) .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender"); - format!("{}.{}", filename, date) + Rotation::format_params(filename, extension, date.as_str()) } Rotation::HOURLY => { let format = format_description::parse("[year]-[month]-[day]-[hour]") @@ -439,7 +437,7 @@ impl Rotation { let date = date .format(&format) .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender"); - format!("{}.{}", filename, date) + Rotation::format_params(filename, extension, date.as_str()) } Rotation::DAILY => { let format = format_description::parse("[year]-[month]-[day]") @@ -447,9 +445,23 @@ impl Rotation { let date = date .format(&format) .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender"); - format!("{}.{}", filename, date) + Rotation::format_params(filename, extension, date.as_str()) } - Rotation::NEVER => filename.to_string(), + Rotation::NEVER => { + if let Some(extension) = extension { + format!("{}.{}", filename.to_string(), extension) + } else { + filename.to_string() + } + } + } + } + + fn format_params(filename: &str, extension: Option, date: &str) -> String { + if let Some(extension) = extension { + format!("{}.{}.{}", filename, date, extension) + } else { + format!("{}.{}", filename, date) } } } @@ -469,17 +481,29 @@ impl io::Write for RollingWriter<'_> { // === impl Inner === impl Inner { - fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) { + fn refresh_writer(&self, now: OffsetDateTime, file: &mut WriterChannel) { debug_assert!(self.should_rollover(now)); - let filename = self.rotation.join_date(&self.log_filename_prefix, &now); + let filename = self + .rotation + .join_date(&self.log_filename_prefix, &now, None); + + #[cfg(feature = "compression_gzip")] + let writer = WriterChannel::new(&self.log_directory, &filename, self.compression.clone()); + + #[cfg(not(feature = "compression_gzip"))] + let writer = WriterChannel::new(&self.log_directory, &filename); + + Self::refresh_writer_channel(file, writer); + } - match create_writer(&self.log_directory, &filename) { - Ok(new_file) => { + fn refresh_writer_channel(file: &mut WriterChannel, writer: io::Result) { + match writer { + Ok(new_writer) => { if let Err(err) = file.flush() { eprintln!("Couldn't flush previous writer: {}", err); } - *file = new_file; + *file = new_writer; } Err(err) => eprintln!("Couldn't create writer for logs: {}", err), } @@ -511,7 +535,7 @@ impl Inner { } } -fn create_writer(directory: &str, filename: &str) -> io::Result { +pub(crate) fn create_writer_file(directory: &str, filename: &str) -> io::Result { let path = Path::new(directory).join(filename); let mut open_options = OpenOptions::new(); open_options.append(true).create(true); @@ -548,7 +572,7 @@ mod test { false } - fn write_to_log(appender: &mut RollingFileAppender, msg: &str) { + pub(crate) fn write_to_log(appender: &mut RollingFileAppender, msg: &str) { appender .write_all(msg.as_bytes()) .expect("Failed to write to appender"); @@ -631,19 +655,35 @@ mod test { let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap(); // per-minute - let path = Rotation::MINUTELY.join_date("app.log", &now); + let path = Rotation::MINUTELY.join_date("app.log", &now, None); assert_eq!("app.log.2020-02-01-10-01", path); // per-hour - let path = Rotation::HOURLY.join_date("app.log", &now); + let path = Rotation::HOURLY.join_date("app.log", &now, None); assert_eq!("app.log.2020-02-01-10", path); // per-day - let path = Rotation::DAILY.join_date("app.log", &now); + let path = Rotation::DAILY.join_date("app.log", &now, None); assert_eq!("app.log.2020-02-01", path); // never - let path = Rotation::NEVER.join_date("app.log", &now); + let path = Rotation::NEVER.join_date("app.log", &now, None); assert_eq!("app.log", path); + + // per-minute compressed + let path = Rotation::MINUTELY.join_date("app.log", &now, Some("gz".into())); + assert_eq!("app.log.2020-02-01-10-01.gz", path); + + // per-hour compressed + let path = Rotation::HOURLY.join_date("app.log", &now, Some("gz".into())); + assert_eq!("app.log.2020-02-01-10.gz", path); + + // per-day compressed + let path = Rotation::DAILY.join_date("app.log", &now, Some("gz".into())); + assert_eq!("app.log.2020-02-01.gz", path); + + // never compressed + let path = Rotation::NEVER.join_date("app.log", &now, Some("gz".into())); + assert_eq!("app.log.gz", path) } } diff --git a/tracing-appender/src/writer.rs b/tracing-appender/src/writer.rs new file mode 100644 index 0000000000..994dd4d55d --- /dev/null +++ b/tracing-appender/src/writer.rs @@ -0,0 +1,123 @@ +use crate::rolling::create_writer_file; +use std::fs::File; +use std::io; + +#[cfg(feature = "compression_gzip")] +use { + crate::rolling::compression::CompressionConfig, flate2::write::GzEncoder, std::io::BufWriter, + std::sync::Mutex, +}; + +#[cfg(feature = "compression_gzip")] +#[derive(Debug)] +pub(crate) struct CompressedGzip { + #[allow(unused)] + compression: CompressionConfig, + buffer: Mutex>>>, +} + +#[derive(Debug)] +pub(crate) enum WriterChannel { + File(File), + #[cfg(feature = "compression_gzip")] + CompressedFileGzip(CompressedGzip), +} + +impl WriterChannel { + #[cfg(feature = "compression_gzip")] + pub(crate) fn new( + directory: &str, + filename: &str, + compression: Option, + ) -> io::Result { + if let Some(compression) = compression { + Self::new_with_compression(directory, filename, compression) + } else { + Self::new_without_compression(directory, filename) + } + } + + #[cfg(not(feature = "compression_gzip"))] + pub(crate) fn new(directory: &str, filename: &str) -> io::Result { + Self::new_without_compression(directory, filename) + } + + pub(crate) fn new_without_compression(directory: &str, filename: &str) -> io::Result { + let file = create_writer_file(directory, filename)?; + Ok(WriterChannel::File(file)) + } + + #[cfg(feature = "compression_gzip")] + pub(crate) fn new_with_compression( + directory: &str, + filename: &str, + compression: CompressionConfig, + ) -> io::Result { + let file = create_writer_file(directory, filename)?; + let buf = BufWriter::new(file); + let gzfile = GzEncoder::new(buf, compression.gz_compress_level()); + let writer = BufWriter::new(gzfile); + let compressed_gz = CompressedGzip { + compression: compression.clone(), + buffer: Mutex::new(writer), + }; + Ok(WriterChannel::CompressedFileGzip(compressed_gz)) + } + + #[allow(unused)] + pub(crate) fn extension(self) -> Option { + match self { + WriterChannel::File(_) => None, + #[cfg(feature = "compression_gzip")] + WriterChannel::CompressedFileGzip(_) => Some("gz".to_string()), + } + } +} + +impl io::Write for WriterChannel { + fn write(&mut self, buf: &[u8]) -> io::Result { + match self { + WriterChannel::File(f) => f.write(buf), + #[cfg(feature = "compression_gzip")] + WriterChannel::CompressedFileGzip(gz) => { + let mut buffer = gz.buffer.lock().unwrap(); + buffer.write(buf) + } + } + } + + fn flush(&mut self) -> io::Result<()> { + match self { + WriterChannel::File(f) => f.flush(), + #[cfg(feature = "compression_gzip")] + WriterChannel::CompressedFileGzip(gz) => { + let mut buffer = gz.buffer.lock().unwrap(); + buffer.flush() + } + } + } +} + +impl io::Write for &WriterChannel { + fn write(&mut self, buf: &[u8]) -> io::Result { + match self { + WriterChannel::File(f) => (&*f).write(buf), + #[cfg(feature = "compression_gzip")] + WriterChannel::CompressedFileGzip(gz) => { + let mut buffer = gz.buffer.lock().unwrap(); + buffer.write(buf) + } + } + } + + fn flush(&mut self) -> io::Result<()> { + match self { + WriterChannel::File(f) => (&*f).flush(), + #[cfg(feature = "compression_gzip")] + WriterChannel::CompressedFileGzip(gz) => { + let mut buffer = gz.buffer.lock().unwrap(); + buffer.flush() + } + } + } +}