rush_sync_server/server/
logging.rs

1// Complete Updated src/server/logging.rs
2use 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    // NEW: Create from main LoggingConfig
46    pub fn from_main_config(logging_config: &LoggingConfig) -> Self {
47        Self {
48            max_file_size_bytes: logging_config.max_file_size_mb * 1024 * 1024, // Convert MB to bytes
49            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, // 100MB
59            max_archive_files: 9,
60            compress_archives: true,
61        }
62    }
63}
64
65pub struct ServerLogger {
66    log_file_path: PathBuf,
67    config: LogRotationConfig, // NEW: Store config instance
68    should_log_requests: bool, // NEW: Configurable logging flags
69    should_log_security: bool,
70    should_log_performance: bool,
71}
72
73impl ServerLogger {
74    // NEW: Primary constructor with full config
75    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    // Legacy constructor for backward compatibility
106    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    // UPDATED: Check if request logging is enabled
146    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(()); // Skip if disabled in config
155        }
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    // UPDATED: Check if security logging is enabled
223    pub async fn log_security_alert(&self, ip: &str, reason: &str, details: &str) -> Result<()> {
224        if !self.should_log_security {
225            return Ok(()); // Skip if disabled in config
226        }
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    // NEW: Performance logging
258    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(()); // Skip if disabled in config
266        }
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    // UPDATED: Use instance config instead of default
322    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    // UPDATED: Use instance config
338    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        // Move existing archives backward
344        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        // Move current file to .1
362        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        // Compression
368        if self.config.compress_archives {
369            self.compress_log_file(&archive_path).await?;
370        }
371
372        // Cleanup
373        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    // NEW: Getters for config validation
500    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, // NEW: Track performance warnings
520    pub total_bytes_sent: u64,
521    pub avg_response_time: u64,
522    pub max_response_time: u64,
523}