1use std::{
2 fs::{self, File, OpenOptions},
3 io::{self, BufWriter, Write},
4 path::PathBuf,
5};
6
7use crate::{
8 Level,
9 header::{
10 DEFAULT_HEADER_PATTERN, DefaultHeader, LEVEL_THREAD_LOCATION_HEADER_PATTERN,
11 LevelThreadLocationHeader, StaticHeader, StaticHeaderWriteFn, TokenizedHeader,
12 },
13};
14
15pub struct RotationConfig {
17 pub max_size: u64,
19 pub max_files: u32,
21}
22
23impl RotationConfig {
24 pub fn new() -> Self {
26 Self { max_size: 100 * 1024 * 1024, max_files: 5 }
27 }
28
29 pub fn by_size(max_size: u64, max_files: u32) -> Self {
32 Self { max_size, max_files }
33 }
34
35 pub fn max_size(mut self, bytes: u64) -> Self {
37 self.max_size = bytes;
38 self
39 }
40
41 pub fn max_files(mut self, count: u32) -> Self {
43 self.max_files = count;
44 self
45 }
46
47 pub fn max_size_mb(mut self, megabytes: u64) -> Self {
49 self.max_size = megabytes * 1024 * 1024;
50 self
51 }
52}
53
54impl Default for RotationConfig {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60struct FileRotation {
61 base_path: PathBuf,
62 config: RotationConfig,
63 current_size: u64,
64}
65
66enum HeaderMode {
67 Empty,
68 Default(DefaultHeader),
69 LevelThreadLocation(LevelThreadLocationHeader),
70 Static(StaticHeader),
71 Dynamic(TokenizedHeader),
72}
73
74impl HeaderMode {
75 #[inline]
76 fn from_pattern(pattern: &str) -> io::Result<Self> {
77 if pattern.is_empty() {
78 Ok(Self::Empty)
79 } else if pattern == DEFAULT_HEADER_PATTERN {
80 Ok(Self::Default(DefaultHeader::new()))
81 } else if pattern == LEVEL_THREAD_LOCATION_HEADER_PATTERN {
82 Ok(Self::LevelThreadLocation(LevelThreadLocationHeader::new()))
83 } else {
84 TokenizedHeader::parse_pattern(pattern).map(Self::Dynamic)
85 }
86 }
87
88 #[inline]
89 fn write_header(
90 &mut self,
91 output: &mut Vec<u8>,
92 nanoseconds: i64,
93 level: Level,
94 thread_name: &[u8],
95 location: &str,
96 ) {
97 match self {
98 Self::Empty => {}
99 Self::Default(header) => {
100 header.write_header(output, nanoseconds, level, thread_name, location);
101 }
102 Self::LevelThreadLocation(header) => {
103 header.write_header(output, nanoseconds, level, thread_name, location);
104 }
105 Self::Static(header) => {
106 header.write_header(output, nanoseconds, level, thread_name, location);
107 }
108 Self::Dynamic(header) => {
109 header.write_header(output, nanoseconds, level, thread_name, location);
110 }
111 }
112 }
113}
114
115pub struct OutputBuffer {
118 pending: Vec<u8>,
119 writer: Box<dyn Write + Send>,
120 header: HeaderMode,
121 rotation: Option<FileRotation>,
122}
123
124impl Default for OutputBuffer {
125 fn default() -> Self {
126 Self::new()
127 }
128}
129
130impl OutputBuffer {
131 pub fn new() -> Self {
133 Self {
134 pending: Vec::with_capacity(8 * 1024),
135 writer: Box::new(io::stderr()),
136 header: HeaderMode::Default(DefaultHeader::new()),
137 rotation: None,
138 }
139 }
140
141 pub fn set_writer(&mut self, writer: Box<dyn Write + Send>) {
143 self.writer = writer;
144 }
145
146 pub fn set_log_file(&mut self, path: &str) -> io::Result<()> {
149 self.flush()?;
150 let file = open_log_file(path)?;
151 self.writer = Box::new(BufWriter::new(file));
152 self.rotation = None;
153 Ok(())
154 }
155
156 pub fn set_log_file_with_rotation(
160 &mut self,
161 path: &str,
162 config: RotationConfig,
163 ) -> io::Result<()> {
164 self.flush()?;
165 let file = open_log_file(path)?;
166 let current_size = file.metadata()?.len();
167 self.writer = Box::new(BufWriter::new(file));
168 self.rotation = Some(FileRotation { base_path: PathBuf::from(path), config, current_size });
169 Ok(())
170 }
171
172 pub fn set_header_pattern(&mut self, pattern: &str) -> io::Result<()> {
174 self.header = HeaderMode::from_pattern(pattern)?;
175 Ok(())
176 }
177
178 pub fn set_static_header(&mut self, write: StaticHeaderWriteFn) {
179 self.header = HeaderMode::Static(StaticHeader::new(write));
180 }
181
182 pub fn write_record<F>(
184 &mut self,
185 timestamp_nanoseconds: i64,
186 level: Level,
187 thread_name: &[u8],
188 location: &str,
189 append_body: F,
190 ) -> io::Result<()>
191 where
192 F: FnOnce(&mut Vec<u8>),
193 {
194 self.header.write_header(
195 &mut self.pending,
196 timestamp_nanoseconds,
197 level,
198 thread_name,
199 location,
200 );
201 append_body(&mut self.pending);
202 self.pending.push(b'\n');
203 Ok(())
204 }
205
206 #[inline]
208 pub fn buffered_len(&self) -> usize {
209 self.pending.len()
210 }
211
212 #[inline]
214 pub fn has_pending(&self) -> bool {
215 !self.pending.is_empty()
216 }
217
218 pub fn flush(&mut self) -> io::Result<()> {
222 if self.pending.is_empty() {
223 return Ok(());
224 }
225 let written = self.pending.len() as u64;
226 self.writer.write_all(&self.pending)?;
227 self.writer.flush()?;
228 self.pending.clear();
229
230 let needs_rotation = if let Some(ref mut rotation) = self.rotation {
231 rotation.current_size += written;
232 rotation.current_size >= rotation.config.max_size
233 } else {
234 false
235 };
236
237 if needs_rotation {
238 self.rotate()?;
239 }
240
241 Ok(())
242 }
243
244 fn rotate(&mut self) -> io::Result<()> {
248 let rotation = self.rotation.as_mut().expect("rotate called without rotation config");
249 let base_path = &rotation.base_path;
250 let max_files = rotation.config.max_files;
251
252 self.writer = Box::new(io::sink());
254
255 let oldest = format!("{}.{}", base_path.display(), max_files);
257 let _ = fs::remove_file(&oldest);
258
259 for i in (1..max_files).rev() {
261 let from = format!("{}.{}", base_path.display(), i);
262 let to = format!("{}.{}", base_path.display(), i + 1);
263 let _ = fs::rename(&from, &to);
264 }
265
266 let first_rotated = format!("{}.1", base_path.display());
268 let _ = fs::rename(base_path, &first_rotated);
269
270 let file = open_log_file(base_path.to_str().unwrap_or(""))?;
272 self.writer = Box::new(BufWriter::new(file));
273
274 rotation.current_size = 0;
275 Ok(())
276 }
277}
278
279fn open_log_file(path: &str) -> io::Result<File> {
280 OpenOptions::new().create(true).append(true).open(path)
281}