Skip to main content

rust_serv/access_log/
writer.rs

1//! Access log writer with file persistence
2
3use std::fs::{File, OpenOptions};
4use std::io::Write as IoWrite;
5use std::path::{Path, PathBuf};
6use std::sync::{Arc, Mutex};
7
8use super::formatter::{AccessLogEntry, LogFormat};
9
10/// Access log writer
11pub struct AccessLogWriter {
12    /// Log file path
13    path: PathBuf,
14    /// Log format
15    format: LogFormat,
16    /// Output file
17    file: Arc<Mutex<File>>,
18}
19
20impl AccessLogWriter {
21    /// Create a new access log writer
22    pub fn new<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
23        Self::with_format(path, LogFormat::default())
24    }
25
26    /// Create a new access log writer with custom format
27    pub fn with_format<P: AsRef<Path>>(path: P, format: LogFormat) -> std::io::Result<Self> {
28        let path = path.as_ref().to_path_buf();
29        
30        // Create parent directories if needed
31        if let Some(parent) = path.parent() {
32            std::fs::create_dir_all(parent)?;
33        }
34        
35        let file = OpenOptions::new()
36            .create(true)
37            .append(true)
38            .open(&path)?;
39        
40        Ok(Self {
41            path,
42            format,
43            file: Arc::new(Mutex::new(file)),
44        })
45    }
46
47    /// Write a log entry
48    pub fn write(&self, entry: &AccessLogEntry) -> std::io::Result<()> {
49        let line = self.format.format(entry);
50        let mut file = self.file.lock().unwrap();
51        writeln!(file, "{}", line)?;
52        file.flush()?;
53        Ok(())
54    }
55
56    /// Write multiple log entries
57    pub fn write_batch(&self, entries: &[AccessLogEntry]) -> std::io::Result<()> {
58        let mut file = self.file.lock().unwrap();
59        for entry in entries {
60            let line = self.format.format(entry);
61            writeln!(file, "{}", line)?;
62        }
63        file.flush()?;
64        Ok(())
65    }
66
67    /// Get the log file path
68    pub fn path(&self) -> &Path {
69        &self.path
70    }
71
72    /// Get the log format
73    pub fn format(&self) -> LogFormat {
74        self.format
75    }
76
77    /// Flush the file buffer
78    pub fn flush(&self) -> std::io::Result<()> {
79        let mut file = self.file.lock().unwrap();
80        file.flush()
81    }
82}
83
84impl Clone for AccessLogWriter {
85    fn clone(&self) -> Self {
86        Self {
87            path: self.path.clone(),
88            format: self.format,
89            file: Arc::clone(&self.file),
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use tempfile::TempDir;
98
99    fn create_test_writer() -> (AccessLogWriter, TempDir) {
100        let dir = TempDir::new().unwrap();
101        let path = dir.path().join("access.log");
102        let writer = AccessLogWriter::new(&path).unwrap();
103        (writer, dir)
104    }
105
106    #[test]
107    fn test_writer_creation() {
108        let dir = TempDir::new().unwrap();
109        let path = dir.path().join("access.log");
110        let writer = AccessLogWriter::new(&path).unwrap();
111        
112        assert_eq!(writer.path(), path);
113        assert_eq!(writer.format(), LogFormat::Combined);
114    }
115
116    #[test]
117    fn test_writer_with_format() {
118        let dir = TempDir::new().unwrap();
119        let path = dir.path().join("access.log");
120        let writer = AccessLogWriter::with_format(&path, LogFormat::Json).unwrap();
121        
122        assert_eq!(writer.format(), LogFormat::Json);
123    }
124
125    #[test]
126    fn test_write_single_entry() {
127        let (writer, _dir) = create_test_writer();
128        
129        let entry = AccessLogEntry::new("127.0.0.1", "GET", "/index.html")
130            .with_status(200)
131            .with_size(1234);
132        
133        let result = writer.write(&entry);
134        assert!(result.is_ok());
135        
136        // Verify file was created and has content
137        let content = std::fs::read_to_string(writer.path()).unwrap();
138        assert!(content.contains("127.0.0.1"));
139        assert!(content.contains("GET /index.html"));
140    }
141
142    #[test]
143    fn test_write_multiple_entries() {
144        let (writer, _dir) = create_test_writer();
145        
146        let entries = vec![
147            AccessLogEntry::new("127.0.0.1", "GET", "/page1"),
148            AccessLogEntry::new("192.168.1.1", "POST", "/api"),
149            AccessLogEntry::new("10.0.0.1", "GET", "/page2"),
150        ];
151        
152        let result = writer.write_batch(&entries);
153        assert!(result.is_ok());
154        
155        let content = std::fs::read_to_string(writer.path()).unwrap();
156        assert!(content.contains("/page1"));
157        assert!(content.contains("/api"));
158        assert!(content.contains("/page2"));
159    }
160
161    #[test]
162    fn test_append_mode() {
163        let dir = TempDir::new().unwrap();
164        let path = dir.path().join("access.log");
165        
166        // Write first entry
167        {
168            let writer = AccessLogWriter::new(&path).unwrap();
169            let entry = AccessLogEntry::new("127.0.0.1", "GET", "/first");
170            writer.write(&entry).unwrap();
171        }
172        
173        // Write second entry (should append)
174        {
175            let writer = AccessLogWriter::new(&path).unwrap();
176            let entry = AccessLogEntry::new("127.0.0.1", "GET", "/second");
177            writer.write(&entry).unwrap();
178        }
179        
180        let content = std::fs::read_to_string(&path).unwrap();
181        assert!(content.contains("/first"));
182        assert!(content.contains("/second"));
183        // Should have 2 lines
184        assert_eq!(content.lines().count(), 2);
185    }
186
187    #[test]
188    fn test_clone() {
189        let (writer, _dir) = create_test_writer();
190        let cloned = writer.clone();
191        
192        // Both should work
193        let entry = AccessLogEntry::new("127.0.0.1", "GET", "/test");
194        assert!(writer.write(&entry).is_ok());
195        assert!(cloned.write(&entry).is_ok());
196    }
197
198    #[test]
199    fn test_flush() {
200        let (writer, _dir) = create_test_writer();
201        
202        let entry = AccessLogEntry::new("127.0.0.1", "GET", "/test");
203        writer.write(&entry).unwrap();
204        
205        let result = writer.flush();
206        assert!(result.is_ok());
207    }
208
209    #[test]
210    fn test_create_parent_directories() {
211        let dir = TempDir::new().unwrap();
212        let path = dir.path().join("logs/app/access.log");
213        
214        let writer = AccessLogWriter::new(&path).unwrap();
215        assert!(path.exists());
216    }
217
218    #[test]
219    fn test_json_format_output() {
220        let dir = TempDir::new().unwrap();
221        let path = dir.path().join("access.log");
222        let writer = AccessLogWriter::with_format(&path, LogFormat::Json).unwrap();
223        
224        let entry = AccessLogEntry::new("127.0.0.1", "GET", "/api")
225            .with_status(200)
226            .with_duration_ms(50);
227        
228        writer.write(&entry).unwrap();
229        
230        let content = std::fs::read_to_string(&path).unwrap();
231        assert!(content.starts_with("{"));
232        assert!(content.contains("\"client_ip\""));
233        assert!(content.contains("\"duration_ms\":50"));
234    }
235
236    #[test]
237    fn test_common_format_output() {
238        let dir = TempDir::new().unwrap();
239        let path = dir.path().join("access.log");
240        let writer = AccessLogWriter::with_format(&path, LogFormat::Common).unwrap();
241        
242        let entry = AccessLogEntry::new("127.0.0.1", "GET", "/page")
243            .with_status(200)
244            .with_size(100);
245        
246        writer.write(&entry).unwrap();
247        
248        let content = std::fs::read_to_string(&path).unwrap();
249        // Common format should NOT have user-agent or referer
250        assert!(!content.contains("\"-\" \"-\""));
251    }
252
253    #[test]
254    fn test_concurrent_writes() {
255        use std::thread;
256
257        let dir = TempDir::new().unwrap();
258        let path = dir.path().join("access.log");
259        let writer = AccessLogWriter::new(&path).unwrap();
260        let writer = Arc::new(writer);
261        
262        let mut handles = vec![];
263        for i in 0..5 {
264            let w = Arc::clone(&writer);
265            handles.push(thread::spawn(move || {
266                let entry = AccessLogEntry::new("127.0.0.1", "GET", format!("/page{}", i));
267                w.write(&entry).unwrap();
268            }));
269        }
270        
271        for h in handles {
272            h.join().unwrap();
273        }
274        
275        let content = std::fs::read_to_string(&path).unwrap();
276        // Should have 5 lines
277        assert_eq!(content.lines().count(), 5);
278    }
279}