Skip to content

Commit 42cd66a

Browse files
committed
Implement configurable native log rotate and delete
1 parent 30aef72 commit 42cd66a

6 files changed

Lines changed: 264 additions & 69 deletions

File tree

contrib/ldk-server-config.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ dir_path = "/tmp/ldk-server/" # Path for LDK and BDK data persis
1515
[log]
1616
level = "Debug" # Log level (Error, Warn, Info, Debug, Trace)
1717
#file = "/tmp/ldk-server/ldk-server.log" # Log file path
18+
log_to_file = true # Enable logging to a file (default: true, also logs to both stdout and stderr)
19+
#max_size_mb = 50 # Max size of log file before rotation (default: 50MB)
20+
#rotation_interval_hours = 24 # Max age of log file before rotation (default: 24h)
21+
#max_files = 5 # Number of rotated log files to keep (default: 5)
1822

1923
[tls]
2024
#cert_path = "/path/to/tls.crt" # Path to TLS certificate, by default uses dir_path/tls.crt

docs/configuration.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,12 @@ Where persistent data is stored. Defaults to `~/.ldk-server/` on Linux and
6161

6262
### `[log]`
6363

64-
Log level and file path. The server reopens the log file on `SIGHUP`, which integrates with
65-
standard `logrotate` setups.
64+
Controls logging behavior. By default, `log_to_file` is `true` and logs are also written
65+
to `stdout`/`stderr`.
66+
67+
If `log_to_file` is enabled, the server performs internal rotation and retention
68+
based on `max_size_mb`, `rotation_interval_hours`, and `max_files`. The server will
69+
also reopen the log file on `SIGHUP` for compatibility with external tools like `logrotate`.
6670

6771
### `[tls]`
6872

docs/operations.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,17 @@ The server handles `SIGTERM` and `CTRL-C` (SIGINT). On receipt, it:
2121

2222
### Log Rotation
2323

24-
> **Important:** LDK Server does not rotate or truncate its own log file. Without log rotation
25-
> configured, the log file will grow indefinitely and can eventually fill your disk. A full
26-
> disk can prevent the node from persisting channel state, risking fund loss.
24+
By default, LDK Server logs to `stdout`/`stderr`. When running under `systemd` or Docker,
25+
this allows the environment (e.g., `journald`) to handle persistence, rotation, and
26+
compression automatically.
2727

28-
The server reopens its log file on `SIGHUP`. This integrates with standard `logrotate`. Save
29-
the following config to `/etc/logrotate.d/ldk-server` (adjust the log path to match your
30-
setup):
28+
If you enable `log_to_file` in the configuration, LDK Server will automatically rotate
29+
logs when they exceed 50MB or 24 hours (configurable) and keep the last 5 uncompressed
30+
log files.
31+
32+
If you prefer to use system `logrotate` for file logs, the server still reopens its log
33+
file on `SIGHUP`. Save the following config to `/etc/logrotate.d/ldk-server`
34+
(adjust the log path to match your setup):
3135

3236
```
3337
/var/lib/ldk-server/regtest/ldk-server.log {

ldk-server/src/main.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ use crate::io::persist::{
4848
};
4949
use crate::service::NodeService;
5050
use crate::util::config::{load_config, ArgsConfig, ChainSource};
51-
use crate::util::logger::ServerLogger;
51+
use crate::util::logger::{LogConfig, ServerLogger};
5252
use crate::util::metrics::Metrics;
5353
use crate::util::proto_adapter::{forwarded_payment_to_proto, payment_to_proto};
5454
use crate::util::systemd;
@@ -121,10 +121,20 @@ fn main() {
121121
std::process::exit(-1);
122122
}
123123

124-
if let Err(e) = ServerLogger::init(config_file.log_level, &log_file_path) {
125-
eprintln!("Failed to initialize logger: {e}");
126-
std::process::exit(-1);
127-
}
124+
let log_config = LogConfig {
125+
log_to_file: config_file.log_to_file,
126+
log_max_files: config_file.log_max_files,
127+
log_max_size_bytes: config_file.log_max_size_bytes,
128+
log_rotation_interval_secs: config_file.log_rotation_interval_secs,
129+
};
130+
131+
let logger = match ServerLogger::init(config_file.log_level, &log_file_path, log_config) {
132+
Ok(logger) => logger,
133+
Err(e) => {
134+
eprintln!("Failed to initialize logger: {e}");
135+
std::process::exit(-1);
136+
},
137+
};
128138

129139
let api_key = match load_or_generate_api_key(&network_dir) {
130140
Ok(key) => key,
@@ -252,6 +262,14 @@ fn main() {
252262
}
253263

254264
runtime.block_on(async {
265+
// Register SIGHUP handler for log rotation
266+
let mut sighup_stream = match tokio::signal::unix::signal(SignalKind::hangup()) {
267+
Ok(stream) => stream,
268+
Err(e) => {
269+
error!("Failed to register SIGHUP handler: {e}");
270+
std::process::exit(-1);
271+
}
272+
};
255273

256274
let mut sigterm_stream = match tokio::signal::unix::signal(SignalKind::terminate()) {
257275
Ok(stream) => stream,
@@ -507,6 +525,12 @@ fn main() {
507525
let _ = shutdown_tx.send(true);
508526
break;
509527
}
528+
_ = sighup_stream.recv() => {
529+
info!("Received SIGHUP, reopening log file..");
530+
if let Err(e) = logger.reopen() {
531+
error!("Failed to reopen log file on SIGHUP: {e}");
532+
}
533+
}
510534
_ = sigterm_stream.recv() => {
511535
info!("Received SIGTERM, shutting down..");
512536
let _ = shutdown_tx.send(true);

ldk-server/src/util/config.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ pub struct Config {
5656
pub lsps2_service_config: Option<LSPS2ServiceConfig>,
5757
pub log_level: LevelFilter,
5858
pub log_file_path: Option<String>,
59+
pub log_max_size_bytes: usize,
60+
pub log_rotation_interval_secs: u64,
61+
pub log_max_files: usize,
62+
pub log_to_file: bool,
5963
pub pathfinding_scores_source_url: Option<String>,
6064
pub metrics_enabled: bool,
6165
pub poll_metrics_interval: Option<u64>,
@@ -110,6 +114,10 @@ struct ConfigBuilder {
110114
lsps2: Option<LiquidityConfig>,
111115
log_level: Option<String>,
112116
log_file_path: Option<String>,
117+
log_max_size_mb: Option<u64>,
118+
log_rotation_interval_hours: Option<u64>,
119+
log_max_files: Option<usize>,
120+
log_to_file: Option<bool>,
113121
pathfinding_scores_source_url: Option<String>,
114122
metrics_enabled: Option<bool>,
115123
poll_metrics_interval: Option<u64>,
@@ -158,6 +166,11 @@ impl ConfigBuilder {
158166
if let Some(log) = toml.log {
159167
self.log_level = log.level.or(self.log_level.clone());
160168
self.log_file_path = log.file.or(self.log_file_path.clone());
169+
self.log_max_size_mb = log.max_size_mb.or(self.log_max_size_mb);
170+
self.log_rotation_interval_hours =
171+
log.rotation_interval_hours.or(self.log_rotation_interval_hours);
172+
self.log_max_files = log.max_files.or(self.log_max_files);
173+
self.log_to_file = log.log_to_file.or(self.log_to_file);
161174
}
162175

163176
if let Some(liquidity) = toml.liquidity {
@@ -249,6 +262,22 @@ impl ConfigBuilder {
249262
if let Some(tor_proxy_address) = &args.tor_proxy_address {
250263
self.tor_proxy_address = Some(tor_proxy_address.clone());
251264
}
265+
266+
if let Some(log_max_size_mb) = args.log_max_size_mb {
267+
self.log_max_size_mb = Some(log_max_size_mb);
268+
}
269+
270+
if let Some(log_rotation_interval_hours) = args.log_rotation_interval_hours {
271+
self.log_rotation_interval_hours = Some(log_rotation_interval_hours);
272+
}
273+
274+
if let Some(log_max_files) = args.log_max_files {
275+
self.log_max_files = Some(log_max_files);
276+
}
277+
278+
if args.log_to_file {
279+
self.log_to_file = Some(true);
280+
}
252281
}
253282

254283
fn build(self) -> io::Result<Config> {
@@ -358,6 +387,11 @@ impl ConfigBuilder {
358387
.transpose()?
359388
.unwrap_or(LevelFilter::Debug);
360389

390+
let log_max_size_bytes = self.log_max_size_mb.unwrap_or(50) * 1024 * 1024;
391+
let log_rotation_interval_secs = self.log_rotation_interval_hours.unwrap_or(24) * 60 * 60;
392+
let log_max_files = self.log_max_files.unwrap_or(5);
393+
let log_to_file = self.log_to_file.unwrap_or(true);
394+
361395
let lsps2_client_config = self
362396
.lsps2
363397
.as_ref()
@@ -428,6 +462,10 @@ impl ConfigBuilder {
428462
lsps2_service_config,
429463
log_level,
430464
log_file_path: self.log_file_path,
465+
log_max_size_bytes: log_max_size_bytes as usize,
466+
log_rotation_interval_secs,
467+
log_max_files,
468+
log_to_file,
431469
pathfinding_scores_source_url,
432470
metrics_enabled,
433471
poll_metrics_interval,
@@ -497,6 +535,10 @@ struct EsploraConfig {
497535
struct LogConfig {
498536
level: Option<String>,
499537
file: Option<String>,
538+
max_size_mb: Option<u64>,
539+
rotation_interval_hours: Option<u64>,
540+
max_files: Option<usize>,
541+
log_to_file: Option<bool>,
500542
}
501543

502544
#[derive(Deserialize, Serialize)]
@@ -733,6 +775,34 @@ pub struct ArgsConfig {
733775
)]
734776
node_alias: Option<String>,
735777

778+
#[arg(
779+
long,
780+
env = "LDK_SERVER_LOG_MAX_SIZE_MB",
781+
help = "The maximum size of the log file in MB before rotation. Defaults to 50MB."
782+
)]
783+
log_max_size_mb: Option<u64>,
784+
785+
#[arg(
786+
long,
787+
env = "LDK_SERVER_LOG_ROTATION_INTERVAL_HOURS",
788+
help = "The maximum age of the log file in hours before rotation. Defaults to 24h."
789+
)]
790+
log_rotation_interval_hours: Option<u64>,
791+
792+
#[arg(
793+
long,
794+
env = "LDK_SERVER_LOG_MAX_FILES",
795+
help = "The maximum number of rotated log files to keep. Defaults to 5."
796+
)]
797+
log_max_files: Option<usize>,
798+
799+
#[arg(
800+
long,
801+
env = "LDK_SERVER_LOG_TO_FILE",
802+
help = "The option to enable logging to a file. Defaults to true. If false, logging to file is disabled."
803+
)]
804+
log_to_file: bool,
805+
736806
#[arg(
737807
long,
738808
env = "LDK_SERVER_BITCOIND_RPC_ADDRESS",
@@ -896,6 +966,10 @@ mod tests {
896966
[log]
897967
level = "Trace"
898968
file = "/var/log/ldk-server.log"
969+
max_size_mb = 50
970+
rotation_interval_hours = 24
971+
max_files = 5
972+
log_to_file = true
899973
900974
[bitcoind]
901975
rpc_address = "127.0.0.1:8332"
@@ -941,6 +1015,10 @@ mod tests {
9411015
metrics_username: None,
9421016
metrics_password: None,
9431017
tor_proxy_address: None,
1018+
log_to_file: true,
1019+
log_max_size_mb: Some(50),
1020+
log_rotation_interval_hours: Some(24),
1021+
log_max_files: Some(5),
9441022
}
9451023
}
9461024

@@ -962,6 +1040,10 @@ mod tests {
9621040
metrics_username: None,
9631041
metrics_password: None,
9641042
tor_proxy_address: None,
1043+
log_to_file: true,
1044+
log_max_size_mb: None,
1045+
log_rotation_interval_hours: None,
1046+
log_max_files: None,
9651047
}
9661048
}
9671049

@@ -1029,6 +1111,10 @@ mod tests {
10291111
}),
10301112
log_level: LevelFilter::Trace,
10311113
log_file_path: Some("/var/log/ldk-server.log".to_string()),
1114+
log_max_size_bytes: 50 * 1024 * 1024,
1115+
log_rotation_interval_secs: 24 * 60 * 60,
1116+
log_max_files: 5,
1117+
log_to_file: true,
10321118
pathfinding_scores_source_url: None,
10331119
metrics_enabled: false,
10341120
poll_metrics_interval: None,
@@ -1344,6 +1430,10 @@ mod tests {
13441430
metrics_password: None,
13451431
tor_config: None,
13461432
hrn_config: HumanReadableNamesConfig::default(),
1433+
log_max_size_bytes: 50 * 1024 * 1024,
1434+
log_rotation_interval_secs: 24 * 60 * 60,
1435+
log_max_files: 5,
1436+
log_to_file: true,
13471437
};
13481438

13491439
assert_eq!(config.listening_addrs, expected.listening_addrs);
@@ -1357,6 +1447,10 @@ mod tests {
13571447
assert_eq!(config.pathfinding_scores_source_url, expected.pathfinding_scores_source_url);
13581448
assert_eq!(config.metrics_enabled, expected.metrics_enabled);
13591449
assert_eq!(config.tor_config, expected.tor_config);
1450+
assert_eq!(config.log_max_size_bytes, expected.log_max_size_bytes);
1451+
assert_eq!(config.log_rotation_interval_secs, expected.log_rotation_interval_secs);
1452+
assert_eq!(config.log_max_files, expected.log_max_files);
1453+
assert_eq!(config.log_to_file, expected.log_to_file);
13601454
}
13611455

13621456
#[test]
@@ -1454,6 +1548,10 @@ mod tests {
14541548
proxy_address: SocketAddress::from_str("127.0.0.1:9050").unwrap(),
14551549
}),
14561550
hrn_config: HumanReadableNamesConfig::default(),
1551+
log_max_size_bytes: 50 * 1024 * 1024,
1552+
log_rotation_interval_secs: 24 * 60 * 60,
1553+
log_max_files: 5,
1554+
log_to_file: false,
14571555
};
14581556

14591557
assert_eq!(config.listening_addrs, expected.listening_addrs);

0 commit comments

Comments
 (0)