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