rush_sync_server/server/handlers/web/
logs.rs

1use super::ServerDataWithConfig;
2use crate::server::logging::ServerLogger;
3use actix_web::{web, HttpRequest, HttpResponse, Result as ActixResult};
4use serde_json::json;
5use std::path::PathBuf;
6use tokio::fs;
7use tokio::io::{AsyncBufReadExt, BufReader};
8
9pub async fn logs_raw_handler(
10    req: HttpRequest,
11    data: web::Data<ServerDataWithConfig>,
12) -> ActixResult<HttpResponse> {
13    let exe_path = std::env::current_exe().unwrap();
14    let base_dir = exe_path.parent().unwrap();
15    let log_file_path = base_dir
16        .join(".rss")
17        .join("servers")
18        .join(format!("{}-[{}].log", data.server.name, data.server.port));
19
20    if !log_file_path.exists() {
21        return Ok(HttpResponse::Ok().json(json!({
22            "new_entries": [],
23            "file_size": 0,
24            "total_lines": 0,
25            "status": "no_log_file"
26        })));
27    }
28
29    let metadata = match fs::metadata(&log_file_path).await {
30        Ok(meta) => meta,
31        Err(e) => {
32            log::error!("Failed to read log file metadata: {}", e);
33            return Ok(HttpResponse::InternalServerError().json(json!({
34                "error": "Failed to access log file"
35            })));
36        }
37    };
38
39    let current_file_size = metadata.len();
40
41    let last_known_size: u64 = req
42        .headers()
43        .get("X-Log-Size")
44        .and_then(|h| h.to_str().ok())
45        .and_then(|s| s.parse().ok())
46        .unwrap_or(0);
47
48    if current_file_size <= last_known_size {
49        return Ok(HttpResponse::Ok().json(json!({
50            "new_entries": [],
51            "file_size": current_file_size,
52            "total_lines": 0,
53            "status": "no_new_data"
54        })));
55    }
56
57    let new_entries = match read_log_entries_from_offset(&log_file_path, last_known_size).await {
58        Ok(entries) => entries,
59        Err(e) => {
60            log::error!("Failed to read log entries: {}", e);
61            return Ok(HttpResponse::InternalServerError().json(json!({
62                "error": "Failed to read log entries"
63            })));
64        }
65    };
66
67    let stats = get_log_stats(&log_file_path).await.ok();
68
69    Ok(HttpResponse::Ok().json(json!({
70        "new_entries": new_entries,
71        "file_size": current_file_size,
72        "total_lines": new_entries.len(),
73        "status": "success",
74        "stats": stats
75    })))
76}
77
78pub async fn logs_handler(data: web::Data<ServerDataWithConfig>) -> ActixResult<HttpResponse> {
79    let server_dir = format!("www/{}-[{}]", data.server.name, data.server.port);
80    let log_path = format!(
81        ".rss/servers/{}-[{}].log",
82        data.server.name, data.server.port
83    );
84
85    let log_entries = if let Ok(logger) = ServerLogger::new(&data.server.name, data.server.port) {
86        match logger.get_log_file_size_bytes() {
87            Ok(size) if size > 0 => format!("Log file size: {} bytes", size),
88            _ => "No log entries yet".to_string(),
89        }
90    } else {
91        "Logger unavailable".to_string()
92    };
93
94    let html = format!(
95        r#"<!DOCTYPE html>
96<html lang="de">
97<head>
98   <meta charset="UTF-8">
99   <meta name="viewport" content="width=device-width, initial-scale=1.0">
100   <title>Server Logs - {}</title>
101   <link rel="icon" href="/.rss/favicon.svg" type="image/svg+xml">
102   <style>
103       body {{
104           font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Monaco', monospace;
105           margin: 0;
106           background: #1a1d23;
107           color: #ffffff;
108           padding: 1rem;
109       }}
110       .header {{
111           background: #252830;
112           padding: 1rem;
113           border-radius: 6px;
114           border: 1px solid #3a3f47;
115           margin-bottom: 1rem;
116       }}
117       .header h1 {{
118           margin: 0 0 0.75rem 0;
119           font-size: 18px;
120           color: #ffffff;
121       }}
122       .server-info {{
123           font-size: 11px;
124           color: #a0a6b1;
125           line-height: 1.5;
126       }}
127       .back-link {{
128           color: #00d4ff;
129           text-decoration: none;
130           font-size: 11px;
131       }}
132       .back-link:hover {{
133           background: #00d4ff;
134           color: #1a1d23;
135           padding: 2px 4px;
136           border-radius: 3px;
137       }}
138       .hot-reload-status {{
139           color: #00ff88;
140           font-weight: bold;
141       }}
142       .log-container {{
143           background: #0d1117;
144           border: 1px solid #3a3f47;
145           border-radius: 6px;
146           padding: 1rem;
147           max-height: 600px;
148           overflow-y: auto;
149       }}
150       .log-entry {{
151           margin: 2px 0;
152           font-size: 11px;
153           color: #58a6ff;
154           line-height: 1.3;
155       }}
156   </style>
157   <script>setInterval(function() {{ location.reload(); }}, 5000);</script>
158</head>
159<body>
160   <div class="header">
161       <h1>Server Logs: {}</h1>
162       <div class="server-info">
163           <p>ID: {} | HTTP: {} | Proxy: {}.localhost:{}</p>
164           <p>Directory: {} | Log: {}</p>
165           <p class="hot-reload-status">Hot Reload: ACTIVE (WebSocket on /ws/hot-reload)</p>
166           <p><a href="/" class="back-link">← Zurück zur Hauptseite</a></p>
167       </div>
168   </div>
169   <div class="log-container">
170       <div class="log-entry">Server Directory: {}</div>
171       <div class="log-entry">HTTP: http://127.0.0.1:{}</div>
172       <div class="log-entry">Proxy: https://{}.localhost:{}</div>
173       <div class="log-entry">TLS Certificate: .rss/certs/{}-{}.cert</div>
174       <div class="log-entry">Log Status: {}</div>
175       <div class="log-entry">Static Files: Enabled (Template-based)</div>
176       <div class="log-entry">Hot Reload: WebSocket active on /ws/hot-reload</div>
177       <div class="log-entry">File Watcher: Monitoring www directory for changes</div>
178       <div class="log-entry">Configuration: Loaded from rush.toml</div>
179       <div class="log-entry">--- REAL LOG ENTRIES WOULD APPEAR HERE ---</div>
180       <div class="log-entry">Live logging with rotation, security alerts, and performance monitoring</div>
181   </div>
182</body>
183</html>"#,
184        data.server.name,
185        data.server.name,
186        data.server.id,
187        data.server.port,
188        data.server.name,
189        data.proxy_https_port, // FIXED: Verwende proxy_https_port aus data
190        server_dir,
191        log_path,
192        server_dir,
193        data.server.port,
194        data.server.name,
195        data.proxy_https_port, // FIXED: Verwende proxy_https_port aus data
196        data.server.name,
197        data.server.port,
198        log_entries
199    );
200
201    Ok(HttpResponse::Ok()
202        .content_type("text/html; charset=utf-8")
203        .body(html))
204}
205
206async fn read_log_entries_from_offset(
207    file_path: &PathBuf,
208    offset: u64,
209) -> Result<Vec<serde_json::Value>, Box<dyn std::error::Error + Send + Sync>> {
210    let file = fs::File::open(file_path).await?;
211    let mut reader = BufReader::new(file);
212
213    if offset > 0 {
214        use tokio::io::AsyncSeekExt;
215        let mut file_with_seek = fs::File::open(file_path).await?;
216        file_with_seek
217            .seek(std::io::SeekFrom::Start(offset))
218            .await?;
219        reader = BufReader::new(file_with_seek);
220    }
221
222    let mut entries = Vec::new();
223    let mut line = String::new();
224    let mut lines_read = 0;
225    const MAX_LINES_PER_REQUEST: usize = 100;
226
227    while lines_read < MAX_LINES_PER_REQUEST {
228        line.clear();
229        let bytes_read = reader.read_line(&mut line).await?;
230
231        if bytes_read == 0 {
232            break;
233        }
234
235        let trimmed_line = line.trim();
236        if trimmed_line.is_empty() {
237            continue;
238        }
239
240        if let Ok(json_entry) = serde_json::from_str::<serde_json::Value>(trimmed_line) {
241            entries.push(json_entry);
242        } else {
243            entries.push(json!({
244                "timestamp": chrono::Local::now().to_rfc3339(),
245                "timestamp_unix": chrono::Utc::now().timestamp(),
246                "event_type": "PlainText",
247                "message": trimmed_line,
248                "level": "INFO"
249            }));
250        }
251
252        lines_read += 1;
253    }
254
255    Ok(entries)
256}
257
258async fn get_log_stats(
259    file_path: &PathBuf,
260) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
261    let file = fs::File::open(file_path).await?;
262    let reader = BufReader::new(file);
263    let mut lines = reader.lines();
264
265    let mut total_requests = 0;
266    let mut error_requests = 0;
267    let mut unique_ips = std::collections::HashSet::new();
268    let mut total_bytes = 0u64;
269    let mut response_times = Vec::new();
270
271    let mut line_count = 0;
272    const MAX_LINES_FOR_STATS: usize = 1000;
273
274    while let Ok(Some(line)) = lines.next_line().await {
275        if line_count >= MAX_LINES_FOR_STATS {
276            break;
277        }
278
279        if let Ok(log_entry) = serde_json::from_str::<serde_json::Value>(&line) {
280            if log_entry.get("event_type").and_then(|v| v.as_str()) == Some("Request") {
281                total_requests += 1;
282
283                if let Some(ip) = log_entry.get("ip_address").and_then(|v| v.as_str()) {
284                    unique_ips.insert(ip.to_string());
285                }
286
287                if let Some(status) = log_entry.get("status_code").and_then(|v| v.as_u64()) {
288                    if status >= 400 {
289                        error_requests += 1;
290                    }
291                }
292
293                if let Some(bytes) = log_entry.get("bytes_sent").and_then(|v| v.as_u64()) {
294                    total_bytes += bytes;
295                }
296
297                if let Some(rt) = log_entry.get("response_time_ms").and_then(|v| v.as_u64()) {
298                    response_times.push(rt);
299                }
300            }
301        }
302
303        line_count += 1;
304    }
305
306    let avg_response_time = if response_times.is_empty() {
307        0
308    } else {
309        response_times.iter().sum::<u64>() / response_times.len() as u64
310    };
311
312    let max_response_time = response_times.iter().max().copied().unwrap_or(0);
313
314    Ok(json!({
315        "total_requests": total_requests,
316        "error_requests": error_requests,
317        "unique_ips": unique_ips.len(),
318        "total_bytes_sent": total_bytes,
319        "avg_response_time_ms": avg_response_time,
320        "max_response_time_ms": max_response_time,
321        "lines_processed": line_count
322    }))
323}