structscope_provenance/
lib.rs1use anyhow::Result;
2use rusqlite::{params, Connection};
3use serde::{Deserialize, Serialize};
4use serde_json::json;
5use std::fs;
6use std::fs::OpenOptions;
7use std::io::Write;
8use std::path::{Path, PathBuf};
9use structscope_events::Event;
10use uuid::Uuid;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ProvenanceConfig {
14 pub sqlite_path: Option<PathBuf>,
15 pub jsonl_path: Option<PathBuf>,
16}
17
18#[derive(Debug)]
19pub struct ProvenanceRecorder {
20 run_id: String,
21 sqlite: Option<Connection>,
22 jsonl_path: Option<PathBuf>,
23}
24
25impl ProvenanceRecorder {
26 pub fn open(config: &ProvenanceConfig, command: &str) -> Result<Self> {
27 let run_id = Uuid::new_v4().to_string();
28 let sqlite = match &config.sqlite_path {
29 Some(path) => {
30 ensure_parent_dir(path)?;
31 let conn = Connection::open(path)?;
32 init_schema(&conn)?;
33 conn.execute(
34 "INSERT INTO runs (run_id, command, started_at) VALUES (?1, ?2, datetime('now'))",
35 params![run_id, command],
36 )?;
37 Some(conn)
38 }
39 None => None,
40 };
41
42 Ok(Self {
43 run_id,
44 sqlite,
45 jsonl_path: config.jsonl_path.clone(),
46 })
47 }
48
49 pub fn run_id(&self) -> &str {
50 &self.run_id
51 }
52
53 pub fn record(&mut self, event: Event) -> Result<()> {
54 if let Some(conn) = &self.sqlite {
55 conn.execute(
56 "INSERT INTO events (run_id, event, timestamp, structure_id, details_json) VALUES (?1, ?2, ?3, ?4, ?5)",
57 params![
58 &self.run_id,
59 &event.event,
60 event.timestamp.to_rfc3339(),
61 &event.structure_id,
62 event.details.to_string()
63 ],
64 )?;
65 }
66
67 if let Some(path) = &self.jsonl_path {
68 append_jsonl(path, &event)?;
69 }
70
71 Ok(())
72 }
73
74 pub fn finish(&mut self) -> Result<()> {
75 self.record(Event::new("run_complete", None, json!({ "run_id": &self.run_id })))
76 }
77}
78
79pub fn inspect_sqlite(path: &Path) -> Result<Vec<String>> {
80 let conn = Connection::open(path)?;
81 let mut stmt = conn.prepare(
82 "SELECT run_id, command, started_at FROM runs ORDER BY started_at DESC",
83 )?;
84 let rows = stmt.query_map([], |row| {
85 Ok(format!(
86 "{}\t{}\t{}",
87 row.get::<_, String>(0)?,
88 row.get::<_, String>(1)?,
89 row.get::<_, String>(2)?
90 ))
91 })?;
92
93 let mut out = Vec::new();
94 for row in rows {
95 out.push(row?);
96 }
97 Ok(out)
98}
99
100fn init_schema(conn: &Connection) -> Result<()> {
101 conn.execute_batch(
102 "
103 CREATE TABLE IF NOT EXISTS runs (
104 run_id TEXT PRIMARY KEY,
105 command TEXT NOT NULL,
106 started_at TEXT NOT NULL
107 );
108 CREATE TABLE IF NOT EXISTS events (
109 id INTEGER PRIMARY KEY AUTOINCREMENT,
110 run_id TEXT NOT NULL,
111 event TEXT NOT NULL,
112 timestamp TEXT NOT NULL,
113 structure_id TEXT,
114 details_json TEXT NOT NULL
115 );
116 ",
117 )?;
118 Ok(())
119}
120
121fn append_jsonl(path: &Path, event: &Event) -> Result<()> {
122 ensure_parent_dir(path)?;
123 let mut file = OpenOptions::new().create(true).append(true).open(path)?;
124 writeln!(file, "{}", serde_json::to_string(event)?)?;
125 Ok(())
126}
127
128fn ensure_parent_dir(path: &Path) -> Result<()> {
129 if let Some(parent) = path.parent() {
130 if !parent.as_os_str().is_empty() {
131 fs::create_dir_all(parent)?;
132 }
133 }
134 Ok(())
135}