// 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. use crate::metrics::schema::{MetricDescriptor, MetricType}; use metrics::{counter, describe_counter, describe_gauge, describe_histogram, gauge}; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::sync::{Mutex, OnceLock}; static NAME_CACHE: OnceLock>> = OnceLock::new(); static HELP_CACHE: OnceLock>> = OnceLock::new(); /// Metric names already handed to `describe_*!`. The metadata never changes, so /// each name is described once instead of on every collection cycle. static DESCRIBED: OnceLock>> = OnceLock::new(); fn intern_string(cache: &OnceLock>>, value: &str) -> &'static str { let cache = cache.get_or_init(Default::default); let mut cache = cache.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); if let Some(existing) = cache.get(value) { existing } else { let value = Box::leak(value.to_string().into_boxed_str()); cache.insert(value.to_string(), value); value } } /// Resolve a metric name/help `Cow` to a `&'static str`, skipping the intern /// lock+leak when it is already borrowed (the common case for statically-named /// metrics); only owned strings pay the intern cost. // The `&Cow` is intentional: we must inspect the borrowed-vs-owned variant to // return the existing `&'static str` without interning, so `&str` will not do. #[allow(clippy::ptr_arg)] fn static_str(cache: &OnceLock>>, value: &Cow<'static, str>) -> &'static str { match value { Cow::Borrowed(s) => s, Cow::Owned(s) => intern_string(cache, s), } } /// Hand a metric name to `describe_*!` only the first time it is seen. The /// metadata (help text) never changes, so describing on every collection cycle /// only re-locks the recorder's metadata map. The help is resolved lazily, so an /// already-described metric costs one `HashSet` probe and nothing else. // `&Cow` mirrors `static_str`, which needs the borrowed-vs-owned variant. #[allow(clippy::ptr_arg)] fn describe_metric_once(name: &'static str, metric_type: MetricType, help: &Cow<'static, str>) { let described = DESCRIBED.get_or_init(Default::default); let mut described = described.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); if described.insert(name) { let help = static_str(&HELP_CACHE, help); match metric_type { MetricType::Counter => describe_counter!(name, help), MetricType::Gauge => describe_gauge!(name, help), MetricType::Histogram => describe_histogram!(name, help), } } } fn counter_value_from_f64(value: f64) -> Option { if !value.is_finite() || value < 0.0 { return None; } if value >= u64::MAX as f64 { Some(u64::MAX) } else { Some(value as u64) } } pub fn report_metrics(metrics: &[PrometheusMetric]) { for metric in metrics { let name = static_str(&NAME_CACHE, &metric.name); describe_metric_once(name, metric.metric_type, &metric.help); // `metric.labels` is already `[(&'static str, Cow<'static, str>)]`, so emit // straight from it — no per-cycle `Vec<(String, String)>` clone. match metric.metric_type { MetricType::Counter => { if let Some(value) = counter_value_from_f64(metric.value) { counter!(name, &metric.labels).absolute(value); } } MetricType::Gauge => gauge!(name, &metric.labels).set(metric.value), MetricType::Histogram => metrics::histogram!(name, &metric.labels).record(metric.value), } } } #[derive(Debug, Clone)] pub struct PrometheusMetric { pub name: Cow<'static, str>, pub metric_type: MetricType, pub help: Cow<'static, str>, pub labels: Vec<(&'static str, Cow<'static, str>)>, pub value: f64, } impl PrometheusMetric { #[inline] pub const fn new(name: &'static str, metric_type: MetricType, help: &'static str, value: f64) -> Self { Self { name: Cow::Borrowed(name), metric_type, help: Cow::Borrowed(help), labels: Vec::new(), value, } } #[inline] pub fn new_owned(name: String, metric_type: MetricType, help: String, value: f64) -> Self { Self { name: Cow::Owned(name), metric_type, help: Cow::Owned(help), labels: Vec::new(), value, } } #[inline] pub fn from_descriptor(descriptor: &MetricDescriptor, value: f64) -> Self { let help = intern_string(&HELP_CACHE, &descriptor.help); Self { name: Cow::Owned(descriptor.get_full_metric_name()), metric_type: descriptor.metric_type, help: Cow::Borrowed(help), labels: Vec::new(), value, } } #[inline] #[allow(dead_code)] pub fn with_label(mut self, key: &'static str, value: impl Into>) -> Self { self.labels.push((key, value.into())); self } #[inline] #[allow(dead_code)] pub fn with_label_owned(mut self, key: &'static str, value: String) -> Self { self.labels.push((key, Cow::Owned(value))); self } #[inline] #[allow(dead_code)] pub fn with_labels(mut self, labels: Vec<(&'static str, Cow<'static, str>)>) -> Self { self.labels = labels; self } } #[cfg(test)] mod tests { use super::*; use crate::metrics::schema::{MetricName, MetricNamespace, MetricSubsystem}; #[test] fn from_descriptor_uses_prometheus_metric_names_for_all_types() { let cases = [ (MetricType::Counter, "rustfs_api_requests_total"), (MetricType::Gauge, "rustfs_system_memory_used_bytes"), (MetricType::Histogram, "rustfs_custom_path_latency_seconds"), ]; for (metric_type, expected_name) in cases { let subsystem = match metric_type { MetricType::Counter => MetricSubsystem::ApiRequests, MetricType::Gauge => MetricSubsystem::SystemMemory, MetricType::Histogram => MetricSubsystem::new("/custom/path"), }; let name = match metric_type { MetricType::Counter => MetricName::ApiRequestsTotal, MetricType::Gauge => MetricName::Custom("used_bytes".to_string()), MetricType::Histogram => MetricName::Custom("latency_seconds".to_string()), }; let metric = PrometheusMetric::from_descriptor( &MetricDescriptor::new(name, metric_type, "test help".to_string(), vec![], MetricNamespace::RustFS, subsystem), 1.0, ); assert_eq!(metric.name, expected_name); assert_eq!(metric.metric_type, metric_type); } } #[test] fn counter_value_from_f64_rejects_negative_and_nan_inputs() { assert_eq!(counter_value_from_f64(-1.0), None); assert_eq!(counter_value_from_f64(f64::NAN), None); assert_eq!(counter_value_from_f64(f64::NEG_INFINITY), None); } #[test] fn counter_value_from_f64_clamps_large_values() { assert_eq!(counter_value_from_f64(42.0), Some(42)); assert_eq!(counter_value_from_f64(u64::MAX as f64 * 2.0), Some(u64::MAX)); } }