ARTEMIS-4743 Improve CLI Queue Stat Output: Split lines and include internal queue attribute

This commit is contained in:
Clebert Suconic 2024-04-22 19:16:28 -04:00 committed by clebertsuconic
parent 07ba37a74a
commit 379515382e
6 changed files with 250 additions and 47 deletions

View File

@ -16,6 +16,7 @@
*/
package org.apache.activemq.artemis.cli.commands.queue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@ -29,14 +30,17 @@ import org.apache.activemq.artemis.cli.commands.ActionContext;
import org.apache.activemq.artemis.cli.commands.messages.ConnectionAbstract;
import org.apache.activemq.artemis.json.JsonArray;
import org.apache.activemq.artemis.json.JsonObject;
import org.apache.activemq.artemis.utils.TableOut;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
@Command(name = "stat", description = "Print basic stats of a queue. Output includes CONSUMER_COUNT (number of consumers), MESSAGE_COUNT (current message count on the queue, including scheduled, paged and in-delivery messages), MESSAGES_ADDED (messages added to the queue), DELIVERING_COUNT (messages broker is currently delivering to consumer(s)), MESSAGES_ACKED (messages acknowledged from the consumer(s))." + " Queues can be filtered using EITHER '--queueName X' where X is contained in the queue name OR using a full filter '--field NAME --operation EQUALS --value X'.")
public class StatQueue extends ConnectionAbstract {
private static final String MANAGEMENT_QUEUE = "activemq.management";
public enum FIELD {
NAME("name"), ADDRESS("address"), CONSUMER_COUNT("consumerCount"), MESSAGE_COUNT("messageCount"), MESSAGES_ADDED("messagesAdded"), DELIVERING_COUNT("deliveringCount"), MESSAGES_ACKED("messagesAcked"), SCHEDULED_COUNT("scheduledCount"), ROUTING_TYPE("routingType");
NAME("name", false), ADDRESS("address", false), CONSUMER_COUNT("consumerCount", true), MESSAGE_COUNT("messageCount", true), MESSAGES_ADDED("messagesAdded", true), DELIVERING_COUNT("deliveringCount", true), MESSAGES_ACKED("messagesAcked", true), SCHEDULED_COUNT("scheduledCount", true), ROUTING_TYPE("routingType", true), INTERNAL("internalQueue", true);
private static final Map<String, FIELD> lookup = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
@ -47,9 +51,11 @@ public class StatQueue extends ConnectionAbstract {
}
private String jsonId;
private boolean center;
FIELD(String jsonId) {
FIELD(String jsonId, boolean center) {
this.jsonId = jsonId;
this.center = center;
}
String getJsonId() {
@ -90,6 +96,9 @@ public class StatQueue extends ConnectionAbstract {
@Option(names = "--clustered", description = "Expands the report for all nodes on the topology")
private boolean clustered = false;
@Option(names = "--include-management", description = "Include queues created for notification management in the output")
private boolean includeManagement = false;
private int statCount = 0;
//easier for testing
@ -216,20 +225,36 @@ public class StatQueue extends ConnectionAbstract {
JsonArray array = queuesAsJsonObject.getJsonArray("data");
int[] columnSizes = new int[FIELD.values().length];
boolean[] centralize = new boolean[columnSizes.length];
ArrayList<String>[] fieldTitles = new ArrayList[columnSizes.length];
FIELD[] fields = FIELD.values();
for (int i = 0; i < fields.length; i++) {
columnSizes[i] = fields[i].toString().length();
ArrayList<String> splitTitleArrayList = new ArrayList<>();
String[] splitTitleStringArray = fields[i].toString().split("_");
centralize[i] = fields[i].center;
for (String s : splitTitleStringArray) {
splitTitleArrayList.add(s);
columnSizes[i] = Math.max(columnSizes[i], s.length());
}
fieldTitles[i] = splitTitleArrayList;
}
for (int i = 0; i < array.size(); i++) {
getColumnSizes(array.getJsonObject(i), columnSizes);
}
printHeadings(columnSizes);
TableOut tableOut = new TableOut("|", 2, columnSizes);
tableOut.print(getActionContext().out, fieldTitles, centralize);
for (int i = 0; i < array.size(); i++) {
printQueueStats(array.getJsonObject(i), columnSizes);
if (!includeManagement && array.getJsonObject(i).getString("name").contains(MANAGEMENT_QUEUE)) {
continue;
}
printQueueStats(array.getJsonObject(i), columnSizes, centralize, tableOut);
statCount++;
}
@ -240,6 +265,9 @@ public class StatQueue extends ConnectionAbstract {
private void getColumnSizes(JsonObject jsonObject, int[] columnSizes) {
int i = 0;
if (!includeManagement && jsonObject.getString("name").startsWith(MANAGEMENT_QUEUE)) {
return;
}
for (FIELD e: FIELD.values()) {
if (jsonObject.getString(e.jsonId).length() > columnSizes[i]) {
columnSizes[i] = jsonObject.getString(e.jsonId).length();
@ -252,19 +280,18 @@ public class StatQueue extends ConnectionAbstract {
}
}
private void printHeadings(int[] columnSizes) {
// add 10 for the various '|' characters
StringBuilder stringBuilder = new StringBuilder(Arrays.stream(columnSizes).sum() + FIELD.values().length + 1).append('|');
private void printHeadings(int[] columnSizes, TableOut tableOut) {
String[] columns = new String[columnSizes.length];
int i = 0;
for (FIELD e: FIELD.values()) {
stringBuilder.append(paddingString(new StringBuilder(e.toString()), columnSizes[i++])).append('|');
columns[i++] = e.toString();
}
getActionContext().out.println(stringBuilder);
tableOut.print(getActionContext().out, columns);
}
private void printQueueStats(JsonObject jsonObject, int[] columnSizes) {
private void printQueueStats(JsonObject jsonObject, int[] columnSizes, boolean[] center, TableOut tableOut) {
//should not happen but just in case..
if (jsonObject == null) {
@ -274,15 +301,12 @@ public class StatQueue extends ConnectionAbstract {
return;
}
// add 10 for the various '|' characters
StringBuilder stringBuilder = new StringBuilder(Arrays.stream(columnSizes).sum() + FIELD.values().length + 1).append('|');
int i = 0;
String[] columns = new String[columnSizes.length];
for (FIELD e: FIELD.values()) {
stringBuilder.append(paddingString(new StringBuilder(jsonObject.getString(e.jsonId)), columnSizes[i++])).append('|');
columns[i++] = jsonObject.getString(e.jsonId);
}
getActionContext().out.println(stringBuilder);
tableOut.print(getActionContext().out, columns, center);
}
private StringBuilder paddingString(StringBuilder value, int maxColumnSize) {

View File

@ -1583,7 +1583,7 @@ public class ArtemisTest extends CliTestBase {
statQueue.execute(context);
ArrayList<String> lines = getOutputLines(context, false);
// Header line + 3 queues
Assert.assertEquals("rows returned using queueName=Test1", 4, lines.size());
Assert.assertEquals("rows returned using queueName=Test1", 5, lines.size());
//check all queues are displayed when no Filter set
context = new TestActionContext();
@ -1606,7 +1606,7 @@ public class ArtemisTest extends CliTestBase {
statQueue.execute(context);
lines = getOutputLines(context, false);
// Header line + 3 queues
Assert.assertEquals("rows returned filtering by NAME ", 4, lines.size());
Assert.assertEquals("rows returned filtering by NAME ", 5, lines.size());
//check all queues NOT containing "management" are displayed using Filter field NAME
context = new TestActionContext();
@ -1619,7 +1619,7 @@ public class ArtemisTest extends CliTestBase {
statQueue.execute(context);
lines = getOutputLines(context, false);
// Header line + 6 queues (Test1/11/12/20+DLQ+ExpiryQueue, but not activemq.management.d6dbba78-d76f-43d6-a2c9-fc0575ed6f5d)
Assert.assertEquals("rows returned filtering by NAME operation NOT_CONTAINS", 7, lines.size());
Assert.assertEquals("rows returned filtering by NAME operation NOT_CONTAINS", 8, lines.size());
//check only queue named "Test1" is displayed using Filter field NAME and operation EQUALS
context = new TestActionContext();
@ -1632,9 +1632,9 @@ public class ArtemisTest extends CliTestBase {
statQueue.execute(context);
lines = getOutputLines(context, false);
//Header line + 1 queue only
Assert.assertEquals("rows returned filtering by NAME operation EQUALS", 2, lines.size());
Assert.assertEquals("rows returned filtering by NAME operation EQUALS", 3, lines.size());
//verify contents of queue stat line is correct
String queueTest1 = lines.get(1);
String queueTest1 = lines.get(2);
String[] parts = queueTest1.split("\\|");
Assert.assertEquals("queue name", "Test1", parts[1].trim());
Assert.assertEquals("address name", "Test1", parts[2].trim());
@ -1657,7 +1657,7 @@ public class ArtemisTest extends CliTestBase {
statQueue.execute(context);
lines = getOutputLines(context, false);
// Header line + 3 queues
Assert.assertEquals("rows returned filtering by ADDRESS", 4, lines.size());
Assert.assertEquals("rows returned filtering by ADDRESS", 5, lines.size());
//check all queues containing address "Test1" are displayed using Filter field MESSAGE_COUNT
context = new TestActionContext();
@ -1671,7 +1671,7 @@ public class ArtemisTest extends CliTestBase {
lines = getOutputLines(context, false);
// Header line + 0 queues
Assert.assertEquals("rows returned filtering by MESSAGE_COUNT", 1, lines.size());
Assert.assertEquals("rows returned filtering by MESSAGE_COUNT", 2, lines.size());
//check all queues containing address "Test1" are displayed using Filter field MESSAGE_ADDED
context = new TestActionContext();
@ -1684,7 +1684,7 @@ public class ArtemisTest extends CliTestBase {
statQueue.execute(context);
lines = getOutputLines(context, false);
// Header line + 0 queues
Assert.assertEquals("rows returned filtering by MESSAGES_ADDED", 1, lines.size());
Assert.assertEquals("rows returned filtering by MESSAGES_ADDED", 2, lines.size());
//check queues with greater_than 19 MESSAGE_ADDED displayed
context = new TestActionContext();
@ -1698,8 +1698,8 @@ public class ArtemisTest extends CliTestBase {
lines = getOutputLines(context, false);
// Header line + 1 queues
Assert.assertEquals("rows returned filtering by MESSAGES_ADDED", 2, lines.size());
String[] columns = lines.get(1).split("\\|");
Assert.assertEquals("rows returned filtering by MESSAGES_ADDED", 3, lines.size());
String[] columns = lines.get(2).split("\\|");
Assert.assertEquals("queue name filtered by MESSAGES_ADDED GREATER_THAN ", "Test20", columns[2].trim());
//check queues with less_than 2 MESSAGE_ADDED displayed
@ -1733,9 +1733,9 @@ public class ArtemisTest extends CliTestBase {
statQueue.setValue("10");
statQueue.execute(context);
lines = getOutputLines(context, false);
columns = lines.get(1).split("\\|");
columns = lines.get(2).split("\\|");
// Header line + 1 queues
Assert.assertEquals("rows returned filtering by DELIVERING_COUNT", 2, lines.size());
Assert.assertEquals("rows returned filtering by DELIVERING_COUNT", 3, lines.size());
Assert.assertEquals("queue name filtered by DELIVERING_COUNT ", "Test1", columns[2].trim());
//check all queues containing address "Test1" are displayed using Filter field CONSUMER_COUNT
@ -1748,9 +1748,9 @@ public class ArtemisTest extends CliTestBase {
statQueue.setValue("2");
statQueue.execute(context);
lines = getOutputLines(context, false);
columns = lines.get(1).split("\\|");
columns = lines.get(2).split("\\|");
// Header line + 1 queues
Assert.assertEquals("rows returned filtering by CONSUMER_COUNT ", 2, lines.size());
Assert.assertEquals("rows returned filtering by CONSUMER_COUNT ", 3, lines.size());
Assert.assertEquals("queue name filtered by CONSUMER_COUNT ", "Test1", columns[2].trim());
//check all queues containing address "Test1" are displayed using Filter field MESSAGE_ACKED
@ -1763,9 +1763,9 @@ public class ArtemisTest extends CliTestBase {
statQueue.setValue("5");
statQueue.execute(context);
lines = getOutputLines(context, false);
columns = lines.get(1).split("\\|");
columns = lines.get(2).split("\\|");
// Header line + 1 queues
Assert.assertEquals("rows returned filtering by MESSAGE_ACKED ", 2, lines.size());
Assert.assertEquals("rows returned filtering by MESSAGE_ACKED ", 3, lines.size());
Assert.assertEquals("queue name filtered by MESSAGE_ACKED", "Test1", columns[2].trim());
//check no queues are displayed when name does not match
@ -1777,7 +1777,7 @@ public class ArtemisTest extends CliTestBase {
statQueue.execute(context);
lines = getOutputLines(context, false);
// Header line + 0 queues
Assert.assertEquals("rows returned by queueName for no Matching queue ", 1, lines.size());
Assert.assertEquals("rows returned by queueName for no Matching queue ", 2, lines.size());
//check maxrows is taking effect"
context = new TestActionContext();
@ -1789,7 +1789,7 @@ public class ArtemisTest extends CliTestBase {
statQueue.execute(context);
lines = getOutputLines(context, false);
// Header line + 1 queue only + warning line
Assert.assertEquals("rows returned by maxRows=1", 3, lines.size());
Assert.assertEquals("rows returned by maxRows=1", 4, lines.size());
} finally {
stopServer();
@ -1850,7 +1850,7 @@ public class ArtemisTest extends CliTestBase {
statQueue.setQueueName(NAME);
statQueue.execute(context);
ArrayList<String> lines = getOutputLines(context, false);
Assert.assertEquals("rows returned", 2, lines.size());
Assert.assertEquals("rows returned", 4, lines.size());
String[] split = lines.get(1).split("\\|");
Assert.assertEquals(StatQueue.DEFAULT_MAX_COLUMN_SIZE, split[1].length());
@ -1862,7 +1862,7 @@ public class ArtemisTest extends CliTestBase {
statQueue.setMaxColumnSize(15);
statQueue.execute(context);
lines = getOutputLines(context, false);
Assert.assertEquals("rows returned", 2, lines.size());
Assert.assertEquals("rows returned", 5, lines.size());
split = lines.get(1).split("\\|");
Assert.assertEquals(15, split[1].length());
@ -1874,7 +1874,7 @@ public class ArtemisTest extends CliTestBase {
statQueue.setMaxColumnSize(50);
statQueue.execute(context);
lines = getOutputLines(context, false);
Assert.assertEquals("rows returned", 2, lines.size());
Assert.assertEquals("rows returned", 3, lines.size());
split = lines.get(1).split("\\|");
Assert.assertEquals(NAME.length(), split[1].length());
@ -1889,10 +1889,7 @@ public class ArtemisTest extends CliTestBase {
for (String line : lines) {
System.out.println(line);
}
Assert.assertEquals("rows returned", 2, lines.size());
split = lines.get(1).split("\\|");
Assert.assertEquals(NAME.length(), split[1].length());
Assert.assertEquals("CONSUMER_COUNT".length(), split[3].length());
Assert.assertEquals("rows returned", 3, lines.size());
} finally {
stopServer();
}
@ -2035,7 +2032,7 @@ public class ArtemisTest extends CliTestBase {
statQueue.execute(context);
lines = getOutputLines(context, false);
// Header line + DEFAULT_MAX_ROWS queues + warning line
Assert.assertEquals("rows returned using queueName=Test", 1 + StatQueue.DEFAULT_MAX_ROWS, lines.size());
Assert.assertEquals("rows returned using queueName=Test", 2 + StatQueue.DEFAULT_MAX_ROWS, lines.size());
Assert.assertFalse(lines.get(lines.size() - 1).startsWith("WARNING"));
//check all queues containing "Test" are displayed
@ -2048,7 +2045,7 @@ public class ArtemisTest extends CliTestBase {
statQueue.execute(context);
lines = getOutputLines(context, false);
// Header line + DEFAULT_MAX_ROWS queues
Assert.assertEquals("rows returned using queueName=Test", 1 + StatQueue.DEFAULT_MAX_ROWS, lines.size());
Assert.assertEquals("rows returned using queueName=Test", 2 + StatQueue.DEFAULT_MAX_ROWS, lines.size());
Assert.assertFalse(lines.get(lines.size() - 1).startsWith("WARNING"));
sendMessages(session, "Test" + StatQueue.DEFAULT_MAX_ROWS, 1);
@ -2062,7 +2059,7 @@ public class ArtemisTest extends CliTestBase {
statQueue.execute(context);
lines = getOutputLines(context, false);
// Header line + DEFAULT_MAX_ROWS queues + warning line
Assert.assertEquals("rows returned using queueName=Test", 1 + StatQueue.DEFAULT_MAX_ROWS + 1, lines.size());
Assert.assertEquals("rows returned using queueName=Test", 2 + StatQueue.DEFAULT_MAX_ROWS + 1, lines.size());
Assert.assertTrue(lines.get(lines.size() - 1).startsWith("WARNING"));
//check all queues containing "Test" are displayed
@ -2075,7 +2072,7 @@ public class ArtemisTest extends CliTestBase {
statQueue.execute(context);
lines = getOutputLines(context, false);
// Header line + DEFAULT_MAX_ROWS queues + warning line
Assert.assertEquals("rows returned using queueName=Test", 1 + StatQueue.DEFAULT_MAX_ROWS + 1, lines.size());
Assert.assertEquals("rows returned using queueName=Test", 2 + StatQueue.DEFAULT_MAX_ROWS + 1, lines.size());
Assert.assertTrue(lines.get(lines.size() - 1).startsWith("WARNING"));
} finally {

View File

@ -133,7 +133,7 @@ public class MessageSerializerTest extends CliTestBase {
int currentMessageCount;
try {
// parse the value for MESSAGE_COUNT from the output
currentMessageCount = Integer.parseInt(getOutputLines(context, false).get(1).split("\\|")[4].trim());
currentMessageCount = Integer.parseInt(getOutputLines(context, false).get(2).split("\\|")[4].trim());
} catch (Exception e) {
currentMessageCount = 0;
}

View File

@ -0,0 +1,122 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.activemq.artemis.utils;
import java.io.PrintStream;
import java.util.ArrayList;
public class TableOut {
final String separator;
final int[] columnSizes;
final int indentation;
final String indentationString;
public TableOut(String separator, int indentation, int[] columnSizes) {
this.separator = separator;
this.columnSizes = columnSizes;
this.indentation = indentation;
// building the indentation String to be reused
StringBuilder indentBuilder = new StringBuilder();
for (int i = 0; i < indentation; i++) {
indentBuilder.append(' ');
}
indentationString = indentBuilder.toString();
}
public void print(PrintStream stream, String[] columns) {
print(stream, columns, null);
}
public void print(PrintStream stream, String[] columns, boolean[] center) {
ArrayList<String>[] splitColumns = new ArrayList[columns.length];
for (int i = 0; i < columns.length; i++) {
splitColumns[i] = splitLine(columns[i], columnSizes[i]);
}
print(stream, splitColumns, center);
}
public void print(PrintStream stream, ArrayList<String>[] splitColumns) {
print(stream, splitColumns, null);
}
public void print(PrintStream stream, ArrayList<String>[] splitColumns, boolean[] centralize) {
boolean hasMoreLines;
int lineNumber = 0;
do {
hasMoreLines = false;
stream.print(separator);
for (int column = 0; column < splitColumns.length; column++) {
StringBuilder cell = new StringBuilder();
String cellString;
if (lineNumber < splitColumns[column].size()) {
cellString = splitColumns[column].get(lineNumber);
} else {
cellString = "";
}
if (centralize != null && centralize[column] && cellString.length() > 0) {
int centralAdd = (columnSizes[column] - cellString.length()) / 2;
for (int i = 0; i < centralAdd; i++) {
cell.append(' ');
}
}
cell.append(cellString);
if (lineNumber + 1 < splitColumns[column].size()) {
hasMoreLines = true;
}
while (cell.length() < columnSizes[column]) {
cell.append(" ");
}
stream.print(cell);
stream.print(separator);
}
stream.println();
lineNumber++;
}
while (hasMoreLines);
}
public ArrayList<String> splitLine(final String column, int size) {
ArrayList<String> cells = new ArrayList<>();
for (int position = 0; position < column.length();) {
int identationUsed;
String identationStringUsed;
if (position == 0 || indentation == 0) {
identationUsed = 0;
identationStringUsed = "";
} else {
identationUsed = indentation;
identationStringUsed = this.indentationString;
}
int maxPosition = Math.min(size - identationUsed, column.length() - position);
cells.add(identationStringUsed + column.substring(position, position + maxPosition));
position += maxPosition;
}
return cells;
}
}

View File

@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.activemq.artemis.utils;
import java.util.ArrayList;
import org.junit.Assert;
import org.junit.Test;
public class TableOutTest {
@Test
public void testSplitString() {
String bigCell = "1234554321321";
TableOut tableOut = new TableOut("|", 0, new int[] {10, 3, 3});
ArrayList<String> lines = tableOut.splitLine(bigCell, 5);
Assert.assertEquals(3, lines.size());
Assert.assertEquals("12345", lines.get(0));
Assert.assertEquals("54321", lines.get(1));
Assert.assertEquals("321", lines.get(2));
}
@Test
public void testSplitStringIdented() {
String bigCell = "1234532132";
TableOut tableOut = new TableOut("|", 2, new int[] {10, 3, 3});
ArrayList<String> lines = tableOut.splitLine(bigCell, 5);
Assert.assertEquals(3, lines.size());
Assert.assertEquals("12345", lines.get(0));
Assert.assertEquals(" 321", lines.get(1));
Assert.assertEquals(" 32", lines.get(2));
}
@Test
public void testOutLine() {
// the output is visual, however this test is good to make sure the output at least works without any issues
TableOut tableOut = new TableOut("|", 2, new int[] {5, 20, 20});
tableOut.print(System.out, new String[]{"This is a big title", "1234567", "1234"});
tableOut = new TableOut("|", 0, new int[] {10, 20, 20});
tableOut.print(System.out, new String[]{"This is a big title", "1234567", "1234"}, new boolean[] {true, true, true});
}
}

View File

@ -32,6 +32,8 @@ However, connections will no longer be pooled regardless of the configuration.
** This was done in an attempt to allow administrators to act when things are not working as expected, to get metrics on these objects and allow more transparency for the broker.
** this includes all Openwire Advisor queues and addresses, MQTT internal objects, Cluster Store and Forward (SNF) Queues, Mirror SNF.
** You may want to revisit authorizations if you mean to control access to certain users on the web console.
* The CLI operation `./artemis queue stat` has its output improved and updated. If you parse the previous output you will see differences in the output.
** It is not recommended to parse the output of a CLI Operation. You may use jolokia calls over management instead.
== 2.33.0