77
88//! Logging-related objects.
99
10- #[ cfg( not( feature = "uniffi" ) ) ]
1110use core:: fmt;
1211use std:: fs;
1312use std:: io:: Write ;
1413use std:: path:: Path ;
1514use std:: sync:: Arc ;
1615
16+ use bitcoin:: secp256k1:: PublicKey ;
1717use chrono:: Utc ;
18+ use lightning:: ln:: types:: ChannelId ;
19+ use lightning:: types:: payment:: PaymentHash ;
1820pub use lightning:: util:: logger:: Level as LogLevel ;
1921pub ( crate ) use lightning:: util:: logger:: { Logger as LdkLogger , Record as LdkRecord } ;
2022pub ( crate ) use lightning:: { log_bytes, log_debug, log_error, log_info, log_trace} ;
@@ -32,6 +34,73 @@ pub struct LogRecord<'a> {
3234 pub module_path : & ' a str ,
3335 /// The line containing the message.
3436 pub line : u32 ,
37+ /// The node id of the peer pertaining to the logged record.
38+ pub peer_id : Option < PublicKey > ,
39+ /// The channel id of the channel pertaining to the logged record.
40+ pub channel_id : Option < ChannelId > ,
41+ /// The payment hash pertaining to the logged record.
42+ pub payment_hash : Option < PaymentHash > ,
43+ }
44+
45+ /// Structured context fields for log messages.
46+ ///
47+ /// Implements `Display` to format context fields (channel_id, peer_id, payment_hash) directly
48+ /// into a formatter, avoiding intermediate heap allocations when used with `format_args!` or
49+ /// `write!` macros.
50+ pub struct LogContext < ' a > {
51+ /// The channel id of the channel pertaining to the logged record.
52+ pub channel_id : Option < & ' a ChannelId > ,
53+ /// The node id of the peer pertaining to the logged record.
54+ pub peer_id : Option < & ' a PublicKey > ,
55+ /// The payment hash pertaining to the logged record.
56+ pub payment_hash : Option < & ' a PaymentHash > ,
57+ }
58+
59+ impl < ' a > LogContext < ' a > {
60+ /// Creates a new `LogContext` from the given fields.
61+ pub fn new (
62+ channel_id : Option < & ' a ChannelId > , peer_id : Option < & ' a PublicKey > ,
63+ payment_hash : Option < & ' a PaymentHash > ,
64+ ) -> Self {
65+ Self { channel_id, peer_id, payment_hash }
66+ }
67+ }
68+
69+ /// Note: LDK's `Record` Display implementation uses fixed-width padded columns and different
70+ /// formatting for test vs production builds. We intentionally use a simpler format here:
71+ /// fields are only included when present (no padding), and the format is consistent across
72+ /// all build configurations.
73+ impl fmt:: Display for LogContext < ' _ > {
74+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
75+ fn truncate ( s : & str ) -> & str {
76+ & s[ ..s. len ( ) . min ( 6 ) ]
77+ }
78+
79+ if self . channel_id . is_none ( ) && self . peer_id . is_none ( ) && self . payment_hash . is_none ( ) {
80+ return Ok ( ( ) ) ;
81+ }
82+
83+ write ! ( f, " (" ) ?;
84+ let mut need_space = false ;
85+ if let Some ( c) = self . channel_id {
86+ write ! ( f, "ch:{}" , truncate( & c. to_string( ) ) ) ?;
87+ need_space = true ;
88+ }
89+ if let Some ( p) = self . peer_id {
90+ if need_space {
91+ write ! ( f, " " ) ?;
92+ }
93+ write ! ( f, "p:{}" , truncate( & p. to_string( ) ) ) ?;
94+ need_space = true ;
95+ }
96+ if let Some ( h) = self . payment_hash {
97+ if need_space {
98+ write ! ( f, " " ) ?;
99+ }
100+ write ! ( f, "h:{}" , truncate( & format!( "{:?}" , h) ) ) ?;
101+ }
102+ write ! ( f, ")" )
103+ }
35104}
36105
37106/// A unit of logging output with metadata to enable filtering `module_path`,
@@ -50,6 +119,12 @@ pub struct LogRecord {
50119 pub module_path : String ,
51120 /// The line containing the message.
52121 pub line : u32 ,
122+ /// The node id of the peer pertaining to the logged record.
123+ pub peer_id : Option < PublicKey > ,
124+ /// The channel id of the channel pertaining to the logged record.
125+ pub channel_id : Option < ChannelId > ,
126+ /// The payment hash pertaining to the logged record.
127+ pub payment_hash : Option < PaymentHash > ,
53128}
54129
55130#[ cfg( feature = "uniffi" ) ]
@@ -60,6 +135,9 @@ impl<'a> From<LdkRecord<'a>> for LogRecord {
60135 args : record. args . to_string ( ) ,
61136 module_path : record. module_path . to_string ( ) ,
62137 line : record. line ,
138+ peer_id : record. peer_id ,
139+ channel_id : record. channel_id ,
140+ payment_hash : record. payment_hash ,
63141 }
64142 }
65143}
@@ -72,6 +150,9 @@ impl<'a> From<LdkRecord<'a>> for LogRecord<'a> {
72150 args : record. args ,
73151 module_path : record. module_path ,
74152 line : record. line ,
153+ peer_id : record. peer_id ,
154+ channel_id : record. channel_id ,
155+ payment_hash : record. payment_hash ,
75156 }
76157 }
77158}
@@ -113,19 +194,26 @@ pub(crate) enum Writer {
113194
114195impl LogWriter for Writer {
115196 fn log ( & self , record : LogRecord ) {
197+ let context = LogContext :: new (
198+ record. channel_id . as_ref ( ) ,
199+ record. peer_id . as_ref ( ) ,
200+ record. payment_hash . as_ref ( ) ,
201+ ) ;
202+
116203 match self {
117204 Writer :: FileWriter { file_path, max_log_level } => {
118205 if record. level < * max_log_level {
119206 return ;
120207 }
121208
122209 let log = format ! (
123- "{} {:<5} [{}:{}] {}\n " ,
210+ "{} {:<5} [{}:{}] {}{} \n " ,
124211 Utc :: now( ) . format( "%Y-%m-%d %H:%M:%S%.3f" ) ,
125212 record. level. to_string( ) ,
126213 record. module_path,
127214 record. line,
128- record. args
215+ record. args,
216+ context,
129217 ) ;
130218
131219 fs:: OpenOptions :: new ( )
@@ -153,7 +241,7 @@ impl LogWriter for Writer {
153241 . target ( record. module_path )
154242 . module_path ( Some ( record. module_path ) )
155243 . line ( Some ( record. line ) )
156- . args ( format_args ! ( "{}" , record. args) )
244+ . args ( format_args ! ( "{}{} " , record. args, context ) )
157245 . build ( ) ,
158246 ) ;
159247 #[ cfg( feature = "uniffi" ) ]
@@ -162,7 +250,7 @@ impl LogWriter for Writer {
162250 . target ( & record. module_path )
163251 . module_path ( Some ( & record. module_path ) )
164252 . line ( Some ( record. line ) )
165- . args ( format_args ! ( "{}" , record. args) )
253+ . args ( format_args ! ( "{}{} " , record. args, context ) )
166254 . build ( ) ,
167255 ) ;
168256 } ,
@@ -222,3 +310,122 @@ impl LdkLogger for Logger {
222310 }
223311 }
224312}
313+
314+ #[ cfg( test) ]
315+ mod tests {
316+ use super :: * ;
317+ use std:: sync:: Mutex ;
318+
319+ /// Tests that LogContext correctly formats all three structured fields
320+ /// (channel_id, peer_id, payment_hash) with space prefixes and 6-char truncation.
321+ #[ test]
322+ fn test_log_context_all_fields ( ) {
323+ let channel_id = ChannelId :: from_bytes ( [
324+ 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x90 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
325+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
326+ 0x00 , 0x00 , 0x00 , 0x00 ,
327+ ] ) ;
328+ let peer_id = PublicKey :: from_slice ( & [
329+ 0x02 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 , 0x45 ,
330+ 0x67 , 0x89 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 ,
331+ 0x45 , 0x67 , 0x89 , 0xab , 0xcd ,
332+ ] )
333+ . unwrap ( ) ;
334+ let payment_hash = PaymentHash ( [
335+ 0xfe , 0xdc , 0xba , 0x98 , 0x76 , 0x54 , 0x32 , 0x10 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
336+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
337+ 0x00 , 0x00 , 0x00 , 0x00 ,
338+ ] ) ;
339+
340+ let context = LogContext :: new ( Some ( & channel_id) , Some ( & peer_id) , Some ( & payment_hash) ) ;
341+
342+ assert_eq ! ( context. to_string( ) , " (ch:abcdef p:02abcd h:fedcba)" ) ;
343+ }
344+
345+ /// Tests that LogContext returns an empty string when no fields are provided.
346+ #[ test]
347+ fn test_log_context_no_fields ( ) {
348+ let context = LogContext :: new ( None , None , None ) ;
349+ assert_eq ! ( context. to_string( ) , "" ) ;
350+ }
351+
352+ /// Tests that LogContext only includes present fields.
353+ #[ test]
354+ fn test_log_context_partial_fields ( ) {
355+ let channel_id = ChannelId :: from_bytes ( [
356+ 0x12 , 0x34 , 0x56 , 0x78 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
357+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
358+ 0x00 , 0x00 , 0x00 , 0x00 ,
359+ ] ) ;
360+
361+ let context = LogContext :: new ( Some ( & channel_id) , None , None ) ;
362+ assert_eq ! ( context. to_string( ) , " (ch:123456)" ) ;
363+ }
364+
365+ /// A minimal log facade logger that captures log output for testing.
366+ struct TestLogger {
367+ log : Arc < Mutex < String > > ,
368+ }
369+
370+ impl log:: Log for TestLogger {
371+ fn enabled ( & self , _metadata : & log:: Metadata ) -> bool {
372+ true
373+ }
374+
375+ fn log ( & self , record : & log:: Record ) {
376+ * self . log . lock ( ) . unwrap ( ) = record. args ( ) . to_string ( ) ;
377+ }
378+
379+ fn flush ( & self ) { }
380+ }
381+
382+ /// Tests that LogFacadeWriter appends structured context fields to the log message.
383+ #[ test]
384+ fn test_log_facade_writer_includes_structured_context ( ) {
385+ let log = Arc :: new ( Mutex :: new ( String :: new ( ) ) ) ;
386+ let test_logger = TestLogger { log : log. clone ( ) } ;
387+
388+ let _ = log:: set_boxed_logger ( Box :: new ( test_logger) ) ;
389+ log:: set_max_level ( log:: LevelFilter :: Trace ) ;
390+
391+ let writer = Writer :: LogFacadeWriter ;
392+
393+ let channel_id = ChannelId :: from_bytes ( [
394+ 0xab , 0xcd , 0xef , 0x12 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
395+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
396+ 0x00 , 0x00 , 0x00 , 0x00 ,
397+ ] ) ;
398+ let peer_id = PublicKey :: from_slice ( & [
399+ 0x02 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 , 0x45 ,
400+ 0x67 , 0x89 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 ,
401+ 0x45 , 0x67 , 0x89 , 0xab , 0xcd ,
402+ ] )
403+ . unwrap ( ) ;
404+
405+ #[ cfg( not( feature = "uniffi" ) ) ]
406+ let record = LogRecord {
407+ level : LogLevel :: Info ,
408+ args : format_args ! ( "Test message" ) ,
409+ module_path : "test_module" ,
410+ line : 42 ,
411+ peer_id : Some ( peer_id) ,
412+ channel_id : Some ( channel_id) ,
413+ payment_hash : None ,
414+ } ;
415+
416+ #[ cfg( feature = "uniffi" ) ]
417+ let record = LogRecord {
418+ level : LogLevel :: Info ,
419+ args : "Test message" . to_string ( ) ,
420+ module_path : "test_module" . to_string ( ) ,
421+ line : 42 ,
422+ peer_id : Some ( peer_id) ,
423+ channel_id : Some ( channel_id) ,
424+ payment_hash : None ,
425+ } ;
426+
427+ writer. log ( record) ;
428+
429+ assert_eq ! ( * log. lock( ) . unwrap( ) , "Test message (ch:abcdef p:02abcd)" ) ;
430+ }
431+ }
0 commit comments