diff --git a/build.gradle b/build.gradle
index 568838605734e..7761a96a7819d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1763,6 +1763,7 @@ project(':tools') {
implementation libs.jacksonJDK8Datatypes
implementation libs.slf4jApi
implementation libs.log4j
+ implementation libs.joptSimple
implementation libs.jose4j // for SASL/OAUTHBEARER JWT validation
implementation libs.jacksonJaxrsJsonProvider
diff --git a/checkstyle/import-control.xml b/checkstyle/import-control.xml
index 0e767baee5d28..303fdab97d2f8 100644
--- a/checkstyle/import-control.xml
+++ b/checkstyle/import-control.xml
@@ -408,6 +408,7 @@
+
diff --git a/core/src/main/scala/kafka/tools/JmxTool.scala b/core/src/main/scala/kafka/tools/JmxTool.scala
deleted file mode 100644
index 223c459bcc270..0000000000000
--- a/core/src/main/scala/kafka/tools/JmxTool.scala
+++ /dev/null
@@ -1,275 +0,0 @@
-/*
- * 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 kafka.tools
-
-import java.util.{Date, Objects}
-import java.text.SimpleDateFormat
-import javax.management._
-import javax.management.remote._
-import javax.rmi.ssl.SslRMIClientSocketFactory
-import joptsimple.OptionParser
-
-import scala.jdk.CollectionConverters._
-import scala.collection.mutable
-import scala.math._
-import kafka.utils.{Exit, Logging}
-import org.apache.kafka.server.util.CommandLineUtils
-
-
-/**
- * A program for reading JMX metrics from a given endpoint.
- *
- * This tool only works reliably if the JmxServer is fully initialized prior to invoking the tool. See KAFKA-4620 for
- * details.
- */
-object JmxTool extends Logging {
-
- def main(args: Array[String]): Unit = {
- // Parse command line
- val parser = new OptionParser(false)
- val objectNameOpt =
- parser.accepts("object-name", "A JMX object name to use as a query. This can contain wild cards, and this option " +
- "can be given multiple times to specify more than one query. If no objects are specified " +
- "all objects will be queried.")
- .withRequiredArg
- .describedAs("name")
- .ofType(classOf[String])
- val attributesOpt =
- parser.accepts("attributes", "The list of attributes to include in the query. This is a comma-separated list. If no " +
- "attributes are specified all objects will be queried.")
- .withRequiredArg
- .describedAs("name")
- .ofType(classOf[String])
- val reportingIntervalOpt = parser.accepts("reporting-interval", "Interval in MS with which to poll jmx stats; default value is 2 seconds. " +
- "Value of -1 equivalent to setting one-time to true")
- .withRequiredArg
- .describedAs("ms")
- .ofType(classOf[java.lang.Integer])
- .defaultsTo(2000)
- val oneTimeOpt = parser.accepts("one-time", "Flag to indicate run once only.")
- .withRequiredArg
- .describedAs("one-time")
- .ofType(classOf[java.lang.Boolean])
- .defaultsTo(false)
- val dateFormatOpt = parser.accepts("date-format", "The date format to use for formatting the time field. " +
- "See java.text.SimpleDateFormat for options.")
- .withRequiredArg
- .describedAs("format")
- .ofType(classOf[String])
- val jmxServiceUrlOpt =
- parser.accepts("jmx-url", "The url to connect to poll JMX data. See Oracle javadoc for JMXServiceURL for details.")
- .withRequiredArg
- .describedAs("service-url")
- .ofType(classOf[String])
- .defaultsTo("service:jmx:rmi:///jndi/rmi://:9999/jmxrmi")
- val reportFormatOpt = parser.accepts("report-format", "output format name: either 'original', 'properties', 'csv', 'tsv' ")
- .withRequiredArg
- .describedAs("report-format")
- .ofType(classOf[java.lang.String])
- .defaultsTo("original")
- val jmxAuthPropOpt = parser.accepts("jmx-auth-prop", "A mechanism to pass property in the form 'username=password' " +
- "when enabling remote JMX with password authentication.")
- .withRequiredArg
- .describedAs("jmx-auth-prop")
- .ofType(classOf[String])
- val jmxSslEnableOpt = parser.accepts("jmx-ssl-enable", "Flag to enable remote JMX with SSL.")
- .withRequiredArg
- .describedAs("ssl-enable")
- .ofType(classOf[java.lang.Boolean])
- .defaultsTo(false)
- val waitOpt = parser.accepts("wait", "Wait for requested JMX objects to become available before starting output. " +
- "Only supported when the list of objects is non-empty and contains no object name patterns.")
- val helpOpt = parser.accepts("help", "Print usage information.")
-
-
- if(args.isEmpty)
- CommandLineUtils.printUsageAndExit(parser, "Dump JMX values to standard output.")
-
- val options = parser.parse(args : _*)
-
- if(options.has(helpOpt)) {
- parser.printHelpOn(System.out)
- Exit.exit(0)
- }
-
- val url = new JMXServiceURL(options.valueOf(jmxServiceUrlOpt))
- val interval = options.valueOf(reportingIntervalOpt).intValue
- val oneTime = interval < 0 || options.has(oneTimeOpt)
- val attributesIncludeExists = options.has(attributesOpt)
- val attributesInclude = if(attributesIncludeExists) Some(options.valueOf(attributesOpt).split(",").filterNot(_.equals(""))) else None
- val dateFormatExists = options.has(dateFormatOpt)
- val dateFormat = if(dateFormatExists) Some(new SimpleDateFormat(options.valueOf(dateFormatOpt))) else None
- val wait = options.has(waitOpt)
-
- val reportFormat = parseFormat(options.valueOf(reportFormatOpt).toLowerCase)
- val reportFormatOriginal = reportFormat.equals("original")
-
- val enablePasswordAuth = options.has(jmxAuthPropOpt)
- val enableSsl = options.has(jmxSslEnableOpt)
-
- var jmxc: JMXConnector = null
- var mbsc: MBeanServerConnection = null
- var connected = false
- val connectTimeoutMs = 10000
- val connectTestStarted = System.currentTimeMillis
- do {
- try {
- System.err.println(s"Trying to connect to JMX url: $url.")
- val env = new java.util.HashMap[String, AnyRef]
- // ssl enable
- if (enableSsl) {
- val csf = new SslRMIClientSocketFactory
- env.put("com.sun.jndi.rmi.factory.socket", csf)
- }
- // password authentication enable
- if (enablePasswordAuth) {
- val credentials = options.valueOf(jmxAuthPropOpt).split("=", 2)
- env.put(JMXConnector.CREDENTIALS, credentials)
- }
- jmxc = JMXConnectorFactory.connect(url, env)
- mbsc = jmxc.getMBeanServerConnection
- connected = true
- } catch {
- case e : Exception =>
- System.err.println(s"Could not connect to JMX url: $url. Exception ${e.getMessage}.")
- e.printStackTrace()
- Thread.sleep(100)
- }
- } while (System.currentTimeMillis - connectTestStarted < connectTimeoutMs && !connected)
-
- if (!connected) {
- System.err.println(s"Could not connect to JMX url $url after $connectTimeoutMs ms.")
- System.err.println("Exiting.")
- sys.exit(1)
- }
-
- val queries: Iterable[ObjectName] =
- if(options.has(objectNameOpt))
- options.valuesOf(objectNameOpt).asScala.map(new ObjectName(_))
- else
- List(null)
-
- val hasPatternQueries = queries.filterNot(Objects.isNull).exists((name: ObjectName) => name.isPattern)
-
- var names: Iterable[ObjectName] = null
- def namesSet = Option(names).toSet.flatten
- def foundAllObjects = queries.toSet == namesSet
- val waitTimeoutMs = 10000
- if (!hasPatternQueries) {
- val start = System.currentTimeMillis
- do {
- if (names != null) {
- System.err.println("Could not find all object names, retrying")
- Thread.sleep(100)
- }
- names = queries.flatMap((name: ObjectName) => mbsc.queryNames(name, null).asScala)
- } while (wait && System.currentTimeMillis - start < waitTimeoutMs && !foundAllObjects)
- }
-
- if (wait && !foundAllObjects) {
- val missing = (queries.toSet - namesSet).mkString(", ")
- System.err.println(s"Could not find all requested object names after $waitTimeoutMs ms. Missing $missing")
- System.err.println("Exiting.")
- sys.exit(1)
- }
-
- val numExpectedAttributes: Map[ObjectName, Int] =
- if (!attributesIncludeExists)
- names.map{name: ObjectName =>
- val mbean = mbsc.getMBeanInfo(name)
- (name, mbsc.getAttributes(name, mbean.getAttributes.map(_.getName)).size)}.toMap
- else {
- if (!hasPatternQueries)
- names.map{name: ObjectName =>
- val mbean = mbsc.getMBeanInfo(name)
- val attributes = mbsc.getAttributes(name, mbean.getAttributes.map(_.getName))
- val expectedAttributes = attributes.asScala.asInstanceOf[mutable.Buffer[Attribute]]
- .filter(attr => attributesInclude.get.contains(attr.getName))
- (name, expectedAttributes.size)}.toMap.filter(_._2 > 0)
- else
- queries.map((_, attributesInclude.get.length)).toMap
- }
-
- if(numExpectedAttributes.isEmpty) {
- CommandLineUtils.printUsageAndExit(parser, s"No matched attributes for the queried objects $queries.")
- }
-
- // print csv header
- val keys = List("time") ++ queryAttributes(mbsc, names, attributesInclude).keys.toArray.sorted
- if(reportFormatOriginal && keys.size == numExpectedAttributes.values.sum + 1) {
- println(keys.map("\"" + _ + "\"").mkString(","))
- }
-
- var keepGoing = true
- while (keepGoing) {
- val start = System.currentTimeMillis
- val attributes = queryAttributes(mbsc, names, attributesInclude)
- attributes("time") = dateFormat match {
- case Some(dFormat) => dFormat.format(new Date)
- case None => System.currentTimeMillis().toString
- }
- if(attributes.keySet.size == numExpectedAttributes.values.sum + 1) {
- if(reportFormatOriginal) {
- println(keys.map(attributes(_)).mkString(","))
- }
- else if(reportFormat.equals("properties")) {
- keys.foreach( k => { println(k + "=" + attributes(k) ) } )
- }
- else if(reportFormat.equals("csv")) {
- keys.foreach( k => { println(k + ",\"" + attributes(k) + "\"" ) } )
- }
- else { // tsv
- keys.foreach( k => { println(k + "\t" + attributes(k) ) } )
- }
- }
-
- if (oneTime) {
- keepGoing = false
- }
- else {
- val sleep = max(0, interval - (System.currentTimeMillis - start))
- Thread.sleep(sleep)
- }
- }
- }
-
- def queryAttributes(mbsc: MBeanServerConnection, names: Iterable[ObjectName], attributesInclude: Option[Array[String]]): mutable.Map[String, Any] = {
- val attributes = new mutable.HashMap[String, Any]()
- for (name <- names) {
- val mbean = mbsc.getMBeanInfo(name)
- for (attrObj <- mbsc.getAttributes(name, mbean.getAttributes.map(_.getName)).asScala) {
- val attr = attrObj.asInstanceOf[Attribute]
- attributesInclude match {
- case Some(allowedAttributes) =>
- if (allowedAttributes.contains(attr.getName))
- attributes(name.toString + ":" + attr.getName) = attr.getValue
- case None => attributes(name.toString + ":" + attr.getName) = attr.getValue
- }
- }
- }
- attributes
- }
-
- def parseFormat(reportFormatOpt : String): String = reportFormatOpt match {
- case "properties" => "properties"
- case "csv" => "csv"
- case "tsv" => "tsv"
- case _ => "original"
- }
-}
diff --git a/tests/kafkatest/services/monitor/jmx.py b/tests/kafkatest/services/monitor/jmx.py
index bff1878fada62..03eec51f7173a 100644
--- a/tests/kafkatest/services/monitor/jmx.py
+++ b/tests/kafkatest/services/monitor/jmx.py
@@ -141,7 +141,7 @@ def read_jmx_output_all_nodes(self):
self.read_jmx_output(self.idx(node), node)
def jmx_class_name(self):
- return "kafka.tools.JmxTool"
+ return "org.apache.kafka.tools.JmxTool"
class JmxTool(JmxMixin, KafkaPathResolverMixin):
"""
diff --git a/tools/src/main/java/org/apache/kafka/tools/JmxTool.java b/tools/src/main/java/org/apache/kafka/tools/JmxTool.java
new file mode 100644
index 0000000000000..ea75748f68cf7
--- /dev/null
+++ b/tools/src/main/java/org/apache/kafka/tools/JmxTool.java
@@ -0,0 +1,430 @@
+/*
+ * 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.kafka.tools;
+
+import joptsimple.OptionSpec;
+import org.apache.kafka.common.utils.Exit;
+import org.apache.kafka.common.utils.Utils;
+import org.apache.kafka.server.util.CommandDefaultOptions;
+import org.apache.kafka.server.util.CommandLineUtils;
+
+import javax.management.Attribute;
+import javax.management.AttributeList;
+import javax.management.MBeanFeatureInfo;
+import javax.management.MBeanInfo;
+import javax.management.MBeanServerConnection;
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXServiceURL;
+import javax.rmi.ssl.SslRMIClientSocketFactory;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiPredicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * A program for reading JMX metrics from a given endpoint.
+ *
+ * This tool only works reliably if the JmxServer is fully initialized prior to invoking the tool.
+ * See KAFKA-4620 for details.
+ */
+public class JmxTool {
+ public static void main(String[] args) {
+ try {
+ JmxToolOptions options = new JmxToolOptions(args);
+ if (CommandLineUtils.isPrintHelpNeeded(options)) {
+ CommandLineUtils.printUsageAndExit(options.parser, "Dump JMX values to standard output.");
+ return;
+ }
+ if (CommandLineUtils.isPrintVersionNeeded(options)) {
+ CommandLineUtils.printVersionAndExit();
+ return;
+ }
+
+ Optional attributesInclude = options.attributesInclude();
+ Optional dateFormat = options.dateFormat();
+ String reportFormat = options.parseFormat();
+ boolean keepGoing = true;
+
+ MBeanServerConnection conn = connectToBeanServer(options);
+ List queries = options.queries();
+ boolean hasPatternQueries = queries.stream().filter(Objects::nonNull).anyMatch(ObjectName::isPattern);
+
+ Set found = findObjectsIfNoPattern(options, conn, queries, hasPatternQueries);
+ Map numExpectedAttributes =
+ findNumExpectedAttributes(conn, attributesInclude, hasPatternQueries, queries, found);
+
+ List keys = new ArrayList<>();
+ keys.add("time");
+ keys.addAll(new TreeSet<>(queryAttributes(conn, found, attributesInclude).keySet()));
+ maybePrintCsvHeader(reportFormat, keys, numExpectedAttributes);
+
+ while (keepGoing) {
+ long start = System.currentTimeMillis();
+ Map attributes = queryAttributes(conn, found, attributesInclude);
+ attributes.put("time", dateFormat.isPresent() ? dateFormat.get().format(new Date()) : String.valueOf(System.currentTimeMillis()));
+ maybePrintDataRows(reportFormat, numExpectedAttributes, keys, attributes);
+ if (options.isOneTime()) {
+ keepGoing = false;
+ } else {
+ TimeUnit.MILLISECONDS.sleep(Math.max(0, options.interval() - (System.currentTimeMillis() - start)));
+ }
+ }
+ Exit.exit(0);
+ } catch (TerseException e) {
+ System.err.println(e.getMessage());
+ Exit.exit(1);
+ } catch (Throwable e) {
+ System.err.println(e.getMessage());
+ System.err.println(Utils.stackTrace(e));
+ Exit.exit(1);
+ }
+ }
+
+ private static String mkString(Stream