88// licenses.
99
1010use std:: fs:: { self , File , OpenOptions } ;
11- use std:: io:: { self , Write } ;
11+ use std:: io:: { self , BufWriter , Write } ;
1212use std:: path:: { Path , PathBuf } ;
13+ use std:: process:: Command ;
1314use std:: sync:: { Arc , Mutex } ;
15+ use std:: thread;
16+ use std:: time:: SystemTime ;
1417
1518use 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 .
2943pub 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