diff --git a/Cargo.lock b/Cargo.lock index 1aa910a..ac38b49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,7 +436,7 @@ dependencies = [ [[package]] name = "dosh" -version = "1.0.0-rc33" +version = "1.0.0-rc34" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index a9b1bc3..3d58292 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dosh" -version = "1.0.0-rc33" +version = "1.0.0-rc34" edition = "2024" license = "MIT" diff --git a/README.md b/README.md index 44d35f5..8e4b5f9 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index 2cd915b..b6d844a 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -792,8 +792,9 @@ struct TraceOptions { #[derive(Debug, Clone, PartialEq, Eq)] struct TraceReportOptions { + host: Option, client_log: Option, - server_log: Option, + server_log: Option, tail: usize, } @@ -888,15 +889,16 @@ fn parse_trace_options(command: &[String]) -> Result { } fn parse_trace_report_options(command: &[String]) -> Result { + let mut host: Option = None; let mut client_log: Option = None; - let mut server_log: Option = Some(PathBuf::from("/tmp/dosh-server.log")); + let mut server_log: Option = 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 } "--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 } "--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 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 { +fn parse_optional_trace_path(value: &str) -> Option { 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 { + 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]