admin api: return total buckets, objects and bytes in GetClusterStatistics

This commit is contained in:
Alex Auvolat
2026-03-06 15:27:12 +01:00
committed by Alex
parent 8abd0fee86
commit de10dc43d5
4 changed files with 87 additions and 7 deletions
+11 -1
View File
@@ -299,7 +299,17 @@ pub struct GetClusterStatisticsResponse {
/// information of disconnected nodes. When this is the case, the actual
/// space available in the cluster might be lower than the reported values.
#[serde(default)]
pub incomplete_info: bool,
pub incomplete_avail_info: bool,
/// number of buckets in the cluster
#[serde(default)]
pub bucket_count: u64,
/// total number of objects stored in all buckets
#[serde(default)]
pub total_object_count: u64,
/// total size of objects stored in all buckets, before compression, deduplication and
/// replication (this is NOT equivalent to actual disk usage in the cluster)
#[serde(default)]
pub total_object_bytes: u64,
}
// ---- ConnectClusterNodes ----
+1 -1
View File
@@ -38,7 +38,7 @@ impl RequestHandler for ListBucketsRequest {
&EmptyKey,
None,
Some(DeletedFilter::NotDeleted),
10000,
1_000_000,
EnumerationOrder::Forward,
)
.await?;
+56 -4
View File
@@ -8,8 +8,10 @@ use garage_util::data::*;
use garage_rpc::layout;
use garage_rpc::layout::PARTITION_BITS;
use garage_table::*;
use garage_model::garage::Garage;
use garage_model::s3::object_table;
use crate::api::*;
use crate::error::*;
@@ -152,7 +154,6 @@ impl RequestHandler for GetClusterHealthRequest {
impl RequestHandler for GetClusterStatisticsRequest {
type Response = GetClusterStatisticsResponse;
// FIXME: return this as a JSON struct instead of text
async fn handle(
self,
garage: &Arc<Garage>,
@@ -160,8 +161,45 @@ impl RequestHandler for GetClusterStatisticsRequest {
) -> Result<GetClusterStatisticsResponse, Error> {
let mut ret = String::new();
// Gather storage node and free space statistics for current nodes
// Gather info on number of buckets, objects and object size
let buckets = garage
.bucket_table
.get_range(
&EmptyKey,
None,
Some(DeletedFilter::NotDeleted),
1_000_000,
EnumerationOrder::Forward,
)
.await?;
let bucket_stats = futures::future::try_join_all(
buckets
.iter()
.map(|b| garage.object_counter_table.table.get(&b.id, &EmptyKey)),
)
.await?;
let layout = &garage.system.cluster_layout();
let bucket_stats = bucket_stats
.into_iter()
.filter_map(|cnt| cnt.map(|x| x.filtered_values(layout)))
.collect::<Vec<_>>();
let bucket_count = buckets.len() as u64;
let total_object_count = bucket_stats
.iter()
.clone()
.map(|cnt| *cnt.get(object_table::OBJECTS).unwrap_or(&0) as u64)
.sum();
let total_object_bytes = bucket_stats
.iter()
.clone()
.map(|cnt| *cnt.get(object_table::BYTES).unwrap_or(&0) as u64)
.sum();
// Gather storage node and free space statistics for current nodes
let mut node_partition_count = HashMap::<Uuid, u64>::new();
if let Ok(current_layout) = layout.current() {
for short_id in current_layout.ring_assignment_data.iter() {
@@ -242,9 +280,20 @@ impl RequestHandler for GetClusterStatisticsRequest {
let incomplete_info = meta_part_avail.len() < node_partition_count.len()
|| data_part_avail.len() < node_partition_count.len();
// Display bucket statistics
let bucket_stats = vec![
format!("Number of buckets:\t{}", bucket_count),
format!("Total number of objects:\t{}", total_object_count),
format!(
"Total size of objects:\t{}",
bytesize::ByteSize(total_object_bytes)
),
];
writeln!(&mut ret, "\n{}", format_table_to_string(bucket_stats)).unwrap();
writeln!(
&mut ret,
"\nEstimated available storage space cluster-wide (might be lower in practice):"
"Estimated available storage space cluster-wide (might be lower in practice):"
)
.unwrap();
if incomplete_info {
@@ -264,7 +313,10 @@ impl RequestHandler for GetClusterStatisticsRequest {
freeform: ret,
metadata_avail,
data_avail,
incomplete_info,
incomplete_avail_info: incomplete_info,
bucket_count,
total_object_count,
total_object_bytes,
})
}
}