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 mz_ore::retry::{self, RetryResult};
24use tracing::{error, info};
25
26use crate::utils::format_base_path;
27use crate::{ContainerDumper, Context};
28
29static DOCKER_RESOURCE_DUMP_TIMEOUT: Duration = Duration::from_secs(30);
30
31pub struct DockerDumper {
32    container_id: String,
33    directory_path: PathBuf,
34}
35
36impl DockerDumper {
37    pub fn new(context: &Context, container_id: String) -> Self {
38        Self {
39            directory_path: format_base_path(context.start_time)
40                .join("docker")
41                .join(&container_id),
42            container_id,
43        }
44    }
45
46    /// Execute a Docker command and return (stdout, stderr).
47    async fn execute_docker_command(
48        &self,
49        args: &[String],
50    ) -> Result<(Vec<u8>, Vec<u8>), anyhow::Error> {
51        retry::Retry::default()
52            .max_duration(DOCKER_RESOURCE_DUMP_TIMEOUT)
53            .retry_async(|_| {
54                let args = args.to_vec();
55                async move {
56                    let output = tokio::process::Command::new("docker")
57                        .args(&args)
58                        .output()
59                        .await;
60
61                    match output {
62                        Ok(output) if output.status.success() => {
63                            RetryResult::Ok((output.stdout, output.stderr))
64                        }
65                        Ok(output) => {
66                            let err_msg = format!(
67                                "Docker command failed: {:#}. Retrying...",
68                                String::from_utf8_lossy(&output.stderr)
69                            );
70                            error!("{}", err_msg);
71                            RetryResult::RetryableErr(anyhow::anyhow!(err_msg))
72                        }
73                        Err(err) => {
74                            let err_msg = format!("Failed to execute Docker command: {:#}", err);
75                            error!("{}", err_msg);
76                            RetryResult::RetryableErr(anyhow::anyhow!(err_msg))
77                        }
78                    }
79                }
80            })
81            .await
82    }
83
84    async fn dump_logs(&self) -> Result<(), anyhow::Error> {
85        let (stdout, stderr) = self
86            .execute_docker_command(&["logs".to_string(), self.container_id.to_string()])
87            .await?;
88
89        write_output(stdout, &self.directory_path, "logs-stdout.txt")?;
90        write_output(stderr, &self.directory_path, "logs-stderr.txt")?;
91
92        Ok(())
93    }
94
95    async fn dump_inspect(&self) -> Result<(), anyhow::Error> {
96        let (stdout, _) = self
97            .execute_docker_command(&["inspect".to_string(), self.container_id.to_string()])
98            .await?;
99
100        write_output(stdout, &self.directory_path, "inspect.txt")?;
101
102        Ok(())
103    }
104
105    async fn dump_stats(&self) -> Result<(), anyhow::Error> {
106        let (stdout, _) = self
107            .execute_docker_command(&[
108                "stats".to_string(),
109                "--no-stream".to_string(),
110                self.container_id.to_string(),
111            ])
112            .await?;
113
114        write_output(stdout, &self.directory_path, "stats.txt")?;
115
116        Ok(())
117    }
118
119    async fn dump_top(&self) -> Result<(), anyhow::Error> {
120        let (stdout, _) = self
121            .execute_docker_command(&["top".to_string(), self.container_id.to_string()])
122            .await?;
123
124        write_output(stdout, &self.directory_path, "top.txt")?;
125
126        Ok(())
127    }
128}
129
130impl ContainerDumper for DockerDumper {
131    async fn dump_container_resources(&self) {
132        let _ = self.dump_logs().await;
133        let _ = self.dump_inspect().await;
134        let _ = self.dump_stats().await;
135        let _ = self.dump_top().await;
136    }
137}
138
139/// Helper closure to write output to file
140fn write_output(
141    output: Vec<u8>,
142    directory_path: &PathBuf,
143    file_name: &str,
144) -> Result<(), anyhow::Error> {
145    create_dir_all(&directory_path)?;
146    let file_path = directory_path.join(file_name);
147    let mut file = File::create(&file_path)?;
148    file.write_all(&output)?;
149    info!("Exported {}", file_path.display());
150    Ok(())
151}