rs_zero/core/logging/
writer.rs1use std::{
2 fs::{self, File, OpenOptions},
3 io::{self, Write},
4 path::{Path, PathBuf},
5 sync::{Arc, Mutex},
6};
7
8use chrono::{Local, NaiveDate};
9use serde::Deserialize;
10
11use crate::core::logging::rotation::{cleanup_rotated_files, rotate_files};
12use crate::core::{CoreError, CoreResult};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum LogWriterConfig {
17 Stdout,
19 Stderr,
21 File(PathBuf),
23 RollingFile(RollingFileConfig),
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)]
29#[serde(rename_all = "snake_case")]
30pub enum RotationPolicy {
31 #[default]
33 Daily,
34 Size,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct RollingFileConfig {
41 pub path: PathBuf,
43 pub rotation: RotationPolicy,
45 pub max_bytes: Option<u64>,
47 pub max_files: usize,
49 pub keep_days: Option<u64>,
51 pub compress: bool,
53}
54
55impl RollingFileConfig {
56 pub fn by_size(path: impl Into<PathBuf>, max_bytes: u64, max_files: usize) -> Self {
58 Self {
59 path: path.into(),
60 rotation: RotationPolicy::Size,
61 max_bytes: Some(max_bytes),
62 max_files,
63 keep_days: None,
64 compress: false,
65 }
66 }
67
68 pub fn daily(path: impl Into<PathBuf>, max_files: usize) -> Self {
70 Self {
71 path: path.into(),
72 rotation: RotationPolicy::Daily,
73 max_bytes: None,
74 max_files,
75 keep_days: None,
76 compress: false,
77 }
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct PreparedLogWriter {
84 pub path: Option<PathBuf>,
86 pub rotation_enabled: bool,
88}
89
90impl LogWriterConfig {
91 pub fn file(path: impl Into<PathBuf>) -> Self {
93 Self::File(path.into())
94 }
95}
96
97pub fn validate_writer(config: &LogWriterConfig) -> CoreResult<PreparedLogWriter> {
99 match config {
100 LogWriterConfig::Stdout | LogWriterConfig::Stderr => Ok(PreparedLogWriter {
101 path: None,
102 rotation_enabled: false,
103 }),
104 LogWriterConfig::File(path) => {
105 open_append_file(path)?;
106 Ok(PreparedLogWriter {
107 path: Some(path.clone()),
108 rotation_enabled: false,
109 })
110 }
111 LogWriterConfig::RollingFile(rolling) => {
112 open_append_file(&rolling.path)?;
113 Ok(PreparedLogWriter {
114 path: Some(rolling.path.clone()),
115 rotation_enabled: true,
116 })
117 }
118 }
119}
120
121#[derive(Clone)]
123pub struct RuntimeRollingFileWriter {
124 inner: Arc<Mutex<RuntimeRollingFileState>>,
125}
126
127impl RuntimeRollingFileWriter {
128 pub fn new(config: RollingFileConfig) -> CoreResult<Self> {
130 if let Some(parent) = config.path.parent() {
131 fs::create_dir_all(parent).map_err(|error| CoreError::Logging(error.to_string()))?;
132 }
133 cleanup_rotated_files(&config).map_err(|error| CoreError::Logging(error.to_string()))?;
134 let file = open_append_file(&config.path)?;
135 let bytes_written = file
136 .metadata()
137 .map_err(|error| CoreError::Logging(error.to_string()))?
138 .len();
139 Ok(Self {
140 inner: Arc::new(Mutex::new(RuntimeRollingFileState {
141 current_day: Local::now().date_naive(),
142 config,
143 file,
144 bytes_written,
145 })),
146 })
147 }
148}
149
150impl Write for RuntimeRollingFileWriter {
151 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
152 let mut state = self
153 .inner
154 .lock()
155 .map_err(|_| io::Error::other("rolling log writer mutex poisoned"))?;
156 state.write(buf)
157 }
158
159 fn flush(&mut self) -> io::Result<()> {
160 let mut state = self
161 .inner
162 .lock()
163 .map_err(|_| io::Error::other("rolling log writer mutex poisoned"))?;
164 state.file.flush()
165 }
166}
167
168pub(crate) fn open_append_file(path: &Path) -> CoreResult<File> {
169 if let Some(parent) = path.parent() {
170 fs::create_dir_all(parent).map_err(|error| CoreError::Logging(error.to_string()))?;
171 }
172 OpenOptions::new()
173 .create(true)
174 .append(true)
175 .open(path)
176 .map_err(|error| CoreError::Logging(error.to_string()))
177}
178
179struct RuntimeRollingFileState {
180 config: RollingFileConfig,
181 file: File,
182 bytes_written: u64,
183 current_day: NaiveDate,
184}
185
186impl RuntimeRollingFileState {
187 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
188 self.rotate_if_needed(buf.len() as u64)?;
189 self.file.write_all(buf)?;
190 self.bytes_written = self.bytes_written.saturating_add(buf.len() as u64);
191 Ok(buf.len())
192 }
193
194 fn rotate_if_needed(&mut self, incoming_bytes: u64) -> io::Result<()> {
195 match self.config.rotation {
196 RotationPolicy::Size => self.rotate_by_size_if_needed(incoming_bytes),
197 RotationPolicy::Daily => self.rotate_by_day_if_needed(),
198 }
199 }
200
201 fn rotate_by_size_if_needed(&mut self, incoming_bytes: u64) -> io::Result<()> {
202 let Some(max_bytes) = self.config.max_bytes else {
203 return Ok(());
204 };
205 if max_bytes == 0 || self.bytes_written.saturating_add(incoming_bytes) <= max_bytes {
206 return Ok(());
207 }
208 self.rotate_active_file()
209 }
210
211 fn rotate_by_day_if_needed(&mut self) -> io::Result<()> {
212 let today = Local::now().date_naive();
213 if today <= self.current_day {
214 return Ok(());
215 }
216 self.current_day = today;
217 self.rotate_active_file()
218 }
219
220 fn rotate_active_file(&mut self) -> io::Result<()> {
221 self.file.flush()?;
222 rotate_files(&self.config)?;
223 self.file = OpenOptions::new()
224 .create(true)
225 .append(true)
226 .open(&self.config.path)?;
227 self.bytes_written = 0;
228 cleanup_rotated_files(&self.config)
229 }
230}