Rust

Rust em Produção — Observabilidade, Métricas e Operação de Sistemas Rust Já leu

17 min de leitura

Rust em Produção — Observabilidade, Métricas e Operação de Sistemas Rust
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 sist

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

  • tracing documentation — https://docs.rs/tracing
  • tracing-subscriber book — https://docs.rs/tracing-subscriber
  • metrics crate — 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
  • config crate — 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


Comentários

Mais em Rust

Variáveis, Tipos e a Arte da Imutabilidade
Variáveis, Tipos e a Arte da Imutabilidade

Em um artigo anterior, instalamos o Rust e entendemos por que ele existe. Hoj...

Testes Avançados — Propriedades, Mocks e Benchmarks
Testes Avançados — Propriedades, Mocks e Benchmarks

No Artigo #16 cobrimos os fundamentos de testes em Rust — testes unitários, d...

Contribuindo com o Ecossistema Rust — Open Source, RFCs e a Comunidade
Contribuindo com o Ecossistema Rust — Open Source, RFCs e a Comunidade

Rust — Artigo #49 Contribuindo com o Ecossistema Rust — Open Source, RFCs e a...