rust_secure_logger/
persistence.rs1use crate::entry::LogEntry;
4use std::fs::{File, OpenOptions};
5use std::io::{self, Write};
6use std::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
65 .log_dir
66 .join(format!("{}.{}.log", self.config.file_prefix, index))
67 }
68
69 fn ensure_file(&mut self) -> io::Result<&mut File> {
71 if self.current_file.is_none() {
72 let path = self.current_log_path();
73 let file = OpenOptions::new().create(true).append(true).open(&path)?;
74
75 self.current_size = file.metadata()?.len();
77 self.current_file = Some(file);
78 }
79
80 Ok(self.current_file.as_mut().unwrap())
81 }
82
83 fn rotate(&mut self) -> io::Result<()> {
85 self.current_file = None;
87
88 for i in (1..self.config.max_files).rev() {
90 let old_path = if i == 1 {
91 self.current_log_path()
92 } else {
93 self.rotated_log_path(i - 1)
94 };
95
96 let new_path = self.rotated_log_path(i);
97
98 if old_path.exists() {
99 std::fs::rename(&old_path, &new_path)?;
100 }
101 }
102
103 let oldest_path = self.rotated_log_path(self.config.max_files);
105 if oldest_path.exists() {
106 std::fs::remove_file(oldest_path)?;
107 }
108
109 self.current_size = 0;
111
112 Ok(())
113 }
114
115 pub fn write_entry(&mut self, entry: &LogEntry) -> io::Result<()> {
117 let log_line = format!("{}\n", entry.to_log_line());
118 let bytes = log_line.as_bytes();
119
120 if self.current_size + bytes.len() as u64 > self.config.max_file_size {
122 self.rotate()?;
123 }
124
125 let file = self.ensure_file()?;
127 file.write_all(bytes)?;
128 file.flush()?;
129
130 self.current_size += bytes.len() as u64;
131
132 Ok(())
133 }
134
135 pub fn write_entries(&mut self, entries: &[LogEntry]) -> io::Result<()> {
137 for entry in entries {
138 self.write_entry(entry)?;
139 }
140 Ok(())
141 }
142
143 pub fn write_entry_json(&mut self, entry: &LogEntry) -> io::Result<()> {
145 let json_line = entry
146 .to_json()
147 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
148 let line = format!("{}\n", json_line);
149 let bytes = line.as_bytes();
150
151 if self.current_size + bytes.len() as u64 > self.config.max_file_size {
152 self.rotate()?;
153 }
154
155 let file = self.ensure_file()?;
156 file.write_all(bytes)?;
157 file.flush()?;
158
159 self.current_size += bytes.len() as u64;
160
161 Ok(())
162 }
163
164 pub fn flush(&mut self) -> io::Result<()> {
166 if let Some(ref mut file) = self.current_file {
167 file.flush()?;
168 }
169 Ok(())
170 }
171
172 pub fn current_file_size(&self) -> u64 {
174 self.current_size
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use crate::entry::SecurityLevel;
182 use std::fs;
183 use tempfile::TempDir;
184
185 #[test]
186 fn test_log_writer_creation() {
187 let temp_dir = TempDir::new().unwrap();
188 let config = PersistenceConfig {
189 log_dir: temp_dir.path().to_path_buf(),
190 file_prefix: "test".to_string(),
191 max_file_size: 1024,
192 max_files: 5,
193 compress_rotated: false,
194 };
195
196 let writer = LogWriter::new(config);
197 assert!(writer.is_ok());
198 }
199
200 #[test]
201 fn test_write_entry() {
202 let temp_dir = TempDir::new().unwrap();
203 let config = PersistenceConfig {
204 log_dir: temp_dir.path().to_path_buf(),
205 file_prefix: "test".to_string(),
206 max_file_size: 1024 * 1024,
207 max_files: 5,
208 compress_rotated: false,
209 };
210
211 let mut writer = LogWriter::new(config).unwrap();
212 let entry = LogEntry::new(SecurityLevel::Info, "Test message".to_string(), None);
213
214 let result = writer.write_entry(&entry);
215 assert!(result.is_ok());
216
217 let log_file = temp_dir.path().join("test.log");
219 assert!(log_file.exists());
220 }
221
222 #[test]
223 fn test_file_rotation() {
224 let temp_dir = TempDir::new().unwrap();
225 let config = PersistenceConfig {
226 log_dir: temp_dir.path().to_path_buf(),
227 file_prefix: "test".to_string(),
228 max_file_size: 100, max_files: 3,
230 compress_rotated: false,
231 };
232
233 let mut writer = LogWriter::new(config).unwrap();
234
235 for i in 0..20 {
237 let entry = LogEntry::new(
238 SecurityLevel::Info,
239 format!("Test message number {}", i),
240 None,
241 );
242 writer.write_entry(&entry).unwrap();
243 }
244
245 let rotated_file = temp_dir.path().join("test.1.log");
247 assert!(rotated_file.exists());
248 }
249
250 #[test]
251 fn test_json_writing() {
252 let temp_dir = TempDir::new().unwrap();
253 let config = PersistenceConfig {
254 log_dir: temp_dir.path().to_path_buf(),
255 file_prefix: "json_test".to_string(),
256 max_file_size: 1024 * 1024,
257 max_files: 5,
258 compress_rotated: false,
259 };
260
261 let mut writer = LogWriter::new(config).unwrap();
262 let entry = LogEntry::new(
263 SecurityLevel::Audit,
264 "Transaction completed".to_string(),
265 Some(serde_json::json!({"amount": 1000, "currency": "USD"})),
266 );
267
268 let result = writer.write_entry_json(&entry);
269 assert!(result.is_ok());
270
271 let log_file = temp_dir.path().join("json_test.log");
272 let contents = fs::read_to_string(log_file).unwrap();
273 assert!(contents.contains("Transaction completed"));
274 assert!(contents.contains("\"amount\":1000"));
275 }
276}