mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-22 12:26:37 +00:00
[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 <housemecn@gmail.com> Co-authored-by: loverustfs <155562731+loverustfs@users.noreply.github.com>
This commit is contained in:
Generated
+38
-7
@@ -4021,6 +4021,16 @@ version = "0.2.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
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]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -4244,6 +4254,15 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mimalloc"
|
||||||
|
version = "0.1.48"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8"
|
||||||
|
dependencies = [
|
||||||
|
"libmimalloc-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
@@ -6015,6 +6034,7 @@ dependencies = [
|
|||||||
"hyper-util",
|
"hyper-util",
|
||||||
"libsystemd",
|
"libsystemd",
|
||||||
"matchit",
|
"matchit",
|
||||||
|
"mimalloc",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
@@ -6983,10 +7003,11 @@ checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.219"
|
version = "1.0.223"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
checksum = "a505d71960adde88e293da5cb5eda57093379f64e61cf77bf0e6a63af07a7bac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -7024,10 +7045,19 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_core"
|
||||||
version = "1.0.219"
|
version = "1.0.223"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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 = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -7056,14 +7086,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.143"
|
version = "1.0.145"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
|
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+2
-2
@@ -209,8 +209,8 @@ rustls-pki-types = "1.12.0"
|
|||||||
rustls-pemfile = "2.2.0"
|
rustls-pemfile = "2.2.0"
|
||||||
s3s = { version = "0.12.0-minio-preview.3" }
|
s3s = { version = "0.12.0-minio-preview.3" }
|
||||||
schemars = "1.0.4"
|
schemars = "1.0.4"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.223", features = ["derive"] }
|
||||||
serde_json = { version = "1.0.143", features = ["raw_value"] }
|
serde_json = { version = "1.0.145", features = ["raw_value"] }
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
serial_test = "3.2.0"
|
serial_test = "3.2.0"
|
||||||
sha1 = "0.10.6"
|
sha1 = "0.10.6"
|
||||||
|
|||||||
@@ -33,3 +33,47 @@ pub const ENV_AUDIT_LOGGER_QUEUE_CAPACITY: &str = "RUSTFS_AUDIT_LOGGER_QUEUE_CAP
|
|||||||
|
|
||||||
// Default values for observability configuration
|
// Default values for observability configuration
|
||||||
pub const DEFAULT_AUDIT_LOGGER_QUEUE_CAPACITY: usize = 10000;
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+46
-1
@@ -21,12 +21,57 @@
|
|||||||
|
|
||||||
## ✨ Features
|
## ✨ 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
|
- OpenTelemetry integration for distributed tracing
|
||||||
- Prometheus metrics collection and exposition
|
- Prometheus metrics collection and exposition
|
||||||
- Structured logging with configurable levels
|
- Structured logging with configurable levels and rotation
|
||||||
- Performance profiling and analytics
|
- Performance profiling and analytics
|
||||||
- Real-time health checks and status monitoring
|
- Real-time health checks and status monitoring
|
||||||
- Custom dashboards and alerting integration
|
- 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
|
## 📚 Documentation
|
||||||
|
|
||||||
|
|||||||
+303
-107
@@ -29,7 +29,7 @@ use opentelemetry_semantic_conventions::{
|
|||||||
SCHEMA_URL,
|
SCHEMA_URL,
|
||||||
attribute::{DEPLOYMENT_ENVIRONMENT_NAME, NETWORK_LOCAL_ADDRESS, SERVICE_VERSION as OTEL_SERVICE_VERSION},
|
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::{
|
use rustfs_config::{
|
||||||
APP_NAME, DEFAULT_LOG_KEEP_FILES, DEFAULT_LOG_LEVEL, ENVIRONMENT, METER_INTERVAL, SAMPLE_RATIO, SERVICE_VERSION, USE_STDOUT,
|
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<opentelemetry_stdout:
|
|||||||
pub(crate) fn init_telemetry(config: &OtelConfig) -> OtelGuard {
|
pub(crate) fn init_telemetry(config: &OtelConfig) -> OtelGuard {
|
||||||
// avoid repeated access to configuration fields
|
// avoid repeated access to configuration fields
|
||||||
let endpoint = &config.endpoint;
|
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 meter_interval = config.meter_interval.unwrap_or(METER_INTERVAL);
|
||||||
let logger_level = config.logger_level.as_deref().unwrap_or(DEFAULT_LOG_LEVEL);
|
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 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
|
// Configure flexi_logger to cut by time and size
|
||||||
let mut flexi_logger_handle = None;
|
let mut flexi_logger_handle = None;
|
||||||
@@ -144,7 +156,7 @@ pub(crate) fn init_telemetry(config: &OtelConfig) -> OtelGuard {
|
|||||||
// initialize tracer provider
|
// initialize tracer provider
|
||||||
let tracer_provider = {
|
let tracer_provider = {
|
||||||
let sample_ratio = config.sample_ratio.unwrap_or(SAMPLE_RATIO);
|
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)
|
Sampler::TraceIdRatioBased(sample_ratio)
|
||||||
} else {
|
} else {
|
||||||
Sampler::AlwaysOn
|
Sampler::AlwaysOn
|
||||||
@@ -249,7 +261,7 @@ pub(crate) fn init_telemetry(config: &OtelConfig) -> OtelGuard {
|
|||||||
.with_line_number(true);
|
.with_line_number(true);
|
||||||
|
|
||||||
// Only add full span events tracking in the development environment
|
// Only add full span events tracking in the development environment
|
||||||
if environment != ENVIRONMENT {
|
if !is_production {
|
||||||
layer = layer.with_span_events(FmtSpan::FULL);
|
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 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(build_env_filter(logger_level, None));
|
||||||
let otel_layer = OpenTelemetryTracingBridge::new(&logger_provider).with_filter(otel_filter);
|
|
||||||
let tracer = tracer_provider.tracer(Cow::Borrowed(service_name).to_string());
|
let tracer = tracer_provider.tracer(Cow::Borrowed(service_name).to_string());
|
||||||
|
|
||||||
// Configure registry to avoid repeated calls to filter methods
|
// 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),
|
tracer_provider: Some(tracer_provider),
|
||||||
meter_provider: Some(meter_provider),
|
meter_provider: Some(meter_provider),
|
||||||
logger_provider: Some(logger_provider),
|
logger_provider: Some(logger_provider),
|
||||||
_flexi_logger_handles: flexi_logger_handle,
|
_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
|
// Obtain the log directory and file name configuration
|
||||||
let keep_files = config.log_keep_files.unwrap_or(DEFAULT_LOG_KEEP_FILES);
|
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
|
// Enhanced error handling for directory creation
|
||||||
let log_spec = LogSpecification::parse(logger_level).unwrap_or(LogSpecification::info());
|
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
|
#[cfg(unix)]
|
||||||
let level_filter = match logger_level.to_lowercase().as_str() {
|
{
|
||||||
|
// 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,
|
"trace" => flexi_logger::Duplicate::Trace,
|
||||||
"debug" => flexi_logger::Duplicate::Debug,
|
"debug" => flexi_logger::Duplicate::Debug,
|
||||||
"info" => flexi_logger::Duplicate::Info,
|
"info" => flexi_logger::Duplicate::Info,
|
||||||
@@ -359,56 +388,85 @@ pub(crate) fn init_telemetry(config: &OtelConfig) -> OtelGuard {
|
|||||||
"error" => flexi_logger::Duplicate::Error,
|
"error" => flexi_logger::Duplicate::Error,
|
||||||
"off" => flexi_logger::Duplicate::None,
|
"off" => flexi_logger::Duplicate::None,
|
||||||
_ => flexi_logger::Duplicate::Info, // the default is info
|
_ => flexi_logger::Duplicate::Info, // the default is info
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Configure the flexi_logger
|
// Choose write mode based on environment
|
||||||
let flexi_logger_result = flexi_logger::Logger::try_with_env_or_str(logger_level)
|
let write_mode = if is_production {
|
||||||
.unwrap_or_else(|e| {
|
WriteMode::Async
|
||||||
eprintln!("Invalid logger level: {logger_level}, using default: {DEFAULT_LOG_LEVEL}, failed error: {e:?}");
|
} else {
|
||||||
flexi_logger::Logger::with(log_spec.clone())
|
WriteMode::BufferAndFlush
|
||||||
})
|
};
|
||||||
.log_to_file(
|
|
||||||
FileSpec::default()
|
// Configure the flexi_logger with enhanced error handling
|
||||||
.directory(log_directory)
|
let mut flexi_logger_builder = flexi_logger::Logger::try_with_env_or_str(logger_level)
|
||||||
.basename(log_filename)
|
.unwrap_or_else(|e| {
|
||||||
.suppress_timestamp(),
|
eprintln!("WARNING: Invalid logger configuration '{logger_level}': {e:?}");
|
||||||
)
|
eprintln!("Falling back to default configuration with level: {DEFAULT_LOG_LEVEL}");
|
||||||
.rotate(rotation_criterion, Naming::TimestampsDirect, Cleanup::KeepLogFiles(keep_files.into()))
|
flexi_logger::Logger::with(log_spec.clone())
|
||||||
.format_for_files(format_for_file) // Add a custom formatting function for file output
|
})
|
||||||
.duplicate_to_stdout(level_filter) // Use dynamic levels
|
.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
|
.format_for_stdout(format_with_color) // Add a custom formatting function for terminal output
|
||||||
.write_mode(WriteMode::BufferAndFlush)
|
.print_message(); // Startup information output to console
|
||||||
.append() // Avoid clearing existing logs at startup
|
}
|
||||||
.print_message() // Startup information output to console
|
|
||||||
.start();
|
|
||||||
|
|
||||||
if let Ok(logger) = flexi_logger_result {
|
let flexi_logger_result = flexi_logger_builder.start();
|
||||||
// Save the logger handle to keep the logging
|
|
||||||
flexi_logger_handle = Some(logger);
|
|
||||||
|
|
||||||
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
|
// Environment-aware success messages
|
||||||
match (config.log_rotation_time.as_deref(), config.log_rotation_size_mb) {
|
if is_production {
|
||||||
(Some(time), Some(size)) => eprintln!(
|
eprintln!("Production logging initialized: file-only mode to {log_directory}/{log_filename}.log");
|
||||||
"Log rotation configured for: every {time} or when size exceeds {size}MB, keeping {keep_files} files"
|
eprintln!("Stdout logging disabled in production environment for security and log aggregation.");
|
||||||
),
|
|
||||||
(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 {
|
} 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 {
|
// Log rotation configuration details
|
||||||
tracer_provider: None,
|
match (config.log_rotation_time.as_deref(), config.log_rotation_size_mb) {
|
||||||
meter_provider: None,
|
(Some(time), Some(size)) => {
|
||||||
logger_provider: None,
|
eprintln!("Log rotation configured for: every {time} or when size exceeds {size}MB, keeping {keep_files} files")
|
||||||
_flexi_logger_handles: flexi_logger_handle,
|
}
|
||||||
|
(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()
|
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<bool>,
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -122,6 +122,9 @@ libsystemd.workspace = true
|
|||||||
[target.'cfg(all(target_os = "linux", target_env = "gnu"))'.dependencies]
|
[target.'cfg(all(target_os = "linux", target_env = "gnu"))'.dependencies]
|
||||||
tikv-jemallocator = "0.6"
|
tikv-jemallocator = "0.6"
|
||||||
|
|
||||||
|
[target.'cfg(all(target_os = "linux", target_env = "musl"))'.dependencies]
|
||||||
|
mimalloc = "0.1"
|
||||||
|
|
||||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||||
pprof = { version = "0.15.0", features = ["flamegraph", "protobuf-codec"] }
|
pprof = { version = "0.15.0", features = ["flamegraph", "protobuf-codec"] }
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ use tracing::{debug, error, info, instrument, warn};
|
|||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
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#"
|
const LOGO: &str = r#"
|
||||||
|
|
||||||
░█▀▄░█░█░█▀▀░▀█▀░█▀▀░█▀▀
|
░█▀▄░█░█░█▀▀░▀█▀░█▀▀░█▀▀
|
||||||
|
|||||||
Reference in New Issue
Block a user