fix(obs): pass RustFS build version to telemetry (#8425)

fix(obs): align telemetry version with build identity

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
This commit is contained in:
Hauser
2026-10-08 21:27:24 +08:00
committed by GitHub
parent 68dcad8387
commit 1df874b724
8 changed files with 77 additions and 34 deletions
+3 -13
View File
@@ -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];
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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");
+39 -3
View File
@@ -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<u64>,
/// OTel `service.name` attribute (default: `APP_NAME`).
pub service_name: Option<String>,
/// OTel `service.version` attribute (default: `SERVICE_VERSION`).
/// OTel `service.version` attribute (default: the supplied application version or Cargo package version).
pub service_version: Option<String>,
/// Deployment environment tag, e.g. `production` or `development`.
pub environment: Option<String>,
@@ -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<String>) -> 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<String>, version: Option<String>) -> 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(|| {
+10 -9
View File
@@ -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<String>) -> Result<OtelGuard, GlobalError> {
// 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<String>, version: Option<String>) -> Result<OtelGuard, GlobalError> {
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<OtelGuard, Glob
/// # use rustfs_obs::{ init_obs, set_global_guard};
///
/// # async fn init() -> Result<(), Box<dyn std::error::Error>> {
/// # 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)); }
/// # };
+1 -1
View File
@@ -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);
+19 -3
View File
@@ -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;
+1 -1
View File
@@ -71,7 +71,7 @@ pub(crate) fn set_buffer_profile_enabled(enabled: bool) {
}
pub(crate) async fn init_observability_guard(obs_endpoint: String) -> Result<OtelGuard, ObservabilityError> {
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> {