@@ -132,6 +132,10 @@ impl std::fmt::Display for BodyCaptureStatus {
132132/// back out and feeds them into `send_webhook` again.
133133#[ derive( Debug , Clone , Serialize , Deserialize ) ]
134134pub struct AuditEntry {
135+ /// Account for queued and retained data through its final clone. Never part
136+ /// of ClickHouse rows or the durable webhook JSON contract.
137+ #[ serde( skip) ]
138+ memory : Option < std:: sync:: Arc < crate :: resources:: Reservation > > ,
135139 pub id : String ,
136140 #[ serde( skip, default = "default_log_type" ) ]
137141 pub log_type : LogType ,
@@ -407,6 +411,7 @@ impl AuditEntry {
407411 ) ]
408412 pub fn new ( action : impl Into < String > ) -> Self {
409413 Self {
414+ memory : None ,
410415 id : Uuid :: new_v4 ( ) . to_string ( ) ,
411416 log_type : LogType :: Audit ,
412417 user_id : None ,
@@ -430,6 +435,61 @@ impl AuditEntry {
430435 }
431436 }
432437
438+ pub ( super ) fn reserve_memory (
439+ & mut self ,
440+ budget : & std:: sync:: Arc < crate :: resources:: ByteBudget > ,
441+ ) -> bool {
442+ fn value_bytes ( value : & serde_json:: Value ) -> usize {
443+ use serde_json:: Value ;
444+ match value {
445+ Value :: String ( s) => s. capacity ( ) ,
446+ Value :: Array ( values) => values
447+ . capacity ( )
448+ . saturating_mul ( std:: mem:: size_of :: < Value > ( ) )
449+ . saturating_add ( values. iter ( ) . map ( value_bytes) . sum :: < usize > ( ) ) ,
450+ Value :: Object ( values) => values. iter ( ) . fold ( 0usize , |total, ( key, value) | {
451+ total
452+ . saturating_add ( key. capacity ( ) )
453+ . saturating_add ( std:: mem:: size_of :: < ( String , Value ) > ( ) * 2 )
454+ . saturating_add ( value_bytes ( value) )
455+ } ) ,
456+ _ => 0 ,
457+ }
458+ }
459+ let strings = [
460+ Some ( & self . id ) ,
461+ Some ( & self . action ) ,
462+ Some ( & self . created_at ) ,
463+ self . user_id . as_ref ( ) ,
464+ self . user_email . as_ref ( ) ,
465+ self . api_key_id . as_ref ( ) ,
466+ self . api_key_lineage_id . as_ref ( ) ,
467+ self . resource . as_ref ( ) ,
468+ self . resource_id . as_ref ( ) ,
469+ self . ip_address . as_ref ( ) ,
470+ self . user_agent . as_ref ( ) ,
471+ self . trace_id . as_ref ( ) ,
472+ self . session_id . as_ref ( ) ,
473+ self . request_body . as_ref ( ) ,
474+ self . response_body . as_ref ( ) ,
475+ self . body_capture_status . as_ref ( ) ,
476+ ] ;
477+ let bytes = strings
478+ . into_iter ( )
479+ . flatten ( )
480+ . fold ( std:: mem:: size_of :: < Self > ( ) , |n, s| {
481+ n. saturating_add ( s. capacity ( ) )
482+ } )
483+ . saturating_add ( self . detail . as_ref ( ) . map_or ( 0 , value_bytes) ) ;
484+ // Flush mappers and forwarders temporarily clone/serialize one entry.
485+ let mut lease = budget. reserve ( ) ;
486+ if !lease. grow ( bytes. saturating_mul ( 2 ) ) {
487+ return false ;
488+ }
489+ self . memory = Some ( std:: sync:: Arc :: new ( lease) ) ;
490+ true
491+ }
492+
433493 /// Create entry for gateway request logs.
434494 ///
435495 /// Bare constructor — same deprecation reasoning as `::new`. Use
@@ -680,3 +740,39 @@ mod tests {
680740 assert_eq ! ( decoded. detail, original. detail) ;
681741 }
682742}
743+
744+ #[ cfg( test) ]
745+ #[ allow( deprecated) ]
746+ mod memory_tests {
747+ use super :: * ;
748+ use crate :: resources:: ByteBudget ;
749+
750+ #[ test]
751+ fn queue_and_retry_clones_retain_body_admission_without_changing_wire_json ( ) {
752+ let budget = ByteBudget :: new ( 32 * 1024 ) ;
753+ let mut entry = AuditEntry :: new ( "tools.call" ) . request_body ( "x" . repeat ( 4096 ) ) ;
754+ let before = serde_json:: to_value ( & entry) . unwrap ( ) ;
755+ assert ! ( entry. reserve_memory( & budget) ) ;
756+ assert ! ( budget. used( ) >= 8192 ) ;
757+ assert_eq ! ( before, serde_json:: to_value( & entry) . unwrap( ) ) ;
758+ let retained = entry. clone ( ) ;
759+ drop ( entry) ;
760+ assert ! ( budget. used( ) >= 8192 ) ;
761+ drop ( retained) ;
762+ assert_eq ! ( budget. used( ) , 0 ) ;
763+ }
764+
765+ #[ test]
766+ fn a_few_large_entries_hit_the_byte_limit_before_the_count_limit ( ) {
767+ let budget = ByteBudget :: new ( 16 * 1024 ) ;
768+ let mut first = AuditEntry :: new ( "gateway" ) . response_body ( "x" . repeat ( 4096 ) ) ;
769+ let mut second = first. clone ( ) ;
770+ assert ! ( first. reserve_memory( & budget) ) ;
771+ let used = budget. used ( ) ;
772+ assert ! ( !second. reserve_memory( & budget) ) ;
773+ assert_eq ! ( used, budget. used( ) ) ;
774+ drop ( first) ;
775+ assert_eq ! ( budget. used( ) , 0 ) ;
776+ assert ! ( second. reserve_memory( & budget) ) ;
777+ }
778+ }
0 commit comments