|
1 | | -use actix_web::{get, web, HttpResponse, Responder}; |
| 1 | +use actix_web::{HttpResponse, Responder, get, web}; |
2 | 2 | use actix_web_lab::sse::{self, Sse}; |
3 | 3 | use futures::StreamExt; |
4 | 4 | use sqlx::PgPool; |
5 | 5 | use uuid::Uuid; |
6 | 6 |
|
7 | | -use crate::db::{get_logs, LogEntry}; |
| 7 | +#[cfg(feature = "websocket")] |
| 8 | +use actix_web::HttpRequest; |
| 9 | +#[cfg(feature = "websocket")] |
| 10 | +use actix_ws::Message; |
| 11 | + |
| 12 | +use crate::db::{LogEntry, get_logs}; |
8 | 13 |
|
9 | 14 | pub struct AppState { |
10 | 15 | pub pool: PgPool, |
@@ -113,3 +118,107 @@ pub async fn stream_worker_logs( |
113 | 118 |
|
114 | 119 | Sse::from_stream(stream).with_keep_alive(std::time::Duration::from_secs(5)) |
115 | 120 | } |
| 121 | + |
| 122 | +#[cfg(feature = "websocket")] |
| 123 | +#[get("/api/v1/workers/{worker_id}/ws-logs")] |
| 124 | +pub async fn ws_worker_logs( |
| 125 | + req: HttpRequest, |
| 126 | + body: web::Payload, |
| 127 | + worker_id: web::Path<Uuid>, |
| 128 | + data: web::Data<AppState>, |
| 129 | +) -> Result<HttpResponse, actix_web::Error> { |
| 130 | + let worker_id = *worker_id; |
| 131 | + |
| 132 | + let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?; |
| 133 | + |
| 134 | + // Fetch last 10 logs from database |
| 135 | + let historical_logs = match get_logs(&data.pool, worker_id, 10).await { |
| 136 | + Ok(logs) => logs, |
| 137 | + Err(e) => { |
| 138 | + log::error!("Failed to fetch historical logs: {:?}", e); |
| 139 | + vec![] |
| 140 | + } |
| 141 | + }; |
| 142 | + |
| 143 | + // Subscribe to NATS for this specific worker's logs |
| 144 | + let subject = format!("{}.console.>", worker_id); |
| 145 | + let nats_sub = match data.nats_client.subscribe(subject).await { |
| 146 | + Ok(sub) => sub, |
| 147 | + Err(e) => { |
| 148 | + log::error!("Failed to subscribe to NATS: {:?}", e); |
| 149 | + return Ok(response); |
| 150 | + } |
| 151 | + }; |
| 152 | + |
| 153 | + // Spawn WebSocket handler task |
| 154 | + actix_web::rt::spawn(async move { |
| 155 | + let mut nats_sub = nats_sub; |
| 156 | + |
| 157 | + // Send historical logs first (oldest first) |
| 158 | + for log_entry in historical_logs.into_iter().rev() { |
| 159 | + let json = serde_json::json!({ |
| 160 | + "date": log_entry.date.timestamp_millis(), |
| 161 | + "level": format!("{:?}", log_entry.level).to_lowercase(), |
| 162 | + "message": log_entry.message |
| 163 | + }); |
| 164 | + |
| 165 | + if session.text(json.to_string()).await.is_err() { |
| 166 | + return; |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | + // Handle incoming messages and NATS subscription concurrently |
| 171 | + loop { |
| 172 | + tokio::select! { |
| 173 | + // Handle incoming WebSocket messages (ping/pong, close) |
| 174 | + Some(msg) = msg_stream.next() => { |
| 175 | + match msg { |
| 176 | + Ok(Message::Ping(bytes)) => { |
| 177 | + if session.pong(&bytes).await.is_err() { |
| 178 | + break; |
| 179 | + } |
| 180 | + } |
| 181 | + Ok(Message::Close(_)) => { |
| 182 | + break; |
| 183 | + } |
| 184 | + Err(_) => { |
| 185 | + break; |
| 186 | + } |
| 187 | + _ => {} |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + // Stream logs from NATS |
| 192 | + Some(nats_msg) = nats_sub.next() => { |
| 193 | + let level_str = nats_msg.subject.split('.').nth(2).unwrap_or("info"); |
| 194 | + |
| 195 | + let message = match String::from_utf8(nats_msg.payload.to_vec()) { |
| 196 | + Ok(m) => m, |
| 197 | + Err(_) => continue, |
| 198 | + }; |
| 199 | + |
| 200 | + let json = serde_json::json!({ |
| 201 | + "date": chrono::Utc::now().timestamp_millis(), |
| 202 | + "level": level_str, |
| 203 | + "message": message |
| 204 | + }); |
| 205 | + |
| 206 | + if session.text(json.to_string()).await.is_err() { |
| 207 | + break; |
| 208 | + } |
| 209 | + } |
| 210 | + |
| 211 | + // Send ping every 30 seconds to keep connection alive |
| 212 | + _ = tokio::time::sleep(std::time::Duration::from_secs(30)) => { |
| 213 | + if session.ping(b"").await.is_err() { |
| 214 | + break; |
| 215 | + } |
| 216 | + } |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + let _ = session.close(None).await; |
| 221 | + }); |
| 222 | + |
| 223 | + Ok(response) |
| 224 | +} |
0 commit comments