rush_sync_server/server/handlers/web/
logs.rs1use 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, server_dir,
191 log_path,
192 server_dir,
193 data.server.port,
194 data.server.name,
195 data.proxy_https_port, 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}