@@ -30,9 +30,11 @@ use opentelemetry_sdk::Resource;
3030use opentelemetry_sdk:: logs:: log_processor_with_async_runtime;
3131use opentelemetry_sdk:: propagation:: TraceContextPropagator ;
3232use opentelemetry_sdk:: trace:: span_processor_with_async_runtime;
33+ use rolling_file:: { BasicRollingFileAppender , RollingConditionBasic } ;
3334use std:: io:: { self , Write } ;
3435use std:: path:: PathBuf ;
3536use std:: sync:: { Arc , Mutex } ;
37+ use std:: time:: Duration ;
3638use tracing:: { info, trace} ;
3739use tracing_appender:: non_blocking:: WorkerGuard ;
3840use tracing_opentelemetry:: OpenTelemetryLayer ;
@@ -253,8 +255,21 @@ impl Logging {
253255 let base_directory = PathBuf :: from ( base_directory) ;
254256 let logs_subdirectory = PathBuf :: from ( config. path . clone ( ) ) ;
255257 let logs_path = base_directory. join ( logs_subdirectory. clone ( ) ) ;
256- let file_appender =
257- tracing_appender:: rolling:: hourly ( logs_path. clone ( ) , IGGY_LOG_FILE_PREFIX ) ;
258+ let max_files = Self :: calculate_max_files (
259+ config. max_size . as_bytes_u64 ( ) ,
260+ config. max_size . as_bytes_u64 ( ) ,
261+ ) ;
262+
263+ let condition = RollingConditionBasic :: new ( )
264+ . max_size ( config. max_size . as_bytes_u64 ( ) )
265+ . hourly ( ) ;
266+
267+ let file_appender = BasicRollingFileAppender :: new (
268+ logs_path. join ( IGGY_LOG_FILE_PREFIX ) ,
269+ condition,
270+ max_files,
271+ )
272+ . map_err ( |_| LogError :: FileReloadFailure ) ?;
258273 let ( mut non_blocking_file, file_guard) = tracing_appender:: non_blocking ( file_appender) ;
259274
260275 self . dump_to_file ( & mut non_blocking_file) ;
@@ -284,9 +299,11 @@ impl Logging {
284299 self . init_telemetry ( telemetry_config) ?;
285300 }
286301
302+ self . _install_log_rotation_handler ( config, logs_path. as_ref ( ) ) ;
303+
287304 if let Some ( logs_path) = logs_path {
288305 info ! (
289- "Logging initialized, logs will be stored at: {logs_path:?}. Logs will be rotated hourly . Log filter: {log_filter}."
306+ "Logging initialized, logs will be stored at: {logs_path:?}. Logs will be rotated based on size . Log filter: {log_filter}."
290307 ) ;
291308 } else {
292309 info ! ( "Logging initialized (file output disabled). Log filter: {log_filter}." ) ;
@@ -397,10 +414,6 @@ impl Logging {
397414 Format :: default ( ) . with_thread_names ( true )
398415 }
399416
400- fn _install_log_rotation_handler ( & self ) {
401- todo ! ( "Implement log rotation handler based on size and retention time" ) ;
402- }
403-
404417 fn print_build_info ( ) {
405418 if option_env ! ( "IGGY_CI_BUILD" ) == Some ( "true" ) {
406419 let hash = option_env ! ( "VERGEN_GIT_SHA" ) . unwrap_or ( "unknown" ) ;
@@ -417,6 +430,160 @@ impl Logging {
417430 )
418431 }
419432 }
433+
434+ fn calculate_max_files ( max_total_size_bytes : u64 , max_file_size_bytes : u64 ) -> usize {
435+ if max_file_size_bytes == 0 {
436+ return 10 ;
437+ }
438+
439+ let max_files = max_total_size_bytes / max_file_size_bytes;
440+ max_files. clamp ( 1 , 1000 ) as usize
441+ }
442+
443+ // Use a mutex lock to ensure log rotation operations do not produce race conditions.
444+ fn _install_log_rotation_handler ( & self , config : & LoggingConfig , logs_path : Option < & PathBuf > ) {
445+ if let Some ( logs_path) = logs_path {
446+ let path = logs_path. to_path_buf ( ) ;
447+ let max_size = config. max_size . as_bytes_u64 ( ) ;
448+ let retention = config. retention . get_duration ( ) ;
449+ let rotation_mutex = Arc :: new ( Mutex :: new ( ( ) ) ) ;
450+ let rotation_mutex_clone = Arc :: clone ( & rotation_mutex) ;
451+ std:: thread:: spawn ( move || {
452+ loop {
453+ std:: thread:: sleep ( Duration :: from_secs ( 3600 ) ) ;
454+ match rotation_mutex_clone. lock ( ) {
455+ Ok ( _guard) => {
456+ Self :: cleanup_log_files ( & path, retention, max_size) ;
457+ }
458+ Err ( e) => {
459+ tracing:: warn!( "Failed to acquire log rotation lock: {:?}" , e) ;
460+ }
461+ }
462+ }
463+ } ) ;
464+ }
465+ }
466+
467+ fn cleanup_log_files ( logs_path : & PathBuf , retention : Duration , max_size_bytes : u64 ) {
468+ use std:: fs;
469+ use std:: time:: { SystemTime , UNIX_EPOCH } ;
470+
471+ tracing:: debug!(
472+ "Starting log cleanup for directory: {:?}, retention: {:?}, max_size: {} bytes" ,
473+ logs_path,
474+ retention,
475+ max_size_bytes
476+ ) ;
477+
478+ let entries = match fs:: read_dir ( logs_path) {
479+ Ok ( entries) => entries,
480+ Err ( e) => {
481+ tracing:: warn!( "Failed to read log directory {:?}: {}" , logs_path, e) ;
482+ return ;
483+ }
484+ } ;
485+
486+ let mut file_entries = Vec :: new ( ) ;
487+
488+ for entry in entries. flatten ( ) {
489+ let metadata = match entry. metadata ( ) {
490+ Ok ( metadata) => metadata,
491+ Err ( e) => {
492+ tracing:: warn!( "Failed to get metadata for {:?}: {}" , entry. path( ) , e) ;
493+ continue ;
494+ }
495+ } ;
496+
497+ if !metadata. is_file ( ) {
498+ continue ;
499+ }
500+
501+ let modified = match metadata. modified ( ) {
502+ Ok ( modified) => modified,
503+ Err ( e) => {
504+ tracing:: warn!(
505+ "Failed to get modification time for {:?}: {}" ,
506+ entry. path( ) ,
507+ e
508+ ) ;
509+ continue ;
510+ }
511+ } ;
512+
513+ let elapsed = match modified. duration_since ( UNIX_EPOCH ) {
514+ Ok ( elapsed) => elapsed,
515+ Err ( e) => {
516+ tracing:: warn!(
517+ "Failed to calculate elapsed time for {:?}: {}" ,
518+ entry. path( ) ,
519+ e
520+ ) ;
521+ continue ;
522+ }
523+ } ;
524+
525+ let file_size = metadata. len ( ) ;
526+ file_entries. push ( ( entry, modified, elapsed, file_size) ) ;
527+ }
528+
529+ tracing:: debug!(
530+ "Processed {} log files from directory: {:?}" ,
531+ file_entries. len( ) ,
532+ logs_path
533+ ) ;
534+
535+ let mut removed_files_count = 0 ;
536+
537+ if !retention. is_zero ( ) {
538+ let cutoff = match SystemTime :: now ( ) . duration_since ( UNIX_EPOCH ) {
539+ Ok ( now) => now - retention,
540+ Err ( e) => {
541+ tracing:: warn!( "Failed to get current time: {}" , e) ;
542+ return ;
543+ }
544+ } ;
545+
546+ for ( entry, _, elapsed, _) in & file_entries {
547+ if * elapsed < cutoff {
548+ if let Err ( e) = fs:: remove_file ( entry. path ( ) ) {
549+ tracing:: warn!( "Failed to remove old log file {:?}: {}" , entry. path( ) , e) ;
550+ } else {
551+ tracing:: debug!( "Removed old log file: {:?}" , entry. path( ) ) ;
552+ removed_files_count += 1 ;
553+ }
554+ }
555+ }
556+ }
557+
558+ if max_size_bytes > 0 {
559+ let total_size: u64 = file_entries. iter ( ) . map ( |( _, _, _, size) | * size) . sum ( ) ;
560+
561+ if total_size > max_size_bytes {
562+ file_entries. sort_by_key ( |( _, modified, _, _) | * modified) ;
563+
564+ let mut current_size = total_size;
565+ for ( entry, _, _, file_size) in file_entries {
566+ if current_size <= max_size_bytes {
567+ break ;
568+ }
569+
570+ if let Err ( e) = fs:: remove_file ( entry. path ( ) ) {
571+ tracing:: warn!( "Failed to remove log file {:?}: {}" , entry. path( ) , e) ;
572+ } else {
573+ tracing:: debug!( "Removed log file to control size: {:?}" , entry. path( ) ) ;
574+ current_size = current_size. saturating_sub ( file_size) ;
575+ removed_files_count += 1 ;
576+ }
577+ }
578+ }
579+ }
580+
581+ tracing:: info!(
582+ "Completed log cleanup for directory: {:?}. Removed {} files." ,
583+ logs_path,
584+ removed_files_count
585+ ) ;
586+ }
420587}
421588
422589impl Default for Logging {
0 commit comments