From 34e7734c0b4f69f73d6f040313b3f43eb893f278 Mon Sep 17 00:00:00 2001 From: nine9ths Date: Fri, 30 May 2025 19:50:55 -0700 Subject: [PATCH 1/3] Adding `SingleFile` appender --- Cargo.toml | 7 ++ examples/single_file.rs | 41 ++++++++++ src/append/mod.rs | 4 + src/append/single_file/append.rs | 133 +++++++++++++++++++++++++++++++ src/append/single_file/mod.rs | 41 ++++++++++ src/append/single_file/single.rs | 109 +++++++++++++++++++++++++ 6 files changed, 335 insertions(+) create mode 100644 examples/single_file.rs create mode 100644 src/append/single_file/append.rs create mode 100644 src/append/single_file/mod.rs create mode 100644 src/append/single_file/single.rs diff --git a/Cargo.toml b/Cargo.toml index 8b47a22..fa60edd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ opentelemetry = [ ] rolling-file = ["internal-non-blocking"] rustls = ["dep:rustls", "fasyslog?/rustls"] +single-file = ["internal-non-blocking"] syslog = ["internal-non-blocking", "dep:fasyslog"] # Internal features - not intended for directly public use @@ -112,6 +113,12 @@ name = "rolling_file" path = "examples/rolling_file.rs" required-features = ["rolling-file", "json"] +[[example]] +doc-scrape-examples = true +name = "single_file" +path = "examples/single_file.rs" +required-features = ["single-file", "json"] + [[example]] doc-scrape-examples = true name = "custom_layout_filter" diff --git a/examples/single_file.rs b/examples/single_file.rs new file mode 100644 index 0000000..4a83235 --- /dev/null +++ b/examples/single_file.rs @@ -0,0 +1,41 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use logforth::append::single_file::SingleFileBuilder; +use logforth::layout::JsonLayout; + +fn main() { + let (single_writer, _guard) = SingleFileBuilder::new("my.log") + .layout(JsonLayout::default()) + .build() + .unwrap(); + + logforth::builder() + .dispatch(|d| d.filter(log::LevelFilter::Trace).append(single_writer)) + .apply(); + + let repeat = 1; + + for i in 0..repeat { + log::error!("Hello error!"); + log::warn!("Hello warn!"); + log::info!("Hello info!"); + log::debug!("Hello debug!"); + log::trace!("Hello trace!"); + + if i + 1 < repeat { + std::thread::sleep(std::time::Duration::from_secs(10)); + } + } +} diff --git a/src/append/mod.rs b/src/append/mod.rs index f1627c7..c24147a 100644 --- a/src/append/mod.rs +++ b/src/append/mod.rs @@ -26,6 +26,8 @@ mod journald; pub mod opentelemetry; #[cfg(feature = "rolling-file")] pub mod rolling_file; +#[cfg(feature = "single-file")] +pub mod single_file; mod stdio; #[cfg(feature = "syslog")] pub mod syslog; @@ -38,6 +40,8 @@ pub use self::journald::Journald; pub use self::opentelemetry::OpentelemetryLog; #[cfg(feature = "rolling-file")] pub use self::rolling_file::RollingFile; +#[cfg(feature = "single-file")] +pub use self::single_file::SingleFile; pub use self::stdio::Stderr; pub use self::stdio::Stdout; #[cfg(feature = "syslog")] diff --git a/src/append/single_file/append.rs b/src/append/single_file/append.rs new file mode 100644 index 0000000..6c3e6df --- /dev/null +++ b/src/append/single_file/append.rs @@ -0,0 +1,133 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::path::PathBuf; +use std::time::Duration; + +use log::Record; + +use crate::append::single_file::single::SingleFileWriter; +use crate::append::single_file::single::SingleFileWriterBuilder; +use crate::append::Append; +use crate::layout::TextLayout; +use crate::non_blocking::NonBlocking; +use crate::non_blocking::NonBlockingBuilder; +use crate::Diagnostic; +use crate::DropGuard; +use crate::Layout; + +/// A builder to configure and create an [`SingleFile`] appender. +#[derive(Debug)] +pub struct SingleFileBuilder { + builder: SingleFileWriterBuilder, + layout: Box, + + // non-blocking options + thread_name: String, + buffered_lines_limit: Option, + shutdown_timeout: Option, +} + +impl SingleFileBuilder { + /// Create a new builder. + pub fn new(log_path: impl Into) -> Self { + Self { + builder: SingleFileWriterBuilder::new(log_path), + layout: Box::new(TextLayout::default().no_color()), + + thread_name: "logforth-single-file".to_string(), + buffered_lines_limit: None, + shutdown_timeout: None, + } + } + + /// Build the [`SingleFile`] appender. + /// + /// # Errors + /// + /// Returns an error if the log file cannot be created. + pub fn build(self) -> anyhow::Result<(SingleFile, DropGuard)> { + let SingleFileBuilder { + builder, + layout, + thread_name, + buffered_lines_limit, + shutdown_timeout, + } = self; + let writer = builder.build()?; + let (non_blocking, guard) = NonBlockingBuilder::new(thread_name, writer) + .buffered_lines_limit(buffered_lines_limit) + .shutdown_timeout(shutdown_timeout) + .build(); + Ok((SingleFile::new(non_blocking, layout), Box::new(guard))) + } + + /// Sets the layout for the logs. + /// + /// Default to [`TextLayout`]. + /// + /// # Examples + /// + /// ``` + /// use logforth::append::single_file::SingleFileBuilder; + /// use logforth::layout::JsonLayout; + /// + /// let builder = SingleFileBuilder::new("my_service.log"); + /// builder.layout(JsonLayout::default()); + /// ``` + pub fn layout(mut self, layout: impl Into>) -> Self { + self.layout = layout.into(); + self + } + + /// Sets the buffer size of pending messages. + pub fn buffered_lines_limit(mut self, buffered_lines_limit: Option) -> Self { + self.buffered_lines_limit = buffered_lines_limit; + self + } + + /// Sets the shutdown timeout before the worker guard dropped. + pub fn shutdown_timeout(mut self, shutdown_timeout: Option) -> Self { + self.shutdown_timeout = shutdown_timeout; + self + } + + /// Sets the thread name for the background sender thread. + pub fn thread_name(mut self, thread_name: impl Into) -> Self { + self.thread_name = thread_name.into(); + self + } +} + +/// An appender that writes log records to a file. +#[derive(Debug)] +pub struct SingleFile { + layout: Box, + writer: NonBlocking, +} + +impl SingleFile { + fn new(writer: NonBlocking, layout: Box) -> Self { + Self { layout, writer } + } +} + +impl Append for SingleFile { + fn append(&self, record: &Record, diagnostics: &[Box]) -> anyhow::Result<()> { + let mut bytes = self.layout.format(record, diagnostics)?; + bytes.push(b'\n'); + self.writer.send(bytes)?; + Ok(()) + } +} diff --git a/src/append/single_file/mod.rs b/src/append/single_file/mod.rs new file mode 100644 index 0000000..0670f3e --- /dev/null +++ b/src/append/single_file/mod.rs @@ -0,0 +1,41 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Appender for writing log records to a file. +//! +//! # Example +//! +//!``` +//! use logforth::append::single_file; +//! use logforth::append::single_file::SingleFile; +//! use logforth::append::single_file::SingleFileBuilder; +//! use logforth::layout::JsonLayout; +//! +//! let (file_writer, _guard) = SingleFileBuilder::new("/path/to/flile.log") +//! .layout(JsonLayout::default()) +//! .build() +//! .unwrap(); +//! +//! logforth::builder() +//! .dispatch(|d| d.filter(log::LevelFilter::Trace).append(file_writer)) +//! .apply(); +//! +//! log::info!("This log will be written to a file."); +//! ``` + +pub use append::SingleFile; +pub use append::SingleFileBuilder; + +mod append; +mod single; diff --git a/src/append/single_file/single.rs b/src/append/single_file/single.rs new file mode 100644 index 0000000..599a9de --- /dev/null +++ b/src/append/single_file/single.rs @@ -0,0 +1,109 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fs; +use std::fs::File; +use std::fs::OpenOptions; +use std::io; +use std::io::Write; +use std::path::PathBuf; + +use anyhow::Context; + +/// A writer for files. +#[derive(Debug)] +pub struct SingleFileWriter { + writer: File, +} + +impl Write for SingleFileWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + let writer = &mut self.writer; + + writer.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.writer.flush() + } +} + +/// A builder for configuring [`SingleFileWriter`]. +#[derive(Debug)] +pub struct SingleFileWriterBuilder { + // required + filepath: PathBuf, +} + +impl SingleFileWriterBuilder { + /// Creates a new [`SingleFileWriterBuilder`]. + #[must_use] + pub fn new(filepath: impl Into) -> Self { + Self { + filepath: filepath.into(), + } + } + + /// Builds the [`SingleFileWriter`]. + pub fn build(self) -> anyhow::Result { + let dir = &self + .filepath + .parent() + .context("failed to get log directory")?; + fs::create_dir_all(dir).context("failed to create log directory")?; + let writer = OpenOptions::new() + .append(true) + .create(true) + .open(&self.filepath) + .context("failed to create log file")?; + Ok(SingleFileWriter { writer }) + } +} + +#[cfg(test)] +mod tests { + use std::io::Write; + + use rand::distr::Alphanumeric; + use rand::Rng; + use tempfile::NamedTempFile; + + use crate::append::single_file::single::SingleFileWriterBuilder; + + #[test] + fn test_single_file() { + // To Do: Make this a file + let temp_file = NamedTempFile::new().expect("failed to create a temporary directory"); + + let mut writer = SingleFileWriterBuilder::new(temp_file.path()) + .build() + .unwrap(); + + let rand_str = generate_random_string(); + assert_eq!(writer.write(rand_str.as_bytes()).unwrap(), rand_str.len()); + writer.flush().unwrap(); + } + + fn generate_random_string() -> String { + let mut rng = rand::rng(); + let len = rng.random_range(50..=100); + let random_string: String = std::iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .map(char::from) + .take(len) + .collect(); + + random_string + } +} From 80191c8e1a90095b38e4d79ae7bd792aed1dd994 Mon Sep 17 00:00:00 2001 From: nine9ths Date: Fri, 30 May 2025 20:11:58 -0700 Subject: [PATCH 2/3] Making some non_blocking strings generic. --- src/non_blocking/builder.rs | 2 +- src/non_blocking/worker.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/non_blocking/builder.rs b/src/non_blocking/builder.rs index 627b6a5..9aba2c6 100644 --- a/src/non_blocking/builder.rs +++ b/src/non_blocking/builder.rs @@ -85,7 +85,7 @@ impl Drop for WorkerGuard { } } -/// A non-blocking writer for rolling files. +/// A non-blocking writer for files. #[derive(Clone, Debug)] pub struct NonBlocking { sender: Sender, diff --git a/src/non_blocking/worker.rs b/src/non_blocking/worker.rs index 2da254a..22c24d6 100644 --- a/src/non_blocking/worker.rs +++ b/src/non_blocking/worker.rs @@ -116,6 +116,6 @@ impl Worker { eprintln!("failed to flush: {err}"); } }) - .expect("failed to spawn the non-blocking rolling file writer thread") + .expect("failed to spawn the non-blocking file writer thread") } } From 130726b2f9a5c00bb4117dd68eddc9b5381bf1a3 Mon Sep 17 00:00:00 2001 From: nine9ths Date: Fri, 30 May 2025 22:17:20 -0700 Subject: [PATCH 3/3] Fixing doc test --- src/append/single_file/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/append/single_file/mod.rs b/src/append/single_file/mod.rs index 0670f3e..6d442c9 100644 --- a/src/append/single_file/mod.rs +++ b/src/append/single_file/mod.rs @@ -22,7 +22,7 @@ //! use logforth::append::single_file::SingleFileBuilder; //! use logforth::layout::JsonLayout; //! -//! let (file_writer, _guard) = SingleFileBuilder::new("/path/to/flile.log") +//! let (file_writer, _guard) = SingleFileBuilder::new("file.log") //! .layout(JsonLayout::default()) //! .build() //! .unwrap();