1+ use std:: path:: Path ;
12use std:: { fs, net:: Ipv4Addr , time:: Duration } ;
2-
33use tauri:: { AppHandle , Manager } ;
4- use tracing:: { metadata:: LevelFilter , subscriber:: set_global_default} ;
4+ use tracing:: { instrument , metadata:: LevelFilter , subscriber:: set_global_default} ;
55use tracing_appender:: rolling:: { RollingFileAppender , Rotation } ;
66use tracing_subscriber:: { fmt:: format:: FmtSpan , layer:: SubscriberExt , Layer } ;
77
@@ -12,13 +12,22 @@ pub fn init(app_handle: &AppHandle) {
1212 . expect ( "failed to get logs dir" ) ;
1313 fs:: create_dir_all ( & logs_dir) . expect ( "failed to create logs dir" ) ;
1414
15+ let log_prefix = "GitButler" ;
16+ let log_suffix = "log" ;
17+ let max_log_files = 14 ;
18+ remove_old_logs ( & logs_dir) . ok ( ) ;
1519 let file_appender = RollingFileAppender :: builder ( )
1620 . rotation ( Rotation :: DAILY )
17- . max_log_files ( 14 )
18- . filename_prefix ( "GitButler.log" )
21+ . max_log_files ( max_log_files)
22+ . filename_prefix ( log_prefix)
23+ . filename_suffix ( log_suffix)
1924 . build ( & logs_dir)
2025 . expect ( "initializing rolling file appender failed" ) ;
2126 let ( file_writer, guard) = tracing_appender:: non_blocking ( file_appender) ;
27+ // As the file-writer only checks `max_log_files` on file rotation, it bascially never happens.
28+ // Run it now.
29+ prune_old_logs ( & logs_dir, Some ( log_prefix) , Some ( log_suffix) , max_log_files) . ok ( ) ;
30+
2231 app_handle. manage ( guard) ; // keep the guard alive for the lifetime of the app
2332
2433 let format_for_humans = tracing_subscriber:: fmt:: format ( )
@@ -81,3 +90,96 @@ fn get_server_addr(app_handle: &AppHandle) -> (Ipv4Addr, u16) {
8190 } ;
8291 ( Ipv4Addr :: LOCALHOST , port)
8392}
93+
94+ /// Originally based on https://github.com/tokio-rs/tracing/blob/44861cad7a821f08b3c13aba14bb8ddf562b7053/tracing-appender/src/rolling.rs#L571
95+ #[ instrument( err( Debug ) ) ]
96+ fn prune_old_logs (
97+ log_directory : & Path ,
98+ log_filename_prefix : Option < & str > ,
99+ log_filename_suffix : Option < & str > ,
100+ max_files : usize ,
101+ ) -> anyhow:: Result < ( ) > {
102+ let mut files: Vec < _ > = {
103+ let dir = fs:: read_dir ( log_directory) ?;
104+ dir. filter_map ( |entry| {
105+ let entry = entry. ok ( ) ?;
106+ let metadata = entry. metadata ( ) . ok ( ) ?;
107+
108+ // the appender only creates files, not directories or symlinks,
109+ // so we should never delete a dir or symlink.
110+ if !metadata. is_file ( ) {
111+ return None ;
112+ }
113+
114+ let filename = entry. file_name ( ) ;
115+ let filename = filename. to_str ( ) ?;
116+ if let Some ( prefix) = log_filename_prefix {
117+ if !filename. starts_with ( prefix) {
118+ return None ;
119+ }
120+ }
121+
122+ if let Some ( suffix) = log_filename_suffix {
123+ if !filename. ends_with ( suffix) {
124+ return None ;
125+ }
126+ }
127+
128+ let created = metadata. created ( ) . ok ( ) ?;
129+ Some ( ( entry, created) )
130+ } )
131+ . collect ( )
132+ } ;
133+
134+ if files. len ( ) < max_files {
135+ return Ok ( ( ) ) ;
136+ }
137+
138+ // sort the files by their creation timestamps.
139+ files. sort_by_key ( |( _, created_at) | * created_at) ;
140+
141+ // delete files, so that (n-1) files remain, because we will create another log file
142+ for ( file, _) in files. iter ( ) . take ( files. len ( ) - ( max_files - 1 ) ) {
143+ if let Err ( err) = fs:: remove_file ( file. path ( ) ) {
144+ tracing:: warn!(
145+ "Failed to remove extra log file {}: {}" ,
146+ file. path( ) . display( ) ,
147+ err,
148+ ) ;
149+ }
150+ }
151+
152+ Ok ( ( ) )
153+ }
154+
155+ #[ instrument( err( Debug ) ) ]
156+ fn remove_old_logs ( log_directory : & Path ) -> anyhow:: Result < ( ) > {
157+ let dir = fs:: read_dir ( log_directory) ?;
158+ let old_log_files = dir. filter_map ( |entry| {
159+ let entry = entry. ok ( ) ?;
160+ let metadata = entry. metadata ( ) . ok ( ) ?;
161+
162+ if !metadata. is_file ( ) {
163+ return None ;
164+ }
165+
166+ let filename = entry. file_name ( ) ;
167+ let filename = filename. to_str ( ) ?;
168+ if !filename. starts_with ( "GitButler.log" ) {
169+ return None ;
170+ }
171+
172+ Some ( entry. path ( ) )
173+ } ) ;
174+ for file_path in old_log_files {
175+ if let Err ( err) = fs:: remove_file ( & file_path) {
176+ tracing:: warn!(
177+ "Failed to remove old log file {}: {}" ,
178+ file_path. display( ) ,
179+ err,
180+ ) ;
181+ }
182+ }
183+
184+ Ok ( ( ) )
185+ }
0 commit comments