Skip to content

Commit dbdd6fe

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 9b53c48 commit dbdd6fe

File tree

4 files changed

+215
-7
lines changed

4 files changed

+215
-7
lines changed

Cargo.lock

Lines changed: 11 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
@@ -669,6 +669,7 @@ rmcp-macros: 0.12.0, "MIT",
669669
rmp: 0.8.15, "MIT",
670670
rmp-serde: 1.3.1, "MIT",
671671
roaring: 0.10.12, "Apache-2.0 OR MIT",
672+
rolling-file: 0.2.0, "Apache-2.0 OR MIT",
672673
route-recognizer: 0.3.1, "MIT",
673674
rsa: 0.9.9, "Apache-2.0 OR MIT",
674675
rust-embed: 8.9.0, "MIT",

core/server/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ error_set = { workspace = true }
6767
figlet-rs = { workspace = true }
6868
figment = { workspace = true }
6969
flume = { workspace = true }
70+
fs2 = "0.4"
7071
futures = { workspace = true }
7172
hash32 = "1.0.0"
7273
human-repr = { workspace = true }
@@ -102,6 +103,7 @@ rand = { workspace = true }
102103
reqwest = { workspace = true, features = ["rustls-tls-no-provider"] }
103104
ring = "0.17.14"
104105
ringbuffer = "0.16.0"
106+
rolling-file = "0.2.0"
105107
rmp-serde = { workspace = true }
106108
rust-embed = { version = "8.9.0", optional = true }
107109
rustls = { workspace = true }

core/server/src/log/logger.rs

Lines changed: 201 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,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

422616
impl Default for Logging {

0 commit comments

Comments
 (0)