1use std::collections::HashMap;
10use std::fs;
11use std::os::unix::fs::PermissionsExt;
12use std::path::PathBuf;
13use std::sync::Arc;
14use std::sync::LazyLock;
15use std::sync::Mutex;
16
17use anyhow::Result;
18use anyhow::anyhow;
19use rusqlite::Connection;
20use rusqlite::functions::FunctionFlags;
21use serde::Serialize;
22use serde_json::Value as JValue;
23use serde_rusqlite::*;
24use tracing::Event;
25use tracing::Subscriber;
26use tracing_subscriber::Layer;
27use tracing_subscriber::Registry;
28use tracing_subscriber::prelude::*;
29use tracing_subscriber::reload;
30
31pub type SqliteReloadHandle = reload::Handle<Option<SqliteLayer>, Registry>;
32
33static RELOAD_HANDLE: Mutex<Option<SqliteReloadHandle>> = Mutex::new(None);
36
37pub trait TableDef {
38 fn name(&self) -> &'static str;
39 fn columns(&self) -> &'static [&'static str];
40 fn create_table_stmt(&self) -> String {
41 let name = self.name();
42 let columns = self
43 .columns()
44 .iter()
45 .map(|col| format!("{col} TEXT "))
46 .collect::<Vec<String>>()
47 .join(",");
48 format!("create table if not exists {name} (seq INTEGER primary key, {columns})")
49 }
50 fn insert_stmt(&self) -> String {
51 let name = self.name();
52 let columns = self.columns().join(", ");
53 let params = self
54 .columns()
55 .iter()
56 .map(|c| format!(":{c}"))
57 .collect::<Vec<String>>()
58 .join(", ");
59 format!("insert into {name} ({columns}) values ({params})")
60 }
61}
62
63impl TableDef for (&'static str, &'static [&'static str]) {
64 fn name(&self) -> &'static str {
65 self.0
66 }
67
68 fn columns(&self) -> &'static [&'static str] {
69 self.1
70 }
71}
72
73#[derive(Clone, Debug)]
74pub struct Table {
75 pub columns: &'static [&'static str],
76 pub create_table_stmt: String,
77 pub insert_stmt: String,
78}
79
80impl From<(&'static str, &'static [&'static str])> for Table {
81 fn from(value: (&'static str, &'static [&'static str])) -> Self {
82 Self {
83 columns: value.columns(),
84 create_table_stmt: value.create_table_stmt(),
85 insert_stmt: value.insert_stmt(),
86 }
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum TableName {
92 ActorLifecycle,
93 Messages,
94 LogEvents,
95}
96
97impl TableName {
98 pub const ACTOR_LIFECYCLE_STR: &'static str = "actor_lifecycle";
99 pub const MESSAGES_STR: &'static str = "messages";
100 pub const LOG_EVENTS_STR: &'static str = "log_events";
101
102 pub fn as_str(&self) -> &'static str {
103 match self {
104 TableName::ActorLifecycle => Self::ACTOR_LIFECYCLE_STR,
105 TableName::Messages => Self::MESSAGES_STR,
106 TableName::LogEvents => Self::LOG_EVENTS_STR,
107 }
108 }
109
110 pub fn get_table(&self) -> &'static Table {
111 match self {
112 TableName::ActorLifecycle => &ACTOR_LIFECYCLE,
113 TableName::Messages => &MESSAGES,
114 TableName::LogEvents => &LOG_EVENTS,
115 }
116 }
117}
118
119static ACTOR_LIFECYCLE: LazyLock<Table> = LazyLock::new(|| {
120 (
121 TableName::ActorLifecycle.as_str(),
122 [
123 "actor_id",
124 "actor",
125 "name",
126 "supervised_actor",
127 "actor_status",
128 "module_path",
129 "line",
130 "file",
131 ]
132 .as_slice(),
133 )
134 .into()
135});
136
137static MESSAGES: LazyLock<Table> = LazyLock::new(|| {
138 (
139 TableName::Messages.as_str(),
140 [
141 "span_id",
142 "time_us",
143 "src",
144 "dest",
145 "payload",
146 "module_path",
147 "line",
148 "file",
149 ]
150 .as_slice(),
151 )
152 .into()
153});
154
155static LOG_EVENTS: LazyLock<Table> = LazyLock::new(|| {
156 (
157 TableName::LogEvents.as_str(),
158 [
159 "span_id",
160 "time_us",
161 "name",
162 "message",
163 "actor_id",
164 "level",
165 "line",
166 "file",
167 "module_path",
168 ]
169 .as_slice(),
170 )
171 .into()
172});
173
174pub static ALL_TABLES: LazyLock<Vec<Table>> = LazyLock::new(|| {
175 vec![
176 ACTOR_LIFECYCLE.clone(),
177 MESSAGES.clone(),
178 LOG_EVENTS.clone(),
179 ]
180});
181
182pub struct SqliteLayer {
183 conn: Arc<Mutex<Connection>>,
184}
185use tracing::field::Visit;
186
187#[derive(Debug, Clone, Default, Serialize)]
188pub struct SqlVisitor(pub HashMap<String, JValue>);
189
190impl Visit for SqlVisitor {
191 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
192 self.0.insert(
193 field.name().to_string(),
194 JValue::String(format!("{:?}", value)),
195 );
196 }
197
198 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
199 self.0
200 .insert(field.name().to_string(), JValue::String(value.to_string()));
201 }
202
203 fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
204 self.0
205 .insert(field.name().to_string(), JValue::Number(value.into()));
206 }
207
208 fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
209 let n = serde_json::Number::from_f64(value).unwrap();
210 self.0.insert(field.name().to_string(), JValue::Number(n));
211 }
212
213 fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
214 self.0
215 .insert(field.name().to_string(), JValue::Number(value.into()));
216 }
217
218 fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
219 self.0.insert(field.name().to_string(), JValue::Bool(value));
220 }
221}
222
223macro_rules! insert_event {
224 ($table:expr, $conn:ident, $event:ident) => {
225 let mut v: SqlVisitor = Default::default();
226 $event.record(&mut v);
227 let meta = $event.metadata();
228 v.0.insert(
229 "module_path".to_string(),
230 meta.module_path().map(String::from).into(),
231 );
232 v.0.insert("line".to_string(), meta.line().into());
233 v.0.insert("file".to_string(), meta.file().map(String::from).into());
234 $conn.prepare_cached(&$table.insert_stmt)?.execute(
235 serde_rusqlite::to_params_named_with_fields(v, $table.columns)?
236 .to_slice()
237 .as_slice(),
238 )?;
239 };
240}
241
242pub fn insert_event_fields(conn: &Connection, table: &Table, fields: SqlVisitor) -> Result<()> {
245 conn.prepare_cached(&table.insert_stmt)?.execute(
246 serde_rusqlite::to_params_named_with_fields(fields, table.columns)?
247 .to_slice()
248 .as_slice(),
249 )?;
250 Ok(())
251}
252
253impl SqliteLayer {
254 pub fn new() -> Result<Self> {
255 let conn = Connection::open_in_memory()?;
256 Self::setup_connection(conn)
257 }
258
259 pub fn new_with_file(db_path: &str) -> Result<Self> {
260 let conn = Connection::open(db_path)?;
261 Self::setup_connection(conn)
262 }
263
264 fn setup_connection(conn: Connection) -> Result<Self> {
265 for table in ALL_TABLES.iter() {
266 conn.execute(&table.create_table_stmt, [])?;
267 }
268 conn.create_scalar_function(
269 "assert",
270 2,
271 FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
272 move |ctx| {
273 let condition: bool = ctx.get(0)?;
274 let message: String = ctx.get(1)?;
275
276 if !condition {
277 return Err(rusqlite::Error::UserFunctionError(
278 anyhow!("assertion failed:{condition} {message}",).into(),
279 ));
280 }
281
282 Ok(condition)
283 },
284 )?;
285
286 Ok(Self {
287 conn: Arc::new(Mutex::new(conn)),
288 })
289 }
290
291 fn insert_event(&self, event: &Event<'_>) -> Result<()> {
292 let conn = self.conn.lock().unwrap();
293 match (event.metadata().target(), event.metadata().name()) {
294 (TableName::MESSAGES_STR, _) => {
295 insert_event!(TableName::Messages.get_table(), conn, event);
296 }
297 (TableName::ACTOR_LIFECYCLE_STR, _) => {
298 insert_event!(TableName::ActorLifecycle.get_table(), conn, event);
299 }
300 _ => {
301 insert_event!(TableName::LogEvents.get_table(), conn, event);
302 }
303 }
304 Ok(())
305 }
306
307 pub fn connection(&self) -> Arc<Mutex<Connection>> {
308 self.conn.clone()
309 }
310}
311
312impl<S: Subscriber> Layer<S> for SqliteLayer {
313 fn on_event(&self, event: &Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>) {
314 self.insert_event(event).unwrap();
315 }
316}
317
318#[allow(dead_code)]
319fn print_table(conn: &Connection, table_name: TableName) -> Result<()> {
320 let table_name_str = table_name.as_str();
321
322 let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", table_name_str))?;
324 let column_info = stmt.query_map([], |row| {
325 row.get::<_, String>(1) })?;
327
328 let columns: Vec<String> = column_info.collect::<Result<Vec<_>, _>>()?;
329
330 println!("=== {} ===", table_name_str.to_uppercase());
332 println!("{}", columns.join(" | "));
333 println!("{}", "-".repeat(columns.len() * 10));
334
335 let mut stmt = conn.prepare(&format!("SELECT * FROM {}", table_name_str))?;
337 let rows = stmt.query_map([], |row| {
338 let mut values = Vec::new();
339 for (i, column) in columns.iter().enumerate() {
340 let value = if i == 0 && *column == "seq" {
342 match row.get::<_, Option<i64>>(i)? {
344 Some(v) => v.to_string(),
345 None => "NULL".to_string(),
346 }
347 } else {
348 match row.get::<_, Option<String>>(i)? {
350 Some(v) => v,
351 None => "NULL".to_string(),
352 }
353 };
354 values.push(value);
355 }
356 Ok(values.join(" | "))
357 })?;
358
359 for row in rows {
360 println!("{}", row?);
361 }
362 println!();
363 Ok(())
364}
365
366fn init_tracing_subscriber(layer: SqliteLayer) {
367 let handle = RELOAD_HANDLE.lock().unwrap();
368 if let Some(reload_handle) = handle.as_ref() {
369 let _ = reload_handle.reload(layer);
370 } else {
371 tracing_subscriber::registry().with(layer).init();
372 }
373}
374
375pub fn get_reloadable_sqlite_layer() -> Result<reload::Layer<Option<SqliteLayer>, Registry>> {
379 let (layer, reload_handle) = reload::Layer::new(None);
380 let mut handle = RELOAD_HANDLE.lock().unwrap();
381 *handle = Some(reload_handle);
382 Ok(layer)
383}
384
385pub struct SqliteTracing {
387 db_path: Option<PathBuf>,
388 connection: Arc<Mutex<Connection>>,
389}
390
391impl SqliteTracing {
392 pub fn new() -> Result<Self> {
394 let temp_dir = std::env::temp_dir();
395 let file_name = format!("hyperactor_trace_{}.db", std::process::id());
396 let db_path = temp_dir.join(file_name);
397
398 let db_path_str = db_path.to_string_lossy();
399 let layer = SqliteLayer::new_with_file(&db_path_str)?;
400 let connection = layer.connection();
401
402 if let Ok(metadata) = fs::metadata(&db_path) {
405 let mut permissions = metadata.permissions();
406 permissions.set_mode(0o664); let _ = fs::set_permissions(&db_path, permissions);
408 }
409
410 init_tracing_subscriber(layer);
411
412 Ok(Self {
413 db_path: Some(db_path),
414 connection,
415 })
416 }
417
418 pub fn new_in_memory() -> Result<Self> {
420 let layer = SqliteLayer::new()?;
421 let connection = layer.connection();
422
423 init_tracing_subscriber(layer);
424
425 Ok(Self {
426 db_path: None,
427 connection,
428 })
429 }
430
431 pub fn db_path(&self) -> Option<&PathBuf> {
433 self.db_path.as_ref()
434 }
435
436 pub fn connection(&self) -> Arc<Mutex<Connection>> {
438 self.connection.clone()
439 }
440}
441
442impl Drop for SqliteTracing {
443 fn drop(&mut self) {
444 let handle = RELOAD_HANDLE.lock().unwrap();
446 if let Some(reload_handle) = handle.as_ref() {
447 let _ = reload_handle.reload(None);
448 }
449
450 if let Some(db_path) = &self.db_path
452 && db_path.exists()
453 {
454 let _ = fs::remove_file(db_path);
455 }
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use tracing::info;
462
463 use super::*;
464
465 #[test]
466 fn test_sqlite_tracing_with_file() -> Result<()> {
467 let tracing = SqliteTracing::new()?;
468 let conn = tracing.connection();
469
470 info!(target:"messages", test_field = "test_value", "Test msg");
471 info!(target:"log_events", test_field = "test_value", "Test event");
472
473 let count: i64 =
474 conn.lock()
475 .unwrap()
476 .query_row("SELECT COUNT(*) FROM messages", [], |row| row.get(0))?;
477 print_table(&conn.lock().unwrap(), TableName::LogEvents)?;
478 assert!(count > 0);
479
480 assert!(tracing.db_path().is_some());
482 let db_path = tracing.db_path().unwrap();
483 assert!(db_path.exists());
484
485 Ok(())
486 }
487
488 #[test]
489 fn test_sqlite_tracing_in_memory() -> Result<()> {
490 let tracing = SqliteTracing::new_in_memory()?;
491 let conn = tracing.connection();
492
493 info!(target:"messages", test_field = "test_value", "Test event in memory");
494
495 let count: i64 =
496 conn.lock()
497 .unwrap()
498 .query_row("SELECT COUNT(*) FROM messages", [], |row| row.get(0))?;
499 print_table(&conn.lock().unwrap(), TableName::Messages)?;
500 assert!(count > 0);
501
502 assert!(tracing.db_path().is_none());
504
505 Ok(())
506 }
507
508 #[test]
509 fn test_sqlite_tracing_cleanup() -> Result<()> {
510 let db_path = {
511 let tracing = SqliteTracing::new()?;
512 let conn = tracing.connection();
513
514 info!(target:"log_events", test_field = "cleanup_test", "Test cleanup event");
515
516 let count: i64 =
517 conn.lock()
518 .unwrap()
519 .query_row("SELECT COUNT(*) FROM log_events", [], |row| row.get(0))?;
520 assert!(count > 0);
521
522 tracing.db_path().unwrap().clone()
523 }; assert!(!db_path.exists());
527
528 Ok(())
529 }
530
531 #[test]
532 fn test_sqlite_tracing_different_targets() -> Result<()> {
533 let tracing = SqliteTracing::new_in_memory()?;
534 let conn = tracing.connection();
535
536 info!(target:"messages", src = "actor1", dest = "actor2", payload = "test_message", "Message event");
538 info!(target:"actor_lifecycle", actor_id = "123", actor = "TestActor", name = "test", "Lifecycle event");
539 info!(target:"log_events", test_field = "general_event", "General event");
540
541 let message_count: i64 =
543 conn.lock()
544 .unwrap()
545 .query_row("SELECT COUNT(*) FROM messages", [], |row| row.get(0))?;
546 assert_eq!(message_count, 1);
547
548 let lifecycle_count: i64 =
549 conn.lock()
550 .unwrap()
551 .query_row("SELECT COUNT(*) FROM actor_lifecycle", [], |row| row.get(0))?;
552 assert_eq!(lifecycle_count, 1);
553
554 let events_count: i64 =
555 conn.lock()
556 .unwrap()
557 .query_row("SELECT COUNT(*) FROM log_events", [], |row| row.get(0))?;
558 assert_eq!(events_count, 1);
559
560 Ok(())
561 }
562}