mz_debug/
docker_dumper.rs1use std::fs::{File, create_dir_all};
19use std::io::Write;
20use std::path::PathBuf;
21use std::time::Duration;
22
23use anyhow::Context as AnyhowContext;
24use mz_ore::retry::{self, RetryResult};
25use tracing::{info, warn};
26
27use crate::{ContainerDumper, Context};
28
29static DOCKER_DUMP_DIR: &str = "docker";
30static DOCKER_RESOURCE_DUMP_TIMEOUT: Duration = Duration::from_secs(30);
31
32pub struct DockerDumper {
33 container_id: String,
34 directory_path: PathBuf,
35}
36
37impl DockerDumper {
38 pub fn new(context: &Context, container_id: String) -> Self {
39 Self {
40 directory_path: context.base_path.join(DOCKER_DUMP_DIR).join(&container_id),
41 container_id,
42 }
43 }
44
45 async fn execute_docker_command(
47 &self,
48 args: &[String],
49 ) -> Result<(Vec<u8>, Vec<u8>), anyhow::Error> {
50 retry::Retry::default()
51 .max_duration(DOCKER_RESOURCE_DUMP_TIMEOUT)
52 .retry_async(|_| {
53 let args = args.to_vec();
54 async move {
55 let output = tokio::process::Command::new("docker")
56 .args(&args)
57 .output()
58 .await;
59
60 match output {
61 Ok(output) if output.status.success() => {
62 RetryResult::Ok((output.stdout, output.stderr))
63 }
64 Ok(output) => {
65 let err_msg = format!(
66 "Docker command failed: {:#}. Retrying...",
67 String::from_utf8_lossy(&output.stderr)
68 );
69 warn!("{}", err_msg);
70 RetryResult::RetryableErr(anyhow::anyhow!(err_msg))
71 }
72 Err(err) => {
73 let err_msg = format!("Failed to execute Docker command: {:#}", err);
74 warn!("{}", err_msg);
75 RetryResult::RetryableErr(anyhow::anyhow!(err_msg))
76 }
77 }
78 }
79 })
80 .await
81 }
82
83 async fn dump_logs(&self) -> Result<(), anyhow::Error> {
84 let (stdout, stderr) = self
85 .execute_docker_command(&["logs".to_string(), self.container_id.to_string()])
86 .await?;
87
88 write_output(stdout, &self.directory_path, "logs-stdout.txt")?;
89 write_output(stderr, &self.directory_path, "logs-stderr.txt")?;
90
91 Ok(())
92 }
93
94 async fn dump_inspect(&self) -> Result<(), anyhow::Error> {
95 let (stdout, _) = self
96 .execute_docker_command(&["inspect".to_string(), self.container_id.to_string()])
97 .await?;
98
99 write_output(stdout, &self.directory_path, "inspect.txt")?;
100
101 Ok(())
102 }
103
104 async fn dump_stats(&self) -> Result<(), anyhow::Error> {
105 let (stdout, _) = self
106 .execute_docker_command(&[
107 "stats".to_string(),
108 "--no-stream".to_string(),
109 self.container_id.to_string(),
110 ])
111 .await?;
112
113 write_output(stdout, &self.directory_path, "stats.txt")?;
114
115 Ok(())
116 }
117
118 async fn dump_top(&self) -> Result<(), anyhow::Error> {
119 let (stdout, _) = self
120 .execute_docker_command(&["top".to_string(), self.container_id.to_string()])
121 .await?;
122
123 write_output(stdout, &self.directory_path, "top.txt")?;
124
125 Ok(())
126 }
127}
128
129impl ContainerDumper for DockerDumper {
130 async fn dump_container_resources(&self) {
131 let _ = self.dump_logs().await;
132 let _ = self.dump_inspect().await;
133 let _ = self.dump_stats().await;
134 let _ = self.dump_top().await;
135 }
136}
137
138fn write_output(
140 output: Vec<u8>,
141 directory_path: &PathBuf,
142 file_name: &str,
143) -> Result<(), anyhow::Error> {
144 create_dir_all(&directory_path)?;
145 let file_path = directory_path.join(file_name);
146 let mut file = File::create(&file_path)?;
147 file.write_all(&output)?;
148 info!("Exported {}", file_path.display());
149 Ok(())
150}
151
152pub async fn get_container_ip(container_id: &str) -> Result<String, anyhow::Error> {
154 let output = tokio::process::Command::new("docker")
155 .args([
156 "inspect",
157 "-f",
158 "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}",
159 container_id,
160 ])
161 .output()
162 .await
163 .with_context(|| format!("Failed to get container IP address for {}", container_id))?;
164
165 if !output.status.success() {
166 return Err(anyhow::anyhow!(
167 "Docker command failed: {}",
168 String::from_utf8_lossy(&output.stderr)
169 ));
170 }
171
172 let ip = String::from_utf8(output.stdout)
173 .with_context(|| "Failed to convert container IP address to string")?
174 .trim()
175 .to_string();
176 if ip.is_empty() {
177 return Err(anyhow::anyhow!("Container IP address not found"));
178 }
179
180 Ok(ip)
181}