Skip to content

Commit 3a73173

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 3a73173

File tree

3 files changed

+222
-8
lines changed

3 files changed

+222
-8
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: 211 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77

88
//! Logging-related objects.
99
10-
#[cfg(not(feature = "uniffi"))]
1110
use core::fmt;
1211
use std::fs;
1312
use std::io::Write;
1413
use std::path::Path;
1514
use std::sync::Arc;
1615

16+
use bitcoin::secp256k1::PublicKey;
1717
use chrono::Utc;
18+
use lightning::ln::types::ChannelId;
19+
use lightning::types::payment::PaymentHash;
1820
pub use lightning::util::logger::Level as LogLevel;
1921
pub(crate) use lightning::util::logger::{Logger as LdkLogger, Record as LdkRecord};
2022
pub(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

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

tests/common/logging.rs

Lines changed: 8 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::{LogContext, 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,18 @@ 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+
LogContext::new(
167+
record.channel_id.as_ref(),
168+
record.peer_id.as_ref(),
169+
record.payment_hash.as_ref()
170+
),
166171
);
167172

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

0 commit comments

Comments
 (0)