Skip to content

Commit 0bd62ff

Browse files
committed
Implement native size/time log rotation with gzip compression
Update the ServerLogger to track file size and age internally, triggering rotation at 50MB or 24 hours. Old logs are now compressed in the background using native OS gzip via `std::thread::spawn`, eliminating the need for the user to configure `logrotate`. Disk writes are also now buffered for better runtime performance. Co-Authored-By: Gemini 3.0 Pro
1 parent 2c0e186 commit 0bd62ff

2 files changed

Lines changed: 98 additions & 66 deletions

File tree

ldk-server/src/main.rs

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,10 @@ fn main() {
121121
std::process::exit(-1);
122122
}
123123

124-
let logger = match ServerLogger::init(config_file.log_level, &log_file_path) {
125-
Ok(logger) => logger,
126-
Err(e) => {
127-
eprintln!("Failed to initialize logger: {e}");
128-
std::process::exit(-1);
129-
},
130-
};
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+
}
131128

132129
let api_key = match load_or_generate_api_key(&network_dir) {
133130
Ok(key) => key,
@@ -254,14 +251,6 @@ fn main() {
254251
}
255252

256253
runtime.block_on(async {
257-
// Register SIGHUP handler for log rotation
258-
let mut sighup_stream = match tokio::signal::unix::signal(SignalKind::hangup()) {
259-
Ok(stream) => stream,
260-
Err(e) => {
261-
error!("Failed to register SIGHUP handler: {e}");
262-
std::process::exit(-1);
263-
}
264-
};
265254

266255
let mut sigterm_stream = match tokio::signal::unix::signal(SignalKind::terminate()) {
267256
Ok(stream) => stream,
@@ -517,11 +506,6 @@ fn main() {
517506
let _ = shutdown_tx.send(true);
518507
break;
519508
}
520-
_ = sighup_stream.recv() => {
521-
if let Err(e) = logger.reopen() {
522-
error!("Failed to reopen log file on SIGHUP: {e}");
523-
}
524-
}
525509
_ = sigterm_stream.recv() => {
526510
info!("Received SIGTERM, shutting down..");
527511
let _ = shutdown_tx.send(true);

ldk-server/src/util/logger.rs

Lines changed: 94 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,26 @@
88
// licenses.
99

1010
use std::fs::{self, File, OpenOptions};
11-
use std::io::{self, Write};
11+
use std::io::{self, BufWriter, Write};
1212
use std::path::{Path, PathBuf};
13+
use std::process::Command;
1314
use std::sync::{Arc, Mutex};
15+
use std::thread;
16+
use std::time::SystemTime;
1417

1518
use log::{Level, LevelFilter, Log, Metadata, Record};
1619

20+
/// Maximum size of the log file before it gets rotated (50 MB)
21+
const MAX_LOG_SIZE_BYTES: usize = 50 * 1024 * 1024;
22+
/// Maximum age of the log file before it gets rotated (24 hours)
23+
const ROTATION_INTERVAL_SECS: u64 = 24 * 60 * 60;
24+
25+
struct LoggerState {
26+
file: BufWriter<File>,
27+
bytes_written: usize,
28+
created_at: SystemTime,
29+
}
30+
1731
/// A logger implementation that writes logs to both stderr and a file.
1832
///
1933
/// The logger formats log messages with RFC3339 timestamps and writes them to:
@@ -25,12 +39,12 @@ use log::{Level, LevelFilter, Log, Metadata, Record};
2539
///
2640
/// Example: `[2025-12-04T10:30:45Z INFO ldk_server:42] Starting up...`
2741
///
28-
/// The logger handles SIGHUP for log rotation by reopening the file handle when signaled.
42+
/// The logger does a native size/time-based rotation and zero-dependency background gzip compression.
2943
pub struct ServerLogger {
3044
/// The maximum log level to display
3145
level: LevelFilter,
32-
/// The file to write logs to, protected by a mutex for thread-safe access
33-
file: Mutex<File>,
46+
/// Groups the file and state in a single Mutex
47+
state: Mutex<LoggerState>,
3448
/// Path to the log file for reopening on SIGHUP
3549
log_file_path: PathBuf,
3650
}
@@ -52,30 +66,60 @@ impl ServerLogger {
5266

5367
let file = open_log_file(log_file_path)?;
5468

69+
// Check existing file metadata to persist size and age across node restarts
70+
let metadata = fs::metadata(log_file_path);
71+
let initial_size = metadata.as_ref().map(|m| m.len() as usize).unwrap_or(0);
72+
let created_at = metadata
73+
.and_then(|m| m.created().or_else(|_| m.modified()))
74+
.unwrap_or_else(|_| SystemTime::now());
75+
5576
let logger = Arc::new(ServerLogger {
5677
level,
57-
file: Mutex::new(file),
5878
log_file_path: log_file_path.to_path_buf(),
79+
state: Mutex::new(LoggerState {
80+
file: BufWriter::new(file),
81+
bytes_written: initial_size,
82+
created_at,
83+
}),
5984
});
6085

6186
log::set_boxed_logger(Box::new(LoggerWrapper(Arc::clone(&logger))))
6287
.map_err(io::Error::other)?;
6388
log::set_max_level(level);
89+
6490
Ok(logger)
6591
}
6692

67-
/// Reopens the log file. Called on SIGHUP for log rotation.
68-
pub fn reopen(&self) -> Result<(), io::Error> {
93+
/// Flushes the current file, renames it with a timestamp, opens a fresh log,
94+
/// and spawns a background thread to compress the old file.
95+
fn rotate(&self, state: &mut LoggerState) -> Result<(), io::Error> {
96+
state.file.flush()?;
97+
98+
let now = chrono::Utc::now().format("%Y-%m-%dT%H-%M-%SZ").to_string();
99+
let mut new_path = self.log_file_path.to_path_buf().into_os_string();
100+
new_path.push(".");
101+
new_path.push(now);
102+
let rotated_path = PathBuf::from(new_path);
103+
104+
fs::rename(&self.log_file_path, &rotated_path)?;
105+
69106
let new_file = open_log_file(&self.log_file_path)?;
70-
match self.file.lock() {
71-
Ok(mut file) => {
72-
// Flush the old buffer before replacing with the new file
73-
file.flush()?;
74-
*file = new_file;
75-
Ok(())
107+
state.file = BufWriter::new(new_file);
108+
109+
// Reset our rotation triggers for the new file
110+
state.bytes_written = 0;
111+
state.created_at = SystemTime::now();
112+
113+
// Spawn independent OS thread to compress the old file using native gzip
114+
thread::spawn(move || match Command::new("gzip").arg("-f").arg(&rotated_path).status() {
115+
Ok(status) if status.success() => {},
116+
Ok(status) => {
117+
eprintln!("Failed to compress log {:?}: exited with {}", rotated_path, status)
76118
},
77-
Err(e) => Err(io::Error::other(format!("Failed to acquire lock: {e}"))),
78-
}
119+
Err(e) => eprintln!("Failed to execute gzip on {:?}: {}", rotated_path, e),
120+
});
121+
122+
Ok(())
79123
}
80124
}
81125

@@ -89,52 +133,56 @@ impl Log for ServerLogger {
89133
let level_str = format_level(record.level());
90134
let line = record.line().unwrap_or(0);
91135

136+
let log_line = format!(
137+
"[{} {} {}:{}] {}",
138+
format_timestamp(),
139+
level_str,
140+
record.target(),
141+
line,
142+
record.args()
143+
);
144+
92145
// Log to console
93-
let _ = match record.level() {
146+
match record.level() {
94147
Level::Error => {
95-
writeln!(
96-
io::stderr(),
97-
"[{} {} {}:{}] {}",
98-
format_timestamp(),
99-
level_str,
100-
record.target(),
101-
line,
102-
record.args()
103-
)
148+
let _ = writeln!(io::stderr(), "{}", log_line);
104149
},
105150
_ => {
106-
writeln!(
107-
io::stdout(),
108-
"[{} {} {}:{}] {}",
109-
format_timestamp(),
110-
level_str,
111-
record.target(),
112-
line,
113-
record.args()
114-
)
151+
let _ = writeln!(io::stdout(), "{}", log_line);
115152
},
116153
};
117154

118155
// Log to file
119-
if let Ok(mut file) = self.file.lock() {
120-
let _ = writeln!(
121-
file,
122-
"[{} {} {}:{}] {}",
123-
format_timestamp(),
124-
level_str,
125-
record.target(),
126-
line,
127-
record.args()
128-
);
156+
let log_bytes = log_line.len() + 1;
157+
158+
if let Ok(mut state) = self.state.lock() {
159+
let mut needs_rotation = false;
160+
161+
if state.bytes_written + log_bytes > MAX_LOG_SIZE_BYTES {
162+
needs_rotation = true;
163+
} else if let Ok(age) = SystemTime::now().duration_since(state.created_at) {
164+
if age.as_secs() > ROTATION_INTERVAL_SECS {
165+
needs_rotation = true;
166+
}
167+
}
168+
169+
if needs_rotation {
170+
if let Err(e) = self.rotate(&mut state) {
171+
eprintln!("Failed to rotate log file: {}", e);
172+
}
173+
}
174+
175+
let _ = writeln!(state.file, "{}", log_line);
176+
state.bytes_written += log_bytes;
129177
}
130178
}
131179
}
132180

133181
fn flush(&self) {
134182
let _ = io::stdout().flush();
135183
let _ = io::stderr().flush();
136-
if let Ok(mut file) = self.file.lock() {
137-
let _ = file.flush();
184+
if let Ok(mut state) = self.state.lock() {
185+
let _ = state.file.flush();
138186
}
139187
}
140188
}

0 commit comments

Comments
 (0)