Skip to content

Commit c74331f

Browse files
joostjagerclaude
andcommitted
Add structured logging context fields to LogRecord
Extend LogRecord with peer_id, channel_id, and payment_hash fields from LDK's Record struct. These structured fields are now available to custom LogWriter implementations and are automatically appended to log messages by the built-in FileWriter and LogFacadeWriter. - Add peer_id, channel_id, payment_hash fields to LogRecord (both uniffi and non-uniffi versions) - Add format_log_context() helper to format fields with truncated hex - Update FileWriter and LogFacadeWriter to append context to messages - Update UDL bindings with new LogRecord fields - Add unit tests for format_log_context and LogFacadeWriter Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 5ea8f3c commit c74331f

File tree

3 files changed

+161
-7
lines changed

3 files changed

+161
-7
lines changed

bindings/ldk_node.udl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ dictionary LogRecord {
8383
string args;
8484
string module_path;
8585
u32 line;
86+
PublicKey? peer_id;
87+
ChannelId? channel_id;
88+
PaymentHash? payment_hash;
8689
};
8790

8891
[Trait, WithForeign]

src/logger.rs

Lines changed: 154 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ use std::io::Write;
1414
use std::path::Path;
1515
use std::sync::Arc;
1616

17+
use bitcoin::secp256k1::PublicKey;
1718
use chrono::Utc;
19+
use lightning::ln::types::ChannelId;
20+
use lightning::types::payment::PaymentHash;
1821
pub use lightning::util::logger::Level as LogLevel;
1922
pub(crate) use lightning::util::logger::{Logger as LdkLogger, Record as LdkRecord};
2023
pub(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

114154
impl 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+
}

tests/common/logging.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::sync::{Arc, Mutex};
22

33
use chrono::Utc;
4-
use ldk_node::logger::{LogLevel, LogRecord, LogWriter};
4+
use ldk_node::logger::{format_log_context, LogLevel, LogRecord, LogWriter};
55
#[cfg(not(feature = "uniffi"))]
66
use log::Record as LogFacadeRecord;
77
use log::{Level as LogFacadeLevel, LevelFilter as LogFacadeLevelFilter, Log as LogFacadeLog};
@@ -156,13 +156,14 @@ impl MultiNodeLogger {
156156
impl LogWriter for MultiNodeLogger {
157157
fn log(&self, record: LogRecord) {
158158
let log = format!(
159-
"[{}] {} {:<5} [{}:{}] {}\n",
159+
"[{}] {} {:<5} [{}:{}] {}{}\n",
160160
self.node_id,
161161
Utc::now().format("%Y-%m-%d %H:%M:%S%.3f"),
162162
record.level.to_string(),
163163
record.module_path,
164164
record.line,
165-
record.args
165+
record.args,
166+
format_log_context(record.channel_id, record.peer_id, record.payment_hash),
166167
);
167168

168169
print!("{}", log);

0 commit comments

Comments
 (0)