// 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. //! Configuration for dial9 Tokio runtime telemetry. //! //! This module carries no dependency on the `dial9-tokio-telemetry` crate, so //! it compiles identically whether or not the `dial9` feature is enabled. That //! lets callers read the configured state (and export metrics about it) from a //! binary that was built without telemetry support. use super::state::dial9_runtime_state; use rustfs_config::{ DEFAULT_RUNTIME_DIAL9_ENABLED, DEFAULT_RUNTIME_DIAL9_FILE_PREFIX, DEFAULT_RUNTIME_DIAL9_MAX_FILE_SIZE, DEFAULT_RUNTIME_DIAL9_OUTPUT_DIR, DEFAULT_RUNTIME_DIAL9_ROTATION_COUNT, ENV_RUNTIME_DIAL9_ENABLED, ENV_RUNTIME_DIAL9_FILE_PREFIX, ENV_RUNTIME_DIAL9_MAX_FILE_SIZE, ENV_RUNTIME_DIAL9_OUTPUT_DIR, ENV_RUNTIME_DIAL9_ROTATION_COUNT, ENV_RUNTIME_DIAL9_S3_BUCKET, ENV_RUNTIME_DIAL9_S3_PREFIX, }; use rustfs_utils::{get_env_bool, get_env_opt_str, get_env_opt_u64, get_env_opt_usize, get_env_str}; use std::path::PathBuf; use tracing::warn; use super::{EVENT_DIAL9_STATE, LOG_COMPONENT_OBS, LOG_SUBSYSTEM_DIAL9}; /// Whether this binary was compiled with dial9 telemetry support. /// /// Telemetry requires both the `dial9` cargo feature and `--cfg tokio_unstable`; /// the crate's build script enforces that pairing, so this single flag is /// sufficient to describe compile-time support. pub const fn is_supported() -> bool { cfg!(feature = "dial9") } /// Whether dial9 telemetry is requested via environment configuration. /// /// This reports operator intent and is independent of whether the binary can /// honour it. Use [`is_enabled`] to test whether telemetry will actually run. pub fn is_configured() -> bool { get_env_bool(ENV_RUNTIME_DIAL9_ENABLED, DEFAULT_RUNTIME_DIAL9_ENABLED) } /// Whether dial9 telemetry will actually run: requested *and* compiled in. pub fn is_enabled() -> bool { is_supported() && is_configured() } fn sanitize_max_file_size(bytes: u64) -> u64 { bytes.max(1) } fn sanitize_rotation_count(count: usize) -> usize { count.max(1) } /// Total on-disk byte budget for the trace family, saturating on overflow. pub(super) fn total_rotation_size(max_file_size: u64, rotation_count: usize) -> u64 { max_file_size.saturating_mul(u64::try_from(rotation_count).unwrap_or(u64::MAX)) } /// Configuration for dial9 Tokio telemetry. #[derive(Debug, Clone)] pub struct Dial9Config { /// Whether dial9 telemetry is enabled pub enabled: bool, /// Directory where trace files are written pub output_dir: String, /// Trace family name under the output directory pub file_prefix: String, /// Maximum size of each trace file in bytes pub max_file_size: u64, /// Number of rotated files to keep pub rotation_count: usize, } impl Default for Dial9Config { fn default() -> Self { Self { enabled: DEFAULT_RUNTIME_DIAL9_ENABLED, output_dir: DEFAULT_RUNTIME_DIAL9_OUTPUT_DIR.to_string(), file_prefix: DEFAULT_RUNTIME_DIAL9_FILE_PREFIX.to_string(), max_file_size: DEFAULT_RUNTIME_DIAL9_MAX_FILE_SIZE, rotation_count: DEFAULT_RUNTIME_DIAL9_ROTATION_COUNT, } } } impl Dial9Config { /// Create configuration from environment variables, recording the resulting /// trace location so metrics can report it even on a disabled binary. pub fn from_env() -> Self { let config = if is_enabled() { Self::from_env_enabled() } else { Self::default() }; dial9_runtime_state().record_config(&config); config } /// Build the enabled configuration from the environment, warning about any /// knob that is set but cannot be honoured. fn from_env_enabled() -> Self { let raw_max_file_size = get_env_opt_u64(ENV_RUNTIME_DIAL9_MAX_FILE_SIZE); let raw_rotation_count = get_env_opt_usize(ENV_RUNTIME_DIAL9_ROTATION_COUNT); if raw_max_file_size == Some(0) { warn!( event = EVENT_DIAL9_STATE, component = LOG_COMPONENT_OBS, subsystem = LOG_SUBSYSTEM_DIAL9, result = "invalid_max_file_size", fallback_bytes = 1_u64, "dial9 state changed" ); } if raw_rotation_count == Some(0) { warn!( event = EVENT_DIAL9_STATE, component = LOG_COMPONENT_OBS, subsystem = LOG_SUBSYSTEM_DIAL9, result = "invalid_rotation_count", fallback_count = 1_usize, "dial9 state changed" ); } // S3 upload is not built (see `warn_unusable_s3_upload`); read the knobs // only to warn, and drop them rather than carrying dead configuration. warn_unusable_s3_upload( get_env_opt_str(ENV_RUNTIME_DIAL9_S3_BUCKET) .filter(|s| !s.is_empty()) .as_deref(), get_env_opt_str(ENV_RUNTIME_DIAL9_S3_PREFIX) .filter(|s| !s.is_empty()) .as_deref(), ); Self { enabled: true, output_dir: get_env_str(ENV_RUNTIME_DIAL9_OUTPUT_DIR, DEFAULT_RUNTIME_DIAL9_OUTPUT_DIR), file_prefix: get_env_str(ENV_RUNTIME_DIAL9_FILE_PREFIX, DEFAULT_RUNTIME_DIAL9_FILE_PREFIX), max_file_size: sanitize_max_file_size(raw_max_file_size.unwrap_or(DEFAULT_RUNTIME_DIAL9_MAX_FILE_SIZE)), rotation_count: sanitize_rotation_count(raw_rotation_count.unwrap_or(DEFAULT_RUNTIME_DIAL9_ROTATION_COUNT)), } } /// Get the trace family directory for rotating trace segments. pub fn base_path(&self) -> PathBuf { PathBuf::from(&self.output_dir).join(&self.file_prefix) } /// Total on-disk byte budget across all retained segments. pub fn total_disk_budget(&self) -> u64 { total_rotation_size(self.max_file_size, self.rotation_count) } } /// S3 upload of sealed segments is not available in any build. /// /// dial9's `worker-s3` feature depends on aws-sdk-s3-transfer-manager 0.1.3, /// which pins hyper-rustls 0.24 / rustls-webpki 0.101.7 — a webpki carrying /// RUSTSEC-2026-0098, -0099 and -0104. Cargo cannot drop a transitive /// dependency, so this needs an upstream fix (rustfs/backlog#1157, D9-14). /// /// Warn loudly rather than silently discarding a configured bucket: an operator /// who set these expects their traces to be uploaded somewhere. fn warn_unusable_s3_upload(s3_bucket: Option<&str>, s3_prefix: Option<&str>) { if s3_bucket.is_none() && s3_prefix.is_none() { return; } warn!( event = EVENT_DIAL9_STATE, component = LOG_COMPONENT_OBS, subsystem = LOG_SUBSYSTEM_DIAL9, result = "s3_upload_unsupported", s3_bucket = s3_bucket.unwrap_or(""), s3_prefix = s3_prefix.unwrap_or(""), reason = "dial9 worker-s3 depends on a vulnerable rustls-webpki (RUSTSEC-2026-0098/0099/0104)", remedy = "collect trace segments from the output directory instead", "dial9 state changed" ); } #[cfg(test)] mod tests { use super::*; #[test] fn sanitizers_reject_zero_values() { assert_eq!(sanitize_max_file_size(0), 1); assert_eq!(sanitize_rotation_count(0), 1); } #[test] fn total_rotation_size_saturates() { assert_eq!(total_rotation_size(u64::MAX, 2), u64::MAX); } #[test] fn default_config_is_disabled() { let config = Dial9Config::default(); assert!(!config.enabled); assert_eq!(config.output_dir, DEFAULT_RUNTIME_DIAL9_OUTPUT_DIR); assert_eq!(config.file_prefix, DEFAULT_RUNTIME_DIAL9_FILE_PREFIX); assert_eq!(config.max_file_size, DEFAULT_RUNTIME_DIAL9_MAX_FILE_SIZE); assert_eq!(config.rotation_count, DEFAULT_RUNTIME_DIAL9_ROTATION_COUNT); } #[test] fn base_path_joins_prefix() { let config = Dial9Config { output_dir: "/tmp/telemetry".to_string(), file_prefix: "rustfs".to_string(), ..Default::default() }; assert_eq!(config.base_path(), PathBuf::from("/tmp/telemetry/rustfs")); } #[test] fn total_disk_budget_multiplies_file_size_by_rotation_count() { let config = Dial9Config { max_file_size: 100, rotation_count: 10, ..Default::default() }; assert_eq!(config.total_disk_budget(), 1_000); } #[test] fn is_enabled_requires_compile_time_support() { // `is_enabled` is the conjunction of the two; when support is absent it // must stay false no matter what the environment requests. if !is_supported() { assert!(!is_enabled()); } } }