@@ -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,48 @@ 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+
259+ if let Err ( e) = std:: fs:: create_dir_all ( & logs_path) {
260+ tracing:: warn!( "Failed to create logs directory {:?}: {}" , logs_path, e) ;
261+ return Err ( LogError :: FileReloadFailure ) ;
262+ }
263+
264+ // Check available disk space (at least 10MB)
265+ let min_disk_space: u64 = 10 * 1024 * 1024 ; // 10MB
266+ if let Ok ( available_space) = fs2:: available_space ( & logs_path) {
267+ if available_space < min_disk_space {
268+ tracing:: warn!(
269+ "Low disk space for logs. Available: {} bytes, Recommended: {} bytes" ,
270+ available_space,
271+ min_disk_space
272+ ) ;
273+ }
274+ } else {
275+ tracing:: warn!(
276+ "Failed to check available disk space for logs directory: {:?}" ,
277+ logs_path
278+ ) ;
279+ }
280+
281+ let max_files = Self :: calculate_max_files (
282+ config. max_size . as_bytes_u64 ( ) ,
283+ config. max_size . as_bytes_u64 ( ) ,
284+ ) ;
285+
286+ let condition = RollingConditionBasic :: new ( )
287+ . max_size ( config. max_size . as_bytes_u64 ( ) )
288+ . hourly ( ) ;
289+
290+ let file_appender = BasicRollingFileAppender :: new (
291+ logs_path. join ( IGGY_LOG_FILE_PREFIX ) ,
292+ condition,
293+ max_files,
294+ )
295+ . map_err ( |e| {
296+ tracing:: error!( "Failed to create file appender: {}" , e) ;
297+ LogError :: FileReloadFailure
298+ } ) ?;
299+
258300 let ( mut non_blocking_file, file_guard) = tracing_appender:: non_blocking ( file_appender) ;
259301
260302 self . dump_to_file ( & mut non_blocking_file) ;
@@ -284,9 +326,11 @@ impl Logging {
284326 self . init_telemetry ( telemetry_config) ?;
285327 }
286328
329+ self . _install_log_rotation_handler ( config, logs_path. as_ref ( ) ) ;
330+
287331 if let Some ( logs_path) = logs_path {
288332 info ! (
289- "Logging initialized, logs will be stored at: {logs_path:?}. Logs will be rotated hourly . Log filter: {log_filter}."
333+ "Logging initialized, logs will be stored at: {logs_path:?}. Logs will be rotated based on size . Log filter: {log_filter}."
290334 ) ;
291335 } else {
292336 info ! ( "Logging initialized (file output disabled). Log filter: {log_filter}." ) ;
@@ -397,10 +441,6 @@ impl Logging {
397441 Format :: default ( ) . with_thread_names ( true )
398442 }
399443
400- fn _install_log_rotation_handler ( & self ) {
401- todo ! ( "Implement log rotation handler based on size and retention time" ) ;
402- }
403-
404444 fn print_build_info ( ) {
405445 if option_env ! ( "IGGY_CI_BUILD" ) == Some ( "true" ) {
406446 let hash = option_env ! ( "VERGEN_GIT_SHA" ) . unwrap_or ( "unknown" ) ;
@@ -417,6 +457,160 @@ impl Logging {
417457 )
418458 }
419459 }
460+
461+ fn calculate_max_files ( max_total_size_bytes : u64 , max_file_size_bytes : u64 ) -> usize {
462+ if max_file_size_bytes == 0 {
463+ return 10 ;
464+ }
465+
466+ let max_files = max_total_size_bytes / max_file_size_bytes;
467+ max_files. clamp ( 1 , 1000 ) as usize
468+ }
469+
470+ // Use a mutex lock to ensure log rotation operations do not produce race conditions.
471+ fn _install_log_rotation_handler ( & self , config : & LoggingConfig , logs_path : Option < & PathBuf > ) {
472+ if let Some ( logs_path) = logs_path {
473+ let path = logs_path. to_path_buf ( ) ;
474+ let max_size = config. max_size . as_bytes_u64 ( ) ;
475+ let retention = config. retention . get_duration ( ) ;
476+ let rotation_mutex = Arc :: new ( Mutex :: new ( ( ) ) ) ;
477+ let rotation_mutex_clone = Arc :: clone ( & rotation_mutex) ;
478+ std:: thread:: spawn ( move || {
479+ loop {
480+ std:: thread:: sleep ( Duration :: from_secs ( 3600 ) ) ;
481+ match rotation_mutex_clone. lock ( ) {
482+ Ok ( _guard) => {
483+ Self :: cleanup_log_files ( & path, retention, max_size) ;
484+ }
485+ Err ( e) => {
486+ tracing:: warn!( "Failed to acquire log rotation lock: {:?}" , e) ;
487+ }
488+ }
489+ }
490+ } ) ;
491+ }
492+ }
493+
494+ fn cleanup_log_files ( logs_path : & PathBuf , retention : Duration , max_size_bytes : u64 ) {
495+ use std:: fs;
496+ use std:: time:: { SystemTime , UNIX_EPOCH } ;
497+
498+ tracing:: debug!(
499+ "Starting log cleanup for directory: {:?}, retention: {:?}, max_size: {} bytes" ,
500+ logs_path,
501+ retention,
502+ max_size_bytes
503+ ) ;
504+
505+ let entries = match fs:: read_dir ( logs_path) {
506+ Ok ( entries) => entries,
507+ Err ( e) => {
508+ tracing:: warn!( "Failed to read log directory {:?}: {}" , logs_path, e) ;
509+ return ;
510+ }
511+ } ;
512+
513+ let mut file_entries = Vec :: new ( ) ;
514+
515+ for entry in entries. flatten ( ) {
516+ let metadata = match entry. metadata ( ) {
517+ Ok ( metadata) => metadata,
518+ Err ( e) => {
519+ tracing:: warn!( "Failed to get metadata for {:?}: {}" , entry. path( ) , e) ;
520+ continue ;
521+ }
522+ } ;
523+
524+ if !metadata. is_file ( ) {
525+ continue ;
526+ }
527+
528+ let modified = match metadata. modified ( ) {
529+ Ok ( modified) => modified,
530+ Err ( e) => {
531+ tracing:: warn!(
532+ "Failed to get modification time for {:?}: {}" ,
533+ entry. path( ) ,
534+ e
535+ ) ;
536+ continue ;
537+ }
538+ } ;
539+
540+ let elapsed = match modified. duration_since ( UNIX_EPOCH ) {
541+ Ok ( elapsed) => elapsed,
542+ Err ( e) => {
543+ tracing:: warn!(
544+ "Failed to calculate elapsed time for {:?}: {}" ,
545+ entry. path( ) ,
546+ e
547+ ) ;
548+ continue ;
549+ }
550+ } ;
551+
552+ let file_size = metadata. len ( ) ;
553+ file_entries. push ( ( entry, modified, elapsed, file_size) ) ;
554+ }
555+
556+ tracing:: debug!(
557+ "Processed {} log files from directory: {:?}" ,
558+ file_entries. len( ) ,
559+ logs_path
560+ ) ;
561+
562+ let mut removed_files_count = 0 ;
563+
564+ if !retention. is_zero ( ) {
565+ let cutoff = match SystemTime :: now ( ) . duration_since ( UNIX_EPOCH ) {
566+ Ok ( now) => now - retention,
567+ Err ( e) => {
568+ tracing:: warn!( "Failed to get current time: {}" , e) ;
569+ return ;
570+ }
571+ } ;
572+
573+ for ( entry, _, elapsed, _) in & file_entries {
574+ if * elapsed < cutoff {
575+ if let Err ( e) = fs:: remove_file ( entry. path ( ) ) {
576+ tracing:: warn!( "Failed to remove old log file {:?}: {}" , entry. path( ) , e) ;
577+ } else {
578+ tracing:: debug!( "Removed old log file: {:?}" , entry. path( ) ) ;
579+ removed_files_count += 1 ;
580+ }
581+ }
582+ }
583+ }
584+
585+ if max_size_bytes > 0 {
586+ let total_size: u64 = file_entries. iter ( ) . map ( |( _, _, _, size) | * size) . sum ( ) ;
587+
588+ if total_size > max_size_bytes {
589+ file_entries. sort_by_key ( |( _, modified, _, _) | * modified) ;
590+
591+ let mut current_size = total_size;
592+ for ( entry, _, _, file_size) in file_entries {
593+ if current_size <= max_size_bytes {
594+ break ;
595+ }
596+
597+ if let Err ( e) = fs:: remove_file ( entry. path ( ) ) {
598+ tracing:: warn!( "Failed to remove log file {:?}: {}" , entry. path( ) , e) ;
599+ } else {
600+ tracing:: debug!( "Removed log file to control size: {:?}" , entry. path( ) ) ;
601+ current_size = current_size. saturating_sub ( file_size) ;
602+ removed_files_count += 1 ;
603+ }
604+ }
605+ }
606+ }
607+
608+ tracing:: info!(
609+ "Completed log cleanup for directory: {:?}. Removed {} files." ,
610+ logs_path,
611+ removed_files_count
612+ ) ;
613+ }
420614}
421615
422616impl Default for Logging {
0 commit comments