Skip to content

Commit e982113

Browse files
committed
Implement configurable native size/time log rotation & deletion
This introduces a configurable native size/time log rotation & deletion functionality. It updates the `ServerLogger` to track file size and age internally, triggering rotation at a default size of 50MB or 24 hours, if the config values `max_size_mb`/`rotation_interval_hours` are unset. We keep only `max_files` (defaults to 5) rotated log files at a time, deleting older log files. We also now allow users to turn off logging to file, but the default is logging to file and stdout/stderr. Internal rotation can be disabled for file logging by setting the `max_size_mb` and `rotation_interval_hours` params to `0`. Co-Authored-By: Gemini 3.0 Pro
1 parent 50fe752 commit e982113

6 files changed

Lines changed: 344 additions & 83 deletions

File tree

contrib/ldk-server-config.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ 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+
# Enabling `log_to_file` automatically rotate logs at `max_size_mb` or `rotation_interval_hours`.
19+
# To disable the internal rotation and keep logging to file, set `max_size_mb` # and `rotation_interval_hours` params to `0`.
20+
log_to_file = true # Enable logging to a file (default: true, also logs to both stdout and stderr)
21+
#max_size_mb = 50 # Max size of log file before rotation (default: 50MB)
22+
#rotation_interval_hours = 24 # Max age of log file before rotation (default: 24h)
23+
#max_files = 5 # Number of rotated log files to keep (default: 5)
1824

1925
[tls]
2026
#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: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,18 @@ 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.
27-
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):
24+
By default, LDK Server logs to `stdout`/`stderr` and also to file. When running under `systemd` or Docker,
25+
this allows the environment (e.g., `journald`) to handle persistence, rotation, and
26+
compression automatically.
27+
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. But you can disable the internal rotation and keep logging to file by setting
31+
the `max_size_mb` and `rotation_interval_hours` params to `0`.
32+
33+
If you prefer to use system `logrotate` for file logs, the server still reopens its log
34+
file on `SIGHUP`. Save the following config to `/etc/logrotate.d/ldk-server`
35+
(adjust the log path to match your setup):
3136

3237
```
3338
/var/lib/ldk-server/regtest/ldk-server.log {

ldk-server/src/main.rs

Lines changed: 24 additions & 11 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;
@@ -110,18 +110,29 @@ fn main() {
110110
Network::Regtest => storage_dir.join("regtest"),
111111
};
112112

113-
let log_file_path = config_file.log_file_path.map(PathBuf::from).unwrap_or_else(|| {
114-
let mut default_log_path = network_dir.clone();
115-
default_log_path.push("ldk-server.log");
116-
default_log_path
117-
});
113+
let log_file_path = if config_file.log_to_file {
114+
let path = config_file.log_file_path.map(PathBuf::from).unwrap_or_else(|| {
115+
let mut default_log_path = network_dir.clone();
116+
default_log_path.push("ldk-server.log");
117+
default_log_path
118+
});
118119

119-
if log_file_path == storage_dir || log_file_path == network_dir {
120-
eprintln!("Log file path cannot be the same as storage directory path.");
121-
std::process::exit(-1);
122-
}
120+
if path == storage_dir || path == network_dir {
121+
eprintln!("Log file path cannot be the same as storage directory path.");
122+
std::process::exit(-1);
123+
}
124+
Some(path)
125+
} else {
126+
None
127+
};
128+
129+
let log_config = LogConfig {
130+
log_max_files: config_file.log_max_files,
131+
log_max_size_bytes: config_file.log_max_size_bytes,
132+
log_rotation_interval_secs: config_file.log_rotation_interval_secs,
133+
};
123134

124-
let logger = match ServerLogger::init(config_file.log_level, &log_file_path) {
135+
let logger = match ServerLogger::init(config_file.log_level, log_file_path, log_config) {
125136
Ok(logger) => logger,
126137
Err(e) => {
127138
eprintln!("Failed to initialize logger: {e}");
@@ -519,6 +530,7 @@ fn main() {
519530
break;
520531
}
521532
_ = sighup_stream.recv() => {
533+
info!("Received SIGHUP, reopening log file..");
522534
if let Err(e) = logger.reopen() {
523535
error!("Failed to reopen log file on SIGHUP: {e}");
524536
}
@@ -535,6 +547,7 @@ fn main() {
535547
systemd::notify_stopping();
536548
node.stop().expect("Shutdown should always succeed.");
537549
info!("Shutdown complete..");
550+
log::logger().flush();
538551
}
539552

540553
fn send_event_and_upsert_payment(

ldk-server/src/util/config.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ use log::LevelFilter;
2323
use serde::{Deserialize, Serialize};
2424

2525
const DEFAULT_GRPC_SERVICE_ADDRESS: &str = "127.0.0.1:3536";
26+
const DEFAULT_LOG_MAX_SIZE_MB: u64 = 50;
27+
const DEFAULT_LOG_ROTATION_INTERVAL_HOURS: u64 = 24;
28+
const DEFAULT_LOG_MAX_FILES: usize = 5;
2629

2730
#[cfg(not(test))]
2831
const DEFAULT_CONFIG_FILE: &str = "config.toml";
@@ -56,6 +59,10 @@ pub struct Config {
5659
pub lsps2_service_config: Option<LSPS2ServiceConfig>,
5760
pub log_level: LevelFilter,
5861
pub log_file_path: Option<String>,
62+
pub log_max_size_bytes: usize,
63+
pub log_rotation_interval_secs: u64,
64+
pub log_max_files: usize,
65+
pub log_to_file: bool,
5966
pub pathfinding_scores_source_url: Option<String>,
6067
pub metrics_enabled: bool,
6168
pub poll_metrics_interval: Option<u64>,
@@ -110,6 +117,10 @@ struct ConfigBuilder {
110117
lsps2: Option<LiquidityConfig>,
111118
log_level: Option<String>,
112119
log_file_path: Option<String>,
120+
log_max_size_mb: Option<u64>,
121+
log_rotation_interval_hours: Option<u64>,
122+
log_max_files: Option<usize>,
123+
log_to_file: Option<bool>,
113124
pathfinding_scores_source_url: Option<String>,
114125
metrics_enabled: Option<bool>,
115126
poll_metrics_interval: Option<u64>,
@@ -158,6 +169,11 @@ impl ConfigBuilder {
158169
if let Some(log) = toml.log {
159170
self.log_level = log.level.or(self.log_level.clone());
160171
self.log_file_path = log.file.or(self.log_file_path.clone());
172+
self.log_max_size_mb = log.max_size_mb.or(self.log_max_size_mb);
173+
self.log_rotation_interval_hours =
174+
log.rotation_interval_hours.or(self.log_rotation_interval_hours);
175+
self.log_max_files = log.max_files.or(self.log_max_files);
176+
self.log_to_file = log.log_to_file.or(self.log_to_file);
161177
}
162178

163179
if let Some(liquidity) = toml.liquidity {
@@ -249,6 +265,22 @@ impl ConfigBuilder {
249265
if let Some(tor_proxy_address) = &args.tor_proxy_address {
250266
self.tor_proxy_address = Some(tor_proxy_address.clone());
251267
}
268+
269+
if let Some(log_max_size_mb) = args.log_max_size_mb {
270+
self.log_max_size_mb = Some(log_max_size_mb);
271+
}
272+
273+
if let Some(log_rotation_interval_hours) = args.log_rotation_interval_hours {
274+
self.log_rotation_interval_hours = Some(log_rotation_interval_hours);
275+
}
276+
277+
if let Some(log_max_files) = args.log_max_files {
278+
self.log_max_files = Some(log_max_files);
279+
}
280+
281+
if let Some(log_file_path) = args.log_to_file {
282+
self.log_to_file = Some(log_file_path);
283+
}
252284
}
253285

254286
fn build(self) -> io::Result<Config> {
@@ -358,6 +390,14 @@ impl ConfigBuilder {
358390
.transpose()?
359391
.unwrap_or(LevelFilter::Debug);
360392

393+
let log_max_size_bytes =
394+
self.log_max_size_mb.unwrap_or(DEFAULT_LOG_MAX_SIZE_MB) * 1024 * 1024;
395+
let log_rotation_interval_secs =
396+
self.log_rotation_interval_hours.unwrap_or(DEFAULT_LOG_ROTATION_INTERVAL_HOURS)
397+
* 60 * 60;
398+
let log_max_files = self.log_max_files.unwrap_or(DEFAULT_LOG_MAX_FILES);
399+
let log_to_file = self.log_to_file.unwrap_or(true);
400+
361401
let lsps2_client_config = self
362402
.lsps2
363403
.as_ref()
@@ -428,6 +468,10 @@ impl ConfigBuilder {
428468
lsps2_service_config,
429469
log_level,
430470
log_file_path: self.log_file_path,
471+
log_max_size_bytes: log_max_size_bytes as usize,
472+
log_rotation_interval_secs,
473+
log_max_files,
474+
log_to_file,
431475
pathfinding_scores_source_url,
432476
metrics_enabled,
433477
poll_metrics_interval,
@@ -497,6 +541,10 @@ struct EsploraConfig {
497541
struct LogConfig {
498542
level: Option<String>,
499543
file: Option<String>,
544+
max_size_mb: Option<u64>,
545+
rotation_interval_hours: Option<u64>,
546+
max_files: Option<usize>,
547+
log_to_file: Option<bool>,
500548
}
501549

502550
#[derive(Deserialize, Serialize)]
@@ -733,6 +781,34 @@ pub struct ArgsConfig {
733781
)]
734782
node_alias: Option<String>,
735783

784+
#[arg(
785+
long,
786+
env = "LDK_SERVER_LOG_MAX_SIZE_MB",
787+
help = "The maximum size of the log file in MB before rotation. Defaults to 50MB."
788+
)]
789+
log_max_size_mb: Option<u64>,
790+
791+
#[arg(
792+
long,
793+
env = "LDK_SERVER_LOG_ROTATION_INTERVAL_HOURS",
794+
help = "The maximum age of the log file in hours before rotation. Defaults to 24h."
795+
)]
796+
log_rotation_interval_hours: Option<u64>,
797+
798+
#[arg(
799+
long,
800+
env = "LDK_SERVER_LOG_MAX_FILES",
801+
help = "The maximum number of rotated log files to keep. Defaults to 5."
802+
)]
803+
log_max_files: Option<usize>,
804+
805+
#[arg(
806+
long,
807+
env = "LDK_SERVER_LOG_TO_FILE",
808+
help = "The option to enable logging to a file. Defaults to true. If false, logging to file is disabled."
809+
)]
810+
log_to_file: Option<bool>,
811+
736812
#[arg(
737813
long,
738814
env = "LDK_SERVER_BITCOIND_RPC_ADDRESS",
@@ -896,6 +972,10 @@ mod tests {
896972
[log]
897973
level = "Trace"
898974
file = "/var/log/ldk-server.log"
975+
max_size_mb = 50
976+
rotation_interval_hours = 24
977+
max_files = 5
978+
log_to_file = true
899979
900980
[bitcoind]
901981
rpc_address = "127.0.0.1:8332"
@@ -941,6 +1021,10 @@ mod tests {
9411021
metrics_username: None,
9421022
metrics_password: None,
9431023
tor_proxy_address: None,
1024+
log_to_file: Some(true),
1025+
log_max_size_mb: Some(50),
1026+
log_rotation_interval_hours: Some(24),
1027+
log_max_files: Some(5),
9441028
}
9451029
}
9461030

@@ -962,6 +1046,10 @@ mod tests {
9621046
metrics_username: None,
9631047
metrics_password: None,
9641048
tor_proxy_address: None,
1049+
log_to_file: Some(true),
1050+
log_max_size_mb: None,
1051+
log_rotation_interval_hours: None,
1052+
log_max_files: None,
9651053
}
9661054
}
9671055

@@ -1029,6 +1117,10 @@ mod tests {
10291117
}),
10301118
log_level: LevelFilter::Trace,
10311119
log_file_path: Some("/var/log/ldk-server.log".to_string()),
1120+
log_max_size_bytes: 50 * 1024 * 1024,
1121+
log_rotation_interval_secs: 24 * 60 * 60,
1122+
log_max_files: 5,
1123+
log_to_file: true,
10321124
pathfinding_scores_source_url: None,
10331125
metrics_enabled: false,
10341126
poll_metrics_interval: None,
@@ -1344,6 +1436,10 @@ mod tests {
13441436
metrics_password: None,
13451437
tor_config: None,
13461438
hrn_config: HumanReadableNamesConfig::default(),
1439+
log_max_size_bytes: 50 * 1024 * 1024,
1440+
log_rotation_interval_secs: 24 * 60 * 60,
1441+
log_max_files: 5,
1442+
log_to_file: true,
13471443
};
13481444

13491445
assert_eq!(config.listening_addrs, expected.listening_addrs);
@@ -1357,6 +1453,10 @@ mod tests {
13571453
assert_eq!(config.pathfinding_scores_source_url, expected.pathfinding_scores_source_url);
13581454
assert_eq!(config.metrics_enabled, expected.metrics_enabled);
13591455
assert_eq!(config.tor_config, expected.tor_config);
1456+
assert_eq!(config.log_max_size_bytes, expected.log_max_size_bytes);
1457+
assert_eq!(config.log_rotation_interval_secs, expected.log_rotation_interval_secs);
1458+
assert_eq!(config.log_max_files, expected.log_max_files);
1459+
assert_eq!(config.log_to_file, expected.log_to_file);
13601460
}
13611461

13621462
#[test]
@@ -1454,6 +1554,10 @@ mod tests {
14541554
proxy_address: SocketAddress::from_str("127.0.0.1:9050").unwrap(),
14551555
}),
14561556
hrn_config: HumanReadableNamesConfig::default(),
1557+
log_max_size_bytes: 50 * 1024 * 1024,
1558+
log_rotation_interval_secs: 24 * 60 * 60,
1559+
log_max_files: 5,
1560+
log_to_file: false,
14571561
};
14581562

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

0 commit comments

Comments
 (0)