mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
83bacd5b84
Co-authored-by: heihutu <heihutu@gmail.com>
498 lines
23 KiB
Rust
498 lines
23 KiB
Rust
// Copyright 2024 RustFS Team
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
//
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
//
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
|
||
//! Observability configuration for RustFS.
|
||
//!
|
||
//! All configuration is read from environment variables. The canonical list of
|
||
//! variable names and their defaults lives in `rustfs-config/src/observability/mod.rs`.
|
||
//!
|
||
//! Two public structs are provided:
|
||
//! - [`OtelConfig`] — the primary flat configuration that drives every backend.
|
||
//! - [`AppConfig`] — a thin wrapper used when the config is embedded inside a
|
||
//! larger application configuration struct.
|
||
|
||
use rustfs_config::observability::{
|
||
DEFAULT_OBS_ENVIRONMENT_PRODUCTION, DEFAULT_OBS_LOG_CLEANUP_INTERVAL_SECONDS, DEFAULT_OBS_LOG_COMPRESS_OLD_FILES,
|
||
DEFAULT_OBS_LOG_COMPRESSED_FILE_RETENTION_DAYS, DEFAULT_OBS_LOG_COMPRESSION_ALGORITHM, DEFAULT_OBS_LOG_DELETE_EMPTY_FILES,
|
||
DEFAULT_OBS_LOG_DRY_RUN, DEFAULT_OBS_LOG_GZIP_COMPRESSION_LEVEL, DEFAULT_OBS_LOG_MATCH_MODE,
|
||
DEFAULT_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES, DEFAULT_OBS_LOG_MAX_TOTAL_SIZE_BYTES, DEFAULT_OBS_LOG_MIN_FILE_AGE_SECONDS,
|
||
DEFAULT_OBS_LOG_PARALLEL_COMPRESS, DEFAULT_OBS_LOG_PARALLEL_WORKERS, DEFAULT_OBS_LOG_ZSTD_COMPRESSION_LEVEL,
|
||
DEFAULT_OBS_LOG_ZSTD_FALLBACK_TO_GZIP, DEFAULT_OBS_LOG_ZSTD_WORKERS, ENV_OBS_ENDPOINT, ENV_OBS_ENDPOINT_HEADERS,
|
||
ENV_OBS_ENDPOINT_LOGS_HEADERS, ENV_OBS_ENDPOINT_LOGS_TIMEOUT_MILLIS, ENV_OBS_ENDPOINT_METRICS_HEADERS,
|
||
ENV_OBS_ENDPOINT_METRICS_TIMEOUT_MILLIS, ENV_OBS_ENDPOINT_TIMEOUT_MILLIS, ENV_OBS_ENDPOINT_TRACES_HEADERS,
|
||
ENV_OBS_ENDPOINT_TRACES_TIMEOUT_MILLIS, ENV_OBS_ENVIRONMENT, ENV_OBS_LOG_CLEANUP_INTERVAL_SECONDS,
|
||
ENV_OBS_LOG_COMPRESS_OLD_FILES, ENV_OBS_LOG_COMPRESSED_FILE_RETENTION_DAYS, ENV_OBS_LOG_COMPRESSION_ALGORITHM,
|
||
ENV_OBS_LOG_DELETE_EMPTY_FILES, ENV_OBS_LOG_DIRECTORY, ENV_OBS_LOG_DRY_RUN, ENV_OBS_LOG_ENDPOINT,
|
||
ENV_OBS_LOG_EXCLUDE_PATTERNS, ENV_OBS_LOG_FILENAME, ENV_OBS_LOG_GZIP_COMPRESSION_LEVEL, ENV_OBS_LOG_KEEP_FILES,
|
||
ENV_OBS_LOG_MATCH_MODE, ENV_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES, ENV_OBS_LOG_MAX_TOTAL_SIZE_BYTES,
|
||
ENV_OBS_LOG_MIN_FILE_AGE_SECONDS, ENV_OBS_LOG_PARALLEL_COMPRESS, ENV_OBS_LOG_PARALLEL_WORKERS, ENV_OBS_LOG_ROTATION_TIME,
|
||
ENV_OBS_LOG_STDOUT_ENABLED, ENV_OBS_LOG_ZSTD_COMPRESSION_LEVEL, ENV_OBS_LOG_ZSTD_FALLBACK_TO_GZIP, ENV_OBS_LOG_ZSTD_WORKERS,
|
||
ENV_OBS_LOGGER_LEVEL, ENV_OBS_LOGS_EXPORT_ENABLED, ENV_OBS_METER_INTERVAL, ENV_OBS_METRIC_ENDPOINT,
|
||
ENV_OBS_METRICS_EXPORT_ENABLED, ENV_OBS_PROFILING_ENDPOINT, ENV_OBS_PROFILING_EXPORT_ENABLED, ENV_OBS_SAMPLE_RATIO,
|
||
ENV_OBS_SERVICE_NAME, ENV_OBS_SERVICE_VERSION, ENV_OBS_TRACE_ENDPOINT, ENV_OBS_TRACES_EXPORT_ENABLED, ENV_OBS_USE_STDOUT,
|
||
};
|
||
use rustfs_config::{
|
||
APP_NAME, DEFAULT_LOG_KEEP_FILES, DEFAULT_LOG_LEVEL, DEFAULT_LOG_ROTATION_TIME, DEFAULT_OBS_LOG_FILENAME,
|
||
DEFAULT_OBS_LOG_STDOUT_ENABLED, DEFAULT_OBS_LOGS_EXPORT_ENABLED, DEFAULT_OBS_METRICS_EXPORT_ENABLED,
|
||
DEFAULT_OBS_PROFILING_EXPORT_ENABLED, DEFAULT_OBS_TRACES_EXPORT_ENABLED, ENVIRONMENT, METER_INTERVAL, SAMPLE_RATIO,
|
||
SERVICE_VERSION, USE_STDOUT,
|
||
};
|
||
use rustfs_utils::{
|
||
get_env_bool, get_env_bool_with_aliases, get_env_f64, get_env_opt_str, get_env_opt_u64, get_env_str, get_env_u64,
|
||
get_env_usize,
|
||
};
|
||
use serde::{Deserialize, Serialize};
|
||
use std::env;
|
||
#[cfg(test)]
|
||
use std::sync::{Mutex, OnceLock};
|
||
|
||
const LEGACY_ENV_OBS_PROFILING_ENABLED: &str = "RUSTFS_OBS_PROFILING_ENABLED";
|
||
|
||
fn get_env_nonempty_str(name: &str, default: &str) -> String {
|
||
match env::var(name) {
|
||
Ok(value) if !value.trim().is_empty() => value,
|
||
_ => default.to_string(),
|
||
}
|
||
}
|
||
|
||
/// Full observability configuration used by all telemetry backends.
|
||
///
|
||
/// Fields are grouped into three logical sections:
|
||
///
|
||
/// ## OpenTelemetry / OTLP export
|
||
/// Controls whether and where traces, metrics, and logs are exported over the
|
||
/// wire using the OTLP/HTTP protocol.
|
||
///
|
||
/// ## Local logging
|
||
/// Controls the rolling-file appender: directory, filename, rotation policy,
|
||
/// and the number of files to retain.
|
||
///
|
||
/// ## Log cleanup
|
||
/// Controls the background cleanup task: size limits, compression, retention
|
||
/// of compressed archives, exclusion patterns, and dry-run mode.
|
||
///
|
||
/// # Design Notes
|
||
///
|
||
/// - All fields are `Option<T>` to allow partial configuration via environment
|
||
/// variables with sensible defaults provided by constants in `rustfs-config`.
|
||
/// - `log_keep_files` is used to derive the rolling-appender's upper bound on
|
||
/// retained files (if enabled) and to set the cleaner's retained-file ceiling.
|
||
///
|
||
/// # Example
|
||
/// ```no_run
|
||
/// use rustfs_obs::OtelConfig;
|
||
///
|
||
/// // Build from environment variables (typical production usage).
|
||
/// let config = OtelConfig::new();
|
||
///
|
||
/// // Build with an explicit OTLP endpoint.
|
||
/// let config = OtelConfig::extract_otel_config_from_env(
|
||
/// Some("http://otel-collector:4318".to_string())
|
||
/// );
|
||
///
|
||
/// // Compression related env mapping examples:
|
||
/// // RUSTFS_OBS_LOG_COMPRESSION_ALGORITHM=zstd
|
||
/// // RUSTFS_OBS_LOG_PARALLEL_COMPRESS=true
|
||
/// // RUSTFS_OBS_LOG_PARALLEL_WORKERS=6
|
||
/// ```
|
||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||
pub struct OtelConfig {
|
||
// ── OTLP export ──────────────────────────────────────────────────────────
|
||
/// Root OTLP/HTTP endpoint (e.g. `http://otel-collector:4318`).
|
||
/// Per-signal endpoints below take precedence when set.
|
||
pub endpoint: String,
|
||
/// Dedicated trace endpoint; overrides `endpoint` + `/v1/traces` fallback.
|
||
pub trace_endpoint: Option<String>,
|
||
/// Dedicated metrics endpoint; overrides `endpoint` + `/v1/metrics` fallback.
|
||
pub metric_endpoint: Option<String>,
|
||
/// Dedicated log endpoint; overrides `endpoint` + `/v1/logs` fallback.
|
||
pub log_endpoint: Option<String>,
|
||
/// Headers applied to all OTLP signals when using HTTP exporter.
|
||
/// Format: comma-separated `key=value` pairs with URL-encoded values, for
|
||
/// example: `Authorization=Bearer%20abc%20123,X-Scope-OrgID=my-tenant`.
|
||
/// URL-encode reserved characters in values such as spaces, commas, and `=`.
|
||
pub endpoint_headers: Option<String>,
|
||
/// Additional headers for traces; merged on top of `endpoint_headers`.
|
||
/// Uses the same comma-separated `key=value` format with URL-encoded values.
|
||
pub trace_headers: Option<String>,
|
||
/// Additional headers for metrics; merged on top of `endpoint_headers`.
|
||
/// Uses the same comma-separated `key=value` format with URL-encoded values.
|
||
pub metric_headers: Option<String>,
|
||
/// Additional headers for logs; merged on top of `endpoint_headers`.
|
||
/// Uses the same comma-separated `key=value` format with URL-encoded values.
|
||
pub log_headers: Option<String>,
|
||
/// Timeout (milliseconds) for all OTLP HTTP exports.
|
||
pub endpoint_timeout_millis: Option<u64>,
|
||
/// Timeout (milliseconds) for trace OTLP HTTP export.
|
||
pub trace_timeout_millis: Option<u64>,
|
||
/// Timeout (milliseconds) for metrics OTLP HTTP export.
|
||
pub metric_timeout_millis: Option<u64>,
|
||
/// Timeout (milliseconds) for log OTLP HTTP export.
|
||
pub log_timeout_millis: Option<u64>,
|
||
/// Dedicated profiling endpoint.
|
||
pub profiling_endpoint: Option<String>,
|
||
/// Whether to export distributed traces (default: `true`).
|
||
pub traces_export_enabled: Option<bool>,
|
||
/// Whether to export metrics (default: `true`).
|
||
pub metrics_export_enabled: Option<bool>,
|
||
/// Whether to export logs via OTLP (default: `true`).
|
||
pub logs_export_enabled: Option<bool>,
|
||
/// Whether to export profiles via pyroscope (default: `false`).
|
||
pub profiling_export_enabled: Option<bool>,
|
||
/// **[OTLP-only]** Mirror all signals to stdout in addition to OTLP export.
|
||
/// Only applies when an OTLP endpoint is configured.
|
||
pub use_stdout: Option<bool>,
|
||
/// Fraction of traces to sample, `0.0`–`1.0` (default: `0.1`).
|
||
pub sample_ratio: Option<f64>,
|
||
/// Metrics export interval in seconds (default: `15`).
|
||
pub meter_interval: Option<u64>,
|
||
/// OTel `service.name` attribute (default: `APP_NAME`).
|
||
pub service_name: Option<String>,
|
||
/// OTel `service.version` attribute (default: `SERVICE_VERSION`).
|
||
pub service_version: Option<String>,
|
||
/// Deployment environment tag, e.g. `production` or `development`.
|
||
pub environment: Option<String>,
|
||
|
||
// ── Local logging ─────────────────────────────────────────────────────────
|
||
/// Minimum log level directive (default: `info`).
|
||
/// Respects `RUST_LOG` syntax when set via environment.
|
||
pub logger_level: Option<String>,
|
||
/// When `true`, a stdout JSON layer is always attached regardless of the
|
||
/// active backend (default: `false` in production, `true` otherwise).
|
||
pub log_stdout_enabled: Option<bool>,
|
||
/// Directory where rolling log files are written.
|
||
/// When absent or empty, logging falls back to stdout-only mode.
|
||
pub log_directory: Option<String>,
|
||
/// Base name for log files (without date suffix), e.g. `rustfs`.
|
||
/// Used for both rolling-appender naming and cleanup scanning.
|
||
pub log_filename: Option<String>,
|
||
/// Rotation time granularity: `"hourly"` or `"daily"` (default: `"daily"`).
|
||
pub log_rotation_time: Option<String>,
|
||
/// Number of rolling log files to retain (default: `30`).
|
||
/// Used by both the rolling-appender (as a loose upper bound) and the
|
||
/// background cleaner (as the minimum retention count).
|
||
pub log_keep_files: Option<usize>,
|
||
|
||
// ── Log cleanup ───────────────────────────────────────────────────────────
|
||
/// Hard ceiling on the total size (bytes) of all log files (default: 2 GiB).
|
||
pub log_max_total_size_bytes: Option<u64>,
|
||
/// Per-file size ceiling (bytes); `0` means unlimited (default: `0`).
|
||
pub log_max_single_file_size_bytes: Option<u64>,
|
||
/// Compress eligible files with gzip before deletion (default: `true`).
|
||
pub log_compress_old_files: Option<bool>,
|
||
/// Gzip compression level `1`–`9` (default: `6`).
|
||
pub log_gzip_compression_level: Option<u32>,
|
||
/// Compression algorithm: `gzip` or `zstd` (default: `zstd`).
|
||
pub log_compression_algorithm: Option<String>,
|
||
/// Enable crossbeam work-stealing parallel compression (default: `true`).
|
||
pub log_parallel_compress: Option<bool>,
|
||
/// Worker count for parallel compression (default: `6`).
|
||
pub log_parallel_workers: Option<usize>,
|
||
/// Zstd compression level `1`–`21` (default: `8`).
|
||
pub log_zstd_compression_level: Option<i32>,
|
||
/// Fallback to gzip when zstd compression fails (default: `true`).
|
||
pub log_zstd_fallback_to_gzip: Option<bool>,
|
||
/// Internal zstdmt workers per job (default: `1`).
|
||
pub log_zstd_workers: Option<usize>,
|
||
/// Delete compressed archives older than this many days; `0` = keep forever
|
||
/// (default: `30`).
|
||
pub log_compressed_file_retention_days: Option<u64>,
|
||
/// Comma-separated glob patterns for files that must never be cleaned up.
|
||
pub log_exclude_patterns: Option<String>,
|
||
/// Delete zero-byte log files during cleanup (default: `true`).
|
||
pub log_delete_empty_files: Option<bool>,
|
||
/// A file younger than this many seconds is never touched (default: `3600`).
|
||
pub log_min_file_age_seconds: Option<u64>,
|
||
/// How often the background cleanup task runs, in seconds (default: `21600`).
|
||
pub log_cleanup_interval_seconds: Option<u64>,
|
||
/// Log what *would* be deleted without actually removing anything
|
||
/// (default: `false`).
|
||
pub log_dry_run: Option<bool>,
|
||
/// File matching mode: "prefix" or "suffix" (default: "suffix").
|
||
pub log_match_mode: Option<String>,
|
||
}
|
||
|
||
impl OtelConfig {
|
||
/// Build an [`OtelConfig`] from environment variables.
|
||
///
|
||
/// The optional `endpoint` argument sets the root OTLP endpoint. If it is
|
||
/// `None` or an empty string the value is read from the
|
||
/// `RUSTFS_OBS_ENDPOINT` environment variable instead.
|
||
///
|
||
/// When no endpoint is configured at all, `use_stdout` is forced to `true`
|
||
/// so that logs are still visible during development.
|
||
///
|
||
/// # Example
|
||
/// ```no_run
|
||
/// use rustfs_obs::OtelConfig;
|
||
///
|
||
/// // Read everything from env vars.
|
||
/// let config = OtelConfig::extract_otel_config_from_env(None);
|
||
///
|
||
/// // Override the endpoint programmatically.
|
||
/// let config = OtelConfig::extract_otel_config_from_env(
|
||
/// Some("http://localhost:4318".to_string())
|
||
/// );
|
||
/// ```
|
||
pub fn extract_otel_config_from_env(endpoint: Option<String>) -> OtelConfig {
|
||
let endpoint = match endpoint {
|
||
Some(ep) if !ep.is_empty() => ep,
|
||
_ => env::var(ENV_OBS_ENDPOINT).unwrap_or_default(),
|
||
};
|
||
|
||
// Force stdout when there is no remote endpoint so that operators
|
||
// always have *some* log output in the default configuration.
|
||
let use_stdout = if endpoint.is_empty() {
|
||
true
|
||
} else {
|
||
get_env_bool(ENV_OBS_USE_STDOUT, USE_STDOUT)
|
||
};
|
||
|
||
// The canonical log directory is resolved only when explicitly set via
|
||
// environment variable. When absent or empty, logging falls back to
|
||
// stdout-only mode (not file-rolling).
|
||
let log_directory = match std::env::var(ENV_OBS_LOG_DIRECTORY) {
|
||
Ok(val) if !val.is_empty() => Some(val),
|
||
_ => None,
|
||
};
|
||
|
||
// `log_keep_files` is the single source of truth for file retention count.
|
||
// It defaults to `DEFAULT_LOG_KEEP_FILES` (30).
|
||
let mut log_keep_files = get_env_usize(ENV_OBS_LOG_KEEP_FILES, DEFAULT_LOG_KEEP_FILES);
|
||
if log_keep_files == 0 {
|
||
log_keep_files = DEFAULT_LOG_KEEP_FILES;
|
||
}
|
||
let log_keep_files = Some(log_keep_files);
|
||
|
||
// `log_rotation_time` drives the rolling-appender rotation period.
|
||
let log_rotation_time = Some(get_env_str(ENV_OBS_LOG_ROTATION_TIME, DEFAULT_LOG_ROTATION_TIME));
|
||
|
||
OtelConfig {
|
||
// OTLP
|
||
endpoint,
|
||
trace_endpoint: get_env_opt_str(ENV_OBS_TRACE_ENDPOINT),
|
||
metric_endpoint: get_env_opt_str(ENV_OBS_METRIC_ENDPOINT),
|
||
log_endpoint: get_env_opt_str(ENV_OBS_LOG_ENDPOINT),
|
||
endpoint_headers: get_env_opt_str(ENV_OBS_ENDPOINT_HEADERS),
|
||
trace_headers: get_env_opt_str(ENV_OBS_ENDPOINT_TRACES_HEADERS),
|
||
metric_headers: get_env_opt_str(ENV_OBS_ENDPOINT_METRICS_HEADERS),
|
||
log_headers: get_env_opt_str(ENV_OBS_ENDPOINT_LOGS_HEADERS),
|
||
endpoint_timeout_millis: get_env_opt_u64(ENV_OBS_ENDPOINT_TIMEOUT_MILLIS),
|
||
trace_timeout_millis: get_env_opt_u64(ENV_OBS_ENDPOINT_TRACES_TIMEOUT_MILLIS),
|
||
metric_timeout_millis: get_env_opt_u64(ENV_OBS_ENDPOINT_METRICS_TIMEOUT_MILLIS),
|
||
log_timeout_millis: get_env_opt_u64(ENV_OBS_ENDPOINT_LOGS_TIMEOUT_MILLIS),
|
||
profiling_endpoint: get_env_opt_str(ENV_OBS_PROFILING_ENDPOINT),
|
||
traces_export_enabled: Some(get_env_bool(ENV_OBS_TRACES_EXPORT_ENABLED, DEFAULT_OBS_TRACES_EXPORT_ENABLED)),
|
||
metrics_export_enabled: Some(get_env_bool(ENV_OBS_METRICS_EXPORT_ENABLED, DEFAULT_OBS_METRICS_EXPORT_ENABLED)),
|
||
logs_export_enabled: Some(get_env_bool(ENV_OBS_LOGS_EXPORT_ENABLED, DEFAULT_OBS_LOGS_EXPORT_ENABLED)),
|
||
profiling_export_enabled: Some(get_env_bool_with_aliases(
|
||
ENV_OBS_PROFILING_EXPORT_ENABLED,
|
||
&[LEGACY_ENV_OBS_PROFILING_ENABLED],
|
||
DEFAULT_OBS_PROFILING_EXPORT_ENABLED,
|
||
)),
|
||
use_stdout: Some(use_stdout),
|
||
sample_ratio: Some(get_env_f64(ENV_OBS_SAMPLE_RATIO, SAMPLE_RATIO)),
|
||
meter_interval: Some(get_env_u64(ENV_OBS_METER_INTERVAL, METER_INTERVAL)),
|
||
service_name: Some(get_env_str(ENV_OBS_SERVICE_NAME, APP_NAME)),
|
||
service_version: Some(get_env_str(ENV_OBS_SERVICE_VERSION, SERVICE_VERSION)),
|
||
environment: Some(get_env_str(ENV_OBS_ENVIRONMENT, ENVIRONMENT)),
|
||
// Local logging
|
||
logger_level: Some(get_env_str(ENV_OBS_LOGGER_LEVEL, DEFAULT_LOG_LEVEL)),
|
||
log_stdout_enabled: Some(get_env_bool(ENV_OBS_LOG_STDOUT_ENABLED, DEFAULT_OBS_LOG_STDOUT_ENABLED)),
|
||
log_directory,
|
||
log_filename: Some(get_env_nonempty_str(ENV_OBS_LOG_FILENAME, DEFAULT_OBS_LOG_FILENAME)),
|
||
log_rotation_time,
|
||
log_keep_files,
|
||
// Log cleanup
|
||
log_max_total_size_bytes: Some(get_env_u64(ENV_OBS_LOG_MAX_TOTAL_SIZE_BYTES, DEFAULT_OBS_LOG_MAX_TOTAL_SIZE_BYTES)),
|
||
log_max_single_file_size_bytes: Some(get_env_u64(
|
||
ENV_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES,
|
||
DEFAULT_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES,
|
||
)),
|
||
log_compress_old_files: Some(get_env_bool(ENV_OBS_LOG_COMPRESS_OLD_FILES, DEFAULT_OBS_LOG_COMPRESS_OLD_FILES)),
|
||
log_gzip_compression_level: Some(get_env_u64(
|
||
ENV_OBS_LOG_GZIP_COMPRESSION_LEVEL,
|
||
DEFAULT_OBS_LOG_GZIP_COMPRESSION_LEVEL as u64,
|
||
) as u32),
|
||
log_compression_algorithm: Some(get_env_str(
|
||
ENV_OBS_LOG_COMPRESSION_ALGORITHM,
|
||
DEFAULT_OBS_LOG_COMPRESSION_ALGORITHM,
|
||
)),
|
||
log_parallel_compress: Some(get_env_bool(ENV_OBS_LOG_PARALLEL_COMPRESS, DEFAULT_OBS_LOG_PARALLEL_COMPRESS)),
|
||
log_parallel_workers: Some(get_env_usize(ENV_OBS_LOG_PARALLEL_WORKERS, DEFAULT_OBS_LOG_PARALLEL_WORKERS)),
|
||
log_zstd_compression_level: Some(get_env_u64(
|
||
ENV_OBS_LOG_ZSTD_COMPRESSION_LEVEL,
|
||
DEFAULT_OBS_LOG_ZSTD_COMPRESSION_LEVEL as u64,
|
||
) as i32),
|
||
log_zstd_fallback_to_gzip: Some(get_env_bool(
|
||
ENV_OBS_LOG_ZSTD_FALLBACK_TO_GZIP,
|
||
DEFAULT_OBS_LOG_ZSTD_FALLBACK_TO_GZIP,
|
||
)),
|
||
log_zstd_workers: Some(get_env_usize(ENV_OBS_LOG_ZSTD_WORKERS, DEFAULT_OBS_LOG_ZSTD_WORKERS)),
|
||
log_compressed_file_retention_days: Some(get_env_u64(
|
||
ENV_OBS_LOG_COMPRESSED_FILE_RETENTION_DAYS,
|
||
DEFAULT_OBS_LOG_COMPRESSED_FILE_RETENTION_DAYS,
|
||
)),
|
||
log_exclude_patterns: get_env_opt_str(ENV_OBS_LOG_EXCLUDE_PATTERNS),
|
||
log_delete_empty_files: Some(get_env_bool(ENV_OBS_LOG_DELETE_EMPTY_FILES, DEFAULT_OBS_LOG_DELETE_EMPTY_FILES)),
|
||
log_min_file_age_seconds: Some(get_env_u64(ENV_OBS_LOG_MIN_FILE_AGE_SECONDS, DEFAULT_OBS_LOG_MIN_FILE_AGE_SECONDS)),
|
||
log_cleanup_interval_seconds: Some(get_env_u64(
|
||
ENV_OBS_LOG_CLEANUP_INTERVAL_SECONDS,
|
||
DEFAULT_OBS_LOG_CLEANUP_INTERVAL_SECONDS,
|
||
)),
|
||
log_dry_run: Some(get_env_bool(ENV_OBS_LOG_DRY_RUN, DEFAULT_OBS_LOG_DRY_RUN)),
|
||
log_match_mode: Some(get_env_str(ENV_OBS_LOG_MATCH_MODE, DEFAULT_OBS_LOG_MATCH_MODE)),
|
||
}
|
||
}
|
||
|
||
/// Create a new [`OtelConfig`] populated entirely from environment variables.
|
||
///
|
||
/// Equivalent to `OtelConfig::extract_otel_config_from_env(None)`.
|
||
///
|
||
/// # Example
|
||
/// ```no_run
|
||
/// use rustfs_obs::OtelConfig;
|
||
/// let config = OtelConfig::new();
|
||
/// ```
|
||
pub fn new() -> Self {
|
||
Self::extract_otel_config_from_env(None)
|
||
}
|
||
}
|
||
|
||
impl Default for OtelConfig {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
/// Top-level application configuration that embeds [`OtelConfig`].
|
||
///
|
||
/// Use this when the observability config lives inside a larger `AppConfig`
|
||
/// struct, e.g. when deserialising from a config file that also contains other
|
||
/// application settings.
|
||
///
|
||
/// # Example
|
||
/// ```
|
||
/// use rustfs_obs::AppConfig;
|
||
///
|
||
/// let config = AppConfig::new_with_endpoint(None);
|
||
/// ```
|
||
#[derive(Debug, Deserialize, Clone)]
|
||
pub struct AppConfig {
|
||
pub observability: OtelConfig,
|
||
}
|
||
|
||
impl AppConfig {
|
||
/// Create an [`AppConfig`] with all observability settings read from the
|
||
/// environment (no explicit endpoint override).
|
||
pub fn new() -> Self {
|
||
Self {
|
||
observability: OtelConfig::default(),
|
||
}
|
||
}
|
||
|
||
/// Create an [`AppConfig`] with an explicit OTLP endpoint.
|
||
///
|
||
/// # Arguments
|
||
/// * `endpoint` - Root OTLP/HTTP endpoint URL, or `None` to read from env.
|
||
///
|
||
/// # Example
|
||
/// ```no_run
|
||
/// use rustfs_obs::AppConfig;
|
||
///
|
||
/// let config = AppConfig::new_with_endpoint(Some("http://localhost:4318".to_string()));
|
||
/// ```
|
||
pub fn new_with_endpoint(endpoint: Option<String>) -> Self {
|
||
Self {
|
||
observability: OtelConfig::extract_otel_config_from_env(endpoint),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Default for AppConfig {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
/// Returns `true` when the current runtime environment is `production`.
|
||
///
|
||
/// Reads the `RUSTFS_OBS_ENVIRONMENT` environment variable and compares it
|
||
/// case-insensitively against the string `"production"`.
|
||
pub fn is_production_environment() -> bool {
|
||
get_env_str(ENV_OBS_ENVIRONMENT, ENVIRONMENT).eq_ignore_ascii_case(DEFAULT_OBS_ENVIRONMENT_PRODUCTION)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
static PROFILING_ENV_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||
|
||
fn with_profiling_env_lock<F>(f: F)
|
||
where
|
||
F: FnOnce(),
|
||
{
|
||
let _guard = PROFILING_ENV_TEST_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap();
|
||
f();
|
||
}
|
||
|
||
fn extract_profiling_export_enabled() -> Option<bool> {
|
||
OtelConfig::extract_otel_config_from_env(None).profiling_export_enabled
|
||
}
|
||
|
||
#[test]
|
||
fn profiling_export_defaults_to_disabled_when_unset() {
|
||
with_profiling_env_lock(|| {
|
||
temp_env::with_var_unset(ENV_OBS_PROFILING_EXPORT_ENABLED, || {
|
||
temp_env::with_var_unset(LEGACY_ENV_OBS_PROFILING_ENABLED, || {
|
||
assert_eq!(extract_profiling_export_enabled(), Some(DEFAULT_OBS_PROFILING_EXPORT_ENABLED));
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn profiling_export_accepts_legacy_env_alias() {
|
||
with_profiling_env_lock(|| {
|
||
temp_env::with_var_unset(ENV_OBS_PROFILING_EXPORT_ENABLED, || {
|
||
temp_env::with_var(LEGACY_ENV_OBS_PROFILING_ENABLED, Some("true"), || {
|
||
assert_eq!(extract_profiling_export_enabled(), Some(true));
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn canonical_profiling_toggle_has_priority_over_legacy_alias() {
|
||
with_profiling_env_lock(|| {
|
||
temp_env::with_var(LEGACY_ENV_OBS_PROFILING_ENABLED, Some("true"), || {
|
||
temp_env::with_var(ENV_OBS_PROFILING_EXPORT_ENABLED, Some("false"), || {
|
||
assert_eq!(extract_profiling_export_enabled(), Some(false));
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn empty_log_filename_falls_back_to_default() {
|
||
with_profiling_env_lock(|| {
|
||
temp_env::with_var(ENV_OBS_LOG_FILENAME, Some(""), || {
|
||
let config = OtelConfig::extract_otel_config_from_env(None);
|
||
assert_eq!(config.log_filename.as_deref(), Some(DEFAULT_OBS_LOG_FILENAME));
|
||
});
|
||
});
|
||
}
|
||
}
|