From cafec06b7ebd6e1c2e6a20cccc7b7ff31230df77 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:52:20 +0800 Subject: [PATCH] [Optimization] Enhance obs module telemetry.rs with environment-aware logging and production security (#539) * Initial plan * Implement environment-aware logging with production stdout auto-disable Co-authored-by: houseme <4829346+houseme@users.noreply.github.com> * add mimalloc crate * fix * improve code --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: houseme <4829346+houseme@users.noreply.github.com> Co-authored-by: houseme Co-authored-by: loverustfs <155562731+loverustfs@users.noreply.github.com> --- Cargo.lock | 45 ++- Cargo.toml | 4 +- crates/config/src/observability/config.rs | 44 +++ crates/obs/README.md | 47 ++- crates/obs/src/telemetry.rs | 410 ++++++++++++++++------ rustfs/Cargo.toml | 3 + rustfs/src/main.rs | 4 + 7 files changed, 440 insertions(+), 117 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 578f4bfa..9eccb17f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4021,6 +4021,16 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libmimalloc-sys" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "libredox" version = "0.1.9" @@ -4244,6 +4254,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mimalloc" +version = "0.1.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" +dependencies = [ + "libmimalloc-sys", +] + [[package]] name = "mime" version = "0.3.17" @@ -6015,6 +6034,7 @@ dependencies = [ "hyper-util", "libsystemd", "matchit", + "mimalloc", "mime_guess", "opentelemetry", "percent-encoding", @@ -6983,10 +7003,11 @@ checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.223" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "a505d71960adde88e293da5cb5eda57093379f64e61cf77bf0e6a63af07a7bac" dependencies = [ + "serde_core", "serde_derive", ] @@ -7024,10 +7045,19 @@ dependencies = [ ] [[package]] -name = "serde_derive" -version = "1.0.219" +name = "serde_core" +version = "1.0.223" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "20f57cbd357666aa7b3ac84a90b4ea328f1d4ddb6772b430caa5d9e1309bb9e9" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.223" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d428d07faf17e306e699ec1e91996e5a165ba5d6bce5b5155173e91a8a01a56" dependencies = [ "proc-macro2", "quote", @@ -7056,14 +7086,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3d08e672..fe8c8eee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -209,8 +209,8 @@ rustls-pki-types = "1.12.0" rustls-pemfile = "2.2.0" s3s = { version = "0.12.0-minio-preview.3" } schemars = "1.0.4" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = { version = "1.0.143", features = ["raw_value"] } +serde = { version = "1.0.223", features = ["derive"] } +serde_json = { version = "1.0.145", features = ["raw_value"] } serde_urlencoded = "0.7.1" serial_test = "3.2.0" sha1 = "0.10.6" diff --git a/crates/config/src/observability/config.rs b/crates/config/src/observability/config.rs index 4dfb5148..32515aef 100644 --- a/crates/config/src/observability/config.rs +++ b/crates/config/src/observability/config.rs @@ -33,3 +33,47 @@ pub const ENV_AUDIT_LOGGER_QUEUE_CAPACITY: &str = "RUSTFS_AUDIT_LOGGER_QUEUE_CAP // Default values for observability configuration pub const DEFAULT_AUDIT_LOGGER_QUEUE_CAPACITY: usize = 10000; + +/// Default values for observability configuration +// ### Supported Environment Values +// - `production` - Secure file-only logging +// - `development` - Full debugging with stdout +// - `test` - Test environment with stdout support +// - `staging` - Staging environment with stdout support +pub const DEFAULT_OBS_ENVIRONMENT_PRODUCTION: &str = "production"; +pub const DEFAULT_OBS_ENVIRONMENT_DEVELOPMENT: &str = "development"; +pub const DEFAULT_OBS_ENVIRONMENT_TEST: &str = "test"; +pub const DEFAULT_OBS_ENVIRONMENT_STAGING: &str = "staging"; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_env_keys() { + assert_eq!(ENV_OBS_ENDPOINT, "RUSTFS_OBS_ENDPOINT"); + assert_eq!(ENV_OBS_USE_STDOUT, "RUSTFS_OBS_USE_STDOUT"); + assert_eq!(ENV_OBS_SAMPLE_RATIO, "RUSTFS_OBS_SAMPLE_RATIO"); + assert_eq!(ENV_OBS_METER_INTERVAL, "RUSTFS_OBS_METER_INTERVAL"); + assert_eq!(ENV_OBS_SERVICE_NAME, "RUSTFS_OBS_SERVICE_NAME"); + assert_eq!(ENV_OBS_SERVICE_VERSION, "RUSTFS_OBS_SERVICE_VERSION"); + assert_eq!(ENV_OBS_ENVIRONMENT, "RUSTFS_OBS_ENVIRONMENT"); + assert_eq!(ENV_OBS_LOGGER_LEVEL, "RUSTFS_OBS_LOGGER_LEVEL"); + assert_eq!(ENV_OBS_LOCAL_LOGGING_ENABLED, "RUSTFS_OBS_LOCAL_LOGGING_ENABLED"); + assert_eq!(ENV_OBS_LOG_DIRECTORY, "RUSTFS_OBS_LOG_DIRECTORY"); + assert_eq!(ENV_OBS_LOG_FILENAME, "RUSTFS_OBS_LOG_FILENAME"); + assert_eq!(ENV_OBS_LOG_ROTATION_SIZE_MB, "RUSTFS_OBS_LOG_ROTATION_SIZE_MB"); + assert_eq!(ENV_OBS_LOG_ROTATION_TIME, "RUSTFS_OBS_LOG_ROTATION_TIME"); + assert_eq!(ENV_OBS_LOG_KEEP_FILES, "RUSTFS_OBS_LOG_KEEP_FILES"); + assert_eq!(ENV_AUDIT_LOGGER_QUEUE_CAPACITY, "RUSTFS_AUDIT_LOGGER_QUEUE_CAPACITY"); + } + + #[test] + fn test_default_values() { + assert_eq!(DEFAULT_AUDIT_LOGGER_QUEUE_CAPACITY, 10000); + assert_eq!(DEFAULT_OBS_ENVIRONMENT_PRODUCTION, "production"); + assert_eq!(DEFAULT_OBS_ENVIRONMENT_DEVELOPMENT, "development"); + assert_eq!(DEFAULT_OBS_ENVIRONMENT_TEST, "test"); + assert_eq!(DEFAULT_OBS_ENVIRONMENT_STAGING, "staging"); + } +} diff --git a/crates/obs/README.md b/crates/obs/README.md index be3d6df6..1eaa2e80 100644 --- a/crates/obs/README.md +++ b/crates/obs/README.md @@ -21,12 +21,57 @@ ## ✨ Features +- **Environment-Aware Logging**: Automatically configures logging behavior based on deployment environment + - Production: File-only logging (stdout disabled by default for security and log aggregation) + - Development/Test: Full logging with stdout support for debugging - OpenTelemetry integration for distributed tracing - Prometheus metrics collection and exposition -- Structured logging with configurable levels +- Structured logging with configurable levels and rotation - Performance profiling and analytics - Real-time health checks and status monitoring - Custom dashboards and alerting integration +- Enhanced error handling and resilience + +## 🚀 Environment-Aware Logging + +The obs module automatically adapts logging behavior based on your deployment environment: + +### Production Environment +```bash +# Set production environment - disables stdout logging by default +export RUSTFS_OBS_ENVIRONMENT=production + +# All logs go to files only (no stdout) for security and log aggregation +# Enhanced error handling with clear failure diagnostics +``` + +### Development/Test Environment +```bash +# Set development environment - enables stdout logging +export RUSTFS_OBS_ENVIRONMENT=development + +# Logs appear both in files and stdout for easier debugging +# Full span tracking and verbose error messages +``` + +### Configuration Override +You can always override the environment defaults: +```rust +use rustfs_obs::OtelConfig; + +let config = OtelConfig { + endpoint: "".to_string(), + use_stdout: Some(true), // Explicit override - forces stdout even in production + environment: Some("production".to_string()), + ..Default::default() +}; +``` + +### Supported Environment Values +- `production` - Secure file-only logging +- `development` - Full debugging with stdout +- `test` - Test environment with stdout support +- `staging` - Staging environment with stdout support ## 📚 Documentation diff --git a/crates/obs/src/telemetry.rs b/crates/obs/src/telemetry.rs index 3b81a9e7..69cde686 100644 --- a/crates/obs/src/telemetry.rs +++ b/crates/obs/src/telemetry.rs @@ -29,7 +29,7 @@ use opentelemetry_semantic_conventions::{ SCHEMA_URL, attribute::{DEPLOYMENT_ENVIRONMENT_NAME, NETWORK_LOCAL_ADDRESS, SERVICE_VERSION as OTEL_SERVICE_VERSION}, }; -use rustfs_config::observability::ENV_OBS_LOG_DIRECTORY; +use rustfs_config::observability::{DEFAULT_OBS_ENVIRONMENT_PRODUCTION, ENV_OBS_LOG_DIRECTORY}; use rustfs_config::{ APP_NAME, DEFAULT_LOG_KEEP_FILES, DEFAULT_LOG_LEVEL, ENVIRONMENT, METER_INTERVAL, SAMPLE_RATIO, SERVICE_VERSION, USE_STDOUT, }; @@ -129,11 +129,23 @@ fn create_periodic_reader(interval: u64) -> PeriodicReader OtelGuard { // avoid repeated access to configuration fields let endpoint = &config.endpoint; - let use_stdout = config.use_stdout.unwrap_or(USE_STDOUT); + let environment = config.environment.as_deref().unwrap_or(ENVIRONMENT); + + // Environment-aware stdout configuration + // Check for explicit environment control via RUSTFS_OBS_ENVIRONMENT + let is_production = environment.to_lowercase() == DEFAULT_OBS_ENVIRONMENT_PRODUCTION; + + // Default stdout behavior based on environment + let default_use_stdout = if is_production { + false // Disable stdout in production for security and log aggregation + } else { + USE_STDOUT // Use configured default for dev/test environments + }; + + let use_stdout = config.use_stdout.unwrap_or(default_use_stdout); let meter_interval = config.meter_interval.unwrap_or(METER_INTERVAL); let logger_level = config.logger_level.as_deref().unwrap_or(DEFAULT_LOG_LEVEL); let service_name = config.service_name.as_deref().unwrap_or(APP_NAME); - let environment = config.environment.as_deref().unwrap_or(ENVIRONMENT); // Configure flexi_logger to cut by time and size let mut flexi_logger_handle = None; @@ -144,7 +156,7 @@ pub(crate) fn init_telemetry(config: &OtelConfig) -> OtelGuard { // initialize tracer provider let tracer_provider = { let sample_ratio = config.sample_ratio.unwrap_or(SAMPLE_RATIO); - let sampler = if sample_ratio > 0.0 && sample_ratio < 1.0 { + let sampler = if (0.0..1.0).contains(&sample_ratio) { Sampler::TraceIdRatioBased(sample_ratio) } else { Sampler::AlwaysOn @@ -249,7 +261,7 @@ pub(crate) fn init_telemetry(config: &OtelConfig) -> OtelGuard { .with_line_number(true); // Only add full span events tracking in the development environment - if environment != ENVIRONMENT { + if !is_production { layer = layer.with_span_events(FmtSpan::FULL); } @@ -257,8 +269,7 @@ pub(crate) fn init_telemetry(config: &OtelConfig) -> OtelGuard { }; let filter = build_env_filter(logger_level, None); - let otel_filter = build_env_filter(logger_level, None); - let otel_layer = OpenTelemetryTracingBridge::new(&logger_provider).with_filter(otel_filter); + let otel_layer = OpenTelemetryTracingBridge::new(&logger_provider).with_filter(build_env_filter(logger_level, None)); let tracer = tracer_provider.tracer(Cow::Borrowed(service_name).to_string()); // Configure registry to avoid repeated calls to filter methods @@ -285,73 +296,91 @@ pub(crate) fn init_telemetry(config: &OtelConfig) -> OtelGuard { } } - OtelGuard { + return OtelGuard { tracer_provider: Some(tracer_provider), meter_provider: Some(meter_provider), logger_provider: Some(logger_provider), _flexi_logger_handles: flexi_logger_handle, - } - } else { - // Obtain the log directory and file name configuration - let default_log_directory = rustfs_utils::dirs::get_log_directory_to_string(ENV_OBS_LOG_DIRECTORY); - let log_directory = config.log_directory.as_deref().unwrap_or(default_log_directory.as_str()); - let log_filename = config.log_filename.as_deref().unwrap_or(service_name); - - if let Err(e) = fs::create_dir_all(log_directory) { - eprintln!("Failed to create log directory {log_directory}: {e}"); - } - #[cfg(unix)] - { - // Linux/macOS Setting Permissions - // Set the log directory permissions to 755 (rwxr-xr-x) - use std::fs::Permissions; - use std::os::unix::fs::PermissionsExt; - match fs::set_permissions(log_directory, Permissions::from_mode(0o755)) { - Ok(_) => eprintln!("Log directory permissions set to 755: {log_directory}"), - Err(e) => eprintln!("Failed to set log directory permissions {log_directory}: {e}"), - } - } - - // Build log cutting conditions - let rotation_criterion = match (config.log_rotation_time.as_deref(), config.log_rotation_size_mb) { - // Cut by time and size at the same time - (Some(time), Some(size)) => { - let age = match time.to_lowercase().as_str() { - "hour" => Age::Hour, - "day" => Age::Day, - "minute" => Age::Minute, - "second" => Age::Second, - _ => Age::Day, // The default is by day - }; - Criterion::AgeOrSize(age, size * 1024 * 1024) // Convert to bytes - } - // Cut by time only - (Some(time), None) => { - let age = match time.to_lowercase().as_str() { - "hour" => Age::Hour, - "day" => Age::Day, - "minute" => Age::Minute, - "second" => Age::Second, - _ => Age::Day, // The default is by day - }; - Criterion::Age(age) - } - // Cut by size only - (None, Some(size)) => { - Criterion::Size(size * 1024 * 1024) // Convert to bytes - } - // By default, it is cut by the day - _ => Criterion::Age(Age::Day), }; + } - // The number of log files retained - let keep_files = config.log_keep_files.unwrap_or(DEFAULT_LOG_KEEP_FILES); + // Obtain the log directory and file name configuration + let default_log_directory = rustfs_utils::dirs::get_log_directory_to_string(ENV_OBS_LOG_DIRECTORY); + let log_directory = config.log_directory.as_deref().unwrap_or(default_log_directory.as_str()); + let log_filename = config.log_filename.as_deref().unwrap_or(service_name); - // Parsing the log level - let log_spec = LogSpecification::parse(logger_level).unwrap_or(LogSpecification::info()); + // Enhanced error handling for directory creation + if let Err(e) = fs::create_dir_all(log_directory) { + eprintln!("ERROR: Failed to create log directory '{log_directory}': {e}"); + eprintln!("Ensure the parent directory exists and you have write permissions."); + eprintln!("Attempting to continue with logging, but file logging may fail."); + } else { + eprintln!("Log directory ready: {log_directory}"); + } - // Convert the logger_level string to the corresponding LevelFilter - let level_filter = match logger_level.to_lowercase().as_str() { + #[cfg(unix)] + { + // Linux/macOS Setting Permissions with better error handling + use std::fs::Permissions; + use std::os::unix::fs::PermissionsExt; + match fs::set_permissions(log_directory, Permissions::from_mode(0o755)) { + Ok(_) => eprintln!("Log directory permissions set to 755: {log_directory}"), + Err(e) => { + eprintln!("WARNING: Failed to set log directory permissions for '{log_directory}': {e}"); + eprintln!("This may affect log file access. Consider checking directory ownership and permissions."); + } + } + } + + // Build log cutting conditions + let rotation_criterion = match (config.log_rotation_time.as_deref(), config.log_rotation_size_mb) { + // Cut by time and size at the same time + (Some(time), Some(size)) => { + let age = match time.to_lowercase().as_str() { + "hour" => Age::Hour, + "day" => Age::Day, + "minute" => Age::Minute, + "second" => Age::Second, + _ => Age::Day, // The default is by day + }; + Criterion::AgeOrSize(age, size * 1024 * 1024) // Convert to bytes + } + // Cut by time only + (Some(time), None) => { + let age = match time.to_lowercase().as_str() { + "hour" => Age::Hour, + "day" => Age::Day, + "minute" => Age::Minute, + "second" => Age::Second, + _ => Age::Day, // The default is by day + }; + Criterion::Age(age) + } + // Cut by size only + (None, Some(size)) => { + Criterion::Size(size * 1024 * 1024) // Convert to bytes + } + // By default, it is cut by the day + _ => Criterion::Age(Age::Day), + }; + + // The number of log files retained + let keep_files = config.log_keep_files.unwrap_or(DEFAULT_LOG_KEEP_FILES); + + // Parsing the log level + let log_spec = LogSpecification::parse(logger_level).unwrap_or_else(|e| { + eprintln!("WARNING: Invalid logger level '{logger_level}': {e}. Using default 'info' level."); + LogSpecification::info() + }); + + // Environment-aware stdout configuration + // In production: disable stdout completely (Duplicate::None) + // In development/test: use level-based filtering + let level_filter = if is_production { + flexi_logger::Duplicate::None // No stdout output in production + } else { + // Convert the logger_level string to the corresponding LevelFilter for dev/test + match logger_level.to_lowercase().as_str() { "trace" => flexi_logger::Duplicate::Trace, "debug" => flexi_logger::Duplicate::Debug, "info" => flexi_logger::Duplicate::Info, @@ -359,56 +388,85 @@ pub(crate) fn init_telemetry(config: &OtelConfig) -> OtelGuard { "error" => flexi_logger::Duplicate::Error, "off" => flexi_logger::Duplicate::None, _ => flexi_logger::Duplicate::Info, // the default is info - }; + } + }; - // Configure the flexi_logger - let flexi_logger_result = flexi_logger::Logger::try_with_env_or_str(logger_level) - .unwrap_or_else(|e| { - eprintln!("Invalid logger level: {logger_level}, using default: {DEFAULT_LOG_LEVEL}, failed error: {e:?}"); - flexi_logger::Logger::with(log_spec.clone()) - }) - .log_to_file( - FileSpec::default() - .directory(log_directory) - .basename(log_filename) - .suppress_timestamp(), - ) - .rotate(rotation_criterion, Naming::TimestampsDirect, Cleanup::KeepLogFiles(keep_files.into())) - .format_for_files(format_for_file) // Add a custom formatting function for file output - .duplicate_to_stdout(level_filter) // Use dynamic levels + // Choose write mode based on environment + let write_mode = if is_production { + WriteMode::Async + } else { + WriteMode::BufferAndFlush + }; + + // Configure the flexi_logger with enhanced error handling + let mut flexi_logger_builder = flexi_logger::Logger::try_with_env_or_str(logger_level) + .unwrap_or_else(|e| { + eprintln!("WARNING: Invalid logger configuration '{logger_level}': {e:?}"); + eprintln!("Falling back to default configuration with level: {DEFAULT_LOG_LEVEL}"); + flexi_logger::Logger::with(log_spec.clone()) + }) + .log_to_file( + FileSpec::default() + .directory(log_directory) + .basename(log_filename) + .suppress_timestamp(), + ) + .rotate(rotation_criterion, Naming::TimestampsDirect, Cleanup::KeepLogFiles(keep_files.into())) + .format_for_files(format_for_file) // Add a custom formatting function for file output + .write_mode(write_mode) + .append(); // Avoid clearing existing logs at startup + + // Environment-aware stdout configuration + flexi_logger_builder = flexi_logger_builder.duplicate_to_stdout(level_filter); + + // Only add stdout formatting and startup messages in non-production environments + if !is_production { + flexi_logger_builder = flexi_logger_builder .format_for_stdout(format_with_color) // Add a custom formatting function for terminal output - .write_mode(WriteMode::BufferAndFlush) - .append() // Avoid clearing existing logs at startup - .print_message() // Startup information output to console - .start(); + .print_message(); // Startup information output to console + } - if let Ok(logger) = flexi_logger_result { - // Save the logger handle to keep the logging - flexi_logger_handle = Some(logger); + let flexi_logger_result = flexi_logger_builder.start(); - eprintln!("Flexi logger initialized with file logging to {log_directory}/{log_filename}.log"); + if let Ok(logger) = flexi_logger_result { + // Save the logger handle to keep the logging + flexi_logger_handle = Some(logger); - // Log logging of log cutting conditions - match (config.log_rotation_time.as_deref(), config.log_rotation_size_mb) { - (Some(time), Some(size)) => eprintln!( - "Log rotation configured for: every {time} or when size exceeds {size}MB, keeping {keep_files} files" - ), - (Some(time), None) => eprintln!("Log rotation configured for: every {time}, keeping {keep_files} files"), - (None, Some(size)) => { - eprintln!("Log rotation configured for: when size exceeds {size}MB, keeping {keep_files} files") - } - _ => eprintln!("Log rotation configured for: daily, keeping {keep_files} files"), - } + // Environment-aware success messages + if is_production { + eprintln!("Production logging initialized: file-only mode to {log_directory}/{log_filename}.log"); + eprintln!("Stdout logging disabled in production environment for security and log aggregation."); } else { - eprintln!("Failed to initialize flexi_logger: {:?}", flexi_logger_result.err()); + eprintln!("Development/Test logging initialized with file logging to {log_directory}/{log_filename}.log"); + eprintln!("Stdout logging enabled for debugging. Environment: {environment}"); } - OtelGuard { - tracer_provider: None, - meter_provider: None, - logger_provider: None, - _flexi_logger_handles: flexi_logger_handle, + // Log rotation configuration details + match (config.log_rotation_time.as_deref(), config.log_rotation_size_mb) { + (Some(time), Some(size)) => { + eprintln!("Log rotation configured for: every {time} or when size exceeds {size}MB, keeping {keep_files} files") + } + (Some(time), None) => eprintln!("Log rotation configured for: every {time}, keeping {keep_files} files"), + (None, Some(size)) => { + eprintln!("Log rotation configured for: when size exceeds {size}MB, keeping {keep_files} files") + } + _ => eprintln!("Log rotation configured for: daily, keeping {keep_files} files"), } + } else { + eprintln!("CRITICAL: Failed to initialize flexi_logger: {:?}", flexi_logger_result.err()); + eprintln!("Possible causes:"); + eprintln!(" 1. Insufficient permissions to write to log directory: {log_directory}"); + eprintln!(" 2. Log directory does not exist or is not accessible"); + eprintln!(" 3. Invalid log configuration parameters"); + eprintln!(" 4. Disk space issues"); + eprintln!("Application will continue but logging to files will not work properly."); + } + + OtelGuard { + tracer_provider: None, + meter_provider: None, + logger_provider: None, + _flexi_logger_handles: flexi_logger_handle, } } @@ -473,3 +531,141 @@ fn format_for_file(w: &mut dyn std::io::Write, now: &mut DeferredNow, record: &R record.args() ) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_production_environment_detection() { + // Test production environment logic + let production_envs = vec!["production", "PRODUCTION", "Production"]; + + for env_value in production_envs { + let is_production = env_value.to_lowercase() == "production"; + assert!(is_production, "Should detect '{}' as production environment", env_value); + } + } + + #[test] + fn test_non_production_environment_detection() { + // Test non-production environment logic + let non_production_envs = vec!["development", "test", "staging", "dev", "local"]; + + for env_value in non_production_envs { + let is_production = env_value.to_lowercase() == "production"; + assert!(!is_production, "Should not detect '{}' as production environment", env_value); + } + } + + #[test] + fn test_stdout_behavior_logic() { + // Test the stdout behavior logic without environment manipulation + struct TestCase { + is_production: bool, + config_use_stdout: Option, + expected_use_stdout: bool, + description: &'static str, + } + + let test_cases = vec![ + TestCase { + is_production: true, + config_use_stdout: None, + expected_use_stdout: false, + description: "Production with no config should disable stdout", + }, + TestCase { + is_production: false, + config_use_stdout: None, + expected_use_stdout: USE_STDOUT, + description: "Non-production with no config should use default", + }, + TestCase { + is_production: true, + config_use_stdout: Some(true), + expected_use_stdout: true, + description: "Production with explicit true should enable stdout", + }, + TestCase { + is_production: true, + config_use_stdout: Some(false), + expected_use_stdout: false, + description: "Production with explicit false should disable stdout", + }, + TestCase { + is_production: false, + config_use_stdout: Some(true), + expected_use_stdout: true, + description: "Non-production with explicit true should enable stdout", + }, + ]; + + for case in test_cases { + let default_use_stdout = if case.is_production { false } else { USE_STDOUT }; + + let actual_use_stdout = case.config_use_stdout.unwrap_or(default_use_stdout); + + assert_eq!(actual_use_stdout, case.expected_use_stdout, "Test case failed: {}", case.description); + } + } + + #[test] + fn test_log_level_filter_mapping_logic() { + // Test the log level mapping logic used in the real implementation + let test_cases = vec![ + ("trace", "Trace"), + ("debug", "Debug"), + ("info", "Info"), + ("warn", "Warn"), + ("warning", "Warn"), + ("error", "Error"), + ("off", "None"), + ("invalid_level", "Info"), // Should default to Info + ]; + + for (input_level, expected_variant) in test_cases { + let filter_variant = match input_level.to_lowercase().as_str() { + "trace" => "Trace", + "debug" => "Debug", + "info" => "Info", + "warn" | "warning" => "Warn", + "error" => "Error", + "off" => "None", + _ => "Info", // default case + }; + + assert_eq!( + filter_variant, expected_variant, + "Log level '{}' should map to '{}'", + input_level, expected_variant + ); + } + } + + #[test] + fn test_otel_config_environment_defaults() { + // Test that OtelConfig properly handles environment detection logic + let config = OtelConfig { + endpoint: "".to_string(), + use_stdout: None, + environment: Some("production".to_string()), + ..Default::default() + }; + + // Simulate the logic from init_telemetry + let environment = config.environment.as_deref().unwrap_or(ENVIRONMENT); + assert_eq!(environment, "production"); + + // Test with development environment + let dev_config = OtelConfig { + endpoint: "".to_string(), + use_stdout: None, + environment: Some("development".to_string()), + ..Default::default() + }; + + let dev_environment = dev_config.environment.as_deref().unwrap_or(ENVIRONMENT); + assert_eq!(dev_environment, "development"); + } +} diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index fb6348c0..761ee3cc 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -122,6 +122,9 @@ libsystemd.workspace = true [target.'cfg(all(target_os = "linux", target_env = "gnu"))'.dependencies] tikv-jemallocator = "0.6" +[target.'cfg(all(target_os = "linux", target_env = "musl"))'.dependencies] +mimalloc = "0.1" + [target.'cfg(not(target_os = "windows"))'.dependencies] pprof = { version = "0.15.0", features = ["flamegraph", "protobuf-codec"] } diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index 3c496051..a68b752a 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -76,6 +76,10 @@ use tracing::{debug, error, info, instrument, warn}; #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; +#[cfg(all(target_os = "linux", target_env = "musl"))] +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + const LOGO: &str = r#" ░█▀▄░█░█░█▀▀░▀█▀░█▀▀░█▀▀