Files
rustfs/crates/obs/src/telemetry/dial9/config.rs
T
houseme c006f84461 feat(info): report all rustfs features (#6722)
* feat(info): report all rustfs features

Co-Authored-By: heihutu <heihutu@gmail.com>

* chore(deps): update s3s revision

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs): adapt dial9 telemetry API

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-27 14:45:50 +08:00

253 lines
9.1 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.
//! 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());
}
}
}