rust_secure_logger/
persistence.rs1use crate::entry::LogEntry;
4use std::fs::{File, OpenOptions};
5use std::io::{self, Write};
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone)]
10pub struct PersistenceConfig {
11 pub log_dir: PathBuf,
13 pub file_prefix: String,
15 pub max_file_size: u64,
17 pub max_files: usize,
19 pub compress_rotated: bool,
21}
22
23impl Default for PersistenceConfig {
24 fn default() -> Self {
25 Self {
26 log_dir: PathBuf::from("./logs"),
27 file_prefix: "secure".to_string(),
28 max_file_size: 10 * 1024 * 1024, max_files: 10,
30 compress_rotated: false,
31 }
32 }
33}
34
35pub struct LogWriter {
37 config: PersistenceConfig,
38 current_file: Option<File>,
39 current_size: u64,
40}
41
42impl LogWriter {
43 pub fn new(config: PersistenceConfig) -> io::Result<Self> {
45 std::fs::create_dir_all(&config.log_dir)?;
47
48 Ok(Self {
49 config,
50 current_file: None,
51 current_size: 0,
52 })
53 }
54
55 fn current_log_path(&self) -> PathBuf {
57 self.config
58 .log_dir
59 .join(format!("{}.log", self.config.file_prefix))
60 }
61
62 fn rotated_log_path(&self, index: usize) -> PathBuf {
64 self.config.log_dir.join(format!(
65 "{}.{}.log",
66 self.config.file_prefix, index
67 ))
68 }
69
70 fn ensure_file(&mut self) -> io::Result<&mut File> {
72 if self.current_file.is_none() {
73 let path = self.current_log_path();
74 let file = OpenOptions::new()
75 .create(true)
76 .append(true)
77 .open(&path)?;
78
79 self.current_size = file.metadata()?.len();
81 self.current_file = Some(file);
82 }
83
84 Ok(self.current_file.as_mut().unwrap())
85 }
86
87 fn rotate(&mut self) -> io::Result<()> {
89 self.current_file = None;
91
92 for i in (1..self.config.max_files).rev() {
94 let old_path = if i == 1 {
95 self.current_log_path()
96 } else {
97 self.rotated_log_path(i - 1)
98 };
99
100 let new_path = self.rotated_log_path(i);
101
102 if old_path.exists() {
103 std::fs::rename(&old_path, &new_path)?;
104 }
105 }
106
107 let oldest_path = self.rotated_log_path(self.config.max_files);
109 if oldest_path.exists() {
110 std::fs::remove_file(oldest_path)?;
111 }
112
113 self.current_size = 0;
115
116 Ok(())
117 }
118
119 pub fn write_entry(&mut self, entry: &LogEntry) -> io::Result<()> {
121 let log_line = format!("{}\n", entry.to_log_line());
122 let bytes = log_line.as_bytes();
123
124 if self.current_size + bytes.len() as u64 > self.config.max_file_size {
126 self.rotate()?;
127 }
128
129 let file = self.ensure_file()?;
131 file.write_all(bytes)?;
132 file.flush()?;
133
134 self.current_size += bytes.len() as u64;
135
136 Ok(())
137 }
138
139 pub fn write_entries(&mut self, entries: &[LogEntry]) -> io::Result<()> {
141 for entry in entries {
142 self.write_entry(entry)?;
143 }
144 Ok(())
145 }
146
147 pub fn write_entry_json(&mut self, entry: &LogEntry) -> io::Result<()> {
149 let json_line = entry
150 .to_json()
151 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
152 let line = format!("{}\n", json_line);
153 let bytes = line.as_bytes();
154
155 if self.current_size + bytes.len() as u64 > self.config.max_file_size {
156 self.rotate()?;
157 }
158
159 let file = self.ensure_file()?;
160 file.write_all(bytes)?;
161 file.flush()?;
162
163 self.current_size += bytes.len() as u64;
164
165 Ok(())
166 }
167
168 pub fn flush(&mut self) -> io::Result<()> {
170 if let Some(ref mut file) = self.current_file {
171 file.flush()?;
172 }
173 Ok(())
174 }
175
176 pub fn current_file_size(&self) -> u64 {
178 self.current_size
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use crate::entry::SecurityLevel;
186 use std::fs;
187 use tempfile::TempDir;
188
189 #[test]
190 fn test_log_writer_creation() {
191 let temp_dir = TempDir::new().unwrap();
192 let config = PersistenceConfig {
193 log_dir: temp_dir.path().to_path_buf(),
194 file_prefix: "test".to_string(),
195 max_file_size: 1024,
196 max_files: 5,
197 compress_rotated: false,
198 };
199
200 let writer = LogWriter::new(config);
201 assert!(writer.is_ok());
202 }
203
204 #[test]
205 fn test_write_entry() {
206 let temp_dir = TempDir::new().unwrap();
207 let config = PersistenceConfig {
208 log_dir: temp_dir.path().to_path_buf(),
209 file_prefix: "test".to_string(),
210 max_file_size: 1024 * 1024,
211 max_files: 5,
212 compress_rotated: false,
213 };
214
215 let mut writer = LogWriter::new(config).unwrap();
216 let entry = LogEntry::new(
217 SecurityLevel::Info,
218 "Test message".to_string(),
219 None,
220 );
221
222 let result = writer.write_entry(&entry);
223 assert!(result.is_ok());
224
225 let log_file = temp_dir.path().join("test.log");
227 assert!(log_file.exists());
228 }
229
230 #[test]
231 fn test_file_rotation() {
232 let temp_dir = TempDir::new().unwrap();
233 let config = PersistenceConfig {
234 log_dir: temp_dir.path().to_path_buf(),
235 file_prefix: "test".to_string(),
236 max_file_size: 100, max_files: 3,
238 compress_rotated: false,
239 };
240
241 let mut writer = LogWriter::new(config).unwrap();
242
243 for i in 0..20 {
245 let entry = LogEntry::new(
246 SecurityLevel::Info,
247 format!("Test message number {}", i),
248 None,
249 );
250 writer.write_entry(&entry).unwrap();
251 }
252
253 let rotated_file = temp_dir.path().join("test.1.log");
255 assert!(rotated_file.exists());
256 }
257
258 #[test]
259 fn test_json_writing() {
260 let temp_dir = TempDir::new().unwrap();
261 let config = PersistenceConfig {
262 log_dir: temp_dir.path().to_path_buf(),
263 file_prefix: "json_test".to_string(),
264 max_file_size: 1024 * 1024,
265 max_files: 5,
266 compress_rotated: false,
267 };
268
269 let mut writer = LogWriter::new(config).unwrap();
270 let entry = LogEntry::new(
271 SecurityLevel::Audit,
272 "Transaction completed".to_string(),
273 Some(serde_json::json!({"amount": 1000, "currency": "USD"})),
274 );
275
276 let result = writer.write_entry_json(&entry);
277 assert!(result.is_ok());
278
279 let log_file = temp_dir.path().join("json_test.log");
280 let contents = fs::read_to_string(log_file).unwrap();
281 assert!(contents.contains("Transaction completed"));
282 assert!(contents.contains("\"amount\":1000"));
283 }
284}