rust_serv/access_log/
writer.rs1use 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
10pub struct AccessLogWriter {
12 path: PathBuf,
14 format: LogFormat,
16 file: Arc<Mutex<File>>,
18}
19
20impl AccessLogWriter {
21 pub fn new<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
23 Self::with_format(path, LogFormat::default())
24 }
25
26 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 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 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 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 pub fn path(&self) -> &Path {
69 &self.path
70 }
71
72 pub fn format(&self) -> LogFormat {
74 self.format
75 }
76
77 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 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 {
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 {
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 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 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 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 assert_eq!(content.lines().count(), 5);
278 }
279}