Rust — Artigo #48
Rust em Produção — Observabilidade, Métricas e Operação de Sistemas Rust
Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano
Um sistema que funciona no seu laptop mas você não consegue diagnosticar em produção não está pronto para produção. Observabilidade — a capacidade de entender o estado interno de um sistema a partir de suas saídas externas — é tão importante quanto a funcionalidade em si. Logs sem estrutura são difíceis de consultar. Métricas sem contexto são difíceis de interpretar. Traces sem correlação são difíceis de seguir.
O ecossistema Rust para observabilidade cresceu dramaticamente nos últimos anos. A crate tracing unificou logs estruturados e traces distribuídos. metrics padronizou a coleta de métricas. OpenTelemetry ganhou suporte nativo. Este artigo monta um sistema completo de observabilidade e cobre as práticas operacionais que separam protótipos de sistemas de produção.
Os três pilares da observabilidade
// LOGS → O que aconteceu (eventos discretos com contexto)
// MÉTRICAS → O quanto aconteceu (números agregados ao longo do tempo)
// TRACES → Como aconteceu (fluxo causal entre componentes)
// Os três juntos respondem perguntas que nenhum responde sozinho:
//
// "O serviço está lento" (métrica: latência P99 subiu)
// → "Quais requests estão lentos?" (trace: span de DB demorando)
// → "Por quê o DB está lento?" (log: "slow query: SELECT ...")
Dependências
[dependencies]
# Tracing — logs estruturados e spans
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = [
"env-filter", "json", "fmt", "registry"
]}
tracing-opentelemetry = "0.23"
# OpenTelemetry — traces distribuídos
opentelemetry = { version = "0.22", features = ["trace"] }
opentelemetry_sdk = { version = "0.22", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.15", features = [
"tonic", "trace", "metrics"
]}
# Métricas
metrics = "0.22"
metrics-exporter-prometheus = "0.13"
# HTTP (para health checks e métricas)
axum = "0.7"
tokio = { version = "1", features = ["full"] }
# Utilitários
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
uuid = { version = "1", features = ["v4"] }
chrono = { version = "0.4", features = ["serde"] }
Configurando tracing estruturado
use tracing::{info, warn, error, debug, trace, instrument, Span};
use tracing_subscriber::{
fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry,
};
use std::io;
pub fn configurar_tracing(formato_json: bool, nivel: &str) {
let filtro = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(nivel));
if formato_json {
// Produção: JSON estruturado para ingestão no ELK/Loki
Registry::default()
.with(filtro)
.with(
fmt::layer()
.json()
.with_current_span(true)
.with_span_list(true)
.with_target(true)
.with_thread_ids(true)
.with_writer(io::stdout),
)
.init();
} else {
// Desenvolvimento: formato legível por humanos
Registry::default()
.with(filtro)
.with(
fmt::layer()
.pretty()
.with_target(true)
.with_thread_names(true)
.with_writer(io::stdout),
)
.init();
}
}
// Campos estruturados — não use interpolação de string
fn exemplos_logging() {
// ✗ Evite — difícil de consultar em produção
info!("Usuário 12345 fez login às 14:30");
// ✓ Prefira — campos indexáveis
info!(
usuario_id = 12345,
acao = "login",
ip = "192.168.1.1",
"Autenticação bem-sucedida"
);
// Campos calculados
let duracao_ms = 42u64;
let status_code = 200u16;
info!(
duracao_ms,
status_code,
endpoint = "/api/pedidos",
metodo = "POST",
"Requisição processada"
);
// Erros com contexto completo
let erro = std::io::Error::new(
std::io::ErrorKind::NotFound,
"arquivo não encontrado"
);
error!(
error = %erro,
error.kind = ?erro.kind(),
caminho = "/dados/config.toml",
"Falha ao carregar configuração"
);
// Níveis adequados
trace!("Entrada na função parse_token"); // muito verboso
debug!(token_len = 32, "Token validado");
info!(pedido_id = "PED-001", "Pedido criado");
warn!(tentativas = 3, "Rate limit próximo do limite");
error!(codigo = 503, "Serviço externo indisponível");
}
Instrumentação com spans
use tracing::{instrument, info_span, Instrument};
use std::time::Duration;
// #[instrument] adiciona um span automaticamente
// Captura argumentos como campos (cuidado com dados sensíveis)
#[instrument(
name = "processar_pedido",
skip(db, payload), // omite campos grandes ou sensíveis
fields(
pedido_id = %pedido_id,
usuario_id = tracing::field::Empty, // preenchido depois
)
)]
async fn processar_pedido(
pedido_id: &str,
payload: Vec<u8>,
db: &DatabasePool,
) -> Result<RespostaPedido, ErroPedido> {
// Preenche campo vazio após obtê-lo
let usuario = db.obter_usuario_do_pedido(pedido_id).await?;
Span::current().record("usuario_id", usuario.id);
info!("Iniciando processamento");
let validado = validar_pedido(&payload)
.instrument(info_span!("validar"))
.await?;
let resultado = salvar_pedido(&validado, db)
.instrument(info_span!("salvar_db"))
.await?;
info!(
itens = resultado.itens.len(),
total_centavos = resultado.total_centavos,
"Pedido processado com sucesso"
);
Ok(resultado)
}
#[instrument(skip(db))]
async fn validar_pedido(payload: &[u8]) -> Result<PedidoValidado, ErroPedido> {
debug!(tamanho_bytes = payload.len(), "Validando payload");
// Simula validação
tokio::time::sleep(Duration::from_millis(5)).await;
Ok(PedidoValidado {
itens: vec![],
total_centavos: 10000,
})
}
#[instrument(skip(db))]
async fn salvar_pedido(
pedido: &PedidoValidado,
db: &DatabasePool,
) -> Result<RespostaPedido, ErroPedido> {
debug!("Salvando no banco de dados");
// Simula operação de banco
tokio::time::sleep(Duration::from_millis(20)).await;
Ok(RespostaPedido {
id: uuid::Uuid::new_v4().to_string(),
itens: pedido.itens.clone(),
total_centavos: pedido.total_centavos,
})
}
// Span manual para controle fino
async fn operacao_complexa() {
let span = info_span!(
"operacao_complexa",
fase = "inicio",
tentativa = 1u32,
);
let _guard = span.enter();
info!("Fase 1 concluída");
// Atualiza campo do span
span.record("fase", "processamento");
info!("Fase 2 concluída");
}
// Stubs para compilar
struct DatabasePool;
impl DatabasePool {
async fn obter_usuario_do_pedido(&self, _: &str)
-> Result<Usuario, ErroPedido> {
Ok(Usuario { id: 1 })
}
}
struct Usuario { id: u64 }
#[derive(Debug)]
struct PedidoValidado {
itens: Vec<String>,
total_centavos: u64,
}
#[derive(Debug)]
struct RespostaPedido {
id: String,
itens: Vec<String>,
total_centavos: u64,
}
#[derive(Debug, thiserror::Error)]
enum ErroPedido {
#[error("Pedido inválido: {0}")]
Invalido(String),
#[error("Erro de banco: {0}")]
Banco(String),
}
Métricas com Prometheus
use metrics::{counter, gauge, histogram, describe_counter,
describe_gauge, describe_histogram, Unit};
use metrics_exporter_prometheus::PrometheusBuilder;
use std::time::{Duration, Instant};
use axum::{Router, routing::get, extract::State, response::IntoResponse};
// Registra e descreve métricas no startup
pub fn inicializar_metricas() {
describe_counter!(
"requisicoes_total",
Unit::Count,
"Número total de requisições HTTP recebidas"
);
describe_histogram!(
"requisicao_duracao_segundos",
Unit::Seconds,
"Duração das requisições HTTP em segundos"
);
describe_gauge!(
"conexoes_ativas",
Unit::Count,
"Número atual de conexões ativas"
);
describe_counter!(
"erros_total",
Unit::Count,
"Número total de erros por tipo"
);
describe_gauge!(
"cache_uso_bytes",
Unit::Bytes,
"Uso atual de memória do cache"
);
describe_histogram!(
"db_query_duracao_segundos",
Unit::Seconds,
"Duração das queries ao banco de dados"
);
}
// Middleware de métricas para Axum
pub async fn middleware_metricas<B>(
req: axum::http::Request<B>,
next: axum::middleware::Next<B>,
) -> impl IntoResponse {
let inicio = Instant::now();
let metodo = req.method().to_string();
let caminho = req.uri().path().to_string();
// Normaliza caminhos com IDs para evitar alta cardinalidade
// /usuarios/123 → /usuarios/:id
let caminho_normalizado = normalizar_caminho(&caminho);
let resposta = next.run(req).await;
let duracao = inicio.elapsed().as_secs_f64();
let status = resposta.status().as_u16().to_string();
// Incrementa contador com labels
counter!(
"requisicoes_total",
"metodo" => metodo.clone(),
"caminho" => caminho_normalizado.clone(),
"status" => status.clone(),
).increment(1);
// Registra histograma de latência
histogram!(
"requisicao_duracao_segundos",
"metodo" => metodo,
"caminho" => caminho_normalizado,
"status" => status,
).record(duracao);
resposta
}
fn normalizar_caminho(caminho: &str) -> String {
// /usuarios/123/pedidos/456 → /usuarios/:id/pedidos/:id
caminho
.split('/')
.map(|segmento| {
if segmento.chars().all(|c| c.is_ascii_digit()) {
":id"
} else if segmento.len() == 36
&& segmento.chars().filter(|&c| c == '-').count() == 4
{
":uuid"
} else {
segmento
}
})
.collect::<Vec<_>>()
.join("/")
}
// Métricas customizadas nas funções de negócio
pub struct ServicoUsuarios {
db: DatabasePool,
}
impl ServicoUsuarios {
pub async fn criar_usuario(
&self,
dados: DadosUsuario,
) -> Result<Usuario, ErroUsuario> {
let inicio = Instant::now();
let resultado = self.db.inserir_usuario(&dados).await;
let duracao = inicio.elapsed().as_secs_f64();
histogram!(
"db_query_duracao_segundos",
"operacao" => "inserir_usuario",
"tabela" => "usuarios",
).record(duracao);
match &resultado {
Ok(_) => {
counter!(
"usuarios_criados_total",
"plano" => dados.plano.clone(),
).increment(1);
info!(email = %dados.email, "Usuário criado com sucesso");
}
Err(e) => {
counter!(
"erros_total",
"tipo" => "criacao_usuario",
"causa" => e.categoria(),
).increment(1);
error!(error = %e, email = %dados.email, "Falha ao criar usuário");
}
}
resultado
}
pub fn atualizar_gauge_conexoes(&self, n: f64) {
gauge!("conexoes_ativas").set(n);
}
}
// Stubs
struct DadosUsuario { email: String, plano: String }
#[derive(Debug, thiserror::Error)]
enum ErroUsuario {
#[error("Banco de dados: {0}")]
Banco(String),
}
impl ErroUsuario {
fn categoria(&self) -> &'static str { "banco" }
}
impl DatabasePool {
async fn inserir_usuario(&self, _: &DadosUsuario)
-> Result<Usuario, ErroUsuario> {
Ok(Usuario { id: 1 })
}
}
Health checks e endpoints de operação
use axum::{
Router, routing::get,
extract::State,
response::{Json, IntoResponse},
http::StatusCode,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
enum StatusComponente {
Saudavel,
Degradado,
Inativo,
}
#[derive(Debug, Clone, Serialize)]
struct VerificacaoComponente {
status: StatusComponente,
latencia_ms: Option<u64>,
mensagem: Option<String>,
ultimo_ok: Option<String>,
}
#[derive(Debug, Serialize)]
struct RespostaHealthcheck {
status: StatusComponente,
versao: String,
uptime_segundos: u64,
timestamp: String,
componentes: std::collections::HashMap<String, VerificacaoComponente>,
}
struct EstadoApp {
inicio: Instant,
versao: String,
db: Arc<DatabasePool>,
cache_redis: Arc<ClienteRedis>,
}
async fn health_live(
State(estado): State<Arc<EstadoApp>>,
) -> impl IntoResponse {
// Liveness: o processo está vivo e rodando?
// Deve ser simples e nunca bloquear
(StatusCode::OK, Json(serde_json::json!({
"status": "vivo",
"uptime_segundos": estado.inicio.elapsed().as_secs(),
})))
}
async fn health_ready(
State(estado): State<Arc<EstadoApp>>,
) -> impl IntoResponse {
// Readiness: o serviço está pronto para receber tráfego?
// Verifica dependências críticas
let mut componentes = std::collections::HashMap::new();
let mut status_geral = StatusComponente::Saudavel;
// Verifica banco de dados
let check_db = verificar_banco(&estado.db).await;
if matches!(check_db.status, StatusComponente::Inativo) {
status_geral = StatusComponente::Inativo;
}
componentes.insert("banco_de_dados".to_string(), check_db);
// Verifica Redis
let check_redis = verificar_redis(&estado.cache_redis).await;
if matches!(check_redis.status, StatusComponente::Inativo) {
// Redis inativo pode ser degradado, não inativo total
// (depende da criticidade)
if matches!(status_geral, StatusComponente::Saudavel) {
status_geral = StatusComponente::Degradado;
}
}
componentes.insert("cache_redis".to_string(), check_redis);
let codigo = match status_geral {
StatusComponente::Saudavel => StatusCode::OK,
StatusComponente::Degradado => StatusCode::OK, // ainda aceita tráfego
StatusComponente::Inativo => StatusCode::SERVICE_UNAVAILABLE,
};
let resposta = RespostaHealthcheck {
status: status_geral,
versao: estado.versao.clone(),
uptime_segundos: estado.inicio.elapsed().as_secs(),
timestamp: chrono::Utc::now().to_rfc3339(),
componentes,
};
(codigo, Json(resposta))
}
async fn verificar_banco(db: &DatabasePool) -> VerificacaoComponente {
let inicio = Instant::now();
match tokio::time::timeout(
Duration::from_millis(500),
db.ping(),
).await {
Ok(Ok(())) => VerificacaoComponente {
status: StatusComponente::Saudavel,
latencia_ms: Some(inicio.elapsed().as_millis() as u64),
mensagem: None,
ultimo_ok: Some(chrono::Utc::now().to_rfc3339()),
},
Ok(Err(e)) => VerificacaoComponente {
status: StatusComponente::Inativo,
latencia_ms: Some(inicio.elapsed().as_millis() as u64),
mensagem: Some(e.to_string()),
ultimo_ok: None,
},
Err(_timeout) => VerificacaoComponente {
status: StatusComponente::Inativo,
latencia_ms: Some(500),
mensagem: Some("Timeout após 500ms".to_string()),
ultimo_ok: None,
},
}
}
async fn verificar_redis(cache: &ClienteRedis) -> VerificacaoComponente {
let inicio = Instant::now();
match tokio::time::timeout(
Duration::from_millis(200),
cache.ping(),
).await {
Ok(Ok(())) => VerificacaoComponente {
status: StatusComponente::Saudavel,
latencia_ms: Some(inicio.elapsed().as_millis() as u64),
mensagem: None,
ultimo_ok: Some(chrono::Utc::now().to_rfc3339()),
},
_ => VerificacaoComponente {
status: StatusComponente::Inativo,
latencia_ms: None,
mensagem: Some("Redis indisponível".to_string()),
ultimo_ok: None,
},
}
}
async fn metricas_prometheus() -> impl IntoResponse {
// O exporter Prometheus expõe automaticamente
// Aqui demonstramos o endpoint manual
let handle = metrics_exporter_prometheus::PrometheusBuilder::new()
.build_recorder();
(
[(axum::http::header::CONTENT_TYPE, "text/plain; version=0.0.4")],
handle.render(),
)
}
// Stubs
struct ClienteRedis;
impl ClienteRedis {
async fn ping(&self) -> Result<(), String> { Ok(()) }
}
impl DatabasePool {
async fn ping(&self) -> Result<(), String> { Ok(()) }
}
pub fn criar_router_operacional(estado: Arc<EstadoApp>) -> Router {
Router::new()
.route("/health/live", get(health_live))
.route("/health/ready", get(health_ready))
.route("/metrics", get(metricas_prometheus))
.route("/info", get(info_versao))
.with_state(estado)
}
async fn info_versao(
State(estado): State<Arc<EstadoApp>>,
) -> Json<serde_json::Value> {
Json(serde_json::json!({
"versao": estado.versao,
"rust_versao": env!("CARGO_PKG_RUST_VERSION"),
"compilado_em": env!("CARGO_PKG_VERSION"),
"uptime_segundos": estado.inicio.elapsed().as_secs(),
}))
}
Graceful shutdown
use tokio::signal;
use tokio::sync::broadcast;
use std::sync::Arc;
#[derive(Clone)]
struct SinalShutdown {
tx: broadcast::Sender<()>,
}
impl SinalShutdown {
fn novo() -> (Self, broadcast::Receiver<()>) {
let (tx, rx) = broadcast::channel(1);
(SinalShutdown { tx }, rx)
}
fn disparar(&self) {
let _ = self.tx.send(());
}
fn assinar(&self) -> broadcast::Receiver<()> {
self.tx.subscribe()
}
}
async fn aguardar_sinal_os() {
let ctrl_c = async {
signal::ctrl_c().await.expect("Falha ao instalar handler Ctrl+C");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("Falha ao instalar handler SIGTERM")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => { info!("Recebido Ctrl+C") }
_ = terminate => { info!("Recebido SIGTERM") }
}
}
pub async fn executar_servidor(
router: Router,
porta: u16,
) -> anyhow::Result<()> {
let endereco = format!("0.0.0.0:{porta}");
let listener = tokio::net::TcpListener::bind(&endereco).await?;
info!(porta, endereco = %endereco, "Servidor iniciado");
axum::serve(listener, router)
.with_graceful_shutdown(async {
aguardar_sinal_os().await;
info!("Sinal recebido — iniciando shutdown gracioso");
})
.await?;
info!("Servidor encerrado com sucesso");
Ok(())
}
// Padrão completo: múltiplos componentes com shutdown coordenado
pub async fn executar_aplicacao() -> anyhow::Result<()> {
let (sinal, mut rx_principal) = SinalShutdown::novo();
// Inicia workers em background
let sinal_worker = sinal.clone();
let worker = tokio::spawn(async move {
let mut rx = sinal_worker.assinar();
loop {
tokio::select! {
_ = rx.recv() => {
info!("Worker: recebeu sinal de shutdown");
// Finaliza trabalho em andamento...
tokio::time::sleep(Duration::from_millis(100)).await;
info!("Worker: encerrado");
break;
}
_ = tokio::time::sleep(Duration::from_secs(1)) => {
debug!("Worker: processando...");
}
}
}
});
// Inicia servidor HTTP
let sinal_http = sinal.clone();
let servidor = tokio::spawn(async move {
let estado = Arc::new(EstadoApp {
inicio: Instant::now(),
versao: env!("CARGO_PKG_VERSION").to_string(),
db: Arc::new(DatabasePool),
cache_redis: Arc::new(ClienteRedis),
});
let router = criar_router_operacional(estado)
.layer(axum::middleware::from_fn(middleware_metricas));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
.await.unwrap();
info!("HTTP: escutando em 0.0.0.0:8080");
axum::serve(listener, router)
.with_graceful_shutdown(async move {
let mut rx = sinal_http.assinar();
let _ = rx.recv().await;
info!("HTTP: iniciando shutdown gracioso");
})
.await
.unwrap();
});
// Aguarda sinal do OS
aguardar_sinal_os().await;
info!("Sinalizando todos os componentes para encerrar");
sinal.disparar();
// Aguarda encerramento com timeout
let timeout = Duration::from_secs(30);
match tokio::time::timeout(timeout, async {
let _ = tokio::join!(worker, servidor);
}).await {
Ok(_) => info!("Shutdown gracioso concluído"),
Err(_) => {
error!("Timeout no shutdown gracioso após {}s — forçando encerramento",
timeout.as_secs());
}
}
Ok(())
}
Configuração de ambiente segura
use serde::Deserialize;
use std::time::Duration;
#[derive(Debug, Deserialize)]
pub struct ConfiguracaoApp {
// Servidor
pub porta: u16,
pub workers: usize,
// Banco de dados
pub database_url: String, // nunca logar
pub db_max_conexoes: u32,
pub db_timeout_segundos: u64,
// Cache
pub redis_url: Option<String>, // nunca logar
// Observabilidade
pub log_nivel: String,
pub log_json: bool,
pub otlp_endpoint: Option<String>,
// Segurança
pub jwt_secret: String, // NUNCA logar
// Feature flags
pub habilitar_cache: bool,
pub habilitar_metricas: bool,
}
impl ConfiguracaoApp {
pub fn do_ambiente() -> anyhow::Result<Self> {
// Carrega de variáveis de ambiente com prefixo APP_
let config = config::Config::builder()
.add_source(
config::Environment::with_prefix("APP")
.separator("__")
.try_parsing(true)
)
// Defaults sensatos
.set_default("porta", 8080)?
.set_default("workers", num_cpus::get() as i64)?
.set_default("db_max_conexoes", 10)?
.set_default("db_timeout_segundos", 30)?
.set_default("log_nivel", "info")?
.set_default("log_json", false)?
.set_default("habilitar_cache", true)?
.set_default("habilitar_metricas", true)?
.build()?;
let app: ConfiguracaoApp = config.try_deserialize()?;
app.validar()?;
Ok(app)
}
fn validar(&self) -> anyhow::Result<()> {
anyhow::ensure!(
self.porta > 1024 || self.porta == 80 || self.porta == 443,
"Porta {} requer privilégios de root (use > 1024)", self.porta
);
anyhow::ensure!(
!self.database_url.is_empty(),
"APP__DATABASE_URL é obrigatório"
);
anyhow::ensure!(
self.jwt_secret.len() >= 32,
"APP__JWT_SECRET deve ter pelo menos 32 caracteres"
);
anyhow::ensure!(
self.db_max_conexoes >= 2,
"APP__DB_MAX_CONEXOES deve ser pelo menos 2"
);
Ok(())
}
// Exibe configuração sem valores sensíveis
pub fn exibir_seguro(&self) {
info!(
porta = self.porta,
workers = self.workers,
db_max_conexoes = self.db_max_conexoes,
log_nivel = %self.log_nivel,
log_json = self.log_json,
habilitar_cache = self.habilitar_cache,
habilitar_metricas = self.habilitar_metricas,
// database_url, redis_url, jwt_secret — OMITIDOS
"Configuração carregada"
);
}
}
Perfil de runtime — diagnóstico em produção
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
// Contadores atômicos de alta performance
// Sem lock, sem overhead de sincronização
pub struct ContadoresRuntime {
requisicoes_ativas: AtomicU64,
requisicoes_total: AtomicU64,
erros_total: AtomicU64,
bytes_processados: AtomicU64,
inicio: Instant,
}
impl ContadoresRuntime {
pub fn novo() -> Arc<Self> {
Arc::new(ContadoresRuntime {
requisicoes_ativas: AtomicU64::new(0),
requisicoes_total: AtomicU64::new(0),
erros_total: AtomicU64::new(0),
bytes_processados: AtomicU64::new(0),
inicio: Instant::now(),
})
}
pub fn iniciar_requisicao(&self) {
self.requisicoes_ativas.fetch_add(1, Ordering::Relaxed);
self.requisicoes_total.fetch_add(1, Ordering::Relaxed);
}
pub fn finalizar_requisicao(&self, bytes: u64, erro: bool) {
self.requisicoes_ativas.fetch_sub(1, Ordering::Relaxed);
self.bytes_processados.fetch_add(bytes, Ordering::Relaxed);
if erro {
self.erros_total.fetch_add(1, Ordering::Relaxed);
}
}
pub fn snapshot(&self) -> SnapshotRuntime {
let uptime = self.inicio.elapsed();
let total = self.requisicoes_total.load(Ordering::Relaxed);
let erros = self.erros_total.load(Ordering::Relaxed);
SnapshotRuntime {
uptime_segundos: uptime.as_secs(),
requisicoes_ativas: self.requisicoes_ativas.load(Ordering::Relaxed),
requisicoes_total: total,
erros_total: erros,
taxa_erro: if total > 0 {
erros as f64 / total as f64 * 100.0
} else {
0.0
},
bytes_processados: self.bytes_processados.load(Ordering::Relaxed),
throughput_rps: if uptime.as_secs() > 0 {
total as f64 / uptime.as_secs() as f64
} else {
0.0
},
}
}
}
#[derive(Debug, serde::Serialize)]
pub struct SnapshotRuntime {
pub uptime_segundos: u64,
pub requisicoes_ativas: u64,
pub requisicoes_total: u64,
pub erros_total: u64,
pub taxa_erro: f64,
pub bytes_processados: u64,
pub throughput_rps: f64,
}
// Task de background que loga métricas periodicamente
pub async fn tarefa_metricas_periodicas(
contadores: Arc<ContadoresRuntime>,
intervalo: Duration,
) {
let mut ticker = tokio::time::interval(intervalo);
ticker.set_missed_tick_behavior(
tokio::time::MissedTickBehavior::Skip
);
loop {
ticker.tick().await;
let snap = contadores.snapshot();
info!(
uptime_s = snap.uptime_segundos,
reqs_ativas = snap.requisicoes_ativas,
reqs_total = snap.requisicoes_total,
erros_total = snap.erros_total,
taxa_erro_pct = snap.taxa_erro,
throughput_rps = snap.throughput_rps,
"Métricas de runtime"
);
// Atualiza gauges do Prometheus também
gauge!("requisicoes_ativas")
.set(snap.requisicoes_ativas as f64);
gauge!("throughput_rps")
.set(snap.throughput_rps);
gauge!("taxa_erro_percentual")
.set(snap.taxa_erro);
}
}
Dockerfile otimizado para produção
# Build multi-stage — imagem final mínima
# Estágio 1: dependências (cache-friendly)
FROM rust:1.75-slim AS deps
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
# Cria src/main.rs fake para compilar dependências
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm -rf src
# Estágio 2: compilação do código real
FROM deps AS builder
COPY src ./src
# Força recompilação do código da aplicação
RUN touch src/main.rs
RUN cargo build --release
# Verifica que o binário existe e funciona
RUN ./target/release/minha-app --version
# Estágio 3: imagem final mínima
FROM debian:bookworm-slim AS runtime
WORKDIR /app
# Certificados TLS para HTTPS
RUN apt-get update && apt-get install -y --no-install-recommends
ca-certificates
&& rm -rf /var/lib/apt/lists/*
# Usuário não-root
RUN useradd --system --uid 1001 --no-create-home app
USER app
# Copia apenas o binário
COPY --from=builder /app/target/release/minha-app /app/minha-app
# Metadados
LABEL org.opencontainers.image.source="https://github.com/org/repo"
LABEL org.opencontainers.image.version="1.0.0"
# Health check nativo do Docker
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3
CMD curl -f http://localhost:8080/health/live || exit 1
EXPOSE 8080
ENV APP__LOG_JSON=true
ENV APP__LOG_NIVEL=info
ENV APP__PORTA=8080
ENTRYPOINT ["/app/minha-app"]
Fontes e leituras recomendadas
tracingdocumentation — https://docs.rs/tracingtracing-subscriberbook — https://docs.rs/tracing-subscribermetricscrate — https://docs.rs/metrics- OpenTelemetry Rust — https://opentelemetry.io/docs/languages/rust/
- "Observability Engineering" — Charity Majors et al. — fundamentos de o11y
- "Production Kubernetes" — Josh Rosso et al. — operação em produção
- Axum documentation — https://docs.rs/axum
configcrate — gestão de configuração — https://docs.rs/config- "The Site Reliability Workbook" — Google SRE — práticas de operação
- Prometheus Operator — https://prometheus-operator.dev
Artigo #48 de 52 | Série: Dominando Rust em 1 Ano Próximo → Artigo #49: Contribuindo com o ecossistema Rust — Open source, RFCs e a comunidade