Microservicio para CULTIVA IA — registro de eventos de skills con CRUD completo y buenas prácticas de producción
# 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"] }
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>, }
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) }
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(()) }
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(()) }
| 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) |
DeveloperToolsCredential (az login). Prod: ManagedIdentityCredential. El SDK Rust NO tiene DefaultAzureCredential — elegir explícitamente.create_item, read_item, replace_item y delete_item.Arc<CosmosClient> entre tasks de tokio. Nunca crear uno por petición.azure_data_cosmos publicado por azure-sdk en crates.io. Ignorar azure_cosmos y azure_sdk_for_rust — son community crates no soportados.COSMOS_ENDPOINT ni claves. Usar std::env::var + secrets manager en Azure Key Vault para producción.crates.io que la versión existe y es publicada por azure-sdk. Ningún crate oficial tiene versión 0.21.0.