mz_debug/
docker_dumper.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Dumps Docker resources to files.
17
18use 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    /// Execute a Docker command and return (stdout, stderr).
46    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
138/// Helper closure to write output to file
139fn 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
152/// Gets the IP address of a Docker container using the container ID.
153pub 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}