From 1df874b7249497c20e091269fc2aa7e443433bb7 Mon Sep 17 00:00:00 2001 From: Hauser Date: Thu, 8 Oct 2026 21:27:24 +0800 Subject: [PATCH] fix(obs): pass RustFS build version to telemetry (#8425) fix(obs): align telemetry version with build identity Co-authored-by: zhi22915 --- crates/config/src/constants/app.rs | 16 ++-------- crates/obs/README.md | 6 ++-- crates/obs/examples/server.rs | 2 +- crates/obs/src/config.rs | 42 +++++++++++++++++++++++++-- crates/obs/src/global.rs | 19 ++++++------ crates/obs/src/lib.rs | 2 +- crates/obs/src/telemetry/resource.rs | 22 ++++++++++++-- rustfs/src/startup_runtime_sources.rs | 2 +- 8 files changed, 77 insertions(+), 34 deletions(-) diff --git a/crates/config/src/constants/app.rs b/crates/config/src/constants/app.rs index 2d4044aa4..7addb89bc 100644 --- a/crates/config/src/constants/app.rs +++ b/crates/config/src/constants/app.rs @@ -18,10 +18,8 @@ use const_str::concat; /// Default value: RustFS /// Environment variable: RUSTFS_APP_NAME pub const APP_NAME: &str = "RustFS"; -/// Application version -/// Default value: 1.0.0 -/// Environment variable: RUSTFS_VERSION -pub const VERSION: &str = "1.0.0"; +/// Cargo package version for this workspace member. +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Default configuration logger level /// Default value: error @@ -39,12 +37,6 @@ pub const SAMPLE_RATIO: f64 = 1.0; /// Default value: 30 pub const METER_INTERVAL: u64 = 30; -/// Default configuration service version -/// Default value: 1.0.0 -/// Environment variable: RUSTFS_OBS_SERVICE_VERSION -/// Uses the same value as VERSION constant -pub const SERVICE_VERSION: &str = "1.0.0"; - /// Default configuration environment /// Default value: production pub const ENVIRONMENT: &str = "production"; @@ -410,7 +402,6 @@ mod tests { APP_NAME, VERSION, DEFAULT_LOG_LEVEL, - SERVICE_VERSION, ENVIRONMENT, RUSTFS_TLS_KEY, RUSTFS_TLS_CERT, @@ -446,8 +437,7 @@ mod tests { fn test_configuration_consistency() { // Test configuration consistency - // Version consistency - assert_eq!(VERSION, SERVICE_VERSION, "Application version should match service version"); + assert_eq!(VERSION, env!("CARGO_PKG_VERSION")); // Port conflict check let ports = [DEFAULT_PORT, DEFAULT_CONSOLE_PORT]; diff --git a/crates/obs/README.md b/crates/obs/README.md index 4d8a1cc67..a66fc5dea 100644 --- a/crates/obs/README.md +++ b/crates/obs/README.md @@ -36,7 +36,7 @@ use rustfs_obs::init_obs; #[tokio::main] async fn main() { // Build config from environment variables, then initialise all backends. - let _guard = init_obs(None).await.expect("failed to initialise observability"); + let _guard = init_obs(None, None).await.expect("failed to initialise observability"); tracing::info!("RustFS started"); @@ -56,7 +56,7 @@ async fn main() { ```rust use rustfs_obs::init_obs; -let _guard = init_obs(Some("http://otel-collector:4318".to_string())) +let _guard = init_obs(Some("http://otel-collector:4318".to_string()), None) .await .expect("observability init failed"); ``` @@ -136,7 +136,7 @@ With OTLP → Collector → Prometheus, the time from a failure to an alert also | Variable | Default | Description | |------------------------------|-------------------|---------------------------------------------------------| | `RUSTFS_OBS_SERVICE_NAME` | `rustfs` | OTel `service.name` | -| `RUSTFS_OBS_SERVICE_VERSION` | _(crate version)_ | OTel `service.version` | +| `RUSTFS_OBS_SERVICE_VERSION` | RustFS build version | OTel `service.version`; the explicit environment value overrides the version passed by the RustFS binary | | `RUSTFS_OBS_ENVIRONMENT` | `development` | Deployment environment (`production`, `development`, …) | ### Local logging diff --git a/crates/obs/examples/server.rs b/crates/obs/examples/server.rs index b8ca821ab..cb56bf0e2 100644 --- a/crates/obs/examples/server.rs +++ b/crates/obs/examples/server.rs @@ -20,7 +20,7 @@ use tracing::{Level, info, instrument}; #[tokio::main] async fn main() { let obs_conf = Some("http://localhost:4318".to_string()); - let _guard = init_obs(obs_conf).await; + let _guard = init_obs(obs_conf, None).await; let span = tracing::span!(Level::INFO, "main"); let _enter = span.enter(); info!("Program starts"); diff --git a/crates/obs/src/config.rs b/crates/obs/src/config.rs index 70cb43aca..c637719ff 100644 --- a/crates/obs/src/config.rs +++ b/crates/obs/src/config.rs @@ -46,7 +46,7 @@ use rustfs_config::observability::{ use rustfs_config::{ APP_NAME, DEFAULT_LOG_KEEP_FILES, DEFAULT_LOG_LEVEL, DEFAULT_LOG_ROTATION_TIME, DEFAULT_OBS_LOG_FILENAME, 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, + DEFAULT_OBS_TRACES_EXPORT_ENABLED, ENVIRONMENT, METER_INTERVAL, SAMPLE_RATIO, USE_STDOUT, VERSION, }; use rustfs_utils::{ get_env_bool, get_env_bool_with_aliases, get_env_f64, get_env_opt_bool, get_env_opt_str, get_env_opt_u64, get_env_str, @@ -162,7 +162,7 @@ pub struct OtelConfig { pub meter_interval: Option, /// OTel `service.name` attribute (default: `APP_NAME`). pub service_name: Option, - /// OTel `service.version` attribute (default: `SERVICE_VERSION`). + /// OTel `service.version` attribute (default: the supplied application version or Cargo package version). pub service_version: Option, /// Deployment environment tag, e.g. `production` or `development`. pub environment: Option, @@ -234,6 +234,10 @@ impl OtelConfig { /// `None` or an empty string the value is read from the /// `RUSTFS_OBS_ENDPOINT` environment variable instead. /// + /// The default service version is the Cargo package version. Applications + /// that know their display/build version should use + /// [`OtelConfig::extract_otel_config_from_env_with_version`]. + /// /// When no endpoint is configured at all, `use_stdout` is forced to `true` /// so that logs are still visible during development. /// @@ -250,6 +254,15 @@ impl OtelConfig { /// ); /// ``` pub fn extract_otel_config_from_env(endpoint: Option) -> OtelConfig { + Self::extract_otel_config_from_env_with_version(endpoint, None) + } + + /// Build an [`OtelConfig`] using `version` as the default OTel service version. + /// + /// An explicit `RUSTFS_OBS_SERVICE_VERSION` environment variable takes + /// precedence over `version`. When `version` is `None`, the current Cargo + /// package version is used. + pub fn extract_otel_config_from_env_with_version(endpoint: Option, version: Option) -> OtelConfig { let endpoint = match endpoint { Some(ep) if !ep.is_empty() => ep, _ => env::var(ENV_OBS_ENDPOINT).unwrap_or_default(), @@ -312,7 +325,7 @@ impl OtelConfig { 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)), + service_version: Some(get_env_str(ENV_OBS_SERVICE_VERSION, version.as_deref().unwrap_or(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)), @@ -459,6 +472,29 @@ mod tests { OtelConfig::extract_otel_config_from_env(None).profiling_export_enabled } + #[test] + fn service_version_uses_application_version_when_environment_is_unset() { + with_profiling_env_lock(|| { + temp_env::with_var_unset(ENV_OBS_SERVICE_VERSION, || { + let config = OtelConfig::extract_otel_config_from_env_with_version(None, Some("2.3.4".to_string())); + assert_eq!(config.service_version.as_deref(), Some("2.3.4")); + + let config = OtelConfig::extract_otel_config_from_env_with_version(None, None); + assert_eq!(config.service_version.as_deref(), Some(VERSION)); + }); + }); + } + + #[test] + fn service_version_environment_override_takes_precedence_over_application_version() { + with_profiling_env_lock(|| { + temp_env::with_var(ENV_OBS_SERVICE_VERSION, Some("custom-build-42"), || { + let config = OtelConfig::extract_otel_config_from_env_with_version(None, Some("2.3.4".to_string())); + assert_eq!(config.service_version.as_deref(), Some("custom-build-42")); + }); + }); + } + #[test] fn profiling_export_defaults_to_disabled_when_unset() { with_profiling_env_lock(|| { diff --git a/crates/obs/src/global.rs b/crates/obs/src/global.rs index d9ca4b1ec..08f1e83b0 100644 --- a/crates/obs/src/global.rs +++ b/crates/obs/src/global.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{AppConfig, GlobalError, OtelConfig, OtelGuard, telemetry::init_telemetry}; +use crate::{GlobalError, OtelConfig, OtelGuard, telemetry::init_telemetry}; use std::sync::{Arc, Mutex}; use tokio::sync::OnceCell; use tracing::{info, warn}; @@ -68,10 +68,12 @@ pub(crate) fn set_observability_metric_enabled(enabled: bool) { /// Initialize the observability module /// /// # Parameters -/// - `config`: Configuration information +/// - `endpoint`: Optional root OTLP endpoint; falls back to `RUSTFS_OBS_ENDPOINT`. +/// - `version`: Optional application build version used as the default OTel +/// `service.version`; `RUSTFS_OBS_SERVICE_VERSION` takes precedence. /// /// # Returns -/// A tuple containing the logger and the telemetry guard +/// An [`OtelGuard`] that must remain alive for the lifetime of the application. /// /// # Example /// ```no_run @@ -79,16 +81,15 @@ pub(crate) fn set_observability_metric_enabled(enabled: bool) { /// /// # #[tokio::main] /// # async fn main() { -/// # match init_obs(None).await { +/// # match init_obs(None, None).await { /// # Ok(guard) => {} /// # Err(e) => { eprintln!("Failed to initialize observability: {}", e); } /// # } /// # } /// ``` -pub async fn init_obs(endpoint: Option) -> Result { - // Load the configuration file - let config = AppConfig::new_with_endpoint(endpoint); - init_obs_with_config(&config.observability).await +pub async fn init_obs(endpoint: Option, version: Option) -> Result { + let config = OtelConfig::extract_otel_config_from_env_with_version(endpoint, version); + init_obs_with_config(&config).await } /// Initialize the observability module with an explicit [`OtelConfig`]. @@ -142,7 +143,7 @@ pub async fn init_obs_with_config(config: &OtelConfig) -> Result Result<(), Box> { -/// # let guard = match init_obs(None).await{ +/// # let guard = match init_obs(None, None).await{ /// # Ok(g) => g, /// # Err(e) => { return Err(Box::new(e)); } /// # }; diff --git a/crates/obs/src/lib.rs b/crates/obs/src/lib.rs index 43ca252ee..46ffcc840 100644 --- a/crates/obs/src/lib.rs +++ b/crates/obs/src/lib.rs @@ -31,7 +31,7 @@ //! //! # #[tokio::main] //! # async fn main() { -//! # let _guard = match init_obs(None).await { +//! # let _guard = match init_obs(None, None).await { //! # Ok(g) => g, //! # Err(e) => { //! # panic!("Failed to initialize observability: {:?}", e); diff --git a/crates/obs/src/telemetry/resource.rs b/crates/obs/src/telemetry/resource.rs index 37bda8a5f..8a7d003ab 100644 --- a/crates/obs/src/telemetry/resource.rs +++ b/crates/obs/src/telemetry/resource.rs @@ -31,7 +31,7 @@ use opentelemetry_semantic_conventions::{ SERVICE_VERSION as OTEL_SERVICE_VERSION, }, }; -use rustfs_config::{APP_NAME, ENVIRONMENT, SERVICE_VERSION}; +use rustfs_config::{APP_NAME, ENVIRONMENT, VERSION}; use rustfs_utils::get_local_ip_with_default; use std::borrow::Cow; @@ -40,7 +40,7 @@ use std::borrow::Cow; /// The resource carries the following attributes: /// - `service.name` — from `config.service_name`, defaulting to [`APP_NAME`]. /// - `service.version` — from `config.service_version`, defaulting to -/// [`SERVICE_VERSION`]. +/// [`VERSION`]. /// - `deployment.environment` — from `config.environment`, defaulting to /// [`ENVIRONMENT`]. /// - `rustfs.node` / `service.instance.id` — the stable RustFS local node name @@ -61,7 +61,7 @@ pub(super) fn build_resource(config: &OtelConfig) -> Resource { [ KeyValue::new( OTEL_SERVICE_VERSION, - Cow::Borrowed(config.service_version.as_deref().unwrap_or(SERVICE_VERSION)).to_string(), + Cow::Borrowed(config.service_version.as_deref().unwrap_or(VERSION)).to_string(), ), KeyValue::new( DEPLOYMENT_ENVIRONMENT_NAME, @@ -81,6 +81,22 @@ mod tests { use super::*; use opentelemetry::Key; + #[test] + fn build_resource_uses_configured_service_version() { + let config = OtelConfig { + service_version: Some("@a1b2c3d".to_string()), + ..OtelConfig::default() + }; + let resource = build_resource(&config); + + assert_eq!( + resource + .get(&Key::from_static_str(OTEL_SERVICE_VERSION)) + .map(|value| value.to_string()), + Some("@a1b2c3d".to_string()) + ); + } + #[tokio::test] async fn build_resource_uses_stable_local_node_identity() { let _guard = crate::node_identity::local_node_identity_test_guard().await; diff --git a/rustfs/src/startup_runtime_sources.rs b/rustfs/src/startup_runtime_sources.rs index 90304d00f..30b35458f 100644 --- a/rustfs/src/startup_runtime_sources.rs +++ b/rustfs/src/startup_runtime_sources.rs @@ -71,7 +71,7 @@ pub(crate) fn set_buffer_profile_enabled(enabled: bool) { } pub(crate) async fn init_observability_guard(obs_endpoint: String) -> Result { - rustfs_obs::init_obs(Some(obs_endpoint)).await + rustfs_obs::init_obs(Some(obs_endpoint), Some(crate::version::DISPLAY_VERSION.to_owned())).await } pub(crate) fn set_observability_guard(guard: OtelGuard) -> Result<(), ObservabilityError> {