Files
rustfs/crates/obs/src/metrics/scheduler.rs
T

831 lines
33 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.
//! Global metrics collector initialization.
//!
//! This module provides the entry point for initializing all metrics collectors.
//! The actual statistics collection functions are in `stats_collector.rs`.
//!
//! System monitoring collectors (migrated from `rustfs-obs::system`):
//! - Process CPU metrics
//! - Process memory metrics
//! - Process disk I/O metrics
//! - Host network I/O metrics
use crate::metrics::collectors::{
AuditTargetStats,
BucketReplicationBandwidthStats,
NotificationStats,
NotificationTargetStats,
// System monitoring collectors (migrated from rustfs-obs::system)
ProcessAttributeError,
ProcessCpuStats,
ProcessDiskStats,
ProcessMemoryStats,
collect_audit_metrics,
collect_bucket_metrics,
collect_bucket_replication_bandwidth_metrics,
collect_bucket_replication_metrics,
collect_bucket_usage_metrics,
collect_cluster_config_metrics,
collect_cluster_health_metrics,
collect_cluster_metrics,
collect_cluster_usage_metrics,
collect_cpu_metrics,
collect_drive_count_metrics,
collect_drive_detailed_metrics,
collect_erasure_set_metrics,
collect_host_network_metrics,
collect_iam_metrics,
collect_ilm_metrics,
collect_memory_metrics,
collect_network_metrics,
collect_node_metrics,
collect_notification_metrics,
collect_notification_target_metrics,
collect_process_attributes,
collect_process_cpu_metrics,
collect_process_disk_metrics,
collect_process_memory_metrics,
collect_process_metrics,
collect_replication_metrics,
collect_resource_metrics,
collect_scanner_metrics,
};
use crate::metrics::config::{
DEFAULT_AUDIT_METRICS_INTERVAL, DEFAULT_BUCKET_METRICS_INTERVAL, DEFAULT_BUCKET_REPLICATION_BANDWIDTH_METRICS_INTERVAL,
DEFAULT_CLUSTER_METRICS_INTERVAL, DEFAULT_NODE_METRICS_INTERVAL, DEFAULT_NOTIFICATION_METRICS_INTERVAL,
DEFAULT_RESOURCE_METRICS_INTERVAL, ENV_AUDIT_METRICS_INTERVAL, ENV_BUCKET_METRICS_INTERVAL,
ENV_BUCKET_REPLICATION_BANDWIDTH_METRICS_INTERVAL, ENV_CLUSTER_METRICS_INTERVAL, ENV_DEFAULT_METRICS_INTERVAL,
ENV_NODE_METRICS_INTERVAL, ENV_NOTIFICATION_METRICS_INTERVAL, ENV_RESOURCE_METRICS_INTERVAL,
};
use crate::metrics::report::{PrometheusMetric, report_metrics};
use crate::metrics::schema::bucket_replication::{
BUCKET_L, BUCKET_REPL_BANDWIDTH_CURRENT_MD, BUCKET_REPL_BANDWIDTH_LIMIT_MD, TARGET_ARN_L,
};
use crate::metrics::stats_collector::{
ProcessMetricBundle, collect_bucket_replication_bandwidth_stats, collect_bucket_replication_detail_stats,
collect_bucket_stats, collect_cluster_and_health_stats, collect_cluster_config_stats, collect_cluster_usage_metric_stats,
collect_disk_and_system_drive_stats, collect_erasure_set_stats, collect_host_network_stats, collect_iam_stats,
collect_ilm_metric_stats, collect_internode_network_stats, collect_process_metric_bundle, collect_replication_stats,
collect_scanner_metric_stats, collect_system_cpu_and_memory_stats_with,
};
use rustfs_audit::audit_target_metrics;
use rustfs_ecstore::global::get_global_bucket_monitor;
use rustfs_notify::{notification_metrics_snapshot, notification_target_metrics};
use rustfs_utils::get_env_opt_u64;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::time::Duration;
use sysinfo::System;
use tokio::time::Instant;
use tokio_util::sync::CancellationToken;
use tracing::warn;
/// Default interval for system monitoring metrics (15 seconds)
const DEFAULT_SYSTEM_METRICS_INTERVAL: Duration = Duration::from_secs(15);
/// Environment variable for system monitoring interval
const ENV_SYSTEM_METRICS_INTERVAL: &str = "RUSTFS_METRICS_SYSTEM_INTERVAL_SEC";
/// Legacy environment variable for system monitoring interval
const LEGACY_SYSTEM_METRICS_INTERVAL: &str = "RUSTFS_OBS_METRICS_SYSTEM_INTERVAL_MS";
/// Default cycles to emit zero for removed replication bandwidth series before letting them expire.
const DEFAULT_REPL_BW_ZERO_TOMBSTONE_CYCLES: u8 = 3;
/// Env var that overrides the zero-emission tombstone cycles for removed replication bandwidth series.
const ENV_REPL_BW_ZERO_TOMBSTONE_CYCLES: &str = "RUSTFS_METRICS_REPL_BW_ZERO_TOMBSTONE_CYCLES";
type ReplBwKey = (String, String); // (bucket, target_arn)
fn repl_bw_live_keys(stats: &[BucketReplicationBandwidthStats]) -> HashSet<ReplBwKey> {
stats.iter().map(|s| (s.bucket.clone(), s.target_arn.clone())).collect()
}
fn update_repl_bw_zero_tombstones(
monitor_available: bool,
has_seen_valid_snapshot: &mut bool,
prev_live_keys: &mut HashSet<ReplBwKey>,
zero_tombstones: &mut HashMap<ReplBwKey, u8>,
current_live_keys: HashSet<ReplBwKey>,
tombstone_cycles: u8,
) {
if !monitor_available {
return;
}
if *has_seen_valid_snapshot {
for removed in prev_live_keys.difference(&current_live_keys) {
zero_tombstones.insert(removed.clone(), tombstone_cycles);
}
}
// Key becomes live again: stop zeroing immediately.
for key in &current_live_keys {
zero_tombstones.remove(key);
}
*prev_live_keys = current_live_keys;
*has_seen_valid_snapshot = true;
}
fn collect_repl_bw_zero_tombstone_metrics(zero_tombstones: &HashMap<ReplBwKey, u8>) -> Vec<PrometheusMetric> {
if zero_tombstones.is_empty() {
return Vec::new();
}
let mut zero_metrics = Vec::with_capacity(zero_tombstones.len() * 2);
for (bucket, target_arn) in zero_tombstones.keys() {
let bucket_label: Cow<'static, str> = Cow::Owned(bucket.clone());
let target_arn_label: Cow<'static, str> = Cow::Owned(target_arn.clone());
zero_metrics.push(
PrometheusMetric::from_descriptor(&BUCKET_REPL_BANDWIDTH_LIMIT_MD, 0.0)
.with_label(BUCKET_L, bucket_label.clone())
.with_label(TARGET_ARN_L, target_arn_label.clone()),
);
zero_metrics.push(
PrometheusMetric::from_descriptor(&BUCKET_REPL_BANDWIDTH_CURRENT_MD, 0.0)
.with_label(BUCKET_L, bucket_label)
.with_label(TARGET_ARN_L, target_arn_label),
);
}
zero_metrics
}
fn expire_repl_bw_zero_tombstones(monitor_available: bool, zero_tombstones: &mut HashMap<ReplBwKey, u8>) {
if monitor_available && !zero_tombstones.is_empty() {
zero_tombstones.retain(|_, remaining| {
if *remaining <= 1 {
false
} else {
*remaining -= 1;
true
}
});
}
}
/// Initialize all metrics collectors.
///
/// This function spawns background tasks that periodically collect metrics
/// from various sources and report them to the metrics system.
///
/// # Arguments
/// * `token` - A `CancellationToken` that can be used to gracefully shut down
/// all metrics collection tasks.
///
/// # Environment Variables
/// The collection intervals can be configured via environment variables:
/// - `RUSTFS_METRICS_CLUSTER_INTERVAL_SEC`: Cluster metrics interval in seconds (default: 60)
/// - `RUSTFS_METRICS_BUCKET_INTERVAL_SEC`: Bucket metrics interval in seconds (default: 300)
/// - `RUSTFS_METRICS_NODE_INTERVAL_SEC`: Node/disk metrics interval in seconds (default: 60)
/// - `RUSTFS_METRICS_BUCKET_REPLICATION_BANDWIDTH_INTERVAL_SEC`: Bucket replication bandwidth interval in seconds (default: 30)
/// - `RUSTFS_METRICS_RESOURCE_INTERVAL_SEC`: Resource metrics interval in seconds (default: 15)
/// - `RUSTFS_METRICS_DEFAULT_INTERVAL_SEC`: Optional global default interval in seconds.
///
/// Legacy interval names without `_SEC` are still accepted for backward compatibility:
/// - `RUSTFS_METRICS_CLUSTER_INTERVAL`
/// - `RUSTFS_METRICS_BUCKET_INTERVAL`
/// - `RUSTFS_METRICS_NODE_INTERVAL`
/// - `RUSTFS_METRICS_BUCKET_REPLICATION_BANDWIDTH_INTERVAL`
/// - `RUSTFS_METRICS_RESOURCE_INTERVAL`
pub fn init_metrics_runtime(token: CancellationToken) {
const LEGACY_CLUSTER_INTERVAL: &str = "RUSTFS_METRICS_CLUSTER_INTERVAL";
const LEGACY_BUCKET_INTERVAL: &str = "RUSTFS_METRICS_BUCKET_INTERVAL";
const LEGACY_NODE_INTERVAL: &str = "RUSTFS_METRICS_NODE_INTERVAL";
const LEGACY_REPLICATION_BANDWIDTH_INTERVAL: &str = "RUSTFS_METRICS_BUCKET_REPLICATION_BANDWIDTH_INTERVAL";
const LEGACY_RESOURCE_INTERVAL: &str = "RUSTFS_METRICS_RESOURCE_INTERVAL";
const LEGACY_AUDIT_INTERVAL: &str = "RUSTFS_METRICS_AUDIT_INTERVAL";
const LEGACY_NOTIFICATION_INTERVAL: &str = "RUSTFS_METRICS_NOTIFICATION_INTERVAL";
const LEGACY_DEFAULT_INTERVAL: &str = "RUSTFS_METRICS_DEFAULT_INTERVAL";
fn parse_repl_bw_zero_tombstone_cycles() -> u8 {
get_env_opt_u64(ENV_REPL_BW_ZERO_TOMBSTONE_CYCLES)
.filter(|&v| v > 0)
.map(|v| v.min(u8::MAX as u64) as u8)
.unwrap_or(DEFAULT_REPL_BW_ZERO_TOMBSTONE_CYCLES)
}
/// Parse metrics interval from environment variables with fallback to default.
///
/// Priority: primary_env > legacy_env > default_env > legacy_default > default_value
fn parse_metrics_interval(primary_env: &str, legacy_env: &str, default_interval: Duration) -> Duration {
get_env_opt_u64(primary_env)
.or_else(|| get_env_opt_u64(legacy_env))
.or_else(|| get_env_opt_u64(ENV_DEFAULT_METRICS_INTERVAL))
.or_else(|| get_env_opt_u64(LEGACY_DEFAULT_INTERVAL))
.filter(|&v| v > 0)
.map(Duration::from_secs)
.unwrap_or(default_interval)
}
// Read intervals from environment or use defaults
let cluster_interval =
parse_metrics_interval(ENV_CLUSTER_METRICS_INTERVAL, LEGACY_CLUSTER_INTERVAL, DEFAULT_CLUSTER_METRICS_INTERVAL);
let bucket_interval =
parse_metrics_interval(ENV_BUCKET_METRICS_INTERVAL, LEGACY_BUCKET_INTERVAL, DEFAULT_BUCKET_METRICS_INTERVAL);
let bucket_replication_bandwidth_interval = parse_metrics_interval(
ENV_BUCKET_REPLICATION_BANDWIDTH_METRICS_INTERVAL,
LEGACY_REPLICATION_BANDWIDTH_INTERVAL,
DEFAULT_BUCKET_REPLICATION_BANDWIDTH_METRICS_INTERVAL,
);
let node_interval = parse_metrics_interval(ENV_NODE_METRICS_INTERVAL, LEGACY_NODE_INTERVAL, DEFAULT_NODE_METRICS_INTERVAL);
let resource_interval =
parse_metrics_interval(ENV_RESOURCE_METRICS_INTERVAL, LEGACY_RESOURCE_INTERVAL, DEFAULT_RESOURCE_METRICS_INTERVAL);
let audit_interval =
parse_metrics_interval(ENV_AUDIT_METRICS_INTERVAL, LEGACY_AUDIT_INTERVAL, DEFAULT_AUDIT_METRICS_INTERVAL);
let notification_interval = parse_metrics_interval(
ENV_NOTIFICATION_METRICS_INTERVAL,
LEGACY_NOTIFICATION_INTERVAL,
DEFAULT_NOTIFICATION_METRICS_INTERVAL,
);
// Spawn task for cluster metrics
let token_clone = token.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(cluster_interval);
loop {
tokio::select! {
_ = interval.tick() => {
let (stats, cluster_health) = collect_cluster_and_health_stats().await;
let mut metrics = collect_cluster_metrics(&stats);
metrics.extend(collect_cluster_health_metrics(&cluster_health));
report_metrics(&metrics);
}
_ = token_clone.cancelled() => {
warn!("Metrics collection for cluster stats cancelled.");
return;
}
}
}
});
// Spawn task for supplementary cluster metrics that are defined in schema/collector
// but filled by later task-specific runtime sources.
let token_clone = token.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(cluster_interval);
loop {
tokio::select! {
_ = interval.tick() => {
let mut metrics = Vec::new();
if let Some(stats) = collect_cluster_config_stats().await {
metrics.extend(collect_cluster_config_metrics(&stats));
}
let erasure_sets = collect_erasure_set_stats().await;
if !erasure_sets.is_empty() {
metrics.extend(collect_erasure_set_metrics(&erasure_sets));
}
if let Some(stats) = collect_iam_stats().await {
metrics.extend(collect_iam_metrics(&stats));
}
if let Some((cluster_usage, bucket_usage)) = collect_cluster_usage_metric_stats().await {
metrics.extend(collect_cluster_usage_metrics(&cluster_usage));
metrics.extend(collect_bucket_usage_metrics(&bucket_usage));
}
if !metrics.is_empty() {
report_metrics(&metrics);
}
}
_ = token_clone.cancelled() => {
warn!("Metrics collection for supplementary cluster stats cancelled.");
return;
}
}
}
});
// Spawn task for bucket metrics
let token_clone = token.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(bucket_interval);
loop {
tokio::select! {
_ = interval.tick() => {
let stats = collect_bucket_stats().await;
let metrics = collect_bucket_metrics(&stats);
report_metrics(&metrics);
}
_ = token_clone.cancelled() => {
warn!("Metrics collection for bucket stats cancelled.");
return;
}
}
}
});
// Spawn task for node/disk metrics
let token_clone = token.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(node_interval);
loop {
tokio::select! {
_ = interval.tick() => {
let (disk_stats, drive_stats, drive_counts) = collect_disk_and_system_drive_stats().await;
let mut metrics = collect_node_metrics(&disk_stats);
metrics.extend(collect_drive_detailed_metrics(&drive_stats));
metrics.extend(collect_drive_count_metrics(&drive_counts));
report_metrics(&metrics);
}
_ = token_clone.cancelled() => {
warn!("Metrics collection for node/disk stats cancelled.");
return;
}
}
}
});
// Spawn task for bucket replication bandwidth metrics
let token_clone = token.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(bucket_replication_bandwidth_interval);
let repl_bw_zero_tombstone_cycles = parse_repl_bw_zero_tombstone_cycles();
let mut prev_live_keys: HashSet<ReplBwKey> = HashSet::new();
let mut zero_tombstones: HashMap<ReplBwKey, u8> = HashMap::new();
let mut has_seen_valid_snapshot = false;
loop {
tokio::select! {
_ = interval.tick() => {
let monitor_available = get_global_bucket_monitor().is_some();
let stats = collect_bucket_replication_bandwidth_stats();
let current_live_keys = repl_bw_live_keys(&stats);
if !monitor_available {
warn!("Bucket monitor unavailable; skip replication bandwidth key-state transition this cycle.");
}
update_repl_bw_zero_tombstones(
monitor_available,
&mut has_seen_valid_snapshot,
&mut prev_live_keys,
&mut zero_tombstones,
current_live_keys,
repl_bw_zero_tombstone_cycles,
);
let mut metrics = collect_bucket_replication_bandwidth_metrics(&stats);
// Phase-1 action: force zero for removed keys during tombstone cycles.
metrics.extend(collect_repl_bw_zero_tombstone_metrics(&zero_tombstones));
let bucket_replication = collect_bucket_replication_detail_stats().await;
metrics.extend(collect_bucket_replication_metrics(&bucket_replication));
let replication = collect_replication_stats().await;
metrics.extend(collect_replication_metrics(&replication));
report_metrics(&metrics);
// Phase-2: after N cycles, stop reporting -> series becomes absent after expiration.
expire_repl_bw_zero_tombstones(monitor_available, &mut zero_tombstones);
}
_ = token_clone.cancelled() => {
warn!("Metrics collection for bucket replication bandwidth stats cancelled.");
return;
}
}
}
});
// Spawn task for audit target delivery metrics
let token_clone = token.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(audit_interval);
loop {
tokio::select! {
_ = interval.tick() => {
let stats = audit_target_metrics().await
.into_iter()
.map(|snapshot| AuditTargetStats {
failed_messages: snapshot.failed_messages,
queue_length: snapshot.queue_length,
target_id: snapshot.target_id,
total_messages: snapshot.total_messages,
})
.collect::<Vec<_>>();
let metrics = collect_audit_metrics(&stats);
report_metrics(&metrics);
}
_ = token_clone.cancelled() => {
warn!("Metrics collection for audit target stats cancelled.");
return;
}
}
}
});
// Spawn task for notification delivery metrics
let token_clone = token.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(notification_interval);
loop {
tokio::select! {
_ = interval.tick() => {
let snapshot = notification_metrics_snapshot();
let mut metrics = collect_notification_metrics(&NotificationStats {
current_send_in_progress: snapshot.current_send_in_progress,
events_errors_total: snapshot.events_errors_total,
events_sent_total: snapshot.events_sent_total,
events_skipped_total: snapshot.events_skipped_total,
});
let target_stats = notification_target_metrics().await
.into_iter()
.map(|snapshot| NotificationTargetStats {
failed_messages: snapshot.failed_messages,
queue_length: snapshot.queue_length,
target_id: snapshot.target_id,
target_type: snapshot.target_type,
total_messages: snapshot.total_messages,
})
.collect::<Vec<_>>();
metrics.extend(collect_notification_target_metrics(&target_stats));
report_metrics(&metrics);
}
_ = token_clone.cancelled() => {
warn!("Metrics collection for notification stats cancelled.");
return;
}
}
}
});
// Spawn task for background workflow metrics such as ILM and scanner.
let token_clone = token.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(cluster_interval);
loop {
tokio::select! {
_ = interval.tick() => {
let mut metrics = Vec::new();
if let Some(stats) = collect_ilm_metric_stats().await {
metrics.extend(collect_ilm_metrics(&stats));
}
if let Some(stats) = collect_scanner_metric_stats().await {
metrics.extend(collect_scanner_metrics(&stats));
}
if !metrics.is_empty() {
report_metrics(&metrics);
}
}
_ = token_clone.cancelled() => {
warn!("Metrics collection for background workflow stats cancelled.");
return;
}
}
}
});
// Spawn task for system monitoring metrics (migrated from rustfs-obs::system)
let system_interval = get_env_opt_u64(ENV_SYSTEM_METRICS_INTERVAL)
.or_else(|| get_env_opt_u64(LEGACY_SYSTEM_METRICS_INTERVAL).map(|ms| ms / 1000)) // Convert ms to seconds
.or_else(|| get_env_opt_u64(ENV_DEFAULT_METRICS_INTERVAL))
.filter(|&v| v > 0)
.map(Duration::from_secs)
.unwrap_or(DEFAULT_SYSTEM_METRICS_INTERVAL);
let token_clone = token.clone();
tokio::spawn(async move {
let labels = current_process_metric_labels();
let mut host_system = System::new_all();
let process_interval = resource_interval.min(system_interval);
let mut interval = tokio::time::interval(process_interval);
let now = Instant::now();
let mut next_resource_run = now;
let mut next_system_run = now;
#[cfg(feature = "gpu")]
let current_pid = match sysinfo::get_current_pid() {
Ok(pid) => Some(pid),
Err(e) => {
warn!("Failed to get current PID for system monitoring: {}", e);
None
}
};
loop {
tokio::select! {
_ = interval.tick() => {
let now = Instant::now();
let bundle = collect_process_metric_bundle();
if now >= next_resource_run {
let mut metrics = collect_resource_metrics(&bundle.resource);
metrics.extend(collect_process_metrics(&bundle.process));
report_metrics(&metrics);
advance_deadline(&mut next_resource_run, resource_interval, now);
}
if now >= next_system_run {
#[cfg(feature = "gpu")]
let mut metrics = collect_system_monitoring_metrics(&bundle, &labels, &mut host_system);
#[cfg(not(feature = "gpu"))]
let metrics = collect_system_monitoring_metrics(&bundle, &labels, &mut host_system);
#[cfg(feature = "gpu")]
if let Some(pid) = current_pid {
use crate::metrics::collectors::{GpuCollector, collect_gpu_metrics};
match GpuCollector::new(pid) {
Ok(collector) => match collector.collect() {
Ok(gpu_stats) => {
metrics.extend(collect_gpu_metrics(&gpu_stats, &labels));
}
Err(e) => {
warn!("GPU metrics collection failed: {}", e);
}
},
Err(e) => {
warn!("GPU collector initialization failed: {}", e);
}
}
}
report_metrics(&metrics);
advance_deadline(&mut next_system_run, system_interval, now);
}
}
_ = token_clone.cancelled() => {
warn!("Process metrics collection cancelled.");
return;
}
}
}
});
// Spawn task for internode/system network metrics.
let token_clone = token;
tokio::spawn(async move {
let mut interval = tokio::time::interval(system_interval);
loop {
tokio::select! {
_ = interval.tick() => {
if let Some(stats) = collect_internode_network_stats() {
let metrics = collect_network_metrics(&stats);
if !metrics.is_empty() {
report_metrics(&metrics);
}
}
}
_ = token_clone.cancelled() => {
warn!("Metrics collection for internode network stats cancelled.");
return;
}
}
}
});
}
/// Backward-compatible alias kept during migration.
pub fn init_metrics_collectors(token: CancellationToken) {
init_metrics_runtime(token);
}
fn advance_deadline(deadline: &mut Instant, interval: Duration, now: Instant) {
if *deadline > now {
return;
}
let interval_nanos = interval.as_nanos();
if interval_nanos == 0 {
return;
}
let elapsed = now.duration_since(*deadline);
let missed_intervals = (elapsed.as_nanos() / interval_nanos) + 1;
let mut remaining = missed_intervals;
while remaining > 0 {
let chunk_u128 = remaining.min(u128::from(u32::MAX));
let chunk_u32 = chunk_u128 as u32;
if let Some(advance_by) = interval.checked_mul(chunk_u32) {
*deadline += advance_by;
remaining -= chunk_u128;
continue;
}
*deadline += interval;
remaining -= 1;
}
}
fn current_process_metric_labels() -> Vec<(&'static str, Cow<'static, str>)> {
match collect_process_attributes() {
Ok(attrs) => vec![
("process_pid", Cow::Owned(attrs.pid.to_string())),
("process_executable_name", Cow::Owned(attrs.executable_name)),
],
Err(err) => fallback_process_metric_labels(err),
}
}
fn fallback_process_metric_labels(err: ProcessAttributeError) -> Vec<(&'static str, Cow<'static, str>)> {
warn!("Failed to collect process attributes for metrics labels: {}", err);
vec![
("process_pid", Cow::Owned(std::process::id().to_string())),
("process_executable_name", Cow::Borrowed("unknown")),
]
}
fn collect_system_monitoring_metrics(
bundle: &ProcessMetricBundle,
labels: &[(&'static str, Cow<'static, str>)],
host_system: &mut System,
) -> Vec<PrometheusMetric> {
let cpu_stats = ProcessCpuStats {
usage: bundle.resource.cpu_percent,
utilization: bundle.resource.cpu_percent,
};
let memory_stats = ProcessMemoryStats {
resident: bundle.process.resident_memory_bytes,
virtual_mem: bundle.process.virtual_memory_bytes,
};
let disk_stats = ProcessDiskStats {
read_bytes: bundle.disk_read_bytes,
written_bytes: bundle.disk_write_bytes,
};
let network_stats = collect_host_network_stats();
let (system_cpu_stats, system_memory_stats) = collect_system_cpu_and_memory_stats_with(host_system);
let mut metrics = Vec::new();
metrics.extend(collect_cpu_metrics(&system_cpu_stats));
metrics.extend(collect_memory_metrics(&system_memory_stats));
metrics.extend(collect_process_cpu_metrics(&cpu_stats, Some(labels)));
metrics.extend(collect_process_memory_metrics(&memory_stats, Some(labels)));
metrics.extend(collect_process_disk_metrics(&disk_stats, Some(labels)));
// Interface counters are host-wide, so keep these metrics free of process labels.
metrics.extend(collect_host_network_metrics(&network_stats, None));
metrics
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::{HashMap, HashSet};
use std::time::Duration;
use tokio::time::Instant;
fn repl_bw_key(bucket: &str, target_arn: &str) -> ReplBwKey {
(bucket.to_string(), target_arn.to_string())
}
fn repl_bw_keys(keys: &[(&str, &str)]) -> HashSet<ReplBwKey> {
keys.iter()
.map(|(bucket, target_arn)| repl_bw_key(bucket, target_arn))
.collect()
}
#[test]
fn advance_deadline_keeps_future_deadline_unchanged() {
let base = Instant::now();
let mut deadline = base + Duration::from_secs(10);
advance_deadline(&mut deadline, Duration::from_secs(5), base);
assert_eq!(deadline, base + Duration::from_secs(10));
}
#[test]
fn advance_deadline_moves_to_first_tick_after_now() {
let base = Instant::now();
let mut deadline = base;
advance_deadline(&mut deadline, Duration::from_secs(5), base + Duration::from_secs(12));
assert_eq!(deadline, base + Duration::from_secs(15));
}
#[test]
fn repl_bw_tombstones_zero_removed_keys_then_expire() {
let mut has_seen_valid_snapshot = false;
let mut prev_live_keys = HashSet::new();
let mut zero_tombstones = HashMap::new();
let key = repl_bw_key("photos", "arn:rustfs:replication:target-a");
update_repl_bw_zero_tombstones(
true,
&mut has_seen_valid_snapshot,
&mut prev_live_keys,
&mut zero_tombstones,
repl_bw_keys(&[("photos", "arn:rustfs:replication:target-a")]),
2,
);
assert!(has_seen_valid_snapshot);
assert_eq!(prev_live_keys, repl_bw_keys(&[("photos", "arn:rustfs:replication:target-a")]));
assert!(zero_tombstones.is_empty());
update_repl_bw_zero_tombstones(
true,
&mut has_seen_valid_snapshot,
&mut prev_live_keys,
&mut zero_tombstones,
HashSet::new(),
2,
);
assert_eq!(zero_tombstones.get(&key), Some(&2));
let metrics = collect_repl_bw_zero_tombstone_metrics(&zero_tombstones);
assert_eq!(metrics.len(), 2);
assert!(metrics.iter().all(|metric| metric.value == 0.0));
let names = metrics.iter().map(|metric| metric.name.to_string()).collect::<HashSet<_>>();
assert!(names.contains(&BUCKET_REPL_BANDWIDTH_LIMIT_MD.get_full_metric_name()));
assert!(names.contains(&BUCKET_REPL_BANDWIDTH_CURRENT_MD.get_full_metric_name()));
for metric in metrics {
let labels = metric
.labels
.into_iter()
.map(|(key, value)| (key, value.to_string()))
.collect::<HashMap<_, _>>();
assert_eq!(labels.get(BUCKET_L).map(String::as_str), Some("photos"));
assert_eq!(labels.get(TARGET_ARN_L).map(String::as_str), Some("arn:rustfs:replication:target-a"));
}
expire_repl_bw_zero_tombstones(true, &mut zero_tombstones);
assert_eq!(zero_tombstones.get(&key), Some(&1));
expire_repl_bw_zero_tombstones(true, &mut zero_tombstones);
assert!(zero_tombstones.is_empty());
}
#[test]
fn repl_bw_tombstones_stop_zeroing_when_key_becomes_live_again() {
let mut has_seen_valid_snapshot = false;
let mut prev_live_keys = HashSet::new();
let mut zero_tombstones = HashMap::new();
let live_keys = repl_bw_keys(&[("photos", "arn:rustfs:replication:target-a")]);
update_repl_bw_zero_tombstones(
true,
&mut has_seen_valid_snapshot,
&mut prev_live_keys,
&mut zero_tombstones,
live_keys.clone(),
3,
);
update_repl_bw_zero_tombstones(
true,
&mut has_seen_valid_snapshot,
&mut prev_live_keys,
&mut zero_tombstones,
HashSet::new(),
3,
);
assert_eq!(zero_tombstones.get(&repl_bw_key("photos", "arn:rustfs:replication:target-a")), Some(&3));
update_repl_bw_zero_tombstones(
true,
&mut has_seen_valid_snapshot,
&mut prev_live_keys,
&mut zero_tombstones,
live_keys.clone(),
3,
);
assert!(zero_tombstones.is_empty());
assert_eq!(prev_live_keys, live_keys);
}
#[test]
fn repl_bw_tombstones_do_not_advance_when_monitor_unavailable() {
let mut has_seen_valid_snapshot = true;
let mut prev_live_keys = repl_bw_keys(&[("photos", "arn:rustfs:replication:target-a")]);
let mut zero_tombstones = HashMap::from([(repl_bw_key("videos", "arn:rustfs:replication:target-b"), 1)]);
update_repl_bw_zero_tombstones(
false,
&mut has_seen_valid_snapshot,
&mut prev_live_keys,
&mut zero_tombstones,
HashSet::new(),
3,
);
assert!(has_seen_valid_snapshot);
assert_eq!(prev_live_keys, repl_bw_keys(&[("photos", "arn:rustfs:replication:target-a")]));
assert_eq!(zero_tombstones.get(&repl_bw_key("videos", "arn:rustfs:replication:target-b")), Some(&1));
expire_repl_bw_zero_tombstones(false, &mut zero_tombstones);
assert_eq!(zero_tombstones.get(&repl_bw_key("videos", "arn:rustfs:replication:target-b")), Some(&1));
}
}