Skip to main content
ImmutableLog logo
BackRust

Rust

Complete integration guide for ImmutableLog in Rust. Automatic middleware for Axum and Actix — or a direct reqwest HTTP client for workers, queue consumers, and jobs. Sending runs in a fire-and-forget tokio::spawn, adding no latency to the response.

Direct HTTP client

Use the Client to send events directly to ImmutableLog without depending on a web framework. Ideal for workers, queue consumers, cron jobs, and any async Rust code that needs to record events.

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;
// });

Wrap the Client in an Arc to share it across tasks. send_event respects the reqwest::Client 5s timeout; use tokio::spawn to fire without awaiting.

Retry with exponential backoff

For high-availability production, add retry with exponential backoff. ImmutableLog returns 202 on success and 200 on an idempotent duplicate — never retry on 429 (monthly limit reached).

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"))
    }
}

Sensitive data hashing

Never send raw personal data (email, CPF, IP) to ImmutableLog. Use SHA-256 to generate a deterministic digest — traceable without exposing the original data.

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"),
});

This documentation reflects the current API behavior. For questions or advanced integrations, contact the support team.