1use crate::core::config::LoggingConfig;
3use crate::core::prelude::*;
4use actix_web::HttpMessage;
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9#[derive(Debug, Serialize, Deserialize, Clone)]
10pub struct ServerLogEntry {
11 pub timestamp: String,
12 pub timestamp_unix: u64,
13 pub event_type: LogEventType,
14 pub ip_address: String,
15 pub user_agent: Option<String>,
16 pub method: String,
17 pub path: String,
18 pub status_code: Option<u16>,
19 pub response_time_ms: Option<u64>,
20 pub bytes_sent: Option<u64>,
21 pub referer: Option<String>,
22 pub query_string: Option<String>,
23 pub headers: std::collections::HashMap<String, String>,
24 pub session_id: Option<String>,
25}
26
27#[derive(Debug, Serialize, Deserialize, Clone)]
28pub enum LogEventType {
29 Request,
30 ServerStart,
31 ServerStop,
32 ServerError,
33 SecurityAlert,
34 PerformanceWarning,
35}
36
37#[derive(Debug, Clone)]
38pub struct LogRotationConfig {
39 pub max_file_size_bytes: u64,
40 pub max_archive_files: u8,
41 pub compress_archives: bool,
42}
43
44impl LogRotationConfig {
45 pub fn from_main_config(logging_config: &LoggingConfig) -> Self {
47 Self {
48 max_file_size_bytes: logging_config.max_file_size_mb * 1024 * 1024, max_archive_files: logging_config.max_archive_files,
50 compress_archives: logging_config.compress_archives,
51 }
52 }
53}
54
55impl Default for LogRotationConfig {
56 fn default() -> Self {
57 Self {
58 max_file_size_bytes: 100 * 1024 * 1024, max_archive_files: 9,
60 compress_archives: true,
61 }
62 }
63}
64
65pub struct ServerLogger {
66 log_file_path: PathBuf,
67 config: LogRotationConfig, should_log_requests: bool, should_log_security: bool,
70 should_log_performance: bool,
71}
72
73impl ServerLogger {
74 pub fn new_with_config(
76 server_name: &str,
77 port: u16,
78 logging_config: &LoggingConfig,
79 ) -> Result<Self> {
80 let exe_path = std::env::current_exe().map_err(AppError::Io)?;
81 let base_dir = exe_path.parent().ok_or_else(|| {
82 AppError::Validation("Cannot determine executable directory".to_string())
83 })?;
84
85 let log_file_path = base_dir
86 .join(".rss")
87 .join("servers")
88 .join(format!("{}-[{}].log", server_name, port));
89
90 if let Some(parent) = log_file_path.parent() {
91 std::fs::create_dir_all(parent).map_err(AppError::Io)?;
92 }
93
94 let config = LogRotationConfig::from_main_config(logging_config);
95
96 Ok(Self {
97 log_file_path,
98 config,
99 should_log_requests: logging_config.log_requests,
100 should_log_security: logging_config.log_security_alerts,
101 should_log_performance: logging_config.log_performance,
102 })
103 }
104
105 pub fn new(server_name: &str, port: u16) -> Result<Self> {
107 let default_config = LoggingConfig::default();
108 Self::new_with_config(server_name, port, &default_config)
109 }
110
111 pub async fn log_server_start(&self) -> Result<()> {
112 let entry = self.create_system_entry(LogEventType::ServerStart);
113 self.write_log_entry(entry).await
114 }
115
116 pub async fn log_server_stop(&self) -> Result<()> {
117 let entry = self.create_system_entry(LogEventType::ServerStop);
118 self.write_log_entry(entry).await
119 }
120
121 fn create_system_entry(&self, event_type: LogEventType) -> ServerLogEntry {
122 ServerLogEntry {
123 timestamp: chrono::Local::now()
124 .format("%Y-%m-%d %H:%M:%S%.3f")
125 .to_string(),
126 timestamp_unix: SystemTime::now()
127 .duration_since(UNIX_EPOCH)
128 .unwrap_or_default()
129 .as_secs(),
130 event_type,
131 ip_address: "127.0.0.1".to_string(),
132 user_agent: None,
133 method: "SYSTEM".to_string(),
134 path: "/".to_string(),
135 status_code: None,
136 response_time_ms: None,
137 bytes_sent: None,
138 referer: None,
139 query_string: None,
140 headers: std::collections::HashMap::new(),
141 session_id: None,
142 }
143 }
144
145 pub async fn log_request(
147 &self,
148 req: &actix_web::HttpRequest,
149 status: u16,
150 response_time: u64,
151 bytes_sent: u64,
152 ) -> Result<()> {
153 if !self.should_log_requests {
154 return Ok(()); }
156
157 let ip = {
158 let connection_info = req.connection_info();
159 connection_info
160 .realip_remote_addr()
161 .or_else(|| connection_info.peer_addr())
162 .unwrap_or("unknown")
163 .split(':')
164 .next()
165 .unwrap_or("unknown")
166 .to_string()
167 };
168
169 let headers: std::collections::HashMap<String, String> = req
170 .headers()
171 .iter()
172 .filter_map(|(name, value)| {
173 let header_name = name.as_str().to_lowercase();
174 if !["authorization", "cookie", "x-api-key"].contains(&header_name.as_str()) {
175 value
176 .to_str()
177 .ok()
178 .map(|v| (name.as_str().to_string(), v.to_string()))
179 } else {
180 Some((name.as_str().to_string(), "[FILTERED]".to_string()))
181 }
182 })
183 .collect();
184
185 let entry = ServerLogEntry {
186 timestamp: chrono::Local::now()
187 .format("%Y-%m-%d %H:%M:%S%.3f")
188 .to_string(),
189 timestamp_unix: SystemTime::now()
190 .duration_since(UNIX_EPOCH)
191 .unwrap_or_default()
192 .as_secs(),
193 event_type: LogEventType::Request,
194 ip_address: ip,
195 user_agent: req
196 .headers()
197 .get("user-agent")
198 .and_then(|h| h.to_str().ok())
199 .map(|s| s.to_string()),
200 method: req.method().to_string(),
201 path: req.path().to_string(),
202 status_code: Some(status),
203 response_time_ms: Some(response_time),
204 bytes_sent: Some(bytes_sent),
205 referer: req
206 .headers()
207 .get("referer")
208 .and_then(|h| h.to_str().ok())
209 .map(|s| s.to_string()),
210 query_string: if req.query_string().is_empty() {
211 None
212 } else {
213 Some(req.query_string().to_string())
214 },
215 headers,
216 session_id: req.extensions().get::<String>().cloned(),
217 };
218
219 self.write_log_entry(entry).await
220 }
221
222 pub async fn log_security_alert(&self, ip: &str, reason: &str, details: &str) -> Result<()> {
224 if !self.should_log_security {
225 return Ok(()); }
227
228 let mut headers = std::collections::HashMap::new();
229 headers.insert("alert_reason".to_string(), reason.to_string());
230 headers.insert("alert_details".to_string(), details.to_string());
231
232 let entry = ServerLogEntry {
233 timestamp: chrono::Local::now()
234 .format("%Y-%m-%d %H:%M:%S%.3f")
235 .to_string(),
236 timestamp_unix: SystemTime::now()
237 .duration_since(UNIX_EPOCH)
238 .unwrap_or_default()
239 .as_secs(),
240 event_type: LogEventType::SecurityAlert,
241 ip_address: ip.to_string(),
242 user_agent: None,
243 method: "SECURITY".to_string(),
244 path: "/".to_string(),
245 status_code: None,
246 response_time_ms: None,
247 bytes_sent: None,
248 referer: None,
249 query_string: None,
250 headers,
251 session_id: None,
252 };
253
254 self.write_log_entry(entry).await
255 }
256
257 pub async fn log_performance_warning(
259 &self,
260 metric: &str,
261 value: u64,
262 threshold: u64,
263 ) -> Result<()> {
264 if !self.should_log_performance {
265 return Ok(()); }
267
268 let mut headers = std::collections::HashMap::new();
269 headers.insert("metric".to_string(), metric.to_string());
270 headers.insert("value".to_string(), value.to_string());
271 headers.insert("threshold".to_string(), threshold.to_string());
272
273 let entry = ServerLogEntry {
274 timestamp: chrono::Local::now()
275 .format("%Y-%m-%d %H:%M:%S%.3f")
276 .to_string(),
277 timestamp_unix: SystemTime::now()
278 .duration_since(UNIX_EPOCH)
279 .unwrap_or_default()
280 .as_secs(),
281 event_type: LogEventType::PerformanceWarning,
282 ip_address: "127.0.0.1".to_string(),
283 user_agent: None,
284 method: "PERFORMANCE".to_string(),
285 path: "/".to_string(),
286 status_code: None,
287 response_time_ms: Some(value),
288 bytes_sent: None,
289 referer: None,
290 query_string: None,
291 headers,
292 session_id: None,
293 };
294
295 self.write_log_entry(entry).await
296 }
297
298 pub async fn write_log_entry(&self, entry: ServerLogEntry) -> Result<()> {
299 self.check_and_rotate_if_needed().await?;
300
301 let json_line = serde_json::to_string(&entry)
302 .map_err(|e| AppError::Validation(format!("Failed to serialize log entry: {}", e)))?;
303
304 let mut file = tokio::fs::OpenOptions::new()
305 .create(true)
306 .append(true)
307 .open(&self.log_file_path)
308 .await
309 .map_err(AppError::Io)?;
310
311 tokio::io::AsyncWriteExt::write_all(&mut file, format!("{}\n", json_line).as_bytes())
312 .await
313 .map_err(AppError::Io)?;
314
315 tokio::io::AsyncWriteExt::flush(&mut file)
316 .await
317 .map_err(AppError::Io)?;
318 Ok(())
319 }
320
321 async fn check_and_rotate_if_needed(&self) -> Result<()> {
323 if !self.log_file_path.exists() {
324 return Ok(());
325 }
326
327 let metadata = tokio::fs::metadata(&self.log_file_path)
328 .await
329 .map_err(AppError::Io)?;
330 if metadata.len() >= self.config.max_file_size_bytes {
331 self.rotate_log_files().await?;
332 }
333
334 Ok(())
335 }
336
337 async fn rotate_log_files(&self) -> Result<()> {
339 let base_path = &self.log_file_path;
340 let base_name = base_path.file_stem().unwrap().to_string_lossy();
341 let parent_dir = base_path.parent().unwrap();
342
343 for i in (1..self.config.max_archive_files).rev() {
345 let old_gz_path = parent_dir.join(format!("{}.{}.log.gz", base_name, i));
346 let old_log_path = parent_dir.join(format!("{}.{}.log", base_name, i));
347 let new_gz_path = parent_dir.join(format!("{}.{}.log.gz", base_name, i + 1));
348 let new_log_path = parent_dir.join(format!("{}.{}.log", base_name, i + 1));
349
350 if old_gz_path.exists() {
351 tokio::fs::rename(&old_gz_path, &new_gz_path)
352 .await
353 .map_err(AppError::Io)?;
354 } else if old_log_path.exists() {
355 tokio::fs::rename(&old_log_path, &new_log_path)
356 .await
357 .map_err(AppError::Io)?;
358 }
359 }
360
361 let archive_path = parent_dir.join(format!("{}.1.log", base_name));
363 tokio::fs::rename(base_path, &archive_path)
364 .await
365 .map_err(AppError::Io)?;
366
367 if self.config.compress_archives {
369 self.compress_log_file(&archive_path).await?;
370 }
371
372 let cleanup_num = self.config.max_archive_files + 1;
374 let cleanup_log = parent_dir.join(format!("{}.{}.log", base_name, cleanup_num));
375 let cleanup_gz = parent_dir.join(format!("{}.{}.log.gz", base_name, cleanup_num));
376
377 if cleanup_log.exists() {
378 tokio::fs::remove_file(&cleanup_log)
379 .await
380 .map_err(AppError::Io)?;
381 }
382 if cleanup_gz.exists() {
383 tokio::fs::remove_file(&cleanup_gz)
384 .await
385 .map_err(AppError::Io)?;
386 }
387
388 Ok(())
389 }
390
391 async fn compress_log_file(&self, file_path: &std::path::Path) -> Result<()> {
392 use flate2::write::GzEncoder;
393 use flate2::Compression;
394 use std::io::Write;
395
396 let content = tokio::fs::read(file_path).await.map_err(AppError::Io)?;
397 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
398 encoder.write_all(&content).map_err(AppError::Io)?;
399 let compressed = encoder.finish().map_err(AppError::Io)?;
400
401 let gz_path = file_path.with_extension("log.gz");
402 tokio::fs::write(&gz_path, compressed)
403 .await
404 .map_err(AppError::Io)?;
405 tokio::fs::remove_file(file_path)
406 .await
407 .map_err(AppError::Io)?;
408
409 Ok(())
410 }
411
412 pub fn get_log_file_size_bytes(&self) -> Result<u64> {
413 if !self.log_file_path.exists() {
414 return Ok(0);
415 }
416 let metadata = std::fs::metadata(&self.log_file_path).map_err(AppError::Io)?;
417 Ok(metadata.len())
418 }
419
420 pub fn list_log_files(&self) -> Result<Vec<PathBuf>> {
421 let parent_dir = self.log_file_path.parent().unwrap();
422 let base_name = self.log_file_path.file_stem().unwrap().to_string_lossy();
423 let mut files = Vec::new();
424
425 if self.log_file_path.exists() {
426 files.push(self.log_file_path.clone());
427 }
428
429 for i in 1..=10 {
430 let archive_path = parent_dir.join(format!("{}.{}.log", base_name, i));
431 let gz_path = parent_dir.join(format!("{}.{}.log.gz", base_name, i));
432
433 if archive_path.exists() {
434 files.push(archive_path);
435 } else if gz_path.exists() {
436 files.push(gz_path);
437 }
438 }
439
440 Ok(files)
441 }
442
443 pub async fn get_request_stats(&self) -> Result<ServerStats> {
444 if !self.log_file_path.exists() {
445 return Ok(ServerStats::default());
446 }
447
448 let content = tokio::fs::read_to_string(&self.log_file_path)
449 .await
450 .map_err(AppError::Io)?;
451 let mut stats = ServerStats::default();
452 let mut unique_ips = std::collections::HashSet::new();
453 let mut response_times = Vec::new();
454
455 for line in content.lines() {
456 if let Ok(entry) = serde_json::from_str::<ServerLogEntry>(line) {
457 match entry.event_type {
458 LogEventType::Request => {
459 stats.total_requests += 1;
460 unique_ips.insert(entry.ip_address.clone());
461
462 if let Some(status) = entry.status_code {
463 if status >= 400 {
464 stats.error_requests += 1;
465 }
466 }
467
468 if let Some(response_time) = entry.response_time_ms {
469 response_times.push(response_time);
470 }
471
472 if let Some(bytes) = entry.bytes_sent {
473 stats.total_bytes_sent += bytes;
474 }
475 }
476 LogEventType::SecurityAlert => {
477 stats.security_alerts += 1;
478 }
479 LogEventType::PerformanceWarning => {
480 stats.performance_warnings += 1;
481 }
482 _ => {}
483 }
484 }
485 }
486
487 stats.unique_ips = unique_ips.len() as u64;
488
489 if !response_times.is_empty() {
490 response_times.sort();
491 stats.avg_response_time =
492 response_times.iter().sum::<u64>() / response_times.len() as u64;
493 stats.max_response_time = *response_times.last().unwrap_or(&0);
494 }
495
496 Ok(stats)
497 }
498
499 pub fn get_config_summary(&self) -> String {
501 format!(
502 "Log Config: Max Size {}MB, Archives: {}, Compression: {}, Requests: {}, Security: {}, Performance: {}",
503 self.config.max_file_size_bytes / 1024 / 1024,
504 self.config.max_archive_files,
505 self.config.compress_archives,
506 self.should_log_requests,
507 self.should_log_security,
508 self.should_log_performance
509 )
510 }
511}
512
513#[derive(Debug, Default)]
514pub struct ServerStats {
515 pub total_requests: u64,
516 pub unique_ips: u64,
517 pub error_requests: u64,
518 pub security_alerts: u64,
519 pub performance_warnings: u64, pub total_bytes_sent: u64,
521 pub avg_response_time: u64,
522 pub max_response_time: u64,
523}