1use std::time::SystemTime;
4
5#[derive(Debug, Clone)]
7pub struct AccessLogEntry {
8 pub client_ip: String,
10 pub timestamp: SystemTime,
12 pub method: String,
14 pub path: String,
16 pub version: String,
18 pub status: u16,
20 pub size: usize,
22 pub duration_ms: u64,
24 pub user_agent: Option<String>,
26 pub referer: Option<String>,
28}
29
30impl AccessLogEntry {
31 pub fn new(
33 client_ip: impl Into<String>,
34 method: impl Into<String>,
35 path: impl Into<String>,
36 ) -> Self {
37 Self {
38 client_ip: client_ip.into(),
39 timestamp: SystemTime::now(),
40 method: method.into(),
41 path: path.into(),
42 version: "HTTP/1.1".to_string(),
43 status: 200,
44 size: 0,
45 duration_ms: 0,
46 user_agent: None,
47 referer: None,
48 }
49 }
50
51 pub fn with_version(mut self, version: impl Into<String>) -> Self {
53 self.version = version.into();
54 self
55 }
56
57 pub fn with_status(mut self, status: u16) -> Self {
59 self.status = status;
60 self
61 }
62
63 pub fn with_size(mut self, size: usize) -> Self {
65 self.size = size;
66 self
67 }
68
69 pub fn with_duration_ms(mut self, duration_ms: u64) -> Self {
71 self.duration_ms = duration_ms;
72 self
73 }
74
75 pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
77 self.user_agent = Some(user_agent.into());
78 self
79 }
80
81 pub fn with_referer(mut self, referer: impl Into<String>) -> Self {
83 self.referer = Some(referer.into());
84 self
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq)]
90pub enum LogFormat {
91 Common,
93 Combined,
95 Json,
97}
98
99impl Default for LogFormat {
100 fn default() -> Self {
101 Self::Combined
102 }
103}
104
105impl LogFormat {
106 pub fn format(&self, entry: &AccessLogEntry) -> String {
108 match self {
109 LogFormat::Common => self.format_common(entry),
110 LogFormat::Combined => self.format_combined(entry),
111 LogFormat::Json => self.format_json(entry),
112 }
113 }
114
115 fn format_common(&self, entry: &AccessLogEntry) -> String {
117 let timestamp = self.format_timestamp(&entry.timestamp);
118 format!(
119 "{} - - [{}] \"{} {} {}\" {} {}",
120 entry.client_ip,
121 timestamp,
122 entry.method,
123 entry.path,
124 entry.version,
125 entry.status,
126 entry.size
127 )
128 }
129
130 fn format_combined(&self, entry: &AccessLogEntry) -> String {
132 let timestamp = self.format_timestamp(&entry.timestamp);
133 let referer = entry.referer.as_deref().unwrap_or("-");
134 let user_agent = entry.user_agent.as_deref().unwrap_or("-");
135 format!(
136 "{} - - [{}] \"{} {} {}\" {} {} \"{}\" \"{}\"",
137 entry.client_ip,
138 timestamp,
139 entry.method,
140 entry.path,
141 entry.version,
142 entry.status,
143 entry.size,
144 referer,
145 user_agent
146 )
147 }
148
149 fn format_json(&self, entry: &AccessLogEntry) -> String {
151 let timestamp = self.format_timestamp_iso(&entry.timestamp);
152 let referer = entry.referer.as_deref().unwrap_or("-");
153 let user_agent = entry.user_agent.as_deref().unwrap_or("-");
154
155 format!(
156 r#"{{"client_ip":"{}","timestamp":"{}","method":"{}","path":"{}","version":"{}","status":{},"size":{},"duration_ms":{},"referer":"{}","user_agent":"{}"}}"#,
157 entry.client_ip,
158 timestamp,
159 entry.method,
160 entry.path,
161 entry.version,
162 entry.status,
163 entry.size,
164 entry.duration_ms,
165 referer,
166 user_agent
167 )
168 }
169
170 fn format_timestamp(&self, time: &SystemTime) -> String {
172 use std::time::UNIX_EPOCH;
173 let duration = time.duration_since(UNIX_EPOCH).unwrap_or_default();
174 let secs = duration.as_secs();
175
176 let days = secs / 86400;
178 let years = 1970 + days / 365;
179 let day_of_year = days % 365;
180 let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
181 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
182
183 let hour = (secs % 86400) / 3600;
184 let min = (secs % 3600) / 60;
185 let sec = secs % 60;
186
187 let month_idx = (day_of_year / 30) as usize;
188 let month = months.get(month_idx).unwrap_or(&"Jan");
189 let day = (day_of_year % 30) + 1;
190
191 format!("{:02}/{}/{}:{:02}:{:02}:{:02} +0000", day, month, years, hour, min, sec)
192 }
193
194 fn format_timestamp_iso(&self, time: &SystemTime) -> String {
196 use std::time::UNIX_EPOCH;
197 let duration = time.duration_since(UNIX_EPOCH).unwrap_or_default();
198 let secs = duration.as_secs();
199
200 let days = secs / 86400;
201 let years = 1970 + days / 365;
202 let day_of_year = days % 365;
203 let month = (day_of_year / 30) + 1;
204 let day = (day_of_year % 30) + 1;
205 let hour = (secs % 86400) / 3600;
206 let min = (secs % 3600) / 60;
207 let sec = secs % 60;
208
209 format!("{}-{:02}-{:02}T{:02}:{:02}:{:02}Z", years, month, day, hour, min, sec)
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_access_log_entry_creation() {
219 let entry = AccessLogEntry::new("127.0.0.1", "GET", "/index.html");
220 assert_eq!(entry.client_ip, "127.0.0.1");
221 assert_eq!(entry.method, "GET");
222 assert_eq!(entry.path, "/index.html");
223 assert_eq!(entry.status, 200);
224 assert_eq!(entry.size, 0);
225 }
226
227 #[test]
228 fn test_access_log_entry_with_status() {
229 let entry = AccessLogEntry::new("127.0.0.1", "GET", "/test")
230 .with_status(404);
231 assert_eq!(entry.status, 404);
232 }
233
234 #[test]
235 fn test_access_log_entry_with_size() {
236 let entry = AccessLogEntry::new("127.0.0.1", "GET", "/test")
237 .with_size(1024);
238 assert_eq!(entry.size, 1024);
239 }
240
241 #[test]
242 fn test_access_log_entry_with_duration() {
243 let entry = AccessLogEntry::new("127.0.0.1", "GET", "/test")
244 .with_duration_ms(50);
245 assert_eq!(entry.duration_ms, 50);
246 }
247
248 #[test]
249 fn test_access_log_entry_with_user_agent() {
250 let entry = AccessLogEntry::new("127.0.0.1", "GET", "/test")
251 .with_user_agent("Mozilla/5.0");
252 assert_eq!(entry.user_agent, Some("Mozilla/5.0".to_string()));
253 }
254
255 #[test]
256 fn test_access_log_entry_with_referer() {
257 let entry = AccessLogEntry::new("127.0.0.1", "GET", "/test")
258 .with_referer("https://example.com");
259 assert_eq!(entry.referer, Some("https://example.com".to_string()));
260 }
261
262 #[test]
263 fn test_access_log_entry_with_version() {
264 let entry = AccessLogEntry::new("127.0.0.1", "GET", "/test")
265 .with_version("HTTP/2.0");
266 assert_eq!(entry.version, "HTTP/2.0");
267 }
268
269 #[test]
270 fn test_access_log_entry_chained() {
271 let entry = AccessLogEntry::new("127.0.0.1", "POST", "/api")
272 .with_status(201)
273 .with_size(256)
274 .with_duration_ms(10)
275 .with_user_agent("curl/7.0");
276
277 assert_eq!(entry.client_ip, "127.0.0.1");
278 assert_eq!(entry.method, "POST");
279 assert_eq!(entry.path, "/api");
280 assert_eq!(entry.status, 201);
281 assert_eq!(entry.size, 256);
282 assert_eq!(entry.duration_ms, 10);
283 assert_eq!(entry.user_agent, Some("curl/7.0".to_string()));
284 }
285
286 #[test]
287 fn test_log_format_default() {
288 assert_eq!(LogFormat::default(), LogFormat::Combined);
289 }
290
291 #[test]
292 fn test_log_format_common() {
293 let entry = AccessLogEntry::new("127.0.0.1", "GET", "/index.html")
294 .with_status(200)
295 .with_size(1234);
296
297 let format = LogFormat::Common;
298 let output = format.format(&entry);
299
300 assert!(output.contains("127.0.0.1"));
301 assert!(output.contains("GET /index.html HTTP/1.1"));
302 assert!(output.contains("200 1234"));
303 }
304
305 #[test]
306 fn test_log_format_combined() {
307 let entry = AccessLogEntry::new("127.0.0.1", "GET", "/index.html")
308 .with_status(200)
309 .with_size(1234)
310 .with_user_agent("Mozilla/5.0")
311 .with_referer("https://example.com");
312
313 let format = LogFormat::Combined;
314 let output = format.format(&entry);
315
316 assert!(output.contains("127.0.0.1"));
317 assert!(output.contains("GET /index.html HTTP/1.1"));
318 assert!(output.contains("200 1234"));
319 assert!(output.contains("Mozilla/5.0"));
320 assert!(output.contains("https://example.com"));
321 }
322
323 #[test]
324 fn test_log_format_combined_without_headers() {
325 let entry = AccessLogEntry::new("127.0.0.1", "GET", "/index.html")
326 .with_status(200);
327
328 let format = LogFormat::Combined;
329 let output = format.format(&entry);
330
331 assert!(output.contains("\"-\" \"-\""));
333 }
334
335 #[test]
336 fn test_log_format_json() {
337 let entry = AccessLogEntry::new("127.0.0.1", "GET", "/api")
338 .with_status(200)
339 .with_size(512)
340 .with_duration_ms(25)
341 .with_user_agent("TestAgent");
342
343 let format = LogFormat::Json;
344 let output = format.format(&entry);
345
346 assert!(output.contains("\"client_ip\":\"127.0.0.1\""));
347 assert!(output.contains("\"method\":\"GET\""));
348 assert!(output.contains("\"path\":\"/api\""));
349 assert!(output.contains("\"status\":200"));
350 assert!(output.contains("\"size\":512"));
351 assert!(output.contains("\"duration_ms\":25"));
352 assert!(output.contains("\"user_agent\":\"TestAgent\""));
353 }
354
355 #[test]
356 fn test_log_format_json_without_optional() {
357 let entry = AccessLogEntry::new("127.0.0.1", "GET", "/test");
358
359 let format = LogFormat::Json;
360 let output = format.format(&entry);
361
362 assert!(output.contains("\"referer\":\"-\""));
363 assert!(output.contains("\"user_agent\":\"-\""));
364 }
365
366 #[test]
367 fn test_format_timestamp() {
368 let format = LogFormat::Common;
369 let timestamp = SystemTime::now();
370 let output = format.format_timestamp(×tamp);
371
372 assert!(output.contains("/"));
374 assert!(output.contains(":"));
375 assert!(output.contains("+0000"));
376 }
377
378 #[test]
379 fn test_format_timestamp_iso() {
380 let format = LogFormat::Json;
381 let timestamp = SystemTime::now();
382 let output = format.format_timestamp_iso(×tamp);
383
384 assert!(output.contains("T"));
386 assert!(output.contains("Z"));
387 assert!(output.contains("-"));
388 }
389}