7
7
*/
8
8
9
9
use std:: collections:: HashMap ;
10
+ use std:: fs;
11
+ use std:: path:: PathBuf ;
10
12
use std:: sync:: Arc ;
11
13
use std:: sync:: Mutex ;
12
14
@@ -20,11 +22,19 @@ use serde_json::Value as JValue;
20
22
use serde_rusqlite:: * ;
21
23
use tracing:: Event ;
22
24
use tracing:: Subscriber ;
23
- use tracing:: level_filters:: LevelFilter ;
24
25
use tracing_subscriber:: Layer ;
25
- use tracing_subscriber:: filter :: Targets ;
26
+ use tracing_subscriber:: Registry ;
26
27
use tracing_subscriber:: prelude:: * ;
28
+ use tracing_subscriber:: reload;
27
29
30
+ pub type SqliteReloadHandle = reload:: Handle < Option < SqliteLayer > , Registry > ;
31
+
32
+ lazy_static ! {
33
+ // Reload handle allows us to include a no-op layer during init, but load
34
+ // the layer dynamically during tests.
35
+ static ref RELOAD_HANDLE : Mutex <Option <SqliteReloadHandle >> =
36
+ Mutex :: new( None ) ;
37
+ }
28
38
pub trait TableDef {
29
39
fn name ( & self ) -> & ' static str ;
30
40
fn columns ( & self ) -> & ' static [ & ' static str ] ;
@@ -224,7 +234,15 @@ macro_rules! insert_event {
224
234
impl SqliteLayer {
225
235
pub fn new ( ) -> Result < Self > {
226
236
let conn = Connection :: open_in_memory ( ) ?;
237
+ Self :: setup_connection ( conn)
238
+ }
239
+
240
+ pub fn new_with_file ( db_path : & str ) -> Result < Self > {
241
+ let conn = Connection :: open ( db_path) ?;
242
+ Self :: setup_connection ( conn)
243
+ }
227
244
245
+ fn setup_connection ( conn : Connection ) -> Result < Self > {
228
246
for table in ALL_TABLES . iter ( ) {
229
247
conn. execute ( & table. create_table_stmt , [ ] ) ?;
230
248
}
@@ -326,21 +344,89 @@ fn print_table(conn: &Connection, table_name: TableName) -> Result<()> {
326
344
Ok ( ( ) )
327
345
}
328
346
329
- pub fn with_tracing_db ( ) -> Arc < Mutex < Connection > > {
330
- let layer = SqliteLayer :: new ( ) . unwrap ( ) ;
331
- let conn = layer. connection ( ) ;
332
-
333
- let layer = layer. with_filter (
334
- Targets :: new ( )
335
- . with_default ( LevelFilter :: TRACE )
336
- . with_targets ( vec ! [
337
- ( "tokio" , LevelFilter :: OFF ) ,
338
- ( "opentelemetry" , LevelFilter :: OFF ) ,
339
- ( "runtime" , LevelFilter :: OFF ) ,
340
- ] ) ,
341
- ) ;
342
- tracing_subscriber:: registry ( ) . with ( layer) . init ( ) ;
343
- conn
347
+ fn init_tracing_subscriber ( layer : SqliteLayer ) {
348
+ let handle = RELOAD_HANDLE . lock ( ) . unwrap ( ) ;
349
+ if let Some ( reload_handle) = handle. as_ref ( ) {
350
+ let _ = reload_handle. reload ( layer) ;
351
+ } else {
352
+ tracing_subscriber:: registry ( ) . with ( layer) . init ( ) ;
353
+ }
354
+ }
355
+
356
+ // === API ===
357
+
358
+ // Creates a new reload handler and no-op layer for initialization
359
+ pub fn get_reloadable_sqlite_layer ( ) -> Result < reload:: Layer < Option < SqliteLayer > , Registry > > {
360
+ let ( layer, reload_handle) = reload:: Layer :: new ( None ) ;
361
+ let mut handle = RELOAD_HANDLE . lock ( ) . unwrap ( ) ;
362
+ * handle = Some ( reload_handle) ;
363
+ Ok ( layer)
364
+ }
365
+
366
+ /// RAII guard for SQLite tracing database
367
+ pub struct SqliteTracing {
368
+ db_path : Option < PathBuf > ,
369
+ connection : Arc < Mutex < Connection > > ,
370
+ }
371
+
372
+ impl SqliteTracing {
373
+ /// Create a new SqliteTracing with a temporary file
374
+ pub fn new ( ) -> Result < Self > {
375
+ let temp_dir = std:: env:: temp_dir ( ) ;
376
+ let file_name = format ! ( "hyperactor_trace_{}.db" , std:: process:: id( ) ) ;
377
+ let db_path = temp_dir. join ( file_name) ;
378
+
379
+ let db_path_str = db_path. to_string_lossy ( ) ;
380
+ let layer = SqliteLayer :: new_with_file ( & db_path_str) ?;
381
+ let connection = layer. connection ( ) ;
382
+
383
+ init_tracing_subscriber ( layer) ;
384
+
385
+ Ok ( Self {
386
+ db_path : Some ( db_path) ,
387
+ connection,
388
+ } )
389
+ }
390
+
391
+ /// Create a new SqliteTracing with in-memory database
392
+ pub fn new_in_memory ( ) -> Result < Self > {
393
+ let layer = SqliteLayer :: new ( ) ?;
394
+ let connection = layer. connection ( ) ;
395
+
396
+ init_tracing_subscriber ( layer) ;
397
+
398
+ Ok ( Self {
399
+ db_path : None ,
400
+ connection,
401
+ } )
402
+ }
403
+
404
+ /// Get the path to the temporary database file (None for in-memory)
405
+ pub fn db_path ( & self ) -> Option < & PathBuf > {
406
+ self . db_path . as_ref ( )
407
+ }
408
+
409
+ /// Get a reference to the database connection
410
+ pub fn connection ( & self ) -> Arc < Mutex < Connection > > {
411
+ self . connection . clone ( )
412
+ }
413
+ }
414
+
415
+ impl Drop for SqliteTracing {
416
+ fn drop ( & mut self ) {
417
+ // Reset the layer to None
418
+ let handle = RELOAD_HANDLE . lock ( ) . unwrap ( ) ;
419
+ if let Some ( reload_handle) = handle. as_ref ( ) {
420
+ let _ = reload_handle. reload ( None ) ;
421
+ }
422
+
423
+ // Delete the temporary file if it exists
424
+ if let Some ( db_path) = & self . db_path {
425
+ if db_path. exists ( ) {
426
+ let _ = fs:: remove_file ( db_path) ;
427
+ }
428
+ }
429
+ }
344
430
}
345
431
346
432
#[ cfg( test) ]
@@ -350,8 +436,9 @@ mod tests {
350
436
use super :: * ;
351
437
352
438
#[ test]
353
- fn test_sqlite_layer ( ) -> Result < ( ) > {
354
- let conn = with_tracing_db ( ) ;
439
+ fn test_sqlite_tracing_with_file ( ) -> Result < ( ) > {
440
+ let tracing = SqliteTracing :: new ( ) ?;
441
+ let conn = tracing. connection ( ) ;
355
442
356
443
info ! ( target: "messages" , test_field = "test_value" , "Test msg" ) ;
357
444
info ! ( target: "log_events" , test_field = "test_value" , "Test event" ) ;
@@ -362,6 +449,87 @@ mod tests {
362
449
. query_row ( "SELECT COUNT(*) FROM messages" , [ ] , |row| row. get ( 0 ) ) ?;
363
450
print_table ( & conn. lock ( ) . unwrap ( ) , TableName :: LogEvents ) ?;
364
451
assert ! ( count > 0 ) ;
452
+
453
+ // Verify we have a file path
454
+ assert ! ( tracing. db_path( ) . is_some( ) ) ;
455
+ let db_path = tracing. db_path ( ) . unwrap ( ) ;
456
+ assert ! ( db_path. exists( ) ) ;
457
+
458
+ Ok ( ( ) )
459
+ }
460
+
461
+ #[ test]
462
+ fn test_sqlite_tracing_in_memory ( ) -> Result < ( ) > {
463
+ let tracing = SqliteTracing :: new_in_memory ( ) ?;
464
+ let conn = tracing. connection ( ) ;
465
+
466
+ info ! ( target: "messages" , test_field = "test_value" , "Test event in memory" ) ;
467
+
468
+ let count: i64 =
469
+ conn. lock ( )
470
+ . unwrap ( )
471
+ . query_row ( "SELECT COUNT(*) FROM messages" , [ ] , |row| row. get ( 0 ) ) ?;
472
+ print_table ( & conn. lock ( ) . unwrap ( ) , TableName :: Messages ) ?;
473
+ assert ! ( count > 0 ) ;
474
+
475
+ // Verify we don't have a file path for in-memory
476
+ assert ! ( tracing. db_path( ) . is_none( ) ) ;
477
+
478
+ Ok ( ( ) )
479
+ }
480
+
481
+ #[ test]
482
+ fn test_sqlite_tracing_cleanup ( ) -> Result < ( ) > {
483
+ let db_path = {
484
+ let tracing = SqliteTracing :: new ( ) ?;
485
+ let conn = tracing. connection ( ) ;
486
+
487
+ info ! ( target: "log_events" , test_field = "cleanup_test" , "Test cleanup event" ) ;
488
+
489
+ let count: i64 =
490
+ conn. lock ( )
491
+ . unwrap ( )
492
+ . query_row ( "SELECT COUNT(*) FROM log_events" , [ ] , |row| row. get ( 0 ) ) ?;
493
+ assert ! ( count > 0 ) ;
494
+
495
+ tracing. db_path ( ) . unwrap ( ) . clone ( )
496
+ } ; // tracing goes out of scope here, triggering Drop
497
+
498
+ // File should be cleaned up after Drop
499
+ assert ! ( !db_path. exists( ) ) ;
500
+
501
+ Ok ( ( ) )
502
+ }
503
+
504
+ #[ test]
505
+ fn test_sqlite_tracing_different_targets ( ) -> Result < ( ) > {
506
+ let tracing = SqliteTracing :: new_in_memory ( ) ?;
507
+ let conn = tracing. connection ( ) ;
508
+
509
+ // Test different event targets
510
+ info ! ( target: "messages" , src = "actor1" , dest = "actor2" , payload = "test_message" , "Message event" ) ;
511
+ info ! ( target: "actor_lifecycle" , actor_id = "123" , actor = "TestActor" , name = "test" , "Lifecycle event" ) ;
512
+ info ! ( target: "log_events" , test_field = "general_event" , "General event" ) ;
513
+
514
+ // Check that events went to the right tables
515
+ let message_count: i64 =
516
+ conn. lock ( )
517
+ . unwrap ( )
518
+ . query_row ( "SELECT COUNT(*) FROM messages" , [ ] , |row| row. get ( 0 ) ) ?;
519
+ assert_eq ! ( message_count, 1 ) ;
520
+
521
+ let lifecycle_count: i64 =
522
+ conn. lock ( )
523
+ . unwrap ( )
524
+ . query_row ( "SELECT COUNT(*) FROM actor_lifecycle" , [ ] , |row| row. get ( 0 ) ) ?;
525
+ assert_eq ! ( lifecycle_count, 1 ) ;
526
+
527
+ let events_count: i64 =
528
+ conn. lock ( )
529
+ . unwrap ( )
530
+ . query_row ( "SELECT COUNT(*) FROM log_events" , [ ] , |row| row. get ( 0 ) ) ?;
531
+ assert_eq ! ( events_count, 1 ) ;
532
+
365
533
Ok ( ( ) )
366
534
}
367
535
}
0 commit comments