Construindo CLIs e Serviços de Dados
Entregue ETL rápido como CLIs baseadas em clap e serviços Tokio/Axum com observabilidade e modos de falha claros.
Receita
Cartão de receita de referência rápida - pronto para copiar e colar.
use anyhow::{Context, Result};
use clap::Parser;
use polars::prelude::*;
use tracing::info;
#[derive(Parser)]
#[command(name = "csv2pq", about = "Converte CSV para Parquet com filtro opcional")]
struct Cli {
#[arg(long)]
input: String,
#[arg(long)]
output: String,
#[arg(long, default_value_t = 0)]
min_amount: i64,
}
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let cli = Cli::parse();
run(&cli).context("etl falhou")?;
Ok(())
}
fn run(cli: &Cli) -> PolarsResult<()> {
info!(input = %cli.input, "iniciando");
let mut df = CsvReadOptions::default()
.try_into_reader_with_file_path(Some(cli.input.clone().into()))?
.finish()?;
if cli.min_amount > 0 {
df = df.lazy().filter(col("amount").gt(lit(cli.min_amount))).collect()?;
}
ParquetWriter::new(std::fs::File::create(&cli.output)?).finish(&mut df)?;
info!(rows = df.height(), "concluído");
Ok(())
}Quando usar isso:
- Substituindo scripts bash + Python frágeis no cron
- Ferramentas internas de plataforma de dados com
--helpconsistente - Camadas leves de ingestão/consulta HTTP na frente de lagos Parquet
- ETL de borda em máquinas sem um runtime Python
Exemplo de Trabalho
use axum::{extract::Query, routing::get, Json, Router};
use polars::prelude::*;
use serde::Deserialize;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)]
struct AppState {
df: Arc<RwLock<DataFrame>>,
}
#[derive(Deserialize)]
struct Params {
region: String,
}
async fn summary(
Query(p): Query<Params>,
axum::extract::State(state): axum::extract::State<AppState>,
) -> Result<Json<serde_json::Value>, String> {
let df = state.df.read().await;
let filtered = df
.clone()
.lazy()
.filter(col("region").eq(lit(p.region.clone())))
.select([col("amount").sum().alias("total")])
.collect()
.map_err(|e| e.to_string())?;
let total: i64 = filtered.column("total").unwrap().get(0).unwrap().try_extract().unwrap();
Ok(Json(serde_json::json!({ "region": p.region, "total": total })))
}
#[tokio::main]
async fn main() -> PolarsResult<()> {
let df = CsvReadOptions::default()
.try_into_reader_with_file_path(Some("sales.csv".into()))?
.finish()?;
let state = AppState { df: Arc::new(RwLock::new(df)) };
let app = Router::new().route("/summary", get(summary)).with_state(state);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
Ok(())
}O que isso demonstra:
- Padrão CLI com contexto
anyhowe logstracing - Handler Axum consultando um frame Polars em memória
- Estado compartilhado com
Arc<RwLock<_>>para datasets recarregáveis
Análise Profunda
Como Funciona
- CLIs analisam
argvuma vez, executam síncronamente ou bloqueiam emasynccom#[tokio::main]. - Serviços mantêm dados "quentes" em memória ou
mmapParquet para consultas repetidas. - Transformações longas pertencem a CLIs em lote; serviços expõem resumos ou pré-agregações.
- Logs estruturados (
tracing) correlacionam tempos de estágio com caminhos de entrada e contagens de linhas.
CLI vs Serviço
| Forma | Melhor para | Nota de Operações |
|---|---|---|
| CLI | Cron, transformações únicas | Versione o binário no repositório de artefatos |
| Serviço Axum | APIs internas de baixo QPS | Adicione autenticação, timeouts, limites de payload |
Notas de Rust
// Distinguir códigos de saída no main da CLI:
// std::process::exit(2) para argumentos ruins, 1 para erros de dados - ajuda orquestradores a tentar novamente corretamente.Armadilhas
- Carregar dataset completo por requisição HTTP - latência e picos de memória. Correção: manter snapshot compartilhado ou consultar Parquet preguiçosamente.
- Sem progresso stdin/stdout - operadores pensam que o trabalho travou no CSV longo. Correção: registrar a cada N chunks com
tracingouindicatif. - Pânicos em código de biblioteca - aborta a CLI sem contexto. Correção: retornar
Resultatravés demaine mapear para código de saída. - Endpoints de dados não autenticados - ferramentas internas vazam em má configuração de VPN. Correção:
mTLSou middleware de token desde o início. - Escrever Parquet parcial em caso de falha - corrompe arquivos downstream. Correção: escrever em caminho temporário e depois renomear atomicamente.
Alternativas
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| Python + Click | Equipe só conhece Python | Requisito de deploy de binário único |
| Airflow orquestrando CLI Rust | Dependências complexas de DAG | Trabalho simples de uma etapa noturna |
| gRPC + Arrow Flight | Service mesh de alta vazão | API JSON interna rápida |
| DuckDB CLI | SQL ad hoc em arquivos | Transformações Rust personalizadas no mesmo trabalho |
FAQs
ETL deve ser síncrono ou assíncrono?
CLIs com foco em arquivos são frequentemente Polars síncrono com tokio opcional apenas para serviços. Não use async para collect limitado por CPU sem spawn_blocking.
Como recarregar dados em um serviço?
Troque Arc<DataFrame> em um timer ou evento S3, ou mapeie em memória Parquet para benefícios de cache do SO.
Qual crate de logging?
Use tracing com tracing-subscriber JSON em produção; emparelhe spans por estágio do pipeline.
Como empacotar a CLI?
cargo build --release mais cargo deb opcional ou binários musl compilados cruzadamente - veja a seção de operações de deploy para padrões de distribuição.
O clap pode ler variáveis de ambiente?
Sim - #[arg(long, env = "INPUT_PATH")] ajuda contêineres sem longas listas de argv.
Como testar CLIs?
Use assert_cmd para executar o binário com CSVs de fixture temporários e afirmar o esquema Parquet de saída.
Serviços devem retornar Arrow ou JSON?
JSON para humanos e resumos pequenos; Arrow IPC para clientes de máquina puxando grandes conjuntos de resultados.
Como lidar com segredos?
Leia credenciais S3 de variáveis de ambiente ou roles IAM - nunca incorpore chaves no binário.
E os arquivos de configuração?
Serde TOML para definições de pipeline; flags da CLI substituem os padrões do arquivo para execuções únicas.
Como isso se conecta ao DataFusion?
Incorpore SQL na camada de serviço - veja DataFusion para manipuladores de consulta.
Relacionados
- Noções Básicas de Dados - exemplo de CSV com
clap - Streaming e Dados Grandes - pipelines CLI em chunks
- E/S CSV/Parquet - formatos
- Polars - núcleo de transformação
Versões da Stack: Esta página foi escrita para Rust 1.97.0 (edição 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, e Polars 0.46+.