Monitor de Containers Docker

Construa um monitor de containers Docker em Rust que exibe status, uso de recursos e logs via API Docker com reqwest.

Docker se tornou a ferramenta padrao para empacotar e executar aplicacoes. Monitorar os containers em execucao – seus recursos, status e logs – e essencial para operacoes de producao. Neste projeto, vamos construir um monitor de containers Docker que se comunica com a API do Docker via HTTP, exibe informacoes de containers em execucao e mostra estatisticas de uso de CPU e memoria.

Este projeto ensina como consumir APIs REST, trabalhar com JSON complexo e construir ferramentas de monitoramento praticas.

O Que Vamos Construir

  • Listagem de containers em execucao com status e portas
  • Exibicao de uso de CPU e memoria por container
  • Visualizacao de logs recentes de um container
  • Inspecao detalhada de um container especifico
  • Interface de terminal formatada e colorida
  • Comunicacao com a API Docker via HTTP

Estrutura do Projeto

container-stats/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── docker_api.rs
    └── formatador.rs

Configurando o Projeto

cargo new container-stats
cd container-stats

Edite o Cargo.toml:

[package]
name = "container-stats"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = { version = "0.12", features = ["json", "blocking"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
colored = "2.1"
clap = { version = "4.5", features = ["derive"] }

Passo 1: Cliente da API Docker

O modulo docker_api encapsula a comunicacao com o daemon Docker. Crie src/docker_api.rs:

use serde::Deserialize;
use std::collections::HashMap;

/// Informacoes resumidas de um container (da listagem).
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ContainerResumo {
    pub id: String,
    pub names: Vec<String>,
    pub image: String,
    pub state: String,
    pub status: String,
    pub ports: Vec<PortaContainer>,
    pub created: i64,
}

/// Mapeamento de porta de um container.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct PortaContainer {
    #[serde(rename = "IP")]
    pub ip: Option<String>,
    #[serde(rename = "PrivatePort")]
    pub private_port: u16,
    #[serde(rename = "PublicPort")]
    pub public_port: Option<u16>,
    #[serde(rename = "Type")]
    pub tipo: String,
}

/// Estatisticas de uso de recursos de um container.
#[derive(Debug, Deserialize)]
pub struct EstatisticasContainer {
    pub cpu_stats: CpuStats,
    pub precpu_stats: CpuStats,
    pub memory_stats: MemoryStats,
}

#[derive(Debug, Deserialize)]
pub struct CpuStats {
    pub cpu_usage: CpuUsage,
    pub system_cpu_usage: Option<u64>,
    pub online_cpus: Option<u32>,
}

#[derive(Debug, Deserialize)]
pub struct CpuUsage {
    pub total_usage: u64,
}

#[derive(Debug, Deserialize)]
pub struct MemoryStats {
    pub usage: Option<u64>,
    pub limit: Option<u64>,
    pub stats: Option<HashMap<String, u64>>,
}

/// Detalhes completos de um container (inspect).
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct DetalhesContainer {
    pub id: String,
    pub name: String,
    pub state: EstadoContainer,
    pub config: ConfigContainer,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct EstadoContainer {
    pub status: String,
    pub running: bool,
    pub pid: u64,
    pub started_at: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ConfigContainer {
    pub image: String,
    pub env: Option<Vec<String>>,
    pub cmd: Option<Vec<String>>,
}

/// Cliente para a API Docker.
pub struct ClienteDocker {
    url_base: String,
    cliente: reqwest::blocking::Client,
}

impl ClienteDocker {
    /// Cria um novo cliente Docker.
    /// Por padrao, usa a API TCP em localhost:2375.
    /// Para usar o socket Unix, configure DOCKER_HOST.
    pub fn new(url_base: Option<String>) -> Self {
        let url = url_base.unwrap_or_else(|| "http://localhost:2375".to_string());
        ClienteDocker {
            url_base: url,
            cliente: reqwest::blocking::Client::new(),
        }
    }

    /// Lista todos os containers (incluindo parados se all=true).
    pub fn listar_containers(
        &self,
        todos: bool,
    ) -> Result<Vec<ContainerResumo>, Box<dyn std::error::Error>> {
        let url = format!("{}/containers/json?all={}", self.url_base, todos);
        let resposta = self.cliente.get(&url).send()?;

        if !resposta.status().is_success() {
            return Err(format!(
                "API retornou status {}: {}",
                resposta.status(),
                resposta.text().unwrap_or_default()
            )
            .into());
        }

        let containers: Vec<ContainerResumo> = resposta.json()?;
        Ok(containers)
    }

    /// Obtem estatisticas de uso de recursos de um container.
    pub fn obter_estatisticas(
        &self,
        container_id: &str,
    ) -> Result<EstatisticasContainer, Box<dyn std::error::Error>> {
        let url = format!(
            "{}/containers/{}/stats?stream=false",
            self.url_base, container_id
        );
        let resposta = self.cliente.get(&url).send()?;
        let stats: EstatisticasContainer = resposta.json()?;
        Ok(stats)
    }

    /// Obtem detalhes completos de um container.
    pub fn inspecionar(
        &self,
        container_id: &str,
    ) -> Result<DetalhesContainer, Box<dyn std::error::Error>> {
        let url = format!(
            "{}/containers/{}/json",
            self.url_base, container_id
        );
        let resposta = self.cliente.get(&url).send()?;
        let detalhes: DetalhesContainer = resposta.json()?;
        Ok(detalhes)
    }

    /// Obtem as ultimas N linhas de log de um container.
    pub fn obter_logs(
        &self,
        container_id: &str,
        linhas: u32,
    ) -> Result<String, Box<dyn std::error::Error>> {
        let url = format!(
            "{}/containers/{}/logs?stdout=true&stderr=true&tail={}",
            self.url_base, container_id, linhas
        );
        let resposta = self.cliente.get(&url).send()?;
        let texto = resposta.text()?;

        // Remover bytes de cabecalho do stream do Docker (8 bytes por linha)
        let logs_limpos: String = texto
            .lines()
            .map(|linha| {
                if linha.len() > 8 {
                    &linha[8..]
                } else {
                    linha
                }
            })
            .collect::<Vec<&str>>()
            .join("\n");

        Ok(logs_limpos)
    }
}

/// Calcula a porcentagem de uso de CPU a partir das estatisticas.
pub fn calcular_uso_cpu(stats: &EstatisticasContainer) -> f64 {
    let delta_cpu = stats.cpu_stats.cpu_usage.total_usage as f64
        - stats.precpu_stats.cpu_usage.total_usage as f64;

    let delta_sistema = match (
        stats.cpu_stats.system_cpu_usage,
        stats.precpu_stats.system_cpu_usage,
    ) {
        (Some(atual), Some(anterior)) => atual as f64 - anterior as f64,
        _ => return 0.0,
    };

    let num_cpus = stats.cpu_stats.online_cpus.unwrap_or(1) as f64;

    if delta_sistema > 0.0 {
        (delta_cpu / delta_sistema) * num_cpus * 100.0
    } else {
        0.0
    }
}

O cliente Docker se comunica via HTTP REST com o daemon Docker. Quando configurado com API TCP (DOCKER_HOST=tcp://localhost:2375), a comunicacao e direta via HTTP. O calculo de CPU compara as estatisticas atuais com as anteriores para obter a porcentagem de uso.

Passo 2: Formatador de Exibicao

O formatador organiza as informacoes para apresentacao no terminal. Crie src/formatador.rs:

use crate::docker_api::{self, ContainerResumo, EstatisticasContainer};
use colored::*;

/// Exibe a lista de containers formatada.
pub fn exibir_lista_containers(containers: &[ContainerResumo]) {
    println!("{}", "=== Monitor de Containers Docker ===".green().bold());
    println!(
        "Containers encontrados: {}\n",
        containers.len().to_string().cyan()
    );

    println!(
        "{:<14} {:<25} {:<20} {:<12} {}",
        "CONTAINER ID".white().bold(),
        "NOME".white().bold(),
        "IMAGEM".white().bold(),
        "ESTADO".white().bold(),
        "PORTAS".white().bold(),
    );
    println!("{}", "-".repeat(85).dimmed());

    for container in containers {
        let id_curto = &container.id[..12.min(container.id.len())];
        let nome = container
            .names
            .first()
            .map(|n| n.trim_start_matches('/').to_string())
            .unwrap_or_else(|| "sem-nome".to_string());

        let nome_exibir = if nome.len() > 23 {
            format!("{}...", &nome[..20])
        } else {
            nome
        };

        let imagem_exibir = if container.image.len() > 18 {
            format!("{}...", &container.image[..15])
        } else {
            container.image.clone()
        };

        let estado_colorido = match container.state.as_str() {
            "running" => "rodando".green().to_string(),
            "exited" => "parado".red().to_string(),
            "paused" => "pausado".yellow().to_string(),
            outro => outro.dimmed().to_string(),
        };

        let portas: String = container
            .ports
            .iter()
            .filter_map(|p| {
                p.public_port
                    .map(|pub_port| format!("{}:{}", pub_port, p.private_port))
            })
            .collect::<Vec<String>>()
            .join(", ");

        println!(
            "{:<14} {:<25} {:<20} {:<12} {}",
            id_curto.cyan(),
            nome_exibir,
            imagem_exibir.dimmed(),
            estado_colorido,
            portas.yellow(),
        );
    }
    println!();
}

/// Exibe estatisticas de um container.
pub fn exibir_estatisticas(container_id: &str, stats: &EstatisticasContainer) {
    let uso_cpu = docker_api::calcular_uso_cpu(stats);
    let memoria_usada = stats.memory_stats.usage.unwrap_or(0) as f64 / 1_048_576.0;
    let memoria_limite = stats.memory_stats.limit.unwrap_or(0) as f64 / 1_048_576.0;
    let porcentagem_mem = if memoria_limite > 0.0 {
        (memoria_usada / memoria_limite) * 100.0
    } else {
        0.0
    };

    println!("  Container: {}", container_id[..12.min(container_id.len())].cyan());
    println!("  CPU:       {:.2}%", formatar_valor_colorido(uso_cpu, 50.0, 80.0));
    println!(
        "  Memoria:   {:.1} MB / {:.1} MB ({:.1}%)",
        memoria_usada,
        memoria_limite,
        porcentagem_mem
    );
    println!();
}

/// Formata um valor numerico com cor baseada em limites.
fn formatar_valor_colorido(valor: f64, limite_amarelo: f64, limite_vermelho: f64) -> String {
    let texto = format!("{:.2}", valor);
    if valor > limite_vermelho {
        texto.red().bold().to_string()
    } else if valor > limite_amarelo {
        texto.yellow().to_string()
    } else {
        texto.green().to_string()
    }
}

Passo 3: Juntando Tudo no main.rs

Crie src/main.rs:

mod docker_api;
mod formatador;

use clap::{Parser, Subcommand};
use colored::*;
use docker_api::ClienteDocker;

/// Monitor de Containers Docker
#[derive(Parser)]
#[command(name = "container-stats")]
#[command(about = "Monitora containers Docker: listagem, estatisticas e logs")]
struct Argumentos {
    /// URL da API Docker (padrao: http://localhost:2375)
    #[arg(short, long, env = "DOCKER_HOST")]
    url: Option<String>,

    #[command(subcommand)]
    comando: Comandos,
}

#[derive(Subcommand)]
enum Comandos {
    /// Listar containers
    Listar {
        /// Incluir containers parados
        #[arg(short, long)]
        todos: bool,
    },
    /// Exibir estatisticas de uso de recursos
    Stats {
        /// ID ou nome do container (exibe todos se omitido)
        container: Option<String>,
    },
    /// Exibir logs de um container
    Logs {
        /// ID ou nome do container
        container: String,
        /// Numero de linhas para exibir
        #[arg(short, long, default_value = "50")]
        linhas: u32,
    },
    /// Inspecionar detalhes de um container
    Inspecionar {
        /// ID ou nome do container
        container: String,
    },
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Argumentos::parse();
    let cliente = ClienteDocker::new(args.url);

    match args.comando {
        Comandos::Listar { todos } => {
            let containers = cliente.listar_containers(todos)?;
            formatador::exibir_lista_containers(&containers);
        }

        Comandos::Stats { container } => {
            match container {
                Some(id) => {
                    // Estatisticas de um container especifico
                    println!("{}", "=== Estatisticas do Container ===".green().bold());
                    match cliente.obter_estatisticas(&id) {
                        Ok(stats) => formatador::exibir_estatisticas(&id, &stats),
                        Err(e) => eprintln!("{} {}", "[ERRO]".red().bold(), e),
                    }
                }
                None => {
                    // Estatisticas de todos os containers em execucao
                    println!(
                        "{}",
                        "=== Estatisticas de Todos os Containers ===".green().bold()
                    );
                    let containers = cliente.listar_containers(false)?;
                    println!();
                    for container in &containers {
                        match cliente.obter_estatisticas(&container.id) {
                            Ok(stats) => {
                                let nome = container
                                    .names
                                    .first()
                                    .map(|n| n.trim_start_matches('/').to_string())
                                    .unwrap_or_else(|| container.id[..12].to_string());
                                println!("  {} ({})", nome.cyan().bold(), container.image.dimmed());
                                formatador::exibir_estatisticas(&container.id, &stats);
                            }
                            Err(e) => {
                                eprintln!(
                                    "  Erro ao obter stats de {}: {}",
                                    &container.id[..12],
                                    e
                                );
                            }
                        }
                    }
                }
            }
        }

        Comandos::Logs { container, linhas } => {
            println!("{}", "=== Logs do Container ===".green().bold());
            println!(
                "Container: {} | Ultimas {} linhas\n",
                container.cyan(),
                linhas
            );
            match cliente.obter_logs(&container, linhas) {
                Ok(logs) => {
                    for linha in logs.lines() {
                        println!("  {}", linha);
                    }
                }
                Err(e) => eprintln!("{} {}", "[ERRO]".red().bold(), e),
            }
        }

        Comandos::Inspecionar { container } => {
            println!("{}", "=== Inspecao do Container ===".green().bold());
            match cliente.inspecionar(&container) {
                Ok(detalhes) => {
                    let nome = detalhes.name.trim_start_matches('/');
                    println!("Nome:      {}", nome.cyan());
                    println!("ID:        {}", &detalhes.id[..12]);
                    println!("Imagem:    {}", detalhes.config.image);
                    println!("Estado:    {}", detalhes.state.status);
                    println!("Rodando:   {}", detalhes.state.running);
                    println!("PID:       {}", detalhes.state.pid);
                    println!("Iniciado:  {}", detalhes.state.started_at);

                    if let Some(cmd) = &detalhes.config.cmd {
                        println!("Comando:   {}", cmd.join(" "));
                    }

                    if let Some(env_vars) = &detalhes.config.env {
                        println!("\nVariaveis de ambiente:");
                        for var in env_vars.iter().take(10) {
                            println!("  {}", var.dimmed());
                        }
                        if env_vars.len() > 10 {
                            println!("  ... e mais {} variaveis", env_vars.len() - 10);
                        }
                    }
                }
                Err(e) => eprintln!("{} {}", "[ERRO]".red().bold(), e),
            }
        }
    }

    Ok(())
}

O programa organiza as funcionalidades em subcomandos claros: listar, stats, logs e inspecionar. Cada subcomando acessa um endpoint diferente da API Docker e formata o resultado para exibicao.

Como Executar

Primeiro, certifique-se de que o Docker esta configurado para aceitar conexoes TCP:

# Habilitar API TCP do Docker (adicionar ao daemon.json)
# /etc/docker/daemon.json:
# { "hosts": ["unix:///var/run/docker.sock", "tcp://127.0.0.1:2375"] }

# Ou exportar a variavel de ambiente
export DOCKER_HOST=http://localhost:2375

# Compilar o projeto
cargo build --release

# Listar containers em execucao
cargo run -- listar

# Listar todos os containers (incluindo parados)
cargo run -- listar --todos

# Ver estatisticas de todos os containers
cargo run -- stats

# Ver estatisticas de um container especifico
cargo run -- stats meu_container

# Ver logs de um container (ultimas 20 linhas)
cargo run -- logs meu_container --linhas 20

# Inspecionar um container
cargo run -- inspecionar meu_container

Saida esperada do comando listar:

=== Monitor de Containers Docker ===
Containers encontrados: 3

CONTAINER ID   NOME                      IMAGEM               ESTADO       PORTAS
-------------------------------------------------------------------------------------
a1b2c3d4e5f6   webapp-api                node:18-alpine       rodando      3000:3000
f6e5d4c3b2a1   postgres-db               postgres:15          rodando      5432:5432
1a2b3c4d5e6f   redis-cache               redis:7-alpine       rodando      6379:6379

Desafios para Expandir

  1. Comunicacao via socket Unix: Implemente a comunicacao diretamente via socket Unix (/var/run/docker.sock) usando a crate hyper com conector Unix, eliminando a necessidade de habilitar a API TCP.
  2. Dashboard em tempo real: Crie uma interface TUI com ratatui que atualiza as estatisticas de todos os containers em tempo real, com graficos de uso de CPU e memoria.
  3. Gerenciamento de containers: Adicione subcomandos parar, iniciar e remover que enviam os comandos correspondentes para a API Docker.
  4. Alertas de recursos: Implemente um modo de monitoramento continuo que verifica os limites de CPU/memoria e envia alertas quando um container exceder thresholds configurados.
  5. Comparacao de imagens: Adicione um comando que lista todas as imagens locais com seus tamanhos e identifica imagens nao utilizadas por nenhum container.

Veja Tambem