Skip to main content

logforth_append_file/
append.rs

1// Copyright 2024 FastLabs Developers
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::io::Write;
16use std::num::NonZeroUsize;
17use std::path::PathBuf;
18use std::sync::Mutex;
19use std::sync::MutexGuard;
20
21use logforth_core::Diagnostic;
22use logforth_core::Error;
23use logforth_core::Layout;
24use logforth_core::Trap;
25use logforth_core::append::Append;
26use logforth_core::layout::PlainTextLayout;
27use logforth_core::record::Record;
28
29use crate::rolling::RollingFileWriter;
30use crate::rolling::RollingFileWriterBuilder;
31use crate::rotation::Rotation;
32
33/// A builder to configure and create an [`File`] appender.
34///
35/// See [module-level documentation](super) for usage examples.
36#[derive(Debug)]
37pub struct FileBuilder {
38    builder: RollingFileWriterBuilder,
39    layout: Box<dyn Layout>,
40}
41
42impl FileBuilder {
43    /// Create a new file appender builder.
44    pub fn new(basedir: impl Into<PathBuf>, filename: impl Into<String>) -> Self {
45        Self {
46            builder: RollingFileWriterBuilder::new(basedir, filename),
47            layout: Box::new(PlainTextLayout::default()),
48        }
49    }
50
51    /// Build the [`File`] appender.
52    ///
53    /// # Errors
54    ///
55    /// Return an error if either:
56    ///
57    /// * The log directory cannot be created.
58    /// * The configured filename is empty.
59    pub fn build(self) -> Result<File, Error> {
60        let FileBuilder { builder, layout } = self;
61        let writer = builder.build()?;
62        Ok(File::new(writer, layout))
63    }
64
65    /// Set the layout for the logs.
66    ///
67    /// Default to [`PlainTextLayout`].
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// use logforth_append_file::FileBuilder;
73    /// use logforth_layout_json::JsonLayout;
74    ///
75    /// let builder = FileBuilder::new("my_service", "my_app");
76    /// builder.layout(JsonLayout::default());
77    /// ```
78    pub fn layout(mut self, layout: impl Into<Box<dyn Layout>>) -> Self {
79        self.layout = layout.into();
80        self
81    }
82
83    /// Set the trap for the file writer.
84    ///
85    /// Default to [`BestEffortTrap`].
86    ///
87    /// # Examples
88    ///
89    /// ```
90    /// use logforth_append_file::FileBuilder;
91    /// use logforth_core::trap::BestEffortTrap;
92    ///
93    /// let builder = FileBuilder::new("my_service", "my_app");
94    /// builder.trap(BestEffortTrap::default());
95    /// ```
96    ///
97    /// [`BestEffortTrap`]: logforth_core::trap::BestEffortTrap
98    pub fn trap(mut self, trap: impl Into<Box<dyn Trap>>) -> Self {
99        self.builder = self.builder.trap(trap);
100        self
101    }
102
103    /// Set the rotation strategy to roll over log files minutely.
104    pub fn rollover_minutely(mut self) -> Self {
105        self.builder = self.builder.rotation(Rotation::Minutely);
106        self
107    }
108
109    /// Set the rotation strategy to roll over log files hourly.
110    pub fn rollover_hourly(mut self) -> Self {
111        self.builder = self.builder.rotation(Rotation::Hourly);
112        self
113    }
114
115    /// Set the rotation strategy to roll over log files daily at 00:00 in the local time zone.
116    pub fn rollover_daily(mut self) -> Self {
117        self.builder = self.builder.rotation(Rotation::Daily);
118        self
119    }
120
121    /// Set the rotation strategy to roll over log files if the current log file exceeds the given
122    /// size.
123    ///
124    /// If any time-based rotation strategy is set, the size-based rotation will be checked on the
125    /// current log file after the time-based rotation check.
126    pub fn rollover_size(mut self, n: NonZeroUsize) -> Self {
127        self.builder = self.builder.max_file_size(n);
128        self
129    }
130
131    /// Set the filename suffix.
132    pub fn filename_suffix(mut self, suffix: impl Into<String>) -> Self {
133        self.builder = self.builder.filename_suffix(suffix);
134        self
135    }
136
137    /// Set the maximum number of log files to keep.
138    pub fn max_log_files(mut self, n: NonZeroUsize) -> Self {
139        self.builder = self.builder.max_log_files(n);
140        self
141    }
142}
143
144/// An appender that writes log records to rolling files.
145#[derive(Debug)]
146pub struct File {
147    writer: Mutex<RollingFileWriter>,
148    layout: Box<dyn Layout>,
149}
150
151impl File {
152    fn new(writer: RollingFileWriter, layout: Box<dyn Layout>) -> Self {
153        let writer = Mutex::new(writer);
154        Self { writer, layout }
155    }
156
157    fn writer(&self) -> MutexGuard<'_, RollingFileWriter> {
158        self.writer.lock().unwrap_or_else(|e| e.into_inner())
159    }
160}
161
162impl Append for File {
163    fn append(&self, record: &Record, diags: &[Box<dyn Diagnostic>]) -> Result<(), Error> {
164        let mut bytes = self.layout.format(record, diags)?;
165        bytes.push(b'\n');
166        let mut writer = self.writer();
167        writer.write_all(&bytes).map_err(Error::from_io_error)?;
168        Ok(())
169    }
170
171    fn flush(&self) -> Result<(), Error> {
172        let mut writer = self.writer();
173        writer.flush().map_err(Error::from_io_error)?;
174        Ok(())
175    }
176}
177
178impl Drop for File {
179    fn drop(&mut self) {
180        let writer = self.writer.get_mut().unwrap_or_else(|e| e.into_inner());
181        let _ = writer.flush();
182    }
183}