Azure Cosmos DB Rust · async/tokio NoSQL API Production Ready

Telemetría de Agentes IA con Azure Cosmos DB + Rust

Microservicio para CULTIVA IA — registro de eventos de skills con CRUD completo y buenas prácticas de producción

📊 Stack del proyecto
Crate oficial
azure_data_cosmos
v0.x — SDK de Microsoft
Partition Key
agent_id
distribución global óptima
Base de datos
cultiva-telemetria
Container: eventos
Autenticación
Entra ID
ManagedIdentity en prod
🏗 Arquitectura del flujo
🤖
Skill Agent
copywriting / seo / ads
evento al finalizar
⚙️
Rust Service
tokio · async
CRUD async
🌐
CosmosClient
thread-safe · reutilizado
HTTPS · TLS
☁️
Azure Cosmos DB
ES · MX · CO
📦 Configuración de dependencias
Cargo.toml TOML
# CULTIVA IA — microservicio de telemetría de agentes
[package]
name = "cultiva-telemetria"
version = "0.1.0"
edition = "2021"

[dependencies]
# SDK oficial de Microsoft — NO usar azure_cosmos ni azure_sdk_for_rust
azure_data_cosmos = { version = "*", features = ["key_auth"] }
azure_identity    = "*"
# NOTA: azure_core NO se añade directamente, lo reexporta azure_data_cosmos
tokio             = { version = "1", features = ["full"] }
serde             = { version = "1", features = ["derive"] }
serde_json        = "1"
chrono            = { version = "0.4", features = ["serde"] }
uuid              = { version = "1", features = ["v4"] }
🗂 Modelo de documento — EventoTelemetria
src/models.rs RUST
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};

/// Documento almacenado en Cosmos DB (container: "eventos")
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EventoTelemetria {
    /// ID único del evento (partition key secundaria de Cosmos)
    pub id:          String,
    /// Partition key — identifica el agente (ej. "agent-copywriting-es")
    pub agent_id:    String,
    /// Nombre de la skill ejecutada
    pub skill:       String,
    /// "ok" | "error" | "timeout"
    pub resultado:   String,
    pub tokens_usados: u32,
    pub latencia_ms: u64,
    pub ts:          DateTime<Utc>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub revisado:    Option<bool>,
}
🔐 Autenticación y cliente compartido
src/db.rs RUST
use std::sync::Arc;
use azure_data_cosmos::{CosmosClient, CosmosAccountReference, CosmosAccountEndpoint};
use azure_identity::{DeveloperToolsCredential, ManagedIdentityCredential};
use azure_core::credentials::TokenCredential;

/// Construye el CosmosClient según el entorno.
/// CULTIVA IA: dev local → DeveloperToolsCredential
///             producción → ManagedIdentityCredential (VM/AKS/ACI)
pub async fn build_client() -> Result<CosmosClient, Box<dyn std::error::Error>> {
    // NUNCA hardcodear — leer desde variable de entorno
    let endpoint: CosmosAccountEndpoint =
        std::env::var("COSMOS_ENDPOINT")?.parse()?;

    let credential: Arc<dyn TokenCredential> = if std::env::var("AZURE_PROD").is_ok() {
        // Producción: Managed Identity asignada al servicio Azure
        ManagedIdentityCredential::new(None)?
    } else {
        // Desarrollo local: az login o VS Code Azure extension
        DeveloperToolsCredential::new(None)?
    };

    let account = CosmosAccountReference::with_credential(endpoint, credential);
    // CosmosClient es thread-safe: crear UNA VEZ y clonar Arc por task
    let client = CosmosClient::builder().build(account).await?;
    Ok(client)
}
Operaciones CRUD — src/repo.rs
src/repo.rs RUST · CRUD COMPLETO
use azure_data_cosmos::CosmosClient;
use crate::models::EventoTelemetria;

const DB: &str = "cultiva-telemetria";
const CONTAINER: &str = "eventos";

/// ✅ CREATE — registrar nuevo evento al finalizar una skill
pub async fn crear_evento(
    client: &CosmosClient,
    evento: EventoTelemetria,
) -> Result<(), Box<dyn std::error::Error>> {
    let pk = evento.agent_id.clone();   // partition key siempre requerida
    client
        .database_client(DB)
        .container_client(CONTAINER).await
        .create_item(&pk, evento, None).await?;
    Ok(())
}

/// 🔍 READ — recuperar evento por partition key + id
pub async fn leer_evento(
    client: &CosmosClient,
    agent_id: &str,
    id: &str,
) -> Result<EventoTelemetria, Box<dyn std::error::Error>> {
    let resp = client
        .database_client(DB)
        .container_client(CONTAINER).await
        .read_item(agent_id, id, None).await?;
    let evento: EventoTelemetria = resp.into_model()?;
    Ok(evento)
}

/// ✏️  UPDATE — marcar evento como revisado
pub async fn marcar_revisado(
    client: &CosmosClient,
    agent_id: &str,
    id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut evento = leer_evento(client, agent_id, id).await?;
    evento.revisado = Some(true);
    client
        .database_client(DB)
        .container_client(CONTAINER).await
        .replace_item(agent_id, id, evento, None).await?;
    Ok(())
}

/// 🗑️  DELETE — purgar evento (ej. retención 90 días)
pub async fn eliminar_evento(
    client: &CosmosClient,
    agent_id: &str,
    id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    client
        .database_client(DB)
        .container_client(CONTAINER).await
        .delete_item(agent_id, id, None).await?;
    Ok(())
}
🚀 Punto de entrada — main.rs
src/main.rs RUST · TOKIO
mod db;
mod models;
mod repo;

use chrono::Utc;
use uuid::Uuid;
use models::EventoTelemetria;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Cliente creado una vez y compartido (thread-safe)
    let client = db::build_client().await?;

    // — CREATE: nuevo evento al finalizar skill de copywriting —
    let evento = EventoTelemetria {
        id:           Uuid::new_v4().to_string(),
        agent_id:     "agent-copywriting-es".into(),
        skill:        "copywriting".into(),
        resultado:    "ok".into(),
        tokens_usados: 1420,
        latencia_ms:  834,
        ts:           Utc::now(),
        revisado:     None,
    };
    let id = evento.id.clone();
    repo::crear_evento(&client, evento).await?;
    println!("✅ CREATE OK — {id}");

    // — READ: recuperar el evento recién creado —
    let leido = repo::leer_evento(&client, "agent-copywriting-es", &id).await?;
    println!("🔍 READ  OK — skill={} tokens={}", leido.skill, leido.tokens_usados);

    // — UPDATE: marcar como revisado —
    repo::marcar_revisado(&client, "agent-copywriting-es", &id).await?;
    println!("✏️  UPDATE OK — revisado=true");

    // — DELETE: purga (simula política de 90 días) —
    repo::eliminar_evento(&client, "agent-copywriting-es", &id).await?;
    println!("🗑️  DELETE OK — evento purgado");

    Ok(())
}
🛡 Roles RBAC para Entra ID
Rol Acceso Entorno CULTIVA IA
Cosmos DB Built-in Data Reader Solo lectura Dashboards / analytics read-only
Cosmos DB Built-in Data Contributor Lectura + escritura Microservicio de telemetría (producción)
Buenas prácticas de producción
1
Credencial por entorno
Dev: DeveloperToolsCredential (az login). Prod: ManagedIdentityCredential. El SDK Rust NO tiene DefaultAzureCredential — elegir explícitamente.
2
Partition key en cada operación
Cosmos DB exige la partition key en todos los CRUD. Incluirla siempre en create_item, read_item, replace_item y delete_item.
3
Reutilizar CosmosClient
Thread-safe por diseño. Crear una instancia al inicio y compartirla vía Arc<CosmosClient> entre tasks de tokio. Nunca crear uno por petición.
4
Solo crate oficial
Usar únicamente azure_data_cosmos publicado por azure-sdk en crates.io. Ignorar azure_cosmos y azure_sdk_for_rust — son community crates no soportados.
5
Variables de entorno
Nunca hardcodear COSMOS_ENDPOINT ni claves. Usar std::env::var + secrets manager en Azure Key Vault para producción.
6
Verificar versiones en crates.io
Antes de añadir cualquier crate Azure, comprobar en crates.io que la versión existe y es publicada por azure-sdk. Ningún crate oficial tiene versión 0.21.0.