vv-agent 0.4.1

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::Path;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};

use serde::{Deserialize, Serialize};
use serde_json::json;

pub trait TraceSink: Send + Sync {
    fn on_span_start(&self, span: &Span);
    fn on_span_end(&self, span: &Span);

    fn flush(&self) -> Result<(), String> {
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Span {
    pub trace_id: String,
    pub span_id: String,
    pub name: String,
    pub run_id: String,
    pub agent_name: Option<String>,
}

impl Span {
    pub fn new(
        run_id: impl Into<String>,
        name: impl Into<String>,
        agent_name: Option<String>,
    ) -> Self {
        let run_id = run_id.into();
        let name = name.into();
        Self {
            trace_id: run_id.clone(),
            span_id: format!("{}_{}", name, timestamp_millis()),
            name,
            run_id,
            agent_name,
        }
    }
}

pub struct JsonlTraceExporter {
    file: Mutex<File>,
}

impl JsonlTraceExporter {
    pub fn new(path: impl AsRef<Path>) -> Result<Self, String> {
        let file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(path)
            .map_err(|error| error.to_string())?;
        Ok(Self {
            file: Mutex::new(file),
        })
    }

    fn write_event(&self, event: &str, span: &Span) {
        if let Ok(mut file) = self.file.lock() {
            let _ = writeln!(
                file,
                "{}",
                json!({
                    "event": event,
                    "timestamp_ms": timestamp_millis(),
                    "span": span,
                })
            );
        }
    }
}

impl TraceSink for JsonlTraceExporter {
    fn on_span_start(&self, span: &Span) {
        self.write_event("span_start", span);
    }

    fn on_span_end(&self, span: &Span) {
        self.write_event("span_end", span);
    }

    fn flush(&self) -> Result<(), String> {
        self.file
            .lock()
            .map_err(|_| "trace exporter lock poisoned".to_string())?
            .flush()
            .map_err(|error| error.to_string())
    }
}

fn timestamp_millis() -> u128 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_millis())
        .unwrap_or_default()
}