@@ -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,31 @@ 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+ pub fn format_log_context (
49+ channel_id : Option < ChannelId > , peer_id : Option < PublicKey > , payment_hash : Option < PaymentHash > ,
50+ ) -> String {
51+ fn truncate_hex ( s : & str , len : usize ) -> & str {
52+ & s[ ..s. len ( ) . min ( len) ]
53+ }
54+
55+ let channel_id_str =
56+ channel_id. map ( |c| format ! ( " ch:{}" , truncate_hex( & c. to_string( ) , 6 ) ) ) . unwrap_or_default ( ) ;
57+ let peer_id_str =
58+ peer_id. map ( |p| format ! ( " p:{}" , truncate_hex( & p. to_string( ) , 6 ) ) ) . unwrap_or_default ( ) ;
59+ let payment_hash_str = payment_hash
60+ . map ( |h| format ! ( " h:{}" , truncate_hex( & format!( "{:?}" , h) , 6 ) ) )
61+ . unwrap_or_default ( ) ;
62+ format ! ( "{}{}{}" , channel_id_str, peer_id_str, payment_hash_str)
3563}
3664
3765/// A unit of logging output with metadata to enable filtering `module_path`,
@@ -50,6 +78,12 @@ pub struct LogRecord {
5078 pub module_path : String ,
5179 /// The line containing the message.
5280 pub line : u32 ,
81+ /// The node id of the peer pertaining to the logged record.
82+ pub peer_id : Option < PublicKey > ,
83+ /// The channel id of the channel pertaining to the logged record.
84+ pub channel_id : Option < ChannelId > ,
85+ /// The payment hash pertaining to the logged record.
86+ pub payment_hash : Option < PaymentHash > ,
5387}
5488
5589#[ cfg( feature = "uniffi" ) ]
@@ -60,6 +94,9 @@ impl<'a> From<LdkRecord<'a>> for LogRecord {
6094 args : record. args . to_string ( ) ,
6195 module_path : record. module_path . to_string ( ) ,
6296 line : record. line ,
97+ peer_id : record. peer_id ,
98+ channel_id : record. channel_id ,
99+ payment_hash : record. payment_hash ,
63100 }
64101 }
65102}
@@ -72,6 +109,9 @@ impl<'a> From<LdkRecord<'a>> for LogRecord<'a> {
72109 args : record. args ,
73110 module_path : record. module_path ,
74111 line : record. line ,
112+ peer_id : record. peer_id ,
113+ channel_id : record. channel_id ,
114+ payment_hash : record. payment_hash ,
75115 }
76116 }
77117}
@@ -113,19 +153,22 @@ pub(crate) enum Writer {
113153
114154impl LogWriter for Writer {
115155 fn log ( & self , record : LogRecord ) {
156+ let context = format_log_context ( record. channel_id , record. peer_id , record. payment_hash ) ;
157+
116158 match self {
117159 Writer :: FileWriter { file_path, max_log_level } => {
118160 if record. level < * max_log_level {
119161 return ;
120162 }
121163
122164 let log = format ! (
123- "{} {:<5} [{}:{}] {}\n " ,
165+ "{} {:<5} [{}:{}] {}{} \n " ,
124166 Utc :: now( ) . format( "%Y-%m-%d %H:%M:%S%.3f" ) ,
125167 record. level. to_string( ) ,
126168 record. module_path,
127169 record. line,
128- record. args
170+ record. args,
171+ context,
129172 ) ;
130173
131174 fs:: OpenOptions :: new ( )
@@ -153,7 +196,7 @@ impl LogWriter for Writer {
153196 . target ( record. module_path )
154197 . module_path ( Some ( record. module_path ) )
155198 . line ( Some ( record. line ) )
156- . args ( format_args ! ( "{}" , record. args) )
199+ . args ( format_args ! ( "{}{} " , record. args, context ) )
157200 . build ( ) ,
158201 ) ;
159202 #[ cfg( feature = "uniffi" ) ]
@@ -162,7 +205,7 @@ impl LogWriter for Writer {
162205 . target ( & record. module_path )
163206 . module_path ( Some ( & record. module_path ) )
164207 . line ( Some ( record. line ) )
165- . args ( format_args ! ( "{}" , record. args) )
208+ . args ( format_args ! ( "{}{} " , record. args, context ) )
166209 . build ( ) ,
167210 ) ;
168211 } ,
@@ -222,3 +265,110 @@ impl LdkLogger for Logger {
222265 }
223266 }
224267}
268+
269+ #[ cfg( test) ]
270+ mod tests {
271+ use super :: * ;
272+ use std:: sync:: Mutex ;
273+
274+ /// Tests that format_log_context correctly formats all three structured fields
275+ /// (channel_id, peer_id, payment_hash) with space prefixes and 6-char truncation.
276+ #[ test]
277+ fn test_format_log_context_all_fields ( ) {
278+ let channel_id = ChannelId :: from_bytes ( [
279+ 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x90 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
280+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
281+ 0x00 , 0x00 , 0x00 , 0x00 ,
282+ ] ) ;
283+ let peer_id = PublicKey :: from_slice ( & [
284+ 0x02 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 , 0x45 ,
285+ 0x67 , 0x89 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 ,
286+ 0x45 , 0x67 , 0x89 , 0xab , 0xcd ,
287+ ] )
288+ . unwrap ( ) ;
289+ let payment_hash = PaymentHash ( [
290+ 0xfe , 0xdc , 0xba , 0x98 , 0x76 , 0x54 , 0x32 , 0x10 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
291+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
292+ 0x00 , 0x00 , 0x00 , 0x00 ,
293+ ] ) ;
294+
295+ let result = format_log_context ( Some ( channel_id) , Some ( peer_id) , Some ( payment_hash) ) ;
296+
297+ assert_eq ! ( result, " ch:abcdef p:02abcd h:fedcba" ) ;
298+ }
299+
300+ /// Tests that format_log_context returns an empty string when no fields are provided.
301+ #[ test]
302+ fn test_format_log_context_no_fields ( ) {
303+ let result = format_log_context ( None , None , None ) ;
304+ assert_eq ! ( result, "" ) ;
305+ }
306+
307+ /// Tests that format_log_context only includes present fields.
308+ #[ test]
309+ fn test_format_log_context_partial_fields ( ) {
310+ let channel_id = ChannelId :: from_bytes ( [
311+ 0x12 , 0x34 , 0x56 , 0x78 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
312+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
313+ 0x00 , 0x00 , 0x00 , 0x00 ,
314+ ] ) ;
315+
316+ let result = format_log_context ( Some ( channel_id) , None , None ) ;
317+ assert_eq ! ( result, " ch:123456" ) ;
318+ }
319+
320+ /// A minimal log facade logger that captures log output for testing.
321+ struct TestLogger {
322+ log : Arc < Mutex < String > > ,
323+ }
324+
325+ impl log:: Log for TestLogger {
326+ fn enabled ( & self , _metadata : & log:: Metadata ) -> bool {
327+ true
328+ }
329+
330+ fn log ( & self , record : & log:: Record ) {
331+ * self . log . lock ( ) . unwrap ( ) = record. args ( ) . to_string ( ) ;
332+ }
333+
334+ fn flush ( & self ) { }
335+ }
336+
337+ /// Tests that LogFacadeWriter appends structured context fields to the log message.
338+ #[ test]
339+ fn test_log_facade_writer_includes_structured_context ( ) {
340+ let log = Arc :: new ( Mutex :: new ( String :: new ( ) ) ) ;
341+ let test_logger = TestLogger { log : log. clone ( ) } ;
342+
343+ let _ = log:: set_boxed_logger ( Box :: new ( test_logger) ) ;
344+ log:: set_max_level ( log:: LevelFilter :: Trace ) ;
345+
346+ let writer = Writer :: LogFacadeWriter ;
347+
348+ let channel_id = ChannelId :: from_bytes ( [
349+ 0xab , 0xcd , 0xef , 0x12 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
350+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
351+ 0x00 , 0x00 , 0x00 , 0x00 ,
352+ ] ) ;
353+ let peer_id = PublicKey :: from_slice ( & [
354+ 0x02 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 , 0x45 ,
355+ 0x67 , 0x89 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf1 , 0x23 ,
356+ 0x45 , 0x67 , 0x89 , 0xab , 0xcd ,
357+ ] )
358+ . unwrap ( ) ;
359+
360+ let record = LogRecord {
361+ level : LogLevel :: Info ,
362+ args : format_args ! ( "Test message" ) ,
363+ module_path : "test_module" ,
364+ line : 42 ,
365+ peer_id : Some ( peer_id) ,
366+ channel_id : Some ( channel_id) ,
367+ payment_hash : None ,
368+ } ;
369+
370+ writer. log ( record) ;
371+
372+ assert_eq ! ( * log. lock( ) . unwrap( ) , "Test message ch:abcdef p:02abcd" ) ;
373+ }
374+ }
0 commit comments