use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::sync::mpsc::{Receiver, Sender, channel};
use std::thread::JoinHandle;
use std::time::Duration;
use rusqlite::{Connection, OpenFlags, params};
use crate::init::TelemetryError;
use crate::span_exporter::SpanSink;
const BATCH_SIZE: usize = 64;
const BATCH_INTERVAL: Duration = Duration::from_millis(250);
const FLUSH_TIMEOUT: Duration = Duration::from_secs(10);
const SESSION_ID_KEY: &str = "gcp.vertex.agent.session_id";
const SCHEMA: &str = "
CREATE TABLE IF NOT EXISTS spans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trace_id TEXT NOT NULL,
span_id TEXT NOT NULL,
session_id TEXT,
span_name TEXT NOT NULL,
start_time_unix_nanos INTEGER NOT NULL,
end_time_unix_nanos INTEGER NOT NULL,
attributes TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_spans_trace ON spans(trace_id);
CREATE INDEX IF NOT EXISTS idx_spans_session ON spans(session_id);
CREATE INDEX IF NOT EXISTS idx_spans_name ON spans(span_name);
";
enum WriterMsg {
Span { name: String, attributes: HashMap<String, String> },
Flush(Sender<()>),
Shutdown,
}
pub struct SqliteSpanExporter {
tx: Sender<WriterMsg>,
writer: Mutex<Option<JoinHandle<()>>>,
record_all: bool,
path: PathBuf,
}
impl SqliteSpanExporter {
pub fn new(path: impl AsRef<Path>) -> Result<Self, TelemetryError> {
let path = path.as_ref().to_path_buf();
let conn = open_writer_connection(&path)?;
let (tx, rx) = channel::<WriterMsg>();
let writer = std::thread::Builder::new()
.name("adk-telemetry-sqlite".into())
.spawn(move || writer_loop(conn, rx))
.map_err(|e| TelemetryError::Init(format!("failed to spawn sqlite writer: {e}")))?;
Ok(Self { tx, writer: Mutex::new(Some(writer)), record_all: false, path })
}
pub fn record_all_spans(mut self, record_all: bool) -> Self {
self.record_all = record_all;
self
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn flush(&self) -> Result<(), TelemetryError> {
let (ack_tx, ack_rx) = channel();
self.tx
.send(WriterMsg::Flush(ack_tx))
.map_err(|_| TelemetryError::Init("sqlite writer thread is gone".into()))?;
ack_rx
.recv_timeout(FLUSH_TIMEOUT)
.map_err(|_| TelemetryError::Init("sqlite flush timed out".into()))
}
pub fn shutdown(&self) {
let _ = self.tx.send(WriterMsg::Shutdown);
if let Some(handle) = self.writer.lock().unwrap_or_else(|e| e.into_inner()).take() {
let _ = handle.join();
}
}
}
impl Drop for SqliteSpanExporter {
fn drop(&mut self) {
self.shutdown();
}
}
impl std::fmt::Debug for SqliteSpanExporter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SqliteSpanExporter")
.field("path", &self.path)
.field("record_all", &self.record_all)
.finish()
}
}
impl SpanSink for SqliteSpanExporter {
fn export_span(&self, span_name: &str, attributes: HashMap<String, String>) {
if !self.record_all && !is_agent_loop_span(span_name) {
return;
}
let _ = self.tx.send(WriterMsg::Span { name: span_name.to_string(), attributes });
}
}
fn is_agent_loop_span(span_name: &str) -> bool {
span_name == "agent.execute"
|| span_name == "call_llm"
|| span_name == "send_data"
|| span_name.starts_with("execute_tool")
}
fn open_writer_connection(path: &Path) -> Result<Connection, TelemetryError> {
let conn = Connection::open(path)
.map_err(|e| TelemetryError::Init(format!("failed to open sqlite db: {e}")))?;
conn.pragma_update(None, "journal_mode", "WAL")
.map_err(|e| TelemetryError::Init(format!("failed to enable WAL: {e}")))?;
conn.pragma_update(None, "synchronous", "NORMAL")
.map_err(|e| TelemetryError::Init(format!("failed to set synchronous: {e}")))?;
conn.busy_timeout(Duration::from_secs(5))
.map_err(|e| TelemetryError::Init(format!("failed to set busy timeout: {e}")))?;
conn.execute_batch(SCHEMA)
.map_err(|e| TelemetryError::Init(format!("failed to create schema: {e}")))?;
Ok(conn)
}
fn writer_loop(mut conn: Connection, rx: Receiver<WriterMsg>) {
let mut batch: Vec<(String, HashMap<String, String>)> = Vec::with_capacity(BATCH_SIZE);
loop {
match rx.recv_timeout(BATCH_INTERVAL) {
Ok(WriterMsg::Span { name, attributes }) => {
batch.push((name, attributes));
if batch.len() >= BATCH_SIZE {
write_batch(&mut conn, &mut batch);
}
}
Ok(WriterMsg::Flush(ack)) => {
write_batch(&mut conn, &mut batch);
let _ = ack.send(());
}
Ok(WriterMsg::Shutdown) => {
write_batch(&mut conn, &mut batch);
break;
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
write_batch(&mut conn, &mut batch);
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
write_batch(&mut conn, &mut batch);
break;
}
}
}
}
fn write_batch(conn: &mut Connection, batch: &mut Vec<(String, HashMap<String, String>)>) {
if batch.is_empty() {
return;
}
let result = (|| -> rusqlite::Result<()> {
let tx = conn.transaction()?;
{
let mut stmt = tx.prepare_cached(
"INSERT INTO spans (trace_id, span_id, session_id, span_name,
start_time_unix_nanos, end_time_unix_nanos, attributes)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
)?;
for (name, attributes) in batch.iter() {
let trace_id = attributes.get("trace_id").cloned().unwrap_or_default();
let span_id = attributes.get("span_id").cloned().unwrap_or_default();
let session_id = attributes.get(SESSION_ID_KEY).cloned();
let start = attr_nanos(attributes, "start_time");
let end = attr_nanos(attributes, "end_time");
let json = serde_json::to_string(attributes).unwrap_or_else(|_| "{}".into());
stmt.execute(params![trace_id, span_id, session_id, name, start, end, json])?;
}
}
tx.commit()
})();
if let Err(e) = result {
tracing::warn!("adk-telemetry sqlite writer failed to commit batch: {e}");
}
batch.clear();
}
fn attr_nanos(attributes: &HashMap<String, String>, key: &str) -> i64 {
attributes.get(key).and_then(|v| v.parse::<i64>().ok()).unwrap_or(0)
}
#[derive(Debug, Clone)]
pub struct SpanRow {
pub trace_id: String,
pub span_id: String,
pub session_id: Option<String>,
pub span_name: String,
pub start_time_unix_nanos: i64,
pub end_time_unix_nanos: i64,
pub attributes: HashMap<String, String>,
}
impl SpanRow {
pub fn duration_nanos(&self) -> i64 {
self.end_time_unix_nanos - self.start_time_unix_nanos
}
}
#[derive(Debug, Clone)]
pub struct SessionSummary {
pub session_id: String,
pub span_count: u64,
pub first_start_unix_nanos: i64,
pub last_end_unix_nanos: i64,
}
pub struct SqliteTraceReader {
conn: Connection,
}
impl SqliteTraceReader {
pub fn open(path: impl AsRef<Path>) -> Result<Self, TelemetryError> {
let conn = Connection::open_with_flags(
path.as_ref(),
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.map_err(|e| TelemetryError::Init(format!("failed to open sqlite db read-only: {e}")))?;
conn.busy_timeout(Duration::from_secs(5))
.map_err(|e| TelemetryError::Init(format!("failed to set busy timeout: {e}")))?;
Ok(Self { conn })
}
pub fn sessions(&self) -> Result<Vec<SessionSummary>, TelemetryError> {
let mut stmt = self
.conn
.prepare(
"SELECT session_id, COUNT(*), MIN(start_time_unix_nanos), MAX(end_time_unix_nanos)
FROM spans WHERE session_id IS NOT NULL
GROUP BY session_id ORDER BY MAX(end_time_unix_nanos) DESC",
)
.map_err(sql_err)?;
let rows = stmt
.query_map([], |row| {
Ok(SessionSummary {
session_id: row.get(0)?,
span_count: row.get(1)?,
first_start_unix_nanos: row.get(2)?,
last_end_unix_nanos: row.get(3)?,
})
})
.map_err(sql_err)?;
rows.collect::<Result<Vec<_>, _>>().map_err(sql_err)
}
pub fn session_trace(&self, session_id: &str) -> Result<Vec<SpanRow>, TelemetryError> {
self.query_spans(
"SELECT trace_id, span_id, session_id, span_name,
start_time_unix_nanos, end_time_unix_nanos, attributes
FROM spans WHERE session_id = ?1 ORDER BY start_time_unix_nanos",
params![session_id],
)
}
pub fn trace(&self, trace_id: &str) -> Result<Vec<SpanRow>, TelemetryError> {
self.query_spans(
"SELECT trace_id, span_id, session_id, span_name,
start_time_unix_nanos, end_time_unix_nanos, attributes
FROM spans WHERE trace_id = ?1 ORDER BY start_time_unix_nanos",
params![trace_id],
)
}
pub fn recent_spans(&self, limit: u32) -> Result<Vec<SpanRow>, TelemetryError> {
self.query_spans(
"SELECT trace_id, span_id, session_id, span_name,
start_time_unix_nanos, end_time_unix_nanos, attributes
FROM spans ORDER BY end_time_unix_nanos DESC LIMIT ?1",
params![limit],
)
}
fn query_spans(
&self,
sql: &str,
params: impl rusqlite::Params,
) -> Result<Vec<SpanRow>, TelemetryError> {
let mut stmt = self.conn.prepare(sql).map_err(sql_err)?;
let rows = stmt
.query_map(params, |row| {
let json: String = row.get(6)?;
Ok(SpanRow {
trace_id: row.get(0)?,
span_id: row.get(1)?,
session_id: row.get(2)?,
span_name: row.get(3)?,
start_time_unix_nanos: row.get(4)?,
end_time_unix_nanos: row.get(5)?,
attributes: serde_json::from_str(&json).unwrap_or_default(),
})
})
.map_err(sql_err)?;
rows.collect::<Result<Vec<_>, _>>().map_err(sql_err)
}
}
fn sql_err(e: rusqlite::Error) -> TelemetryError {
TelemetryError::Init(format!("sqlite query failed: {e}"))
}