Skip to content

Commit 628d95f

Browse files
authored
Merge pull request #189 from Anyitechs/log-rotation
Implement configurable native size/time log rotation & deletion
2 parents 7250de4 + 6a1af28 commit 628d95f

6 files changed

Lines changed: 365 additions & 83 deletions

File tree

contrib/ldk-server-config.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ dir_path = "/tmp/ldk-server/" # Path for LDK and BDK data persis
1616
[log]
1717
level = "Debug" # Log level (Error, Warn, Info, Debug, Trace)
1818
#file = "/tmp/ldk-server/ldk-server.log" # Log file path
19+
# Enabling `log_to_file` writes logs to the configured file while still keeping
20+
# the `stdout`/`stderr` logs available too. Logs files are automatically rotated at # `max_size_mb` or `rotation_interval_hours`.
21+
#
22+
# To disable the internal rotation and keep logging to file, set `max_size_mb` # and `rotation_interval_hours` params to `0`.
23+
log_to_file = true # Enable logging to a file (default: true, also logs to both stdout and stderr)
24+
#max_size_mb = 50 # Max size of log file before rotation (default: 50MB)
25+
#rotation_interval_hours = 24 # Max age of log file before rotation (default: 24h)
26+
#max_files = 5 # Number of rotated log files to keep (default: 5)
1927

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

docs/configuration.md

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

6767
### `[log]`
6868

69-
Log level and file path. The server reopens the log file on `SIGHUP`, which integrates with
70-
standard `logrotate` setups.
69+
Controls logging behavior. By default, `log_to_file` is `true` and logs are also written
70+
to `stdout`/`stderr`.
71+
72+
If `log_to_file` is enabled, logs are written to the configured file while still keeping
73+
the `stdout`/`stderr` logs available too. Logs files are automatically rotated at
74+
`max_size_mb` or `rotation_interval_hours`. To disable the internal rotation and keep
75+
logging to file, set `max_size_mb` and `rotation_interval_hours` params to `0`.
76+
77+
The server will also reopen the log file on `SIGHUP` for compatibility with external
78+
tools like `logrotate`.
7179

7280
### `[tls]`
7381

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` 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.
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 writes logs to the configured file while still
29+
keeping the `stdout`/`stderr` logs available too. Logs files are automatically rotated at `max_size_mb` or `rotation_interval_hours`, and the last `max_files` uncompressed log files are retained. But you can disable
30+
the internal rotation and keep logging to file by setting the `max_size_mb` and `rotation_interval_hours`
31+
params to `0`.
32+
33+
If you prefer to use system `logrotate` for file logs, the server still reopens its log file on `SIGHUP`. Save
34+
the following config to `/etc/logrotate.d/ldk-server` (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: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ use crate::io::persist::{
5050
};
5151
use crate::service::NodeService;
5252
use crate::util::config::{load_config, ArgsConfig, ChainSource};
53-
use crate::util::logger::ServerLogger;
53+
use crate::util::logger::{LogConfig, ServerLogger};
5454
use crate::util::metrics::Metrics;
5555
use crate::util::proto_adapter::{forwarded_payment_to_proto, payment_to_proto};
5656
use crate::util::tls::get_or_generate_tls_config;
@@ -112,18 +112,29 @@ fn main() {
112112
Network::Regtest => storage_dir.join("regtest"),
113113
};
114114

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

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

126-
let logger = match ServerLogger::init(config_file.log_level, &log_file_path) {
137+
let logger = match ServerLogger::init(config_file.log_level, log_file_path, log_config) {
127138
Ok(logger) => logger,
128139
Err(e) => {
129140
eprintln!("Failed to initialize logger: {e}");
@@ -635,6 +646,7 @@ fn main() {
635646
break;
636647
}
637648
_ = sighup_stream.recv() => {
649+
info!("Received SIGHUP, reopening log file..");
638650
if let Err(e) = logger.reopen() {
639651
error!("Failed to reopen log file on SIGHUP: {e}");
640652
}
@@ -651,6 +663,7 @@ fn main() {
651663
systemd::notify_stopping();
652664
node.stop().expect("Shutdown should always succeed.");
653665
info!("Shutdown complete..");
666+
log::logger().flush();
654667
}
655668

656669
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 async_payments_role: Option<AsyncPaymentsRole>,
6168
pub metrics_enabled: bool,
@@ -111,6 +118,10 @@ struct ConfigBuilder {
111118
lsps2: Option<LiquidityConfig>,
112119
log_level: Option<String>,
113120
log_file_path: Option<String>,
121+
log_max_size_mb: Option<u64>,
122+
log_rotation_interval_hours: Option<u64>,
123+
log_max_files: Option<usize>,
124+
log_to_file: Option<bool>,
114125
pathfinding_scores_source_url: Option<String>,
115126
async_payments_role: Option<String>,
116127
metrics_enabled: Option<bool>,
@@ -162,6 +173,11 @@ impl ConfigBuilder {
162173
if let Some(log) = toml.log {
163174
self.log_level = log.level.or(self.log_level.clone());
164175
self.log_file_path = log.file.or(self.log_file_path.clone());
176+
self.log_max_size_mb = log.max_size_mb.or(self.log_max_size_mb);
177+
self.log_rotation_interval_hours =
178+
log.rotation_interval_hours.or(self.log_rotation_interval_hours);
179+
self.log_max_files = log.max_files.or(self.log_max_files);
180+
self.log_to_file = log.log_to_file.or(self.log_to_file);
165181
}
166182

167183
if let Some(liquidity) = toml.liquidity {
@@ -257,6 +273,22 @@ impl ConfigBuilder {
257273
if let Some(tor_proxy_address) = &args.tor_proxy_address {
258274
self.tor_proxy_address = Some(tor_proxy_address.clone());
259275
}
276+
277+
if let Some(log_max_size_mb) = args.log_max_size_mb {
278+
self.log_max_size_mb = Some(log_max_size_mb);
279+
}
280+
281+
if let Some(log_rotation_interval_hours) = args.log_rotation_interval_hours {
282+
self.log_rotation_interval_hours = Some(log_rotation_interval_hours);
283+
}
284+
285+
if let Some(log_max_files) = args.log_max_files {
286+
self.log_max_files = Some(log_max_files);
287+
}
288+
289+
if let Some(log_to_file) = args.log_to_file {
290+
self.log_to_file = Some(log_to_file);
291+
}
260292
}
261293

262294
fn build(self) -> io::Result<Config> {
@@ -366,6 +398,14 @@ impl ConfigBuilder {
366398
.transpose()?
367399
.unwrap_or(LevelFilter::Debug);
368400

401+
let log_max_size_bytes =
402+
self.log_max_size_mb.unwrap_or(DEFAULT_LOG_MAX_SIZE_MB) * 1024 * 1024;
403+
let log_rotation_interval_secs =
404+
self.log_rotation_interval_hours.unwrap_or(DEFAULT_LOG_ROTATION_INTERVAL_HOURS)
405+
* 60 * 60;
406+
let log_max_files = self.log_max_files.unwrap_or(DEFAULT_LOG_MAX_FILES);
407+
let log_to_file = self.log_to_file.unwrap_or(true);
408+
369409
let lsps2_client_config = self
370410
.lsps2
371411
.as_ref()
@@ -446,6 +486,10 @@ impl ConfigBuilder {
446486
lsps2_service_config,
447487
log_level,
448488
log_file_path: self.log_file_path,
489+
log_max_size_bytes: log_max_size_bytes as usize,
490+
log_rotation_interval_secs,
491+
log_max_files,
492+
log_to_file,
449493
pathfinding_scores_source_url,
450494
async_payments_role,
451495
metrics_enabled,
@@ -525,6 +569,10 @@ struct EsploraConfig {
525569
struct LogConfig {
526570
level: Option<String>,
527571
file: Option<String>,
572+
max_size_mb: Option<u64>,
573+
rotation_interval_hours: Option<u64>,
574+
max_files: Option<usize>,
575+
log_to_file: Option<bool>,
528576
}
529577

530578
#[derive(Deserialize, Serialize)]
@@ -786,6 +834,34 @@ pub struct ArgsConfig {
786834
)]
787835
node_alias: Option<String>,
788836

837+
#[arg(
838+
long,
839+
env = "LDK_SERVER_LOG_MAX_SIZE_MB",
840+
help = "The maximum size of the log file in MB before rotation. Defaults to 50MB."
841+
)]
842+
log_max_size_mb: Option<u64>,
843+
844+
#[arg(
845+
long,
846+
env = "LDK_SERVER_LOG_ROTATION_INTERVAL_HOURS",
847+
help = "The maximum age of the log file in hours before rotation. Defaults to 24h."
848+
)]
849+
log_rotation_interval_hours: Option<u64>,
850+
851+
#[arg(
852+
long,
853+
env = "LDK_SERVER_LOG_MAX_FILES",
854+
help = "The maximum number of rotated log files to keep. Defaults to 5."
855+
)]
856+
log_max_files: Option<usize>,
857+
858+
#[arg(
859+
long,
860+
env = "LDK_SERVER_LOG_TO_FILE",
861+
help = "The option to enable logging to a file. Defaults to true. If false, logging to file is disabled."
862+
)]
863+
log_to_file: Option<bool>,
864+
789865
#[arg(
790866
long,
791867
env = "LDK_SERVER_BITCOIND_RPC_ADDRESS",
@@ -958,6 +1034,10 @@ mod tests {
9581034
[log]
9591035
level = "Trace"
9601036
file = "/var/log/ldk-server.log"
1037+
max_size_mb = 50
1038+
rotation_interval_hours = 24
1039+
max_files = 5
1040+
log_to_file = true
9611041
9621042
[bitcoind]
9631043
rpc_address = "127.0.0.1:8332"
@@ -1004,6 +1084,10 @@ mod tests {
10041084
metrics_username: None,
10051085
metrics_password: None,
10061086
tor_proxy_address: None,
1087+
log_to_file: Some(true),
1088+
log_max_size_mb: Some(50),
1089+
log_rotation_interval_hours: Some(24),
1090+
log_max_files: Some(5),
10071091
}
10081092
}
10091093

@@ -1026,6 +1110,10 @@ mod tests {
10261110
metrics_username: None,
10271111
metrics_password: None,
10281112
tor_proxy_address: None,
1113+
log_to_file: Some(true),
1114+
log_max_size_mb: None,
1115+
log_rotation_interval_hours: None,
1116+
log_max_files: None,
10291117
}
10301118
}
10311119

@@ -1093,6 +1181,10 @@ mod tests {
10931181
}),
10941182
log_level: LevelFilter::Trace,
10951183
log_file_path: Some("/var/log/ldk-server.log".to_string()),
1184+
log_max_size_bytes: 50 * 1024 * 1024,
1185+
log_rotation_interval_secs: 24 * 60 * 60,
1186+
log_max_files: 5,
1187+
log_to_file: true,
10961188
pathfinding_scores_source_url: None,
10971189
async_payments_role: Some(AsyncPaymentsRole::Client),
10981190
metrics_enabled: false,
@@ -1492,6 +1584,10 @@ mod tests {
14921584
metrics_password: None,
14931585
tor_config: None,
14941586
hrn_config: HumanReadableNamesConfig::default(),
1587+
log_max_size_bytes: 50 * 1024 * 1024,
1588+
log_rotation_interval_secs: 24 * 60 * 60,
1589+
log_max_files: 5,
1590+
log_to_file: true,
14951591
};
14961592

14971593
assert_eq!(config.listening_addrs, expected.listening_addrs);
@@ -1506,6 +1602,10 @@ mod tests {
15061602
assert!(matches!(config.async_payments_role, Some(AsyncPaymentsRole::Server)));
15071603
assert_eq!(config.metrics_enabled, expected.metrics_enabled);
15081604
assert_eq!(config.tor_config, expected.tor_config);
1605+
assert_eq!(config.log_max_size_bytes, expected.log_max_size_bytes);
1606+
assert_eq!(config.log_rotation_interval_secs, expected.log_rotation_interval_secs);
1607+
assert_eq!(config.log_max_files, expected.log_max_files);
1608+
assert_eq!(config.log_to_file, expected.log_to_file);
15091609
}
15101610

15111611
#[test]
@@ -1604,6 +1704,10 @@ mod tests {
16041704
proxy_address: SocketAddress::from_str("127.0.0.1:9050").unwrap(),
16051705
}),
16061706
hrn_config: HumanReadableNamesConfig::default(),
1707+
log_max_size_bytes: 50 * 1024 * 1024,
1708+
log_rotation_interval_secs: 24 * 60 * 60,
1709+
log_max_files: 5,
1710+
log_to_file: false,
16071711
};
16081712

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

0 commit comments

Comments
 (0)