Fetch remote server trace reports
ci / test (push) Has been cancelled
ci / fuzz-smoke (push) Has been cancelled
ci / windows-client (push) Has been cancelled
ci / package-release (linux-x86_64, ubuntu-latest) (push) Has been cancelled
ci / package-release (macos-aarch64, macos-14) (push) Has been cancelled
ci / package-release (macos-x86_64, macos-13) (push) Has been cancelled
ci / package-release (windows-x86_64, windows-latest) (push) Has been cancelled
ci / remote-bench (push) Has been cancelled

This commit is contained in:
DuProcess
2026-07-12 11:55:23 -04:00
parent c65aba9d7a
commit fdbd58b628
4 changed files with 118 additions and 23 deletions
Generated
+1 -1
View File
@@ -436,7 +436,7 @@ dependencies = [
[[package]] [[package]]
name = "dosh" name = "dosh"
version = "1.0.0-rc33" version = "1.0.0-rc34"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "dosh" name = "dosh"
version = "1.0.0-rc33" version = "1.0.0-rc34"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
+5 -4
View File
@@ -85,12 +85,13 @@ dosh trace --client-log /tmp/dosh-client.log HOST
Summarize collected traces with: Summarize collected traces with:
```sh ```sh
dosh trace report --client-log /tmp/dosh-client.log dosh trace report HOST --client-log /tmp/dosh-client.log
``` ```
Set `DOSH_TRACE=/tmp/dosh-server.log` on `dosh-server` for matching server When `HOST` is given, Dosh fetches `/tmp/dosh-server.log` over SSH before
events. Trace byte prefixes are enabled for `dosh trace`, so use it only for building the report. Set `DOSH_TRACE=/tmp/dosh-server.log` on `dosh-server` for
short reproductions. matching server events. Trace byte prefixes are enabled for `dosh trace`, so use
it only for short reproductions.
## VS Code ## VS Code
+111 -17
View File
@@ -792,8 +792,9 @@ struct TraceOptions {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
struct TraceReportOptions { struct TraceReportOptions {
host: Option<String>,
client_log: Option<PathBuf>, client_log: Option<PathBuf>,
server_log: Option<PathBuf>, server_log: Option<String>,
tail: usize, tail: usize,
} }
@@ -888,15 +889,16 @@ fn parse_trace_options(command: &[String]) -> Result<TraceOptions> {
} }
fn parse_trace_report_options(command: &[String]) -> Result<TraceReportOptions> { fn parse_trace_report_options(command: &[String]) -> Result<TraceReportOptions> {
let mut host: Option<String> = None;
let mut client_log: Option<PathBuf> = None; let mut client_log: Option<PathBuf> = None;
let mut server_log: Option<PathBuf> = Some(PathBuf::from("/tmp/dosh-server.log")); let mut server_log: Option<String> = Some("/tmp/dosh-server.log".to_string());
let mut tail = 20usize; let mut tail = 20usize;
let mut index = 0usize; let mut index = 0usize;
while index < command.len() { while index < command.len() {
match command[index].as_str() { match command[index].as_str() {
"--client-log" => { "--client-log" => {
let path = command.get(index + 1).ok_or_else(|| { let path = command.get(index + 1).ok_or_else(|| {
anyhow!("usage: dosh trace report [--client-log PATH] [--server-log PATH|none] [--tail N]") anyhow!("usage: dosh trace report [HOST] [--client-log PATH] [--server-log PATH|none] [--tail N]")
})?; })?;
client_log = Some(expand_tilde(path)); client_log = Some(expand_tilde(path));
index += 2; index += 2;
@@ -911,7 +913,7 @@ fn parse_trace_report_options(command: &[String]) -> Result<TraceReportOptions>
} }
"--server-log" => { "--server-log" => {
let path = command.get(index + 1).ok_or_else(|| { let path = command.get(index + 1).ok_or_else(|| {
anyhow!("usage: dosh trace report [--client-log PATH] [--server-log PATH|none] [--tail N]") anyhow!("usage: dosh trace report [HOST] [--client-log PATH] [--server-log PATH|none] [--tail N]")
})?; })?;
server_log = parse_optional_trace_path(path); server_log = parse_optional_trace_path(path);
index += 2; index += 2;
@@ -929,7 +931,7 @@ fn parse_trace_report_options(command: &[String]) -> Result<TraceReportOptions>
} }
"--tail" => { "--tail" => {
let value = command.get(index + 1).ok_or_else(|| { let value = command.get(index + 1).ok_or_else(|| {
anyhow!("usage: dosh trace report [--client-log PATH] [--server-log PATH|none] [--tail N]") anyhow!("usage: dosh trace report [HOST] [--client-log PATH] [--server-log PATH|none] [--tail N]")
})?; })?;
tail = parse_trace_tail(value)?; tail = parse_trace_tail(value)?;
index += 2; index += 2;
@@ -941,25 +943,35 @@ fn parse_trace_report_options(command: &[String]) -> Result<TraceReportOptions>
tail = parse_trace_tail(value)?; tail = parse_trace_tail(value)?;
index += 1; index += 1;
} }
value => { value if value.starts_with('-') => {
return Err(anyhow!( return Err(anyhow!(
"usage: dosh trace report [--client-log PATH] [--server-log PATH|none] [--tail N] (unknown option {value})" "usage: dosh trace report [HOST] [--client-log PATH] [--server-log PATH|none] [--tail N] (unknown option {value})"
)); ));
} }
value => {
if host.is_some() {
return Err(anyhow!(
"usage: dosh trace report [host] [--client-log PATH] [--server-log PATH|none] [--tail N]"
));
}
host = Some(value.to_string());
index += 1;
}
} }
} }
Ok(TraceReportOptions { Ok(TraceReportOptions {
host,
client_log, client_log,
server_log, server_log,
tail, tail,
}) })
} }
fn parse_optional_trace_path(value: &str) -> Option<PathBuf> { fn parse_optional_trace_path(value: &str) -> Option<String> {
if matches!(value, "" | "none" | "off" | "0") { if matches!(value, "" | "none" | "off" | "0") {
None None
} else { } else {
Some(expand_tilde(value)) Some(value.to_string())
} }
} }
@@ -1006,7 +1018,8 @@ fn sanitize_trace_name(value: &str) -> String {
fn run_trace_command(args: &Args) -> Result<()> { fn run_trace_command(args: &Args) -> Result<()> {
if args.command.first().is_some_and(|value| value == "report") { if args.command.first().is_some_and(|value| value == "report") {
return run_trace_report_command(&args.command[1..]); let config = load_client_config(None).unwrap_or_default();
return run_trace_report_command(&config, args, &args.command[1..]);
} }
let options = parse_trace_options(&args.command)?; let options = parse_trace_options(&args.command)?;
if let Some(parent) = options.client_log.parent() { if let Some(parent) = options.client_log.parent() {
@@ -1042,7 +1055,11 @@ fn run_trace_command(args: &Args) -> Result<()> {
Ok(()) Ok(())
} }
fn run_trace_report_command(command: &[String]) -> Result<()> { fn run_trace_report_command(
config: &dosh::config::ClientConfig,
args: &Args,
command: &[String],
) -> Result<()> {
let options = parse_trace_report_options(command)?; let options = parse_trace_report_options(command)?;
let client_log = match options.client_log { let client_log = match options.client_log {
Some(path) => Some(path), Some(path) => Some(path),
@@ -1061,6 +1078,20 @@ fn run_trace_report_command(command: &[String]) -> Result<()> {
); );
} }
if let Some(path) = options.server_log { if let Some(path) = options.server_log {
let server_log = if let Some(host) = options.host.as_deref() {
match fetch_trace_server_log(config, args, host, &path) {
Ok(path) => Some(path),
Err(err) => {
println!("[warn] server log {host}:{path}: {err:#}");
None
}
}
} else {
Some(expand_tilde(&path))
};
let Some(path) = server_log else {
return Ok(());
};
match summarize_trace_file(&path, options.tail) { match summarize_trace_file(&path, options.tail) {
Ok(report) => print_trace_report("server", &report), Ok(report) => print_trace_report("server", &report),
Err(err) Err(err)
@@ -1076,6 +1107,66 @@ fn run_trace_report_command(command: &[String]) -> Result<()> {
Ok(()) Ok(())
} }
fn fetch_trace_server_log(
config: &dosh::config::ClientConfig,
args: &Args,
requested: &str,
remote_log: &str,
) -> Result<PathBuf> {
let hosts = load_hosts_config(None).unwrap_or_default();
let host_config = hosts.hosts.get(requested).cloned().unwrap_or_default();
let server = status_ssh_target(requested, &host_config);
let ssh_port = args.ssh_port.or(host_config.ssh_port).or(config.ssh_port);
if is_local_status_target(&server) {
return Ok(expand_tilde(remote_log));
}
let local_path = fetched_trace_server_log_path(requested);
if let Some(parent) = local_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create trace log directory {}", parent.to_string_lossy()))?;
}
let mut command = Command::new("ssh");
if let Some(ssh_port) = ssh_port {
command.arg("-p").arg(ssh_port.to_string());
}
command.arg("-T");
if let Some(key) = args.ssh_key.as_deref() {
command.arg("-i").arg(key);
}
if let Some(known_hosts) = args.ssh_known_hosts.as_deref() {
command
.arg("-o")
.arg(format!("UserKnownHostsFile={}", known_hosts.display()));
}
let script = format!("test -r {0} && cat {0}", shell_word(remote_log));
let output = command
.arg(&server)
.arg(format!("sh -c {}", shell_word(&script)))
.output()
.with_context(|| format!("fetch server trace log from {server}"))?;
anyhow::ensure!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
fs::write(&local_path, &output.stdout)
.with_context(|| format!("write fetched server trace {}", local_path.display()))?;
Ok(local_path)
}
fn fetched_trace_server_log_path(host: &str) -> PathBuf {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0);
trace_cache_dir().join(format!(
"server-{}-{}-{}.log",
sanitize_trace_name(host),
now,
std::process::id()
))
}
fn trace_cache_dir() -> PathBuf { fn trace_cache_dir() -> PathBuf {
dirs::cache_dir() dirs::cache_dir()
.unwrap_or_else(|| expand_tilde("~/.cache")) .unwrap_or_else(|| expand_tilde("~/.cache"))
@@ -10324,27 +10415,30 @@ mod tests {
#[test] #[test]
fn trace_report_options_parse_defaults_and_overrides() { fn trace_report_options_parse_defaults_and_overrides() {
let parsed = parse_trace_report_options(&[]).unwrap(); let parsed = parse_trace_report_options(&[]).unwrap();
assert_eq!(parsed.host, None);
assert_eq!(parsed.client_log, None); assert_eq!(parsed.client_log, None);
assert_eq!( assert_eq!(parsed.server_log, Some("/tmp/dosh-server.log".to_string()));
parsed.server_log,
Some(std::path::PathBuf::from("/tmp/dosh-server.log"))
);
assert_eq!(parsed.tail, 20); assert_eq!(parsed.tail, 20);
let parsed = parse_trace_report_options(&[ let parsed = parse_trace_report_options(&[
"palav".to_string(),
"--client-log=/tmp/client.log".to_string(), "--client-log=/tmp/client.log".to_string(),
"--server-log".to_string(), "--server-log".to_string(),
"none".to_string(), "/tmp/server.log".to_string(),
"--tail".to_string(), "--tail".to_string(),
"3".to_string(), "3".to_string(),
]) ])
.unwrap(); .unwrap();
assert_eq!(parsed.host.as_deref(), Some("palav"));
assert_eq!( assert_eq!(
parsed.client_log, parsed.client_log,
Some(std::path::PathBuf::from("/tmp/client.log")) Some(std::path::PathBuf::from("/tmp/client.log"))
); );
assert_eq!(parsed.server_log, None); assert_eq!(parsed.server_log.as_deref(), Some("/tmp/server.log"));
assert_eq!(parsed.tail, 3); assert_eq!(parsed.tail, 3);
let parsed = parse_trace_report_options(&["--server-log=none".to_string()]).unwrap();
assert_eq!(parsed.server_log, None);
} }
#[test] #[test]