sekuire 0.1.0

The official SDK for the Sekuire Agent Identity Protocol
Documentation
/*!
🛡️ Sekuire Logger - Asynchronous Event Logging for Rust SDK

Provides batched, fire-and-forget logging to Sekuire Registry API
*/

use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::time::interval;
use uuid::Uuid;

#[derive(Debug, Clone)]
pub struct LoggerConfig {
    pub sekuire_id: String,
    pub api_base_url: String,
    pub api_key: Option<String>,
    pub session_id: Option<String>,
    pub workspace_id: Option<String>,
    pub environment: Option<String>,
    pub enabled: bool,
    pub batch_size: usize,
    pub flush_interval: Duration,
}

impl Default for LoggerConfig {
    fn default() -> Self {
        Self {
            sekuire_id: String::new(),
            api_base_url: "http://localhost:9300".to_string(),
            api_key: None,
            session_id: Some(Uuid::new_v4().to_string()),
            workspace_id: None,
            environment: Some("development".to_string()),
            enabled: true,
            batch_size: 50,
            flush_interval: Duration::from_secs(10),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventType {
    ToolExecution,
    ModelCall,
    PolicyViolation,
    PolicyCheck,
    NetworkAccess,
    FileAccess,
    Health,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
    Debug,
    Info,
    Warn,
    Error,
}

#[derive(Debug, Clone, Serialize)]
pub struct EventLog {
    pub sekuire_id: String,
    pub session_id: String,
    pub workspace_id: Option<String>,
    pub event_type: EventType,
    pub severity: Severity,
    pub event_timestamp: String, // ISO8601
    pub event_data: Value,
    pub metadata: Value,
}

#[derive(Debug, Serialize)]
struct LogEventRequest {
    events: Vec<EventLog>,
}

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct LogEventResponse {
    success: bool,
    events_logged: usize,
    errors: Vec<String>,
}

/// Asynchronous logger for Sekuire compliance events
///
/// Features:
/// - Buffered batching (flush every N events or X seconds)
/// - Fire-and-forget (non-blocking)
/// - Health heartbeat tracking
/// - Graceful shutdown
pub struct SekuireLogger {
    config: LoggerConfig,
    buffer: Arc<Mutex<Vec<EventLog>>>,
    client: Client,
    sdk_version: &'static str,
}

impl SekuireLogger {
    /// Create a new logger with configuration
    pub fn new(config: LoggerConfig) -> Self {
        let client = Client::builder()
            .timeout(Duration::from_secs(5))
            .build()
            .expect("Failed to create HTTP client");

        Self {
            config,
            buffer: Arc::new(Mutex::new(Vec::new())),
            client,
            sdk_version: "rust-sdk-0.1.0",
        }
    }

    /// Log an event (buffered, non-blocking)
    pub fn log_event(&self, event_type: EventType, severity: Severity, event_data: Value) {
        if !self.config.enabled {
            return;
        }

        let event = EventLog {
            sekuire_id: self.config.sekuire_id.clone(),
            session_id: self
                .config
                .session_id
                .clone()
                .unwrap_or_else(|| Uuid::new_v4().to_string()),
            workspace_id: self.config.workspace_id.clone(),
            event_type,
            severity,
            event_timestamp: chrono::Utc::now().to_rfc3339(),
            event_data,
            metadata: serde_json::json!({
                "sdk_version": self.sdk_version,
                "environment": self.config.environment,
            }),
        };

        let mut buffer = self.buffer.lock().unwrap();
        buffer.push(event);

        // Auto-flush if batch size reached
        let should_flush = buffer.len() >= self.config.batch_size;
        drop(buffer); // Release lock before async flush

        if should_flush {
            let logger = self.clone();
            tokio::spawn(async move {
                if let Err(e) = logger.flush().await {
                    eprintln!("Failed to auto-flush logs: {}", e);
                }
            });
        }
    }

    /// Flush buffer to API (fire-and-forget)
    pub async fn flush(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let events_to_send = {
            let mut buffer = self.buffer.lock().unwrap();
            if buffer.is_empty() {
                return Ok(());
            }
            buffer.drain(..).take(100).collect::<Vec<_>>()
        };

        let url = format!(
            "{}/api/v1/agents/{}/logs/batch",
            self.config.api_base_url, self.config.sekuire_id
        );

        let mut request = self.client.post(&url).json(&LogEventRequest {
            events: events_to_send.clone(),
        });

        if let Some(api_key) = &self.config.api_key {
            request = request.header("X-API-Key", api_key);
        }

        match request.send().await {
            Ok(response) => {
                if response.status().is_success() {
                    match response.json::<LogEventResponse>().await {
                        Ok(data) => {
                            println!(
                                "✅ Logged {}/{} events to Sekuire API",
                                data.events_logged,
                                events_to_send.len()
                            );
                            if !data.errors.is_empty() {
                                eprintln!("⚠️ Some events failed: {:?}", data.errors);
                            }
                        }
                        Err(e) => {
                            eprintln!("Failed to parse response: {}", e);
                        }
                    }
                } else {
                    eprintln!("❌ Failed to flush logs: HTTP {}", response.status());
                }
            }
            Err(e) => {
                eprintln!("❌ Failed to flush logs to Sekuire API: {}", e);
                // Fire-and-forget: Don't retry, don't propagate error
            }
        }

        Ok(())
    }

    /// Send health heartbeat to API
    async fn send_health_heartbeat(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let url = format!(
            "{}/api/v1/agents/{}/logs/health",
            self.config.api_base_url, self.config.sekuire_id
        );

        let payload = serde_json::json!({
            "sekuire_id": self.config.sekuire_id,
            "status": "active",
            "session_id": self.config.session_id,
            "sdk_version": self.sdk_version,
            "environment": self.config.environment,
            "metadata": {}
        });

        let mut request = self.client.post(&url).json(&payload);

        if let Some(api_key) = &self.config.api_key {
            request = request.header("X-API-Key", api_key);
        }

        match request.send().await {
            Ok(_) => {
                println!("💓 Health heartbeat sent");
                Ok(())
            }
            Err(e) => {
                eprintln!("Failed to send health heartbeat: {}", e);
                Ok(()) // Fire-and-forget: Don't propagate error
            }
        }
    }

    /// Start background flush task
    ///
    /// This spawns a tokio task that flushes logs periodically and sends health heartbeats
    pub fn start_background_flush(self: Arc<Self>) -> tokio::task::JoinHandle<()> {
        tokio::spawn(async move {
            let mut flush_interval = interval(self.config.flush_interval);
            let mut heartbeat_interval = interval(Duration::from_secs(30));

            loop {
                tokio::select! {
                    _ = flush_interval.tick() => {
                        if let Err(e) = self.flush().await {
                            eprintln!("Flush error: {}", e);
                        }
                    }
                    _ = heartbeat_interval.tick() => {
                        if let Err(e) = self.send_health_heartbeat().await {
                            eprintln!("Heartbeat error: {}", e);
                        }
                    }
                }
            }
        })
    }

    /// Shutdown logger gracefully
    pub async fn shutdown(&self) {
        if let Err(e) = self.flush().await {
            eprintln!("Error during shutdown flush: {}", e);
        }
        println!("📴 Sekuire logger shutdown complete");
    }

    /// Get logger status for debugging
    pub fn get_status(&self) -> LoggerStatus {
        let buffer = self.buffer.lock().unwrap();
        LoggerStatus {
            enabled: self.config.enabled,
            buffered_events: buffer.len(),
            session_id: self
                .config
                .session_id
                .clone()
                .unwrap_or_else(|| "none".to_string()),
            environment: self
                .config
                .environment
                .clone()
                .unwrap_or_else(|| "unknown".to_string()),
        }
    }
}

impl Clone for SekuireLogger {
    fn clone(&self) -> Self {
        Self {
            config: self.config.clone(),
            buffer: Arc::clone(&self.buffer),
            client: self.client.clone(),
            sdk_version: self.sdk_version,
        }
    }
}

#[derive(Debug, Clone)]
pub struct LoggerStatus {
    pub enabled: bool,
    pub buffered_events: usize,
    pub session_id: String,
    pub environment: String,
}