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

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]]
name = "dosh"
version = "1.0.0-rc33"
version = "1.0.0-rc34"
dependencies = [
"anyhow",
"base64",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "dosh"
version = "1.0.0-rc33"
version = "1.0.0-rc34"
edition = "2024"
license = "MIT"
+5 -4
View File
@@ -85,12 +85,13 @@ dosh trace --client-log /tmp/dosh-client.log HOST
Summarize collected traces with:
```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
events. Trace byte prefixes are enabled for `dosh trace`, so use it only for
short reproductions.
When `HOST` is given, Dosh fetches `/tmp/dosh-server.log` over SSH before
building the report. Set `DOSH_TRACE=/tmp/dosh-server.log` on `dosh-server` for
matching server events. Trace byte prefixes are enabled for `dosh trace`, so use
it only for short reproductions.
## VS Code
+111 -17
View File
@@ -792,8 +792,9 @@ struct TraceOptions {
#[derive(Debug, Clone, PartialEq, Eq)]
struct TraceReportOptions {
host: Option<String>,
client_log: Option<PathBuf>,
server_log: Option<PathBuf>,
server_log: Option<String>,
tail: usize,
}
@@ -888,15 +889,16 @@ fn parse_trace_options(command: &[String]) -> Result<TraceOptions> {
}
fn parse_trace_report_options(command: &[String]) -> Result<TraceReportOptions> {
let mut host: Option<String> = 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 index = 0usize;
while index < command.len() {
match command[index].as_str() {
"--client-log" => {
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));
index += 2;
@@ -911,7 +913,7 @@ fn parse_trace_report_options(command: &[String]) -> Result<TraceReportOptions>
}
"--server-log" => {
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);
index += 2;
@@ -929,7 +931,7 @@ fn parse_trace_report_options(command: &[String]) -> Result<TraceReportOptions>
}
"--tail" => {
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)?;
index += 2;
@@ -941,25 +943,35 @@ fn parse_trace_report_options(command: &[String]) -> Result<TraceReportOptions>
tail = parse_trace_tail(value)?;
index += 1;
}
value => {
value if value.starts_with('-') => {
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 {
host,
client_log,
server_log,
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") {
None
} 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<()> {
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)?;
if let Some(parent) = options.client_log.parent() {
@@ -1042,7 +1055,11 @@ fn run_trace_command(args: &Args) -> Result<()> {
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 client_log = match options.client_log {
Some(path) => Some(path),
@@ -1061,6 +1078,20 @@ fn run_trace_report_command(command: &[String]) -> Result<()> {
);
}
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) {
Ok(report) => print_trace_report("server", &report),
Err(err)
@@ -1076,6 +1107,66 @@ fn run_trace_report_command(command: &[String]) -> Result<()> {
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 {
dirs::cache_dir()
.unwrap_or_else(|| expand_tilde("~/.cache"))
@@ -10324,27 +10415,30 @@ mod tests {
#[test]
fn trace_report_options_parse_defaults_and_overrides() {
let parsed = parse_trace_report_options(&[]).unwrap();
assert_eq!(parsed.host, None);
assert_eq!(parsed.client_log, None);
assert_eq!(
parsed.server_log,
Some(std::path::PathBuf::from("/tmp/dosh-server.log"))
);
assert_eq!(parsed.server_log, Some("/tmp/dosh-server.log".to_string()));
assert_eq!(parsed.tail, 20);
let parsed = parse_trace_report_options(&[
"palav".to_string(),
"--client-log=/tmp/client.log".to_string(),
"--server-log".to_string(),
"none".to_string(),
"/tmp/server.log".to_string(),
"--tail".to_string(),
"3".to_string(),
])
.unwrap();
assert_eq!(parsed.host.as_deref(), Some("palav"));
assert_eq!(
parsed.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);
let parsed = parse_trace_report_options(&["--server-log=none".to_string()]).unwrap();
assert_eq!(parsed.server_log, None);
}
#[test]