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,72 @@ 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+ pub fn new (
61+ channel_id : Option < & ' a ChannelId > , peer_id : Option < & ' a PublicKey > ,
62+ payment_hash : Option < & ' a PaymentHash > ,
63+ ) -> Self {
64+ Self { channel_id, peer_id, payment_hash }
65+ }
66+ }
67+
68+ /// Note: LDK's `Record` Display implementation uses fixed-width padded columns and different
69+ /// formatting for test vs production builds. We intentionally use a simpler format here:
70+ /// fields are only included when present (no padding), and the format is consistent across
71+ /// all build configurations.
72+ impl fmt:: Display for LogContext < ' _ > {
73+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
74+ fn truncate ( s : & str ) -> & str {
75+ & s[ ..s. len ( ) . min ( 6 ) ]
76+ }
77+
78+ if self . channel_id . is_none ( ) && self . peer_id . is_none ( ) && self . payment_hash . is_none ( ) {
79+ return Ok ( ( ) ) ;
80+ }
81+
82+ write ! ( f, " (" ) ?;
83+ let mut need_space = false ;
84+ if let Some ( c) = self . channel_id {
85+ write ! ( f, "ch:{}" , truncate( & c. to_string( ) ) ) ?;
86+ need_space = true ;
87+ }
88+ if let Some ( p) = self . peer_id {
89+ if need_space {
90+ write ! ( f, " " ) ?;
91+ }
92+ write ! ( f, "p:{}" , truncate( & p. to_string( ) ) ) ?;
93+ need_space = true ;
94+ }
95+ if let Some ( h) = self . payment_hash {
96+ if need_space {
97+ write ! ( f, " " ) ?;
98+ }
99+ write ! ( f, "h:{}" , truncate( & format!( "{:?}" , h) ) ) ?;
100+ }
101+ write ! ( f, ")" )
102+ }
35103}
36104
37105/// A unit of logging output with metadata to enable filtering `module_path`,
@@ -50,6 +118,12 @@ pub struct LogRecord {
50118 pub module_path : String ,
51119 /// The line containing the message.
52120 pub line : u32 ,
121+ /// The node id of the peer pertaining to the logged record.
122+ pub peer_id : Option < PublicKey > ,
123+ /// The channel id of the channel pertaining to the logged record.
124+ pub channel_id : Option < ChannelId > ,
125+ /// The payment hash pertaining to the logged record.
126+ pub payment_hash : Option < PaymentHash > ,
53127}
54128
55129#[ cfg( feature = "uniffi" ) ]
@@ -60,6 +134,9 @@ impl<'a> From<LdkRecord<'a>> for LogRecord {
60134 args : record. args . to_string ( ) ,
61135 module_path : record. module_path . to_string ( ) ,
62136 line : record. line ,
137+ peer_id : record. peer_id ,
138+ channel_id : record. channel_id ,
139+ payment_hash : record. payment_hash ,
63140 }
64141 }
65142}
@@ -72,6 +149,9 @@ impl<'a> From<LdkRecord<'a>> for LogRecord<'a> {
72149 args : record. args ,
73150 module_path : record. module_path ,
74151 line : record. line ,
152+ peer_id : record. peer_id ,
153+ channel_id : record. channel_id ,
154+ payment_hash : record. payment_hash ,
75155 }
76156 }
77157}
@@ -113,19 +193,26 @@ pub(crate) enum Writer {
113193
114194impl LogWriter for Writer {
115195 fn log ( & self , record : LogRecord ) {
196+ let context = LogContext :: new (
197+ record. channel_id . as_ref ( ) ,
198+ record. peer_id . as_ref ( ) ,
199+ record. payment_hash . as_ref ( ) ,
200+ ) ;
201+
116202 match self {
117203 Writer :: FileWriter { file_path, max_log_level } => {
118204 if record. level < * max_log_level {
119205 return ;
120206 }
121207
122208 let log = format ! (
123- "{} {:<5} [{}:{}] {}\n " ,
209+ "{} {:<5} [{}:{}] {}{} \n " ,
124210 Utc :: now( ) . format( "%Y-%m-%d %H:%M:%S%.3f" ) ,
125211 record. level. to_string( ) ,
126212 record. module_path,
127213 record. line,
128- record. args
214+ record. args,
215+ context,
129216 ) ;
130217
131218 fs:: OpenOptions :: new ( )
@@ -153,7 +240,7 @@ impl LogWriter for Writer {
153240 . target ( record. module_path )
154241 . module_path ( Some ( record. module_path ) )
155242 . line ( Some ( record. line ) )
156- . args ( format_args ! ( "{}" , record. args) )
243+ . args ( format_args ! ( "{}{} " , record. args, context ) )
157244 . build ( ) ,
158245 ) ;
159246 #[ cfg( feature = "uniffi" ) ]
@@ -162,7 +249,7 @@ impl LogWriter for Writer {
162249 . target ( & record. module_path )
163250 . module_path ( Some ( & record. module_path ) )
164251 . line ( Some ( record. line ) )
165- . args ( format_args ! ( "{}" , record. args) )
252+ . args ( format_args ! ( "{}{} " , record. args, context ) )
166253 . build ( ) ,
167254 ) ;
168255 } ,
@@ -222,3 +309,122 @@ impl LdkLogger for Logger {
222309 }
223310 }
224311}
312+
313+ #[ cfg( test) ]
314+ mod tests {
315+ use super :: * ;
316+ use std:: sync:: Mutex ;
317+
318+ /// Tests that LogContext correctly formats all three structured fields
319+ /// (channel_id, peer_id, payment_hash) with space prefixes and 6-char truncation.
320+ #[ test]
321+ fn test_log_context_all_fields ( ) {
322+ let channel_id = ChannelId :: from_bytes ( [
323+ 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x90 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
324+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
325+ 0x00 , 0x00 , 0x00 , 0x00 ,
326+ ] ) ;
327+ let peer_id = PublicKey :: from_slice ( & [
328+ 0x02 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 , 0x45 ,
329+ 0x67 , 0x89 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 ,
330+ 0x45 , 0x67 , 0x89 , 0xab , 0xcd ,
331+ ] )
332+ . unwrap ( ) ;
333+ let payment_hash = PaymentHash ( [
334+ 0xfe , 0xdc , 0xba , 0x98 , 0x76 , 0x54 , 0x32 , 0x10 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
335+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
336+ 0x00 , 0x00 , 0x00 , 0x00 ,
337+ ] ) ;
338+
339+ let context = LogContext :: new ( Some ( & channel_id) , Some ( & peer_id) , Some ( & payment_hash) ) ;
340+
341+ assert_eq ! ( context. to_string( ) , " (ch:abcdef p:02abcd h:fedcba)" ) ;
342+ }
343+
344+ /// Tests that LogContext returns an empty string when no fields are provided.
345+ #[ test]
346+ fn test_log_context_no_fields ( ) {
347+ let context = LogContext :: new ( None , None , None ) ;
348+ assert_eq ! ( context. to_string( ) , "" ) ;
349+ }
350+
351+ /// Tests that LogContext only includes present fields.
352+ #[ test]
353+ fn test_log_context_partial_fields ( ) {
354+ let channel_id = ChannelId :: from_bytes ( [
355+ 0x12 , 0x34 , 0x56 , 0x78 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
356+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
357+ 0x00 , 0x00 , 0x00 , 0x00 ,
358+ ] ) ;
359+
360+ let context = LogContext :: new ( Some ( & channel_id) , None , None ) ;
361+ assert_eq ! ( context. to_string( ) , " (ch:123456)" ) ;
362+ }
363+
364+ /// A minimal log facade logger that captures log output for testing.
365+ struct TestLogger {
366+ log : Arc < Mutex < String > > ,
367+ }
368+
369+ impl log:: Log for TestLogger {
370+ fn enabled ( & self , _metadata : & log:: Metadata ) -> bool {
371+ true
372+ }
373+
374+ fn log ( & self , record : & log:: Record ) {
375+ * self . log . lock ( ) . unwrap ( ) = record. args ( ) . to_string ( ) ;
376+ }
377+
378+ fn flush ( & self ) { }
379+ }
380+
381+ /// Tests that LogFacadeWriter appends structured context fields to the log message.
382+ #[ test]
383+ fn test_log_facade_writer_includes_structured_context ( ) {
384+ let log = Arc :: new ( Mutex :: new ( String :: new ( ) ) ) ;
385+ let test_logger = TestLogger { log : log. clone ( ) } ;
386+
387+ let _ = log:: set_boxed_logger ( Box :: new ( test_logger) ) ;
388+ log:: set_max_level ( log:: LevelFilter :: Trace ) ;
389+
390+ let writer = Writer :: LogFacadeWriter ;
391+
392+ let channel_id = ChannelId :: from_bytes ( [
393+ 0xab , 0xcd , 0xef , 0x12 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
394+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
395+ 0x00 , 0x00 , 0x00 , 0x00 ,
396+ ] ) ;
397+ let peer_id = PublicKey :: from_slice ( & [
398+ 0x02 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 , 0x45 ,
399+ 0x67 , 0x89 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 ,
400+ 0x45 , 0x67 , 0x89 , 0xab , 0xcd ,
401+ ] )
402+ . unwrap ( ) ;
403+
404+ #[ cfg( not( feature = "uniffi" ) ) ]
405+ let record = LogRecord {
406+ level : LogLevel :: Info ,
407+ args : format_args ! ( "Test message" ) ,
408+ module_path : "test_module" ,
409+ line : 42 ,
410+ peer_id : Some ( peer_id) ,
411+ channel_id : Some ( channel_id) ,
412+ payment_hash : None ,
413+ } ;
414+
415+ #[ cfg( feature = "uniffi" ) ]
416+ let record = LogRecord {
417+ level : LogLevel :: Info ,
418+ args : "Test message" . to_string ( ) ,
419+ module_path : "test_module" . to_string ( ) ,
420+ line : 42 ,
421+ peer_id : Some ( peer_id) ,
422+ channel_id : Some ( channel_id) ,
423+ payment_hash : None ,
424+ } ;
425+
426+ writer. log ( record) ;
427+
428+ assert_eq ! ( * log. lock( ) . unwrap( ) , "Test message (ch:abcdef p:02abcd)" ) ;
429+ }
430+ }
0 commit comments