From 89aa698ac5a466a906e4df28e495663eef7d5a56 Mon Sep 17 00:00:00 2001 From: Mitchell Armstrong <48131175+armstrmi@users.noreply.github.com> Date: Wed, 19 Jan 2022 13:06:20 -0500 Subject: [PATCH] jmx-metrics: Activemq (#188) * wip * Added activeMQ update * updated docs * implemented changes recommended by pr review * updated docs to remove curly braces * spotless check * added activemq to list * fixed incorrect link * forced reset * forced reset --- jmx-metrics/README.md | 1 + jmx-metrics/docs/target-systems/activemq.md | 79 +++++++++ .../ActivemqIntegrationTest.java | 151 ++++++++++++++++++ .../resources/activemq/Dockerfile | 8 + .../resources/activemq/config/env | 117 ++++++++++++++ .../target-systems/activemq.properties | 7 + .../contrib/jmxmetrics/JmxConfig.java | 3 +- .../resources/target-systems/activemq.groovy | 124 ++++++++++++++ .../contrib/jmxmetrics/JmxConfigTest.java | 5 +- 9 files changed, 492 insertions(+), 3 deletions(-) create mode 100644 jmx-metrics/docs/target-systems/activemq.md create mode 100644 jmx-metrics/src/integrationTest/java/io/opentelemetry/contrib/jmxmetrics/target_systems/ActivemqIntegrationTest.java create mode 100644 jmx-metrics/src/integrationTest/resources/activemq/Dockerfile create mode 100644 jmx-metrics/src/integrationTest/resources/activemq/config/env create mode 100644 jmx-metrics/src/integrationTest/resources/target-systems/activemq.properties create mode 100644 jmx-metrics/src/main/resources/target-systems/activemq.groovy diff --git a/jmx-metrics/README.md b/jmx-metrics/README.md index 12f402785..65e277eed 100644 --- a/jmx-metrics/README.md +++ b/jmx-metrics/README.md @@ -69,6 +69,7 @@ mutually exclusive with `otel.jmx.groovy.script`. The currently supported target | `otel.jmx.target.system` | | ------------------------ | | [`jvm`](./docs/target-systems/jvm.md) | +| [`activemq`](./docs/target-systems/activemq.md)| | [`cassandra`](./docs/target-systems/cassandra.md) | | [`kafka`](./docs/target-systems/kafka.md) | | [`kafka-consumer`](./docs/target-systems/kafka-consumer.md) | diff --git a/jmx-metrics/docs/target-systems/activemq.md b/jmx-metrics/docs/target-systems/activemq.md new file mode 100644 index 000000000..843749f65 --- /dev/null +++ b/jmx-metrics/docs/target-systems/activemq.md @@ -0,0 +1,79 @@ +# ActiveMQ Metrics + +The JMX Metric Gatherer provides built in ActiveMQ metric gathering capabilities. +These metrics are sourced from: https://activemq.apache.org/jmx + +### Metrics + +* Name: `activemq.consumer.count` +* Description: The number of consumers currently reading from the broker. +* Unit: `consumers` +* Labels: `destination` +* Instrument Type: ObservableLongUpDownCounter + + +* Name: `activemq.producer.count` +* Description: The number of producers currently attached to the broker. +* Unit: `producers` +* Labels: `destination` +* Instrument Type: ObservableLongUpDownCounter + + +* Name: `activemq.connectin.count` +* Description: The total number of current connections. +* Unit: `connections` +* Instrument Type: ObservableLongUpDownCounter + + +* Name: `activemq.memory.usage` +* Description: The percentage of configured memory used. +* Unit: `%` +* Labels: `destination` +* Instrument Type: ObservableDoubleValue + + +* Name: `activemq.disk.store_usage` +* Description: The percentage of configured disk used for persistent messages. +* Unit: `%` +* Instrument Type: ObservableDoubleValue + + +* Name: `activemq.disk.temp_usage` +* Description: The percentage of configured disk used for non-persistent messages. +* Unit: `%` +* Instrument Type: ObservableDoubleValue + + +* Name: `activemq.message.current` +* Description: The current number of messages waiting to be consumed. +* Unit: `messages` +* Labels: `destination` +* Instrument Type: ObservableLongUpDownCounter + + +* Name: `activemq.message.expired` +* Description: The total number of messages not delivered because they expired. +* Unit: `messages` +* Labels: `destination` +* Instrument Type: ObservableLongCounter + + +* Name: `activemq.message.enqueued` +* Description: The total number of messages received by the broker. +* Unit: `messages` +* Labels: `destination` +* Instrument Type: ObservableLongCounter + + +* Name: `activemq.message.dequeued` +* Description: The total number of messages delivered to consumers. +* Unit: `messages` +* Labels: `destination` +* Instrument Type: ObservableLongCounter + + +* Name: `activemq.message.wait_time.avg` +* Description: The average time a message was held on a destination. +* Unit: `ms` +* Labels: `destination` +* Instrument Type: ObservableDoubleValue diff --git a/jmx-metrics/src/integrationTest/java/io/opentelemetry/contrib/jmxmetrics/target_systems/ActivemqIntegrationTest.java b/jmx-metrics/src/integrationTest/java/io/opentelemetry/contrib/jmxmetrics/target_systems/ActivemqIntegrationTest.java new file mode 100644 index 000000000..2bbb46bc7 --- /dev/null +++ b/jmx-metrics/src/integrationTest/java/io/opentelemetry/contrib/jmxmetrics/target_systems/ActivemqIntegrationTest.java @@ -0,0 +1,151 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxmetrics.target_systems; + +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.contrib.jmxmetrics.AbstractIntegrationTest; +import java.time.Duration; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.junit.jupiter.Container; + +class ActivemqIntegrationTest extends AbstractIntegrationTest { + + ActivemqIntegrationTest() { + super(/* configFromStdin= */ false, "target-systems/activemq.properties"); + } + + @Container + GenericContainer activemq = + new GenericContainer<>( + new ImageFromDockerfile() + .withFileFromClasspath("config/env", "activemq/config/env") + .withFileFromClasspath("Dockerfile", "activemq/Dockerfile")) + .withNetwork(Network.SHARED) + .withEnv("LOCAL_JMX", "no") + .withNetworkAliases("activemq") + .withExposedPorts(10991) + .withStartupTimeout(Duration.ofMinutes(2)) + .waitingFor(Wait.forListeningPort()); + + @Test + void endToEnd() { + waitAndAssertMetrics( + metric -> + assertSumWithAttributes( + metric, + "activemq.consumer.count", + "The number of consumers currently reading from the broker.", + "consumers", + attrs -> + attrs.containsOnly( + entry("destination", "ActiveMQ.Advisory.MasterBroker"), + entry("broker", "localhost"))), + metric -> + assertSumWithAttributes( + metric, + "activemq.producer.count", + "The number of producers currently attached to the broker.", + "producers", + attrs -> + attrs.containsOnly( + entry("destination", "ActiveMQ.Advisory.MasterBroker"), + entry("broker", "localhost"))), + metric -> + assertSum( + metric, + "activemq.connection.count", + "The total number of current connections.", + "connections", + /* isMonotonic= */ false), + metric -> + assertGaugeWithAttributes( + metric, + "activemq.memory.usage", + "The percentage of configured memory used.", + "%", + attrs -> + attrs.containsOnly( + entry("destination", "ActiveMQ.Advisory.MasterBroker"), + entry("broker", "localhost"))), + metric -> + assertGauge( + metric, + "activemq.disk.store_usage", + "The percentage of configured disk used for persistent messages.", + "%"), + metric -> + assertGauge( + metric, + "activemq.disk.temp_usage", + "The percentage of configured disk used for non-persistent messages.", + "%"), + metric -> + assertSumWithAttributes( + metric, + "activemq.message.current", + "The current number of messages waiting to be consumed.", + "messages", + attrs -> + attrs.containsOnly( + entry("destination", "ActiveMQ.Advisory.MasterBroker"), + entry("broker", "localhost"))), + metric -> + assertSumWithAttributes( + metric, + "activemq.message.current", + "The current number of messages waiting to be consumed.", + "messages", + attrs -> + attrs.containsOnly( + entry("destination", "ActiveMQ.Advisory.MasterBroker"), + entry("broker", "localhost"))), + metric -> + assertSumWithAttributes( + metric, + "activemq.message.expired", + "The total number of messages not delivered because they expired.", + "messages", + attrs -> + attrs.containsOnly( + entry("destination", "ActiveMQ.Advisory.MasterBroker"), + entry("broker", "localhost"))), + metric -> + assertSumWithAttributes( + metric, + "activemq.message.enqueued", + "The total number of messages received by the broker.", + "messages", + attrs -> + attrs.containsOnly( + entry("destination", "ActiveMQ.Advisory.MasterBroker"), + entry("broker", "localhost"))), + metric -> + assertSumWithAttributes( + metric, + "activemq.message.dequeued", + "The total number of messages delivered to consumers.", + "messages", + attrs -> + attrs.containsOnly( + entry("destination", "ActiveMQ.Advisory.MasterBroker"), + entry("broker", "localhost"))), + metric -> + assertGaugeWithAttributes( + metric, + "activemq.message.wait_time.avg", + "The average time a message was held on a destination.", + "ms", + attrs -> + attrs.containsOnly( + entry("destination", "ActiveMQ.Advisory.MasterBroker"), + entry("broker", "localhost")))); + } +} diff --git a/jmx-metrics/src/integrationTest/resources/activemq/Dockerfile b/jmx-metrics/src/integrationTest/resources/activemq/Dockerfile new file mode 100644 index 000000000..be578bf61 --- /dev/null +++ b/jmx-metrics/src/integrationTest/resources/activemq/Dockerfile @@ -0,0 +1,8 @@ +FROM rmohr/activemq:5.15.9-alpine + + +COPY --chown=activemq config/env /opt/activemq/bin/env +ENV ACTIVEMQ_JMX=10991 +EXPOSE $ACTIVEMQ_JMX + +ENV ACTIVEMQ_JMX_OPTS="-Dcom.sun.management.jmxremote.port=10991 -Dcom.sun.management.jmxremote.rmi.port=10991 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false" diff --git a/jmx-metrics/src/integrationTest/resources/activemq/config/env b/jmx-metrics/src/integrationTest/resources/activemq/config/env new file mode 100644 index 000000000..bfcc87c6f --- /dev/null +++ b/jmx-metrics/src/integrationTest/resources/activemq/config/env @@ -0,0 +1,117 @@ + #!/bin/sh + # ------------------------------------------------------------------------ + # 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. + # ------------------------------------------------------------------------ + # + # Configuration file for running Apache Active MQ as standalone provider. + # + # This file overwrites the predefined settings of the sysv init-script. + # You can also use alternate location for default settings - + # invoke the init-script without a argument an review help section "Configuration of this script" + # /etc/default/activemq /.activemqrc /bin/env + + # Active MQ installation dirs + # ACTIVEMQ_HOME="/" + # ACTIVEMQ_BASE="$ACTIVEMQ_HOME" + # ACTIVEMQ_CONF="$ACTIVEMQ_BASE/conf" + # ACTIVEMQ_DATA="$ACTIVEMQ_BASE/data" + # ACTIVEMQ_TMP="$ACTIVEMQ_BASE/tmp" + + # Set jvm memory configuration (minimal/maximum amount of memory) + ACTIVEMQ_OPTS_MEMORY="-Xms64M -Xmx256M" + + if [ -z "$ACTIVEMQ_OPTS" ] ; then + ACTIVEMQ_OPTS="$ACTIVEMQ_OPTS_MEMORY -Djava.util.logging.config.file=logging.properties -Djava.security.auth.login.config=$ACTIVEMQ_CONF/login.config" + fi + + if [ -z "$ACTIVEMQ_OUT" ]; then + ACTIVEMQ_OUT="/dev/null" + fi + + # Uncomment to enable audit logging + #ACTIVEMQ_OPTS="$ACTIVEMQ_OPTS -Dorg.apache.activemq.audit=true" + + # Set jvm jmx configuration + # This enables jmx access over a configured jmx-tcp-port. + # You have to configure the first four settings if you run a ibm jvm, caused by the + # fact that IBM's jvm does not support VirtualMachine.attach(PID). + # JMX access is needed for quering a running activemq instance to gain data or to + # trigger management operations. + # + # Example for ${ACTIVEMQ_CONF}/jmx.access: + # --- + # # The "monitorRole" role has readonly access. + # # The "controlRole" role has readwrite access. + # monitorRole readonly + # controlRole readwrite + # --- + # + # Example for ${ACTIVEMQ_CONF}/jmx.password: + # --- + # # The "monitorRole" role has password "abc123". + # # # The "controlRole" role has password "abcd1234". + # monitorRole abc123 + # controlRole abcd1234 + # --- + # + # ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.port=11099 " + # ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.password.file=${ACTIVEMQ_CONF}/jmx.password" + # ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.access.file=${ACTIVEMQ_CONF}/jmx.access" + # ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.ssl=false" + # ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote" + ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote" + + # Set jvm jmx configuration for controlling the broker process + # You only have to configure the first four settings if you run a ibm jvm, caused by the + # fact that IBM's jvm does not support VirtualMachine.attach(PID) + # (see also com.sun.management.jmxremote.port, .jmx.password.file and .jmx.access.file ) + #ACTIVEMQ_SUNJMX_CONTROL="--jmxurl service:jmx:rmi:///jndi/rmi://127.0.0.1:1099/jmxrmi --jmxuser controlRole --jmxpassword abcd1234" + ACTIVEMQ_SUNJMX_CONTROL="" + + # Specify the queue manager URL for using "browse" option of sysv initscript + if [ -z "$ACTIVEMQ_QUEUEMANAGERURL" ]; then + ACTIVEMQ_QUEUEMANAGERURL="--amqurl tcp://localhost:61616" + fi + + # Set additional JSE arguments + if [ -z "$ACTIVEMQ_SSL_OPTS" ] ; then + #ACTIVEMQ_SSL_OPTS="-Djava.security.properties=$ACTIVEMQ_CONF/java.security" + ACTIVEMQ_SSL_OPTS="" + fi + + # Uncomment to enable remote debugging + #ACTIVEMQ_DEBUG_OPTS="-Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005" + + # ActiveMQ tries to shutdown the broker by jmx, + # after a specified number of seconds send SIGKILL + if [ -z "$ACTIVEMQ_KILL_MAXSECONDS" ]; then + ACTIVEMQ_KILL_MAXSECONDS=30 + fi + + # Configure a user with non root privileges, if no user is specified do not change user + # (the entire activemq installation should be owned by this user) + ACTIVEMQ_USER="" + + # location of the pidfile + # ACTIVEMQ_PIDFILE="$ACTIVEMQ_DATA/activemq.pid" + + # Location of the java installation + # Specify the location of your java installation using JAVA_HOME, or specify the + # path to the "java" binary using JAVACMD + # (set JAVACMD to "auto" for automatic detection) + #JAVA_HOME="" + JAVACMD="auto" + ACTIVEMQ_OPTS="$ACTIVEMQ_OPTS $ACTIVEMQ_JMX_OPTS -Dhawtio.authenticationEnabled=false -Dhawtio.realm=activemq -Dhawtio.role=admins -Dhawtio.rolePrincipalClasses=org.apache.activemq.jaas.GroupPrincipal" diff --git a/jmx-metrics/src/integrationTest/resources/target-systems/activemq.properties b/jmx-metrics/src/integrationTest/resources/target-systems/activemq.properties new file mode 100644 index 000000000..e69b6ba2e --- /dev/null +++ b/jmx-metrics/src/integrationTest/resources/target-systems/activemq.properties @@ -0,0 +1,7 @@ +otel.jmx.interval.milliseconds = 3000 +otel.metrics.exporter = otlp +otel.jmx.service.url = service:jmx:rmi:///jndi/rmi://activemq:10991/jmxrmi +otel.jmx.target.system = activemq + +# these will be overridden by cmd line +otel.exporter.otlp.endpoint = http://host.testcontainers.internal diff --git a/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/JmxConfig.java b/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/JmxConfig.java index 2f12d1d6b..be3a7b501 100644 --- a/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/JmxConfig.java +++ b/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/JmxConfig.java @@ -33,7 +33,8 @@ class JmxConfig { static final String JMX_REALM = PREFIX + "jmx.realm"; static final List AVAILABLE_TARGET_SYSTEMS = - Arrays.asList("cassandra", "jvm", "kafka", "kafka-consumer", "kafka-producer", "tomcat"); + Arrays.asList( + "activemq", "cassandra", "jvm", "kafka", "kafka-consumer", "kafka-producer", "tomcat"); final String serviceUrl; final String groovyScript; diff --git a/jmx-metrics/src/main/resources/target-systems/activemq.groovy b/jmx-metrics/src/main/resources/target-systems/activemq.groovy new file mode 100644 index 000000000..4be94529c --- /dev/null +++ b/jmx-metrics/src/main/resources/target-systems/activemq.groovy @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * + * 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. + */ + + +def activemqMetrics = otel.mbeans( + [ + "org.apache.activemq:type=Broker,brokerName=*,destinationType=Queue,destinationName=*", + "org.apache.activemq:type=Broker,brokerName=*,destinationType=Topic,destinationName=*" + ] +) + + +otel.instrument(activemqMetrics, + "activemq.producer.count", + "The number of producers currently attached to the broker.", + "producers", + ["destination" : { mbean -> mbean.name().getKeyProperty("destinationName")}, + "broker" : { mbean -> mbean.name().getKeyProperty("brokerName")}], + "ProducerCount", + otel.&longUpDownCounterCallback) + +otel.instrument(activemqMetrics, + "activemq.consumer.count", + "The number of consumers currently reading from the broker.", + "consumers", + ["destination" : { mbean -> mbean.name().getKeyProperty("destinationName") }, + "broker" : { mbean -> mbean.name().getKeyProperty("brokerName")}], + "ConsumerCount", + otel.&longUpDownCounterCallback) + +otel.instrument(activemqMetrics, + "activemq.memory.usage", + "The percentage of configured memory used.", + "%", + ["destination" : { mbean -> mbean.name().getKeyProperty("destinationName") }, + "broker" : { mbean -> mbean.name().getKeyProperty("brokerName")}], + "MemoryPercentUsage", + otel.&doubleValueCallback) + +otel.instrument(activemqMetrics, + "activemq.message.current", + "The current number of messages waiting to be consumed.", + "messages", + ["destination" : { mbean -> mbean.name().getKeyProperty("destinationName") }, + "broker" : { mbean -> mbean.name().getKeyProperty("brokerName")}], + "QueueSize", + otel.&longUpDownCounterCallback) + +otel.instrument(activemqMetrics, + "activemq.message.expired", + "The total number of messages not delivered because they expired.", + "messages", + ["destination" : { mbean -> mbean.name().getKeyProperty("destinationName") }, + "broker" : { mbean -> mbean.name().getKeyProperty("brokerName")}], + "ExpiredCount", + otel.&longCounterCallback) + +otel.instrument(activemqMetrics, + "activemq.message.enqueued", + "The total number of messages received by the broker.", + "messages", + ["destination" : { mbean -> mbean.name().getKeyProperty("destinationName") }, + "broker" : { mbean -> mbean.name().getKeyProperty("brokerName")}], + "EnqueueCount", + otel.&longCounterCallback) + +otel.instrument(activemqMetrics, + "activemq.message.dequeued", + "The total number of messages delivered to consumers.", + "messages", + ["destination" : { mbean -> mbean.name().getKeyProperty("destinationName") }, + "broker" : { mbean -> mbean.name().getKeyProperty("brokerName")}], + "DequeueCount", + otel.&longCounterCallback) + +otel.instrument(activemqMetrics, + "activemq.message.wait_time.avg", + "The average time a message was held on a destination.", + "ms", + ["destination" : { mbean -> mbean.name().getKeyProperty("destinationName") }, + "broker" : { mbean -> mbean.name().getKeyProperty("brokerName")}], + "AverageEnqueueTime", + otel.&doubleValueCallback) + + + + +def activemqMetricsNoDestination = otel.mbean( + "org.apache.activemq:type=Broker,brokerName=*" +) + +otel.instrument(activemqMetricsNoDestination, + "activemq.connection.count", + "The total number of current connections.", + "connections", + "CurrentConnectionsCount", + otel.&longUpDownCounterCallback) + +otel.instrument(activemqMetricsNoDestination, + "activemq.disk.store_usage", + "The percentage of configured disk used for persistent messages.", + "%", + "StorePercentUsage", + otel.&doubleValueCallback) + +otel.instrument(activemqMetricsNoDestination, + "activemq.disk.temp_usage", + "The percentage of configured disk used for non-persistent messages.", + "%", + "TempPercentUsage", + otel.&doubleValueCallback) \ No newline at end of file diff --git a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/JmxConfigTest.java b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/JmxConfigTest.java index 6339e9c09..ccab68539 100644 --- a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/JmxConfigTest.java +++ b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/JmxConfigTest.java @@ -17,7 +17,8 @@ class JmxConfigTest { @Test void staticValues() { assertThat(JmxConfig.AVAILABLE_TARGET_SYSTEMS) - .containsOnly("cassandra", "jvm", "kafka", "kafka-consumer", "kafka-producer", "tomcat"); + .containsOnly( + "activemq", "cassandra", "jvm", "kafka", "kafka-consumer", "kafka-producer", "tomcat"); } @Test @@ -115,7 +116,7 @@ void invalidTargetSystem() { .isInstanceOf(ConfigurationException.class) .hasMessage( "[jvm, unavailabletargetsystem] must specify targets from " - + "[cassandra, jvm, kafka, kafka-consumer, kafka-producer, tomcat]"); + + "[activemq, cassandra, jvm, kafka, kafka-consumer, kafka-producer, tomcat]"); } @Test