@@ -14,7 +14,10 @@ use std::io::Write;
1414use std:: path:: Path ;
1515use std:: sync:: Arc ;
1616
17+ use bitcoin:: secp256k1:: PublicKey ;
1718use chrono:: Utc ;
19+ use lightning:: ln:: types:: ChannelId ;
20+ use lightning:: types:: payment:: PaymentHash ;
1821pub use lightning:: util:: logger:: Level as LogLevel ;
1922pub ( crate ) use lightning:: util:: logger:: { Logger as LdkLogger , Record as LdkRecord } ;
2023pub ( crate ) use lightning:: { log_bytes, log_debug, log_error, log_info, log_trace} ;
@@ -32,6 +35,37 @@ pub struct LogRecord<'a> {
3235 pub module_path : & ' a str ,
3336 /// The line containing the message.
3437 pub line : u32 ,
38+ /// The node id of the peer pertaining to the logged record.
39+ pub peer_id : Option < PublicKey > ,
40+ /// The channel id of the channel pertaining to the logged record.
41+ pub channel_id : Option < ChannelId > ,
42+ /// The payment hash pertaining to the logged record.
43+ pub payment_hash : Option < PaymentHash > ,
44+ }
45+
46+ /// Formats the structured context fields (channel_id, peer_id, payment_hash) into a string
47+ /// suitable for appending to log messages.
48+ ///
49+ /// Note: LDK's `Record` Display implementation uses fixed-width padded columns and different
50+ /// formatting for test vs production builds. We intentionally use a simpler format here:
51+ /// fields are only included when present (no padding), and the format is consistent across
52+ /// all build configurations. This keeps the implementation straightforward and avoids
53+ /// the complexity of conditional compilation for log formatting.
54+ pub fn format_log_context (
55+ channel_id : Option < ChannelId > , peer_id : Option < PublicKey > , payment_hash : Option < PaymentHash > ,
56+ ) -> String {
57+ fn truncate_hex ( s : & str , len : usize ) -> & str {
58+ & s[ ..s. len ( ) . min ( len) ]
59+ }
60+
61+ let channel_id_str =
62+ channel_id. map ( |c| format ! ( " ch:{}" , truncate_hex( & c. to_string( ) , 6 ) ) ) . unwrap_or_default ( ) ;
63+ let peer_id_str =
64+ peer_id. map ( |p| format ! ( " p:{}" , truncate_hex( & p. to_string( ) , 6 ) ) ) . unwrap_or_default ( ) ;
65+ let payment_hash_str = payment_hash
66+ . map ( |h| format ! ( " h:{}" , truncate_hex( & format!( "{:?}" , h) , 6 ) ) )
67+ . unwrap_or_default ( ) ;
68+ format ! ( "{}{}{}" , channel_id_str, peer_id_str, payment_hash_str)
3569}
3670
3771/// A unit of logging output with metadata to enable filtering `module_path`,
@@ -50,6 +84,12 @@ pub struct LogRecord {
5084 pub module_path : String ,
5185 /// The line containing the message.
5286 pub line : u32 ,
87+ /// The node id of the peer pertaining to the logged record.
88+ pub peer_id : Option < PublicKey > ,
89+ /// The channel id of the channel pertaining to the logged record.
90+ pub channel_id : Option < ChannelId > ,
91+ /// The payment hash pertaining to the logged record.
92+ pub payment_hash : Option < PaymentHash > ,
5393}
5494
5595#[ cfg( feature = "uniffi" ) ]
@@ -60,6 +100,9 @@ impl<'a> From<LdkRecord<'a>> for LogRecord {
60100 args : record. args . to_string ( ) ,
61101 module_path : record. module_path . to_string ( ) ,
62102 line : record. line ,
103+ peer_id : record. peer_id ,
104+ channel_id : record. channel_id ,
105+ payment_hash : record. payment_hash ,
63106 }
64107 }
65108}
@@ -72,6 +115,9 @@ impl<'a> From<LdkRecord<'a>> for LogRecord<'a> {
72115 args : record. args ,
73116 module_path : record. module_path ,
74117 line : record. line ,
118+ peer_id : record. peer_id ,
119+ channel_id : record. channel_id ,
120+ payment_hash : record. payment_hash ,
75121 }
76122 }
77123}
@@ -113,19 +159,22 @@ pub(crate) enum Writer {
113159
114160impl LogWriter for Writer {
115161 fn log ( & self , record : LogRecord ) {
162+ let context = format_log_context ( record. channel_id , record. peer_id , record. payment_hash ) ;
163+
116164 match self {
117165 Writer :: FileWriter { file_path, max_log_level } => {
118166 if record. level < * max_log_level {
119167 return ;
120168 }
121169
122170 let log = format ! (
123- "{} {:<5} [{}:{}] {}\n " ,
171+ "{} {:<5} [{}:{}] {}{} \n " ,
124172 Utc :: now( ) . format( "%Y-%m-%d %H:%M:%S%.3f" ) ,
125173 record. level. to_string( ) ,
126174 record. module_path,
127175 record. line,
128- record. args
176+ record. args,
177+ context,
129178 ) ;
130179
131180 fs:: OpenOptions :: new ( )
@@ -153,7 +202,7 @@ impl LogWriter for Writer {
153202 . target ( record. module_path )
154203 . module_path ( Some ( record. module_path ) )
155204 . line ( Some ( record. line ) )
156- . args ( format_args ! ( "{}" , record. args) )
205+ . args ( format_args ! ( "{}{} " , record. args, context ) )
157206 . build ( ) ,
158207 ) ;
159208 #[ cfg( feature = "uniffi" ) ]
@@ -162,7 +211,7 @@ impl LogWriter for Writer {
162211 . target ( & record. module_path )
163212 . module_path ( Some ( & record. module_path ) )
164213 . line ( Some ( record. line ) )
165- . args ( format_args ! ( "{}" , record. args) )
214+ . args ( format_args ! ( "{}{} " , record. args, context ) )
166215 . build ( ) ,
167216 ) ;
168217 } ,
@@ -222,3 +271,110 @@ impl LdkLogger for Logger {
222271 }
223272 }
224273}
274+
275+ #[ cfg( test) ]
276+ mod tests {
277+ use super :: * ;
278+ use std:: sync:: Mutex ;
279+
280+ /// Tests that format_log_context correctly formats all three structured fields
281+ /// (channel_id, peer_id, payment_hash) with space prefixes and 6-char truncation.
282+ #[ test]
283+ fn test_format_log_context_all_fields ( ) {
284+ let channel_id = ChannelId :: from_bytes ( [
285+ 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x90 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
286+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
287+ 0x00 , 0x00 , 0x00 , 0x00 ,
288+ ] ) ;
289+ let peer_id = PublicKey :: from_slice ( & [
290+ 0x02 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 , 0x45 ,
291+ 0x67 , 0x89 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 ,
292+ 0x45 , 0x67 , 0x89 , 0xab , 0xcd ,
293+ ] )
294+ . unwrap ( ) ;
295+ let payment_hash = PaymentHash ( [
296+ 0xfe , 0xdc , 0xba , 0x98 , 0x76 , 0x54 , 0x32 , 0x10 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
297+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
298+ 0x00 , 0x00 , 0x00 , 0x00 ,
299+ ] ) ;
300+
301+ let result = format_log_context ( Some ( channel_id) , Some ( peer_id) , Some ( payment_hash) ) ;
302+
303+ assert_eq ! ( result, " ch:abcdef p:02abcd h:fedcba" ) ;
304+ }
305+
306+ /// Tests that format_log_context returns an empty string when no fields are provided.
307+ #[ test]
308+ fn test_format_log_context_no_fields ( ) {
309+ let result = format_log_context ( None , None , None ) ;
310+ assert_eq ! ( result, "" ) ;
311+ }
312+
313+ /// Tests that format_log_context only includes present fields.
314+ #[ test]
315+ fn test_format_log_context_partial_fields ( ) {
316+ let channel_id = ChannelId :: from_bytes ( [
317+ 0x12 , 0x34 , 0x56 , 0x78 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
318+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
319+ 0x00 , 0x00 , 0x00 , 0x00 ,
320+ ] ) ;
321+
322+ let result = format_log_context ( Some ( channel_id) , None , None ) ;
323+ assert_eq ! ( result, " ch:123456" ) ;
324+ }
325+
326+ /// A minimal log facade logger that captures log output for testing.
327+ struct TestLogger {
328+ log : Arc < Mutex < String > > ,
329+ }
330+
331+ impl log:: Log for TestLogger {
332+ fn enabled ( & self , _metadata : & log:: Metadata ) -> bool {
333+ true
334+ }
335+
336+ fn log ( & self , record : & log:: Record ) {
337+ * self . log . lock ( ) . unwrap ( ) = record. args ( ) . to_string ( ) ;
338+ }
339+
340+ fn flush ( & self ) { }
341+ }
342+
343+ /// Tests that LogFacadeWriter appends structured context fields to the log message.
344+ #[ test]
345+ fn test_log_facade_writer_includes_structured_context ( ) {
346+ let log = Arc :: new ( Mutex :: new ( String :: new ( ) ) ) ;
347+ let test_logger = TestLogger { log : log. clone ( ) } ;
348+
349+ let _ = log:: set_boxed_logger ( Box :: new ( test_logger) ) ;
350+ log:: set_max_level ( log:: LevelFilter :: Trace ) ;
351+
352+ let writer = Writer :: LogFacadeWriter ;
353+
354+ let channel_id = ChannelId :: from_bytes ( [
355+ 0xab , 0xcd , 0xef , 0x12 , 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+ let peer_id = PublicKey :: from_slice ( & [
360+ 0x02 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 , 0x45 ,
361+ 0x67 , 0x89 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 ,
362+ 0x45 , 0x67 , 0x89 , 0xab , 0xcd ,
363+ ] )
364+ . unwrap ( ) ;
365+
366+ let record = LogRecord {
367+ level : LogLevel :: Info ,
368+ args : format_args ! ( "Test message" ) ,
369+ module_path : "test_module" ,
370+ line : 42 ,
371+ peer_id : Some ( peer_id) ,
372+ channel_id : Some ( channel_id) ,
373+ payment_hash : None ,
374+ } ;
375+
376+ writer. log ( record) ;
377+
378+ assert_eq ! ( * log. lock( ) . unwrap( ) , "Test message ch:abcdef p:02abcd" ) ;
379+ }
380+ }
0 commit comments