rush_sync_server/server/
logging.rs

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        // Ein-Pass-Statistik (ohne Sort)
453        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}