Rust
Guia completo de integração com o ImmutableLog em Rust. Middleware automático para Axum e Actix — ou cliente HTTP direto com reqwest para workers, consumidores de fila e jobs. O envio roda em tokio::spawn fire-and-forget, sem adicionar latência à resposta.
Middlewares
Integração automática via middleware — toda requisição HTTP é capturada sem modificar nenhum handler. Clique no framework para ver o código completo.
Middleware com axum::middleware::from_fn. Captura o StatusCode após next.run(req), resolve a trilha e envia via tokio::spawn.
pub async fn audit(
State(cfg): State<Arc<Config>>,
req: Request,
next: Next,
) -> Response {
let started = Instant::now();
let res = next.run(req).await;
tokio::spawn(emit(cfg, res.status(), started.elapsed()));
res
}Ver documentação completa →
Middleware via Transform + Service. Captura o status na resolução do future e dispara o envio com actix_web::rt::spawn.
fn call(&self, req: ServiceRequest) -> Self::Future {
let started = Instant::now();
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
actix_web::rt::spawn(emit(res.status(), started.elapsed()));
Ok(res)
})
}Ver documentação completa →
Cliente HTTP direto
Use o Client para enviar eventos diretamente ao ImmutableLog sem depender de um framework web. Ideal para workers, consumidores de fila, cron jobs e qualquer código Rust async que precise registrar eventos.
// Cargo.toml:
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
// uuid = { version = "1", features = ["v4"] }
use std::collections::HashMap;
use std::time::Duration;
use serde_json::json;
use uuid::Uuid;
pub struct Client {
api_key: String,
api_url: String,
service: String,
env: String,
http: reqwest::Client,
}
impl Client {
pub fn new(api_key: impl Into<String>, service: impl Into<String>, env: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
api_url: "https://api.immutablelog.com".to_string(),
service: service.into(),
env: env.into(),
http: reqwest::Client::builder()
.timeout(Duration::from_secs(5))
.build()
.expect("build http client"),
}
}
pub async fn send_event(
&self,
event_name: &str,
kind: &str, // "success" | "info" | "error"
payload: serde_json::Value,
trail: Option<&str>,
) -> Result<(), reqwest::Error> {
let request_id = Uuid::new_v4().to_string();
let trail = trail.and_then(sanitize_trail);
// Todos os valores de meta precisam ser strings.
let mut meta: HashMap<&str, String> = HashMap::new();
meta.insert("type", kind.to_string());
meta.insert("event_name", event_name.to_string());
meta.insert("service", self.service.clone());
meta.insert("request_id", request_id.clone());
meta.insert("env", self.env.clone());
if let Some(t) = &trail {
meta.insert("immutable_trail", t.clone());
}
let event = json!({ "payload": payload.to_string(), "meta": meta });
let mut req = self
.http
.post(format!("{}/v1/events", self.api_url))
.bearer_auth(&self.api_key)
.header("Content-Type", "application/json")
// Idempotency-Key e OBRIGATORIO (sem ele a API responde 400).
.header("Idempotency-Key", format!("{}-{}", event_name, request_id))
.header("Request-Id", &request_id)
.header("X-Client-TZ", "America/Sao_Paulo")
.header("X-Client-Offset-Minutes", "-180");
if let Some(t) = &trail {
req = req.header("X-Imtbl-Trail", t);
}
req.json(&event).send().await?;
Ok(())
}
}
// Normaliza o immutable_trail (trim, nao-vazio, max 256, sem ':').
// O servidor responde 400 invalid_immutable_trail se a regra for violada.
pub fn sanitize_trail(v: &str) -> Option<String> {
let mut s = v.trim().replace(':', "-");
if s.is_empty() {
return None;
}
if s.chars().count() > 256 {
s = s.chars().take(256).collect();
}
Some(s)
}
// Uso (fire-and-forget via tokio::spawn):
// let client = Arc::new(Client::new(std::env::var("IMTBL_API_KEY")?, "my-service", "production"));
// let c = client.clone();
// tokio::spawn(async move {
// let _ = c.send_event("user.created", "success",
// json!({ "user_id": "usr_123", "plan": "pro" }), Some("order-7782")).await;
// });Envolva o Client em Arc para compartilhá-lo entre tasks. send_event respeita o timeout de 5s do reqwest::Client; use tokio::spawn para disparar sem aguardar.
Retry com backoff exponencial
Para produção de alta disponibilidade, adicione retry com backoff exponencial. O ImmutableLog retorna 202 em sucesso e 200 em duplicata idempotente — nunca faça retry em 429 (limite mensal atingido).
// Retry em erros transitorios (nunca em 429).
// Retries transient errors (never on 429).
impl Client {
pub async fn send_with_retry(
&self,
event_name: &str,
kind: &str,
payload: serde_json::Value,
trail: Option<&str>,
max_attempts: u32,
) -> Result<(), reqwest::Error> {
let mut delay = Duration::from_millis(500);
let mut last_err = None;
for attempt in 0..max_attempts {
match self.send_event(event_name, kind, payload.clone(), trail).await {
Ok(()) => return Ok(()),
Err(e) => {
// 429 = limite mensal — nao fazer retry.
if e.status().map(|s| s.as_u16()) == Some(429) {
return Err(e);
}
last_err = Some(e);
if attempt + 1 < max_attempts {
tokio::time::sleep(delay).await;
delay *= 2; // 500ms -> 1s -> 2s
}
}
}
}
Err(last_err.expect("at least one attempt"))
}
}Hash de dados sensíveis
Nunca envie dados pessoais brutos (e-mail, CPF, IP) para o ImmutableLog. Use SHA-256 para gerar um digest determinístico — rastreável sem expor o dado original.
// Cargo.toml: sha2 = "0.10", hex = "0.4"
use sha2::{Digest, Sha256};
// Faz o hash de dados sensiveis antes de enviar — nunca logue PII bruto.
fn sha256_hex(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
hex::encode(hasher.finalize())
}
// Uso no payload do evento:
let payload = serde_json::json!({
"user_id": "usr_123",
"email_hash": sha256_hex(b"user@example.com"),
"ip_hash": sha256_hex(b"192.168.1.1"),
});Esta documentação reflete o comportamento atual da API. Para dúvidas ou integrações avançadas, entre em contato com o time de suporte.
