Skip to content

Commit f4390b4

Browse files
Implement 'show memory_breakdown --node [node]''
1 parent 7293ff6 commit f4390b4

File tree

8 files changed

+241
-16
lines changed

8 files changed

+241
-16
lines changed

src/cli.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -754,14 +754,26 @@ fn declare_subcommands() -> [Command; 11] {
754754
]
755755
}
756756

757-
fn show_subcommands() -> [Command; 3] {
758-
[
759-
Command::new("overview")
760-
.about("displays a essential information about target node and its cluster"),
761-
Command::new("churn").about("displays object churn metrics"),
762-
Command::new("endpoint")
763-
.about("for troubleshooting: displays the computed HTTP API endpoint URI"),
764-
]
757+
fn show_subcommands() -> [Command; 4] {
758+
let overview_cmd = Command::new("overview")
759+
.about("displays a essential information about target node and its cluster");
760+
let churn_cmd = Command::new("churn").about("displays object churn metrics");
761+
let endpoint_cmd = Command::new("endpoint")
762+
.about("for troubleshooting: displays the computed HTTP API endpoint URI");
763+
let memory_breakdown_cmd = Command::new("memory_breakdown")
764+
.about("use it to understand what consumes memory on the target node")
765+
.arg(
766+
Arg::new("node")
767+
.long("node")
768+
.help("target node, must be a cluster member")
769+
.required(true),
770+
)
771+
.after_long_help(color_print::cformat!(
772+
"<bold>Doc guide:</bold>: {}",
773+
MEMORY_FOOTPRINT_GUIDE_URL
774+
));
775+
776+
[overview_cmd, churn_cmd, endpoint_cmd, memory_breakdown_cmd]
765777
}
766778

767779
fn delete_subcommands() -> [Command; 11] {

src/commands.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,24 @@ use rabbitmq_http_client::requests::EnforcedLimitParams;
2828
use crate::constants::DEFAULT_QUEUE_TYPE;
2929
use rabbitmq_http_client::commons::BindingDestinationType;
3030
use rabbitmq_http_client::commons::QueueType;
31-
use rabbitmq_http_client::responses::{FeatureFlagList, Overview};
3231
use rabbitmq_http_client::{password_hashing, requests, responses};
3332

3433
type APIClient<'a> = Client<&'a str, &'a str, &'a str>;
3534

36-
pub fn show_overview(client: APIClient) -> ClientResult<Overview> {
35+
pub fn show_overview(client: APIClient) -> ClientResult<responses::Overview> {
3736
client.overview()
3837
}
3938

39+
pub fn show_memory_breakdown(
40+
client: APIClient,
41+
command_args: &ArgMatches,
42+
) -> ClientResult<responses::NodeMemoryBreakdown> {
43+
let node = command_args.get_one::<String>("node").unwrap();
44+
client
45+
.get_node_memory_footprint(node)
46+
.map(|footprint| footprint.breakdown)
47+
}
48+
4049
pub fn list_nodes(client: APIClient) -> ClientResult<Vec<responses::ClusterNode>> {
4150
client.list_nodes()
4251
}
@@ -122,7 +131,7 @@ pub fn list_parameters(
122131
}
123132
}
124133

125-
pub fn list_feature_flags(client: APIClient) -> ClientResult<FeatureFlagList> {
134+
pub fn list_feature_flags(client: APIClient) -> ClientResult<responses::FeatureFlagList> {
126135
client.list_feature_flags()
127136
}
128137

src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,10 @@ fn dispatch_subcommand(
253253
println!("Using endpoint: {}", endpoint);
254254
res_handler.no_output_on_success(Ok(()))
255255
}
256+
("show", "memory_breakdown") => {
257+
let result = commands::show_memory_breakdown(client, command_args);
258+
res_handler.memory_breakdown_result(result)
259+
}
256260

257261
("list", "nodes") => {
258262
let result = commands::list_nodes(client);

src/output.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use crate::errors::CommandRunError;
21
// Copyright (C) 2023-2025 RabbitMQ Core Team ([email protected])
32
//
43
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,11 +12,12 @@ use crate::errors::CommandRunError;
1312
// See the License for the specific language governing permissions and
1413
// limitations under the License.
1514
use crate::config::SharedSettings;
15+
use crate::errors::CommandRunError;
1616
use crate::tables;
1717
use clap::ArgMatches;
1818
use rabbitmq_http_client::blocking_api::{HttpClientError, Result as ClientResult};
1919
use rabbitmq_http_client::error::Error as ClientError;
20-
use rabbitmq_http_client::responses::Overview;
20+
use rabbitmq_http_client::responses::{NodeMemoryBreakdown, Overview};
2121
use reqwest::StatusCode;
2222
use std::fmt;
2323
use sysexits::ExitCode;
@@ -130,6 +130,20 @@ impl ResultHandler {
130130
}
131131
}
132132

133+
pub fn memory_breakdown_result(&mut self, result: ClientResult<NodeMemoryBreakdown>) {
134+
match result {
135+
Ok(output) => {
136+
self.exit_code = Some(ExitCode::Ok);
137+
138+
let mut table = tables::memory_breakdown(output);
139+
self.table_styler.apply(&mut table);
140+
141+
println!("{}", table);
142+
}
143+
Err(error) => self.report_command_run_error(&error),
144+
}
145+
}
146+
133147
pub fn no_output_on_success<T>(&mut self, result: ClientResult<T>) {
134148
match result {
135149
Ok(_) => {

src/static_urls.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub(crate) const DEPRECATED_FEATURE_GUIDE_URL: &str =
4040
pub(crate) const ACCESS_CONTROL_GUIDE_URL: &str = "https://rabbitmq.com/docs/access-control";
4141
pub(crate) const HTTP_API_ACCESS_PERMISSIONS_GUIDE_URL: &str =
4242
"https://rabbitmq.com/docs/management#permissions";
43+
pub(crate) const MEMORY_FOOTPRINT_GUIDE_URL: &str = "https://www.rabbitmq.com/docs/memory-use";
4344
pub(crate) const DEFINITION_GUIDE_URL: &str = "https://rabbitmq.com/docs/definitions";
4445
pub(crate) const CONSUMER_GUIDE_URL: &str = "https://rabbitmq.com/docs/consumers";
4546
pub(crate) const POLLING_CONSUMER_GUIDE_URL: &str = "https://rabbitmq.com/docs/consumers#polling";

src/tables.rs

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
// limitations under the License.
1414
use rabbitmq_http_client::blocking_api::HttpClientError;
1515
use rabbitmq_http_client::responses::{
16-
ClusterAlarmCheckDetails, HealthCheckFailureDetails, Overview, QuorumCriticalityCheckDetails,
16+
ClusterAlarmCheckDetails, HealthCheckFailureDetails, NodeMemoryBreakdown, Overview,
17+
QuorumCriticalityCheckDetails,
1718
};
1819
use reqwest::StatusCode;
1920
use tabled::settings::Panel;
@@ -27,9 +28,9 @@ struct OverviewRow<'a> {
2728
}
2829

2930
#[derive(Debug, Tabled)]
30-
struct RowOfTwoStrings<'a> {
31+
struct RowOfTwoStrings<'a, T: ?Sized + std::fmt::Display> {
3132
key: &'a str,
32-
value: &'a str,
33+
value: &'a T,
3334
}
3435

3536
pub fn overview(ov: Overview) -> Table {
@@ -336,3 +337,138 @@ pub fn health_check_failure(
336337

337338
tb.build()
338339
}
340+
341+
pub(crate) fn memory_breakdown(breakdown: NodeMemoryBreakdown) -> Table {
342+
// There is no easy way to transpose an existing table in Tabled, so…
343+
let atom_table_val = breakdown.atom_table;
344+
let allocated_but_unused_val = breakdown.allocated_but_unused;
345+
let binary_heap_val = breakdown.binary_heap;
346+
let classic_queue_procs_val = breakdown.classic_queue_procs;
347+
let code_val = breakdown.code;
348+
let connection_channels_val = breakdown.connection_channels;
349+
let connection_readers_val = breakdown.connection_readers;
350+
let connection_writers_val = breakdown.connection_writers;
351+
let connection_other_val = breakdown.connection_other;
352+
let management_db_val = breakdown.management_db;
353+
let message_indices_val = breakdown.message_indices;
354+
let metadata_store_val = breakdown.metadata_store;
355+
let metadata_store_ets_tables_val = breakdown.metadata_store_ets_tables;
356+
let metrics_val = breakdown.metrics;
357+
let mnesia_val = breakdown.mnesia;
358+
let other_ets_tables_val = breakdown.other_ets_tables;
359+
let other_system_val = breakdown.other_system;
360+
let other_procs_val = breakdown.other_procs;
361+
let quorum_queue_procs_val = breakdown.quorum_queue_procs;
362+
let quorum_queue_ets_tables_val = breakdown.quorum_queue_ets_tables;
363+
let plugins_val = breakdown.plugins;
364+
let reserved_but_unallocated_val = breakdown.reserved_but_unallocated;
365+
let stream_queue_procs_val = breakdown.stream_queue_procs;
366+
let stream_queue_replica_reader_procs_val = breakdown.stream_queue_replica_reader_procs;
367+
let stream_queue_coordinator_procs_val = breakdown.stream_queue_coordinator_procs;
368+
let mut data: Vec<RowOfTwoStrings<u64>> = vec![
369+
RowOfTwoStrings {
370+
key: "Atom table",
371+
value: &atom_table_val,
372+
},
373+
RowOfTwoStrings {
374+
key: "Allocated but unused",
375+
value: &allocated_but_unused_val,
376+
},
377+
RowOfTwoStrings {
378+
key: "Binary heap",
379+
value: &binary_heap_val,
380+
},
381+
RowOfTwoStrings {
382+
key: "Classic queue processes",
383+
value: &classic_queue_procs_val,
384+
},
385+
RowOfTwoStrings {
386+
key: "Code ",
387+
value: &code_val,
388+
},
389+
RowOfTwoStrings {
390+
key: "AMQP 0-9-1 channels",
391+
value: &connection_channels_val,
392+
},
393+
RowOfTwoStrings {
394+
key: "Client connections: reader processes",
395+
value: &connection_readers_val,
396+
},
397+
RowOfTwoStrings {
398+
key: "Client connections: writer processes",
399+
value: &connection_writers_val,
400+
},
401+
RowOfTwoStrings {
402+
key: "Client connections: others processes",
403+
value: &connection_other_val,
404+
},
405+
RowOfTwoStrings {
406+
key: "Management stats database",
407+
value: &management_db_val,
408+
},
409+
RowOfTwoStrings {
410+
key: "Message store indices",
411+
value: &message_indices_val,
412+
},
413+
RowOfTwoStrings {
414+
key: "Metadata store",
415+
value: &metadata_store_val,
416+
},
417+
RowOfTwoStrings {
418+
key: "Metadata store ETS tables",
419+
value: &metadata_store_ets_tables_val,
420+
},
421+
RowOfTwoStrings {
422+
key: "Metrics data",
423+
value: &metrics_val,
424+
},
425+
RowOfTwoStrings {
426+
key: "Mnesia",
427+
value: &mnesia_val,
428+
},
429+
RowOfTwoStrings {
430+
key: "Other (ETS tables)",
431+
value: &other_ets_tables_val,
432+
},
433+
RowOfTwoStrings {
434+
key: "Other (used by the runtime)",
435+
value: &other_system_val,
436+
},
437+
RowOfTwoStrings {
438+
key: "Other processes",
439+
value: &other_procs_val,
440+
},
441+
RowOfTwoStrings {
442+
key: "Quorum queue replica processes",
443+
value: &quorum_queue_procs_val,
444+
},
445+
RowOfTwoStrings {
446+
key: "Quorum queue ETS tables",
447+
value: &quorum_queue_ets_tables_val,
448+
},
449+
RowOfTwoStrings {
450+
key: "Plugins and their data",
451+
value: &plugins_val,
452+
},
453+
RowOfTwoStrings {
454+
key: "Reserved by the kernel but unallocated",
455+
value: &reserved_but_unallocated_val,
456+
},
457+
RowOfTwoStrings {
458+
key: "Stream replica processes",
459+
value: &stream_queue_procs_val,
460+
},
461+
RowOfTwoStrings {
462+
key: "Stream replica reader processes",
463+
value: &stream_queue_replica_reader_procs_val,
464+
},
465+
RowOfTwoStrings {
466+
key: "Stream coordinator processes",
467+
value: &stream_queue_coordinator_procs_val,
468+
},
469+
];
470+
// Note: this is descending ordering
471+
data.sort_by(|a, b| b.value.cmp(a.value));
472+
let tb = Table::builder(data);
473+
tb.build()
474+
}

tests/memory_breakdown_tests.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (C) 2023-2025 RabbitMQ Core Team ([email protected])
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
use predicates::prelude::*;
15+
16+
mod test_helpers;
17+
use crate::test_helpers::*;
18+
19+
#[test]
20+
fn test_memory_breakdown_succeeds() -> Result<(), Box<dyn std::error::Error>> {
21+
let rc = api_client();
22+
let nodes = rc.list_nodes()?;
23+
let first = nodes.get(0).unwrap();
24+
25+
run_succeeds(["show", "memory_breakdown", "--node", first.name.as_str()]).stdout(
26+
predicates::str::contains("Allocated but unused")
27+
.and(predicates::str::contains("Quorum queue ETS tables"))
28+
.and(predicates::str::contains("Client connections"))
29+
.and(predicates::str::contains("Metadata store")),
30+
);
31+
32+
Ok(())
33+
}

tests/test_helpers.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,24 @@ use assert_cmd::assert::Assert;
2121
use assert_cmd::prelude::*;
2222
use std::process::Command;
2323

24+
use rabbitmq_http_client::blocking_api::Client as GenericAPIClient;
25+
26+
type APIClient<'a> = GenericAPIClient<&'a str, &'a str, &'a str>;
27+
2428
type CommandRunResult = Result<(), Box<dyn std::error::Error>>;
2529

30+
pub const ENDPOINT: &str = "http://localhost:15672/api";
31+
pub const USERNAME: &str = "guest";
32+
pub const PASSWORD: &str = "guest";
33+
34+
pub fn endpoint() -> String {
35+
ENDPOINT.to_owned()
36+
}
37+
38+
pub fn api_client() -> APIClient<'static> {
39+
APIClient::new(ENDPOINT, USERNAME, PASSWORD)
40+
}
41+
2642
pub fn await_metric_emission(ms: u64) {
2743
std::thread::sleep(Duration::from_millis(ms));
2844
}

0 commit comments

Comments
 (0)