Prometheus e metrics em Rust: Axum em Produção | Rust Brasil

Guia prático de métricas Prometheus em Rust com Axum: counters, histograms, labels seguros, /metrics, SLIs e a escolha entre metrics e prometheus_client.

Para instrumentar uma API Axum com métricas que o Prometheus consegue scrapear, use o crate metrics com metrics-exporter-prometheus, exponha um /metrics protegido e registre counters, gauges e histograms com labels de baixa cardinalidade. Isso responde perguntas de produção que logs sozinhos não resolvem: quantas requisições por segundo, qual a taxa de 5xx, qual o p95 por rota e se o pool do banco está saturando. Tracing e OpenTelemetry continuam essenciais para investigar um caso; métricas sustentam SLIs, alertas e capacidade.

Este guia fecha o espaço entre o artigo amplo de logging e observabilidade e o projeto didático de coletor Prometheus: aqui o foco é o caminho de produção em Rust — crates, Axum, labels seguros, histogramas, erros comuns e checklist.

Resposta rápida: qual ferramenta para cada sinal

Pergunta operacionalSinalFerramenta típica em Rust
A API está saudável agora?métricasmetrics + Prometheus/Grafana
Por que esta requisição falhou?traces/logstracing + OTel
Qual o p95 da rota /pedidos?histogramahistogram!("http_request_duration_seconds")
O pool do Postgres está no limite?gaugesaturação de conexões / espera
Preciso alertar indisponibilidade?SLI/SLOtaxa de sucesso + burn rate
Quero portabilidade entre backendstelemetria padrãoOpenTelemetry Metrics
Quero scrape clássico no clusterexposição HTTP/metrics + ServiceMonitor

Regra prática: métricas para tendência e alerta; traces para causa; logs para contexto. Se você só tem println!, ainda não tem observabilidade de produção.

O que Prometheus espera de um serviço Rust

Prometheus funciona por pull: um scraper consulta um endpoint texto, lê séries e armazena amostras. Seu binário Rust não “empurra” cada contador a cada request para o servidor (salvo Pushgateway em jobs curtos). O contrato é estável:

  1. expor um endpoint HTTP, em geral /metrics;
  2. devolver texto no formato Prometheus/OpenMetrics;
  3. manter nomes estáveis, tipos corretos e labels controlados;
  4. evitar cardinalidade explosiva;
  5. instrumentar o que sustenta decisão operacional, não cada detalhe interno.

Em Rust, duas famílias aparecem com frequência:

  • metrics + metrics-exporter-prometheus: API ergonômica (counter!, histogram!, gauge!) e exporter pronto. Bom padrão para Axum/Tokio.
  • prometheus_client: registry explícito, controle fino do formato OpenMetrics e tipos encoding-first. Útil quando a equipe quer ownership total do modelo de métricas.

Para a maioria dos backends Axum em 2026, comece por metrics. Troque ou combine com prometheus_client só com motivo concreto (formato, restrições de encoding, integração interna já padronizada).

Setup mínimo com Axum

Dependências típicas:

[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
metrics = "0.24"
metrics-exporter-prometheus = "0.16"

Bootstrap do exporter e do servidor:

use axum::{
    extract::State,
    http::{Request, StatusCode},
    middleware::{self, Next},
    response::{IntoResponse, Response},
    routing::get,
    Router,
};
use metrics::{counter, histogram};
use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
use std::time::Instant;
use tower_http::trace::TraceLayer;

#[derive(Clone)]
struct AppState {
    metrics: PrometheusHandle,
}

async fn health() -> &'static str {
    "ok"
}

async fn metrics_handler(State(state): State<AppState>) -> impl IntoResponse {
    state.metrics.render()
}

async fn http_metrics(req: Request<axum::body::Body>, next: Next) -> Response {
    let method = req.method().as_str().to_owned();
    let route = req
        .uri()
        .path()
        .trim_end_matches('/')
        .to_owned();
    let started = Instant::now();

    let response = next.run(req).await;
    let status = response.status().as_u16().to_string();
    let elapsed = started.elapsed().as_secs_f64();

    // Em produção, normalize a rota para templates (/users/:id), não path bruto.
    counter!(
        "http_requests_total",
        "method" => method.clone(),
        "route" => route.clone(),
        "status" => status
    )
    .increment(1);

    histogram!(
        "http_request_duration_seconds",
        "method" => method,
        "route" => route
    )
    .record(elapsed);

    response
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt()
        .with_env_filter("info")
        .init();

    let metrics = PrometheusBuilder::new()
        .install_recorder()
        .expect("falha ao instalar recorder Prometheus");

    let state = AppState { metrics };

    let app = Router::new()
        .route("/health", get(health))
        .route("/metrics", get(metrics_handler))
        .layer(middleware::from_fn(http_metrics))
        .layer(TraceLayer::new_for_http())
        .with_state(state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .expect("bind");
    axum::serve(listener, app)
        .with_graceful_shutdown(shutdown_signal())
        .await
        .expect("serve");
}

async fn shutdown_signal() {
    let _ = tokio::signal::ctrl_c().await;
}

O exemplo acima é didático. Em produção, separe a porta de métricas da porta pública, normalize rotas e evite label com path cru. O tower-http já cobre TraceLayer, timeouts e request ID; métricas entram como sinal agregado ao lado desses middlewares.

Counters, gauges e histograms na prática

Counter

Use para eventos que só aumentam: requisições, erros, timeouts, retries, jobs concluídos.

counter!("payments_authorized_total", "provider" => "stripe").increment(1);
counter!("db_query_errors_total", "operation" => "insert_order").increment(1);

Nunca use counter para valor que sobe e desce. Reinício do processo zera o counter; o Prometheus lida com isso via rate()/increase().

Gauge

Use para nível atual: conexões ativas, tamanho de fila, goroutines equivalentes (tasks), memória estimada de um cache local, tickets disponíveis de um semáforo.

use metrics::gauge;

gauge!("db_pool_connections_in_use").set(12.0);
gauge!("queue_depth", "queue" => "email").set(340.0);

Gauge é o sinal certo para saturação. Se o pool do SQLx passa muito tempo no máximo, o problema aparece aqui antes do timeout virar incidente.

Histogram

Use para distribuição: duração de request, tempo de query, tamanho de payload, lag de consumo. Em SLOs de latência, histograma (ou summary, com trade-offs) é o instrumento central.

histogram!(
    "db_query_duration_seconds",
    "operation" => "select_order"
)
.record(elapsed);

Defina buckets alinhados ao orçamento do produto. Buckets genéricos demais escondem regressões; buckets absurdamente finos aumentam custo. Para APIs web, faixas em torno de 5ms–5s costumam ser um ponto de partida — ajuste com dados reais.

Labels seguros: a diferença entre insight e incidente de custo

Labels transformam uma métrica em várias séries. Isso é poder e risco.

Faça

  • method: GET, POST
  • route: /users/:id, /orders
  • status_class: 2xx, 4xx, 5xx (ou status code se o conjunto for pequeno)
  • dependency: postgres, redis, stripe
  • outcome: success, timeout, error

Não faça

  • user_id, email, cpf, order_uuid
  • path bruto /users/a1b2c3...
  • mensagem de erro completa
  • hostname de pod com cardinalidade alta se não for necessário
  • versionagem fina demais em todo sinal

Uma regra útil: se o valor do label pode crescer sem limite com o tráfego de usuários, ele não pertence à métrica. Coloque esse identificador em tracing pontual, não em série temporal global.

SLIs que um backend Rust deveria expor cedo

Não comece pelo dashboard mais bonito. Comece pelas perguntas de plantão:

  1. Disponibilidade: proporção de respostas não-5xx em janela móvel.
  2. Latência: p95/p99 por rota crítica.
  3. Saturação: pool de banco, tamanho de fila, tasks bloqueadas, uso de memória do processo.
  4. Erros por dependência: Postgres, Redis, HTTP externo, fila.
  5. Trabalho assíncrono: jobs processados, retries, dead letters.

Esses cinco grupos já sustentam alertas acionáveis. Depois você refina por tenant, região ou feature flag — com parcimônia.

Para quem opera serviços async, combine com tokio-console no diagnóstico e com métricas estáveis no dia a dia. Console explica o runtime numa sessão; Prometheus mostra a tendência na semana.

Métricas, Tracing e OpenTelemetry: como encaixar

O site já cobre os outros vértices:

O desenho saudável em Rust costuma ser:

  1. instrumentar a aplicação com tracing nos limites úteis;
  2. expor métricas Prometheus dos SLIs;
  3. exportar traces (e, se fizer sentido, metrics) via OTel para o backend da empresa;
  4. manter configuração de exportação na borda, longe do domínio.

Não force um único fornecedor no meio do handler. O código de negócio deve dizer “autorização concluída” ou “query lenta”; a infraestrutura decide se isso vira série Prometheus, span Tempo ou ambos.

Protegendo /metrics e o runtime

/metrics não é página de marketing. Ele revela superfície interna: nomes de rotas, dependências, throughput, erros e às vezes topologia. Práticas mínimas:

  • porta separada (:9090 admin vs :3000 público);
  • NetworkPolicy / security group restrito aos scrapers;
  • em desenvolvimento local, bind em 127.0.0.1 quando possível;
  • graceful shutdown para não cortar scrapes no meio do deploy;
  • evitar alocações pesadas no caminho quente só para telemetria;
  • não registrar valores sensíveis em labels ou em exemplos de documentação interna.

Se o serviço usa Moka ou Redis, exponha hit/miss e evictions com labels curtos. Cache sem métrica vira achismo.

Erros comuns

Path bruto como label

Transforma cada ID em série nova. Normalize para o template da rota Axum.

Medir só contagem e esquecer latência

Um serviço pode estar “up” e mesmo assim inútil com p95 de 8s.

Misturar domínio e infraestrutura na mesma métrica sem legenda

Separe http_requests_total de payments_authorized_total. Alertas ficam mais claros.

Cardinalidade “só temporária”

Série temporal rara quase nunca é temporária no Prometheus. Remover depois não apaga o custo histórico de imediato.

Instrumentar tudo na primeira semana

Comece pelos SLIs. Adicione métricas quando uma pergunta operacional real aparecer duas vezes.

Expor /metrics público

É detalhe de segurança e de abuso. Trate como endpoint administrativo.

Achar que OTel elimina Prometheus

Muitas empresas ainda scrapam Prometheus. OTel e Prometheus convivem; a escolha é de arquitetura, não de moda.

Usar Pushgateway para API longa

Pushgateway é para jobs batch/efêmeros. API Axum estável deve ser scrapada.

Checklist de produção

  1. Escolha metrics + exporter Prometheus ou prometheus_client com motivo explícito.
  2. Exponha /metrics em superfície administrativa.
  3. Registre request rate, error rate e duração por rota normalizada.
  4. Adicione saturação de pool, fila e timeouts.
  5. Revise labels contra explosão de cardinalidade.
  6. Defina buckets de histograma a partir do orçamento de latência.
  7. Crie alertas para burn de SLO, não só para CPU alta.
  8. Correlacione com tracing/OTel nos incidentes.
  9. Documente no README como scrapear localmente e no cluster.
  10. Teste reinício do processo: counters zeram, dashboards com rate() continuam corretos.
  11. Inclua métricas no smoke de deploy (scrape ok + série esperada).
  12. Revise trimestralmente séries mortas e labels inúteis.

Prometheus como evidência de carreira

Em vagas Rust de backend, plataforma e SRE, “sei Rust” pesa menos que “operei um serviço Rust”. Um repositório bom mostra:

  • API Axum com middleware de métricas;
  • /metrics scrapável;
  • dashboard Grafana exportado em JSON;
  • alerta de taxa de erro e latência;
  • README com SLIs e como reproduzir carga;
  • comparação antes/depois de uma otimização com p95.

Isso conversa com empresas que usam Rust, com o nicho de backend web e com trilhas de DevOps/infra. Se quiser praticar o formato Prometheus do zero, o projeto de coletor de métricas continua útil como laboratório; este guia é o passo seguinte, plugado em Axum de verdade.

Perguntas frequentes

Qual crate usar para métricas Prometheus em Rust?

Para a maioria das APIs Axum, metrics com metrics-exporter-prometheus resolve rápido e com boa ergonomia. Escolha prometheus_client quando precisar de registry/encoding mais explícitos ou quando o padrão interno da empresa já for esse.

Métricas substituem tracing e OpenTelemetry?

Não. Métricas agregam saúde e alimentam SLO. Tracing explica o caminho de uma requisição. OpenTelemetry padroniza a exportação. Os três se complementam.

Onde expor o endpoint /metrics?

Em porta administrativa ou rede restrita aos scrapers. Evite deixá-lo aberto na mesma borda pública da API de usuários sem controle de acesso.

Quais métricas uma API Rust precisa no começo?

Taxa de requisições, erros 5xx, latência por rota normalizada, saturação de pool, timeouts/retries e profundidade de fila quando houver workers assíncronos.

Por que cardinalidade de labels é perigosa?

Porque cada combinação vira série armazenada e consultada. IDs de usuário, UUIDs e paths parametrizados fazem o custo crescer com o tráfego e degradam consultas.

Quando preferir OpenTelemetry Metrics em vez de Prometheus direto?

Quando a organização já centraliza telemetria via OTLP e quer um pipeline único. Se o padrão local é Prometheus + Grafana e o serviço é um binário Rust simples, o exporter direto costuma ser o caminho mais curto.

Conclusão

Prometheus em Rust deixa de ser “detalhe de DevOps” quando o serviço precisa provar comportamento sob carga. Com metrics, histograms bem escolhidos, labels disciplinados e um /metrics protegido, uma API Axum ganha SLIs reais — e o time deixa de depurar produção no escuro. Continue com OpenTelemetry em produção, tower-http, Tokio e o panorama de logging e observabilidade. Para aplicar no mercado, acompanhe as vagas e as empresas que já operam Rust em produção no Brasil.