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
- Comunicacao via socket Unix: Implemente a comunicacao diretamente via socket Unix (
/var/run/docker.sock) usando a cratehypercom conector Unix, eliminando a necessidade de habilitar a API TCP. - Dashboard em tempo real: Crie uma interface TUI com
ratatuique atualiza as estatisticas de todos os containers em tempo real, com graficos de uso de CPU e memoria. - Gerenciamento de containers: Adicione subcomandos
parar,iniciareremoverque enviam os comandos correspondentes para a API Docker. - 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.
- Comparacao de imagens: Adicione um comando que lista todas as imagens locais com seus tamanhos e identifica imagens nao utilizadas por nenhum container.