mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 13:04:11 +00:00
feat: add timestamps to process logs and inspector UI
Backend changes:
- Add timestamps to log lines: [2026-01-30T12:32:45.123Z] <line>
- stdout.log and stderr.log get timestamps per line
- combined.log includes [stdout]/[stderr] prefix after timestamp
- Add strip_timestamps query param to GET /process/{id}/logs
- Use time crate with RFC3339 format for timestamps
Frontend changes:
- Add Processes tab to inspector debug panel
- Show list of processes with status badges (running/stopped/killed)
- Click to expand and view logs
- Log viewer options:
- Select stream: combined, stdout, stderr
- Toggle strip_timestamps
- Refresh logs button
- Action buttons: stop (SIGTERM), kill (SIGKILL), delete
- Auto-refresh process list every 5 seconds
This commit is contained in:
parent
afb2c74eea
commit
db0268b88f
5 changed files with 464 additions and 15 deletions
|
|
@ -11,6 +11,8 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader as TokioBufReader};
|
||||
use tokio::process::{Child, Command};
|
||||
use tokio::sync::{broadcast, Mutex, RwLock};
|
||||
|
|
@ -103,6 +105,9 @@ pub struct LogsQuery {
|
|||
/// Which log stream to read: "stdout", "stderr", or "combined" (default)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stream: Option<String>,
|
||||
/// Strip timestamp prefixes from log lines
|
||||
#[serde(default)]
|
||||
pub strip_timestamps: bool,
|
||||
}
|
||||
|
||||
/// Response with log content
|
||||
|
|
@ -356,13 +361,14 @@ impl ProcessManager {
|
|||
};
|
||||
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
let log_line = format!("[stdout] {}\n", line);
|
||||
let _ = file.write_all(line.as_bytes());
|
||||
let _ = file.write_all(b"\n");
|
||||
let timestamp = format_timestamp();
|
||||
let timestamped_line = format!("[{}] {}\n", timestamp, line);
|
||||
let combined_line = format!("[{}] [stdout] {}\n", timestamp, line);
|
||||
let _ = file.write_all(timestamped_line.as_bytes());
|
||||
if let Ok(mut combined) = combined.lock() {
|
||||
let _ = combined.write_all(log_line.as_bytes());
|
||||
let _ = combined.write_all(combined_line.as_bytes());
|
||||
}
|
||||
let _ = log_tx.send(log_line);
|
||||
let _ = log_tx.send(combined_line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -380,13 +386,14 @@ impl ProcessManager {
|
|||
};
|
||||
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
let log_line = format!("[stderr] {}\n", line);
|
||||
let _ = file.write_all(line.as_bytes());
|
||||
let _ = file.write_all(b"\n");
|
||||
let timestamp = format_timestamp();
|
||||
let timestamped_line = format!("[{}] {}\n", timestamp, line);
|
||||
let combined_line = format!("[{}] [stderr] {}\n", timestamp, line);
|
||||
let _ = file.write_all(timestamped_line.as_bytes());
|
||||
if let Ok(mut combined) = combined.lock() {
|
||||
let _ = combined.write_all(log_line.as_bytes());
|
||||
let _ = combined.write_all(combined_line.as_bytes());
|
||||
}
|
||||
let _ = log_tx.send(log_line);
|
||||
let _ = log_tx.send(combined_line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -588,7 +595,7 @@ impl ProcessManager {
|
|||
let content = fs::read_to_string(log_path).unwrap_or_default();
|
||||
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
let (content, line_count) = if let Some(tail) = query.tail {
|
||||
let (mut content, line_count) = if let Some(tail) = query.tail {
|
||||
let start = lines.len().saturating_sub(tail);
|
||||
let tail_lines: Vec<&str> = lines[start..].to_vec();
|
||||
(tail_lines.join("\n"), tail_lines.len())
|
||||
|
|
@ -596,6 +603,11 @@ impl ProcessManager {
|
|||
(content.clone(), lines.len())
|
||||
};
|
||||
|
||||
// Strip timestamps if requested
|
||||
if query.strip_timestamps {
|
||||
content = strip_timestamps(&content);
|
||||
}
|
||||
|
||||
Ok(LogsResponse {
|
||||
content,
|
||||
lines: line_count,
|
||||
|
|
@ -628,6 +640,35 @@ fn process_data_dir() -> PathBuf {
|
|||
.join("processes")
|
||||
}
|
||||
|
||||
/// Format the current time as an ISO 8601 timestamp
|
||||
fn format_timestamp() -> String {
|
||||
OffsetDateTime::now_utc()
|
||||
.format(&Rfc3339)
|
||||
.unwrap_or_else(|_| "unknown".to_string())
|
||||
}
|
||||
|
||||
/// Strip timestamp prefixes from log lines
|
||||
/// Timestamps are in format: [2026-01-30T12:32:45.123Z] or [2026-01-30T12:32:45Z]
|
||||
fn strip_timestamps(content: &str) -> String {
|
||||
content
|
||||
.lines()
|
||||
.map(|line| {
|
||||
// Match pattern: [YYYY-MM-DDTHH:MM:SS...Z] at start of line
|
||||
if line.starts_with('[') {
|
||||
if let Some(end) = line.find("] ") {
|
||||
// Check if it looks like a timestamp (starts with digit after [)
|
||||
let potential_ts = &line[1..end];
|
||||
if potential_ts.len() >= 19 && potential_ts.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
|
||||
return &line[end + 2..];
|
||||
}
|
||||
}
|
||||
}
|
||||
line
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// Helper to save state from within a spawned task (simplified version)
|
||||
async fn save_state_to_file(base_dir: &PathBuf) -> Result<(), std::io::Error> {
|
||||
// This is a no-op for now - the state will be saved on the next explicit save_state call
|
||||
|
|
|
|||
|
|
@ -4040,7 +4040,8 @@ async fn delete_process(
|
|||
("id" = String, Path, description = "Process ID"),
|
||||
("tail" = Option<usize>, Query, description = "Number of lines from end"),
|
||||
("follow" = Option<bool>, Query, description = "Stream logs via SSE"),
|
||||
("stream" = Option<String>, Query, description = "Log stream: stdout, stderr, or combined")
|
||||
("stream" = Option<String>, Query, description = "Log stream: stdout, stderr, or combined"),
|
||||
("strip_timestamps" = Option<bool>, Query, description = "Strip timestamp prefixes from log lines")
|
||||
),
|
||||
responses(
|
||||
(status = 200, body = LogsResponse, description = "Log content"),
|
||||
|
|
@ -4059,6 +4060,7 @@ async fn get_process_logs(
|
|||
tail: query.tail,
|
||||
follow: false,
|
||||
stream: query.stream.clone(),
|
||||
strip_timestamps: query.strip_timestamps,
|
||||
}).await?;
|
||||
|
||||
let receiver = state.process_manager.subscribe_logs(&id).await?;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue