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

Async/Await — Concorrência sem Threads com Tokio
Async/Await — Concorrência sem Threads com Tokio

No artigo anterior exploramos concorrência com threads do sistema operacional...

Construindo uma API REST com Axum
Construindo uma API REST com Axum

Chegamos ao primeiro artigo verdadeiramente orientado a projeto. Até aqui con...

Structs — Criando seus próprios tipos de dados
Structs — Criando seus próprios tipos de dados

&nbsp; At&eacute; agora trabalhamos com tipos primitivos &mdash; inteiros, s...