mirror of
https://github.com/rustfs/rustfs.git
synced 2026-03-17 14:24:08 +00:00
fix: improve env compatibility and observability startup
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -7406,6 +7406,7 @@ dependencies = [
|
||||
"starshard",
|
||||
"subtle",
|
||||
"sysinfo",
|
||||
"temp-env",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tikv-jemalloc-ctl",
|
||||
@@ -8173,6 +8174,7 @@ dependencies = [
|
||||
"siphasher",
|
||||
"snap",
|
||||
"sysinfo",
|
||||
"temp-env",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
|
||||
@@ -89,7 +89,14 @@ pub(super) fn init_local_logging(
|
||||
let log_dir_str = config.log_directory.as_deref().filter(|s| !s.is_empty());
|
||||
|
||||
if let Some(log_directory) = log_dir_str {
|
||||
init_file_logging_internal(config, log_directory, logger_level, is_production)
|
||||
match init_file_logging_internal(config, log_directory, logger_level, is_production) {
|
||||
Ok(guard) => Ok(guard),
|
||||
Err(error) if should_fallback_to_stdout(&error) => {
|
||||
emit_file_logging_fallback_warning(log_directory, &error);
|
||||
Ok(init_stdout_only(config, logger_level, is_production))
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
} else {
|
||||
Ok(init_stdout_only(config, logger_level, is_production))
|
||||
}
|
||||
@@ -316,6 +323,24 @@ pub fn ensure_dir_permissions(log_directory: &str) -> Result<(), TelemetryError>
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn should_fallback_to_stdout(error: &TelemetryError) -> bool {
|
||||
match error {
|
||||
TelemetryError::SetPermissions(_) => true,
|
||||
TelemetryError::Io(message) => {
|
||||
let message = message.to_ascii_lowercase();
|
||||
message.contains("permission denied") || message.contains("os error 13")
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn emit_file_logging_fallback_warning(log_directory: &str, error: &TelemetryError) {
|
||||
eprintln!(
|
||||
"[WARN] Failed to initialize file observability logging at '{}': {}. Falling back to stdout logging.",
|
||||
log_directory, error
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Cleanup task ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Spawn a background task that periodically cleans up old log files.
|
||||
@@ -435,4 +460,17 @@ mod tests {
|
||||
assert!(result.is_err(), "invalid filename must return Err, not panic");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permission_denied_errors_fall_back_to_stdout() {
|
||||
assert!(should_fallback_to_stdout(&TelemetryError::Io(
|
||||
"Permission denied (os error 13)".to_string()
|
||||
)));
|
||||
assert!(should_fallback_to_stdout(&TelemetryError::SetPermissions(
|
||||
"dir='/logs', want=0o755, have=0o777, err=Permission denied (os error 13)".to_string()
|
||||
)));
|
||||
assert!(!should_fallback_to_stdout(&TelemetryError::Io(
|
||||
"No such file or directory (os error 2)".to_string()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ pub(super) fn init_observability_http(
|
||||
let mut cleanup_handle = None;
|
||||
let mut tracing_guard = None; // Guard for file writer
|
||||
let mut stdout_guard = None; // Guard for stdout writer (File mode)
|
||||
let mut force_stdout_logging = false;
|
||||
|
||||
// ── Case 1: OTLP Logging
|
||||
if !log_ep.is_empty() {
|
||||
@@ -189,43 +190,36 @@ pub(super) fn init_observability_http(
|
||||
{
|
||||
let log_filename = config.log_filename.as_deref().unwrap_or(&service_name);
|
||||
let keep_files = config.log_keep_files.unwrap_or(DEFAULT_LOG_KEEP_FILES);
|
||||
let file_logging_result = (|| -> Result<_, TelemetryError> {
|
||||
fs::create_dir_all(log_directory).map_err(|e| TelemetryError::Io(e.to_string()))?;
|
||||
|
||||
// 1. Ensure dir exists
|
||||
if let Err(e) = fs::create_dir_all(log_directory) {
|
||||
return Err(TelemetryError::Io(e.to_string()));
|
||||
}
|
||||
// 2. Permissions
|
||||
#[cfg(unix)]
|
||||
crate::telemetry::local::ensure_dir_permissions(log_directory)?;
|
||||
#[cfg(unix)]
|
||||
crate::telemetry::local::ensure_dir_permissions(log_directory)?;
|
||||
|
||||
// 3. Rotation
|
||||
let rotation_str = config
|
||||
.log_rotation_time
|
||||
.as_deref()
|
||||
.unwrap_or(DEFAULT_LOG_ROTATION_TIME)
|
||||
.to_lowercase();
|
||||
let match_mode = match config.log_match_mode.as_deref().map(|s| s.to_lowercase()).as_deref() {
|
||||
Some("prefix") => FileMatchMode::Prefix,
|
||||
_ => FileMatchMode::Suffix,
|
||||
};
|
||||
let rotation = match rotation_str.as_str() {
|
||||
"minutely" => Rotation::Minutely,
|
||||
"hourly" => Rotation::Hourly,
|
||||
"daily" => Rotation::Daily,
|
||||
_ => Rotation::Daily,
|
||||
};
|
||||
let max_single_file_size = config
|
||||
.log_max_single_file_size_bytes
|
||||
.unwrap_or(DEFAULT_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES);
|
||||
let rotation_str = config
|
||||
.log_rotation_time
|
||||
.as_deref()
|
||||
.unwrap_or(DEFAULT_LOG_ROTATION_TIME)
|
||||
.to_lowercase();
|
||||
let match_mode = match config.log_match_mode.as_deref().map(|s| s.to_lowercase()).as_deref() {
|
||||
Some("prefix") => FileMatchMode::Prefix,
|
||||
_ => FileMatchMode::Suffix,
|
||||
};
|
||||
let rotation = match rotation_str.as_str() {
|
||||
"minutely" => Rotation::Minutely,
|
||||
"hourly" => Rotation::Hourly,
|
||||
"daily" => Rotation::Daily,
|
||||
_ => Rotation::Daily,
|
||||
};
|
||||
let max_single_file_size = config
|
||||
.log_max_single_file_size_bytes
|
||||
.unwrap_or(DEFAULT_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES);
|
||||
|
||||
let file_appender =
|
||||
RollingAppender::new(log_directory, log_filename.to_string(), rotation, max_single_file_size, match_mode)?;
|
||||
let file_appender =
|
||||
RollingAppender::new(log_directory, log_filename.to_string(), rotation, max_single_file_size, match_mode)?;
|
||||
|
||||
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
||||
tracing_guard = Some(guard);
|
||||
|
||||
file_layer_opt = Some(
|
||||
tracing_subscriber::fmt::layer()
|
||||
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
||||
let file_layer = tracing_subscriber::fmt::layer()
|
||||
.with_timer(LocalTime::rfc_3339())
|
||||
.with_target(true)
|
||||
.with_ansi(false)
|
||||
@@ -238,16 +232,29 @@ pub(super) fn init_observability_http(
|
||||
.with_current_span(true)
|
||||
.with_span_list(true)
|
||||
.with_span_events(span_events.clone())
|
||||
.with_filter(build_env_filter(logger_level, None)),
|
||||
);
|
||||
.with_filter(build_env_filter(logger_level, None));
|
||||
let cleanup_handle = spawn_cleanup_task(config, log_directory, log_filename, keep_files);
|
||||
|
||||
// Cleanup task
|
||||
cleanup_handle = Some(spawn_cleanup_task(config, log_directory, log_filename, keep_files));
|
||||
Ok((file_layer, guard, cleanup_handle, rotation_str))
|
||||
})();
|
||||
|
||||
info!(
|
||||
"Init file logging at '{}', rotation: {}, keep {} files",
|
||||
log_directory, rotation_str, keep_files
|
||||
);
|
||||
match file_logging_result {
|
||||
Ok((file_layer, guard, new_cleanup_handle, rotation_str)) => {
|
||||
tracing_guard = Some(guard);
|
||||
file_layer_opt = Some(file_layer);
|
||||
cleanup_handle = Some(new_cleanup_handle);
|
||||
|
||||
info!(
|
||||
"Init file logging at '{}', rotation: {}, keep {} files",
|
||||
log_directory, rotation_str, keep_files
|
||||
);
|
||||
}
|
||||
Err(error) if crate::telemetry::local::should_fallback_to_stdout(&error) => {
|
||||
crate::telemetry::local::emit_file_logging_fallback_warning(log_directory, &error);
|
||||
force_stdout_logging = true;
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tracing subscriber registry ───────────────────────────────────────────
|
||||
@@ -258,7 +265,7 @@ pub(super) fn init_observability_http(
|
||||
|
||||
// Optional stdout mirror (matching init_file_logging_internal logic)
|
||||
// This is separate from OTLP stdout logic. If file logging is enabled, we honor its stdout rules.
|
||||
if config.log_stdout_enabled.unwrap_or(DEFAULT_OBS_LOG_STDOUT_ENABLED) || !is_production {
|
||||
if force_stdout_logging || config.log_stdout_enabled.unwrap_or(DEFAULT_OBS_LOG_STDOUT_ENABLED) || !is_production {
|
||||
let (stdout_nb, stdout_g) = tracing_appender::non_blocking(std::io::stdout());
|
||||
stdout_guard = Some(stdout_g);
|
||||
stdout_layer_opt = Some(
|
||||
|
||||
@@ -70,6 +70,7 @@ zstd = { workspace = true, optional = true }
|
||||
tempfile = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
temp-env = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { workspace = true, optional = true, features = ["Win32_Storage_FileSystem", "Win32_Foundation"] }
|
||||
|
||||
@@ -31,7 +31,7 @@ use tracing::warn;
|
||||
/// - `i8`: The parsed value as i8 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_i8(key: &str, default: i8) -> i8 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
@@ -44,7 +44,7 @@ pub fn get_env_i8(key: &str, default: i8) -> i8 {
|
||||
/// - `Option<i8>`: The parsed value as i8 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_i8(key: &str) -> Option<i8> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
@@ -58,7 +58,7 @@ pub fn get_env_opt_i8(key: &str) -> Option<i8> {
|
||||
/// - `u8`: The parsed value as u8 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_u8(key: &str, default: u8) -> u8 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
@@ -71,7 +71,7 @@ pub fn get_env_u8(key: &str, default: u8) -> u8 {
|
||||
/// - `Option<u8>`: The parsed value as u8 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_u8(key: &str) -> Option<u8> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
static WARNED_ENV_MESSAGES: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
|
||||
@@ -87,19 +87,38 @@ fn log_once(key: &str, message: impl FnOnce() -> String) {
|
||||
}
|
||||
}
|
||||
|
||||
fn external_alias_for_key(key: &str) -> Option<String> {
|
||||
let suffix = key.strip_prefix("RUSTFS_")?;
|
||||
if is_external_compatible_suffix(suffix) {
|
||||
Some(format!("{}{}", external_env_prefix(), suffix))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_env_with_aliases(key: &str, deprecated: &[&str]) -> Option<(String, String)> {
|
||||
if let Ok(value) = env::var(key) {
|
||||
return Some((key.to_string(), value));
|
||||
}
|
||||
|
||||
let (alias, value) = deprecated
|
||||
if let Some((alias, value)) = deprecated
|
||||
.iter()
|
||||
.find_map(|alias| env::var(alias).ok().map(|value| (*alias, value)))?;
|
||||
.find_map(|alias| env::var(alias).ok().map(|value| (*alias, value)))
|
||||
{
|
||||
let deprecated_key = format!("env_alias:{alias}->{key}");
|
||||
log_once(&deprecated_key, || {
|
||||
format!("Environment variable {alias} is deprecated, use {key} instead")
|
||||
});
|
||||
return Some((alias.to_string(), value));
|
||||
}
|
||||
|
||||
let alias = external_alias_for_key(key)?;
|
||||
let value = env::var(&alias).ok()?;
|
||||
let deprecated_key = format!("env_alias:{alias}->{key}");
|
||||
log_once(&deprecated_key, || {
|
||||
format!("Environment variable {alias} is deprecated, use {key} instead")
|
||||
});
|
||||
Some((alias.to_string(), value))
|
||||
Some((alias, value))
|
||||
}
|
||||
|
||||
const EXTERNAL_ENV_PREFIX_BYTES: [u8; 6] = [77, 73, 78, 73, 79, 95];
|
||||
@@ -239,6 +258,13 @@ pub fn build_external_env_compat_report() -> ExternalEnvCompatReport {
|
||||
build_external_env_compat_report_from_entries(env::vars())
|
||||
}
|
||||
|
||||
fn parse_env_value<T>(key: &str) -> Option<T>
|
||||
where
|
||||
T: std::str::FromStr,
|
||||
{
|
||||
resolve_env_with_aliases(key, &[]).and_then(|(_, value)| value.parse().ok())
|
||||
}
|
||||
|
||||
pub fn get_env_str_with_aliases(key: &str, deprecated: &[&str], default: &str) -> String {
|
||||
resolve_env_with_aliases(key, deprecated).map_or_else(|| default.to_string(), |(_, value)| value)
|
||||
}
|
||||
@@ -272,7 +298,7 @@ pub fn get_env_bool_with_aliases(key: &str, deprecated: &[&str], default: bool)
|
||||
/// - `i16`: The parsed value as i16 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_i16(key: &str, default: i16) -> i16 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
/// 16-bit type: signed i16
|
||||
@@ -284,7 +310,7 @@ pub fn get_env_i16(key: &str, default: i16) -> i16 {
|
||||
/// - `Option<i16>`: The parsed value as i16 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_i16(key: &str) -> Option<i16> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
@@ -298,7 +324,7 @@ pub fn get_env_opt_i16(key: &str) -> Option<i16> {
|
||||
/// - `u16`: The parsed value as u16 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_u16(key: &str, default: u16) -> u16 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
/// 16-bit type: unsigned u16
|
||||
@@ -310,7 +336,7 @@ pub fn get_env_u16(key: &str, default: u16) -> u16 {
|
||||
/// - `Option<u16>`: The parsed value as u16 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_u16_opt(key: &str) -> Option<u16> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
/// 16-bit type: unsigned u16
|
||||
@@ -335,7 +361,7 @@ pub fn get_env_opt_u16(key: &str) -> Option<u16> {
|
||||
/// - `i32`: The parsed value as i32 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_i32(key: &str, default: i32) -> i32 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
/// 32-bit type: signed i32
|
||||
@@ -347,7 +373,7 @@ pub fn get_env_i32(key: &str, default: i32) -> i32 {
|
||||
/// - `Option<i32>`: The parsed value as i32 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_i32(key: &str) -> Option<i32> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
@@ -361,7 +387,7 @@ pub fn get_env_opt_i32(key: &str) -> Option<i32> {
|
||||
/// - `u32`: The parsed value as u32 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_u32(key: &str, default: u32) -> u32 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
/// 32-bit type: unsigned u32
|
||||
@@ -373,7 +399,7 @@ pub fn get_env_u32(key: &str, default: u32) -> u32 {
|
||||
/// - `Option<u32>`: The parsed value as u32 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_u32(key: &str) -> Option<u32> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
///
|
||||
@@ -385,7 +411,7 @@ pub fn get_env_opt_u32(key: &str) -> Option<u32> {
|
||||
/// - `f32`: The parsed value as f32 if successful, otherwise the default value
|
||||
///
|
||||
pub fn get_env_f32(key: &str, default: f32) -> f32 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
///
|
||||
@@ -396,7 +422,7 @@ pub fn get_env_f32(key: &str, default: f32) -> f32 {
|
||||
/// - `Option<f32>`: The parsed value as f32 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_f32(key: &str) -> Option<f32> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
@@ -409,7 +435,7 @@ pub fn get_env_opt_f32(key: &str) -> Option<f32> {
|
||||
/// - `i64`: The parsed value as i64 if successful, otherwise the default value
|
||||
///
|
||||
pub fn get_env_i64(key: &str, default: i64) -> i64 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
///
|
||||
@@ -420,7 +446,7 @@ pub fn get_env_i64(key: &str, default: i64) -> i64 {
|
||||
/// - `Option<i64>`: The parsed value as i64 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_i64(key: &str) -> Option<i64> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, returning Option<Option<i64>> if not set or parsing fails.
|
||||
@@ -432,7 +458,7 @@ pub fn get_env_opt_i64(key: &str) -> Option<i64> {
|
||||
/// - `Option<Option<i64>>`: The parsed value as i64 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_opt_i64(key: &str) -> Option<Option<i64>> {
|
||||
env::var(key).ok().map(|v| v.parse().ok())
|
||||
resolve_env_with_aliases(key, &[]).map(|(_, value)| value.parse().ok())
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
@@ -445,7 +471,7 @@ pub fn get_env_opt_opt_i64(key: &str) -> Option<Option<i64>> {
|
||||
/// - `u64`: The parsed value as u64 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_u64(key: &str, default: u64) -> u64 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as an unsigned 64-bit integer, returning `None` if not set or parsing fails.
|
||||
@@ -479,7 +505,7 @@ pub fn get_env_opt_u64_with_aliases(key: &str, deprecated: &[&str]) -> Option<u6
|
||||
/// - `Option<u64>`: The parsed value as u64 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_u64(key: &str) -> Option<u64> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
@@ -492,7 +518,7 @@ pub fn get_env_opt_u64(key: &str) -> Option<u64> {
|
||||
/// - `f64`: The parsed value as f64 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_f64(key: &str, default: f64) -> f64 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
@@ -504,7 +530,7 @@ pub fn get_env_f64(key: &str, default: f64) -> f64 {
|
||||
/// - `Option<f64>`: The parsed value as f64 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_f64(key: &str) -> Option<f64> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
@@ -517,7 +543,7 @@ pub fn get_env_opt_f64(key: &str) -> Option<f64> {
|
||||
/// - `usize`: The parsed value as usize if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_usize(key: &str, default: usize) -> usize {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
///
|
||||
@@ -528,7 +554,7 @@ pub fn get_env_usize(key: &str, default: usize) -> usize {
|
||||
/// - `Option<usize>`: The parsed value as usize if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_usize_opt(key: &str) -> Option<usize> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
@@ -565,7 +591,7 @@ pub fn get_env_str(key: &str, default: &str) -> String {
|
||||
/// - `Option<String>`: The environment variable value if set, otherwise None.
|
||||
///
|
||||
pub fn get_env_opt_str(key: &str) -> Option<String> {
|
||||
env::var(key).ok()
|
||||
resolve_env_with_aliases(key, &[]).map(|(_, value)| value)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a boolean, with a default value if not set or parsing fails.
|
||||
@@ -615,9 +641,27 @@ pub fn get_env_opt_bool(key: &str) -> Option<bool> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Copy supported external-prefix variables such as `MINIO_*` into their
|
||||
/// canonical `RUSTFS_*` names in the current process when the canonical key is
|
||||
/// missing.
|
||||
#[allow(unsafe_code)]
|
||||
pub fn apply_external_env_compat() -> ExternalEnvCompatReport {
|
||||
let report = build_external_env_compat_report();
|
||||
for (source_key, rustfs_key) in &report.mapped_pairs {
|
||||
if let Ok(value) = env::var(source_key) {
|
||||
// Safety: this helper is intended for early startup bootstrap
|
||||
// before any background threads are created.
|
||||
unsafe {
|
||||
env::set_var(rustfs_key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
report
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_external_env_compat_report_from_entries;
|
||||
use super::{apply_external_env_compat, build_external_env_compat_report_from_entries, get_env_str};
|
||||
|
||||
fn source_key(suffix: &str) -> String {
|
||||
let mut key = super::external_env_prefix().to_string();
|
||||
@@ -672,4 +716,29 @@ mod tests {
|
||||
assert_eq!(report.mapped_count(), 0);
|
||||
assert_eq!(report.conflict_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minio_alias_is_used_for_rustfs_reads() {
|
||||
temp_env::with_var("MINIO_ROOT_USER", Some("compat-admin"), || {
|
||||
temp_env::with_var_unset("RUSTFS_ROOT_USER", || {
|
||||
assert_eq!(get_env_str("RUSTFS_ROOT_USER", "default-user"), "compat-admin");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_external_env_compat_copies_missing_rustfs_keys() {
|
||||
temp_env::with_var("MINIO_ROOT_USER", Some("compat-admin"), || {
|
||||
temp_env::with_var_unset("RUSTFS_ROOT_USER", || {
|
||||
let report = apply_external_env_compat();
|
||||
assert!(
|
||||
report
|
||||
.mapped_pairs
|
||||
.iter()
|
||||
.any(|(source_key, rustfs_key)| source_key == "MINIO_ROOT_USER" && rustfs_key == "RUSTFS_ROOT_USER")
|
||||
);
|
||||
assert_eq!(std::env::var("RUSTFS_ROOT_USER").as_deref(), Ok("compat-admin"));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +167,7 @@ aws-sdk-s3 = { workspace = true }
|
||||
aws-config = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true, features = ["test-util"] }
|
||||
temp-env = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
http.workspace = true
|
||||
|
||||
@@ -121,6 +121,26 @@ mod tests {
|
||||
assert_eq!(console_port, 9001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_minio_prefixed_envs_are_accepted_by_parser() {
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("MINIO_VOLUMES", Some("/compat/vol1")),
|
||||
("MINIO_ADDRESS", Some(":9100")),
|
||||
("RUSTFS_VOLUMES", None),
|
||||
("RUSTFS_ADDRESS", None),
|
||||
],
|
||||
|| {
|
||||
let opt = Opt::parse_from(["rustfs"]);
|
||||
assert_eq!(opt.volumes, vec!["/compat/vol1"]);
|
||||
assert_eq!(opt.address, ":9100");
|
||||
assert_eq!(std::env::var("RUSTFS_VOLUMES").as_deref(), Ok("/compat/vol1"));
|
||||
assert_eq!(std::env::var("RUSTFS_ADDRESS").as_deref(), Ok(":9100"));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_volumes_and_disk_layout_parsing() {
|
||||
|
||||
@@ -16,6 +16,7 @@ use clap::builder::NonEmptyStringValueParser;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use const_str::concat;
|
||||
use rustfs_config::RUSTFS_REGION;
|
||||
use rustfs_utils::apply_external_env_compat;
|
||||
use std::path::PathBuf;
|
||||
use std::string::ToString;
|
||||
|
||||
@@ -262,6 +263,7 @@ impl Opt {
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<std::ffi::OsString> + Clone,
|
||||
{
|
||||
let _ = apply_external_env_compat();
|
||||
let args: Vec<String> = args.into_iter().map(|a| a.into().to_string_lossy().into_owned()).collect();
|
||||
let args = preprocess_args_for_legacy(args);
|
||||
let cli = Cli::parse_from(args);
|
||||
@@ -276,6 +278,7 @@ impl Opt {
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<std::ffi::OsString> + Clone,
|
||||
{
|
||||
let _ = apply_external_env_compat();
|
||||
let args: Vec<String> = args.into_iter().map(|a| a.into().to_string_lossy().into_owned()).collect();
|
||||
let args = preprocess_args_for_legacy(args);
|
||||
let cli = Cli::try_parse_from(args)?;
|
||||
|
||||
@@ -70,10 +70,11 @@ use rustfs_iam::{init_iam_sys, init_oidc_sys};
|
||||
use rustfs_metrics::init_metrics_system;
|
||||
use rustfs_obs::{init_obs, set_global_guard};
|
||||
use rustfs_scanner::init_data_scanner;
|
||||
use rustfs_utils::{build_external_env_compat_report, get_env_bool_with_aliases, net::parse_and_resolve_address};
|
||||
use rustfs_utils::{
|
||||
ExternalEnvCompatReport, apply_external_env_compat, get_env_bool_with_aliases, net::parse_and_resolve_address,
|
||||
};
|
||||
use rustls::crypto::aws_lc_rs::default_provider;
|
||||
use std::io::{Error, Result};
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
@@ -82,7 +83,6 @@ const ENV_SCANNER_ENABLED: &str = "RUSTFS_SCANNER_ENABLED";
|
||||
const ENV_SCANNER_ENABLED_DEPRECATED: &str = "RUSTFS_ENABLE_SCANNER";
|
||||
const ENV_HEAL_ENABLED: &str = "RUSTFS_HEAL_ENABLED";
|
||||
const ENV_HEAL_ENABLED_DEPRECATED: &str = "RUSTFS_ENABLE_HEAL";
|
||||
const ENV_EXTERNAL_COMPAT_BOOTSTRAPPED: &str = "_RUSTFS_EXTERNAL_PREFIX_COMPAT_BOOTSTRAPPED";
|
||||
|
||||
#[cfg(all(target_os = "linux", target_env = "gnu", target_arch = "x86_64"))]
|
||||
#[global_allocator]
|
||||
@@ -117,11 +117,7 @@ fn main() {
|
||||
}
|
||||
|
||||
fn bootstrap_external_prefix_compat() -> Result<()> {
|
||||
if std::env::var_os(ENV_EXTERNAL_COMPAT_BOOTSTRAPPED).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let env_compat_report = build_external_env_compat_report();
|
||||
let env_compat_report = apply_external_env_compat();
|
||||
if env_compat_report.conflict_count() > 0 {
|
||||
// RUSTFS_* is the canonical namespace in this codebase, so on key conflicts we keep RUSTFS_*
|
||||
// to preserve explicit user/operator overrides and avoid changing existing runtime behavior.
|
||||
@@ -137,23 +133,21 @@ fn bootstrap_external_prefix_compat() -> Result<()> {
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"[INFO] Applying external-prefix compatibility for {} variable(s) and re-executing process.",
|
||||
env_compat_report.mapped_count()
|
||||
"[INFO] Applying external-prefix compatibility in-process for {} variable(s): {}",
|
||||
env_compat_report.mapped_count(),
|
||||
format_external_prefix_mappings(&env_compat_report)
|
||||
);
|
||||
|
||||
// Re-exec ensures the child process starts with RUSTFS_* already present in its environment,
|
||||
// so all subsequent config/env reads (including early startup paths) observe consistent keys.
|
||||
let mut cmd = Command::new(std::env::current_exe()?);
|
||||
cmd.args(std::env::args_os().skip(1));
|
||||
cmd.env(ENV_EXTERNAL_COMPAT_BOOTSTRAPPED, "1");
|
||||
for (source_key, rustfs_key) in env_compat_report.mapped_pairs {
|
||||
if let Some(value) = std::env::var_os(source_key.as_str()) {
|
||||
cmd.env(rustfs_key, value);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let status = cmd.status()?;
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
fn format_external_prefix_mappings(report: &ExternalEnvCompatReport) -> String {
|
||||
report
|
||||
.mapped_pairs
|
||||
.iter()
|
||||
.map(|(source_key, rustfs_key)| format!("{source_key}->{rustfs_key}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
async fn async_main() -> Result<()> {
|
||||
@@ -221,6 +215,32 @@ async fn async_main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn format_external_prefix_mappings_lists_mapped_pairs() {
|
||||
let report = ExternalEnvCompatReport {
|
||||
mapped_pairs: vec![
|
||||
("MINIO_ROOT_USER".to_string(), "RUSTFS_ROOT_USER".to_string()),
|
||||
(
|
||||
"MINIO_NOTIFY_WEBHOOK_ENABLE_PRIMARY".to_string(),
|
||||
"RUSTFS_NOTIFY_WEBHOOK_ENABLE_PRIMARY".to_string(),
|
||||
),
|
||||
],
|
||||
conflict_keys: Vec::new(),
|
||||
};
|
||||
|
||||
let formatted = format_external_prefix_mappings(&report);
|
||||
|
||||
assert_eq!(
|
||||
formatted,
|
||||
"MINIO_ROOT_USER->RUSTFS_ROOT_USER, MINIO_NOTIFY_WEBHOOK_ENABLE_PRIMARY->RUSTFS_NOTIFY_WEBHOOK_ENABLE_PRIMARY"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(config))]
|
||||
async fn run(config: config::Config) -> Result<()> {
|
||||
debug!("config: {:?}", &config);
|
||||
|
||||
@@ -5,7 +5,7 @@ RUSTFS_VOLUMES="http://node{1...4}:7000/data/rustfs{0...3} http://node{5...8
|
||||
RUSTFS_ADDRESS=":7000"
|
||||
RUSTFS_CONSOLE_ENABLE=true
|
||||
RUST_LOG=warn
|
||||
RUSTFS_OBS_LOG_DIRECTORY="/var/logs/rustfs/"
|
||||
RUSTFS_OBS_LOG_DIRECTORY="./deploy/logs"
|
||||
RUSTFS_NS_SCANNER_INTERVAL=60
|
||||
#RUSTFS_SKIP_BACKGROUND_TASK=true
|
||||
RUSTFS_COMPRESSION_ENABLED=true
|
||||
RUSTFS_COMPRESSION_ENABLED=true
|
||||
|
||||
Reference in New Issue
Block a user