Pular para o conteúdo principal
ImmutableLog logo
VoltarRust

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.

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.

rust
// 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).

rust
// 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.

rust
// 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.