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, 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>,
}
pub struct SekuireLogger {
config: LoggerConfig,
buffer: Arc<Mutex<Vec<EventLog>>>,
client: Client,
sdk_version: &'static str,
}
impl SekuireLogger {
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",
}
}
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);
let should_flush = buffer.len() >= self.config.batch_size;
drop(buffer);
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);
}
});
}
}
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);
}
}
Ok(())
}
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(()) }
}
}
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);
}
}
}
}
})
}
pub async fn shutdown(&self) {
if let Err(e) = self.flush().await {
eprintln!("Error during shutdown flush: {}", e);
}
println!("📴 Sekuire logger shutdown complete");
}
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,
}