Skip to content

Commit 9e992d1

Browse files
committed
feat(server): implement comprehensive log rotation based on size and retention
- Configurable maximum log size and retention period from server configuration - Optimized single directory traversal for collecting file information - Eliminated redundant metadata() calls for improved performance - Enhanced error handling with detailed logging for all failure cases - Merged directory traversals into a single operation to improve performance Fixes apache#46, detail changes can be found at apache#2452.
1 parent 49c6c5b commit 9e992d1

File tree

4 files changed

+186
-7
lines changed

4 files changed

+186
-7
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

DEPENDENCIES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,7 @@ rle-decode-fast: 1.0.3, "Apache-2.0 OR MIT",
661661
rmcp: 0.11.0, "MIT",
662662
rmcp-macros: 0.11.0, "MIT",
663663
roaring: 0.10.12, "Apache-2.0 OR MIT",
664+
rolling-file: 0.2.0, "Apache-2.0 OR MIT",
664665
route-recognizer: 0.3.1, "MIT",
665666
rsa: 0.9.9, "Apache-2.0 OR MIT",
666667
rust-ini: 0.21.3, "MIT",

core/server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ rand = { workspace = true }
100100
reqwest = { workspace = true, features = ["rustls-tls-no-provider"] }
101101
ring = "0.17.14"
102102
ringbuffer = "0.16.0"
103+
rolling-file = "0.2.0"
103104
rustls = { workspace = true }
104105
rustls-pemfile = "2.2.0"
105106
send_wrapper = "0.6.0"

core/server/src/log/logger.rs

Lines changed: 174 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ use opentelemetry_sdk::Resource;
3030
use opentelemetry_sdk::logs::log_processor_with_async_runtime;
3131
use opentelemetry_sdk::propagation::TraceContextPropagator;
3232
use opentelemetry_sdk::trace::span_processor_with_async_runtime;
33+
use rolling_file::{BasicRollingFileAppender, RollingConditionBasic};
3334
use std::io::{self, Write};
3435
use std::path::PathBuf;
3536
use std::sync::{Arc, Mutex};
37+
use std::time::Duration;
3638
use tracing::{info, trace};
3739
use tracing_appender::non_blocking::WorkerGuard;
3840
use 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

422589
impl Default for Logging {

0 commit comments

Comments
 (0)