Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
41 changes: 41 additions & 0 deletions examples/single_file.rs
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
4 changes: 4 additions & 0 deletions src/append/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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")]
Expand Down
133 changes: 133 additions & 0 deletions src/append/single_file/append.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Layout>,

// non-blocking options
thread_name: String,
buffered_lines_limit: Option<usize>,
shutdown_timeout: Option<Duration>,
}

impl SingleFileBuilder {
/// Create a new builder.
pub fn new(log_path: impl Into<PathBuf>) -> 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<Box<dyn Layout>>) -> Self {
self.layout = layout.into();
self
}

/// Sets the buffer size of pending messages.
pub fn buffered_lines_limit(mut self, buffered_lines_limit: Option<usize>) -> 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<Duration>) -> 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<String>) -> Self {
self.thread_name = thread_name.into();
self
}
}

/// An appender that writes log records to a file.
#[derive(Debug)]
pub struct SingleFile {
layout: Box<dyn Layout>,
writer: NonBlocking<SingleFileWriter>,
}

impl SingleFile {
fn new(writer: NonBlocking<SingleFileWriter>, layout: Box<dyn Layout>) -> Self {
Self { layout, writer }
}
}

impl Append for SingleFile {
fn append(&self, record: &Record, diagnostics: &[Box<dyn Diagnostic>]) -> anyhow::Result<()> {
let mut bytes = self.layout.format(record, diagnostics)?;
bytes.push(b'\n');
self.writer.send(bytes)?;
Ok(())
}
}
41 changes: 41 additions & 0 deletions src/append/single_file/mod.rs
Original file line number Diff line number Diff line change
@@ -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("file.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;
109 changes: 109 additions & 0 deletions src/append/single_file/single.rs
Original file line number Diff line number Diff line change
@@ -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<usize> {
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<PathBuf>) -> Self {
Self {
filepath: filepath.into(),
}
}

/// Builds the [`SingleFileWriter`].
pub fn build(self) -> anyhow::Result<SingleFileWriter> {
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
}
}
2 changes: 1 addition & 1 deletion src/non_blocking/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Writer + Send + 'static> {
sender: Sender<Message>,
Expand Down
2 changes: 1 addition & 1 deletion src/non_blocking/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,6 @@ impl<T: Writer + Send + 'static> Worker<T> {
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")
}
}
Loading