From 1fd985a55e451b0063669f09f04e0cf6a420fa59 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Wed, 27 Jun 2018 16:06:19 -0700 Subject: [PATCH 01/49] moving to 2.1.0 framework --- .idea/compiler.xml | 2 + ...om_appdynamics_appd_exts_commons_1_6_4.xml | 13 -- .../Maven__com_google_guava_guava_11_0_2.xml | 13 -- .../Maven__javax_ws_rs_jsr311_api_1_1_1.xml | 13 -- kafka-monitoring-extension.iml | 30 --- pom.xml | 2 +- .../kafka/JMXConnectionAdapter.java | 17 +- .../extensions/kafka/KafkaMonitor.java | 182 +++--------------- .../extensions/kafka/KafkaMonitorTask.java | 166 ++++++++-------- .../appdynamics/extensions/kafka/Util.java | 31 --- .../metrics/DefaultMetricProperties.java | 26 --- .../kafka/metrics/DomainMetricsProcessor.java | 96 +++------ .../extensions/kafka/metrics/Metric.java | 56 ------ .../kafka/metrics/MetricPrinter.java | 70 ------- .../kafka/metrics/MetricProperties.java | 107 ---------- .../metrics/MetricPropertiesBuilder.java | 65 ------- .../kafka/metrics/MetricValueTransformer.java | 73 ------- .../Constants.java} | 16 +- src/main/resources/config/config.yml | 2 +- 19 files changed, 156 insertions(+), 824 deletions(-) delete mode 100644 .idea/libraries/Maven__com_appdynamics_appd_exts_commons_1_6_4.xml delete mode 100644 .idea/libraries/Maven__com_google_guava_guava_11_0_2.xml delete mode 100644 .idea/libraries/Maven__javax_ws_rs_jsr311_api_1_1_1.xml delete mode 100644 kafka-monitoring-extension.iml delete mode 100644 src/main/java/com/appdynamics/extensions/kafka/Util.java delete mode 100644 src/main/java/com/appdynamics/extensions/kafka/metrics/DefaultMetricProperties.java delete mode 100644 src/main/java/com/appdynamics/extensions/kafka/metrics/Metric.java delete mode 100644 src/main/java/com/appdynamics/extensions/kafka/metrics/MetricPrinter.java delete mode 100644 src/main/java/com/appdynamics/extensions/kafka/metrics/MetricProperties.java delete mode 100644 src/main/java/com/appdynamics/extensions/kafka/metrics/MetricPropertiesBuilder.java delete mode 100644 src/main/java/com/appdynamics/extensions/kafka/metrics/MetricValueTransformer.java rename src/main/java/com/appdynamics/extensions/kafka/{ConfigConstants.java => utils/Constants.java} (72%) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 96ad45e..cac0afc 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -10,6 +10,8 @@ + + diff --git a/.idea/libraries/Maven__com_appdynamics_appd_exts_commons_1_6_4.xml b/.idea/libraries/Maven__com_appdynamics_appd_exts_commons_1_6_4.xml deleted file mode 100644 index eb58e12..0000000 --- a/.idea/libraries/Maven__com_appdynamics_appd_exts_commons_1_6_4.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__com_google_guava_guava_11_0_2.xml b/.idea/libraries/Maven__com_google_guava_guava_11_0_2.xml deleted file mode 100644 index 01a573a..0000000 --- a/.idea/libraries/Maven__com_google_guava_guava_11_0_2.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__javax_ws_rs_jsr311_api_1_1_1.xml b/.idea/libraries/Maven__javax_ws_rs_jsr311_api_1_1_1.xml deleted file mode 100644 index a0c4d76..0000000 --- a/.idea/libraries/Maven__javax_ws_rs_jsr311_api_1_1_1.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/kafka-monitoring-extension.iml b/kafka-monitoring-extension.iml deleted file mode 100644 index c13495a..0000000 --- a/kafka-monitoring-extension.iml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index e85060b..955642c 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ com.appdynamics appd-exts-commons - 1.6.4 + 2.1.0 commons-lang diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index 8cfbbdf..0d4e87b 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -43,19 +43,16 @@ private JMXConnectionAdapter(String host, int port, String username, String pass this.password = password; } - private JMXConnectionAdapter(String serviceUrl, String username, String password) throws MalformedURLException { - this.serviceUrl = new JMXServiceURL(serviceUrl); - this.username = username; - this.password = password; + private JMXConnectionAdapter(Map requestMap) throws MalformedURLException { + this.serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + requestMap.get("host") + ":" + requestMap.get("port") + "/jmxrmi"); + this.username = requestMap.get("username"); + this.password = requestMap.get("password"); } - static JMXConnectionAdapter create(String serviceUrl, String host, int port, String username, String password) throws MalformedURLException { - if (Strings.isNullOrEmpty(serviceUrl)) { - return new JMXConnectionAdapter(host, port, username, password); - } else { - return new JMXConnectionAdapter(serviceUrl, username, password); - } + static JMXConnectionAdapter create(Map requestMap) throws MalformedURLException { + + return new JMXConnectionAdapter(requestMap); } JMXConnector open() throws IOException { diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index 7904eff..cc30911 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -8,175 +8,57 @@ package com.appdynamics.extensions.kafka; -import static com.appdynamics.TaskInputArgs.PASSWORD_ENCRYPTED; -import static com.appdynamics.extensions.kafka.ConfigConstants.DISPLAY_NAME; -import static com.appdynamics.extensions.kafka.ConfigConstants.ENCRYPTED_PASSWORD; -import static com.appdynamics.extensions.kafka.ConfigConstants.HOST; -import static com.appdynamics.extensions.kafka.ConfigConstants.INSTANCES; -import static com.appdynamics.extensions.kafka.ConfigConstants.MBEANS; -import static com.appdynamics.extensions.kafka.ConfigConstants.PASSWORD; -import static com.appdynamics.extensions.kafka.ConfigConstants.PORT; -import static com.appdynamics.extensions.kafka.ConfigConstants.SERVICE_URL; -import static com.appdynamics.extensions.kafka.ConfigConstants.USERNAME; -import static com.appdynamics.extensions.kafka.Util.convertToString; -import com.appdynamics.TaskInputArgs; -import com.appdynamics.extensions.conf.MonitorConfiguration; -import com.appdynamics.extensions.crypto.CryptoUtil; -import com.appdynamics.extensions.util.MetricWriteHelper; -import com.appdynamics.extensions.util.MetricWriteHelperFactory; -import com.google.common.base.Strings; -import com.google.common.collect.Maps; -import com.singularity.ee.agent.systemagent.api.AManagedMonitor; -import com.singularity.ee.agent.systemagent.api.TaskExecutionContext; -import com.singularity.ee.agent.systemagent.api.TaskOutput; -import com.singularity.ee.agent.systemagent.api.exception.TaskExecutionException; -import org.apache.log4j.ConsoleAppender; -import org.apache.log4j.Level; -import org.apache.log4j.Logger; -import org.apache.log4j.PatternLayout; - -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.util.HashMap; +import com.appdynamics.extensions.ABaseMonitor; +import com.appdynamics.extensions.TasksExecutionServiceProvider; +import com.appdynamics.extensions.util.AssertUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; import java.util.Map; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -public class KafkaMonitor extends AManagedMonitor { - private static final Logger logger = Logger.getLogger(KafkaMonitor.class); - private static final String CONFIG_ARG = "config-file"; - private static final String METRIC_PREFIX = "Custom Metrics|Kafka|"; +import com.appdynamics.extensions.util.AssertUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.Map; - private boolean initialized; - private MonitorConfiguration configuration; +public class KafkaMonitor extends ABaseMonitor { + private static final String DEFAULT_METRIC_PREFIX = "Custom Metrics|Kafka"; + private static final Logger logger = LoggerFactory.getLogger(KafkaMonitor.class); - public KafkaMonitor() { - System.out.println(logVersion()); - } - - public TaskOutput execute(Map taskArgs, TaskExecutionContext out) throws TaskExecutionException { - logVersion(); - if (!initialized) { - initialize(taskArgs); - } - logger.debug(String.format("The raw arguments are {}", taskArgs)); - configuration.executeTask(); - logger.info("Kafka monitor run completed successfully."); - return new TaskOutput("Kafka monitor run completed successfully."); + @Override + protected void initializeMoreStuff(Map args){ } - private void initialize(Map taskArgs) { - if (!initialized) { - //read the config. - final String configFilePath = taskArgs.get(CONFIG_ARG); - MetricWriteHelper metricWriteHelper = MetricWriteHelperFactory.create(this); - MonitorConfiguration conf = new MonitorConfiguration(METRIC_PREFIX, new TaskRunnable(), metricWriteHelper); - conf.setConfigYml(configFilePath); - conf.checkIfInitialized(MonitorConfiguration.ConfItem.CONFIG_YML, MonitorConfiguration.ConfItem.EXECUTOR_SERVICE, - MonitorConfiguration.ConfItem.METRIC_PREFIX, MonitorConfiguration.ConfItem.METRIC_WRITE_HELPER); - this.configuration = conf; - initialized = true; - } + @Override + protected String getDefaultMetricPrefix() { + return DEFAULT_METRIC_PREFIX; } - private class TaskRunnable implements Runnable { - - public void run() { - Map config = configuration.getConfigYml(); - if (config != null) { - List servers = (List) config.get(INSTANCES); - if (servers != null && !servers.isEmpty()) { - - for (Map server : servers) { - try { - KafkaMonitorTask task = createTask(server); - configuration.getExecutorService().execute(task); - } catch (IOException e) { - logger.error(String.format("Cannot construct JMX uri for {}", convertToString(server.get(DISPLAY_NAME), ""))); - } - - } - } else { - logger.error("There are no servers configured"); - } - } else { - logger.error("The config.yml is not loaded due to previous errors.The task will not run"); - } - } + @Override + public String getMonitorName() { + return "Kafka Extension"; } - private KafkaMonitorTask createTask(Map server) throws IOException { - String serviceUrl = convertToString(server.get(SERVICE_URL), ""); - String host = convertToString(server.get(HOST), ""); - String portStr = convertToString(server.get(PORT), ""); - int port = portStr != null ? Integer.parseInt(portStr) : -1; - String username = convertToString(server.get(USERNAME), ""); - String password = getPassword(server); + @Override + protected void doRun(TasksExecutionServiceProvider tasksExecutionServiceProvider) { + List> kafkaServers = (List>) this.getContextConfiguration().getConfigYml().get("servers"); + for (Map kafkaServer : kafkaServers) { - JMXConnectionAdapter adapter = JMXConnectionAdapter.create(serviceUrl, host, port, username, password); - return new KafkaMonitorTask.Builder() - .metricPrefix(configuration.getMetricPrefix()) - .metricWriter(configuration.getMetricWriter()) - .jmxConnectionAdapter(adapter) - .server(server) - .mbeans((List) configuration.getConfigYml().get(MBEANS)) - .build(); - } - - private String getPassword(Map server) { - String password = convertToString(server.get(PASSWORD), ""); - if (!Strings.isNullOrEmpty(password)) { - return password; - } - String encryptionKey = convertToString(configuration.getConfigYml().get(ConfigConstants.ENCRYPTION_KEY), ""); - String encryptedPassword = convertToString(server.get(ENCRYPTED_PASSWORD), ""); - if (!Strings.isNullOrEmpty(encryptionKey) && !Strings.isNullOrEmpty(encryptedPassword)) { - Map cryptoMap = Maps.newHashMap(); - cryptoMap.put(PASSWORD_ENCRYPTED, encryptedPassword); - cryptoMap.put(TaskInputArgs.ENCRYPTION_KEY, encryptionKey); - return CryptoUtil.getPassword(cryptoMap); + KafkaMonitorTask task = new KafkaMonitorTask(tasksExecutionServiceProvider, this.getContextConfiguration(), kafkaServer); + AssertUtils.assertNotNull(kafkaServer.get("displayName"), "The displayName can not be null"); + tasksExecutionServiceProvider.submit((String) kafkaServer.get("displayName"), task); } - return null; - } - private static String getImplementationVersion() { - return KafkaMonitor.class.getPackage().getImplementationTitle(); } - private String logVersion() { - String msg = "Using Monitor Version [" + getImplementationVersion() + "]"; - logger.info(msg); - return msg; + @Override + protected int getTaskCount() { + List> servers = (List>) getContextConfiguration().getConfigYml().get("servers"); + AssertUtils.assertNotNull(servers, "The 'servers' section in config.yml is not initialised"); + return servers.size(); } - public static void main(String[] args) throws TaskExecutionException { - - ConsoleAppender ca = new ConsoleAppender(); - ca.setWriter(new OutputStreamWriter(System.out)); - ca.setLayout(new PatternLayout("%-5p [%t]: %m%n")); - ca.setThreshold(Level.DEBUG); - logger.getRootLogger().addAppender(ca); - - final Map taskArgs = new HashMap(); - taskArgs.put(CONFIG_ARG, "/Users/Muddam/AppDynamics/Code/extensions/kafka-monitoring-extension/src/main/resources/config/config.yml"); - - final KafkaMonitor monitor = new KafkaMonitor(); - //monitor.execute(taskArgs, null); - - ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); - scheduler.scheduleAtFixedRate(new Runnable() { - public void run() { - try { - monitor.execute(taskArgs, null); - } catch (Exception e) { - logger.error("Error while running the Task ", e); - } - } - }, 2, 60, TimeUnit.SECONDS); - } } diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index 2f740db..24c0e96 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -8,103 +8,95 @@ package com.appdynamics.extensions.kafka; -import static com.appdynamics.extensions.kafka.ConfigConstants.DISPLAY_NAME; -import static com.appdynamics.extensions.kafka.Util.convertToString; +import static com.appdynamics.extensions.kafka.utils.Constants.DISPLAY_NAME; +import com.appdynamics.extensions.AMonitorTaskRunnable; +import com.appdynamics.extensions.MetricWriteHelper; +import com.appdynamics.extensions.TasksExecutionServiceProvider; +import com.appdynamics.extensions.conf.MonitorContextConfiguration; +import com.appdynamics.extensions.crypto.CryptoUtil; import com.appdynamics.extensions.kafka.metrics.DomainMetricsProcessor; -import com.appdynamics.extensions.kafka.metrics.Metric; -import com.appdynamics.extensions.kafka.metrics.MetricPrinter; -import com.appdynamics.extensions.kafka.metrics.MetricProperties; -import com.appdynamics.extensions.kafka.metrics.MetricPropertiesBuilder; -import com.appdynamics.extensions.util.MetricWriteHelper; -import com.singularity.ee.agent.systemagent.api.MetricWriter; +import com.appdynamics.extensions.metrics.Metric; +import com.appdynamics.extensions.metrics.MetricProperties; +import com.google.common.base.Strings; +import com.google.common.collect.Maps; import org.apache.log4j.Logger; +import org.slf4j.LoggerFactory; import javax.management.MalformedObjectNameException; import javax.management.remote.JMXConnector; import java.io.IOException; import java.math.BigDecimal; +import java.util.HashMap; import java.util.List; import java.util.Map; -/** - * @author Satish Muddam - */ -public class KafkaMonitorTask implements Runnable { - private static final Logger logger = Logger.getLogger(KafkaMonitorTask.class); - private static final String METRICS_COLLECTION_SUCCESSFUL = "Metrics Collection Successful"; + +public class KafkaMonitorTask implements AMonitorTaskRunnable { + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(KafkaMonitorTask.class); + private MonitorContextConfiguration configuration; + private Map kafkaServer; + private MetricWriteHelper metricWriter; + private String metricPrefix; + private String displayName; + private JMXConnectionAdapter jmxAdapter; private static final BigDecimal ERROR_VALUE = BigDecimal.ZERO; private static final BigDecimal SUCCESS_VALUE = BigDecimal.ONE; + public KafkaMonitorTask(TasksExecutionServiceProvider serviceProvider, MonitorContextConfiguration configuration, Map kafkaServer) { + this.configuration = configuration; + this.kafkaServer = kafkaServer; + this.metricPrefix = configuration.getMetricPrefix() + "|" + kafkaServer.get("displayName"); + this.metricWriter = serviceProvider.getMetricWriteHelper(); + this.displayName = (String) kafkaServer.get("displayName"); + } - private String displayName; - /* metric prefix from the config.yaml to be applied to each metric path*/ - private String metricPrefix; - - /* server properties */ - private Map server; + public void onTaskComplete() { - /* a facade to report metrics to the machine agent.*/ - private MetricWriteHelper metricWriter; + } - /* a stateless JMX adapter that abstracts out all JMX methods.*/ - private JMXConnectionAdapter jmxAdapter; + public void run() { + populateAndPrintMetrics(); + } - /* config mbeans from config.yaml. */ - private List configMBeans; + private BigDecimal populateAndPrintMetrics() { - public void run() { - displayName = convertToString(server.get(DISPLAY_NAME), ""); - long startTime = System.currentTimeMillis(); - MetricPrinter metricPrinter = new MetricPrinter(metricPrefix, displayName, metricWriter); - try { - logger.debug(String.format("Kafka monitor thread for server {} started.", displayName)); - BigDecimal status = extractAndReportMetrics(metricPrinter); - metricPrinter.printMetric(metricPrinter.formMetricPath(METRICS_COLLECTION_SUCCESSFUL), status - , MetricWriter.METRIC_AGGREGATION_TYPE_OBSERVATION, MetricWriter.METRIC_TIME_ROLLUP_TYPE_CURRENT, MetricWriter.METRIC_CLUSTER_ROLLUP_TYPE_INDIVIDUAL); + JMXConnector jmxConnection = null; + try{ + jmxAdapter = JMXConnectionAdapter.create(buildRequestMap()); } catch (Exception e) { - logger.error(String.format("Error in Kafka Monitor thread for server {}", displayName), e); - metricPrinter.printMetric(metricPrinter.formMetricPath(METRICS_COLLECTION_SUCCESSFUL), ERROR_VALUE - , MetricWriter.METRIC_AGGREGATION_TYPE_OBSERVATION, MetricWriter.METRIC_TIME_ROLLUP_TYPE_CURRENT, MetricWriter.METRIC_CLUSTER_ROLLUP_TYPE_INDIVIDUAL); - - } finally { - long endTime = System.currentTimeMillis() - startTime; - logger.debug(String.format("Kafka monitor thread for server {%s} ended. Time taken = {%s} and Total metrics reported = {%d}", displayName, endTime, metricPrinter.getTotalMetricsReported())); + logger.debug("Error in connecting to Kafka" + e); } - } - private BigDecimal extractAndReportMetrics(final MetricPrinter metricPrinter) throws Exception { - JMXConnector jmxConnection = null; - try { + try{ jmxConnection = jmxAdapter.open(); logger.debug("JMX Connection is open"); - MetricPropertiesBuilder propertyBuilder = new MetricPropertiesBuilder(); - for (Map aConfigMBean : configMBeans) { + MetricProperties metricProperties; + List> mbeansFromConfig = (List>) configuration.getConfigYml().get("mbeans"); - List mbeanNames = (List) aConfigMBean.get("mbeanFullPath"); + for (Map mbeanFromConfig : mbeansFromConfig) { + List mbeanNames = (List) mbeanFromConfig.get("objectName"); - for (String mbeanFullName : mbeanNames) { - - String configObjectName = convertToString(mbeanFullName, ""); - logger.debug(String.format("Processing mbean %s from the config file", mbeanFullName)); + for (String mbeanName : mbeanNames) { + logger.debug(String.format("Processing mbean %s from the config file", mbeanName)); try { - Map metricPropsMap = propertyBuilder.build(aConfigMBean); + DomainMetricsProcessor nodeProcessor = new DomainMetricsProcessor(jmxAdapter, jmxConnection); - List nodeMetrics = nodeProcessor.getNodeMetrics(mbeanFullName, aConfigMBean, metricPropsMap); + List nodeMetrics = nodeProcessor.getNodeMetrics(mbeanName, mbeanFromConfig); if (nodeMetrics.size() > 0) { - metricPrinter.reportNodeMetrics(nodeMetrics); + metricWriter.transformAndPrintMetrics(nodeMetrics);//review: if needs to be printed in every iteration } + } catch (MalformedObjectNameException e) { - logger.error("Illegal object name {}" + configObjectName, e); - throw e; + logger.error("Illegal object name {}" + mbeanName, e); } catch (Exception e) { - //System.out.print("" + e); - logger.error(String.format("Error fetching JMX metrics for {%s} and mbean={%s}", displayName, configObjectName), e); - throw e; + logger.error(String.format("Error fetching JMX metrics for {%s} and mbean={%s}", kafkaServer.get("name"), mbeanName), e); } } } + } catch (IOException ioe) { + logger.error("Error while opening JMX connection {}" + kafkaServer.get("name")); } finally { try { jmxAdapter.close(jmxConnection); @@ -115,40 +107,40 @@ private BigDecimal extractAndReportMetrics(final MetricPrinter metricPrinter) th } } return SUCCESS_VALUE; - } - static class Builder { - private KafkaMonitorTask task = new KafkaMonitorTask(); + } - Builder metricPrefix(String metricPrefix) { - task.metricPrefix = metricPrefix; - return this; - } + private Map buildRequestMap() { + Map requestMap = new HashMap(); + requestMap.put("host", kafkaServer.get("host")); + requestMap.put("port", kafkaServer.get("port")); + requestMap.put("username", kafkaServer.get("username")); + requestMap.put("displayName", kafkaServer.get("displayName")); + requestMap.put("password", getPassword(kafkaServer)); - Builder metricWriter(MetricWriteHelper metricWriter) { - task.metricWriter = metricWriter; - return this; - } + return requestMap; + } - Builder server(Map server) { - task.server = server; - return this; + private String getPassword(Map server) { + String password = server.get("password"); + String encryptedPassword = server.get("encryptedPassword"); + Map configMap = configuration.getConfigYml(); + String encryptionKey = configMap.get("encryptionKey").toString(); + if(!Strings.isNullOrEmpty(password)){ + return password; } - - Builder jmxConnectionAdapter(JMXConnectionAdapter adapter) { - task.jmxAdapter = adapter; - return this; + if(!Strings.isNullOrEmpty(encryptedPassword) && !Strings.isNullOrEmpty(encryptionKey)){ + Map cryptoMap = Maps.newHashMap(); + cryptoMap.put("password-encrypted", encryptedPassword); + cryptoMap.put("encryption-key", encryptionKey); + logger.debug("Decrypting the ecncrypted password........"); + return CryptoUtil.getPassword(cryptoMap); } + return ""; + } - Builder mbeans(List mBeans) { - task.configMBeans = mBeans; - return this; - } - KafkaMonitorTask build() { - return task; - } - } } + diff --git a/src/main/java/com/appdynamics/extensions/kafka/Util.java b/src/main/java/com/appdynamics/extensions/kafka/Util.java deleted file mode 100644 index bf0496f..0000000 --- a/src/main/java/com/appdynamics/extensions/kafka/Util.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2018. AppDynamics LLC and its affiliates. - * All Rights Reserved. - * This is unpublished proprietary source code of AppDynamics LLC and its affiliates. - * The copyright notice above does not evidence any actual or intended publication of such source code. - * - */ - -package com.appdynamics.extensions.kafka; - - -import java.math.BigDecimal; -import java.math.RoundingMode; - -public class Util { - - public static String convertToString(final Object field,final String defaultStr){ - if(field == null){ - return defaultStr; - } - return field.toString(); - } - - public static String[] split(final String metricType,final String splitOn) { - return metricType.split(splitOn); - } - - public static String toBigIntString(final BigDecimal bigD) { - return bigD.setScale(0, RoundingMode.HALF_UP).toBigInteger().toString(); - } -} diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DefaultMetricProperties.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DefaultMetricProperties.java deleted file mode 100644 index 6a83af8..0000000 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DefaultMetricProperties.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2018. AppDynamics LLC and its affiliates. - * All Rights Reserved. - * This is unpublished proprietary source code of AppDynamics LLC and its affiliates. - * The copyright notice above does not evidence any actual or intended publication of such source code. - * - */ - -package com.appdynamics.extensions.kafka.metrics; - -import com.singularity.ee.agent.systemagent.api.MetricWriter; - -public class DefaultMetricProperties extends MetricProperties{ - - private static final String DEFAULT_METRIC_TYPE = MetricWriter.METRIC_AGGREGATION_TYPE_AVERAGE + " " + MetricWriter.METRIC_TIME_ROLLUP_TYPE_AVERAGE + " " + MetricWriter.METRIC_CLUSTER_ROLLUP_TYPE_INDIVIDUAL; - private static final boolean DEFAULT_AGGREGATION = false; - private static final boolean DEFAULT_DELTA = false; - - public DefaultMetricProperties(){ - setAggregationFields(DEFAULT_METRIC_TYPE); - setMultiplier(DEFAULT_MULTIPLIER); - setAggregation(DEFAULT_AGGREGATION); - setDelta(DEFAULT_DELTA); - } - -} diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index 20efc83..2feb64c 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -8,14 +8,15 @@ package com.appdynamics.extensions.kafka.metrics; -import static com.appdynamics.extensions.kafka.ConfigConstants.EXCLUDE; -import static com.appdynamics.extensions.kafka.ConfigConstants.INCLUDE; -import static com.appdynamics.extensions.kafka.ConfigConstants.METRICS; -import static com.appdynamics.extensions.kafka.Util.convertToString; +import static com.appdynamics.extensions.kafka.utils.Constants.EXCLUDE; +import static com.appdynamics.extensions.kafka.utils.Constants.INCLUDE; +import static com.appdynamics.extensions.kafka.utils.Constants.METRICS; import com.appdynamics.extensions.kafka.JMXConnectionAdapter; import com.appdynamics.extensions.kafka.filters.ExcludeFilter; import com.appdynamics.extensions.kafka.filters.IncludeFilter; +import com.appdynamics.extensions.kafka.utils.Constants; +import com.appdynamics.extensions.metrics.Metric; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Sets; @@ -28,6 +29,8 @@ import javax.management.ObjectInstance; import javax.management.ObjectName; import javax.management.ReflectionException; +import javax.management.openmbean.CompositeData; +import javax.management.openmbean.CompositeDataSupport; import javax.management.remote.JMXConnector; import java.io.IOException; import java.math.BigDecimal; @@ -40,27 +43,22 @@ public class DomainMetricsProcessor { static final org.slf4j.Logger logger = LoggerFactory.getLogger(DomainMetricsProcessor.class); - private final JMXConnectionAdapter jmxAdapter; private final JMXConnector jmxConnection; - - private final MetricValueTransformer valueConverter = new MetricValueTransformer(); - public DomainMetricsProcessor(JMXConnectionAdapter jmxAdapter, JMXConnector jmxConnection) { this.jmxAdapter = jmxAdapter; this.jmxConnection = jmxConnection; } - public List getNodeMetrics(String objectName, Map aConfigMBean, Map metricPropsMap) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { + public List getNodeMetrics(String objectName, Map aConfigMBean) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { List nodeMetrics = Lists.newArrayList(); - String configObjectName = convertToString(objectName, ""); - Set objectInstances = jmxAdapter.queryMBeans(jmxConnection, ObjectName.getInstance(configObjectName)); + Set objectInstances = jmxAdapter.queryMBeans(jmxConnection, ObjectName.getInstance(objectName)); for (ObjectInstance instance : objectInstances) { List metricNamesDictionary = jmxAdapter.getReadableAttributeNames(jmxConnection, instance); List metricNamesToBeExtracted = applyFilters(aConfigMBean, metricNamesDictionary); List attributes = jmxAdapter.getAttributes(jmxConnection, instance.getObjectName(), metricNamesToBeExtracted.toArray(new String[metricNamesToBeExtracted.size()])); - collect(nodeMetrics, attributes, instance, metricPropsMap); + collect(nodeMetrics, attributes, instance); } return nodeMetrics; } @@ -68,75 +66,39 @@ public List getNodeMetrics(String objectName, Map aConfigMBean, Map applyFilters(Map aConfigMBean, List metricNamesDictionary) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException { Set filteredSet = Sets.newHashSet(); Map configMetrics = (Map) aConfigMBean.get(METRICS); - List includeDictionary = (List) configMetrics.get(INCLUDE); - List excludeDictionary = (List) configMetrics.get(EXCLUDE); + List includeDictionary = (List) configMetrics.get(Constants.INCLUDE); + List excludeDictionary = (List) configMetrics.get(Constants.EXCLUDE); new ExcludeFilter(excludeDictionary).apply(filteredSet, metricNamesDictionary); new IncludeFilter(includeDictionary).apply(filteredSet, metricNamesDictionary); return Lists.newArrayList(filteredSet); } - private void collect(List nodeMetrics, List attributes, ObjectInstance instance, Map metricPropsPerMetricName) { - for (Attribute attr : attributes) { + + private void collect(List nodeMetrics, List attributes, ObjectInstance instance) { + for (Attribute attribute : attributes) { try { - String attrName = attr.getName(); - MetricProperties props = metricPropsPerMetricName.get(attrName); - if (props == null) { - logger.error("Could not find metric props for {}", attrName); - continue; + String attrName = attribute.getName(); + if(isCompositeDataObject(attribute)){ + Set attributesFound = ((CompositeData)attribute.getValue()).getCompositeType().keySet(); + for(String str: attributesFound){ + String key = attribute.getName()+ "."+ str; + Object attributeValue = ((CompositeDataSupport) attribute.getValue()).get(str); +// setMetricDetails(metricPrefix, key, attributeValue, instance, metricPropsPerMetricName, nodeMetrics); + + } } - //get metric value by applying conversions if necessary - +// else +// setMetricDetails(metricPrefix, attribute.getName(), attribute.getValue(), instance, metricPropsPerMetricName, nodeMetrics); - BigDecimal metricValue = valueConverter.transform(attrName, attr.getValue(), props); - if (metricValue != null) { - Metric nodeMetric = new Metric(); - nodeMetric.setMetricName(attrName); - String metricName = nodeMetric.getMetricNameOrAlias(); - nodeMetric.setProperties(props); - String path = buildName(instance); - - nodeMetric.setMetricKey(path + metricName); - nodeMetric.setMetricValue(metricValue); - nodeMetrics.add(nodeMetric); - } } catch (Exception e) { - logger.error("Error collecting value for {} {}", instance.getObjectName(), attr.getName(), e); + logger.error("Error collecting value for {} {}", instance.getObjectName(), attribute.getName(), e); } } } - private String buildName(ObjectInstance instance) { - - ObjectName objectName = instance.getObjectName(); - Hashtable keyPropertyList = objectName.getKeyPropertyList(); - - StringBuilder sb = new StringBuilder(); - - sb.append(objectName.getDomain()); - - String type = keyPropertyList.get("type"); - String name = keyPropertyList.get("name"); - - if(!Strings.isNullOrEmpty(type)) { - sb.append("|"); - sb.append(type); - } - - if(!Strings.isNullOrEmpty(name)) { - sb.append("|"); - sb.append(name); - } - - sb.append("|"); - - keyPropertyList.remove("type"); - keyPropertyList.remove("name"); - - for (Map.Entry entry : keyPropertyList.entrySet()) { - sb.append(entry.getKey()).append("|").append(entry.getValue()).append("|"); - } - return sb.toString(); + private boolean isCompositeDataObject(Attribute attribute){ + return attribute.getValue().getClass().equals(CompositeDataSupport.class); } } diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/Metric.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/Metric.java deleted file mode 100644 index 4849f37..0000000 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/Metric.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2018. AppDynamics LLC and its affiliates. - * All Rights Reserved. - * This is unpublished proprietary source code of AppDynamics LLC and its affiliates. - * The copyright notice above does not evidence any actual or intended publication of such source code. - * - */ - -package com.appdynamics.extensions.kafka.metrics; - -import com.google.common.base.Strings; - -import java.math.BigDecimal; - -public class Metric { - private String metricName; - private String metricKey; - private BigDecimal metricValue; - private MetricProperties properties; - - public String getMetricNameOrAlias() { - if(properties == null || Strings.isNullOrEmpty(properties.getAlias())){ - return metricName; - } - return properties.getAlias(); - } - - public void setMetricName(String metricName) { - this.metricName = metricName; - } - - public String getMetricKey() { - return metricKey; - } - - public void setMetricKey(String metricKey) { - this.metricKey = metricKey; - } - - public BigDecimal getMetricValue() { - return metricValue; - } - - public void setMetricValue(BigDecimal metricValue) { - this.metricValue = metricValue; - } - - public MetricProperties getProperties() { - return properties; - } - - public void setProperties(MetricProperties properties) { - this.properties = properties; - } - -} diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/MetricPrinter.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/MetricPrinter.java deleted file mode 100644 index 848dff0..0000000 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/MetricPrinter.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2018. AppDynamics LLC and its affiliates. - * All Rights Reserved. - * This is unpublished proprietary source code of AppDynamics LLC and its affiliates. - * The copyright notice above does not evidence any actual or intended publication of such source code. - * - */ - -package com.appdynamics.extensions.kafka.metrics; - - -import static com.appdynamics.extensions.kafka.Util.toBigIntString; - -import com.appdynamics.extensions.util.MetricWriteHelper; -import com.google.common.base.Strings; -import org.slf4j.LoggerFactory; - -import java.math.BigDecimal; -import java.util.List; - -public class MetricPrinter { - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(MetricPrinter.class); - - private int totalMetricsReported; - private String metricPrefix; - private String displayName; - private MetricWriteHelper metricWriter; - - public MetricPrinter(String metricPrefix, String displayName, MetricWriteHelper metricWriter) { - this.metricPrefix = metricPrefix; - this.displayName = displayName; - this.metricWriter = metricWriter; - } - - public void reportNodeMetrics(final List componentMetrics) { - if (componentMetrics == null || componentMetrics.isEmpty()) { - return; - } - for (Metric metric : componentMetrics) { - MetricProperties props = metric.getProperties(); - String fullMetricPath = formMetricPath(metric.getMetricKey()); - printMetric(fullMetricPath, metric.getMetricValue(), props.getAggregationType(), props.getTimeRollupType(), props.getClusterRollupType()); - } - } - - public void printMetric(String metricPath, BigDecimal metricValue, String aggType, String timeRollupType, String clusterRollupType) { - try { - String metricValStr = toBigIntString(metricValue); - if (metricValStr != null) { - metricWriter.printMetric(metricPath, metricValStr, aggType, timeRollupType, clusterRollupType); - logger.debug("Sending [{}|{}|{}] metric= {},value={}", aggType, timeRollupType, clusterRollupType, metricPath, metricValStr); - totalMetricsReported++; - } - } catch (Exception e) { - logger.error("Error reporting metric {} with value {}", metricPath, metricValue, e); - } - } - - public String formMetricPath(String metricKey) { - if (!Strings.isNullOrEmpty(displayName)) { - return metricPrefix + "|" + displayName + "|" + metricKey; - } - return metricPrefix + "|" + metricKey; - } - - public int getTotalMetricsReported() { - return totalMetricsReported; - } -} diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/MetricProperties.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/MetricProperties.java deleted file mode 100644 index fe4ad22..0000000 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/MetricProperties.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2018. AppDynamics LLC and its affiliates. - * All Rights Reserved. - * This is unpublished proprietary source code of AppDynamics LLC and its affiliates. - * The copyright notice above does not evidence any actual or intended publication of such source code. - * - */ - -package com.appdynamics.extensions.kafka.metrics; - - -import static com.appdynamics.extensions.kafka.Util.split; - -import java.util.Map; - - -public class MetricProperties { - static final double DEFAULT_MULTIPLIER = 1d; - private String alias; - private String metricName; - private String aggregationType; - private String timeRollupType; - private String clusterRollupType; - private double multiplier = DEFAULT_MULTIPLIER; - private boolean aggregation; - private boolean delta; - private Map conversionValues; - - public String getAlias() { - return alias; - } - - public void setAlias(String alias) { - this.alias = alias; - } - - public String getMetricName() { - return metricName; - } - - public void setMetricName(String metricName) { - this.metricName = metricName; - } - - public String getAggregationType() { - return aggregationType; - } - - public void setAggregationType(String aggregationType) { - this.aggregationType = aggregationType; - } - - public String getTimeRollupType() { - return timeRollupType; - } - - public void setTimeRollupType(String timeRollupType) { - this.timeRollupType = timeRollupType; - } - - public String getClusterRollupType() { - return clusterRollupType; - } - - public void setClusterRollupType(String clusterRollupType) { - this.clusterRollupType = clusterRollupType; - } - - public double getMultiplier() { - return multiplier; - } - - public void setMultiplier(double multiplier) { - this.multiplier = multiplier; - } - - public boolean isAggregation() { - return aggregation; - } - - public void setAggregation(boolean aggregation) { - this.aggregation = aggregation; - } - - public Map getConversionValues() { - return conversionValues; - } - - public void setConversionValues(Map conversionValues) { - this.conversionValues = conversionValues; - } - - public boolean isDelta() { - return delta; - } - - public void setDelta(boolean delta) { - this.delta = delta; - } - - public void setAggregationFields(String metricType) { - String[] metricTypes = split(metricType, " "); - this.setAggregationType(metricTypes[0]); - this.setTimeRollupType(metricTypes[1]); - this.setClusterRollupType(metricTypes[2]); - } -} diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/MetricPropertiesBuilder.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/MetricPropertiesBuilder.java deleted file mode 100644 index 624011d..0000000 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/MetricPropertiesBuilder.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2018. AppDynamics LLC and its affiliates. - * All Rights Reserved. - * This is unpublished proprietary source code of AppDynamics LLC and its affiliates. - * The copyright notice above does not evidence any actual or intended publication of such source code. - * - */ - -package com.appdynamics.extensions.kafka.metrics; - - -import static com.appdynamics.extensions.kafka.ConfigConstants.INCLUDE; -import static com.appdynamics.extensions.kafka.ConfigConstants.METRICS; - -import com.google.common.collect.Maps; - -import java.util.List; -import java.util.Map; - - -public class MetricPropertiesBuilder { - - public Map build(Map aConfigMBean) { - Map metricPropsMap = Maps.newHashMap(); - if (aConfigMBean == null || aConfigMBean.isEmpty()) { - return metricPropsMap; - } - Map configMetrics = (Map) aConfigMBean.get(METRICS); - List includeMetrics = (List) configMetrics.get(INCLUDE); - if (includeMetrics != null) { - for (Object metad : includeMetrics) { - Map localMetaData = (Map) metad; - Map.Entry entry = (Map.Entry) localMetaData.entrySet().iterator().next(); - String metricName = entry.getKey().toString(); - String alias = entry.getValue().toString(); - MetricProperties props = new DefaultMetricProperties(); - props.setAlias(alias); - props.setMetricName(metricName); - setProps(aConfigMBean, props); //global level - setProps(localMetaData, props); //local level - metricPropsMap.put(metricName, props); - } - } - return metricPropsMap; - } - - private void setProps(Map metadata, MetricProperties props) { - if (metadata.get("metricType") != null) { - props.setAggregationFields(metadata.get("metricType").toString()); - } - if (metadata.get("multiplier") != null) { - props.setMultiplier(Double.parseDouble(metadata.get("multiplier").toString())); - } - if (metadata.get("convert") != null) { - props.setConversionValues((Map) metadata.get("convert")); - } - if (metadata.get("aggregation") != null) { - props.setAggregation(Boolean.parseBoolean(metadata.get("aggregation").toString())); - } - if (metadata.get("delta") != null) { - props.setDelta(Boolean.parseBoolean(metadata.get("delta").toString())); - } - } - -} diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/MetricValueTransformer.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/MetricValueTransformer.java deleted file mode 100644 index c9bbb80..0000000 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/MetricValueTransformer.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2018. AppDynamics LLC and its affiliates. - * All Rights Reserved. - * This is unpublished proprietary source code of AppDynamics LLC and its affiliates. - * The copyright notice above does not evidence any actual or intended publication of such source code. - * - */ - -package com.appdynamics.extensions.kafka.metrics; - - -import com.appdynamics.extensions.util.DeltaMetricsCalculator; -import org.slf4j.LoggerFactory; - -import java.math.BigDecimal; - -class MetricValueTransformer { - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(MetricValueTransformer.class); - - private final DeltaMetricsCalculator deltaCalculator = new DeltaMetricsCalculator(10); - - BigDecimal transform(String metricPath,Object metricValue,MetricProperties props){ - if(metricValue == null){ - logger.error("Metric value for {} is null",metricPath); - throw new IllegalArgumentException("Metric value cannot be null"); - } - Object convertedValue = applyConvert(metricPath,metricValue,props); - BigDecimal val = applyMultiplier(metricPath,convertedValue,props); - BigDecimal deltaValue = applyDelta(metricPath,val,props); - return deltaValue; - } - - private BigDecimal applyDelta(String metricPath, BigDecimal val,MetricProperties props) { - if(props.isDelta()){ - return deltaCalculator.calculateDelta(metricPath,val); - } - return val; - } - - private BigDecimal applyMultiplier(String metricName, Object metricValue, MetricProperties props) { - try { - BigDecimal bigD = new BigDecimal(metricValue.toString()); - double multiplier = props.getMultiplier(); - bigD = bigD.multiply(new BigDecimal(multiplier)); - return bigD; - } - catch(NumberFormatException nfe){ - logger.error("Cannot convert into BigDecimal {} value for metric {}.",metricValue,metricName,nfe); - } - throw new IllegalArgumentException("Cannot convert into BigInteger " + metricValue); - } - - private Object applyConvert(String metricName,Object metricValue,MetricProperties props){ - //get converted values if configured - if(props.getConversionValues() != null && !props.getConversionValues().isEmpty()) { - Object convertedValue = props.getConversionValues().get(metricValue); - if (convertedValue != null) { - logger.debug("Applied conversion on {} and replaced value {} with {}", metricName, metricValue, convertedValue); - return convertedValue; - } - else{ - - if(props.getConversionValues().get("$default") != null){ - logger.debug("Choosing the $default value to go with {} for conversion",metricValue); - return props.getConversionValues().get("$default"); - } - } - } - return metricValue; - } -} - diff --git a/src/main/java/com/appdynamics/extensions/kafka/ConfigConstants.java b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java similarity index 72% rename from src/main/java/com/appdynamics/extensions/kafka/ConfigConstants.java rename to src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java index 0a11bce..e98c5ef 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/ConfigConstants.java +++ b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java @@ -1,16 +1,11 @@ -/* - * Copyright 2018. AppDynamics LLC and its affiliates. - * All Rights Reserved. - * This is unpublished proprietary source code of AppDynamics LLC and its affiliates. - * The copyright notice above does not evidence any actual or intended publication of such source code. - * - */ +package com.appdynamics.extensions.kafka.utils; -package com.appdynamics.extensions.kafka; +import java.math.BigDecimal; +public class Constants { + public static final String DEFAULT_METRIC_PREFIX = "Custom Metrics|Kafka|"; -public class ConfigConstants { - + public static final String METRIC_SEPARATOR = "|"; public static final String INCLUDE = "include"; @@ -49,5 +44,4 @@ public class ConfigConstants { public static final String METRICS = "metrics"; - } diff --git a/src/main/resources/config/config.yml b/src/main/resources/config/config.yml index 80a9f85..6ac59db 100755 --- a/src/main/resources/config/config.yml +++ b/src/main/resources/config/config.yml @@ -10,7 +10,7 @@ metricPrefix: Custom Metrics|Kafka # List of Kafka Instances instances: - host: "localhost" - port: 9999 + port: "9999" username: password: #encryptedPassword: From cc79fc6adfc9dfdb5f41f0cbb764df397fa6da70 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Sun, 1 Jul 2018 22:14:55 -0700 Subject: [PATCH 02/49] updated DomainMetricprocessor & threads structure --- .idea/compiler.xml | 8 +- .idea/encodings.xml | 4 + .idea/modules.xml | 4 + pom.xml | 4 +- .../kafka/JMXConnectionAdapter.java | 6 - .../extensions/kafka/KafkaMonitorTask.java | 63 ++++------ .../kafka/filters/ExcludeFilter.java | 33 ------ .../kafka/filters/IncludeFilter.java | 38 ------ .../kafka/metrics/DomainMetricsProcessor.java | 111 +++++++++++------- src/main/resources/config/config.yml | 16 ++- src/main/resources/config/monitor.xml | 2 +- 11 files changed, 125 insertions(+), 164 deletions(-) delete mode 100644 src/main/java/com/appdynamics/extensions/kafka/filters/ExcludeFilter.java delete mode 100644 src/main/java/com/appdynamics/extensions/kafka/filters/IncludeFilter.java diff --git a/.idea/compiler.xml b/.idea/compiler.xml index cac0afc..3e2c1d7 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -6,13 +6,19 @@ + + + + + - + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml index b26911b..56c40e7 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,6 +1,10 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index acca38c..047ecaa 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,11 @@ + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 955642c..163c05e 100644 --- a/pom.xml +++ b/pom.xml @@ -68,8 +68,8 @@ maven-compiler-plugin 2.3.2 - 1.5 - 1.5 + 7 + 7 diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index 0d4e87b..ac69066 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -37,12 +37,6 @@ public class JMXConnectionAdapter { private final String username; private final String password; - private JMXConnectionAdapter(String host, int port, String username, String password) throws MalformedURLException { - this.serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + host + ":" + port + "/jmxrmi"); - this.username = username; - this.password = password; - } - private JMXConnectionAdapter(Map requestMap) throws MalformedURLException { this.serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + requestMap.get("host") + ":" + requestMap.get("port") + "/jmxrmi"); this.username = requestMap.get("username"); diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index 24c0e96..a879b8e 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -8,46 +8,43 @@ package com.appdynamics.extensions.kafka; -import static com.appdynamics.extensions.kafka.utils.Constants.DISPLAY_NAME; - import com.appdynamics.extensions.AMonitorTaskRunnable; import com.appdynamics.extensions.MetricWriteHelper; import com.appdynamics.extensions.TasksExecutionServiceProvider; import com.appdynamics.extensions.conf.MonitorContextConfiguration; import com.appdynamics.extensions.crypto.CryptoUtil; import com.appdynamics.extensions.kafka.metrics.DomainMetricsProcessor; -import com.appdynamics.extensions.metrics.Metric; -import com.appdynamics.extensions.metrics.MetricProperties; + import com.google.common.base.Strings; import com.google.common.collect.Maps; -import org.apache.log4j.Logger; import org.slf4j.LoggerFactory; - -import javax.management.MalformedObjectNameException; import javax.management.remote.JMXConnector; import java.io.IOException; import java.math.BigDecimal; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.Phaser; + public class KafkaMonitorTask implements AMonitorTaskRunnable { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(KafkaMonitorTask.class); private MonitorContextConfiguration configuration; private Map kafkaServer; - private MetricWriteHelper metricWriter; + private MetricWriteHelper metricWriteHelper; private String metricPrefix; private String displayName; private JMXConnectionAdapter jmxAdapter; private static final BigDecimal ERROR_VALUE = BigDecimal.ZERO; private static final BigDecimal SUCCESS_VALUE = BigDecimal.ONE; + private Phaser phaser; public KafkaMonitorTask(TasksExecutionServiceProvider serviceProvider, MonitorContextConfiguration configuration, Map kafkaServer) { this.configuration = configuration; this.kafkaServer = kafkaServer; this.metricPrefix = configuration.getMetricPrefix() + "|" + kafkaServer.get("displayName"); - this.metricWriter = serviceProvider.getMetricWriteHelper(); + this.metricWriteHelper = serviceProvider.getMetricWriteHelper(); this.displayName = (String) kafkaServer.get("displayName"); } @@ -56,47 +53,33 @@ public void onTaskComplete() { } public void run() { + phaser = new Phaser(); populateAndPrintMetrics(); + + phaser.arriveAndAwaitAdvance(); + logger.info("Completed the Kafka Monitoring task"); } private BigDecimal populateAndPrintMetrics() { - JMXConnector jmxConnection = null; try{ jmxAdapter = JMXConnectionAdapter.create(buildRequestMap()); - } catch (Exception e) { + }catch (Exception e) { logger.debug("Error in connecting to Kafka" + e); - } - + } try{ jmxConnection = jmxAdapter.open(); logger.debug("JMX Connection is open"); - MetricProperties metricProperties; - List> mbeansFromConfig = (List>) configuration.getConfigYml().get("mbeans"); - + List> mbeansFromConfig = (List>) configuration.getConfigYml().get("mbeans"); for (Map mbeanFromConfig : mbeansFromConfig) { - List mbeanNames = (List) mbeanFromConfig.get("objectName"); - - for (String mbeanName : mbeanNames) { - logger.debug(String.format("Processing mbean %s from the config file", mbeanName)); - try { - DomainMetricsProcessor nodeProcessor = new DomainMetricsProcessor(jmxAdapter, jmxConnection); - List nodeMetrics = nodeProcessor.getNodeMetrics(mbeanName, mbeanFromConfig); - if (nodeMetrics.size() > 0) { - metricWriter.transformAndPrintMetrics(nodeMetrics);//review: if needs to be printed in every iteration - } - - - } catch (MalformedObjectNameException e) { - logger.error("Illegal object name {}" + mbeanName, e); - } catch (Exception e) { - logger.error(String.format("Error fetching JMX metrics for {%s} and mbean={%s}", kafkaServer.get("name"), mbeanName), e); - } - } + DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( jmxAdapter, jmxConnection, mbeanFromConfig, displayName,metricWriteHelper, metricPrefix, phaser); + configuration.getContext().getExecutorService().execute("DomainMetricsProcessor",domainMetricsProcessor); + logger.debug("Registering phaser for " + displayName); } - } catch (IOException ioe) { - logger.error("Error while opening JMX connection {}" + kafkaServer.get("name")); + + } catch (Exception e) { + logger.error("Error while opening JMX connection {}{}" + kafkaServer.get("name"), e); } finally { try { jmxAdapter.close(jmxConnection); @@ -107,10 +90,9 @@ private BigDecimal populateAndPrintMetrics() { } } return SUCCESS_VALUE; - - } + private Map buildRequestMap() { Map requestMap = new HashMap(); requestMap.put("host", kafkaServer.get("host")); @@ -118,7 +100,6 @@ private Map buildRequestMap() { requestMap.put("username", kafkaServer.get("username")); requestMap.put("displayName", kafkaServer.get("displayName")); requestMap.put("password", getPassword(kafkaServer)); - return requestMap; } @@ -141,6 +122,10 @@ private String getPassword(Map server) { } + + + + } diff --git a/src/main/java/com/appdynamics/extensions/kafka/filters/ExcludeFilter.java b/src/main/java/com/appdynamics/extensions/kafka/filters/ExcludeFilter.java deleted file mode 100644 index 00cc052..0000000 --- a/src/main/java/com/appdynamics/extensions/kafka/filters/ExcludeFilter.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2018. AppDynamics LLC and its affiliates. - * All Rights Reserved. - * This is unpublished proprietary source code of AppDynamics LLC and its affiliates. - * The copyright notice above does not evidence any actual or intended publication of such source code. - * - */ - -package com.appdynamics.extensions.kafka.filters; - - -import java.util.List; -import java.util.Set; - -public class ExcludeFilter { - - private List dictionary; - - public ExcludeFilter(List excludeDictionary) { - this.dictionary = excludeDictionary; - } - - public void apply(Set filteredSet, List allMetrics){ - if(allMetrics == null || dictionary == null){ - return; - } - for(String metric : allMetrics){ - if(!dictionary.contains(metric)){ - filteredSet.add(metric); - } - } - } -} diff --git a/src/main/java/com/appdynamics/extensions/kafka/filters/IncludeFilter.java b/src/main/java/com/appdynamics/extensions/kafka/filters/IncludeFilter.java deleted file mode 100644 index 1651f75..0000000 --- a/src/main/java/com/appdynamics/extensions/kafka/filters/IncludeFilter.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2018. AppDynamics LLC and its affiliates. - * All Rights Reserved. - * This is unpublished proprietary source code of AppDynamics LLC and its affiliates. - * The copyright notice above does not evidence any actual or intended publication of such source code. - * - */ - -package com.appdynamics.extensions.kafka.filters; - - -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class IncludeFilter { - - private List dictionary; - - public IncludeFilter(List includeDictionary) { - this.dictionary = includeDictionary; - } - - public void apply(Set filteredSet, List allMetrics){ - if(allMetrics == null || dictionary == null){ - return; - } - for(Object inc : dictionary){ - Map metric = (Map) inc; - //Get the First Entry which is the metric - Map.Entry firstEntry = (Map.Entry) metric.entrySet().iterator().next(); - String metricName = firstEntry.getKey().toString(); - if(allMetrics.contains(metricName)) { - filteredSet.add(metricName); //to get jmx metrics - } - } - } -} diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index 2feb64c..8e7608a 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -8,87 +8,109 @@ package com.appdynamics.extensions.kafka.metrics; -import static com.appdynamics.extensions.kafka.utils.Constants.EXCLUDE; -import static com.appdynamics.extensions.kafka.utils.Constants.INCLUDE; -import static com.appdynamics.extensions.kafka.utils.Constants.METRICS; +import com.appdynamics.extensions.MetricWriteHelper; +import com.appdynamics.extensions.conf.MonitorContext; import com.appdynamics.extensions.kafka.JMXConnectionAdapter; -import com.appdynamics.extensions.kafka.filters.ExcludeFilter; -import com.appdynamics.extensions.kafka.filters.IncludeFilter; -import com.appdynamics.extensions.kafka.utils.Constants; import com.appdynamics.extensions.metrics.Metric; -import com.google.common.base.Strings; + import com.google.common.collect.Lists; -import com.google.common.collect.Sets; + import org.slf4j.LoggerFactory; -import javax.management.Attribute; -import javax.management.InstanceNotFoundException; -import javax.management.IntrospectionException; -import javax.management.MalformedObjectNameException; -import javax.management.ObjectInstance; -import javax.management.ObjectName; -import javax.management.ReflectionException; +import javax.management.*; import javax.management.openmbean.CompositeData; import javax.management.openmbean.CompositeDataSupport; import javax.management.remote.JMXConnector; import java.io.IOException; -import java.math.BigDecimal; -import java.util.Hashtable; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.Phaser; - -public class DomainMetricsProcessor { - +public class DomainMetricsProcessor implements Runnable{ static final org.slf4j.Logger logger = LoggerFactory.getLogger(DomainMetricsProcessor.class); private final JMXConnectionAdapter jmxAdapter; private final JMXConnector jmxConnection; - - public DomainMetricsProcessor(JMXConnectionAdapter jmxAdapter, JMXConnector jmxConnection) { + private MonitorContext context; + private MetricWriteHelper metricWriteHelper; + private List metrics = new ArrayList(); + private String metricPrefix; + private Phaser phaser; + private Map mbeanFromConfig; + private String displayName; + + public DomainMetricsProcessor( JMXConnectionAdapter jmxAdapter, JMXConnector jmxConnection, Map mbeanFromConfig, String displayName, MetricWriteHelper metricWriteHelper, String metricPrefix, Phaser phaser) { this.jmxAdapter = jmxAdapter; this.jmxConnection = jmxConnection; + this.metricWriteHelper = metricWriteHelper; + this.metricPrefix = metricPrefix; + this.phaser = phaser; + this.phaser.register(); + this.mbeanFromConfig = mbeanFromConfig; + this.displayName = displayName; } - public List getNodeMetrics(String objectName, Map aConfigMBean) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { + @Override + public void run() { + try { + + logger.debug(String.format("Processing metricProps from the config file")); + Map metricProperties = (Map) this.mbeanFromConfig.get("metrics"); + + + List mbeanNames = (List) this.mbeanFromConfig.get("mbeanFullPath"); + for (String mbeanName : mbeanNames) { + + logger.debug(String.format("Processing mbean %s from the config file", mbeanName)); + + getNodeMetrics(jmxConnection,mbeanName,metricProperties); + logger.debug("Registering phaser for " + displayName); + } + + + + //printmetric and heartbeat + + } catch (IntrospectionException | IOException | MalformedObjectNameException | InstanceNotFoundException | ReflectionException e) { + e.printStackTrace(); + } finally { + logger.debug("DomainProcessor Phaser arrived for {}", displayName); + phaser.arriveAndDeregister(); + } + + } + + private List getNodeMetrics(JMXConnector jmxConnection, String objectName,Map metricProperties) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { List nodeMetrics = Lists.newArrayList(); Set objectInstances = jmxAdapter.queryMBeans(jmxConnection, ObjectName.getInstance(objectName)); for (ObjectInstance instance : objectInstances) { List metricNamesDictionary = jmxAdapter.getReadableAttributeNames(jmxConnection, instance); - List metricNamesToBeExtracted = applyFilters(aConfigMBean, metricNamesDictionary); - List attributes = jmxAdapter.getAttributes(jmxConnection, instance.getObjectName(), metricNamesToBeExtracted.toArray(new String[metricNamesToBeExtracted.size()])); - collect(nodeMetrics, attributes, instance); + List attributes = jmxAdapter.getAttributes(jmxConnection, instance.getObjectName(), metricNamesDictionary.toArray(new String[metricNamesDictionary.size()])); + collect(nodeMetrics, attributes, instance,metricProperties); } return nodeMetrics; } - private List applyFilters(Map aConfigMBean, List metricNamesDictionary) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException { - Set filteredSet = Sets.newHashSet(); - Map configMetrics = (Map) aConfigMBean.get(METRICS); - List includeDictionary = (List) configMetrics.get(Constants.INCLUDE); - List excludeDictionary = (List) configMetrics.get(Constants.EXCLUDE); - new ExcludeFilter(excludeDictionary).apply(filteredSet, metricNamesDictionary); - new IncludeFilter(includeDictionary).apply(filteredSet, metricNamesDictionary); - return Lists.newArrayList(filteredSet); - } - - private void collect(List nodeMetrics, List attributes, ObjectInstance instance) { + private void collect(List nodeMetrics, List attributes, ObjectInstance instance, Map metricProperties) { for (Attribute attribute : attributes) { try { - String attrName = attribute.getName(); + if(isCompositeDataObject(attribute)){ Set attributesFound = ((CompositeData)attribute.getValue()).getCompositeType().keySet(); for(String str: attributesFound){ String key = attribute.getName()+ "."+ str; Object attributeValue = ((CompositeDataSupport) attribute.getValue()).get(str); -// setMetricDetails(metricPrefix, key, attributeValue, instance, metricPropsPerMetricName, nodeMetrics); + if(metricProperties.containsKey(key)){ +// setMetricDetails(metricPrefix, key, attributeValue, instance, metricProperties, nodeMetrics); + } } } // else -// setMetricDetails(metricPrefix, attribute.getName(), attribute.getValue(), instance, metricPropsPerMetricName, nodeMetrics); +// setMetricDetails(metricPrefix, attribute.getName(), attribute.getValue(), instance, metricProperties, nodeMetrics); @@ -101,4 +123,13 @@ private void collect(List nodeMetrics, List attributes, Objec private boolean isCompositeDataObject(Attribute attribute){ return attribute.getValue().getClass().equals(CompositeDataSupport.class); } + + + + } + + + + + diff --git a/src/main/resources/config/config.yml b/src/main/resources/config/config.yml index 6ac59db..b8a127f 100755 --- a/src/main/resources/config/config.yml +++ b/src/main/resources/config/config.yml @@ -38,9 +38,18 @@ mbeans: "kafka.controller:type=ControllerStats,*" ] metrics: - include: - - Count: "Count" - - MeanRate: "MeanRate" + Count: + alias: "Count" + multiplier: + delta: + aggregationType: + timeRollUpType: + clusterRollUpType: + + + MeanRate: + alias: "MeanRate" + #All MBeans which have attributes Value - mbeanFullPath: ["kafka.server:type=DelayedOperationPurgatory,*", @@ -54,6 +63,5 @@ mbeans: "kafka.network:type=SocketServer,*" ] metrics: - include: - Value: "Value" diff --git a/src/main/resources/config/monitor.xml b/src/main/resources/config/monitor.xml index fe3e4e4..a2f5d30 100644 --- a/src/main/resources/config/monitor.xml +++ b/src/main/resources/config/monitor.xml @@ -9,7 +9,7 @@ KafkaMonitor managed - Kafka monitor + Kafka Monitor periodic From 258c8b7a9c387699078ed551f34c09208b479cca Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Tue, 3 Jul 2018 16:17:26 -0700 Subject: [PATCH 03/49] fixed metricPath and NFE --- .idea/compiler.xml | 1 + pom.xml | 23 +++-- .../kafka/JMXConnectionAdapter.java | 8 +- .../extensions/kafka/KafkaMonitor.java | 24 ++++- .../extensions/kafka/KafkaMonitorTask.java | 33 ++++--- .../kafka/metrics/DomainMetricsProcessor.java | 98 ++++++++++++------- .../extensions/kafka/utils/Constants.java | 5 +- .../resources/{config => conf}/config.yml | 15 +-- .../resources/{config => conf}/monitor.xml | 4 +- 9 files changed, 134 insertions(+), 77 deletions(-) rename src/main/resources/{config => conf}/config.yml (92%) rename src/main/resources/{config => conf}/monitor.xml (93%) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 3e2c1d7..ba08bbe 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -18,6 +18,7 @@ + diff --git a/pom.xml b/pom.xml index 163c05e..b774bac 100644 --- a/pom.xml +++ b/pom.xml @@ -7,46 +7,55 @@ 1.0.3 jar kafka-monitoring-extension - http://maven.apache.org + UTF-8 yyyy-MM-dd HH:mm:ss + ${project.build.directory}/KafkaMonitor + com.appdynamics appd-exts-commons 2.1.0 + provided + commons-lang commons-lang 2.6 + commons-io commons-io 2.4 provided + com.appdynamics machine-agent 3.7.11 - provided + + log4j log4j 1.2.17 - provided + + org.slf4j slf4j-log4j12 1.7.21 provided + org.mockito mockito-all @@ -119,14 +128,14 @@ install - + - - + + - + diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index ac69066..95f2e8c 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -49,14 +49,16 @@ static JMXConnectionAdapter create(Map requestMap) throws Malfor return new JMXConnectionAdapter(requestMap); } - JMXConnector open() throws IOException { + JMXConnector open() throws IOException { JMXConnector jmxConnector; final Map env = new HashMap(); if (!Strings.isNullOrEmpty(username)) { env.put(JMXConnector.CREDENTIALS, new String[]{username, password}); jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); } else { + jmxConnector = JMXConnectorFactory.connect(serviceUrl); + } if (jmxConnector == null) { throw new IOException("Unable to connect to Mbean server"); @@ -97,7 +99,5 @@ public List getAttributes(JMXConnector jmxConnection, ObjectName obje } - boolean matchAttributeName(Attribute attribute, String matchedWith) { - return attribute.getName().equalsIgnoreCase(matchedWith); - } + } diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index cc30911..e5762e5 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -12,8 +12,15 @@ import com.appdynamics.extensions.ABaseMonitor; import com.appdynamics.extensions.TasksExecutionServiceProvider; import com.appdynamics.extensions.util.AssertUtils; +import com.singularity.ee.agent.systemagent.api.exception.TaskExecutionException; +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.Level; +import org.apache.log4j.PatternLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import java.io.OutputStreamWriter; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -49,7 +56,7 @@ protected void doRun(TasksExecutionServiceProvider tasksExecutionServiceProvider KafkaMonitorTask task = new KafkaMonitorTask(tasksExecutionServiceProvider, this.getContextConfiguration(), kafkaServer); AssertUtils.assertNotNull(kafkaServer.get("displayName"), "The displayName can not be null"); - tasksExecutionServiceProvider.submit((String) kafkaServer.get("displayName"), task); + tasksExecutionServiceProvider.submit(kafkaServer.get("displayName"), task); } } @@ -57,8 +64,21 @@ protected void doRun(TasksExecutionServiceProvider tasksExecutionServiceProvider @Override protected int getTaskCount() { List> servers = (List>) getContextConfiguration().getConfigYml().get("servers"); - AssertUtils.assertNotNull(servers, "The 'servers' section in config.yml is not initialised"); + AssertUtils.assertNotNull(servers, "The 'servers' section in conf.yml is not initialised"); return servers.size(); } + public static void main(String[] args) throws TaskExecutionException { + ConsoleAppender ca = new ConsoleAppender(); + ca.setWriter(new OutputStreamWriter(System.out)); + ca.setLayout(new PatternLayout("%-5p [%t]: %m%n")); + ca.setThreshold(Level.DEBUG); + org.apache.log4j.Logger.getRootLogger().addAppender(ca); + + KafkaMonitor monitor = new KafkaMonitor(); + Map taskArgs = new HashMap(); + taskArgs.put("config-file", "/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/main/resources/conf/config.yml"); + monitor.execute(taskArgs, null); + } + } diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index a879b8e..660bafc 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -35,7 +35,9 @@ public class KafkaMonitorTask implements AMonitorTaskRunnable { private MetricWriteHelper metricWriteHelper; private String metricPrefix; private String displayName; + private JMXConnector jmxConnection; private JMXConnectionAdapter jmxAdapter; + private static final BigDecimal ERROR_VALUE = BigDecimal.ZERO; private static final BigDecimal SUCCESS_VALUE = BigDecimal.ONE; private Phaser phaser; @@ -56,35 +58,34 @@ public void run() { phaser = new Phaser(); populateAndPrintMetrics(); - phaser.arriveAndAwaitAdvance(); +// phaser.arriveAndAwaitAdvance(); logger.info("Completed the Kafka Monitoring task"); } private BigDecimal populateAndPrintMetrics() { - JMXConnector jmxConnection = null; - try{ - jmxAdapter = JMXConnectionAdapter.create(buildRequestMap()); - }catch (Exception e) { - logger.debug("Error in connecting to Kafka" + e); - } try{ + Map requestMap; + requestMap = buildRequestMap(); + jmxAdapter = JMXConnectionAdapter.create(requestMap); jmxConnection = jmxAdapter.open(); logger.debug("JMX Connection is open"); List> mbeansFromConfig = (List>) configuration.getConfigYml().get("mbeans"); + for (Map mbeanFromConfig : mbeansFromConfig) { DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( jmxAdapter, jmxConnection, mbeanFromConfig, displayName,metricWriteHelper, metricPrefix, phaser); configuration.getContext().getExecutorService().execute("DomainMetricsProcessor",domainMetricsProcessor); - logger.debug("Registering phaser for " + displayName); +// logger.debug("Registering phaser for " + displayName); } } catch (Exception e) { - logger.error("Error while opening JMX connection {}{}" + kafkaServer.get("name"), e); +// e.printStackTrace(); + logger.error("Error while opening JMX connection {}{}" + this.kafkaServer.get("name"), e.getStackTrace()); } finally { try { - jmxAdapter.close(jmxConnection); +// jmxAdapter.close(jmxConnection); logger.debug("JMX connection is closed"); - } catch (IOException ioe) { + } catch (Exception ioe) { logger.error("Unable to close the connection."); return ERROR_VALUE; } @@ -97,14 +98,20 @@ private Map buildRequestMap() { Map requestMap = new HashMap(); requestMap.put("host", kafkaServer.get("host")); requestMap.put("port", kafkaServer.get("port")); - requestMap.put("username", kafkaServer.get("username")); requestMap.put("displayName", kafkaServer.get("displayName")); - requestMap.put("password", getPassword(kafkaServer)); + + if(!Strings.isNullOrEmpty(kafkaServer.get("username"))) { + requestMap.put("username", kafkaServer.get("username")); + requestMap.put("password", getPassword(kafkaServer)); + } return requestMap; } private String getPassword(Map server) { String password = server.get("password"); + if(Strings.isNullOrEmpty(password)){ + logger.error("Password cannot be null"); + } String encryptedPassword = server.get("encryptedPassword"); Map configMap = configuration.getConfigYml(); String encryptionKey = configMap.get("encryptionKey").toString(); diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index 8e7608a..13ffba1 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -12,8 +12,11 @@ import com.appdynamics.extensions.MetricWriteHelper; import com.appdynamics.extensions.conf.MonitorContext; import com.appdynamics.extensions.kafka.JMXConnectionAdapter; +import com.appdynamics.extensions.kafka.utils.Constants; import com.appdynamics.extensions.metrics.Metric; +import com.appdynamics.extensions.metrics.MetricProperties; +import com.google.common.base.Strings; import com.google.common.collect.Lists; import org.slf4j.LoggerFactory; @@ -23,10 +26,8 @@ import javax.management.openmbean.CompositeDataSupport; import javax.management.remote.JMXConnector; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.math.BigDecimal; +import java.util.*; import java.util.concurrent.Phaser; public class DomainMetricsProcessor implements Runnable{ @@ -47,7 +48,7 @@ public DomainMetricsProcessor( JMXConnectionAdapter jmxAdapter, JMXConnector jmx this.metricWriteHelper = metricWriteHelper; this.metricPrefix = metricPrefix; this.phaser = phaser; - this.phaser.register(); +// this.phaser.register(); this.mbeanFromConfig = mbeanFromConfig; this.displayName = displayName; } @@ -55,65 +56,56 @@ public DomainMetricsProcessor( JMXConnectionAdapter jmxAdapter, JMXConnector jmx @Override public void run() { try { - - logger.debug(String.format("Processing metricProps from the config file")); Map metricProperties = (Map) this.mbeanFromConfig.get("metrics"); - - + logger.debug(String.format("Processing metrics section from the conf file")); + logger.debug("Size of metric section {}",metricProperties.size()); List mbeanNames = (List) this.mbeanFromConfig.get("mbeanFullPath"); - for (String mbeanName : mbeanNames) { - - logger.debug(String.format("Processing mbean %s from the config file", mbeanName)); - - getNodeMetrics(jmxConnection,mbeanName,metricProperties); - logger.debug("Registering phaser for " + displayName); - } - - - - //printmetric and heartbeat + logger.debug("Size of mbean section {}",mbeanNames.size()); + for (String mbeanName : mbeanNames) { + logger.debug(String.format("Processing mbean %s from the conf file", mbeanName)); + List finalMetricList = getNodeMetrics(jmxConnection,mbeanName,metricProperties); + logger.debug("Printing metrics for server {}", mbeanName); + metricWriteHelper.transformAndPrintMetrics(finalMetricList); + } + // heartbeat } catch (IntrospectionException | IOException | MalformedObjectNameException | InstanceNotFoundException | ReflectionException e) { e.printStackTrace(); } finally { logger.debug("DomainProcessor Phaser arrived for {}", displayName); - phaser.arriveAndDeregister(); +// phaser.arriveAndDeregister(); } - } - private List getNodeMetrics(JMXConnector jmxConnection, String objectName,Map metricProperties) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { + private List getNodeMetrics(JMXConnector jmxConnection, String objectName, Map metricProperties) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { List nodeMetrics = Lists.newArrayList(); - Set objectInstances = jmxAdapter.queryMBeans(jmxConnection, ObjectName.getInstance(objectName)); + Set objectInstances = this.jmxAdapter.queryMBeans(jmxConnection, ObjectName.getInstance(objectName)); for (ObjectInstance instance : objectInstances) { - List metricNamesDictionary = jmxAdapter.getReadableAttributeNames(jmxConnection, instance); - List attributes = jmxAdapter.getAttributes(jmxConnection, instance.getObjectName(), metricNamesDictionary.toArray(new String[metricNamesDictionary.size()])); - collect(nodeMetrics, attributes, instance,metricProperties); + List metricNamesDictionary = this.jmxAdapter.getReadableAttributeNames(jmxConnection, instance); + List attributes =this.jmxAdapter.getAttributes(jmxConnection, instance.getObjectName(), metricNamesDictionary.toArray(new String[metricNamesDictionary.size()])); + collect(nodeMetrics, attributes, instance, metricProperties); } return nodeMetrics; } - private void collect(List nodeMetrics, List attributes, ObjectInstance instance, Map metricProperties) { for (Attribute attribute : attributes) { try { - if(isCompositeDataObject(attribute)){ Set attributesFound = ((CompositeData)attribute.getValue()).getCompositeType().keySet(); for(String str: attributesFound){ String key = attribute.getName()+ "."+ str; Object attributeValue = ((CompositeDataSupport) attribute.getValue()).get(str); if(metricProperties.containsKey(key)){ -// setMetricDetails(metricPrefix, key, attributeValue, instance, metricProperties, nodeMetrics); - + setMetricDetails(metricPrefix, key, attributeValue.toString(), instance, metricProperties, nodeMetrics); } } } -// else -// setMetricDetails(metricPrefix, attribute.getName(), attribute.getValue(), instance, metricProperties, nodeMetrics); - - - + else{ + if(metricProperties.containsKey(attribute.getName())) { + setMetricDetails(metricPrefix, attribute.getName(), attribute.getValue(), instance, metricProperties, nodeMetrics); + } + } } catch (Exception e) { logger.error("Error collecting value for {} {}", instance.getObjectName(), attribute.getName(), e); } @@ -124,8 +116,38 @@ private boolean isCompositeDataObject(Attribute attribute){ return attribute.getValue().getClass().equals(CompositeDataSupport.class); } + private void setMetricDetails (String metricPrefix, String attributeName, Object attributeValue, ObjectInstance instance, Map metricPropertiesMap, List nodeMetrics) { + String metricPath = metricPrefix + Constants.METRIC_SEPARATOR + buildName(instance)+ attributeName; + Metric metric = new Metric(attributeName, attributeValue.toString(), metricPath, metricPropertiesMap); + nodeMetrics.add(metric); + } + + private String buildName(ObjectInstance instance) { + ObjectName objectName = instance.getObjectName(); + Hashtable keyPropertyList = objectName.getKeyPropertyList(); + StringBuilder sb = new StringBuilder(); + sb.append(objectName.getDomain()); + String type = keyPropertyList.get("type"); + String name = keyPropertyList.get("name"); + if(!Strings.isNullOrEmpty(type)) { + sb.append(Constants.METRIC_SEPARATOR); + sb.append(type); + } + if(!Strings.isNullOrEmpty(name)) { + sb.append(Constants.METRIC_SEPARATOR); + sb.append(name); + } + sb.append(Constants.METRIC_SEPARATOR); + keyPropertyList.remove("type"); + keyPropertyList.remove("name"); + + for (Map.Entry entry : keyPropertyList.entrySet()) { + sb.append(entry.getKey()).append(Constants.METRIC_SEPARATOR).append(entry.getValue()).append(Constants.METRIC_SEPARATOR); + } + return sb.toString(); + } } @@ -133,3 +155,7 @@ private boolean isCompositeDataObject(Attribute attribute){ + + + + diff --git a/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java index e98c5ef..d815692 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java +++ b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java @@ -3,13 +3,10 @@ import java.math.BigDecimal; public class Constants { - public static final String DEFAULT_METRIC_PREFIX = "Custom Metrics|Kafka|"; + public static final String DEFAULT_METRIC_PREFIX = "Custom Metrics|Kafka"; public static final String METRIC_SEPARATOR = "|"; - public static final String INCLUDE = "include"; - - public static final String EXCLUDE = "exclude"; static final String MULTIPLIER = "multiplier"; diff --git a/src/main/resources/config/config.yml b/src/main/resources/conf/config.yml similarity index 92% rename from src/main/resources/config/config.yml rename to src/main/resources/conf/config.yml index b8a127f..9e1c9d0 100755 --- a/src/main/resources/config/config.yml +++ b/src/main/resources/conf/config.yml @@ -8,11 +8,11 @@ metricPrefix: Custom Metrics|Kafka #metricPrefix: Server|Component:|Custom Metrics|Kafka # List of Kafka Instances -instances: +servers: - host: "localhost" port: "9999" - username: - password: + username: "" + password: "" #encryptedPassword: #encryptionKey: displayName: "Local Kafka Server" #displayName is a REQUIRED field for level metrics. @@ -40,12 +40,6 @@ mbeans: metrics: Count: alias: "Count" - multiplier: - delta: - aggregationType: - timeRollUpType: - clusterRollUpType: - MeanRate: alias: "MeanRate" @@ -63,5 +57,6 @@ mbeans: "kafka.network:type=SocketServer,*" ] metrics: - - Value: "Value" + Value: + alias: "Value" diff --git a/src/main/resources/config/monitor.xml b/src/main/resources/conf/monitor.xml similarity index 93% rename from src/main/resources/config/monitor.xml rename to src/main/resources/conf/monitor.xml index a2f5d30..3aeda98 100644 --- a/src/main/resources/config/monitor.xml +++ b/src/main/resources/conf/monitor.xml @@ -9,7 +9,9 @@ KafkaMonitor managed + true Kafka Monitor + true periodic @@ -20,7 +22,7 @@ java 60 - + From fd86ed3f833dfd49d2bdce9d96fd022ad4c4656e Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Mon, 9 Jul 2018 17:14:04 -0700 Subject: [PATCH 04/49] initial unit tests --- .gitignore | 11 +++++++ .idea/compiler.xml | 4 +++ .idea/encodings.xml | 2 ++ .idea/modules.xml | 2 ++ .../kafka/JMXConnectionAdapter.java | 24 +++++--------- .../extensions/kafka/KafkaMonitor.java | 19 ++++++----- .../extensions/kafka/KafkaMonitorTask.java | 26 +++++++-------- .../kafka/metrics/DomainMetricsProcessor.java | 11 ++----- .../extensions/kafka/utils/Constants.java | 32 ++++++++----------- 9 files changed, 64 insertions(+), 67 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b58a3b --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +target +*.iml +.settings +.classpath +.project +*.log +*.ipr +*.iws +.idea +.DS_Store + diff --git a/.idea/compiler.xml b/.idea/compiler.xml index ba08bbe..f5ff636 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -11,6 +11,8 @@ + + @@ -20,6 +22,8 @@ + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 56c40e7..343def5 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -6,5 +6,7 @@ + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index 047ecaa..23da7b5 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -7,6 +7,8 @@ + + \ No newline at end of file diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index 95f2e8c..be8e2c2 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -9,6 +9,7 @@ package com.appdynamics.extensions.kafka; +import com.appdynamics.extensions.kafka.utils.Constants; import com.google.common.base.Strings; import com.google.common.collect.Lists; @@ -38,31 +39,25 @@ public class JMXConnectionAdapter { private final String password; private JMXConnectionAdapter(Map requestMap) throws MalformedURLException { - this.serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + requestMap.get("host") + ":" + requestMap.get("port") + "/jmxrmi"); - this.username = requestMap.get("username"); - this.password = requestMap.get("password"); + this.serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + requestMap.get(Constants.HOST) + ":" + requestMap.get(Constants.PORT) + "/jmxrmi"); + this.username = requestMap.get(Constants.USERNAME); + this.password = requestMap.get(Constants.PASSWORD); } - static JMXConnectionAdapter create(Map requestMap) throws MalformedURLException { - return new JMXConnectionAdapter(requestMap); } - JMXConnector open() throws IOException { + JMXConnector open() throws IOException { JMXConnector jmxConnector; final Map env = new HashMap(); if (!Strings.isNullOrEmpty(username)) { env.put(JMXConnector.CREDENTIALS, new String[]{username, password}); jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); - } else { - - jmxConnector = JMXConnectorFactory.connect(serviceUrl); - - } - if (jmxConnector == null) { - throw new IOException("Unable to connect to Mbean server"); } + else { jmxConnector = JMXConnectorFactory.connect(serviceUrl); } + + if (jmxConnector == null) { throw new IOException("Unable to connect to Mbean server"); } return jmxConnector; } @@ -97,7 +92,4 @@ public List getAttributes(JMXConnector jmxConnection, ObjectName obje } return Lists.newArrayList(); } - - - } diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index e5762e5..b4660c5 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -11,8 +11,10 @@ import com.appdynamics.extensions.ABaseMonitor; import com.appdynamics.extensions.TasksExecutionServiceProvider; +import com.appdynamics.extensions.kafka.utils.Constants; import com.appdynamics.extensions.util.AssertUtils; import com.singularity.ee.agent.systemagent.api.exception.TaskExecutionException; +import com.sun.tools.internal.jxc.ap.Const; import org.apache.log4j.ConsoleAppender; import org.apache.log4j.Level; import org.apache.log4j.PatternLayout; @@ -30,14 +32,13 @@ import java.util.Map; +import static com.appdynamics.extensions.kafka.utils.Constants.DEFAULT_METRIC_PREFIX; + public class KafkaMonitor extends ABaseMonitor { - private static final String DEFAULT_METRIC_PREFIX = "Custom Metrics|Kafka"; private static final Logger logger = LoggerFactory.getLogger(KafkaMonitor.class); @Override - protected void initializeMoreStuff(Map args){ - - } + protected void initializeMoreStuff(Map args){ } @Override protected String getDefaultMetricPrefix() { @@ -51,19 +52,17 @@ public String getMonitorName() { @Override protected void doRun(TasksExecutionServiceProvider tasksExecutionServiceProvider) { - List> kafkaServers = (List>) this.getContextConfiguration().getConfigYml().get("servers"); + List> kafkaServers = (List>) this.getContextConfiguration().getConfigYml().get(Constants.SERVERS); for (Map kafkaServer : kafkaServers) { - KafkaMonitorTask task = new KafkaMonitorTask(tasksExecutionServiceProvider, this.getContextConfiguration(), kafkaServer); - AssertUtils.assertNotNull(kafkaServer.get("displayName"), "The displayName can not be null"); - tasksExecutionServiceProvider.submit(kafkaServer.get("displayName"), task); + AssertUtils.assertNotNull(kafkaServer.get(Constants.DISPLAY_NAME), "The displayName can not be null"); + tasksExecutionServiceProvider.submit(kafkaServer.get(Constants.DISPLAY_NAME), task); } - } @Override protected int getTaskCount() { - List> servers = (List>) getContextConfiguration().getConfigYml().get("servers"); + List> servers = (List>) getContextConfiguration().getConfigYml().get(Constants.SERVERS); AssertUtils.assertNotNull(servers, "The 'servers' section in conf.yml is not initialised"); return servers.size(); } diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index 660bafc..08fe0df 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -15,6 +15,7 @@ import com.appdynamics.extensions.crypto.CryptoUtil; import com.appdynamics.extensions.kafka.metrics.DomainMetricsProcessor; +import com.appdynamics.extensions.kafka.utils.Constants; import com.google.common.base.Strings; import com.google.common.collect.Maps; import org.slf4j.LoggerFactory; @@ -45,9 +46,9 @@ public class KafkaMonitorTask implements AMonitorTaskRunnable { public KafkaMonitorTask(TasksExecutionServiceProvider serviceProvider, MonitorContextConfiguration configuration, Map kafkaServer) { this.configuration = configuration; this.kafkaServer = kafkaServer; - this.metricPrefix = configuration.getMetricPrefix() + "|" + kafkaServer.get("displayName"); + this.metricPrefix = configuration.getMetricPrefix() + Constants.METRIC_SEPARATOR + kafkaServer.get(Constants.DISPLAY_NAME); this.metricWriteHelper = serviceProvider.getMetricWriteHelper(); - this.displayName = (String) kafkaServer.get("displayName"); + this.displayName = (String) kafkaServer.get(Constants.DISPLAY_NAME); } public void onTaskComplete() { @@ -69,17 +70,14 @@ private BigDecimal populateAndPrintMetrics() { jmxAdapter = JMXConnectionAdapter.create(requestMap); jmxConnection = jmxAdapter.open(); logger.debug("JMX Connection is open"); - List> mbeansFromConfig = (List>) configuration.getConfigYml().get("mbeans"); + List> mbeansFromConfig = (List>) configuration.getConfigYml().get(Constants.MBEANS); for (Map mbeanFromConfig : mbeansFromConfig) { - DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( jmxAdapter, jmxConnection, mbeanFromConfig, displayName,metricWriteHelper, metricPrefix, phaser); configuration.getContext().getExecutorService().execute("DomainMetricsProcessor",domainMetricsProcessor); // logger.debug("Registering phaser for " + displayName); } - } catch (Exception e) { -// e.printStackTrace(); logger.error("Error while opening JMX connection {}{}" + this.kafkaServer.get("name"), e.getStackTrace()); } finally { try { @@ -96,25 +94,25 @@ private BigDecimal populateAndPrintMetrics() { private Map buildRequestMap() { Map requestMap = new HashMap(); - requestMap.put("host", kafkaServer.get("host")); - requestMap.put("port", kafkaServer.get("port")); - requestMap.put("displayName", kafkaServer.get("displayName")); + requestMap.put("host", kafkaServer.get(Constants.HOST)); + requestMap.put("port", kafkaServer.get(Constants.PORT)); + requestMap.put("displayName", kafkaServer.get(Constants.DISPLAY_NAME)); - if(!Strings.isNullOrEmpty(kafkaServer.get("username"))) { - requestMap.put("username", kafkaServer.get("username")); + if(!Strings.isNullOrEmpty(kafkaServer.get(Constants.USERNAME))) { + requestMap.put("username", kafkaServer.get(Constants.USERNAME)); requestMap.put("password", getPassword(kafkaServer)); } return requestMap; } private String getPassword(Map server) { - String password = server.get("password"); + String password = server.get(Constants.PASSWORD); if(Strings.isNullOrEmpty(password)){ logger.error("Password cannot be null"); } - String encryptedPassword = server.get("encryptedPassword"); + String encryptedPassword = server.get(Constants.ENCRYPTED_PASSWORD); Map configMap = configuration.getConfigYml(); - String encryptionKey = configMap.get("encryptionKey").toString(); + String encryptionKey = configMap.get(Constants.ENCRYPTION_KEY).toString(); if(!Strings.isNullOrEmpty(password)){ return password; } diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index 13ffba1..fb71e94 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -14,19 +14,14 @@ import com.appdynamics.extensions.kafka.JMXConnectionAdapter; import com.appdynamics.extensions.kafka.utils.Constants; import com.appdynamics.extensions.metrics.Metric; - -import com.appdynamics.extensions.metrics.MetricProperties; import com.google.common.base.Strings; import com.google.common.collect.Lists; - import org.slf4j.LoggerFactory; - import javax.management.*; import javax.management.openmbean.CompositeData; import javax.management.openmbean.CompositeDataSupport; import javax.management.remote.JMXConnector; import java.io.IOException; -import java.math.BigDecimal; import java.util.*; import java.util.concurrent.Phaser; @@ -56,7 +51,7 @@ public DomainMetricsProcessor( JMXConnectionAdapter jmxAdapter, JMXConnector jmx @Override public void run() { try { - Map metricProperties = (Map) this.mbeanFromConfig.get("metrics"); + Map metricProperties = (Map) this.mbeanFromConfig.get(Constants.METRICS); logger.debug(String.format("Processing metrics section from the conf file")); logger.debug("Size of metric section {}",metricProperties.size()); List mbeanNames = (List) this.mbeanFromConfig.get("mbeanFullPath"); @@ -126,11 +121,10 @@ private String buildName(ObjectInstance instance) { ObjectName objectName = instance.getObjectName(); Hashtable keyPropertyList = objectName.getKeyPropertyList(); StringBuilder sb = new StringBuilder(); - sb.append(objectName.getDomain()); - String type = keyPropertyList.get("type"); String name = keyPropertyList.get("name"); + sb.append(objectName.getDomain()); if(!Strings.isNullOrEmpty(type)) { sb.append(Constants.METRIC_SEPARATOR); sb.append(type); @@ -142,7 +136,6 @@ private String buildName(ObjectInstance instance) { sb.append(Constants.METRIC_SEPARATOR); keyPropertyList.remove("type"); keyPropertyList.remove("name"); - for (Map.Entry entry : keyPropertyList.entrySet()) { sb.append(entry.getKey()).append(Constants.METRIC_SEPARATOR).append(entry.getValue()).append(Constants.METRIC_SEPARATOR); } diff --git a/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java index d815692..9522649 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java +++ b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java @@ -1,42 +1,38 @@ package com.appdynamics.extensions.kafka.utils; -import java.math.BigDecimal; public class Constants { public static final String DEFAULT_METRIC_PREFIX = "Custom Metrics|Kafka"; public static final String METRIC_SEPARATOR = "|"; + public static final String MULTIPLIER = "multiplier"; - static final String MULTIPLIER = "multiplier"; + public static final String METRIC_TYPE = "metricType"; - static final String METRIC_TYPE = "metricType"; + public static final String AGGREGATION = "aggregation"; - static final String AGGREGATION = "aggregation"; + public static final String CONVERT = "convert"; - static final String CONVERT = "convert"; + public static final String SERVERS = "servers"; - static final String INSTANCES = "instances"; + public static final String SERVICE_URL = "serviceUrl"; - static final String SERVICE_URL = "serviceUrl"; + public static final String HOST = "host"; - static final String HOST = "host"; + public static final String PORT = "port"; - static final String PORT = "port"; + public static final String USERNAME = "username"; - static final String USERNAME = "username"; + public static final String PASSWORD = "password"; - static final String PASSWORD = "password"; + public static final String ENCRYPTION_KEY = "encryptionKey"; - static final String ENCRYPTION_KEY = "encryptionKey"; + public static final String ENCRYPTED_PASSWORD = "encryptedPassword"; - static final String ENCRYPTED_PASSWORD = "encryptedPassword"; + public static final String DISPLAY_NAME = "displayName"; - static final String DISPLAY_NAME = "displayName"; - - static final String MBEANS = "mbeans"; - - public static final String OBJECT_NAME = "objectName"; + public static final String MBEANS = "mbeans"; public static final String METRICS = "metrics"; From 51fe6e126a397002f264da1a531b62b634908a63 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Wed, 11 Jul 2018 07:02:27 -0700 Subject: [PATCH 05/49] fixed phaser, heartbeat, removed thread in metricprocessor --- .gitignore | 2 +- .idea/compiler.xml | 2 ++ .idea/encodings.xml | 1 + .idea/modules.xml | 1 + .../kafka/JMXConnectionAdapter.java | 2 ++ .../extensions/kafka/KafkaMonitor.java | 9 +----- .../extensions/kafka/KafkaMonitorTask.java | 19 +++++------ .../kafka/metrics/DomainMetricsProcessor.java | 32 ++++++++++++------- .../extensions/kafka/utils/Constants.java | 2 ++ src/main/resources/conf/config.yml | 17 ++++++++-- 10 files changed, 55 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 5b58a3b..32e72a6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,6 @@ target *.log *.ipr *.iws -.idea +*.idea .DS_Store diff --git a/.idea/compiler.xml b/.idea/compiler.xml index f5ff636..6e8c656 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -6,6 +6,7 @@ + @@ -16,6 +17,7 @@ + diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 343def5..aa10de0 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,6 +1,7 @@ + diff --git a/.idea/modules.xml b/.idea/modules.xml index 23da7b5..6b28912 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,6 +2,7 @@ + diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index be8e2c2..53bd2d1 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -25,6 +25,7 @@ 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.util.HashMap; @@ -53,6 +54,7 @@ JMXConnector open() throws IOException { final Map env = new HashMap(); if (!Strings.isNullOrEmpty(username)) { env.put(JMXConnector.CREDENTIALS, new String[]{username, password}); + jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); } else { jmxConnector = JMXConnectorFactory.connect(serviceUrl); } diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index b4660c5..80cb996 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -14,23 +14,16 @@ import com.appdynamics.extensions.kafka.utils.Constants; import com.appdynamics.extensions.util.AssertUtils; import com.singularity.ee.agent.systemagent.api.exception.TaskExecutionException; -import com.sun.tools.internal.jxc.ap.Const; import org.apache.log4j.ConsoleAppender; import org.apache.log4j.Level; import org.apache.log4j.PatternLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import java.io.OutputStreamWriter; import java.util.HashMap; import java.util.List; import java.util.Map; -import com.appdynamics.extensions.util.AssertUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Map; import static com.appdynamics.extensions.kafka.utils.Constants.DEFAULT_METRIC_PREFIX; @@ -47,7 +40,7 @@ protected String getDefaultMetricPrefix() { @Override public String getMonitorName() { - return "Kafka Extension"; + return "Kafka Monitor"; } @Override diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index 08fe0df..5494e78 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -20,7 +20,6 @@ import com.google.common.collect.Maps; import org.slf4j.LoggerFactory; import javax.management.remote.JMXConnector; -import java.io.IOException; import java.math.BigDecimal; import java.util.HashMap; import java.util.List; @@ -38,7 +37,6 @@ public class KafkaMonitorTask implements AMonitorTaskRunnable { private String displayName; private JMXConnector jmxConnection; private JMXConnectionAdapter jmxAdapter; - private static final BigDecimal ERROR_VALUE = BigDecimal.ZERO; private static final BigDecimal SUCCESS_VALUE = BigDecimal.ONE; private Phaser phaser; @@ -52,19 +50,20 @@ public KafkaMonitorTask(TasksExecutionServiceProvider serviceProvider, MonitorCo } public void onTaskComplete() { - + logger.info("All tasks for server {} finished", this.kafkaServer.get(Constants.DISPLAY_NAME)); } public void run() { - phaser = new Phaser(); + populateAndPrintMetrics(); -// phaser.arriveAndAwaitAdvance(); logger.info("Completed the Kafka Monitoring task"); } private BigDecimal populateAndPrintMetrics() { + try{ + phaser = new Phaser(); Map requestMap; requestMap = buildRequestMap(); jmxAdapter = JMXConnectionAdapter.create(requestMap); @@ -74,14 +73,16 @@ private BigDecimal populateAndPrintMetrics() { for (Map mbeanFromConfig : mbeansFromConfig) { DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( jmxAdapter, jmxConnection, mbeanFromConfig, displayName,metricWriteHelper, metricPrefix, phaser); - configuration.getContext().getExecutorService().execute("DomainMetricsProcessor",domainMetricsProcessor); -// logger.debug("Registering phaser for " + displayName); + domainMetricsProcessor.populateMetricsForMBean(); + logger.debug("Registering phaser for " + displayName); + } + } catch (Exception e) { - logger.error("Error while opening JMX connection {}{}" + this.kafkaServer.get("name"), e.getStackTrace()); + logger.error("Error while opening JMX connection {}{}" ,this.kafkaServer.get(Constants.DISPLAY_NAME), e.getMessage()); } finally { try { -// jmxAdapter.close(jmxConnection); + jmxAdapter.close(jmxConnection); logger.debug("JMX connection is closed"); } catch (Exception ioe) { logger.error("Unable to close the connection."); diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index fb71e94..cfe08b0 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -10,7 +10,6 @@ import com.appdynamics.extensions.MetricWriteHelper; -import com.appdynamics.extensions.conf.MonitorContext; import com.appdynamics.extensions.kafka.JMXConnectionAdapter; import com.appdynamics.extensions.kafka.utils.Constants; import com.appdynamics.extensions.metrics.Metric; @@ -22,16 +21,16 @@ import javax.management.openmbean.CompositeDataSupport; import javax.management.remote.JMXConnector; import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.*; import java.util.concurrent.Phaser; -public class DomainMetricsProcessor implements Runnable{ +public class DomainMetricsProcessor { static final org.slf4j.Logger logger = LoggerFactory.getLogger(DomainMetricsProcessor.class); private final JMXConnectionAdapter jmxAdapter; private final JMXConnector jmxConnection; - private MonitorContext context; private MetricWriteHelper metricWriteHelper; - private List metrics = new ArrayList(); private String metricPrefix; private Phaser phaser; private Map mbeanFromConfig; @@ -42,36 +41,45 @@ public DomainMetricsProcessor( JMXConnectionAdapter jmxAdapter, JMXConnector jmx this.jmxConnection = jmxConnection; this.metricWriteHelper = metricWriteHelper; this.metricPrefix = metricPrefix; - this.phaser = phaser; -// this.phaser.register(); + this.phaser = phaser;this.phaser.register(); this.mbeanFromConfig = mbeanFromConfig; this.displayName = displayName; } - @Override - public void run() { + @SuppressWarnings("unchecked") + public void populateMetricsForMBean() { + phaser.arriveAndAwaitAdvance(); try { Map metricProperties = (Map) this.mbeanFromConfig.get(Constants.METRICS); logger.debug(String.format("Processing metrics section from the conf file")); logger.debug("Size of metric section {}",metricProperties.size()); - List mbeanNames = (List) this.mbeanFromConfig.get("mbeanFullPath"); + List mbeanNames = (List) this.mbeanFromConfig.get(Constants.OBJECTNAME); logger.debug("Size of mbean section {}",mbeanNames.size()); for (String mbeanName : mbeanNames) { + logger.debug(String.format("Processing mbean %s from the conf file", mbeanName)); List finalMetricList = getNodeMetrics(jmxConnection,mbeanName,metricProperties); + finalMetricList.add(new Metric("HeartBeat", String.valueOf(BigInteger.ONE), metricPrefix + "|HeartBeat", "AVG", "AVG", "IND")); + + logger.debug("Printing metrics for server {}", mbeanName); metricWriteHelper.transformAndPrintMetrics(finalMetricList); + } - // heartbeat + + } catch (IntrospectionException | IOException | MalformedObjectNameException | InstanceNotFoundException | ReflectionException e) { - e.printStackTrace(); + logger.error("Kafka Monitor error: " + e.getMessage()); + metricWriteHelper.printMetric(metricPrefix + "|HeartBeat", BigDecimal.ZERO, "AVG.AVG.IND"); } finally { + phaser.arriveAndDeregister(); logger.debug("DomainProcessor Phaser arrived for {}", displayName); -// phaser.arriveAndDeregister(); + } } + @SuppressWarnings("unchecked") private List getNodeMetrics(JMXConnector jmxConnection, String objectName, Map metricProperties) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { List nodeMetrics = Lists.newArrayList(); Set objectInstances = this.jmxAdapter.queryMBeans(jmxConnection, ObjectName.getInstance(objectName)); diff --git a/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java index 9522649..e0621a3 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java +++ b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java @@ -36,5 +36,7 @@ public class Constants { public static final String METRICS = "metrics"; + public static final String OBJECTNAME = "objectName"; + } diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index 9e1c9d0..7a0ffb2 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -15,9 +15,22 @@ servers: password: "" #encryptedPassword: #encryptionKey: + useSSl: false displayName: "Local Kafka Server" #displayName is a REQUIRED field for level metrics. +connection: + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocols: ["TLSV1.2"] + sslCertCheckEnabled: true + sslVerifyHostname: true + sslCipherSuites: [] + sslTrustStorePath: "" + sslTrustStorePassword: "" + sslTrustStoreEncryptedPassword: "" + useDefaultSslConnectionFactory: +#nsme of flag needs review # number of concurrent tasks. # This doesn't need to be changed unless many instances are configured numberOfThreads: 10 @@ -28,7 +41,7 @@ numberOfThreads: 10 mbeans: #All MBeans which have attributes Count and MeanRate - - mbeanFullPath: ["kafka.server:type=BrokerTopicMetrics,*", + - objectName: ["kafka.server:type=BrokerTopicMetrics,*", "kafka.server:type=DelayedFetchMetrics,*", "kafka.server:type=KafkaRequestHandlerPool,*", "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", @@ -46,7 +59,7 @@ mbeans: #All MBeans which have attributes Value - - mbeanFullPath: ["kafka.server:type=DelayedOperationPurgatory,*", + - objectName: ["kafka.server:type=DelayedOperationPurgatory,*", "kafka.server:type=KafkaServer,name=BrokerState", "kafka.server:type=ReplicaFetcherManager,*", "kafka.server:type=ReplicaManager,name=LeaderCount", From a3ebd3833421292480b29d72690d1f46783d34ca Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Wed, 11 Jul 2018 07:46:54 -0700 Subject: [PATCH 06/49] uploading test files --- .../extensions/kafka/KafkaMonitorTask.java | 16 ---------------- .../kafka/metrics/DomainMetricsProcessor.java | 18 +++++------------- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index 5494e78..84c4c13 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -26,8 +26,6 @@ import java.util.Map; import java.util.concurrent.Phaser; - - public class KafkaMonitorTask implements AMonitorTaskRunnable { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(KafkaMonitorTask.class); private MonitorContextConfiguration configuration; @@ -54,14 +52,11 @@ public void onTaskComplete() { } public void run() { - populateAndPrintMetrics(); - logger.info("Completed the Kafka Monitoring task"); } private BigDecimal populateAndPrintMetrics() { - try{ phaser = new Phaser(); Map requestMap; @@ -70,14 +65,11 @@ private BigDecimal populateAndPrintMetrics() { jmxConnection = jmxAdapter.open(); logger.debug("JMX Connection is open"); List> mbeansFromConfig = (List>) configuration.getConfigYml().get(Constants.MBEANS); - for (Map mbeanFromConfig : mbeansFromConfig) { DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( jmxAdapter, jmxConnection, mbeanFromConfig, displayName,metricWriteHelper, metricPrefix, phaser); domainMetricsProcessor.populateMetricsForMBean(); logger.debug("Registering phaser for " + displayName); - } - } catch (Exception e) { logger.error("Error while opening JMX connection {}{}" ,this.kafkaServer.get(Constants.DISPLAY_NAME), e.getMessage()); } finally { @@ -92,13 +84,11 @@ private BigDecimal populateAndPrintMetrics() { return SUCCESS_VALUE; } - private Map buildRequestMap() { Map requestMap = new HashMap(); requestMap.put("host", kafkaServer.get(Constants.HOST)); requestMap.put("port", kafkaServer.get(Constants.PORT)); requestMap.put("displayName", kafkaServer.get(Constants.DISPLAY_NAME)); - if(!Strings.isNullOrEmpty(kafkaServer.get(Constants.USERNAME))) { requestMap.put("username", kafkaServer.get(Constants.USERNAME)); requestMap.put("password", getPassword(kafkaServer)); @@ -126,12 +116,6 @@ private String getPassword(Map server) { } return ""; } - - - - - - } diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index cfe08b0..97f9493 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -56,26 +56,18 @@ public void populateMetricsForMBean() { List mbeanNames = (List) this.mbeanFromConfig.get(Constants.OBJECTNAME); logger.debug("Size of mbean section {}",mbeanNames.size()); for (String mbeanName : mbeanNames) { - - logger.debug(String.format("Processing mbean %s from the conf file", mbeanName)); - List finalMetricList = getNodeMetrics(jmxConnection,mbeanName,metricProperties); - finalMetricList.add(new Metric("HeartBeat", String.valueOf(BigInteger.ONE), metricPrefix + "|HeartBeat", "AVG", "AVG", "IND")); - - - logger.debug("Printing metrics for server {}", mbeanName); - metricWriteHelper.transformAndPrintMetrics(finalMetricList); - + logger.debug(String.format("Processing mbean %s from the conf file", mbeanName)); + List finalMetricList = getNodeMetrics(jmxConnection,mbeanName,metricProperties); + finalMetricList.add(new Metric("HeartBeat", String.valueOf(BigInteger.ONE), metricPrefix + "|HeartBeat", "AVG", "AVG", "IND")); + logger.debug("Printing metrics for server {}", mbeanName); + metricWriteHelper.transformAndPrintMetrics(finalMetricList); } - - - } catch (IntrospectionException | IOException | MalformedObjectNameException | InstanceNotFoundException | ReflectionException e) { logger.error("Kafka Monitor error: " + e.getMessage()); metricWriteHelper.printMetric(metricPrefix + "|HeartBeat", BigDecimal.ZERO, "AVG.AVG.IND"); } finally { phaser.arriveAndDeregister(); logger.debug("DomainProcessor Phaser arrived for {}", displayName); - } } From 0af05a966fc17932cf5e12dd84262d8ab69e8a96 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Wed, 11 Jul 2018 07:50:46 -0700 Subject: [PATCH 07/49] uploading test folders --- .../kafka/DomainMetricsProcessorTest.java | 73 +++++++++++++++++++ .../extensions/kafka/KafkaMonitorTest.java | 24 ++++++ src/test/resources/config.yml | 0 3 files changed, 97 insertions(+) create mode 100644 src/test/java/com/appdynamics/extensions/kafka/DomainMetricsProcessorTest.java create mode 100644 src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java create mode 100644 src/test/resources/config.yml diff --git a/src/test/java/com/appdynamics/extensions/kafka/DomainMetricsProcessorTest.java b/src/test/java/com/appdynamics/extensions/kafka/DomainMetricsProcessorTest.java new file mode 100644 index 0000000..5132c77 --- /dev/null +++ b/src/test/java/com/appdynamics/extensions/kafka/DomainMetricsProcessorTest.java @@ -0,0 +1,73 @@ +package com.appdynamics.extensions.kafka; + +import com.appdynamics.extensions.AMonitorJob; +import com.appdynamics.extensions.MetricWriteHelper; +import com.appdynamics.extensions.conf.MonitorContextConfiguration; +import com.appdynamics.extensions.kafka.metrics.DomainMetricsProcessor; +import com.appdynamics.extensions.yml.YmlReader; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import javax.management.*; +import javax.management.remote.JMXConnector; + +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DomainMetricsProcessorTest { + + JMXConnector jmxConnector = mock(JMXConnector.class); + JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); + + @Test + + public void getNodeMetrics() throws IOException,IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException { + + ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); + AMonitorJob aMonitorJob = mock(AMonitorJob.class); + //pass config file name + MonitorContextConfiguration monitorContextConfiguration = new MonitorContextConfiguration("Kafka Monitor", "Custom Metrics|Kafka|", aMonitorJob); + monitorContextConfiguration.setConfigYml("src/test/resources/conf/conf.yml"); + MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); + +// Map config = YmlReader.readFromFileAsMap(new File(this.getClass().getResource("/conf/config.yml").getFile())); + List mBeans = (List) config.get("mbeans"); + Set objectInstances = Sets.newHashSet(); + objectInstances.add(new ObjectInstance("kafka.server:type=BrokerTopicMetrics,*", "test")); + + Set attributes = Sets.newHashSet(); + attributes.add(new Attribute("Count", new Integer(100)); + attributes.add(new Attribute("Value", new Integer(200) ); + + List metricNames = Lists.newArrayList(); + metricNames.add("testmetric1"); + metricNames.add("testmetric2"); + + when(jmxConnectionAdapter.queryMBeans(eq(jmxConnector), Mockito.any(ObjectName.class))).thenReturn(objectInstances); + when(jmxConnectionAdapter.getReadableAttributeNames(eq(jmxConnector), Mockito.any(ObjectInstance.class))).thenReturn(metricNames); + when(jmxConnectionAdapter.getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), Mockito.any(String[] + .class))).thenReturn((List) eq(attributes)); + + DomainMetricsProcessor domainMetricsProcessor = new + + + + + + } + + + + +} diff --git a/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java b/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java new file mode 100644 index 0000000..a885cb5 --- /dev/null +++ b/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java @@ -0,0 +1,24 @@ +package com.appdynamics.extensions.kafka; + +import com.google.common.collect.Maps; +import com.singularity.ee.agent.systemagent.api.exception.TaskExecutionException; +import org.junit.Test; + +import java.util.Map; + +public class KafkaMonitorTest { + + + public static final String CONFIG_ARG = "config-file"; + + @Test + public void testKafkaMonitorExtension() throws TaskExecutionException { + KafkaMonitor kafkaMonitor = new KafkaMonitor(); + Map taskArgs = Maps.newHashMap(); + taskArgs.put(CONFIG_ARG, "src/test/resources/conf/config.yml"); + kafkaMonitor.execute(taskArgs, null); + + } + + +} \ No newline at end of file diff --git a/src/test/resources/config.yml b/src/test/resources/config.yml new file mode 100644 index 0000000..e69de29 From 292e5cfc2ff923d99f96f4bba8e7d4bf66786261 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Wed, 11 Jul 2018 15:40:11 -0700 Subject: [PATCH 08/49] non composite unit testing --- .idea/compiler.xml | 31 ---------- .idea/encodings.xml | 13 ---- ...__com_appdynamics_machine_agent_3_7_11.xml | 13 ---- .../Maven__commons_io_commons_io_2_4.xml | 13 ---- .../Maven__commons_lang_commons_lang_2_6.xml | 13 ---- .idea/libraries/Maven__junit_junit_4_11.xml | 13 ---- .idea/libraries/Maven__log4j_log4j_1_2_17.xml | 13 ---- ...dehaus_jackson_jackson_core_asl_1_9_13.xml | 13 ---- ...haus_jackson_jackson_mapper_asl_1_9_13.xml | 13 ---- .../Maven__org_hamcrest_hamcrest_core_1_3.xml | 13 ---- .../Maven__org_mockito_mockito_all_1_9_5.xml | 13 ---- .../Maven__org_nanohttpd_nanohttpd_2_3_0.xml | 13 ---- .../Maven__org_slf4j_slf4j_api_1_7_6.xml | 13 ---- .../Maven__org_slf4j_slf4j_log4j12_1_7_21.xml | 13 ---- .../Maven__org_yaml_snakeyaml_1_13.xml | 13 ---- .idea/modules.xml | 15 ----- pom.xml | 4 +- .../extensions/kafka/KafkaMonitorTask.java | 4 +- .../kafka/metrics/DomainMetricsProcessor.java | 10 ++-- src/main/resources/conf/config.yml | 35 +++++------ .../kafka/DomainMetricsProcessorTest.java | 60 +++++++++++-------- .../extensions/kafka/KafkaMonitorTest.java | 2 +- .../resources/conf/config_for_metrics.yml | 11 ++++ src/test/resources/config.yml | 0 24 files changed, 70 insertions(+), 284 deletions(-) delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/encodings.xml delete mode 100644 .idea/libraries/Maven__com_appdynamics_machine_agent_3_7_11.xml delete mode 100644 .idea/libraries/Maven__commons_io_commons_io_2_4.xml delete mode 100644 .idea/libraries/Maven__commons_lang_commons_lang_2_6.xml delete mode 100644 .idea/libraries/Maven__junit_junit_4_11.xml delete mode 100644 .idea/libraries/Maven__log4j_log4j_1_2_17.xml delete mode 100644 .idea/libraries/Maven__org_codehaus_jackson_jackson_core_asl_1_9_13.xml delete mode 100644 .idea/libraries/Maven__org_codehaus_jackson_jackson_mapper_asl_1_9_13.xml delete mode 100644 .idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml delete mode 100644 .idea/libraries/Maven__org_mockito_mockito_all_1_9_5.xml delete mode 100644 .idea/libraries/Maven__org_nanohttpd_nanohttpd_2_3_0.xml delete mode 100644 .idea/libraries/Maven__org_slf4j_slf4j_api_1_7_6.xml delete mode 100644 .idea/libraries/Maven__org_slf4j_slf4j_log4j12_1_7_21.xml delete mode 100644 .idea/libraries/Maven__org_yaml_snakeyaml_1_13.xml delete mode 100644 .idea/modules.xml create mode 100644 src/test/resources/conf/config_for_metrics.yml delete mode 100644 src/test/resources/config.yml diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 6e8c656..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index aa10de0..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__com_appdynamics_machine_agent_3_7_11.xml b/.idea/libraries/Maven__com_appdynamics_machine_agent_3_7_11.xml deleted file mode 100644 index 8d819c5..0000000 --- a/.idea/libraries/Maven__com_appdynamics_machine_agent_3_7_11.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__commons_io_commons_io_2_4.xml b/.idea/libraries/Maven__commons_io_commons_io_2_4.xml deleted file mode 100644 index bc2aad0..0000000 --- a/.idea/libraries/Maven__commons_io_commons_io_2_4.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__commons_lang_commons_lang_2_6.xml b/.idea/libraries/Maven__commons_lang_commons_lang_2_6.xml deleted file mode 100644 index 2ec8376..0000000 --- a/.idea/libraries/Maven__commons_lang_commons_lang_2_6.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__junit_junit_4_11.xml b/.idea/libraries/Maven__junit_junit_4_11.xml deleted file mode 100644 index f33320d..0000000 --- a/.idea/libraries/Maven__junit_junit_4_11.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__log4j_log4j_1_2_17.xml b/.idea/libraries/Maven__log4j_log4j_1_2_17.xml deleted file mode 100644 index e383c1b..0000000 --- a/.idea/libraries/Maven__log4j_log4j_1_2_17.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_codehaus_jackson_jackson_core_asl_1_9_13.xml b/.idea/libraries/Maven__org_codehaus_jackson_jackson_core_asl_1_9_13.xml deleted file mode 100644 index 98eb549..0000000 --- a/.idea/libraries/Maven__org_codehaus_jackson_jackson_core_asl_1_9_13.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_codehaus_jackson_jackson_mapper_asl_1_9_13.xml b/.idea/libraries/Maven__org_codehaus_jackson_jackson_mapper_asl_1_9_13.xml deleted file mode 100644 index 77f3bad..0000000 --- a/.idea/libraries/Maven__org_codehaus_jackson_jackson_mapper_asl_1_9_13.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml b/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml deleted file mode 100644 index f58bbc1..0000000 --- a/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_mockito_mockito_all_1_9_5.xml b/.idea/libraries/Maven__org_mockito_mockito_all_1_9_5.xml deleted file mode 100644 index 7797878..0000000 --- a/.idea/libraries/Maven__org_mockito_mockito_all_1_9_5.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_nanohttpd_nanohttpd_2_3_0.xml b/.idea/libraries/Maven__org_nanohttpd_nanohttpd_2_3_0.xml deleted file mode 100644 index 6146614..0000000 --- a/.idea/libraries/Maven__org_nanohttpd_nanohttpd_2_3_0.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_slf4j_slf4j_api_1_7_6.xml b/.idea/libraries/Maven__org_slf4j_slf4j_api_1_7_6.xml deleted file mode 100644 index 65280d3..0000000 --- a/.idea/libraries/Maven__org_slf4j_slf4j_api_1_7_6.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_slf4j_slf4j_log4j12_1_7_21.xml b/.idea/libraries/Maven__org_slf4j_slf4j_log4j12_1_7_21.xml deleted file mode 100644 index 5ca45bb..0000000 --- a/.idea/libraries/Maven__org_slf4j_slf4j_log4j12_1_7_21.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_yaml_snakeyaml_1_13.xml b/.idea/libraries/Maven__org_yaml_snakeyaml_1_13.xml deleted file mode 100644 index 1852ff0..0000000 --- a/.idea/libraries/Maven__org_yaml_snakeyaml_1_13.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 6b28912..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index b774bac..91e8c91 100644 --- a/pom.xml +++ b/pom.xml @@ -39,14 +39,14 @@ com.appdynamics machine-agent 3.7.11 - + provided log4j log4j 1.2.17 - + provided diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index 84c4c13..88e795e 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -31,7 +31,6 @@ public class KafkaMonitorTask implements AMonitorTaskRunnable { private MonitorContextConfiguration configuration; private Map kafkaServer; private MetricWriteHelper metricWriteHelper; - private String metricPrefix; private String displayName; private JMXConnector jmxConnection; private JMXConnectionAdapter jmxAdapter; @@ -42,7 +41,6 @@ public class KafkaMonitorTask implements AMonitorTaskRunnable { public KafkaMonitorTask(TasksExecutionServiceProvider serviceProvider, MonitorContextConfiguration configuration, Map kafkaServer) { this.configuration = configuration; this.kafkaServer = kafkaServer; - this.metricPrefix = configuration.getMetricPrefix() + Constants.METRIC_SEPARATOR + kafkaServer.get(Constants.DISPLAY_NAME); this.metricWriteHelper = serviceProvider.getMetricWriteHelper(); this.displayName = (String) kafkaServer.get(Constants.DISPLAY_NAME); } @@ -66,7 +64,7 @@ private BigDecimal populateAndPrintMetrics() { logger.debug("JMX Connection is open"); List> mbeansFromConfig = (List>) configuration.getConfigYml().get(Constants.MBEANS); for (Map mbeanFromConfig : mbeansFromConfig) { - DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( jmxAdapter, jmxConnection, mbeanFromConfig, displayName,metricWriteHelper, metricPrefix, phaser); + DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( configuration, jmxAdapter, jmxConnection, mbeanFromConfig, displayName,metricWriteHelper, phaser); domainMetricsProcessor.populateMetricsForMBean(); logger.debug("Registering phaser for " + displayName); } diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index 97f9493..7efd89f 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -10,6 +10,7 @@ import com.appdynamics.extensions.MetricWriteHelper; +import com.appdynamics.extensions.conf.MonitorContextConfiguration; import com.appdynamics.extensions.kafka.JMXConnectionAdapter; import com.appdynamics.extensions.kafka.utils.Constants; import com.appdynamics.extensions.metrics.Metric; @@ -36,12 +37,13 @@ public class DomainMetricsProcessor { private Map mbeanFromConfig; private String displayName; - public DomainMetricsProcessor( JMXConnectionAdapter jmxAdapter, JMXConnector jmxConnection, Map mbeanFromConfig, String displayName, MetricWriteHelper metricWriteHelper, String metricPrefix, Phaser phaser) { + public DomainMetricsProcessor(MonitorContextConfiguration configuration, JMXConnectionAdapter jmxAdapter, JMXConnector jmxConnection, Map mbeanFromConfig, String displayName, MetricWriteHelper metricWriteHelper, Phaser phaser) { this.jmxAdapter = jmxAdapter; this.jmxConnection = jmxConnection; this.metricWriteHelper = metricWriteHelper; - this.metricPrefix = metricPrefix; - this.phaser = phaser;this.phaser.register(); + this.metricPrefix = configuration.getMetricPrefix() + Constants.METRIC_SEPARATOR + displayName; + this.phaser = phaser; + this.phaser.register(); this.mbeanFromConfig = mbeanFromConfig; this.displayName = displayName; } @@ -72,7 +74,7 @@ public void populateMetricsForMBean() { } @SuppressWarnings("unchecked") - private List getNodeMetrics(JMXConnector jmxConnection, String objectName, Map metricProperties) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { + public List getNodeMetrics(JMXConnector jmxConnection, String objectName, Map metricProperties) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { List nodeMetrics = Lists.newArrayList(); Set objectInstances = this.jmxAdapter.queryMBeans(jmxConnection, ObjectName.getInstance(objectName)); for (ObjectInstance instance : objectInstances) { diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index 7a0ffb2..77f9052 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -41,14 +41,9 @@ numberOfThreads: 10 mbeans: #All MBeans which have attributes Count and MeanRate - - objectName: ["kafka.server:type=BrokerTopicMetrics,*", - "kafka.server:type=DelayedFetchMetrics,*", - "kafka.server:type=KafkaRequestHandlerPool,*", + - objectName: [ "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", - "kafka.server:type=ReplicaManager,name=IsrShrinksPerSec", - "kafka.server:type=SessionExpireListener,*", - "kafka.network:type=RequestMetrics,*", - "kafka.controller:type=ControllerStats,*" + ] metrics: Count: @@ -59,17 +54,17 @@ mbeans: #All MBeans which have attributes Value - - objectName: ["kafka.server:type=DelayedOperationPurgatory,*", - "kafka.server:type=KafkaServer,name=BrokerState", - "kafka.server:type=ReplicaFetcherManager,*", - "kafka.server:type=ReplicaManager,name=LeaderCount", - "kafka.server:type=ReplicaManager,name=PartitionCount", - "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions", - "kafka.network:type=Processor,*", - "kafka.network:type=RequestChannel,*", - "kafka.network:type=SocketServer,*" - ] - metrics: - Value: - alias: "Value" +# - objectName: ["kafka.server:type=DelayedOperationPurgatory,*", +# "kafka.server:type=KafkaServer,name=BrokerState", +# "kafka.server:type=ReplicaFetcherManager,*", +# "kafka.server:type=ReplicaManager,name=LeaderCount", +# "kafka.server:type=ReplicaManager,name=PartitionCount", +# "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions", +# "kafka.network:type=Processor,*", +# "kafka.network:type=RequestChannel,*", +# "kafka.network:type=SocketServer,*" +# ] +# metrics: +# Value: +# alias: "Value" diff --git a/src/test/java/com/appdynamics/extensions/kafka/DomainMetricsProcessorTest.java b/src/test/java/com/appdynamics/extensions/kafka/DomainMetricsProcessorTest.java index 5132c77..8e94a7b 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/DomainMetricsProcessorTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/DomainMetricsProcessorTest.java @@ -4,9 +4,16 @@ import com.appdynamics.extensions.MetricWriteHelper; import com.appdynamics.extensions.conf.MonitorContextConfiguration; import com.appdynamics.extensions.kafka.metrics.DomainMetricsProcessor; +import com.appdynamics.extensions.kafka.utils.Constants; +import com.appdynamics.extensions.metrics.Metric; +import com.appdynamics.extensions.util.PathResolver; import com.appdynamics.extensions.yml.YmlReader; +import com.google.common.base.Strings; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import com.google.common.collect.Sets; +import com.singularity.ee.agent.systemagent.api.AManagedMonitor; +import org.junit.Assert; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -20,54 +27,55 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.Phaser; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class DomainMetricsProcessorTest { - JMXConnector jmxConnector = mock(JMXConnector.class); JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); @Test - public void getNodeMetrics() throws IOException,IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException { - - ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); - AMonitorJob aMonitorJob = mock(AMonitorJob.class); - //pass config file name - MonitorContextConfiguration monitorContextConfiguration = new MonitorContextConfiguration("Kafka Monitor", "Custom Metrics|Kafka|", aMonitorJob); - monitorContextConfiguration.setConfigYml("src/test/resources/conf/conf.yml"); MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); + MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration("Kafka", "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), Mockito.mock(AMonitorJob.class)); + contextConfiguration.setConfigYml("/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/test/resources/conf/config_for_metrics.yml"); -// Map config = YmlReader.readFromFileAsMap(new File(this.getClass().getResource("/conf/config.yml").getFile())); + Map config = contextConfiguration.getConfigYml(); List mBeans = (List) config.get("mbeans"); Set objectInstances = Sets.newHashSet(); - objectInstances.add(new ObjectInstance("kafka.server:type=BrokerTopicMetrics,*", "test")); + objectInstances.add(new ObjectInstance("org.apache.kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", "test")); - Set attributes = Sets.newHashSet(); - attributes.add(new Attribute("Count", new Integer(100)); - attributes.add(new Attribute("Value", new Integer(200) ); + List attributes = Lists.newArrayList(); + attributes.add(new Attribute("Count", new Integer(100))); + attributes.add(new Attribute("Value", new Integer(200) )); List metricNames = Lists.newArrayList(); - metricNames.add("testmetric1"); - metricNames.add("testmetric2"); + metricNames.add("Count"); + metricNames.add("Value"); when(jmxConnectionAdapter.queryMBeans(eq(jmxConnector), Mockito.any(ObjectName.class))).thenReturn(objectInstances); when(jmxConnectionAdapter.getReadableAttributeNames(eq(jmxConnector), Mockito.any(ObjectInstance.class))).thenReturn(metricNames); when(jmxConnectionAdapter.getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), Mockito.any(String[] - .class))).thenReturn((List) eq(attributes)); - - DomainMetricsProcessor domainMetricsProcessor = new - - - - + .class))).thenReturn(attributes); + + Map server = Maps.newHashMap(); + server.put("host", "localhost"); + server.put("port", "9999"); + server.put("displayName", "TestServer1"); + + for(Map mBean : mBeans){ + Phaser phaser = new Phaser(); + phaser.register(); + Map metricProperties = (Map) mBean.get("metrics"); + DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor(contextConfiguration, jmxConnectionAdapter, + jmxConnector, mBean, server.get("displayName"), metricWriteHelper,phaser); + List metrics = domainMetricsProcessor.getNodeMetrics(jmxConnector, mBean.get("objectName").toString(),metricProperties ); + Assert.assertTrue(metrics.get(0).getMetricName().equals("Count")); + Assert.assertTrue(metrics.get(1).getMetricName().equals("Value")); + } } - - - - } diff --git a/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java b/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java index a885cb5..c93c5ba 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java @@ -15,7 +15,7 @@ public class KafkaMonitorTest { public void testKafkaMonitorExtension() throws TaskExecutionException { KafkaMonitor kafkaMonitor = new KafkaMonitor(); Map taskArgs = Maps.newHashMap(); - taskArgs.put(CONFIG_ARG, "src/test/resources/conf/config.yml"); + taskArgs.put(CONFIG_ARG, "/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/test/resources/conf/config.yml"); kafkaMonitor.execute(taskArgs, null); } diff --git a/src/test/resources/conf/config_for_metrics.yml b/src/test/resources/conf/config_for_metrics.yml new file mode 100644 index 0000000..38b64c2 --- /dev/null +++ b/src/test/resources/conf/config_for_metrics.yml @@ -0,0 +1,11 @@ +metricPrefix: "Custom Metrics|Kafka|" + +mbeans: + + - objectName: "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec" + metrics: + + Count: + alias: "Min Latency" + Value: + alias: "Max Latency" diff --git a/src/test/resources/config.yml b/src/test/resources/config.yml deleted file mode 100644 index e69de29..0000000 From a6489ba977d1035d895fb3afc2f1e87b9e53c3a7 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Thu, 12 Jul 2018 13:54:33 -0700 Subject: [PATCH 09/49] composite metrics --- pom.xml | 16 ++---- src/main/resources/conf/config.yml | 52 +++++++++++++------ .../kafka/DomainMetricsProcessorTest.java | 17 ++---- 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/pom.xml b/pom.xml index 91e8c91..fdb0c34 100644 --- a/pom.xml +++ b/pom.xml @@ -7,55 +7,48 @@ 1.0.3 jar kafka-monitoring-extension + http://maven.apache.org UTF-8 yyyy-MM-dd HH:mm:ss ${project.build.directory}/KafkaMonitor - com.appdynamics appd-exts-commons 2.1.0 - provided - commons-lang commons-lang 2.6 - commons-io commons-io 2.4 provided - com.appdynamics machine-agent 3.7.11 provided - log4j log4j 1.2.17 provided - org.slf4j slf4j-log4j12 1.7.21 provided - org.mockito mockito-all @@ -131,11 +124,12 @@ - + + - + @@ -184,4 +178,4 @@ scm:git:https://github.com/Appdynamics/kafka-monitoring-extension.git - + \ No newline at end of file diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index 77f9052..78cda3f 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -41,30 +41,52 @@ numberOfThreads: 10 mbeans: #All MBeans which have attributes Count and MeanRate - - objectName: [ + - objectName: ["kafka.server:type=BrokerTopicMetrics,*", + "kafka.server:type=DelayedFetchMetrics,*", + "kafka.server:type=KafkaRequestHandlerPool,*", "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", - + "kafka.server:type=ReplicaManager,name=IsrShrinksPerSec", + "kafka.server:type=SessionExpireListener,*", + "kafka.network:type=RequestMetrics,*", + "kafka.controller:type=ControllerStats,*" ] metrics: Count: alias: "Count" + multiplier: + delta: + aggregationType: + timeRollUpType: + clusterRollUpType: + MeanRate: alias: "MeanRate" #All MBeans which have attributes Value -# - objectName: ["kafka.server:type=DelayedOperationPurgatory,*", -# "kafka.server:type=KafkaServer,name=BrokerState", -# "kafka.server:type=ReplicaFetcherManager,*", -# "kafka.server:type=ReplicaManager,name=LeaderCount", -# "kafka.server:type=ReplicaManager,name=PartitionCount", -# "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions", -# "kafka.network:type=Processor,*", -# "kafka.network:type=RequestChannel,*", -# "kafka.network:type=SocketServer,*" -# ] -# metrics: -# Value: -# alias: "Value" + - objectName: ["kafka.server:type=DelayedOperationPurgatory,*", + "kafka.server:type=KafkaServer,name=BrokerState", + "kafka.server:type=ReplicaFetcherManager,*", + "kafka.server:type=ReplicaManager,name=LeaderCount", + "kafka.server:type=ReplicaManager,name=PartitionCount", + "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions", + "kafka.network:type=Processor,*", + "kafka.network:type=RequestChannel,*", + "kafka.network:type=SocketServer,*" + ] + metrics: + - Value: "Value" + +#JVM Metrics + - objectName: ["java.lang:type=Memory"] + metrics: + HeapMemoryUsage.committed: "Heap Memory Usage | Committed" + HeapMemoryUsage.max: "Heap Memory Usage | Max" + HeapMemoryUsage.used: "Heap Memory Usage | Used" + NonHeapMemoryUsage.committed: "Heap Memory Usage | Committed" + NonHeapMemoryUsage.used: "Heap Memory Usage | Used" + + + diff --git a/src/test/java/com/appdynamics/extensions/kafka/DomainMetricsProcessorTest.java b/src/test/java/com/appdynamics/extensions/kafka/DomainMetricsProcessorTest.java index 8e94a7b..27afe18 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/DomainMetricsProcessorTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/DomainMetricsProcessorTest.java @@ -4,26 +4,18 @@ import com.appdynamics.extensions.MetricWriteHelper; import com.appdynamics.extensions.conf.MonitorContextConfiguration; import com.appdynamics.extensions.kafka.metrics.DomainMetricsProcessor; -import com.appdynamics.extensions.kafka.utils.Constants; import com.appdynamics.extensions.metrics.Metric; import com.appdynamics.extensions.util.PathResolver; -import com.appdynamics.extensions.yml.YmlReader; -import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.singularity.ee.agent.systemagent.api.AManagedMonitor; import org.junit.Assert; import org.junit.Test; -import org.mockito.ArgumentCaptor; import org.mockito.Mockito; - import javax.management.*; import javax.management.remote.JMXConnector; - -import java.io.File; import java.io.IOException; -import java.math.BigDecimal; import java.util.List; import java.util.Map; import java.util.Set; @@ -38,6 +30,7 @@ public class DomainMetricsProcessorTest { JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); @Test + @SuppressWarnings("unchecked") public void getNodeMetrics() throws IOException,IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException { MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration("Kafka", "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), Mockito.mock(AMonitorJob.class)); @@ -49,8 +42,8 @@ public void getNodeMetrics() throws IOException,IntrospectionException,Reflectio objectInstances.add(new ObjectInstance("org.apache.kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", "test")); List attributes = Lists.newArrayList(); - attributes.add(new Attribute("Count", new Integer(100))); - attributes.add(new Attribute("Value", new Integer(200) )); + attributes.add(new Attribute("Count", 100)); + attributes.add(new Attribute("Value", 200 )); List metricNames = Lists.newArrayList(); metricNames.add("Count"); @@ -73,8 +66,8 @@ public void getNodeMetrics() throws IOException,IntrospectionException,Reflectio DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor(contextConfiguration, jmxConnectionAdapter, jmxConnector, mBean, server.get("displayName"), metricWriteHelper,phaser); List metrics = domainMetricsProcessor.getNodeMetrics(jmxConnector, mBean.get("objectName").toString(),metricProperties ); - Assert.assertTrue(metrics.get(0).getMetricName().equals("Count")); - Assert.assertTrue(metrics.get(1).getMetricName().equals("Value")); + Assert.assertEquals(metrics.get(0).getMetricName(),"Count"); + Assert.assertEquals(metrics.get(1).getMetricName(), "Value"); } } From fbdda3571cacf7657385cfeab99d439f089c96d8 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Thu, 12 Jul 2018 17:08:51 -0700 Subject: [PATCH 10/49] config files --- src/main/resources/conf/config.yml | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index 78cda3f..50d05ee 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -1,7 +1,7 @@ ### ANY CHANGES TO THIS FILE DOES NOT REQUIRE A RESTART ### #This will create this metric in all the tiers, under this path -metricPrefix: Custom Metrics|Kafka +metricPrefix: "Server|Kafka" #This will create it in specific Tier/Component. Make sure to replace with the appropriate one from your environment. #To find the in your environment, please follow the screenshot https://docs.appdynamics.com/display/PRO42/Build+a+Monitoring+Extension+Using+Java @@ -53,15 +53,22 @@ mbeans: metrics: Count: alias: "Count" - multiplier: - delta: - aggregationType: - timeRollUpType: - clusterRollUpType: + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" MeanRate: alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + #All MBeans which have attributes Value @@ -76,7 +83,7 @@ mbeans: "kafka.network:type=SocketServer,*" ] metrics: - - Value: "Value" + Value: "Value" #JVM Metrics - objectName: ["java.lang:type=Memory"] From 5960ff096b711a97fcb37116f2b883acb0fd32a5 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Mon, 16 Jul 2018 13:57:58 -0700 Subject: [PATCH 11/49] Custom SSL Socket Factory --- .../kafka/JMXConnectionAdapter.java | 23 ++++++-- .../extensions/kafka/KafkaMonitor.java | 18 ++++-- .../extensions/kafka/KafkaMonitorTask.java | 19 +++++- src/main/resources/conf/config.yml | 22 ++++--- .../DomainMetricsProcessorTest.java | 58 +++++++++++++++++-- ...l => config_for_non_composite_metrics.yml} | 0 6 files changed, 115 insertions(+), 25 deletions(-) rename src/test/java/com/appdynamics/extensions/kafka/{ => metrics}/DomainMetricsProcessorTest.java (51%) rename src/test/resources/conf/{config_for_metrics.yml => config_for_non_composite_metrics.yml} (100%) diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index 53bd2d1..8b27cff 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -25,7 +25,9 @@ import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; +import javax.management.remote.rmi.RMIConnectorServer; import javax.rmi.ssl.SslRMIClientSocketFactory; +import javax.rmi.ssl.SslRMIServerSocketFactory; import java.io.IOException; import java.net.MalformedURLException; import java.util.HashMap; @@ -49,16 +51,27 @@ static JMXConnectionAdapter create(Map requestMap) throws Malfor return new JMXConnectionAdapter(requestMap); } - JMXConnector open() throws IOException { + JMXConnector open(boolean useDefaultSslFactory ) throws IOException { JMXConnector jmxConnector; final Map env = new HashMap(); - if (!Strings.isNullOrEmpty(username)) { - env.put(JMXConnector.CREDENTIALS, new String[]{username, password}); - jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); +// + if(useDefaultSslFactory){ + SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory(); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf); + + } + else if(!useDefaultSslFactory){ + CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory.createSocketFactory()); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory.createSocketFactory()); } - else { jmxConnector = JMXConnectorFactory.connect(serviceUrl); } + + if (!Strings.isNullOrEmpty(username)) { + env.put(JMXConnector.CREDENTIALS, new String[]{username, password}); + } + jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); if (jmxConnector == null) { throw new IOException("Unable to connect to Mbean server"); } return jmxConnector; } diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index 80cb996..fbf7e9e 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -31,13 +31,20 @@ public class KafkaMonitor extends ABaseMonitor { private static final Logger logger = LoggerFactory.getLogger(KafkaMonitor.class); @Override - protected void initializeMoreStuff(Map args){ } + protected void initializeMoreStuff(Map args){ + Map connectionMapFromConfig = new HashMap(); + connectionMapFromConfig = (Map) this.getContextConfiguration().getConfigYml().get("connection"); + Object useSsl = (connectionMapFromConfig.get("useSsl")); - @Override - protected String getDefaultMetricPrefix() { - return DEFAULT_METRIC_PREFIX; + if(Boolean.valueOf(useSsl.toString())) { + System.setProperty("javax.net.ssl.trustStore", connectionMapFromConfig.get("sslTrustStorePath")); + System.setProperty("javax.net.ssl.trustStorePassword", connectionMapFromConfig.get("sslTrustStorePassword")); + } } + @Override + protected String getDefaultMetricPrefix() { return DEFAULT_METRIC_PREFIX; } + @Override public String getMonitorName() { return "Kafka Monitor"; @@ -45,10 +52,11 @@ public String getMonitorName() { @Override protected void doRun(TasksExecutionServiceProvider tasksExecutionServiceProvider) { + List> kafkaServers = (List>) this.getContextConfiguration().getConfigYml().get(Constants.SERVERS); for (Map kafkaServer : kafkaServers) { KafkaMonitorTask task = new KafkaMonitorTask(tasksExecutionServiceProvider, this.getContextConfiguration(), kafkaServer); - AssertUtils.assertNotNull(kafkaServer.get(Constants.DISPLAY_NAME), "The displayName can not be null"); +// AssertUtils.assertNotNull(kafkaServer.get(Constants.DISPLAY_NAME), "The displayName can not be null"); tasksExecutionServiceProvider.submit(kafkaServer.get(Constants.DISPLAY_NAME), task); } } diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index 88e795e..eede088 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -58,10 +58,16 @@ private BigDecimal populateAndPrintMetrics() { try{ phaser = new Phaser(); Map requestMap; + Map connectionMap; + requestMap = buildRequestMap(); jmxAdapter = JMXConnectionAdapter.create(requestMap); - jmxConnection = jmxAdapter.open(); + connectionMap = getConnectionParameters(); + Object flag1 = connectionMap.get("useDefaultSslConnectionFactory"); + boolean flag = Boolean.valueOf(flag1.toString()); + jmxConnection = jmxAdapter.open(flag); logger.debug("JMX Connection is open"); + List> mbeansFromConfig = (List>) configuration.getConfigYml().get(Constants.MBEANS); for (Map mbeanFromConfig : mbeansFromConfig) { DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( configuration, jmxAdapter, jmxConnection, mbeanFromConfig, displayName,metricWriteHelper, phaser); @@ -70,6 +76,7 @@ private BigDecimal populateAndPrintMetrics() { } } catch (Exception e) { logger.error("Error while opening JMX connection {}{}" ,this.kafkaServer.get(Constants.DISPLAY_NAME), e.getMessage()); + e.printStackTrace(); } finally { try { jmxAdapter.close(jmxConnection); @@ -91,9 +98,11 @@ private Map buildRequestMap() { requestMap.put("username", kafkaServer.get(Constants.USERNAME)); requestMap.put("password", getPassword(kafkaServer)); } + return requestMap; } + private String getPassword(Map server) { String password = server.get(Constants.PASSWORD); if(Strings.isNullOrEmpty(password)){ @@ -114,6 +123,14 @@ private String getPassword(Map server) { } return ""; } + + private Map getConnectionParameters(){ + Map connectionMap = new HashMap<>(); + connectionMap = (Map) configuration.getConfigYml().get("connection"); + return connectionMap; + } + + } diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index 50d05ee..cbabb22 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -15,21 +15,25 @@ servers: password: "" #encryptedPassword: #encryptionKey: - useSSl: false displayName: "Local Kafka Server" #displayName is a REQUIRED field for level metrics. connection: + useSsl: true socketTimeout: 3000 connectTimeout: 1000 - sslProtocols: ["TLSV1.2"] - sslCertCheckEnabled: true - sslVerifyHostname: true - - sslCipherSuites: [] - sslTrustStorePath: "" - sslTrustStorePassword: "" + sslProtocols: "TLSv1.2" + sslCertCheckEnabled: false + sslVerifyHostname: false + + sslCipherSuites: "" + sslTrustStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks" + sslTrustStorePassword: "test1234" + sslKeyStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.keystore.jks" + sslKeyStorePassword: "test1234" sslTrustStoreEncryptedPassword: "" - useDefaultSslConnectionFactory: + useDefaultSslConnectionFactory: false + + #nsme of flag needs review # number of concurrent tasks. # This doesn't need to be changed unless many instances are configured diff --git a/src/test/java/com/appdynamics/extensions/kafka/DomainMetricsProcessorTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java similarity index 51% rename from src/test/java/com/appdynamics/extensions/kafka/DomainMetricsProcessorTest.java rename to src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java index 27afe18..b724373 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/DomainMetricsProcessorTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java @@ -1,8 +1,9 @@ -package com.appdynamics.extensions.kafka; +package com.appdynamics.extensions.kafka.metrics; import com.appdynamics.extensions.AMonitorJob; import com.appdynamics.extensions.MetricWriteHelper; import com.appdynamics.extensions.conf.MonitorContextConfiguration; +import com.appdynamics.extensions.kafka.JMXConnectionAdapter; import com.appdynamics.extensions.kafka.metrics.DomainMetricsProcessor; import com.appdynamics.extensions.metrics.Metric; import com.appdynamics.extensions.util.PathResolver; @@ -28,14 +29,59 @@ public class DomainMetricsProcessorTest { JMXConnector jmxConnector = mock(JMXConnector.class); JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); + MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); + MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration("Kafka", "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), Mockito.mock(AMonitorJob.class)); +// contextConfiguration.setConfigYml("/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/test/resources/conf/config_for_non_composite_metrics.yml"); + + @Test @SuppressWarnings("unchecked") - public void getNodeMetrics() throws IOException,IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException { - MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); - MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration("Kafka", "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), Mockito.mock(AMonitorJob.class)); - contextConfiguration.setConfigYml("/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/test/resources/conf/config_for_metrics.yml"); + public void getNodeMetricsForNonCompositeMetrics() throws IOException,IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException { + + contextConfiguration.setConfigYml("/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/test/resources/conf/config_for_non_composite_metrics.yml"); + Map config = contextConfiguration.getConfigYml(); + List mBeans = (List) config.get("mbeans"); + Set objectInstances = Sets.newHashSet(); + objectInstances.add(new ObjectInstance("org.apache.kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", "test")); + + List attributes = Lists.newArrayList(); + attributes.add(new Attribute("Count", 100)); + attributes.add(new Attribute("Value", 200 )); + + List metricNames = Lists.newArrayList(); + metricNames.add("Count"); + metricNames.add("Value"); + + when(jmxConnectionAdapter.queryMBeans(eq(jmxConnector), Mockito.any(ObjectName.class))).thenReturn(objectInstances); + when(jmxConnectionAdapter.getReadableAttributeNames(eq(jmxConnector), Mockito.any(ObjectInstance.class))).thenReturn(metricNames); + when(jmxConnectionAdapter.getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), Mockito.any(String[] + .class))).thenReturn(attributes); + + Map server = Maps.newHashMap(); + server.put("host", "localhost"); + server.put("port", "9999"); + server.put("displayName", "TestServer1"); + + for(Map mBean : mBeans){ + Phaser phaser = new Phaser(); + phaser.register(); + Map metricProperties = (Map) mBean.get("metrics"); + DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor(contextConfiguration, jmxConnectionAdapter, + jmxConnector, mBean, server.get("displayName"), metricWriteHelper,phaser); + List metrics = domainMetricsProcessor.getNodeMetrics(jmxConnector, mBean.get("objectName").toString(),metricProperties ); + Assert.assertEquals(metrics.get(0).getMetricName(),"Count"); + Assert.assertEquals(metrics.get(1).getMetricName(), "Value"); + } + + } + + + @Test + public void getNodeMetricsForCompositeMetrics() throws MalformedObjectNameException, ReflectionException, InstanceNotFoundException,IntrospectionException,IOException{ + + contextConfiguration.setConfigYml("/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/test/resources/conf/config_for_non_composite_metrics.yml"); Map config = contextConfiguration.getConfigYml(); List mBeans = (List) config.get("mbeans"); Set objectInstances = Sets.newHashSet(); @@ -70,5 +116,7 @@ public void getNodeMetrics() throws IOException,IntrospectionException,Reflectio Assert.assertEquals(metrics.get(1).getMetricName(), "Value"); } + + } } diff --git a/src/test/resources/conf/config_for_metrics.yml b/src/test/resources/conf/config_for_non_composite_metrics.yml similarity index 100% rename from src/test/resources/conf/config_for_metrics.yml rename to src/test/resources/conf/config_for_non_composite_metrics.yml From 8663f28a28fa4d8e52c6e9b556779db24e025ced Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Mon, 16 Jul 2018 14:01:09 -0700 Subject: [PATCH 12/49] Custom SSL Socket Factory --- .../kafka/CustomSSLSocketFactory.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java diff --git a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java new file mode 100644 index 0000000..fa7d7aa --- /dev/null +++ b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java @@ -0,0 +1,50 @@ +package com.appdynamics.extensions.kafka; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; +import javax.rmi.ssl.SslRMIClientSocketFactory; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +public class CustomSSLSocketFactory extends SslRMIClientSocketFactory { + + private static final Logger logger = LoggerFactory.getLogger(CustomSSLSocketFactory.class); + + + public SSLSocketFactory createSocketFactory() throws IOException { + String truststore = "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks"; + char truststorepass[] = "test1234".toCharArray(); + SSLSocketFactory ssf = null; + try { + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(new FileInputStream(truststore), truststorepass); + TrustManagerFactory tmf = + TrustManagerFactory.getInstance("SunX509"); + tmf.init(ks); + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, tmf.getTrustManagers(), null); + ssf = ctx.getSocketFactory(); + + }catch(NoSuchAlgorithmException exception){ + logger.debug("No Such algorithm"); + } catch (CertificateException e) { + e.printStackTrace(); + } catch (KeyStoreException e) { + e.printStackTrace(); + } catch (KeyManagementException e) { + e.printStackTrace(); + } + return ssf; + } +} From c87cdf1eb312a53afa54cf1ec02d36a59d5352f5 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Mon, 16 Jul 2018 14:06:17 -0700 Subject: [PATCH 13/49] removing hardcoded values --- .../extensions/kafka/CustomSSLSocketFactory.java | 4 ++-- src/main/resources/conf/config.yml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java index fa7d7aa..a91bb3d 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java +++ b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java @@ -23,8 +23,8 @@ public class CustomSSLSocketFactory extends SslRMIClientSocketFactory { public SSLSocketFactory createSocketFactory() throws IOException { - String truststore = "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks"; - char truststorepass[] = "test1234".toCharArray(); + String truststore = ""; + char truststorepass[] = "".toCharArray(); SSLSocketFactory ssf = null; try { KeyStore ks = KeyStore.getInstance("JKS"); diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index cbabb22..9760b1d 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -26,10 +26,10 @@ connection: sslVerifyHostname: false sslCipherSuites: "" - sslTrustStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks" - sslTrustStorePassword: "test1234" - sslKeyStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.keystore.jks" - sslKeyStorePassword: "test1234" + sslTrustStorePath: "" + sslTrustStorePassword: "" + sslKeyStorePath: "" + sslKeyStorePassword: "" sslTrustStoreEncryptedPassword: "" useDefaultSslConnectionFactory: false From db9bab18ccb7dc42277f9a37dd9e6f7cf4e060f1 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Wed, 18 Jul 2018 13:16:38 -0700 Subject: [PATCH 14/49] ssl test and sslenabled flags --- LICENSE.txt | 211 +----------- NOTICE.txt | 4 + Notice.txt | 61 ---- README.md | 4 +- .../kafka/CustomSSLSocketFactory.java | 34 +- .../kafka/JMXConnectionAdapter.java | 22 +- .../extensions/kafka/KafkaMonitor.java | 18 +- .../extensions/kafka/KafkaMonitorTask.java | 18 +- .../kafka/metrics/DomainMetricsProcessor.java | 27 +- src/main/resources/conf/config.yml | 319 ++++++++++++++---- .../extensions/kafka/KafkaMonitorTest.java | 66 ++++ .../metrics/CustomSSLSocketFactoryTest.java | 45 +++ src/test/resources/conf/config.yml | 42 +++ .../conf/config_for_composite_metrics.yml | 17 + 14 files changed, 496 insertions(+), 392 deletions(-) mode change 100644 => 100755 LICENSE.txt create mode 100755 NOTICE.txt delete mode 100644 Notice.txt create mode 100644 src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java create mode 100755 src/test/resources/conf/config.yml create mode 100755 src/test/resources/conf/config_for_composite_metrics.yml diff --git a/LICENSE.txt b/LICENSE.txt old mode 100644 new mode 100755 index fe4e910..88e8bf8 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,204 +1,13 @@ +Copyright 2018 AppDynamics LLC and its affiliates - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +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 - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - - Copyright © 2016 AppDynamics, Inc. - - 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. + 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. diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100755 index 0000000..5b424c3 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,4 @@ +Notice and Disclaimer + +All Extensions published by AppDynamics are governed by the Apache License v2 and are excluded from the definition of covered software under any agreement between AppDynamics and the User governing AppDynamics Pro Edition, Test & Dev Edition, or any other Editions. + diff --git a/Notice.txt b/Notice.txt deleted file mode 100644 index faf5c89..0000000 --- a/Notice.txt +++ /dev/null @@ -1,61 +0,0 @@ - - -commons-lang -============================= - -Apache Commons Lang -Copyright 2001-2016 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - -This product includes software from the Spring Framework, -under the Apache License 2.0 (see: StringUtils.containsWhitespace()) - - -commons-io -============================= - -Apache Commons IO -Copyright 2002-2016 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - - -slf4j-log4j12 -============================= - -Copyright (c) 2004-2007 QOS.ch -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -log4j -============================= - -This product includes software developed by -The Apache Software Foundation (http://www.apache.org/). - -This product includes source code based on Sun -Microsystems' book titled "Java Nativer Interface: -Programmer's Guide and Specification" and freely available -to the public at http://java.sun.com/docs/books/jni. \ No newline at end of file diff --git a/README.md b/README.md index 0ba7ac2..713c3bc 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ AppDynamics Monitoring Extension for use with Kafka ============================== -An AppDynamics extension to be used with a stand alone Java machine agent to provide metrics for Apache Kafka +An AppDynamics extension to be used with a stand alone machine agent to provide metrics for Apache Kafka ## Use Case ## @@ -32,7 +32,7 @@ Before configuring the extension, please make sure to run the below steps to che If telnet works, it confirm the access to the Kafka server. -2. Start jconsole. Jconsole comes as a utility with installed jdk. After giving the correct host and port , check if Kafka +2. Start jconsole. Jconsole comes as a utility with installed jdk. After giving the correct host and port, check if Kafka mbean shows up. 3. It is a good idea to match the mbean configuration in the config.yml against the jconsole. JMX is case sensitive so make diff --git a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java index a91bb3d..8f074d5 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java +++ b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java @@ -2,39 +2,33 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManagerFactory; import javax.rmi.ssl.SslRMIClientSocketFactory; -import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.net.Socket; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; +import java.security.*; import java.security.cert.CertificateException; public class CustomSSLSocketFactory extends SslRMIClientSocketFactory { private static final Logger logger = LoggerFactory.getLogger(CustomSSLSocketFactory.class); - public SSLSocketFactory createSocketFactory() throws IOException { - String truststore = ""; - char truststorepass[] = "".toCharArray(); + + String truststore = "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks"; + char truststorepass[] = "test1234".toCharArray(); SSLSocketFactory ssf = null; try { - KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(new FileInputStream(truststore), truststorepass); - TrustManagerFactory tmf = - TrustManagerFactory.getInstance("SunX509"); - tmf.init(ks); - SSLContext ctx = SSLContext.getInstance("TLS"); - ctx.init(null, tmf.getTrustManagers(), null); - ssf = ctx.getSocketFactory(); + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(new FileInputStream(truststore), truststorepass); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, tmf.getTrustManagers(), new SecureRandom()); + ssf = ctx.getSocketFactory(); + return ssf; }catch(NoSuchAlgorithmException exception){ logger.debug("No Such algorithm"); @@ -45,6 +39,8 @@ public SSLSocketFactory createSocketFactory() throws IOException { } catch (KeyManagementException e) { e.printStackTrace(); } - return ssf; + return null; } + + } diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index 8b27cff..d81dc7d 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -27,7 +27,6 @@ import javax.management.remote.JMXServiceURL; import javax.management.remote.rmi.RMIConnectorServer; import javax.rmi.ssl.SslRMIClientSocketFactory; -import javax.rmi.ssl.SslRMIServerSocketFactory; import java.io.IOException; import java.net.MalformedURLException; import java.util.HashMap; @@ -51,23 +50,20 @@ static JMXConnectionAdapter create(Map requestMap) throws Malfor return new JMXConnectionAdapter(requestMap); } - JMXConnector open(boolean useDefaultSslFactory ) throws IOException { + JMXConnector open(boolean useDefaultSslFactory, boolean useSsl) throws IOException { JMXConnector jmxConnector; final Map env = new HashMap(); -// - if(useDefaultSslFactory){ - SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory(); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf); + if(useSsl) { + if (useDefaultSslFactory) { //check if null:TODO + SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory(); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf); + } else if (!useDefaultSslFactory) { + CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory.createSocketFactory()); + } } - else if(!useDefaultSslFactory){ - CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory.createSocketFactory()); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory.createSocketFactory()); - } - - if (!Strings.isNullOrEmpty(username)) { env.put(JMXConnector.CREDENTIALS, new String[]{username, password}); } diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index fbf7e9e..5a62ba5 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -17,32 +17,28 @@ import org.apache.log4j.ConsoleAppender; import org.apache.log4j.Level; import org.apache.log4j.PatternLayout; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.OutputStreamWriter; import java.util.HashMap; import java.util.List; import java.util.Map; - - import static com.appdynamics.extensions.kafka.utils.Constants.DEFAULT_METRIC_PREFIX; public class KafkaMonitor extends ABaseMonitor { - private static final Logger logger = LoggerFactory.getLogger(KafkaMonitor.class); @Override - protected void initializeMoreStuff(Map args){ - Map connectionMapFromConfig = new HashMap(); + @SuppressWarnings("unchecked") + protected void initializeMoreStuff(Map args) { + Map connectionMapFromConfig; connectionMapFromConfig = (Map) this.getContextConfiguration().getConfigYml().get("connection"); Object useSsl = (connectionMapFromConfig.get("useSsl")); - - if(Boolean.valueOf(useSsl.toString())) { + if (Boolean.valueOf(useSsl.toString())) { System.setProperty("javax.net.ssl.trustStore", connectionMapFromConfig.get("sslTrustStorePath")); System.setProperty("javax.net.ssl.trustStorePassword", connectionMapFromConfig.get("sslTrustStorePassword")); } } @Override + @SuppressWarnings("unchecked") protected String getDefaultMetricPrefix() { return DEFAULT_METRIC_PREFIX; } @Override @@ -51,12 +47,12 @@ public String getMonitorName() { } @Override + @SuppressWarnings("unchecked") protected void doRun(TasksExecutionServiceProvider tasksExecutionServiceProvider) { - List> kafkaServers = (List>) this.getContextConfiguration().getConfigYml().get(Constants.SERVERS); for (Map kafkaServer : kafkaServers) { KafkaMonitorTask task = new KafkaMonitorTask(tasksExecutionServiceProvider, this.getContextConfiguration(), kafkaServer); -// AssertUtils.assertNotNull(kafkaServer.get(Constants.DISPLAY_NAME), "The displayName can not be null"); + AssertUtils.assertNotNull(kafkaServer.get(Constants.DISPLAY_NAME), "The displayName can not be null"); tasksExecutionServiceProvider.submit(kafkaServer.get(Constants.DISPLAY_NAME), task); } } diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index eede088..2ca888f 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -36,9 +36,9 @@ public class KafkaMonitorTask implements AMonitorTaskRunnable { private JMXConnectionAdapter jmxAdapter; private static final BigDecimal ERROR_VALUE = BigDecimal.ZERO; private static final BigDecimal SUCCESS_VALUE = BigDecimal.ONE; - private Phaser phaser; - public KafkaMonitorTask(TasksExecutionServiceProvider serviceProvider, MonitorContextConfiguration configuration, Map kafkaServer) { + + KafkaMonitorTask(TasksExecutionServiceProvider serviceProvider, MonitorContextConfiguration configuration, Map kafkaServer) { this.configuration = configuration; this.kafkaServer = kafkaServer; this.metricWriteHelper = serviceProvider.getMetricWriteHelper(); @@ -54,7 +54,9 @@ public void run() { logger.info("Completed the Kafka Monitoring task"); } + @SuppressWarnings("unchecked") private BigDecimal populateAndPrintMetrics() { + Phaser phaser; try{ phaser = new Phaser(); Map requestMap; @@ -63,9 +65,9 @@ private BigDecimal populateAndPrintMetrics() { requestMap = buildRequestMap(); jmxAdapter = JMXConnectionAdapter.create(requestMap); connectionMap = getConnectionParameters(); - Object flag1 = connectionMap.get("useDefaultSslConnectionFactory"); - boolean flag = Boolean.valueOf(flag1.toString()); - jmxConnection = jmxAdapter.open(flag); + Object useDefaultSslConnectionFactory = connectionMap.get("useDefaultSslConnectionFactory"); + Object useSsl = connectionMap.get("useSsl"); + jmxConnection = jmxAdapter.open(Boolean.valueOf(useDefaultSslConnectionFactory.toString()), Boolean.valueOf(useSsl.toString())); logger.debug("JMX Connection is open"); List> mbeansFromConfig = (List>) configuration.getConfigYml().get(Constants.MBEANS); @@ -89,8 +91,9 @@ private BigDecimal populateAndPrintMetrics() { return SUCCESS_VALUE; } + @SuppressWarnings("unchecked") private Map buildRequestMap() { - Map requestMap = new HashMap(); + Map requestMap = new HashMap<>(); requestMap.put("host", kafkaServer.get(Constants.HOST)); requestMap.put("port", kafkaServer.get(Constants.PORT)); requestMap.put("displayName", kafkaServer.get(Constants.DISPLAY_NAME)); @@ -98,11 +101,10 @@ private Map buildRequestMap() { requestMap.put("username", kafkaServer.get(Constants.USERNAME)); requestMap.put("password", getPassword(kafkaServer)); } - return requestMap; } - + @SuppressWarnings("unchecked") private String getPassword(Map server) { String password = server.get(Constants.PASSWORD); if(Strings.isNullOrEmpty(password)){ diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index 7efd89f..938d441 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -55,15 +55,15 @@ public void populateMetricsForMBean() { Map metricProperties = (Map) this.mbeanFromConfig.get(Constants.METRICS); logger.debug(String.format("Processing metrics section from the conf file")); logger.debug("Size of metric section {}",metricProperties.size()); - List mbeanNames = (List) this.mbeanFromConfig.get(Constants.OBJECTNAME); - logger.debug("Size of mbean section {}",mbeanNames.size()); - for (String mbeanName : mbeanNames) { - logger.debug(String.format("Processing mbean %s from the conf file", mbeanName)); - List finalMetricList = getNodeMetrics(jmxConnection,mbeanName,metricProperties); - finalMetricList.add(new Metric("HeartBeat", String.valueOf(BigInteger.ONE), metricPrefix + "|HeartBeat", "AVG", "AVG", "IND")); - logger.debug("Printing metrics for server {}", mbeanName); - metricWriteHelper.transformAndPrintMetrics(finalMetricList); - } + String mbeanName = (String) this.mbeanFromConfig.get(Constants.OBJECTNAME); + + + logger.debug(String.format("Processing mbean %s from the conf file", mbeanName)); + List finalMetricList = getNodeMetrics(jmxConnection,mbeanName,metricProperties); + finalMetricList.add(new Metric("HeartBeat", String.valueOf(BigInteger.ONE), metricPrefix + "|HeartBeat", "AVG", "AVG", "IND")); + logger.debug("Printing metrics for server {}", mbeanName); + metricWriteHelper.transformAndPrintMetrics(finalMetricList); + } catch (IntrospectionException | IOException | MalformedObjectNameException | InstanceNotFoundException | ReflectionException e) { logger.error("Kafka Monitor error: " + e.getMessage()); metricWriteHelper.printMetric(metricPrefix + "|HeartBeat", BigDecimal.ZERO, "AVG.AVG.IND"); @@ -145,12 +145,3 @@ private String buildName(ObjectInstance instance) { } } - - - - - - - - - diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index 9760b1d..9ff7859 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -1,11 +1,12 @@ ### ANY CHANGES TO THIS FILE DOES NOT REQUIRE A RESTART ### #This will create this metric in all the tiers, under this path -metricPrefix: "Server|Kafka" +metricPrefix: "Server|Component:|Custom Metrics|Kafka" + +#This will create it in specific Tier/Component. +#Please make sure to replace with the appropriate one from your environment. +#To find the in your environment, please follow the screenshot https://docs.appdynamics.com/display/PRO42/Build+a+Monitoring+Extension+Using+Java -#This will create it in specific Tier/Component. Make sure to replace with the appropriate one from your environment. -#To find the in your environment, please follow the screenshot https://docs.appdynamics.com/display/PRO42/Build+a+Monitoring+Extension+Using+Java -#metricPrefix: Server|Component:|Custom Metrics|Kafka # List of Kafka Instances servers: @@ -13,84 +14,284 @@ servers: port: "9999" username: "" password: "" - #encryptedPassword: - #encryptionKey: + encryptedPassword: "" displayName: "Local Kafka Server" #displayName is a REQUIRED field for level metrics. +encryptionKey: "" + connection: useSsl: true socketTimeout: 3000 connectTimeout: 1000 - sslProtocols: "TLSv1.2" + sslProtocols: ["TLSv1.2"] sslCertCheckEnabled: false sslVerifyHostname: false sslCipherSuites: "" - sslTrustStorePath: "" - sslTrustStorePassword: "" - sslKeyStorePath: "" - sslKeyStorePassword: "" + sslTrustStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks" + sslTrustStorePassword: "test1234" sslTrustStoreEncryptedPassword: "" - useDefaultSslConnectionFactory: false - + sslKeyStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.keystore.jks" + sslKeyStorePassword: "test1234" + useDefaultSslConnectionFactory: true -#nsme of flag needs review # number of concurrent tasks. # This doesn't need to be changed unless many instances are configured numberOfThreads: 10 - # The configuration of different metrics from various mbeans of Kafka server # For most cases, the mbean configuration does not need to be changed. mbeans: #All MBeans which have attributes Count and MeanRate - - objectName: ["kafka.server:type=BrokerTopicMetrics,*", - "kafka.server:type=DelayedFetchMetrics,*", - "kafka.server:type=KafkaRequestHandlerPool,*", - "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", - "kafka.server:type=ReplicaManager,name=IsrShrinksPerSec", - "kafka.server:type=SessionExpireListener,*", - "kafka.network:type=RequestMetrics,*", - "kafka.controller:type=ControllerStats,*" - ] - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - - -#All MBeans which have attributes Value - - objectName: ["kafka.server:type=DelayedOperationPurgatory,*", - "kafka.server:type=KafkaServer,name=BrokerState", - "kafka.server:type=ReplicaFetcherManager,*", - "kafka.server:type=ReplicaManager,name=LeaderCount", - "kafka.server:type=ReplicaManager,name=PartitionCount", - "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions", - "kafka.network:type=Processor,*", - "kafka.network:type=RequestChannel,*", - "kafka.network:type=SocketServer,*" - ] - metrics: - Value: "Value" + - objectName: "kafka.server:type=BrokerTopicMetrics,*" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + - objectName: "kafka.server:type=DelayedFetchMetrics,*" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + - objectName: "kafka.server:type=KafkaRequestHandlerPool,*" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + - objectName: "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + - objectName: "kafka.server:type=ReplicaManager,name=IsrShrinksPerSec" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + - objectName: "kafka.server:type=SessionExpireListener,*" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + - objectName: "kafka.network:type=RequestMetrics,*" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + - objectName: "kafka.controller:type=ControllerStats,*" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + ### + + - objectName: "kafka.server:type=DelayedOperationPurgatory,*" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + + - objectName: "kafka.server:type=KafkaServer,name=BrokerState" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + - objectName: "kafka.server:type=ReplicaFetcherManager,*" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + + - objectName: "kafka.server:type=ReplicaManager,name=LeaderCount" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + - objectName: "kafka.server:type=ReplicaManager,name=PartitionCount" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + - objectName: "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + - objectName: "kafka.network:type=Processor,*" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + - objectName: "kafka.network:type=Processor,*" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + + - objectName: "kafka.network:type=RequestChannel,*" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + + - objectName: "kafka.network:type=SocketServer,*" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + #JVM Metrics - - objectName: ["java.lang:type=Memory"] + - objectName: "java.lang:type=Memory" metrics: HeapMemoryUsage.committed: "Heap Memory Usage | Committed" HeapMemoryUsage.max: "Heap Memory Usage | Max" diff --git a/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java b/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java index c93c5ba..da44f58 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java @@ -1,15 +1,43 @@ package com.appdynamics.extensions.kafka; +import com.appdynamics.extensions.AMonitorJob; +import com.appdynamics.extensions.conf.MonitorContextConfiguration; +import com.appdynamics.extensions.http.Http4ClientBuilder; +import com.appdynamics.extensions.kafka.metrics.CustomSSLSocketFactoryTest; +import com.appdynamics.extensions.util.PathResolver; import com.google.common.collect.Maps; +import com.singularity.ee.agent.systemagent.api.AManagedMonitor; import com.singularity.ee.agent.systemagent.api.exception.TaskExecutionException; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.Assert; import org.junit.Test; +import org.mockito.Mockito; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXConnectorServer; +import javax.management.remote.JMXServiceURL; +import javax.management.remote.rmi.RMIConnectorServer; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import javax.rmi.ssl.SslRMIClientSocketFactory; +import javax.rmi.ssl.SslRMIServerSocketFactory; +import java.io.FileInputStream; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Map; public class KafkaMonitorTest { public static final String CONFIG_ARG = "config-file"; + MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration("Kafka", "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), Mockito.mock(AMonitorJob.class)); @Test public void testKafkaMonitorExtension() throws TaskExecutionException { @@ -20,5 +48,43 @@ public void testKafkaMonitorExtension() throws TaskExecutionException { } +// @Test(expected = Exception.class) +// public void testConfigureSSL() throws Exception { +// int port = 8745; +// JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); +// jmxConnectorServer.start(); +// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:"+port+"/jmxrmi"); +// Map env = new HashMap(); +//// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE,new CustomSSLSocketFactory()); +// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); +// jmxConnectorServer.stop(); +// } + +// @Test +// public void testConfigureSSLwithKeys() throws Exception { +// int port = 8745; +// JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); +// jmxConnectorServer.start(); +// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:"+port+"/jmxrmi"); +//// System.setProperty("javax.net.ssl.trustStore", "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks"); +//// System.setProperty("javax.net.ssl.trustStorePassword", "test1234"); +// String truststore = "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks"; +// char truststorepass[] = "test1234".toCharArray(); +// KeyStore ks = KeyStore.getInstance("JKS"); +// ks.load(new FileInputStream(truststore), truststorepass); +// TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); +// tmf.init(ks); +// SSLContext ctx = SSLContext.getInstance("TLS"); +// ctx.init(null, tmf.getTrustManagers(), new SecureRandom()); +// +// Map env = new HashMap(); +// CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); +// SslRMIServerSocketFactory ssf = new SslRMIServerSocketFactory(); +// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE,ctx.getSocketFactory()); +// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory); +// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); +// jmxConnectorServer.stop(); +// } + } \ No newline at end of file diff --git a/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java new file mode 100644 index 0000000..87d0b88 --- /dev/null +++ b/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java @@ -0,0 +1,45 @@ +package com.appdynamics.extensions.kafka.metrics; + +import com.appdynamics.extensions.kafka.CustomSSLSocketFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.management.*; +import javax.management.remote.JMXConnectorServer; +import javax.management.remote.JMXConnectorServerFactory; +import javax.management.remote.JMXServiceURL; +import javax.management.remote.rmi.RMIConnectorServer; +import javax.rmi.ssl.SslRMIClientSocketFactory; +import javax.rmi.ssl.SslRMIServerSocketFactory; + +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.rmi.server.RMIServerSocketFactory; +import java.util.HashMap; + +public class CustomSSLSocketFactoryTest { + private static final Logger logger = LoggerFactory.getLogger(CustomSSLSocketFactoryTest.class); + + public static JMXConnectorServer startSSL(int port) { + + MBeanServer mbs = MBeanServerFactory.createMBeanServer(); + HashMap env = new HashMap(); + SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory(); + SslRMIServerSocketFactory ssf = new SslRMIServerSocketFactory(); +// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE,new CustomSSLSocketFactory()); +// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE,ssf); + JMXConnectorServer cs = null; + try { + JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:"+port+"/jmxrmi"); + Registry registry = LocateRegistry.createRegistry(port, csf, null); + cs = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs); + + }catch(Exception ioe){ + logger.debug("could not connect"); + } + + return cs; + + } + +} diff --git a/src/test/resources/conf/config.yml b/src/test/resources/conf/config.yml new file mode 100755 index 0000000..3615321 --- /dev/null +++ b/src/test/resources/conf/config.yml @@ -0,0 +1,42 @@ +### ANY CHANGES TO THIS FILE DOES NOT REQUIRE A RESTART ### + + +metricPrefix: Custom Metrics|Kafka +#metricPrefix: Server|Component:|Custom Metrics|Kafka + +# List of Kafka Instances +servers: + - host: "localhost" + port: "9999" + username: "" + password: "" + #encryptedPassword: + #encryptionKey: + useSSl: false + displayName: "Local Kafka Server" + +connection: + useSsl: false + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocols: ["TLSV1.2"] + sslCertCheckEnabled: true + sslVerifyHostname: true + + sslCipherSuites: [] + sslTrustStorePath: "" + sslTrustStorePassword: "" + sslTrustStoreEncryptedPassword: "" + useDefaultSslConnectionFactory: false + +numberOfThreads: 10 + +mbeans: + + - objectName: ["kafka.server:type=BrokerTopicMetrics,*"] + metrics: + Count: + alias: "Count" + + MeanRate: + alias: "MeanRate" \ No newline at end of file diff --git a/src/test/resources/conf/config_for_composite_metrics.yml b/src/test/resources/conf/config_for_composite_metrics.yml new file mode 100755 index 0000000..f489623 --- /dev/null +++ b/src/test/resources/conf/config_for_composite_metrics.yml @@ -0,0 +1,17 @@ +connection: + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocols: ["TLSV1.2"] + sslCertCheckEnabled: true + sslVerifyHostname: true + + sslCipherSuites: [] + sslTrustStorePath: "" + sslTrustStorePassword: "" + sslTrustStoreEncryptedPassword: "" + useDefaultSslConnectionFactory: + +- objectName: ["java.lang:type=Memory"] + metrics: + HeapMemoryUsage.committed: "Heap Memory Usage | Committed" + HeapMemoryUsage.max: "Heap Memory Usage | Max" \ No newline at end of file From 1bd4dce1acee75f373a0b0be68b136861299d18a Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Wed, 18 Jul 2018 13:26:04 -0700 Subject: [PATCH 15/49] ssl test and sslenabled flags, removing hardcoded values --- .../extensions/kafka/CustomSSLSocketFactory.java | 4 ++-- .../com/appdynamics/extensions/kafka/KafkaMonitor.java | 2 +- src/main/resources/conf/config.yml | 8 ++++---- .../appdynamics/extensions/kafka/KafkaMonitorTest.java | 10 +++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java index 8f074d5..2e3a274 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java +++ b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java @@ -17,8 +17,8 @@ public class CustomSSLSocketFactory extends SslRMIClientSocketFactory { public SSLSocketFactory createSocketFactory() throws IOException { - String truststore = "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks"; - char truststorepass[] = "test1234".toCharArray(); + String truststore = ""; + char truststorepass[] = "".toCharArray(); SSLSocketFactory ssf = null; try { KeyStore ks = KeyStore.getInstance("JKS"); diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index 5a62ba5..4e4dbad 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -73,7 +73,7 @@ public static void main(String[] args) throws TaskExecutionException { KafkaMonitor monitor = new KafkaMonitor(); Map taskArgs = new HashMap(); - taskArgs.put("config-file", "/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/main/resources/conf/config.yml"); + taskArgs.put("config-file", "/src/main/resources/conf/config.yml"); monitor.execute(taskArgs, null); } diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index 9ff7859..e17cdff 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -28,11 +28,11 @@ connection: sslVerifyHostname: false sslCipherSuites: "" - sslTrustStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks" - sslTrustStorePassword: "test1234" + sslTrustStorePath: "" + sslTrustStorePassword: "" sslTrustStoreEncryptedPassword: "" - sslKeyStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.keystore.jks" - sslKeyStorePassword: "test1234" + sslKeyStorePath: "" + sslKeyStorePassword: "" useDefaultSslConnectionFactory: true # number of concurrent tasks. diff --git a/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java b/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java index da44f58..3702c53 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java @@ -43,7 +43,7 @@ public class KafkaMonitorTest { public void testKafkaMonitorExtension() throws TaskExecutionException { KafkaMonitor kafkaMonitor = new KafkaMonitor(); Map taskArgs = Maps.newHashMap(); - taskArgs.put(CONFIG_ARG, "/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/test/resources/conf/config.yml"); + taskArgs.put(CONFIG_ARG, "/conf/config.yml"); kafkaMonitor.execute(taskArgs, null); } @@ -66,10 +66,10 @@ public void testKafkaMonitorExtension() throws TaskExecutionException { // JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); // jmxConnectorServer.start(); // JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:"+port+"/jmxrmi"); -//// System.setProperty("javax.net.ssl.trustStore", "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks"); -//// System.setProperty("javax.net.ssl.trustStorePassword", "test1234"); -// String truststore = "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks"; -// char truststorepass[] = "test1234".toCharArray(); +//// System.setProperty("javax.net.ssl.trustStore", ""); +//// System.setProperty("javax.net.ssl.trustStorePassword", ""); +// String truststore = ""; +// char truststorepass[] = "".toCharArray(); // KeyStore ks = KeyStore.getInstance("JKS"); // ks.load(new FileInputStream(truststore), truststorepass); // TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); From cb2f69734c5ca391a8d73058b380258e65c64028 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Thu, 19 Jul 2018 11:22:36 -0700 Subject: [PATCH 16/49] updating readme --- README.md | 377 ++++++++++++++++++++++++----- src/main/resources/conf/config.yml | 4 +- 2 files changed, 317 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 713c3bc..f66045f 100755 --- a/README.md +++ b/README.md @@ -48,13 +48,11 @@ For eg. java -Dappdynamics.agent.maxMetrics=2500 -jar machineagent.jar ``` - ## Installation ## 1. Run "mvn clean install" and find the KafkaMonitor.zip file in the "target" folder. You can also download the KafkaMonitor.zip from [AppDynamics Exchange][]. 2. Unzip as "KafkaMonitor" and copy the "KafkaMonitor" directory to `/monitors` - # Configuration ## Note : Please make sure to not use tab (\t) while editing yaml files. You may want to validate the yaml file using a [yaml validator](http://yamllint.com/) @@ -67,61 +65,303 @@ Note : Please make sure to not use tab (\t) while editing yaml files. You may wa ### ANY CHANGES TO THIS FILE DOES NOT REQUIRE A RESTART ### #This will create this metric in all the tiers, under this path -metricPrefix: Custom Metrics|Kafka +metricPrefix: "Server|Component:|Custom Metrics|Kafka" + +#This will create it in specific Tier/Component. +#Please make sure to replace with the appropriate one from your environment. +#To find the in your environment, please follow the screenshot https://docs.appdynamics.com/display/PRO42/Build+a+Monitoring+Extension+Using+Java -#This will create it in specific Tier/Component. Make sure to replace with the appropriate one from your environment. -#To find the in your environment, please follow the screenshot https://docs.appdynamics.com/display/PRO42/Build+a+Monitoring+Extension+Using+Java -#metricPrefix: Server|Component:|Custom Metrics|Kafka # List of Kafka Instances -instances: +servers: - host: "localhost" - port: 9999 - username: - password: - #encryptedPassword: - #encryptionKey: + port: "9999" + username: "" + password: "" + encryptedPassword: "" displayName: "Local Kafka Server" #displayName is a REQUIRED field for level metrics. +encryptionKey: "" + +connection: + useSsl: true + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocols: ["TLSv1.2"] + sslCertCheckEnabled: false + sslVerifyHostname: false + + sslCipherSuites: "" + sslTrustStorePath: "" + sslTrustStorePassword: "" + sslTrustStoreEncryptedPassword: "" + sslKeyStorePath: "" + sslKeyStorePassword: "" + useDefaultSslConnectionFactory: true # number of concurrent tasks. # This doesn't need to be changed unless many instances are configured numberOfThreads: 10 - # The configuration of different metrics from various mbeans of Kafka server # For most cases, the mbean configuration does not need to be changed. mbeans: -#All MBeans which have attributes Count and MeanRate - - mbeanFullPath: ["kafka.server:type=BrokerTopicMetrics,*", - "kafka.server:type=DelayedFetchMetrics,*", - "kafka.server:type=KafkaRequestHandlerPool,*", - "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", - "kafka.server:type=ReplicaManager,name=IsrShrinksPerSec", - "kafka.server:type=SessionExpireListener,*", - "kafka.network:type=RequestMetrics,*", - "kafka.controller:type=ControllerStats,*" - ] - metrics: - include: - - Count: "Count" - - MeanRate: "MeanRate" - -#All MBeans which have attributes Value - - mbeanFullPath: ["kafka.server:type=DelayedOperationPurgatory,*", - "kafka.server:type=KafkaServer,name=BrokerState", - "kafka.server:type=ReplicaFetcherManager,*", - "kafka.server:type=ReplicaManager,name=LeaderCount", - "kafka.server:type=ReplicaManager,name=PartitionCount", - "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions", - "kafka.network:type=Processor,*", - "kafka.network:type=RequestChannel,*", - "kafka.network:type=SocketServer,*" - ] - metrics: - include: - - Value: "Value" + + - objectName: "kafka.server:type=BrokerTopicMetrics,*" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + - objectName: "kafka.server:type=DelayedFetchMetrics,*" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + - objectName: "kafka.server:type=KafkaRequestHandlerPool,*" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + - objectName: "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + - objectName: "kafka.server:type=ReplicaManager,name=IsrShrinksPerSec" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + - objectName: "kafka.server:type=SessionExpireListener,*" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + - objectName: "kafka.network:type=RequestMetrics,*" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + - objectName: "kafka.controller:type=ControllerStats,*" + metrics: + Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + MeanRate: + alias: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + + + - objectName: "kafka.server:type=DelayedOperationPurgatory,*" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + + - objectName: "kafka.server:type=KafkaServer,name=BrokerState" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + - objectName: "kafka.server:type=ReplicaFetcherManager,*" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + + - objectName: "kafka.server:type=ReplicaManager,name=LeaderCount" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + - objectName: "kafka.server:type=ReplicaManager,name=PartitionCount" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + - objectName: "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + - objectName: "kafka.network:type=Processor,*" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + - objectName: "kafka.network:type=Processor,*" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + + - objectName: "kafka.network:type=RequestChannel,*" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + + - objectName: "kafka.network:type=SocketServer,*" + metrics: + Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "average" + timeRollUpType: "average" + clusterRollUpType: "individual" + + +#JVM Metrics + - objectName: "java.lang:type=Memory" + metrics: + HeapMemoryUsage.committed: "Heap Memory Usage | Committed" + HeapMemoryUsage.max: "Heap Memory Usage | Max" + HeapMemoryUsage.used: "Heap Memory Usage | Used" + NonHeapMemoryUsage.committed: "Heap Memory Usage | Committed" + NonHeapMemoryUsage.used: "Heap Memory Usage | Used" ``` 3. Configure the path to the config.yml file by editing the in the monitor.xml file in the `/monitors/KafkaMonitor/` directory. Below is the sample @@ -133,14 +373,7 @@ mbeans: .... ``` - -## Password Encryption Support -To avoid setting the clear text password in the config.yaml, please follow the process below to encrypt the password -1. Download the util jar to encrypt the password from [https://github.com/Appdynamics/maven-repo/blob/master/releases/com/appdynamics/appd-exts-commons/1.1.2/appd-exts-commons-1.1.2.jar](https://github.com/Appdynamics/maven-repo/blob/master/releases/com/appdynamics/appd-exts-commons/1.1.2/appd-exts-commons-1.1.2.jar) and navigate to the downloaded directory -2. Encrypt password from the commandline -`java -cp appd-exts-commons-1.1.2.jar com.appdynamics.extensions.crypto.Encryptor encryptionKey myPassword` -3. Specify the passwordEncrypted and encryptionKey in config.yaml ## Custom Dashboard ## ![](https://github.com/Appdynamics/kafka-monitoring-extension/blob/master/Kafka_CustomDashboard.png?raw=true) @@ -156,25 +389,45 @@ KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremo Add below line in kafka-server-start.sh ``` -export JMX_PORT=${JMX_PORT:-9999} + export JMX_PORT=${JMX_PORT:-9999} ``` -## Contributing ## +## Credentials Encryption +Please visit [this](https://community.appdynamics.com/t5/Knowledge-Base/How-to-use-Password-Encryption-with-Extensions/ta-p/29397) page to get detailed instructions on password encryption. The steps in this document will guide you through the whole process. + +## Extensions Workbench +Workbench is an inbuilt feature provided with each extension in order to assist you to fine tune the extension setup before you actually deploy it on the controller. Please review the following [document](https://community.appdynamics.com/t5/Knowledge-Base/How-to-use-the-Extensions-WorkBench/ta-p/30130) for how to use the Extensions WorkBench + +## Troubleshooting +Please follow the steps listed in the [extensions troubleshooting document](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) in order to troubleshoot your issue. These are a set of common issues that customers might have faced during the installation of the extension. If these don't solve your issue, please follow the last step on the troubleshooting-document to contact the support team. -Always feel free to fork and contribute any changes directly via [GitHub][]. +## Support Tickets +If after going through the Troubleshooting Document you have not been able to get your extension working, please file a ticket and add the following information. -## Community ## +Please provide the following in order for us to assist you better.

 -Find out more in the [AppDynamics Exchange][]. +1. Stop the running machine agent
. +2. Delete all existing logs under /logs
. +3. Please enable debug logging by editing the file /conf/logging/log4j.xml. Change the level value of the following elements to debug.
 + ``` + + + ``` +4. Start the machine agent and please let it run for 10 mins. Then zip and upload all the logs in the directory /logs/*. +5. Attach the zipped /conf/* directory here. +
6. Attach the zipped /monitors/ directory here
. -## Support ## +For any support related questions, you can also contact help@appdynamics.com. -For any questions or feature request, please contact [AppDynamics Center of Excellence][]. +## Contributing +Always feel free to fork and contribute any changes directly via [GitHub](https://github.com/Appdynamics/activemq-monitoring-extension). -**Version:** 1.0.0 -**Controller Compatibility:** 3.7+ -**Kafka Versions Tested On:** 2.11-0.9.0.0 and 2.11-0.10.1.0 +## Version +| Name | Version | +|--------------------------|------------| +|Extension Version |2.1.0 | +|Controller Compatibility |4.4 or Later| +|Product Tested On |Kafka 2.11. | +|Last Update |07/18/2018 | +|List of Changes |[Change log](https://github.com/Appdynamics/kafka-monitoring-extension/blob/master/Changelog.md) | -[Github]: https://github.com/Appdynamics/kafka-monitoring-extension -[AppDynamics Exchange]: https://www.appdynamics.com/community/exchange/kafka-monitoring-extension/ -[AppDynamics Center of Excellence]: mailto:help@appdynamics.com diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index e17cdff..aa72676 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -43,7 +43,7 @@ numberOfThreads: 10 # For most cases, the mbean configuration does not need to be changed. mbeans: -#All MBeans which have attributes Count and MeanRate + - objectName: "kafka.server:type=BrokerTopicMetrics,*" metrics: Count: @@ -183,7 +183,7 @@ mbeans: timeRollUpType: "average" clusterRollUpType: "individual" - ### + - objectName: "kafka.server:type=DelayedOperationPurgatory,*" metrics: From ab8a8a33fa6021d2b41482a4b5de1cfc52578972 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Thu, 19 Jul 2018 16:59:16 -0700 Subject: [PATCH 17/49] code review comments --- Kafka_CustomDashboard.png | Bin 165470 -> 0 bytes pom.xml | 24 +++++++----------- .../kafka/CustomSSLSocketFactory.java | 16 ++++++------ .../kafka/JMXConnectionAdapter.java | 6 +++-- .../extensions/kafka/KafkaMonitor.java | 9 ++++--- .../extensions/kafka/KafkaMonitorTask.java | 15 ++++++++--- .../kafka/metrics/DomainMetricsProcessor.java | 15 ++++++----- .../extensions/kafka/utils/Constants.java | 10 +------- src/main/resources/conf/config.yml | 16 ++++++++---- 9 files changed, 57 insertions(+), 54 deletions(-) delete mode 100644 Kafka_CustomDashboard.png diff --git a/Kafka_CustomDashboard.png b/Kafka_CustomDashboard.png deleted file mode 100644 index 4f15a04a03d109280f98bc064a302e12030ce14d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165470 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

%jj%bEKI9yy-N*3iY-3HpQ*4Z_A7T zT2{B@3vd3%$X|(WV@+wB9I*zG$#^^()=6}vux4Gk|Nk)d7GPC%TidW82)GeJbdyTD zQF7DWrBWi&NT;+&NlAm0NOvQGGzv<$bV_$hcW>aE%X6OR^*R54-tW6EE-&4}X6-fS z8gq<$-1j|A< zi69OSX97IAD~Wd9Z4qnhzYH3_kD`nC_XYD?&v9+Odroo-1ToJ7oJW-U4;oZRsnvpo z0H>(5t^t5$C$0++HJp!@(uah{-2j9rV#C&kW2kZmDt%wkp81;?Y&CmHTbba=VbS1p zAHEZC4E9D6^x**=`TXJ8Bwz^iV*%Zu6pw?qZfhG|iiL2H>TW40q%%Pb&(aH12>@d4 zlAX%Y8x8oN)^ld36JKZk{ww`8=<;Rqc~OW9Tppo$-GSIY1K^a?SMb{GxX^aQS2x*btfmP~oE_mjc_VX&!%!1v#fMP-5s)gydra&n zC%MU9^l*(^$W0z}e84$}0#!9IMG1NEeYOn)5>>mqT2^hy@;pUAP5?~nLe9}8TZvr6t$XN z`@Q-G;b7%KZF;vyq}H}q6zc}>Zheb=%5f82KSaIo$wq^BbAz+-v)S940NZxxhLz2D z!MIw8nhYD@@HMs$#t2U4UhXu#D^!8NZ%E*U>rV3$Eo76l0>s2l^X0XlXz)ef^gg!* zvB$6w8#A@7j%S4an)Tr@Ek4ftH&w}X!t%>S7gS1Kw;ZNFv=Ux0|zuTiLp+k z&~Y7>5He@I_lFIjIGE5yj(?D0j%#f!^6CR2FLo^mrxnBkL?EAjc0xrZ6NQZT|I6y~ ze41dSM_9yNN!IftiRWDlN|H;|oS9^t(Il>}C`kw_8)>qfL`t)r%c>}aYE#Z7Orwl7 zfZf3w!s9(ZA{NMnda6=qnba_5C)WDCc-_}0%(ncSJyS0BKjLt9R3kxLas;e|m=c?t zCwrB*$8XdQPmG0lR7|KwnJA;yK9|aC2jrGmA33vhe4eaW)P|xU9~FrE^@G67D)co7 z_)GpN=j$e=@Z5UlXYb~}n`Or#%w2W$s^VseaX5sKo9PPH+2^w?9w#zG4R19u?h`w9 zfv%lbaPXzh;%;h>XP&_1VTS!jKW@52M$b$321Mw`!^oxyB+*TC8bMj|fh$cTc{ z0|g+UayAZp`>Oof>?AO3$!0Y^2f$%6=!%EglQYhzTf5Ga@m4`PFq{yBd|dYe;M3$f zfz|3CiEH(Mfw&4h zk%b0`6`B>Ftt}(!?A?%hT}`xvWsPQYO(&18-#&H?lk~(!94e}!K!V6+;#;zyW#&NP zppV_tofUdfdsMx(SeiR;#}R>a-t76=awCZBY^&B|=kqqPRNl1vTrH6oyn1Pf{@i~3 zF=0H{)6q!=st?AC>QUK+ZX4!-%_&xLg&m*A_kYwv2l5~lk^qBR<%RvvGC)01nM)P+ z03`C#RPPWLkk%EHbud@rs&^vrg$oSGy^sq4K-VOxQBMRc3UxblbUbX_Dv*_M=gmcB z01>#Jzt0jq2OrYCsgRosUcmQc4t&21k(Q0w|K9`=On#*{g-Wh=4aoT%Z zZRSrXDNLVjxAYHo*b>ZUczR_>b;^R{#0=8zSWpw$DcMW*8iM%Qg-%d?FD9WE`vgoj z^$;)&9HI=9A8x#*pmch-6k&zqPP!==r0}v`FUhWcnGkV?0uP9kElI z;bqFtH>CE>Ozo!`tn%kR87Nd6l+(ck0m<`~hCvADEiswNShZvy$#0$DDp$ba`*^(~ zB>N@7!|&z?l{!X4^xC6PN-DyQf$$Q+n8&~%|LoyU)bnf4U|K|{63V}$R5U)la-D6p z2WW|dhTFkUO}HnKm?ZX1y(+$zxm*pc@?x7$ES3BHG6T>-NZzGko*#8K`YaJKb0q@6 zu9b_n@Zj2CwV(L2ia%t1ikYXDDIZSM>k05@#n9pn%?LG9eUK&}yVSDxFyv%Bq;h!C zU}aGGuRJrA7i1|M%QTw*xb0w|A%D+kTQl#G?2nR!5S-@@k-*R31+P|jo>5 zR+-E-W|{#NL8$SIQTg(R9~0Sx=W?(Su(Q2p3IB+O(42#TYdoy z&pf)@?%xC@iyf3e?Ph*UGP5M5u0mqmdqOISJsdhRbC)WADQx=w4T9ruJ1t~;&NHTu z>)*qDo;cAh1M&x|^Fk0LDeC5!T3t_TolLNU=5fGM@MD7gal!eu5uE zsUJvG<+pc^Ay8A`Y;Ydlg!sv#Mh;L8c?vkf{idy?(ccN6WJ?GiDoQqV#mxye-mE9M z5pxxY>)HZT+k9*|PFT2R3~U?Cm;OkP{V%~}mQ@DlwWFF9ij-cqSkgwiPsCtL(?<#R zW#X~IEhH#)iq%>&MmgnqYFNeqgINtQ<{ALo>O(8F5P6F;b{S*M@tj<1!N&Fb$2=O7 zF4?x7VbczChk6IOt{Pc7hkxfu*0oEafh&(n72)Ku?M;yh^qu*7{@YEE<&6jXNt@w3BnT>6 z&gNJ#!7KQ_np=f$R=Y74qU$49p`n)bSRKx%SZv<%u^7)_VS3DjQhh0gQRWcNdD|VK#kWZE=NlRVea*@Sjh2qAyX;6fg>iVc7 zznP0+3S`uDfeq&DIWjR4b#x`_=lXh+MD0jM?ifoI4N~IR-FB>@lwq0APKsyI5cUf( zqH9$YWa2=`Mam{?dbND1kYh)Oj%kn;t`pGXfTltEM3p}x-q)ok{JZ}oAf*m(1C!## zjma^)o{*yjI^mPH)GAK&)2*)S44>a)7Nw7^{tBJW;;G_)F0#jb{N4I6>AKXh?^Rew zt+haDX;)XW!`1pv-gtZupf@|@fpOR4b{o%C25*z1n?XJ)ybrbWJ9WiKZwjNfUxSV> z(d4d8H(Cfh=kb=W%KP?5hE&iQaP@b9G?-y=orS4m4-oOqGo4anxR^x{TyU}ji_R=4 zEsfJH7F>SmhKBwYD)>^}8!GpXxbT=ml&dS9w$3<(5uA=(R(rD_ZenmEv9DPGx|?@P z)=~_E*Qa1}{Pn!N4RjBPAjSuBYlx$P;u4V6w8@s%-ol)5U#w6v(m zMEbkjkFmX6hKMFc{4uXYxeJwV%g$!~J(bcOXkvG=eCy;fXsB5b-s%%yF=ZXvDc<}_ z^F{!KqI%2d22m!%>5JBX7V8_xledz2An(BqcuJlz|hD_z=DmTL62IkMlY*4IQ;$;eW=V`Q)YCw%zR zGNM!QRV#bHp-F;EIcvV+qf~kU3<>;5Y8}R84)(;5P-IYb*!^-wZOR#YV6KRFYnQEj zpx3Xi@x_y>4YFRP!MHDb&ZVammI9R8b{~3|M9vp@QjJ?; zNbEgsM;=EO!!tlx^#!N-3-jgxAun5c1D0SSwrzJyEmfnf z$+7AaGwA^I+I>OkyY|nAS_}=I!6NcP#x1J+-d=Iz%$65k|H7Fz3X^*stdKy`l53hI zHW1#tcJ@X_iQJq6R;tP0LTFkoZjLccW=t&o8Q^D!rM3stPuvrGZF=XfSuGHDX`03~ zP$Ii13>0fbD_DFdO4TrCZ_xsaPTiY?Np);fuDeR3Dk}3d7UPD z-MEAiUw^f+*RlssXP>6Di6NFy=uEjAyz$L60I85IRgNGou0w~FX<;+&=$8w@0upiV$|Y7XpM<&3gq*$^Ii8%aFp$0g zmLSPtyn3$&WDN;M6|b$U7&gRb$aT(GmrpvFn=UTa&#TB%cd09B41F(Y~1EC336Y7I2fk+4IkP8M>X11W2p zw5b{?!68IwBW!g^y*~%pBj&j-^5B;7Li+XYtr3X(ikg7&;_hxwQ?v+PS2!;Fz(PD$ zEC9!=QtZKnt=9M&9rU*+ABB|msxQpO#s{&~@UTd2Z$3@7$2FsLcStz(#og0XMN&t9 zJmIcqo@S0Uz9~azk&%kkS{(c4RVdr+@eTHcg!xBmpK~;gBUu`VrYK*RcCvM30v7NC zmN(A)6V$|1G*HAkr-l;nCtes`POr!(fXnRrq{ylooY%qio7F289f* zatp92P^+r7^w)VtDsIeTwGi`fRoUJ1Cl~@`>Lg4$lHunhwLChD*`s#C@@$UL2;m`Q z<;ygk!oR!#W-Hi|MRF2$9vDZ+akUvZP?YyLJ~pkdJU9wQ<6Y`*@tP`lt#iFHsiJ!P z*>QeyRQty$iruiu@|GUa1NpfejJZNr8@D;MxvL_! z+l!X4G}cMWZ{t-hpvmvDqK)>|3B!@}s2s7N9!9-NvJ=TagH)r+ z9i?Q+k}qH#m3iMyY$4yQ?goPO_Py+GtD0LZ6n?cP&ko;<^~=uId0J9@P6Min#v%4#o~< zePby>!Gh9XQ2>W1nX#MnbKYO08!r~0_tSX_T=XTa@?r)p9ga&WJp*T{@av25vroLX zfez{5ZAmGP1L7Nj7E&}$#Vjyl4gfb6V#o$u`aFV2J-vGeL$Y25IT6@0Bk48Y!pzE- zu~%&}Z=m07sN>24ebLGp*=<(`ToEi5m8GO@$U0vcttGK031lN@>* z`hRERzboprLDK<}qrRBFk0q&_At8a|W*J>!X7^uy^x&v<|7^JOFr>UQ)SQ8dphh7D z6xsb^Fs4D`>{{i`UC56W520G`t0NS+c$<{@c*tyR~? zHJJp1fe64AjJ;W8!tg)oP%+_y}O>pW9v!m{v^nGTXF zV^jfkLNF)4Lr`g6VqmQmE-W8NrB_p(a9EgcN6!URJ+VTuhr;zl6I>0_FX^Xb-QOsm zX4^I{3zir;3YrkHi;|C?aEw-OJS(5pGKg(d)DC|fB_bh9b|M+%O#;uZ=E6)K$E)s7 zw@*YasYO9FIk=X%r9HbGmUce!o?h4tzJ@@qtZWCAHDiK_C7svEEZ(nW2s1Z^M(SZv z(`T?ohMDCBnAP92#PJDx-vAo0C6w>0KaP-H6+Y>*qhPM>YAV~SdRhMR^;%=_L1meZ zuI;Ok6?T3nJ0A@6`=)PJT8j2QWEfOYb!Cq>3LhP@rhw;CAzluKd#L!!m*-P!6_z4) z*DCu3HDM2t3$9s)pmdNQ2@ezScm5`CBQLmGQ#};Rw~^|b^?Lk1WXU>BLS1b>|6Uh8aDR;Y( zg9Yq*CK1IqI^f*yj<5H}yJ>BL{Q?^R-XWZ1Kcs#ms&w658hKS*qwkt-E4swxA^)xy zT0BWbga70iOU-dQZ;xu|YY}V=!gJM~(9QK)#qs850fsZZ;^NP@EJQ=^?O6CI7tiLDhKO4wW&ZHFDSe;Z;Jh?TD7+ zHHnNXm0N08rh(dw6gEJvUIalU?}+5^WM2)-1ubTb{zP;c2)W*?zPu$3Ypk3X`hK9- z!p1$6y+P&Iyah^swj`SdocnpG;L{>7zmwA^jQ%bkY)YBKIQ1^N+`UN76|P2R)m0p? ztICKaLPzX1SJ3w9=GX2MBH%2goDdst3zbJf=^c*T4ddSE`f#}ED(eRCAQLJzlw9d! z=M%G1o+qliwR7(n5E!ovp`;;I2jEhQ2p2%w%!K5Pu=_N(*Ulu!u6|*Me`QttS&^Ig zj{D4-5XscMd}dR2CsprVS^skEQ!KqL0Oa841)8FcBz_W8pPC(v_b;@P=A-r-lQRl< z^xxNzd774O#O<6r8qBCj7_B#zXlrfCa_R0^zZV;S5ppg0aKPY7vghBICjQ+bx z^siQtNGpZ~dLOze=ZXU`K*rOUm`|JsB2>rsr0yPv#d0$m$N2^Bv%;rF4fdchX?mxL z(xptasN(`~AQ^M#jmkODVe7As2z$KIZH1{>eDGt-xa!dn-n>Jzpu=x|tWQlERW92v zb*WsH!SB#ZQzt@fJcnLmwob_uo^-M^>?DEaV2?UMzERX>EmS5vVzpe4HV_+4a6-M^ zc*@9x+ZwnUi>I0CZ}Ni5hmUZFm1%*9vz184)fQ@9zH(4QN^h_o_~E?9J%GGPC^9h{ zTbJp^O8rSMWn~`iuQeXjHItjas6I9)ON%}swrGCmS$wfx# z=W&)iakb6-UYIV1fgO9z^r69((xYJ@rG0qU{do2ZhUE_AHL}P?6AFca$*|oW=Yi_K z4+2hr6Fn~t5Bckb-z)!G5@fPW?^Ou%6&cWAevp7ve57vhG{X(efUp8C6j6mOi?mf` zTW?ARNNtJozGnOkgRfo>%J%AEX92lUJ;oMLeGlLpBP)%Ef8>=p^~hWMPEOAYNbu(;C4>5xV=FlsWlL?C0v7>G?v;8d>pQPINDl~ekDw2qR#RYdq64HA7NQ{moZy6Z#0 z8Gf?6ZSeM)E7wxKKKXg6E=ISn~%`Y zWSVGB(zflJg2%-bSr}RD4o^fOjynrphg{f}0(0qX(3Z>?>fU-58?O3T9HZ4A9Q=Ye zwuE~6JnmROUo7dE7+frU+GdMq*U2i4S6K^};<0F1PTmMga%J)lX zv{u_W24we)MJ~@N1~%s@7Hnpu*~^PNcY7H;~Ebl7px3l_^aQwfFbB8BAJf#|%w{c`_1x$=Hi;lq_qt3BRoS zByJi=b*YHmSXpRCu@^|uEerXa+X)O>k8xZ7FxM(dD@es`7`EOM7yMFEVq)L14k#*} z4Y!xV7-p%6{g90yoB2Q_1ewseW+hdFe#EX=@JaD$>sEw3-@_L`3qJae@5{Ic=9fod zOc;7?`uGic`S9siBByld)W|gKzYrMM_!oM$mR2HdCp|te(4<7^v|JF>0KEieY9kc| zFDKOQgi#Dr<+OOY-hRbiTQ;wqGu*;tY^_RGezFrZIvQ2PgXjp&PA|;1=I|Cg=Xbkp z3}}cy#72#$H-Ex#9LQ8EZRe9wro1R7ocr1W%s@=}DE_xA&tcQ8Qut+dIc+{h_kf3h_CY*haP!3M{gnCP_%SdH?Fd zW2(fQF3;tPv>STioe3&Cr)+!t(r(0X;Y2-Ya%}x#N~tr0C5Wgx$8!(9iJ!bH9XgV( z^H}rAPyKk`JWz(zHPJ#iN3Ylykn0-N3+l9&m7B88RPg#d1m``R zTJ&Z8C4-N_Avy=zbHbJasr3&Ifi}ccSR{z3s%pEeSvtjZjOYiA+bB3%EQc(0c*nwQ z$;`wkUxQH-PxkY)l?Xkl13Y_a#>iz;!*@u}9~i`#;=8h zJCdSEPO?&jc7M>6>hktD`xWmPd4k1IGNt^ z5#}qyY;%%KY#B)>D1h8r+~Kjbqpa@XGfHZ|>7eOw)jVWe9SYC^r)=Qo<#SyH^- zXbs{KXk)^RldMb^N%ew+3yg55d`5vVzc-K1IS9jOij^KarNAlc ziPs(=M0i7kJaFlTvmrURSwHP%h3zG9r7^a&MhJ)s;g{&j;#|d$fCGDmb)oB05jW@j z`NwK368(#C=7q|X5}lXgqCjt}$r5MN=tM2{``u*Ef2j<}KAqDx(u}`6bgw13(*O?aru&uY}ImwVG!F3o~=Mww} zu{4NitygV+j;J?iw@}gDvo&BcM?XlhRXq~oPWi|XmKk;SY++N;elXMbtB!JQdiQr3)y($AUh zxIu2`y=p#DA8l~g0&PZ4)%wC@b^#cQJexa?ndQptiZOZUKE{oqX{w{n_=#Hb{^MdS(-|5591K0n+NnxEGF- zoQf$$d#wfdh%GI%kEJI+0i33*|C^s$mJ)2QNisIYi-oZOwMjpFrj8!R74eM0m@w>H zSF(;tJRqXn3eZI{shco~>YTKzYTUbTH2;;y{oysf?cT7*=Q~zY=YVCI*lMVKHN1Q| z**$W;D%}CLg{M~onqE15J#lyEE9ueX?dA&sPhe;!%+`rPeEI@3hHK0yto3XdiRZ6z zj7rX9#uhhjKw@c8jfBpd&gnvwxbNQj;dDJ`{kpx{V!30pp?q?Tv}gI75D^Jcq}cih z|2oBf6C#L9F=E#Z*%Bv})-UuwMyaOp8?z5B`;^LbY)Q;Bnrg1En+#@rt9nGHaaE1h zgoqLy>waU?SE2yjTO^+M21$j%q3lc@*KF$W%f2IWGMJGk;$FWbIU`<}57h-Yy{Rbt zuUvI7+MVK*x#|9NF{J+P7J_i&oFnr_{q1(li%deRtP+t}EICrs*t+*AJN@oO+n({( z?N_c|^6_hoGnJ}I>k%uvDBW9kUrdSxch>IB`m_%4W6`>{{_H?ZCvqNm350iCLxW+6 z{>C*Ze=iejVa5)(f%PdFnQ)&&56^tC_}FRqBYwBAkPc$ld#=+ohn8o@qTcB9#;1y> z+^2c+XSAnkEqC_=g9VA_cYI|CX8G9V_M&b)+Yhp9h)GwhGD64D z?b^#Xib6sHP-^|JXBHRYw$=;m%lwtl{!MgFy@4)EDeW2H;BfsbK~&;9kR_<8&8|Cz zb|JmL`T?2KnEStuig_=vJ}r{0^n`!rz=F^l-@B@8^XH4u-;v?ifR;!dbvV+-Acjv~ z+t5cx{!L$6T@B9x7-Mm?lG}|aynw1QT`hc2306FiG7|ga;!<0o6viO+y8It6{=R6$ zEs%=KX7=CXcV0Zc*$tDnbmov@`tp3tb+2pQ;2aLM%2**vw?Ped)bBm?7t2-%{2~d8 zRQgB%*kRy*V}ZX-zz8Vh1|TwC;$j;jfpM!d1+YC4*U=a4Zr{qe6)=+%47iSLiV8*$ zKBZgro59SWxG4R{^`#O)7Y%!NUHp$1|8>z1&@iaw0ab_*pA~>GOBpz7Hpwq@KOr6v z7`brZ&-}n$T?FT5)Y`&th67+%a)6#@nBT&~8_><{B?9%)FPIAEpO_@F1tNY16QzFm z`x*L;RQ|_G+zNPl_uqHmUw7lbalZZ$-Rokh!NRgCYd!h=M_N#agg!RXQ-$L{e#t-n z*~&3Ot7VKy?*E$t_5XLZSHT@jd->1*5r=;A0ihs-8~nE~PgY52 z$#W|r)&BSZe|{oxyA0kzkJEG8hsA&1yG9A{DH<^}`u?#Pe?7(jeo>?r z{L*wC!#f3kE{z)WBo=GEdGf!|*WY5}f@i=d*Yl4b1h3r(Kba=GFY&*9(3acau?zk* z_e}q9COwa}qD$<9 zh=knJSBU?htGbF?b8^{Y!Yi)180{U~1712ze2J>U^1r z30I@c2V?10@~z#><>;YLg(Sz&_BcQI&C3QH6^`mfChNz5)aDM_(BJ%MR&nlNZOnjR z9}ncksC_UX;=UYf^X+efiEf9Z0}!Vd_|XUM15d^UHZTH>yC3)nL;kO?U(wJFyG0&` zP5^{MwF7|Z9-|F-)Yp6=BP=tZ;I%^CljBy^vX%S}Bu!*NV$e}O0J>sQlDG7n!07Ie z5PA#^hFJNjs?%j=Q2#Q`7v3K935+p2lakbERicLSplr}ZW-jKj^gFwwqDEy`ZgSB+ z*9#O&QK0$vsfy>NLu3!B`DfN&aibyo2hfyMzJ>SVq-{<;E>jVZdMTt*GD9sH+XJFb zn*k=h#X?d7SYYN!aKRy8uXsRiV}2E63!j>Au3`q`lpE$yU<(TX7QyHn@DnnBi+WxO zEhUFvLYxU`;N*=oPk=zt6(0)?-EckwEW;6p(GWv7pA*0+i3%0IIHtY;@R{RbqC%U*m_MUXeJXr$LvZJXAPLWbg{Y4(b94PoO(#8q%jpssX5oE`J=j zaSWN%q?jhi6&2v6Knb}SpkLlCwzVy6t-&@-dfor{TesT9iLuC!V?a?a7bJb1rN9q# zD_E>`J= zA0^Uc)32iSkLS-9+D_4fYPSP)ch>-YyPd+rHKzr~SSYm?Z1S780-$kial$Xn^-18M zjRkmjGLZim=}t&=^CN`WX!UE*e&Oy{u`dM@t);+b`ZIT7Yh{YlyB@*~VG%uWtO$zr zDHcFjF#$>*5?Tq*THyPf;+ zC!AC2viZ&A6nKgD{%9~&)3v6}4kFw2yP{ZW#QotQ+&#=8_a)T{grUTPZ?wBKFqQgV z)m!H|gR`N{&ff2)S6UV#T`9lj!J9U+!sNu5=jLS+v$Cif$l8bcGt0NfOYJ_~-F1a|$}c6)kZ*=M=Cj9CPp*h>Pv zVuP@OHMTW`oPA5~q>oMvdjrq_?!i^ z3m`lS(A;Hz#_L~7iQAab5b+~--_~lA97C2VD`4PP&YcDDXRL~rZL@}z%SMj8+b{dX zMgr|b*RMRIBtB#M4p@?ej`DGCEJXT{G170*CDtTNGzjuGuf>WUQUH=cGk_izrO>?E zXmz>M!CtfM0Y+Z-Lz_jkfw4NiD6Dcv{j(-`K1)SLAs+-HUUzU%lvn3Wg*BvY%$kNl zFsj~_R(DmN1Cge-zeYdc{J2|+#MkW*M+o}bgT0aS1@O#IQz7-+iY5Au+6EkMb&*WuLFwLK?XTjUSPLN2UkYn%S_@e&TP*Co|zs3?!>p<2b~P5mkyG zj#jy2&C*dzANsxLY)2?^E?Pt)@Lh7?ALp{{YmiOks9$Y~7iro8It^5x!r}3@Fo?pX zt98#D)PLx5)8|ghM2OxOZrll$^aOKs3X8{Y|MX{U&>uS)DNnj_tVa;%8w)8CUzkvB z))ik7-cP%^DEYk14xihPjR)w;l{Cys^>ZB#ZFn6KWp4}6>y&UNlosPBAwlt|+a^rKrwoTz9=wf&dnJpek1 zR(_~CVoP-Ph>2d~4eF;kue$SM)Se>C7UrO!{|u;9oiXU2Be?I^1|*xlsfw`K0HSz z;y>4VNAZ+=^Df?>q^iLb(?Qswh!3;79A>Lp8e^Q zDmp<~1c(9#1{P$k$(D03dA9DCOQpV;0O~p3lM1obVbz-5D4(S&D>+7mhWP;88cg1j zHDIlA@0KJ>%j0wUQmVlG6Uwjr_N^71-U`ZzW<=1C@0&P&#jwQ6^Ub@rY-UAQ)R=cK zi^JR)dn|2TukC5Y8prEi7d<*q6rS>b=5-%mF+JVn&ohEB2|6Q&-WJp1?||G$i(a=5 zI07-dV|j2c^yX8T3ony;X$2|Ybt^ulqFo5BjJ4gYSQUkAYXds<=L2-%@ zI^<<=|2k^iv3b(k3XLjA_@bo7Te|llkkVn=Mx`=JX7Y4~j+w`QuK~s)3lzoHp%IU7 z&V8JKa5gO+(m;}m9p|~K7st<*T(@b0>zl~!^*k}*KZ44^v!^7K>&pYog1<^tL)sd` z+)x7Q90L?BJ}R(v6}$C;Ma{{Qw-=S1Y=2#(BbvcQE}~~V|8mgQN9PgjT0sY6sciC7 zzZ(%8P^IpV6!*s7nu27dJ|T*EXi@u&7rY4o5565HZ{icd#KCNqEC9rUQpAsu|g@-r5Y zO{i2nt=r0(^?GEXj7ZgD=94!jGLVFSjl)daxqaT0VVDjz4=o z($9_L+F1_S(`HAc>2Qea$Y?&xH4n~v1T)A5lAeb!-`;!0-r**L>Y;%mE>AfIl^s-t zKs3C;FqLM7L9;q|>0b6?`xLm%_V-?h`29Njab4u2ZB_3y8)+ZHG zQ#pA^iPb+xArKH#VwmDD$eglqsu^%cP(qJf_$g|G3zDE99sJ3uqBtOGVxhA7p zdK#QCOJ6v`jPJNE?zD8nz4xM?JB`VQ%{^@^tIH&qVj6Zza)!vf3+8x->=DmGA?YHFUva_wiXALFJj0}GPb^23*Hi~?Kn=cz9X!%nJH&s_;LmjO7Mn?n4lx# zs0$YQ3WNOo;duecDv$GmW?ReZ$j;>H*YfJF#6<>6#6hA%XvxfY{8q+yBV*4mXLkc@ z9cuOrDhXRFR49H>c&z1@YYW{AL!AiW$;Ct5K!5B>?s?g%5!1TZPT5bB<}scMcBQA? zu_)Ct_#g=%m_w=oR2a0`v z%LhSafAE;@p{~K525YzxE)uCmb+}O57^VX3>Bjw z%d=!U`OYcC`(wG->Ejme-O}k;zJwHh-AyajUrlv4AJJs(kNne-z zkbHN7-*y1$vbpFYLj%6OLse6oJ^~LrXg@a4ms!*C8N=Hg%PRN+?@NhsoWf-!}r zDA-lFV}r#yeB%1g8Zqw5CXnPwE2B$Ku^gbQWMST%IGC^AU3^R)*+u0~{=pYpckKqu z2QgyQlh($Rh#FUoWl+^OXw|cegx}rcsIwmo!AMf;;A-TPT z%W!9;gGAh!JSI&}t&oS0$w6~N!S`n-J8N`F8Jer-EmvVww#AuAZ#sN9RuAOf(zWQ- z3OXi(RIY^GV`t3LZJoB7aft@wXQx_a9lB1G2*w7??SqM=<|E!>{JxIUM;h}BRx9V9 z+m-V8-XdZtP%~KGr{vSp4c^UVKoa%p>1}L@*z9|Zt~w@o>Q!Rqp4^~8Ee*%bnCkVQ zZhCI?Yi+y7NAQ}kyOv}%-4sau3JEX{SVF&a-n!h0#VK8Tj!aiR-?ATB%q_^g+hw>;Z)xmG5uD=wlW_N)O=O(I8nRVZkwRT7 zmu;w4eSD&O#OFJC*P{c4;>&V}4;;^=HmYLmSCO+V#Ww5gkFHu4gk;43G~vNBaSm^F zI7I>W8!HP3wS85_pEUflW}(XL>#%00#$m#&)pYR!gFv-?&) z!3hVJO?v2U8(#ynP|rhnzY_!pEUrvD%UqovcI}8u3nmDd3Ak-N$1SiCdspz~CO+-V z-2zSHWAkPH{<(UEL<1y$rN#**CE57hb=1-0&o{B#To7=CDyqdH|IYHrVR~`s@y1yt z;WeSI%ul&Bb{H`f;`qzJvU`v?;-sqiOnywW!F2h&xa%S7rhC;*Ry=nZ%5(|IoavnRbfMCHzEOR4z@$m9yH+U~XL0|t3PN712}cRl+I&BubAIFs z93w8)!kFh~jIY|R3p5VrD)73-q`gAhu;p};;W1{o z`0Bnd<9>0*1dFj+iZrR&H2&vp#GzYK8J19$6Eup23pU%lyi7mcfY2Uo{%viRi{Bh;hB1k_qD`m}M;zN7| zKdKyjD|mbJt&5^|@e9g{Tp?iCJy%3j)aceWa^#QWIe3EDpWPO0aS07{pFh+2c$pf! z9-B>^9L61O)X5(7%ha5G;IYQGxf=;eZ*|%dHm)N_nXW$8Rh{+l`5e$^TU0SuaU4GeKEXCUj{a+EVuHQ4vlgDo)qC-ArYYwm<>ob z0qGL{I|K0ml9s2Y2{V-?R&n59Y*S00+;ZI|9k-Znjkx&Hw)@=Ph}=i_!K`k&$8>=n z#opt)(y8~lnEUXKl(Qqg(Q;_?TAgfBmWf+{p=LOqKj{a|T*f!g2y2zx-n=;%IxgDK zMkkOGQ~e)g5kQQ`6yUi+g-H%n7E|Nb>tENPW(HGIL{CI5*`KYtn(n0q(96pHMAupI zzUje;EJE7%Kc?gKA;iYwY*>Fi+sNY1V?Yrix81aS!A+6pBRP`Unin2QaW>e3*Q z#a#Jv-shU$5J4~{lPz_#0vBfxMJ10d0iBHvA(5HI2e!=#Z*D>!#+r1}+XpeOx@6N1 zt5;u&&WDm9*hk#zPqx6!l&g=;i3eWSdB@V(`HU%t`s?b^?Y7*2DKUI55>vUqNk{rH zC}+37VSkvIrPVN1d#qu1@zyA7px66gx$V*j4Tpo*E?~ovatKO};OmI@GZ4{P2l934|5MYK1%C628p|&q~T^6m}s! z9u7KsSETQ(aeTqoh|9i^q$DdJ9n56fXN9b9TM&@ON=+Vx-6GK(N#*r9wInsgCeovL z{yhQ_`ao}%`D={qas74o3id`XO8!+8^W$}k9lS4PIa5a5_YdxL-E(gkn$m$4jz)cN zXJsmDt2*l&yc0}Go+8{(>_F#dRx7iG<50Bt>kdY%0MVs(IcS8-!*?OP&V&hT(5&4L zM{jPIe+uPe*-5stUAfG-?zAE4X2D&({#>#HcT2#26xU4Zhj$eBuq`1k%@Cdqzt?sa@G(+T^Qwm%v>$uw1RGQs$Bs|udpttjGH=GX z%w-7;965z*^Wz?PH4~I6#Ib%^$$IQMW*aJ;E^O$S&&?H1u@>)WaD?a;Bh@6Sc(oq5 zrPQhLqCOxNF}LA3?yKQeLH%I<%pIN%fAPAniY8%R#?&N31jFs78-ZmGH~GNV#c0i? zn!)7qGxsn0_b^@5ZZG`6cKGmtZqVv%cg;~SElG^BD1wiiD5#CUHJ}S{_P&Lrog*77 z&4OC&#{}1eyF7kIeIG8o2;`Athizn&jCXCF^&R8IYI=NmCHbRJ)j;nw?fZdqMMqv! zW^T`N0B#HM)Hd_Ly$@E?(=&cYk+yh}24gS)k z5bBlaPUdlfIxA+|!aKitp8qYxtiI4q;eCyY8_Dz{`KL!Z#?y?);>fqQWE%+EodT-h z*@4i?55;^Glc#0TV~i~Wu7u(4HQ@wEvDkN8jVxI;bk#!erYp{pzap2UeBk5oA!<^6`;aLZ2`?p(zusWmThvuE=pyu&r^`7|zv( z5L=CrizjZ<8QQ^(6TKc8KfMK{{vA5zna($Vo;|&;iB52z{>`6lCS)Jz45b5zmI0l> z&>xczk+D7>9f*hEvz+TKA*X9ZVR2BHF!GHy-E@#mE$ub2EgBykl)B~rX^7FSNxvtP zTl&d+AzZy7&aQq9ZrrWV?brz^Z*+?CsS!Lyp#>R4HYT_GZs9_!a1bGpEP0X5{CrT5 zAlGA}^IMxlHQTj^X21-4v*5eVW{8u}5%$1e$EYskNU zy0k56QXoGOXeu+1j&oa)(P8FxA6Np>0KkI#IS z)`3p#q-eXdat)KoNnZ2OWRTyf98csYZphC3JMao24O<;N_cL&8bY3>C3JCf*=(y@` zI9G+&M&Xd(FDe+Q!D1mwa+zg7W}#R)`9{Xc*px+IdyYToJu%_eGM3`?oIUrFk%o|A z?Lp}V5DAVy4TLtiqet}suY~i|$F2?ONl6|TC*STGzIj0bJX6*gC%7JbxV(7Fo7?N` z(bdE!61M?-D?(}*hFEm6S+{(6m+P!V=2E*P9|buQc+wQUi9h;KvYaIH_J>dXc9_c) z;n?F)v+W?x;_85)heO&;-=!*uPsKx>b;Etinz;b6=;x)LXAxXnj6YIy39IXZSU zn?tAQ_9lrh7|9^kuKkGl>oTMdH25qrMZ8C2NZ5fAhHtopkbi7Cy;*m7zE?J=ktg*uR5a z_nYC;l2>y#^jtcxn8mhvhXt04%{$^U_kTEh%YZ7E^?let1wl#aPAN$NY3c4x>5`O^ z6eXoZ8l=0sQ&L(Q79}9v-SD61oU^x_^Zxdi_uF#GT5Fz}xo7UWuIs=jnMPn3d)5<{ zk(;)muK@yQjbNU4_a0ui1KipA6ld+xwEk6QQ(8i1A7d-~6iA_c>y!oVHKs@cZQu=j zED8Q=uI=EXoNM*^(6gZ9YxbkZ(pYR*3TW9SY{iBv7n7Vun4Rfux+a7^)-M^z#RxUr zS323^t1q#u+RTQf+56_=yk#}IlH5OCIH6puCOHLj3db@LN*r zitu5i6hyec*pm`{H0KvjcD~aT<47pFyv*CE=wZjQJ*&Uqm!SQ-!BAgqxue`GnWrO! zim}eIFriVkOR4I z@pPfL7bO;^?@tY{<~zR3dmq&u0GJu42yj-vk@@7=e_^^+=DEEid1etAa+*k--u#gn zzqjq&tTW-KXENucRGy2)b~Wj4ivasx8Nc|Mf)k2MNr3vzYr4!-yTOq2eUrre zF5MWyJNA_i#ZS1hQL&2~9je#GsT?@W+enE_m-)HiEGZEM=>%Hlf+D4Jq;fkbm#elb zqOTOoIH<{H5gm~YBpgZ(bruSv4dxYaC6C^dcjvXWt$&bplET|bA(~sPoK#C5iB{(d z(e)N{E5>Of;Z4-Cv0l3y+q*yl-211; z<&ABK4R5RsK0Nov?5qqEr!Lq_Aa~p60RSH9^RCAnjT$apb!7n zW8V>^Rznbq$X>nOGSMtV1+`+le3ObG)P_8$5_2k7F#|tnZAOx%>^#UTGktcNI;GNd zre;hUlTePWSY&Z6r`-L*J^rwA#gSh6`I#``J5`6^@IDG&dAv{3PthpXIb}KKy>ovZ z$czND!Lpg=(HCh;0^Ja}UzA4dTM9fWSnxllYm5;=U7%^-IS7jX8Zcj)=3mOc=(S}( z{{uPEx?)r%xXY80F$z!aS$Dr|OY^Cnze6=fmFIG-NE5Ht4pn9XsBH=)zZkzA{1rcgBI%fr;cE zZxDCK7_;gz{9jxEp!nVW#x2fm>R0QU=+x3;MVm0@HNkJopVUeliBw&Jq1|I zG@`}MMkFs3?;)u??J|3zrsHcA>&^TBvMbYSeXX{bHY9c8x;;Ih;|)I5r}C$_hNc)z^U8BMK-Um*3Owv;aN z9JIDH#mKJnYW%2HpnAnSaxQ%k@&@l?Gs=Q1Wr%}L^@rRu?;C-oI~TESAn0*928w&&ztGv5I5d9WE9 z-(GhWJ&fRziFLW7kq?_I!ds?Sd+jkY94SifZs$RTi>HKMyGer!k$&~%KbJ#6|06y| zI2N6EWcsH>DVdEap*4fs|5sZyR0U zu7lJ!twVH!Q>vouBg3jP96qM+ESO>;pfcYfkY7_qOSE;GqNVRf;TXm+{}$kP)J>aP z=xRzHq+>q{mvAI}d7b7iB}ly)VnDDT`P4zsb(*qdzla@ODnk!`*V@1@ zL>KDocQgB?F)f7C{6{L*m`&#_9(1L(A9r3HoXm`LS`A6!l4>RqNgCvr9Z%~vowhS9 zlSN*Zd5nq@JvxCGxdi!jD|n)~hG2yC^tnr$qkHUw)@25HpqV_DTijX?^=aTNom(tp zI5O85*1`Cu`Mn2&MMv!T2BRSbRg{?5R{s-#4&brE8$~iS5`#~cfr0ow`sVEaoiAXz^eO#qTjOoLDF22n60aO&#JQvifMZZ#k;t#fb8rW-7KYti(+A8jM z9?&e}2sQPsyHaz*k;VA!9`(YmPOo(LiIN6qzcx**M8kSltJa;0=KK`6|F8NA)ZS>( zECia`t3neV_4vJGb!#hy|MAkJ4JIUYINZN;TGB6Cu$6B5?5RnL%gd};9-_ldN_EiL zW+B}eGsl5*kBIVS9bU1mOPI3b2cq7aA9=bzSJc-&O<|^?_WN_a*-T3kj=|Oq&`#h` zx0waFC0s5l1it`sI8J?h`5$DkPS&>lf^L?9hJbku?K+oVQn1Aq`RB_6lm?4WQJa2Y zZ`iOO@s$?ax0S2bw7N@$E}_?{VNJ!dmUdxNdRXdxVPx`XIqswSbaS%U@LB4Y)yJ26 z3CT~-42Vg|6fSB8++P%w4@sz>Qovk&tAp{=+;c(}7;q^cA#<)KzwQiaD;DQknPjdJ zAxv{$b3fUWb9%KgLHV|UK#%NB+i$~!;~$q%`{OhO@)w_yOe<{`>r6$Bg}nS#j@ZVY zv{2;(Q8|W4*sB}fPRNXgaEqAw9oG#Gr(|4g@iy{+bI3A=2b{yhE_pGk^NmX0^HYfv zQ$qoYN*pd--m$x|W{9YhsKn6fDl(xy6MkmdhI{Y#zalP2Es-VWqZ41EOQbZwocT@dtJe|ergB*4mA6eK;$}4 zxm|uDid4SdBlf87@YO42~&u%S+LdO zP7L9u<^F@CiG&!?N>!gCGkV!sCcgDbwO;^2njHwqe*589z>oxS4Zmm4(@j1`N9Wb5 zTS3xtG@cTw(uK>@B%1IFg}JlXwy?eMI2Rv`$>LB)+j7??YgB9Gwum&DZnLp$NpC|r z!5*eVxjJWcQoJFfBHE6pc0TYdJ8j_FHXx{U)5oG(o`~)I$Q}2nw>g?1Q#sIdrl4}!K2I;bl$7^5YDEzs#M`WBDiCRwMc zZf@G9zJb1ND9^EJy{q@quuqe-PylkB;au)^r&AmU*R$;whRg^)OzOBT{?Hwqd?&o0 z5B1jV5LjNkP@g5+0X9=vQ!)br>4w1f2S@9D)6q9hibMu3FmZ;QfSqVyEo{u+0Ghq@ zC+<~%uGT){Ovbsfkw7)Yg~|~sUpY}5krO=HG6YhSE3tYw-vz=XiX@!v2jf2sBBSsR zEXHF_RLq2$Kag3}m$d74cZ6F1kkuj>N0;B) zaNEJWX&ouzLhusC9MiPTCdw6-(XZg68SahaD^klt>f!{z5{!H04Wbiur8CfQ6bAY( z99<)(oJ0Eg8)|jMK()gx>uIO3E8477pahx0efAP53CmC?q8-s(Pl4c9TYl^+bNbqE zkDWRMEXN^?4(Oesx|h^nJkby!1!EpM}};jD~J)ma2&N zo4Qi`gB{(cEAhWT=P0f{-Q$5irHX*v!}%E3!jgLDO29Q>{JJgI8hx6T1&kM$6x$V| z8VG))Kk2+hnOPFd-Te5|mq#sZvm?}HR*hutT8?v@Tzr+2CGLUW^k>XQF@hw?+gUl+cPoK`4VtCJ;J>BNa^*^dw@ZnoWTJmZ2yuL4*NPWkZm{5+;2N={ zA4nfn!BTACMeu&pWfwha^YL<8la;$OU}yG~qGUmU#2%JwF_dLfaU(eyK`Ze%w2jDEzX>TviVymaJc+LI(m zsgG|bI+)r__>3-dYShUc&eitvXGa$3Dx*ru>b{4LD89)m9C-~IkG{>6TKL(RveV6E zbQvWTdep0iiRV-Oi_~BMZz=(us(dhnpZC=Y?r07KvVUmyy|`9iue~eBlxX9*8O!TT zY<9i#_PXlkdrp*#jBOUVu?L^LJPX!&E#a@JX}#q*RJ@%U@X!xL)!>8Z0nK_ar`|k^ zx{T)MFRE?>*^>C8&2a3Z`pq49Ax~cmJJcOTc?4EV$ps-;w21Sm75; z*D6!mL3@3p&k099<$8w}5!8rx@%IFA6jvCTb{uALrsXmRBQU)o4L6h5lsi%H+6GCz zh##0FDDD&j_i&nTyZ)fRh9PK2{%xz9bB60BI;I{=P3S|6+1E{66~Bun%M{{#)3!eE zeeF<_*&{eW)Bv&$zmv)Avg_z%QWLqx_8ejNyF;9OnCN^7THz}O9kl_6cfi3E&dJYf zTO@qFInbn|{!a9mad%)H$sO^Y_3U9@Vnjy>W>JP;d0c>Kon9vgda&shn8Nf~&B--m|qCDAR9xzR^LBGd*YertXi&ej&OG+KiLY70&IA$~ z*PbDdwtp73q5}U8YOv!S(g#dJKU%m!?z8d9e%LO70H4gOe8BYm9%=1CRl`&*;NJ3< zo=Iy;Cm=0qv_iEK4~G{u1pRmvf~iLigPLR1Eay8ijbvPB{h^kQZ|6v9bhtf-L06oXhYaoDkM=s(MC-(1bzP%Kknt139HrS&zpFY9w!T2Hxq(rr6$bzYz> z{;C4SZKcE8fR{I^5JvYRWLHW#xjv(YqX zsU%UAKr&0?u~Rk4^?(aAK+XBBv6S>Cpuu8W#s5viNB5;Tu+H4BjA%#M5N!jY?s=?G zl@HyJjJBxS3eZR)NUg}AvF1K;@R_sqdI{^~lz_d}e^OVShHnK3gew$LbXkUTx6z5# zRo>rl&ZuK8hMol;?^PkBMqq}fYF-305(p?2lQe>!KJ+7aFFDb5?0@;7psQMk{V0qZ z%B7&sy$7+Jb$(gnY{bZ3>4yqHf%SASN`GEi+r65CJGvIvN_4`Ozv{iVJ{naeI(8Y7A$UPQ6t&;NGB+35 z?QQK-EAT4fLhMBLH?_IB`EkQ1SC;gqXahY9JkVU@o$p&$zh0lz;mw5gvC-jtuG$}e zjvcXV9VQjiGOjo@*N^@29B){EOI|tlBJDw{ZW8AHkeP1BgnQN9J`LrL^DLD;*PFg+ zHuU^`J+*6?CAvHIHKCUcTq){u(NvW4O~yscz}NWGw-9c?vcZ$EA|i(1BPoDlS6sc- z+K5u8nhPtrvJ}F#x^(h5MDHXB3y&;#Fqup5e=WHvk4US-rN@uW)E{FUNL5?5l99KA z{_jmJvGz+HYS-2J=U7V(&7WAZufMGv!Gw~$e!chft~3Wk4$lebXR0mD-?lsy-CHbQ zC#y93@SWKszq38;m)nE~b4}}Si9GR^nAz&ObmfnhLxtOZ)k$*Xo18~ptYZm>LWsVq z(Rfq_3x?uSPQPEDo!5%R(GN_k#$p#0S;%Lw=_Nn&D*MGWSk!=(-X}$MMAWp|sE;My zrG_iHr%KOR?vgdZGf=4ClFv+eDxgMpUCvw?-V|A<0~4hzNkmaKyPHU$i-(+DCMwoR z^z0{{rOMM~N||$!3pG*kDF6hSn<&vi^Db%U+Fz>hRr{tq&gsz9ZJjk>aIN99IQ+vF z!St@F@dS{A;=TAy5WQTO;DTs=%=Ds=y6nwNs;cpM)$>{%$kUctTg0y8+NN-zo@Wcsx)|KiAdu%@&r>n?^YupcjQv zA5+S;e>7y3iV0_+jEKjv&=fbwawt$goq<(IC^o?gu^G;G9U|tyC9p$(oWX1mwvIP< zDl(QoEbfYeRp!hfC5>;cz3I*=>#GNE$Exkfq6AfU#PG{`$` zm{YOZ0qjBfKCKr4Cz;|Y;cQI4qeXK}Nd*h@r?{|M)90VQqF^kN4|}X#jnK1PmR}Iy zG)24DU(b4iauiocG-;N?jOFnY!mH?xG+xWSM(u~Ep|%~Yn)o_`-aNG0YgK0tOVV6y zzzhU7InEMIho*<|w}}m@PTy`ePmBB)AgLEXQ;0Rkwcbgq1QV3~*bXYFgwgOFBT~B2 zm0J8%Qi$hcCHV#j{mxnk6DK1PxLX7ZSk_CJY)dD1ikUpObYk7I!^a$3w|rbjT+$?> zguf4^v*b3#stE)$hhrE6sjo)2%rn3m#LR#5);uWy-C?r>>sab^!ca`=o%j;#UWMk(o}h;e_a3+co~iD> z`!cVsC(qbH6#HJ*VV1f(c+o=cgy4y}nY>@!<$ezporZ&gq+)@^>ABf6*ds708b$-LJS2%|2u#hZ+1Ix;`WqHg%q2dYjhg2+4( z8{D{J^(oq~uej@S3V3#p9X9Aa#K>r(W0OyO)ATAQ^aELe+6Y_7n_bwO^@s%If$h42 z(2*&bA<0YmWW+68)3mKJY4^ za8iSrUC08K@6=^-?`JVcS%m6DFq$3A!X^de)G@zYamNEcA||tP*lc94Z+>*A8*Ses$wv5>O zCd%JxanNbR;GE&y=WC9l@p4hZ&JUT!1I=J%%~3xEO@1N^ONHNBxS<+4d4*ey#6c=% zmCmMFXJY^;nHPA)HrM1fus375Q+l=pvvRfY*l6Ti_DC-TP-E)-&;NxeXN6F_rY{l* z(}RT%R+2n<{n|u%5Jfke*V>XvZy*3+r_Ao%&k`n-SZ_%h!WlcmiPYZBz6!^2vu|}B zN*9k+EOTT2{(}kE=6C&iZdWw+!|-#dk0?aG)z9aQGiT15Lntu4{68f)7PMGno~TuA z0Nn<^E=;7r3#of27w#$b*@`2NW)&$vNi2zI7jw25Vy~qJaaj_HD4w<8A7k%74II?p zOu&_`=h4(%dEcKZkie;1HH$`h1cOUh^?QrO=$JMbw0wA+##>q!M-T|os;@j^n=MB@ zf`wk;(XQifQJMYrNX72)tTTdH^Cr~mM$AEd=igXEtOG1D^oIE2P>{f0G8xpSw(c-1 z2c5mW*pVgh?z}C^BlEQe{R%o%-7K`j^Z1CN%loC}@}bobSqW`ti2Rp3DWu+wIJ7^< zSeDCoGOX4WxeWZR7IHSK!W(C3( z)>MFoOL|W6?cUiALA znO8<)g&h0)etegDaX>W3Z(_|0KIkR%R~8hRhqs4^^axA?(j%kxe+e1fzJb7Nbnj*W z1pfxXHASo(3`OqySc!aT{tM9gnwlS5|6;337N~*UB(I#4+%teKtq$`p>Y3YLbOk-R z$B^>G?Kl7Pn(;e4H^}#|fg+_i=*Ex$vR)b*AUqlZ^yz%xpFgil3vPxoP>>GNA*834 zlNm=tU%%U8)K>7 z*b9P6z|7BZ-CK>PUV9SY&BY4C$o~-%`U~_9q2Ij_piiP_W=G23{wNznBk@Gd?l!5& z|A(g{4N@)>G01r?eS0a$q`?=GY2W*-dus;%XPN*V7PN{l?*5mcVskb0rmcE@cf_q3 z_{i0b2;_V1ff_2}9-|}mN8P1lJ*#1AP@9*D!69KVrJg3K|xXpF*MhN0x5Za@) zyEOr~r|4g=R};ar)|zCa-2Qb>U6;KI$Q>z_#kL`Rc~)@USZrA2neG9{n-6|H;BX+669V> z*l{z_u=)OASaWtiYB2F_hU`DCV=c54g}Ys4x0b7!1ELJE)hLF4i2oOmIT9%GOfwc# zFMO_;4lx#m^g)M#vzQp}&);_U5&joYGbkPWW|;BMhE)qOm=vPY+_|+zUbbS;{w|j5 zLhCfZ2fV$4b|?Q*aRl@f0QIXQ0aR9ODwBX@=`h5|oe3xwNVYp+pFj?U7f6LEzna9q zUplViDlXWjMk|zY`#%1A%YYZjmXPt{#d6;^IjK9)>F4Kj2_}`rvaF9SAe5NJRQ$Bx zsWBYzY4W;8=y;qh*+bf-;CLnjj-f{IisJ!b$wY-q340S*8fc`EUo0_|9E;{yX<>zg(pSL`$o*uf#cT{U@&Y9WQ^8p-l=8 zapzwE47-VjhaiYV#9QA}X2)kg-2sM!RUs8oMs_z4T?o(`L7wa zCj|rMWE%Hp`SV2lWeCJ5gKt~tk9F@oLG$=2k8aEc;Ect9PXspbd7N4?(;H&-4on~s z^z{b9cq9;_Qg5LxKzlYr11pXKr39!_`L$!|a%%ycIE5afu)saStZ#M_*lcgrxP2iS ze0XpP)3nR(09BPKXM5j}K{de2-#R48pzSj7|7&i&bkY33sQ4ac2Y?%#7O2FR%56f- z|26>f9JvVrdVz?>_0>hE38e8{4ZI@BoeYWv&>V{N>EV;lEsul8P)LHG4s8;%Si;Ja z)A%6n|5MF-$W-5K1k}K(w`k4KOY#Jb^JAKBlgA#<&8^LN`{hqnXzoNf}3+S%xDe7h-ff-OVkD)I`7rbQLTVbr2v<4S=g9ni??S5%BPxrfpw$+MPNf z(iQV#^v3{JaEWb;SIh>#n!V;hRsw-CDCLv@VCd%oFT?XQr{=qlyzNA-ez!2Z`u*^~ zOy`e5x%-0=xIbqRe|&41Tj9Y_86z56)4s`l3JJ!3X|6O1zwAv7@yYlAhL?)pY%5I& zS{SJQySS9*sHQ*Ki!e_zglz8r*ADvIT)lWjfKl>BrI0`RGm2yqV7;dB9kWXTA?06w zdBu*ZXwn0!7~bQu&6iX*j4p?(#bCEYPygP8PHH`XUEInf5zrvBH}o;{|1t%-lW4ht z3NytG4FYREDEYtM+4>QvH4!$&qi3m&gQq_J&_QLDFr=bXq4RI&96T~NgoZF{|YRq7at{gxMyj0Y<7x47X17esh zyWheTH@ZOH(gtvf3ws_}1F0@#h(BP>31~{|5U2e{{l{a>06NADa3t(*Z5LbtFizh$ zo7#y|vCI_sAd{d?S^@UvBLYj3yAQ21rQWYPkS8G0?=2o$tBlC7YmhgHEF z^We-05dC!!wlb3(Gzr#qjm!rSsr4H|ULF(gVq(fHmdwXGBPU zhW20A#yKu@0&9ePCH%8ByhKpI35<&2nnG06cy7)C_7h^eR2;s2xlg%}Sd4(^8>vL@ znlDqWPr35R4R=8v|G||9psada6PY3MSV$^~mxpjA?6F}ws=CvRLo4K=@GcPk*;;=r z9TokVAr#+t=kE zn@3qS1Uuf|ZX4C<)>Igyf~in9bPZ@?G46$jLPTsn54Y39VcEKNbHJm3$cbhETs|bU zAV$Tmgpe+59v6GS3QE!I61@RYTiJkojV~Z!$m%(O_{TxX`Ne#8S_q9<*iTffRzKv> zV~8Hh254MFO+&m4khVG9Rn+n4VHqxZsW{f0Ln#f$C{W`cvYxB~pKbxH;qorV>=6CO z1aNh?s8s)q_P-9ALmcophq3wOB!53|vMTyd&Qi9qDOlN9jNelN-1DQ2aoWXTexah? zs18wy2m2u9^%Syd47b&}CQoc`XB!X#zRG>b22T0iy-p(0Dqu}c*vy7v%Iu|#4(lVy z2CPb_D(} z?}znz2yNlJD0}z*Y`bJcFHr9!4@i4`JLi0@g@ud)PD$g{+ zvp*lXHC*T=|9`FkXSly-+W7swp~trST~rVYPU#CwPKRIMOW z4DIia690q_`_(T&q;Y>%=spM-7XVNfv8d_Vg}QR+v>0fI`zfoaQ}lu**ZZ0LdLA4V zPoN{D8pJoPfH_UV(E&KlQ{DiEU)WRTcnKbyU<3jEpP%ABWL^9*&JX?!1qC`gpMqvE z$eMj1pCBJdQKo>q+o)?Ac!%@A*Pw48Vtm67cu`nj9`+CKp=Qv{|I_^mrpg3bU6zAj zHNn3o(Q8)f&H6}=-s;a3J#aTGr%@N6aZ)aVT@~sG{JCSBzmDhJDPs-r;aK}UynlWO zMCdlY_XFt1*SX*Eg0k=9bBrGq?-scg(FI0xG$oJ3tg%Tb7V%LYg zTgOx~jThK`FSS4bGMETQ8uDs6rWiP)N)F>vi10$J;V(S?ezZSh_>V(wdiO3Ism7gH z;#;>D4I0PIwju2_T+juf}xT%SK?m`W6@zNKpU2g?!~59~B6uN;^bmfb*K%rqPJ#cK77fTdi6W!o z|NEe0>_Pdm><8$c7|cI>`O=#F`csP^k;er6l}i=4le|GUVX7*;;Pa2*IPb~4ybnjZ zujTys&)ohsz<(QNc@miEt%3Xcf8IW)@lqmARlo)3_DV5esy)QjgKvN4G`#-P%=7k8qij)a~Z0b)J z0_9V!09pnC@)8gQ5-UE+aRH|S*%zWkn+Gh4PwqcN^52>VH06X)ALTBn8+t>z@ntXG zQ$Kt=F>sv!$bXP?Uop?^3J6G|1GI5XNDo&eyL%cG^)uT1Y8JZzqj1CKb#qn&%@i$w zdIx{>Ys1a;#eCh|m6e)ttPv-ppT8Yw397r?xNrbBqBjCMr8jV{I`%n1ZSY}59(WcH zH!3b>ke2j31$1(MMrGpq#^t`d6%v*D-*XFKQqZnBC#+bZx zgunOP;nRC@7H+`RScogtA8GQuAtng`nFo6A!=HkC1D*B&Opcf)8fsNn2@gl=)*o>; zgY|$Ezy-^S!BG96YdB&Zs9zEXVKb#3*0Ik3*MeXkg2K>0c3Mk`n= z25!I(HV+;&Xl3QOl}51!h`1K@*HZ&uz-Sj>(WHBT$-gwynT>4RsFj-R4S4w;qV=3s0 zl`tFc0|-Vu$V?P8iJhPzhuu{hU->M>j-+Fs-9(9({Bjq>Uv1-Gt+)Y&R0{}(!pmlD z2O_Tb7CZfb3``C=>n^1KX}Z@i96aO*oS>0!PFr-L`W_&t^MUk50(A-cpmc;5aLY1I z8s;wwe6Zc~09X6=1d|HESd|UrodPaO7d4R9S#&DFFtdu2NuJ%G^Y@nqPF=Q^xdN|A zBWTv_EG1QTWj?P=0FuQ#cbqT^daTPpH3PAdYpBo1R|e2BTr`4 z5?^{KOuhZ+Abje!{VoX+3Ie9i+k>|+B#14ht8L{cMu&=+K6wYNKuMpGrCe(mkaU^w z@tV#DolWz!x@Qttbq!0RX*EYt_Giw&_lGyLwPOIQRtsq$9om$4QiuGDyXrd2toh9V1 z0fh}Ba6CflrpSNhRLUP1k%@)`;bDx7Zv{W}@}G^NZTqtedvW1dqsErA!A#Xkespb} zet8d7yEOeWR0hNU=kIY)y~U&QD2GpZP(NZ?veJAXd6N`n4{$j?f1#Fs40A7XP6YnO z{LS^wjfDIfeP9^wbBmL&l!UoB()_0SCEA%9?@d+fUC;`X+}N5RP4jA3KK9r0G8qG) zJCVTY_{>G$W6Vyu<`kGDK9$VxBFA?t3koB$2(WL!a49z29R`1_m=q=gD(qp~@A+^B z%fU)7%v5bI^3yUrM5k4PPxF#8gzQothbcYd@ztmkm0JD)H_I_Z@0?$7&@h9+#I1->VB| zgu(#GKQw6CN7OwP7)cyP_r&Wfpt3c~oU%KMMO3_p8h7voGgTx;WJR59A>C2o9$(d* zhpX}qq@7d@a7+*Ax7lIO2}e7cE6Ue6t(}1aviTY1ouxe>$Bpw+Q(2$TdiM`KenQs_3EVldLQd@;?_V9)=?=rr(T~iXl`(`;FJ6%sVK0Zg(98Uf4aJ_O$X>Xh~Uuy6H{bU6Jxm}vD zA$=RGz~{N0W_dMc+JbTgEul!qr#|#)+#JB%%McF-1;TGe6$s<-BYjo z+I04Bg~>*8BnWaHRbuz*Ca!*K!1}@(pGmQ67}pwi@M2li0__xY_`6G5fJmAhr>jFoVHIU-09mQs=&5-b)PSX_)dC~K~gsE)r;Bk^PlBo`<8avH191O{!L`A;Ir{V=wr=X3vS1%m+Bkumr$&dD3*lvwbK{Rm)mZ zC=UNjASZaFrjHpm54m`44?qunZkJQSNdhhciD`-4`hv9LCrZ4=^U)%aXK85ZV!7qJlea#)^21DV@ubW@m8Nb?B8=7`1Lo9c&E|G$~zo+ zvht8kPakRwC~oR_>7$%O&L(r&Byjy=(-OC$+M@2}5RT2J>DTY*@^ zv(^#A>=xBVGiLrkjU4gebe4~8s_zRC7!eFihxm==dvveuwCc5iOS&LW|9BBxZZ9Hf zrF+FjOT6Jf$JpYZnjK)6cCXHrVc#P8`>`R_>s<)JLaSs0BszP6VS8 z^>=+QTfwB^AIx=oB}#tsUr+2;9eOpOpFhXh@j9F#SFzimWDIAr8_`+`6KcTjoQcJ3 zoX(b#5c~|g+hamcES69cl3LJgoTLfBLr}J$ffiD@~O|6P%Nw z(y`KVxs6TF%+CdlPuAnpSAz3&@?=wYQgPfZMx%ar$DJ$3NvS4D`(zqrPMJ_bh^68iCXEKU&Zr0y+C`U zd$^?>U&Py3gmvCO5hJ&?yE>R=Ks2w~bgoP{wQiDIlvlcaQL$Vp@2)#%t$gp$QvR&V z41?*rE}m*S-6ebf?C3r{y6n*z^mTCurwvh2@S}ozLno%djSrc#SX8ros~r(Plx1BN ze1w7E?lgKcu12v%J3dSqh4%8G;{+9#1bhBK@reEzeggM(T4p0^UFYjA zS+*O^UibSn>T~;)Ua4|r)&HE(SWHWOZ=5*F84^*a(Q#J8rn;nIKz_v6E7p@3;wX0} z7R*YbrjvIG@zjU;4CV8NG`=@hGJfeej@<#f0ZGh^w$fE>j`8bs%UV`Rv!KX0t&5SM zox*byeZx4yXOE7~+WOB^KR2D5J@y0zSL%}D)3KqaF?ibT)1etka}E(99Vs-QB0H9I zbZbS4ThQm(|HR?^c?j7%ZXjTfgexD->bDd#HL5@F(_%ay&wWEHxYbhf;xF3OGBlYq6N8N}j!HlRwjSUAAotuT4M7NYUcZiY~#0C{)p4 z=^LTztZ~h-r|D#zcyA`&LB6pl%Xt4{b#wP_+OFKKB=sQ^g~J!m8^WA57_ z9yOtc6c@;`)xnW?!4Hq5VXam5)0q!xXdo)Q1Hppmcg9FOcRq?XrT$e-s*||lsId5Q z-3+@To8g3b8qX!p8@rv**y7@iXF=I2*LX9I)b;ko274!}6%1%}*+4Xh*(&#UUqeWs zyvMj=Q*Wr9YP)t^S0YG}*r_ZW9OBWj_t8*Eatc9KQ!Im}7DQS9U z%Qr55H{FcM(1diSX{;NCG1Gav>rQyGU_7d*84Jx-N^CEQJj-Am+wb!PcjO5b#rq-+$AM>y%%4!W+$HEbov*>m{QfAKFdSR4Os%rK&0TwyOTliR0pd+%;@^LHN zR|`w@cWcz+E2{x-yaxu#`!(@d8^_f8gZ9<#p~6O!aQS>yxr{|a;~D@n-K)nc&-z$u z@{I7!=6?hy85BD?R$PXC$RD?%;#wD7zgU;vUj1I~rbNrtNMGSBrl?DA@XLHFboQcA z&Sg4omVKix&#ovKK8N4inU zY`Z8A?B9OCgnP-P-nGiDMc+o#09%vqN?%Huwndw5z-5qDf(oH~k+R`cx*OIAED-y7 zMnd1xv??Nd0*XSc#BdgZ4E--ydVW%MVZ~)&O%5jmmM%}#fmdF;wxgw;EX~Sr0FJI_ zw=`=vGD|lLK|)#=>Q+mM{gkWcXh<~3Jn4+6^^E7vayT(tTzsPEg+^7Vr%z}H`1HzLcIbD$uUwj8Y_M>T^$ z%|1(H#yr@PgV{c`Bi!%lN~zqHdt+lKyn1)6if2-JyoQl*oI!84Y-Wjg!I1)@tMGx+ zmO@6OO?QsQ`xi97(hgseHo4Uid-k{=C`XlZe0x06<yEPb|#RVyi)w@*W8oHQraWr4Knm>%7eNqVF5EI|iwYbs3lHMVz6Rjia| z*<1;f)yNhN(PcTxXk3!VX!WVZt92#9p-}vaL~qL$Wo*eB4qZ%-6?b^G6)$P=5$=J) zUFvrA77kqNm@v6-Y~C^QL=xf7xT=_aWg4Af>yjg!{i-|%R}6gD#jF#`EotafGUZ9O z`8j*Rm-|w~rDTs9XNs@A&ohJ@FW4R)U;o%Czv_xqv^Y5|G{w`lzL64OSQCM3myJ_| zWZ}L(l;JtZ6pJTkAA1_ZXgOHAa8b4&-7YqGh#eb4D(OF8vJ>E9eX#eH%@>n3H3Iwr<*ShZ`kr5;-Z(pUNi z`o%eq2nE*cAr{6($WI%w-hKYE)ST+`ZdEl~s!mrCsT3q>Q~gdNJp7dbqJ}v;>-U_l z_XGsQN4+`3VI|M-%$~}hjV&bOAY6!L)GefwNtD~HOi7BfevUltO2nauHy72el~mYy zyVpOGXwII|oyY2$j$Wza+1DhY8mXLynMBp0q4PjT{XG_sqvj*f94{3i`6Nr$FUkH1A6FYYe^WRsqaH~*M z)-YCDQI6_+-k%xu|Jr-&u&TCi@mmoCP*L&#f=CMzN=YNqwdqcg*dQQ{q$nlbUDBJD zMoK}tyFt1^a?||gI`^w`y!YJadEdXUf9+?>T6?a!#vF5uF~|4}W$dmPg-+t=j}4Ym zQJwu~`qC+vrh}!k1GU%+xy6kn7P%Z#>{o{$!A)e;Lu1CJEuEQPYS$^L6^pe6CP-BJ z`kf1>pRH(3XK}BNg<49M)MsiKSqP41v`3BZj?Wd3S`fSLf?SryiIN3jWo-04efkoJ zyqWw|iFqbV57uk$Q}Gbe3aPJSCv3%XYs!s3Ql0LbJTM+mRQBg8v~l_{s>*ohqj_MB zknwZ*6unU>1NwlzWDzI|+al~X0DW*I6B4kvGxipsVG~kF+$F?tfKmzTM zHH$H4Q>JHoeC4F|@k3-s*E+wlrjlc(eZ~~gV?`cCK9NSG_MW7(0$Q8c5-GixL!?V( zwfbsPs}_&5cRyc)Zi+vf&sN!)a?DwAe$MBV&A`S0gP|rNGgI!52a3lLaE&~2+&%iD z$G+oYO#G*8C9&udHd1we!N>I(4RhKZE8!YerV&SdgxC+y2+1YBh}J0()((tPEzHa( zB6tQqVPLK7HlmVSHs#w_^2R&lALUm`jiFjaM$i+{4ObT5>StzTA$|6$!wsi{xue4V zYbeg@TR6symsOd|^J~huZS%Hr} z^2RwX3$3qHjtz}to#IQ&Gx@TAgf$@$WiUFtqrp}6Xu@y~e|^fO4BjN?l&n2HIlepI zqVU(4zfat{u~7&UjkP3a1!H{?3yF;`SaH%#t!jq-4ia3qz`5eGoe#fzNXk`CDd7(2}bE`CKCHCl(qbop{2r2U#E7;>*;U3ueMr1_2}sI0myBM2*?$s#X24oDd}{NVmDbR-cs12`$blXq5-UOy#IEL7{N+(;twRYPt7Eq zYw?d9v`U3;fvy3(thF~O+w`s{91T$W^l}iTlo<34I}Mqi6JCs$nDx-+R(zv8f|SI{ zn6PzT)-18ljI+3t)w(aesj+uHSEN*$_hiqTg!Yh3|+y{zP!d*o4r?QXsT@Id>6t2r18^u)P}on=!c*rgoV=A7yqxv}PMt2Ry~1GU>EwCU zEfX7;`KW^@WYQJoX_d=+lZ*?9niz*SQ;Q0!g_=j(+P=<+AE7n4YTv`z$T!(cy-txS zYE#O(Z_X+BNgrI#riOo}$fW%UCm3)okysAc61PCit1IPh5|A_PBnDO{L=ve(i{JTh z5GlQGot3AcR|<~Ap6(|an_|<~2<54rLllKQ?G}a)NnO!&gmZvgm74uS;>lOqo zQCJ7BElj3OjgY-rI8H^cZRYlAG)4p^^~%8G60@6@54iMokXSmLmgzw`(Z%NT(dqgk z`_A^_ksZX#&cQV%o%eY{NlQ?5Def#U8sG$=t(47FW*^MCKiM7kbN|c|ZQ`snMTKj6 zo#M`Xlib-bhrfxUHjA>X?=#Y5Jhfqu-#IUtf}|5H1t3+WsbgQFgPT?ZeSzsr)l%Q#?bQa{wJX--F&1q`pQ=Nej4Y*7!o@G(~vrp)GepI@>v(z}Ah`0Oi z;nktSS8v^1WeuNE%TH8O=w|o;oxyGiyqA(68J};cbv-Cwvfz{ru?pqgISgg^7R$qT zSwA#vGIvC|KE_#IuAB7(w~g1;idqyKWPJx?UiRF*2V(k(Dfn_95kdk|o~@YCEcXdj&*WY8 z_|MN4XY-m8Dn^htwDC#Xm1V_rQe2fMO__1p`{TuKIiO#pg;zLF{+^n7wcJwE@nQ}NXc_m-&c?&khEb#@UDdZ>nr+JCK@@dljs(%Yl1Am}W6s3%rkTaI93B{e zd9pYgAUkLRL58i!!CtsLEjmEvb~)1Qh76VOJvsO zI3iVm(ZSsL5~KUVacP1WLtc$^v+GDUTOipjd@WJ|Ges(^*%J*OCDrgGM~>MFvF{@K zVkqCv;$g9vut)N4H#T{P>=di2_~vJfyXZ+Qhg#2~w`EbD6B^D*jwT$lA&j_44G;z4 zCC`PK@eAm8tCqQ95cP+O%rvc zD287!O(5ZU#BJ~DG0XSUaeVU$hi+DcS{&^23ANj;bQWYPTyTBK>b#N2EV=bwWuXp& zh6Y$vx-T~e`!X}%NZjmf$>ZU@2x#IH=J$6 z-$=B*Ew)z?&W`-t) zFSNg~5}>QB$AWVk7|{9;LEuesW3sNRB3CJ}^$@X0`y6YoFPN!RC#sYq9Zd)H8Kqm5 zBFrc@6+MLukA-`h1n)fR<&22XEXvQe)7x~At8K@04QJ{M1P9k43pD3iRV6aCw+6JkM0v>8T!0wian< zsXFwPFRTrW;pe4sJ1)TES|`*`s>hNK!zHp0qdlW{v;~?*ufAx+kV&?+q6Xb6zxfv5G?wp;or$r!mskA3#Y`E+uquT@95L`m-Wt1MABtUCf>t#On*ZF zmz1f_X^5{VW0@OY80EGyRQAtSAhB%9Z4GD*Z4EaMZFjE7F1Z3x-^Z(T_tz>)g7>Qu z0Jwfnrv5^u2YxI)@vA3fIzFFzDUI2%D9IjI)U7 zj!*K-T<)x1|2}Zit)ql}X*Bp%maIJcHtdR8JT=|k#{oa(Ar(`0;g!ThjomBSX^O@? zC1WFg_X|wi=D;y39sRc$o=RiTaw{;|9^^f=#5Muuqz%>19LRQ$=jPEkvI5B$wgU^n13Xx*Tu6*r3&vO8Qb+vgsf2 zDXW1dP(h?uD7wG8_@0=5>54q->}*)klwTQN7m|8c%VlVc#Qyx$d|hSm zu`m{4E5WA_h2s$B5@DmoOuT)c7>72oT>bJJ%J&V`50GYbds0>Rmp2VSL@bJaZ^eM( z2Es|qKb&0~uWdlW9Okl@${5$!uwgYn!pvS7p2yRXuc79zOlZbnMvH{k)|t4;6H5Kn z;Myih(~EaZ+fZLAb&PgK6jUQIn>gus9;QW1P5Hn$OT!OiGESSsY z^U#|XWM5`PSk$0WnSWkR4;dQlV!NG&4=_G4kL9R%C$-82-g#r87tc~TyN*_N?P=or zYp5zXDPE&!Cv&6g$TcD%Tj!e}KioZ>+H;Q=wz^2>Zo#-=>cD7Dv< zAnZ{o`Ob(;?26re&-?AiNZ#ALu5D?mUGlT1C57pYZ*sWM8^%Xo=1|%ApbPG%o;c#+ zVctht?!T;+i{IA&)JuWzFjbC?>;INde{+;8Hd9t>UXpnw+>mU8N@aG{eMY}&m&PR_ zIKg?AXzB(iHWkBh+1kv_X-@E4T$);JU8>cvJ!Sm(mbvq3O4}#$N%Mxkvt|?)b6Jj% z6-No4$%WZP0j70q&eXM(mr~KbkgW$-bfo30aaZkoS<4Na?Agd-$2o3R+i9#xQQyuE zSJ!5nt34S73Fxyg5w3~Sbw|Y?g4e$l*)QZxcB!?d*r`i6=?_H?vGCw2Yn!dTaTs79 z!C1DpeBmw}LQ_SKyKQQ%1yi5$ zcM)cY@kqLQPU(90>wPt{X*FU2>XkLH@BpnJ`k%(x`S+a?j-IGXXg_Td>2i8wCgSpj z$|n+*op4zGL30!tLR{O&%kv^>v`liVo57jb!k}?xdLsSw$@#6!L9d{xNj{I1svv{M zXdR1w>u39BbpK_NPOtoqwb-Wy?NM<>2Y_$U`y-s0vsK{T z9jN)ISkwcFOP9-I%DJcU3HyQFpc1ndr;B(+ksu z+CUoVXxuj|GpWBm?lM(uS@dQn4AON`&{J1vu7l`6UcAL@WB3c24dWsO{Ff)kVILSKIN0Y$nv0e4~+RFn+cYI=zUA?i{>JfNw51GWqN~zhIt9&Mh z5%U4{vg(UcX+L~N6%&p(ebH#rO_Lf?V}`{8g~aMO#$RAG(5a1~o^cwGKg{L$)i zm0OJoVeCy3?%^tL_%6R(H=ke>FeG%Ps;SIGOzEDpZi}2SK zM?MJILwx2~E7`FWi-h6+zKp$g+z8a@QwC1|3YI}S<)+@JqdcGq+dI17Nfwt-&z^&% z{PTiL`ynxuyM&Kc;^YzEnNu`-iOkA%ExND1gmAT9Hu|HcGJ0 z+nuK>rZ0ve%&7~1znnZjo$q>TQr7ZP3I*h%(MC|x2aJ8!wg@#=@c5|7JV4cb>Iljp zT<2@kRFU(3$f4GllO3+kIj_2s4=S5%@w>st4sEsDGKSlPbW$2d0#aOwEL+)6l27t* zYGCGRob%KZb4u?d)J|knI19gnmZz9dCSROjuRw@xjCeRyAD^r#w$^gLga~hQe9n?E z-=Lqa>sw?fl+Luo$XZ(20W*@|de@6Un*gb?9uN|8{fVlPe#6OjXvtYMV~S5K+LObA zdBe~4i})(vG)I+Q;^ZL=0x%8EvX%A+Je#Yx#RX_MmA=8^qP2zcd7Uk(-9AB;$O=^L zj8dPo=);2|^9^YwI0N{x$44JhvLSkU9>~dRnp6zf(Z&BztPar@uRW2gL_9HT99p!T z;w(z6+y@!}ZAEP+yZqqqP|!+S4k8Gt+g8&WhxZycP&iT~oOrA{&*9^lN1gnH{4qJi zE~Q~mD(hDg?-S$vU_K>KqI;D05LR+0iQFZuiPq`~@4C>6-x>|9x`qA*)K%W%9al-V z)weVufzVmbRgL+LNykn9siwq}IqEI-rPW~bdbx8;q$OkRTp3kO+GQadriu6%E9GTX z2I*X*N~dz46i6%dUd;uiB(-gKIcO`@;-HG(SplKyb+>J48zE;Eg1waD*QuovwD~qJ zl}9|r=hipl>cTns;TueP`odFq^zNzYs`C^>cEzvhd3~}8T#I8T5s44|jXp=ZuB5CC zQ#$kQ5(jU+2{Cbey`^ajt9g z?MVvP71B~8MnDzX3)PsX7BARwx6>7$?VpxOcJ=f1Aw_MZowtiM2boJ?g|ic*WzSvu zN4;bc9M2zB*Gw^->&r>OCrt?>6P((rz9bz@5-u*QBTsg^yl|B@9`E~jxE?iPx7X6Up=(2mHd;eJ;>sa>X*17sGsUBwi*hH zWmKL&SrvPuK)C9kKQp`P6hq>h&sYSnXhCEQq6`WXnf6Pm`y487#2m#I%^ohSrFIn` zy{EozUyj0;F?BaDqC#nw+wv>)6c7BH>Ku5XxEqlPlhZ1%#Bgm$EwrlnR#|@ zsqq^QMB2?oJh&CSr)bwPvJKlG*9U!`W@_xzSuUn3Jzad3c!WLWfW46MmFe@e?MDx? zl@^tQkKmI1iy=wo!JGNog8O?NCy(rMDI|j<p`1AR+G=C?I`zo^MXY?<|y zFF99FJ=vQGj6wAFZm&l3|~{i$`L;;*ahdM=`N{AtQ(R1rFap(rId){@oJc1 zXMn3MC;@9I<#paD2^;1@^Z75-f;D}N;hrD4^RYU5|3vDnQ*}tvdv|x={+|Bq#M3oH zS)ICsHy!5tRpSFl3!eZt6S%9>u;tj=nQD(^et86&x6eW?iGTUH^+@V|mMj zREgKKM6*rjpnXtiduEno0}|_n#18tMHzuiG?RLUshEv5mHG2$QzUP#Wy1U5o#puF9 zxneXBY?r5JSM4Gdd}F0XapRlCsd47*g13{N;>GXou2`O;25499Z0&Q0)xuR3;^;d* zGa5RT!&P@^OXj_b$(C|t2{s2;JZ22i6Sf)4MxE2*#W?gV+C8+*dOoVf_o7DF;20>4_-ZRtdRf@HU@=70Wmf!aLkhz5i1^O`M9cK z-S6^OBDOWt!{;fQQsf8*k#aGLa`cZL_iiMj8LwEBa9c>LZ!;@L&|a;YYu7BHHHi?J zbS^RHFFLBLI4RoBu^es%8^pK1p{_8Cyv^{Mp=yxWAV=8KKG*wd^F+~^3N12AvdzA% zMq9}Q#5W_;*{RF3Y`+Fv+A(w1_*Rr)9z8DBt-LPUsd6vdBk^;tfpXJjQ4A4Ey%?Mo zI5yqkShF2({J_cJ7OU}Ul`P4eds08_$$BIrHu((|dr1KKsXfOn_G%;$<4_s*;+ZNC zr$-E|RoCdPOo@)Au$Sy=I!{Tu;1XQ%)#aIW@ii%`txWIvR!3)t%V$b*=Q=@Jc7iQG ziq*`6T&8)+02_|V{d;aV$iM!hO#b}ATmEmZDhgGrEN(X_4FGyWmbDZXWovly!;8-^ zu60?u@zIsr_`}S2lhS7Yeu=on;bQ{soDz9`#DGUj7im z5;{4){E7ZSlGIJ#2=xltduf)c*t@_0WP*qX2X**0GIGkX#_u17*dbhPr~0-tE!AO8 zxHb4oA-1=qKBwPIYgMc4X+Hov83^DLkKP?=-p&-$Kzqo&-Aq2RnG_%v%pY@Zt1>>Y zA4tT`kj)`hhpy%%$5Ci*R8kyUyR10>Ws`OnRBbpvlfiP(_95RiPln%&HBs`^7-kLy-zut;R{~d z%1vF%!#Jyz!^cYD*dFpG1TG-vmV$nwF~z^79Gm3Q0w4v%q4E^4M-#c97g|Fw)z{qv zWO~aciX)?mEAnk__V@u$Pd<-3Ryay&UO519o=3!2nRTA&vH%LSjRM7$_w-Xenul)H z8qw1W5gKZNq7cfm&pj&JTvLbA_(=b?W7?bNnkApYCl6E7`x!(yme0OQ6@rmSglxzn7*!V9Pd;1uK29qD7X2prhKi!UgjiQ&+&-g6fd8ZZsvJy zC+Di*GjHoh)9Xhmn+gGYr@eL$41MOJq3qg`P>Y&6`Rc*DFwVlx##%f~O~v%oY0+63 zy0K-)i8f4U#{OE#MZ%Zbv-J=j!o_P>+UaCGcwCw2%Hn9E+SRRjk)k6*S2tSu&$Et@G*(BsT=-GJo9sRcXzyqeKlL8}}J6 z6Xu-F-aq$7^d~7|h>W_)2L}6`fIuTV3D?IDb%2RL&Y)l%wsy$YV+#fG-BDTvy5GCA3*FCSzqd(_b&pOn8xF+qY z3_FY@iWTjdsiAU<)zl8trj@6~%*w#>Y@Uy8eHZ>fs*Xmhb=G0bYcj0%1;hRm@k*6l zJUyE%#x3cQbbYO^l1!P@c6|D>ZTb08Du+GdLd`D@Lqt;M0p`rDDYnWckrk4;^BQKX zZtA(G#v;5#bm1H&i9RVChPZmm`K++mj*Nt(v=eX=d4`vgKCA9TM9H`3Tr7mP*VfOs zvu_D!mafi?qv=>l$zn`4uW`>-@bum1u!6@cCa5Nq7BY^=0N+jDBmiF@d$v}fAi34diirGbVrS7X8h$M$_< zl2Hx$G`>kC_&yByWop}!`ZW=6aEMX{o^--y5l=F=f>t6=g1S|DHlt2tCk8Ft*)n8( z)hl34S2{?psPnSSR+^ZGk8%+VgpZeV)>xmfz^~&3G;Yo0@p{z^WJL-c-Dfx$Zls;c zjStt}J0N$(W$aK!JVNlu@YT{>zt8v&qD4|X#G;~huJinz&G8>x&hBFrEz;x|r3Bsx zr;xtOsHr33gq-b0UsalP#Vb*-p~OpGx^*``&hZ=ZZ-Giug>HwZdU7E-MC>fm5>}gN=Vn0A8U!!qzQ{`E**F}*!5<(SYW!@LPU}HU9{g&5SGBV#ddH>@>|xN zmX*X!3G9qE9Y_KUPc?5+nRH?GLXg~hs!hLQ~ZNh;--MU||;;-!o zT)(r#0zrV(0`6-6x`B)zKvqKZ52iQUa|M8J?FrDQ)ltu134uSAxpUz#;O9r9zuZ7W z8-{zJh@bNO>5~-qcd=|(<^QQ}?xVbh<$16b=z32Hlr}PS9bR^mrUXkZW8vP`@0kt< zxXGE+P-%3C52)j(8LhZMj{;m>?h6O@KiNe|3@GMRTf*f_$-Z^pzrL^EUGA%I4koX^ zM)XQ1Qv2yU{*)VlEQLJ{xDIHWwdXDnz6Ziw0Tu*n-P_Ao2ys!*_>T2@W55X1z>)&Bt#}#($pI6w(x9MQ?xqg%Bz4r}4jFQn-MVH=1x& z@HYU3#RK3tcT5vgQU8m@NJd868lpy>9_?kvBq*taSsM6; zb^-G8=VShdgk^vImlQwaue%1-&e;R{R~-mC-DjJy?u+Sl2793wk$Kf)bhdkz%@1^& zgC0;K@4Fo6?jpZlzkf53_sA&d&NE{Q?j)wNF|$f0Q2e~B7>y^q^Ozl5lYC=QHF$&+rrBm@HEV;TuIw-Y+$;>v|F6X@TD?0C)EQiT}l^ z52itM2qeJR>pt+t^81@eDNdIgR-!FPIs*uyx0N+4gaA|HY8@!UQc)rOh-U}cK)YiY zwg;jT{4V)DZl;Iz0{!6; zZ{NEU`9Q}95X_NGEu6m|g!jZdNkB6si1F1p^*1*I;+=ue48b*!g>;nxjJ*2C@zgml zcpCbifMU8hhZThGaDgr*-&(rT-QPx>AcuCswB;JSX}{=ZK&CN$B{-#-0`oYIM){F~==ik#vD2l-sOpj|Eo zlwtY+FH+m(O*yD(tOqrIDFt7E77*V6$3b^D01Tu6@I52kuZc5;#wv943tWS89zx5` zKbw~Ee*;fRcpHEV{vt@*T1pAL4bjId8{{#e)CJ zq9J`lrFrsmxh3Ue2kE>=y_Y)j9njS5}Dt#ov;})JHs8={Z6c8l~G{h*9@RoGmr26L{V0=Yr4cHc?X~bFZc`L zYZkX1E1mzn?vDBGLjpbn{K-3o>MDTQ`jjkD-6nuRkE5hv(NNGxXi{9QgS^~Mars|n z+yhLv>|eNezlcZt>?oYu(*XE!6HwLRe*v_M3IH8^9ud3G4UkH*uLXS1fX|?z@-kSc zcZhX@4^ql=p|IN!5{KJBpvf>bJs#z*D=06n2W$3T3Q!t34$%*Iph-v{1oC^S6xifX^f02lOYj)^e zB(XziJNEf&7HyB=8P1^VImH+0AJZki~xm%0}<~goskns1^PY-xqTf)S_j{O zCO`ukLD?^wqDn!x=S~j>vk%P$(CM!?w{tAUdDOXpXvN=rUYr2;hv~3H5(?|2Tcfj${;T0Acd_2t1Hjb# zL3d1`G$D4MvQ&0Clqxsz%(WfN*a21E3ED2~p^!5Efi=MkpYK zhYoRC4j?;x9h_)A6)kB8lt-oQ`jbe)NU)pou!%ZQJ@bZOilg#QgBn&H1t6diYm@hL z;(kr)e~G&h5m4)+o(XS%uF|FFsCl5T#A|>_Y20f8*wV_eqj|6y;q6bkp8x3N+Wh9f ztk>xGFxDYgU%avoBFcd-X~DfQdK}l)T-T)t$%;v`in9_`^JQZ!o8I=XYV&LW)Tfsk zqpcpTq7#lI*qiQ8+*aRi~ekfQ5O~snS z=B`u$Cj*B(5PL|;$)k|jbXRUOLc_9!^rK%K0SHe#J)8?o`Qd(Y0jv!2wH1u##PKSA zF5Ex%H2zR%A9(r2{$~Zomq*X|iAtd^Em5E~BxL$UT4$_AaxTnBXdlT%BKMyY>?_ zG1|~c-2#1ACBn3}WfeYV@6q^Y=Y6If2XsoxeCt79)Vp)atmM-uVlWP!RdK07d%?a&`9+4)M=pMP51$;4X!vU?}t zLVL+o{koqP9oS2{Ij3bsh;bphX7A&>STC5Y09V5612(niRXi0^Gn%g4NQiSMi&0(C zlWv-81vNmqop?OHm@MO7-8aBibzCA{l=5^WCR3Y{m8+B?%f2O#We@*x_;?!1UG3x1 zp*ny^n8+Nc^{zz_qq(Hs?6b=)7a>i?4I9Jp?2kGD2EL!+@WGCT!bhJQ!kO>V)cy(! zv&5y3l+6CJTzL@%{pqXd6a<2PaOY!ddO^eU#lh4@LI&155CgN{=rKUphu}UQ1FRiA zkAVtapz~j-fB>4r=D)uFN{B(nul>-R#x}djfvrz32-*eLBAwyX`G8+GDe;dgl zz=)42MfVQvvh^E|9WQ*&?&J5jIcdN?JFdRI&y`EDFNZ6H+vt~?0(5094P}~P@Auc> zfZ-}}TaS^O_fM8Ho^=$bjqy{k7*a}a@Zh{xXl?UKG`S(3VDWFLXg0|8_5XqP(;sr< z75NTuez!rpAm{)`uX{Z$S0q-Uj&OB&ns+h-ir+}^cRlRTH| zzBN(3S{CFGnfDej$vq$fpcx5c+3h2M!gmL(Cyr}imx?NwbXZjlBDMmY2))8AdqKs! zln4+39C8QL-;~(r^e5)PCi=UaFT5=FDw1DBO;po|ARqoU;n+ltV|@b9JL-WyX3kec zNCTP31pwD;+jnQWD?R$Lo^V)AuMVscgGJ@No}2Tk`V=~pBDijdb5pED-X3{FFihrO zrvcLDZOqW!1?ffm%VIl54S7VJwYKl_3q{rUu*(+D-SgQ z#Sfyb%03t5a9kKhCD|^rdzkbIL{$Z&W`vBW0mrUyE&jsI3iwYVglrou(m)lEz3wrSUUGY=Vr!T-) zbiUuAAP2stwXD-7yNRO+D3!hmP?s*tpR}`!@r6*ydTr!$;l|a9XW`6{eto)GwIiP2 zh9=njj%{Yu=oSII_296}mEXX8X`P6hEI`(kNaYa{LP3eD3&k+- z;4|kc!=jiTaDyZc54MpuO=eKM*^xSSt+r3diC7906R0cXgb5`__3e_{g}9FbtxyMg z_TGbdFc1dK3#edHQIr;ULWN)$1Uvt_^eS35MOf$<>O;mI+4u z5<}*}b(^|hjshcgH{mkWf0UJiCLSUO&2k#h>I6x+nmJo{axC85hOGwl#_1StbXqsj_FYjY z#MMemdFu2RLgyWAra~bgt&ga;o$;;})QbyT%VN2H-|2Ov@PpN*Da^jtdzl%8WC|8G zab7H9bM3&aCxR;c$LSJ&YKlK6A`O~|hh`{0Jt`f%2SMoUqDH#FC-acYvyp}?MzO%< z2G7vxa_&HWbpePeMxc`X%_wgqFciG}zCUHiHRWl@~&vUgC z`Dw5I5VlNn5NLpc4Rr9ep~pl`B#y6wx&p)MvkrQOSy2T(;@ z5Bo9~ftTeo_vm|IKlQXP>Z^hs^4O*Q=o2 zf}FB(Ca@CoUIm3y6cpaJzwr&{x_sh>O1hj{4Ij0yoKF!rH;0NKQzU7$Omkf zCcp@jAf(GG727pVhX7!H|ED_tH6U5;V5NO`clkOpivW`NsBHePpoLR zewyL9m@W7-l=k*ZjtCTdXY3Mt{QLj;Z9q6x!O;EIzbJk%;OZDKwrWmC?|T<6i-qh0 zsvoF7?c3imykGXeEF|bSVkZ|oq$LUP-I7FwnA)#V{c)k2Y6$qXqL-ziM!!eoMhh;> zMDhM#0?H0h6x+e^_FcXUm)SxQ@c+*o_HIMy*$fLq+pCcKL?&C2$!hX{a~Zg_&%FObg37)PGqp;HQ&5K{NRX=JZo#03li;f@YHF zP?YxHe$Gub3Yy8+3!&B*rlb&DM&s*=e_`BzeB^u&E{hL_eZ%^-Liv+Qpb^lU3tSk% z&-eTv$G7`65G0m+4@oZghr5@d!wWY_3ba3d$#pKqb74vnzzE(jaR2v1 zs-G{U`!V$NQ8{GE7j8^X0S*u_bg^-M-|0``GJphvKw;|wm_Mlxn*U`E(UYISi9a6f zlA9_to+78amoD6n-xGXCK*Rm+!gv4t*ewQ)F?F2mVpJrT42&^b6#MqSkMX}NNdBZ0 zXpB80B6I&X#>+R{-}0lT{UZo50bLZ22KzPH{~RCKm#lFhd8DoMe)M}d{JxO%lXav1 ziys8ZwK1ULLX=Dg%4O$7vmdXdE9r*-+q zZ-IBn6bkN)bLHZcSwp9Pc2wLKRPDuS1{VQ;1xRD&8d>3mLC{0e=*y4&Z;fe4FS-4v z_sSOj@25}tJG9kOAu@hpZZFOlcpL3tEuuF3W9gWT1#rlQ_N!8VCxJzEGmSmHIm;<&SM)Nfd>x$3);Rmivz;{bp$% z;n>BKPXt)*OuC>$#mYyY3mw_qlqy_T?bl&|x7Vk^k diff --git a/pom.xml b/pom.xml index fdb0c34..91678cb 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.appdynamics.extensions kafka-monitoring-extension - 1.0.3 + 2.0 jar kafka-monitoring-extension http://maven.apache.org @@ -20,17 +20,11 @@ appd-exts-commons 2.1.0 - - commons-lang - commons-lang - 2.6 - - - commons-io - commons-io - 2.4 - provided - + + + + + com.appdynamics machine-agent @@ -70,8 +64,8 @@ maven-compiler-plugin 2.3.2 - 7 - 7 + 1.8 + 1.8 @@ -135,7 +129,7 @@ - + diff --git a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java index 2e3a274..7d0b51e 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java +++ b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java @@ -17,15 +17,17 @@ public class CustomSSLSocketFactory extends SslRMIClientSocketFactory { public SSLSocketFactory createSocketFactory() throws IOException { - String truststore = ""; - char truststorepass[] = "".toCharArray(); + //TODO:refactor this file + //TODO:re-name variables + String trustStorePath = ""; + char trustStorePassword[] = "".toCharArray();//todo:check char-set SSLSocketFactory ssf = null; try { KeyStore ks = KeyStore.getInstance("JKS"); ks.load(new FileInputStream(truststore), truststorepass); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(ks); - SSLContext ctx = SSLContext.getInstance("TLS"); + SSLContext ctx = SSLContext.getInstance("TLS");//todo: take it from config ctx.init(null, tmf.getTrustManagers(), new SecureRandom()); ssf = ctx.getSocketFactory(); return ssf; @@ -33,12 +35,10 @@ public SSLSocketFactory createSocketFactory() throws IOException { }catch(NoSuchAlgorithmException exception){ logger.debug("No Such algorithm"); } catch (CertificateException e) { - e.printStackTrace(); - } catch (KeyStoreException e) { - e.printStackTrace(); - } catch (KeyManagementException e) { - e.printStackTrace(); +// e.printStackTrace(); + //todo: logger.error } + return null; } diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index d81dc7d..0e7b6d9 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -41,6 +41,7 @@ public class JMXConnectionAdapter { private final String password; private JMXConnectionAdapter(Map requestMap) throws MalformedURLException { + // TODO: read from config and conditional this.serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + requestMap.get(Constants.HOST) + ":" + requestMap.get(Constants.PORT) + "/jmxrmi"); this.username = requestMap.get(Constants.USERNAME); this.password = requestMap.get(Constants.PASSWORD); @@ -50,14 +51,15 @@ static JMXConnectionAdapter create(Map requestMap) throws Malfor return new JMXConnectionAdapter(requestMap); } + //TODO:change according to config params JMXConnector open(boolean useDefaultSslFactory, boolean useSsl) throws IOException { JMXConnector jmxConnector; final Map env = new HashMap(); if(useSsl) { if (useDefaultSslFactory) { //check if null:TODO - SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory(); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf); + SslRMIClientSocketFactory sslRMIClientSocketFactory = new SslRMIClientSocketFactory(); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, sslRMIClientSocketFactory); } else if (!useDefaultSslFactory) { CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory.createSocketFactory()); diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index 4e4dbad..7e14dfa 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -28,10 +28,9 @@ public class KafkaMonitor extends ABaseMonitor { @Override @SuppressWarnings("unchecked") protected void initializeMoreStuff(Map args) { - Map connectionMapFromConfig; - connectionMapFromConfig = (Map) this.getContextConfiguration().getConfigYml().get("connection"); - Object useSsl = (connectionMapFromConfig.get("useSsl")); - if (Boolean.valueOf(useSsl.toString())) { + + Map connectionMapFromConfig = (Map) this.getContextConfiguration().getConfigYml().get("connection"); + if (Boolean.valueOf((connectionMapFromConfig.get("useSsl")))) { System.setProperty("javax.net.ssl.trustStore", connectionMapFromConfig.get("sslTrustStorePath")); System.setProperty("javax.net.ssl.trustStorePassword", connectionMapFromConfig.get("sslTrustStorePassword")); } @@ -64,6 +63,8 @@ protected int getTaskCount() { return servers.size(); } + +// @TODO: to be removed public static void main(String[] args) throws TaskExecutionException { ConsoleAppender ca = new ConsoleAppender(); ca.setWriter(new OutputStreamWriter(System.out)); diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index 2ca888f..de3c093 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -55,13 +55,15 @@ public void run() { } @SuppressWarnings("unchecked") + //TODO: Change return type private BigDecimal populateAndPrintMetrics() { - Phaser phaser; + Phaser phaser;//todo:no need of phasers in a non-threaded DomainProcessor task try{ phaser = new Phaser(); Map requestMap; Map connectionMap; + //TODO: //put in a diff method requestMap = buildRequestMap(); jmxAdapter = JMXConnectionAdapter.create(requestMap); connectionMap = getConnectionParameters(); @@ -74,11 +76,12 @@ private BigDecimal populateAndPrintMetrics() { for (Map mbeanFromConfig : mbeansFromConfig) { DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( configuration, jmxAdapter, jmxConnection, mbeanFromConfig, displayName,metricWriteHelper, phaser); domainMetricsProcessor.populateMetricsForMBean(); + //awaitadvannce logger.debug("Registering phaser for " + displayName); } } catch (Exception e) { logger.error("Error while opening JMX connection {}{}" ,this.kafkaServer.get(Constants.DISPLAY_NAME), e.getMessage()); - e.printStackTrace(); + } finally { try { jmxAdapter.close(jmxConnection); @@ -92,9 +95,10 @@ private BigDecimal populateAndPrintMetrics() { } @SuppressWarnings("unchecked") + //TODO: add a serviceurl field, explain in yaml private Map buildRequestMap() { Map requestMap = new HashMap<>(); - requestMap.put("host", kafkaServer.get(Constants.HOST)); + requestMap.put("host", kafkaServer.get(Constants.HOST));//TODO: move keys to constants requestMap.put("port", kafkaServer.get(Constants.PORT)); requestMap.put("displayName", kafkaServer.get(Constants.DISPLAY_NAME)); if(!Strings.isNullOrEmpty(kafkaServer.get(Constants.USERNAME))) { @@ -105,8 +109,11 @@ private Map buildRequestMap() { } @SuppressWarnings("unchecked") + //change un -modified params to final + // use CryptoUtils private String getPassword(Map server) { - String password = server.get(Constants.PASSWORD); + CryptoUtil.getPassword() + String password = server.get(Constants.PASSWORD); if(Strings.isNullOrEmpty(password)){ logger.error("Password cannot be null"); } diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index 938d441..6d97f8c 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -48,32 +48,33 @@ public DomainMetricsProcessor(MonitorContextConfiguration configuration, JMXConn this.displayName = displayName; } - @SuppressWarnings("unchecked") + public void populateMetricsForMBean() { - phaser.arriveAndAwaitAdvance(); +// phaser.arriveAndAwaitAdvance();//todo: phaser logic try { + //todo:change the names Map metricProperties = (Map) this.mbeanFromConfig.get(Constants.METRICS); - logger.debug(String.format("Processing metrics section from the conf file")); - logger.debug("Size of metric section {}",metricProperties.size()); String mbeanName = (String) this.mbeanFromConfig.get(Constants.OBJECTNAME); logger.debug(String.format("Processing mbean %s from the conf file", mbeanName)); List finalMetricList = getNodeMetrics(jmxConnection,mbeanName,metricProperties); + //todo: move it one level up finalMetricList.add(new Metric("HeartBeat", String.valueOf(BigInteger.ONE), metricPrefix + "|HeartBeat", "AVG", "AVG", "IND")); logger.debug("Printing metrics for server {}", mbeanName); metricWriteHelper.transformAndPrintMetrics(finalMetricList); } catch (IntrospectionException | IOException | MalformedObjectNameException | InstanceNotFoundException | ReflectionException e) { logger.error("Kafka Monitor error: " + e.getMessage()); - metricWriteHelper.printMetric(metricPrefix + "|HeartBeat", BigDecimal.ZERO, "AVG.AVG.IND"); + //todo:avverage, average, individual +// metricWriteHelper.printMetric(metricPrefix + "|HeartBeat", BigDecimal.ZERO, "AVG.AVG.IND"); } finally { - phaser.arriveAndDeregister(); +// phaser.arriveAndDeregister(); logger.debug("DomainProcessor Phaser arrived for {}", displayName); } } - @SuppressWarnings("unchecked") + public List getNodeMetrics(JMXConnector jmxConnection, String objectName, Map metricProperties) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { List nodeMetrics = Lists.newArrayList(); Set objectInstances = this.jmxAdapter.queryMBeans(jmxConnection, ObjectName.getInstance(objectName)); diff --git a/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java index e0621a3..d5f92d3 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java +++ b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java @@ -6,17 +6,9 @@ public class Constants { public static final String METRIC_SEPARATOR = "|"; - public static final String MULTIPLIER = "multiplier"; - - public static final String METRIC_TYPE = "metricType"; - - public static final String AGGREGATION = "aggregation"; - - public static final String CONVERT = "convert"; - public static final String SERVERS = "servers"; - public static final String SERVICE_URL = "serviceUrl"; +// public static final String SERVICE_URL = "serviceUrl"; remove missing fields public static final String HOST = "host"; diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index aa72676..fdcb1d5 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -1,14 +1,16 @@ ### ANY CHANGES TO THIS FILE DOES NOT REQUIRE A RESTART ### -#This will create this metric in all the tiers, under this path metricPrefix: "Server|Component:|Custom Metrics|Kafka" - +# +#This will create this metric in all the tiers, under this path +#default prefix--swap @todo #This will create it in specific Tier/Component. #Please make sure to replace with the appropriate one from your environment. #To find the in your environment, please follow the screenshot https://docs.appdynamics.com/display/PRO42/Build+a+Monitoring+Extension+Using+Java # List of Kafka Instances +#@todo:remove localhost / port servers: - host: "localhost" port: "9999" @@ -19,8 +21,9 @@ servers: encryptionKey: "" +#configure these only if useSSL is true connection: - useSsl: true + useSsl: true #@todo: move it to servers section socketTimeout: 3000 connectTimeout: 1000 sslProtocols: ["TLSv1.2"] @@ -37,13 +40,15 @@ connection: # number of concurrent tasks. # This doesn't need to be changed unless many instances are configured +# @todo: explain this field numberOfThreads: 10 # The configuration of different metrics from various mbeans of Kafka server # For most cases, the mbean configuration does not need to be changed. mbeans: - +#@todo: change metric qualifiers to uppercase +#@todo: compare actual with output values - objectName: "kafka.server:type=BrokerTopicMetrics,*" metrics: Count: @@ -289,7 +294,8 @@ mbeans: timeRollUpType: "average" clusterRollUpType: "individual" - +#@todo: qualifiers?? +#@todo:add non heap memory max #JVM Metrics - objectName: "java.lang:type=Memory" metrics: From 5da40f16654ffc8a5b04015e40b6dad26f60a51a Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Mon, 23 Jul 2018 13:12:51 -0700 Subject: [PATCH 18/49] code review comments --- .../extensions/kafka/KafkaMonitorTask.java | 17 +- .../kafka/metrics/DomainMetricsProcessor.java | 12 +- src/main/resources/conf/config.yml | 593 +++++++++--------- .../extensions/kafka/KafkaMonitorTest.java | 79 ++- .../metrics/DomainMetricsProcessorTest.java | 19 +- src/test/resources/conf/config.yml | 1 + 6 files changed, 372 insertions(+), 349 deletions(-) diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index de3c093..863a68b 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -50,8 +50,13 @@ public void onTaskComplete() { } public void run() { - populateAndPrintMetrics(); - logger.info("Completed the Kafka Monitoring task"); + try { + populateAndPrintMetrics(); + }catch(Exception e ) { + //@todo: specify server + logger.info("Completed the Kafka Monitoring task"); + + } } @SuppressWarnings("unchecked") @@ -60,6 +65,7 @@ private BigDecimal populateAndPrintMetrics() { Phaser phaser;//todo:no need of phasers in a non-threaded DomainProcessor task try{ phaser = new Phaser(); + //@todo: combine declaration and usage Map requestMap; Map connectionMap; @@ -77,17 +83,16 @@ private BigDecimal populateAndPrintMetrics() { DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( configuration, jmxAdapter, jmxConnection, mbeanFromConfig, displayName,metricWriteHelper, phaser); domainMetricsProcessor.populateMetricsForMBean(); //awaitadvannce - logger.debug("Registering phaser for " + displayName); +// logger.debug("Registering phaser for " + displayName); } } catch (Exception e) { - logger.error("Error while opening JMX connection {}{}" ,this.kafkaServer.get(Constants.DISPLAY_NAME), e.getMessage()); - + logger.error("Error while opening JMX connection {}{}" ,this.kafkaServer.get(Constants.DISPLAY_NAME), e); } finally { try { jmxAdapter.close(jmxConnection); logger.debug("JMX connection is closed"); } catch (Exception ioe) { - logger.error("Unable to close the connection."); + logger.error("Unable to close the connection {} ", ioe); return ERROR_VALUE; } } diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index 6d97f8c..2a8daf6 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -60,6 +60,7 @@ public void populateMetricsForMBean() { logger.debug(String.format("Processing mbean %s from the conf file", mbeanName)); List finalMetricList = getNodeMetrics(jmxConnection,mbeanName,metricProperties); //todo: move it one level up + //todo: use metric finalMetricList.add(new Metric("HeartBeat", String.valueOf(BigInteger.ONE), metricPrefix + "|HeartBeat", "AVG", "AVG", "IND")); logger.debug("Printing metrics for server {}", mbeanName); metricWriteHelper.transformAndPrintMetrics(finalMetricList); @@ -68,14 +69,15 @@ public void populateMetricsForMBean() { logger.error("Kafka Monitor error: " + e.getMessage()); //todo:avverage, average, individual // metricWriteHelper.printMetric(metricPrefix + "|HeartBeat", BigDecimal.ZERO, "AVG.AVG.IND"); - } finally { -// phaser.arriveAndDeregister(); - logger.debug("DomainProcessor Phaser arrived for {}", displayName); } +// finally { @todo: remove finally block +//// phaser.arriveAndDeregister(); +// logger.debug("DomainProcessor Phaser arrived for {}", displayName); +// } } - public List getNodeMetrics(JMXConnector jmxConnection, String objectName, Map metricProperties) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { + private List getNodeMetrics(JMXConnector jmxConnection, String objectName, Map metricProperties) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { List nodeMetrics = Lists.newArrayList(); Set objectInstances = this.jmxAdapter.queryMBeans(jmxConnection, ObjectName.getInstance(objectName)); for (ObjectInstance instance : objectInstances) { @@ -116,6 +118,8 @@ private boolean isCompositeDataObject(Attribute attribute){ private void setMetricDetails (String metricPrefix, String attributeName, Object attributeValue, ObjectInstance instance, Map metricPropertiesMap, List nodeMetrics) { String metricPath = metricPrefix + Constants.METRIC_SEPARATOR + buildName(instance)+ attributeName; + //@todo:metric properties map should have only metric qualifiers + //@todo: change to metricproperties.get(""); Metric metric = new Metric(attributeName, attributeValue.toString(), metricPath, metricPropertiesMap); nodeMetrics.add(metric); } diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index fdcb1d5..e8824bc 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -1,42 +1,40 @@ -### ANY CHANGES TO THIS FILE DOES NOT REQUIRE A RESTART ### +metricPrefix: "Server|Component:<[or]>|Custom Metrics|Kafka" -metricPrefix: "Server|Component:|Custom Metrics|Kafka" -# -#This will create this metric in all the tiers, under this path -#default prefix--swap @todo -#This will create it in specific Tier/Component. -#Please make sure to replace with the appropriate one from your environment. -#To find the in your environment, please follow the screenshot https://docs.appdynamics.com/display/PRO42/Build+a+Monitoring+Extension+Using+Java - - -# List of Kafka Instances -#@todo:remove localhost / port + # Add your Kafka Instances below: + #@todo:remove localhost / port servers: - - host: "localhost" - port: "9999" - username: "" - password: "" - encryptedPassword: "" - displayName: "Local Kafka Server" #displayName is a REQUIRED field for level metrics. - + - host: "localhost" + port: "9999" + username: "" + password: "" + encryptedPassword: "" + displayName: "Local Kafka Server" + useSsl: true + + #Provide the encryption key for the password encryptionKey: "" -#configure these only if useSSL is true + #Configure this section only if useSSL is true. connection: - useSsl: true #@todo: move it to servers section - socketTimeout: 3000 - connectTimeout: 1000 - sslProtocols: ["TLSv1.2"] - sslCertCheckEnabled: false - sslVerifyHostname: false - - sslCipherSuites: "" - sslTrustStorePath: "" - sslTrustStorePassword: "" - sslTrustStoreEncryptedPassword: "" - sslKeyStorePath: "" - sslKeyStorePassword: "" - useDefaultSslConnectionFactory: true + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocols: ["TLSv1.2"] + sslCertCheckEnabled: false + sslVerifyHostname: false + sslCipherSuites: "[]" + sslTrustStorePath: "" + sslTrustStorePassword: "" + sslTrustStoreEncryptedPassword: "" + + + #key store details for mutual auth on ssl + sslKeyStorePath: "" + sslKeyStorePassword: "" + sslKeyStoreEncryptedPassword: "" + +# Set the following flag to true if you want to use the certs in the JDK +# Set this to false if you want to use your own certs + useDefaultSslConnectionFactory: true # number of concurrent tasks. # This doesn't need to be changed unless many instances are configured @@ -49,262 +47,271 @@ mbeans: #@todo: change metric qualifiers to uppercase #@todo: compare actual with output values - - objectName: "kafka.server:type=BrokerTopicMetrics,*" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - objectName: "kafka.server:type=DelayedFetchMetrics,*" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - objectName: "kafka.server:type=KafkaRequestHandlerPool,*" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - objectName: "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - objectName: "kafka.server:type=ReplicaManager,name=IsrShrinksPerSec" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - objectName: "kafka.server:type=SessionExpireListener,*" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - objectName: "kafka.network:type=RequestMetrics,*" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - objectName: "kafka.controller:type=ControllerStats,*" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - - - objectName: "kafka.server:type=DelayedOperationPurgatory,*" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - - objectName: "kafka.server:type=KafkaServer,name=BrokerState" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - objectName: "kafka.server:type=ReplicaFetcherManager,*" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - - objectName: "kafka.server:type=ReplicaManager,name=LeaderCount" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - objectName: "kafka.server:type=ReplicaManager,name=PartitionCount" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - objectName: "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - objectName: "kafka.network:type=Processor,*" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - objectName: "kafka.network:type=Processor,*" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - - objectName: "kafka.network:type=RequestChannel,*" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - - objectName: "kafka.network:type=SocketServer,*" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - -#@todo: qualifiers?? -#@todo:add non heap memory max -#JVM Metrics - - objectName: "java.lang:type=Memory" - metrics: - HeapMemoryUsage.committed: "Heap Memory Usage | Committed" - HeapMemoryUsage.max: "Heap Memory Usage | Max" - HeapMemoryUsage.used: "Heap Memory Usage | Used" - NonHeapMemoryUsage.committed: "Heap Memory Usage | Committed" - NonHeapMemoryUsage.used: "Heap Memory Usage | Used" - - - - + - objectName: "kafka.server:type=BrokerTopicMetrics,*" + metrics: + - Count: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - objectName: "kafka.server:type=DelayedFetchMetrics,*" + metrics: + - Count: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=KafkaRequestHandlerPool,*" + metrics: + - Count: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec" + metrics: + - Count: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=ReplicaManager,name=IsrShrinksPerSec" + metrics: + - Count: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=SessionExpireListener,*" + metrics: + - Count: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.network:type=RequestMetrics,*" + metrics: + - Count: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.controller:type=ControllerStats,*" + metrics: + - Count: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: "MeanRate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=DelayedOperationPurgatory,*" + metrics: + - Value: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=ReplicaFetcherManager,*" + metrics: + - Value: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=ReplicaManager,name=LeaderCount" + metrics: + - Value: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=ReplicaManager,name=PartitionCount" + metrics: + - Value: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions" + metrics: + - Value: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.network:type=Processor,*" + metrics: + - Value: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.network:type=RequestChannel,*" + metrics: + - Value: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.network:type=SocketServer,*" + metrics: + - Value: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=KafkaServer,name=BrokerState" + metrics: + - Value: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + # - objectName: "kafka.server:type=BrokerTopicMetrics,*" + # metrics: + # - Count: "Count" + # multiplier: "" + # delta: "false" + # aggregationType: "AVERAGE" + # timeRollUpType: "AVERAGE" + # clusterRollUpType: "INDIVIDUAL" + + # - MeanRate: "MeanRate" + # multiplier: "" + # delta: "false" + # aggregationType: "AVERAGE" + # timeRollUpType: "AVERAGE" + # clusterRollUpType: "INDIVIDUAL" + # - objectName: "kafka.server:type=BrokerTopicMetrics,*" + # metrics: + # - Count: "Count" + # multiplier: "" + # delta: "false" + # aggregationType: "AVERAGE" + # timeRollUpType: "AVERAGE" + # clusterRollUpType: "INDIVIDUAL" + + # - MeanRate: "MeanRate" + # multiplier: "" + # delta: "false" + # aggregationType: "AVERAGE" + # timeRollUpType: "AVERAGE" + # clusterRollUpType: "INDIVIDUAL" + # - objectName: "kafka.server:type=BrokerTopicMetrics,*" + # metrics: + # - Count: "Count" + # multiplier: "" + # delta: "false" + # aggregationType: "AVERAGE" + # timeRollUpType: "AVERAGE" + # clusterRollUpType: "INDIVIDUAL" + + # - MeanRate: "MeanRate" + # multiplier: "" + # delta: "false" + # aggregationType: "AVERAGE" + # timeRollUpType: "AVERAGE" + # clusterRollUpType: "INDIVIDUAL" + # - objectName: "kafka.server:type=BrokerTopicMetrics,*" + # metrics: + # - Count: "Count" + # multiplier: "" + # delta: "false" + # aggregationType: "AVERAGE" + # timeRollUpType: "AVERAGE" + # clusterRollUpType: "INDIVIDUAL" + + # - MeanRate: "MeanRate" + # multiplier: "" + # delta: "false" + # aggregationType: "AVERAGE" + # timeRollUpType: "AVERAGE" + # clusterRollUpType: "INDIVIDUAL" + # - objectName: "kafka.server:type=BrokerTopicMetrics,*" + # metrics: + # - Count: "Count" + # multiplier: "" + # delta: "false" + # aggregationType: "AVERAGE" + # timeRollUpType: "AVERAGE" + # clusterRollUpType: "INDIVIDUAL" + + # - MeanRate: "MeanRate" + # multiplier: "" + # delta: "false" + # aggregationType: "AVERAGE" + # timeRollUpType: "AVERAGE" + # clusterRollUpType: "INDIVIDUAL" diff --git a/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java b/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java index 3702c53..6a77606 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java @@ -39,52 +39,47 @@ public class KafkaMonitorTest { public static final String CONFIG_ARG = "config-file"; MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration("Kafka", "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), Mockito.mock(AMonitorJob.class)); - @Test - public void testKafkaMonitorExtension() throws TaskExecutionException { - KafkaMonitor kafkaMonitor = new KafkaMonitor(); - Map taskArgs = Maps.newHashMap(); - taskArgs.put(CONFIG_ARG, "/conf/config.yml"); - kafkaMonitor.execute(taskArgs, null); + + //@todo:move it to the customsslfactorytest file + @Test(expected = Exception.class) + public void testConfigureSSL() throws Exception { + int port = 8745; + JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); + jmxConnectorServer.start(); + JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:"+port+"/jmxrmi"); + Map env = new HashMap(); +// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE,new CustomSSLSocketFactory()); + JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); + jmxConnectorServer.stop(); } -// @Test(expected = Exception.class) -// public void testConfigureSSL() throws Exception { -// int port = 8745; -// JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); -// jmxConnectorServer.start(); -// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:"+port+"/jmxrmi"); -// Map env = new HashMap(); -//// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE,new CustomSSLSocketFactory()); -// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); -// jmxConnectorServer.stop(); -// } + @Test + public void testConfigureSSLWithKeys() throws Exception { + int port = 8745; + JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); + jmxConnectorServer.start(); + JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:"+port+"/jmxrmi"); +// System.setProperty("javax.net.ssl.trustStore", ""); +// System.setProperty("javax.net.ssl.trustStorePassword", ""); + String truststore = ""; + char truststorepass[] = "".toCharArray(); + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(new FileInputStream(truststore), truststorepass); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, tmf.getTrustManagers(), new SecureRandom()); -// @Test -// public void testConfigureSSLwithKeys() throws Exception { -// int port = 8745; -// JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); -// jmxConnectorServer.start(); -// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:"+port+"/jmxrmi"); -//// System.setProperty("javax.net.ssl.trustStore", ""); -//// System.setProperty("javax.net.ssl.trustStorePassword", ""); -// String truststore = ""; -// char truststorepass[] = "".toCharArray(); -// KeyStore ks = KeyStore.getInstance("JKS"); -// ks.load(new FileInputStream(truststore), truststorepass); -// TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); -// tmf.init(ks); -// SSLContext ctx = SSLContext.getInstance("TLS"); -// ctx.init(null, tmf.getTrustManagers(), new SecureRandom()); -// -// Map env = new HashMap(); -// CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); -// SslRMIServerSocketFactory ssf = new SslRMIServerSocketFactory(); -// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE,ctx.getSocketFactory()); -// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory); -// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); -// jmxConnectorServer.stop(); -// } + Map env = new HashMap(); + CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); + SslRMIServerSocketFactory ssf = new SslRMIServerSocketFactory(); + env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE,ctx.getSocketFactory()); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory); + JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); + jmxConnectorServer.stop(); + } + //todo: test for default ssl factory } \ No newline at end of file diff --git a/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java index b724373..98d4467 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java @@ -27,19 +27,21 @@ import static org.mockito.Mockito.when; public class DomainMetricsProcessorTest { + //todo:make access private JMXConnector jmxConnector = mock(JMXConnector.class); JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); - MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration("Kafka", "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), Mockito.mock(AMonitorJob.class)); + MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration("Kafka", + "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), Mockito.mock(AMonitorJob.class)); // contextConfiguration.setConfigYml("/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/test/resources/conf/config_for_non_composite_metrics.yml"); @Test - @SuppressWarnings("unchecked") - public void getNodeMetricsForNonCompositeMetrics() throws IOException,IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException { - contextConfiguration.setConfigYml("/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/test/resources/conf/config_for_non_composite_metrics.yml"); + public void getNodeMetricsForNonCompositeAttributes() throws IOException,IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException { + //todo:give relative path + contextConfiguration.setConfigYml("conf/config.yml"); Map config = contextConfiguration.getConfigYml(); List mBeans = (List) config.get("mbeans"); Set objectInstances = Sets.newHashSet(); @@ -59,19 +61,25 @@ public void getNodeMetricsForNonCompositeMetrics() throws IOException,Introspect .class))).thenReturn(attributes); Map server = Maps.newHashMap(); + //@todo: pull values from config server.put("host", "localhost"); server.put("port", "9999"); server.put("displayName", "TestServer1"); for(Map mBean : mBeans){ + //@todo:remove phaser Phaser phaser = new Phaser(); phaser.register(); Map metricProperties = (Map) mBean.get("metrics"); DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor(contextConfiguration, jmxConnectionAdapter, jmxConnector, mBean, server.get("displayName"), metricWriteHelper,phaser); + //@todo:call populateAndPrintMetrics(), + //@todo: mock metricWriterHelper + //@todo: argument capture the list of metrics passed to transformAndPrintMetric List metrics = domainMetricsProcessor.getNodeMetrics(jmxConnector, mBean.get("objectName").toString(),metricProperties ); Assert.assertEquals(metrics.get(0).getMetricName(),"Count"); Assert.assertEquals(metrics.get(1).getMetricName(), "Value"); + //@todo: test values and properties also } } @@ -79,8 +87,11 @@ public void getNodeMetricsForNonCompositeMetrics() throws IOException,Introspect @Test + //todo: check the composite metrics, check Cassandra Test public void getNodeMetricsForCompositeMetrics() throws MalformedObjectNameException, ReflectionException, InstanceNotFoundException,IntrospectionException,IOException{ + + //@todo:change file contextConfiguration.setConfigYml("/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/test/resources/conf/config_for_non_composite_metrics.yml"); Map config = contextConfiguration.getConfigYml(); List mBeans = (List) config.get("mbeans"); diff --git a/src/test/resources/conf/config.yml b/src/test/resources/conf/config.yml index 3615321..ed8b437 100755 --- a/src/test/resources/conf/config.yml +++ b/src/test/resources/conf/config.yml @@ -37,6 +37,7 @@ mbeans: metrics: Count: alias: "Count" +# @todo: metric qualifiers MeanRate: alias: "MeanRate" \ No newline at end of file From 6e29863291db6e2f2ceaa2c69a7302df0ff08a55 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Tue, 24 Jul 2018 15:36:34 -0700 Subject: [PATCH 19/49] code review changes --- pom.xml | 17 +- .../kafka/CustomSSLSocketFactory.java | 38 +- .../kafka/JMXConnectionAdapter.java | 38 +- .../extensions/kafka/KafkaMonitor.java | 47 +- .../extensions/kafka/KafkaMonitorTask.java | 121 ++--- .../kafka/metrics/DomainMetricsProcessor.java | 85 ++-- .../extensions/kafka/utils/Constants.java | 4 +- src/main/resources/conf/config.yml | 451 +++++++++--------- .../metrics/DomainMetricsProcessorTest.java | 21 +- 9 files changed, 409 insertions(+), 413 deletions(-) diff --git a/pom.xml b/pom.xml index 91678cb..2b4de81 100644 --- a/pom.xml +++ b/pom.xml @@ -20,11 +20,18 @@ appd-exts-commons 2.1.0 - - - - - + + commons-lang + commons-lang + 2.6 + + + commons-io + commons-io + 2.4 + + provided + com.appdynamics machine-agent diff --git a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java index 7d0b51e..343cb66 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java +++ b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java @@ -1,5 +1,6 @@ package com.appdynamics.extensions.kafka; +import com.appdynamics.extensions.util.YmlUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.SSLContext; @@ -12,35 +13,30 @@ import java.security.cert.CertificateException; public class CustomSSLSocketFactory extends SslRMIClientSocketFactory { - private static final Logger logger = LoggerFactory.getLogger(CustomSSLSocketFactory.class); - - public SSLSocketFactory createSocketFactory() throws IOException { - - //TODO:refactor this file - //TODO:re-name variables + SSLSocketFactory createSocketFactory() throws IOException { String trustStorePath = ""; - char trustStorePassword[] = "".toCharArray();//todo:check char-set - SSLSocketFactory ssf = null; + char trustStorePassword[] = "".toCharArray(); try { - KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(new FileInputStream(truststore), truststorepass); - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(ks); - SSLContext ctx = SSLContext.getInstance("TLS");//todo: take it from config - ctx.init(null, tmf.getTrustManagers(), new SecureRandom()); - ssf = ctx.getSocketFactory(); - return ssf; + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(new FileInputStream(trustStorePath), trustStorePassword); + TrustManagerFactory trustManagerFactory = TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + SSLContext sslContext = SSLContext.getInstance("TLS");//todo: take it from config + sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom()); + SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + return sslSocketFactory; }catch(NoSuchAlgorithmException exception){ logger.debug("No Such algorithm"); } catch (CertificateException e) { -// e.printStackTrace(); - //todo: logger.error + logger.error("CommonName in the certificate is not the same as the host name: {}",e); + } catch (KeyStoreException e) { + logger.error(""); + } catch (KeyManagementException e) { + logger.error(""); } - return null; } - - } diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index 0e7b6d9..11a25be 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -10,9 +10,9 @@ import com.appdynamics.extensions.kafka.utils.Constants; +import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Lists; - import javax.management.Attribute; import javax.management.AttributeList; import javax.management.InstanceNotFoundException; @@ -41,8 +41,11 @@ public class JMXConnectionAdapter { private final String password; private JMXConnectionAdapter(Map requestMap) throws MalformedURLException { - // TODO: read from config and conditional - this.serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + requestMap.get(Constants.HOST) + ":" + requestMap.get(Constants.PORT) + "/jmxrmi"); + if(!Strings.isNullOrEmpty(requestMap.get(Constants.SERVICE_URL))) + this.serviceUrl = new JMXServiceURL(requestMap.get(Constants.SERVICE_URL)); + else + this.serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + + requestMap.get(Constants.HOST) + ":" + requestMap.get(Constants.PORT) + "/jmxrmi"); this.username = requestMap.get(Constants.USERNAME); this.password = requestMap.get(Constants.PASSWORD); } @@ -55,21 +58,20 @@ static JMXConnectionAdapter create(Map requestMap) throws Malfor JMXConnector open(boolean useDefaultSslFactory, boolean useSsl) throws IOException { JMXConnector jmxConnector; final Map env = new HashMap(); - if(useSsl) { - if (useDefaultSslFactory) { //check if null:TODO + if (Preconditions.checkNotNull(useDefaultSslFactory) && useDefaultSslFactory){ SslRMIClientSocketFactory sslRMIClientSocketFactory = new SslRMIClientSocketFactory(); env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, sslRMIClientSocketFactory); - } else if (!useDefaultSslFactory) { + } else if (Preconditions.checkNotNull(useDefaultSslFactory) && !useDefaultSslFactory) { CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory.createSocketFactory()); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE + , customSSLSocketFactory.createSocketFactory()); } - } - if (!Strings.isNullOrEmpty(username)) { + if (!Strings.isNullOrEmpty(this.username)) { env.put(JMXConnector.CREDENTIALS, new String[]{username, password}); } - jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); + jmxConnector = JMXConnectorFactory.connect(this.serviceUrl,env); if (jmxConnector == null) { throw new IOException("Unable to connect to Mbean server"); } return jmxConnector; } @@ -85,19 +87,21 @@ public Set queryMBeans(JMXConnector jmxConnection, ObjectName ob return connection.queryMBeans(objectName, null); } - public List getReadableAttributeNames(JMXConnector jmxConnection, ObjectInstance instance) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException { + public List getReadableAttributeNames(JMXConnector jmxConnection, ObjectInstance instance) + throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException { MBeanServerConnection connection = jmxConnection.getMBeanServerConnection(); - List attrNames = Lists.newArrayList(); + List attributeNames = Lists.newArrayList(); MBeanAttributeInfo[] attributes = connection.getMBeanInfo(instance.getObjectName()).getAttributes(); - for (MBeanAttributeInfo attr : attributes) { - if (attr.isReadable()) { - attrNames.add(attr.getName()); + for (MBeanAttributeInfo attribute : attributes) { + if (attribute.isReadable()) { + attributeNames.add(attribute.getName()); } } - return attrNames; + return attributeNames; } - public List getAttributes(JMXConnector jmxConnection, ObjectName objectName, String[] strings) throws IOException, ReflectionException, InstanceNotFoundException { + public List getAttributes(JMXConnector jmxConnection, ObjectName objectName, String[] strings) + throws IOException, ReflectionException, InstanceNotFoundException { MBeanServerConnection connection = jmxConnection.getMBeanServerConnection(); AttributeList list = connection.getAttributes(objectName, strings); if (list != null) { diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index 7e14dfa..eec309a 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -25,46 +25,44 @@ public class KafkaMonitor extends ABaseMonitor { - @Override - @SuppressWarnings("unchecked") - protected void initializeMoreStuff(Map args) { +// @Override +// @SuppressWarnings("unchecked") +// protected void initializeMoreStuff(Map args) { +// +// Map servers = (Map) this.getContextConfiguration().getConfigYml().get(Constants.SERVERS); +// if (Boolean.valueOf((servers.get("useSsl")))) { +// System.setProperty("javax.net.ssl.trustStore", connectionMapFromConfig.get("sslTrustStorePath")); +// System.setProperty("javax.net.ssl.trustStorePassword", connectionMapFromConfig.get("sslTrustStorePassword")); +// } +// } - Map connectionMapFromConfig = (Map) this.getContextConfiguration().getConfigYml().get("connection"); - if (Boolean.valueOf((connectionMapFromConfig.get("useSsl")))) { - System.setProperty("javax.net.ssl.trustStore", connectionMapFromConfig.get("sslTrustStorePath")); - System.setProperty("javax.net.ssl.trustStorePassword", connectionMapFromConfig.get("sslTrustStorePassword")); - } - } - - @Override - @SuppressWarnings("unchecked") protected String getDefaultMetricPrefix() { return DEFAULT_METRIC_PREFIX; } - @Override public String getMonitorName() { return "Kafka Monitor"; } - @Override - @SuppressWarnings("unchecked") protected void doRun(TasksExecutionServiceProvider tasksExecutionServiceProvider) { - List> kafkaServers = (List>) this.getContextConfiguration().getConfigYml().get(Constants.SERVERS); + List> kafkaServers = (List>) + this.getContextConfiguration().getConfigYml().get(Constants.SERVERS); for (Map kafkaServer : kafkaServers) { - KafkaMonitorTask task = new KafkaMonitorTask(tasksExecutionServiceProvider, this.getContextConfiguration(), kafkaServer); - AssertUtils.assertNotNull(kafkaServer.get(Constants.DISPLAY_NAME), "The displayName can not be null"); + KafkaMonitorTask task = new KafkaMonitorTask(tasksExecutionServiceProvider, + this.getContextConfiguration(), kafkaServer); + AssertUtils.assertNotNull(kafkaServer.get(Constants.DISPLAY_NAME), + "The displayName can not be null"); tasksExecutionServiceProvider.submit(kafkaServer.get(Constants.DISPLAY_NAME), task); } } - @Override protected int getTaskCount() { - List> servers = (List>) getContextConfiguration().getConfigYml().get(Constants.SERVERS); - AssertUtils.assertNotNull(servers, "The 'servers' section in conf.yml is not initialised"); + List> servers = (List>) getContextConfiguration(). + getConfigYml().get(Constants.SERVERS); + AssertUtils.assertNotNull(servers, "The 'servers' section in config.yml is not initialised"); return servers.size(); } -// @TODO: to be removed +// @TODO: to be removed before publishing public static void main(String[] args) throws TaskExecutionException { ConsoleAppender ca = new ConsoleAppender(); ca.setWriter(new OutputStreamWriter(System.out)); @@ -74,8 +72,9 @@ public static void main(String[] args) throws TaskExecutionException { KafkaMonitor monitor = new KafkaMonitor(); Map taskArgs = new HashMap(); - taskArgs.put("config-file", "/src/main/resources/conf/config.yml"); + taskArgs.put("config-file", + "/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/main/resources/conf/config.yml"); monitor.execute(taskArgs, null); } -} +} \ No newline at end of file diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index 863a68b..cc4d9e0 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -10,21 +10,22 @@ import com.appdynamics.extensions.AMonitorTaskRunnable; import com.appdynamics.extensions.MetricWriteHelper; +import com.appdynamics.extensions.TaskInputArgs; import com.appdynamics.extensions.TasksExecutionServiceProvider; import com.appdynamics.extensions.conf.MonitorContextConfiguration; import com.appdynamics.extensions.crypto.CryptoUtil; import com.appdynamics.extensions.kafka.metrics.DomainMetricsProcessor; - import com.appdynamics.extensions.kafka.utils.Constants; +import com.appdynamics.extensions.util.YmlUtils; import com.google.common.base.Strings; import com.google.common.collect.Maps; import org.slf4j.LoggerFactory; import javax.management.remote.JMXConnector; +import java.io.IOException; import java.math.BigDecimal; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.Phaser; public class KafkaMonitorTask implements AMonitorTaskRunnable { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(KafkaMonitorTask.class); @@ -34,11 +35,9 @@ public class KafkaMonitorTask implements AMonitorTaskRunnable { private String displayName; private JMXConnector jmxConnection; private JMXConnectionAdapter jmxAdapter; - private static final BigDecimal ERROR_VALUE = BigDecimal.ZERO; - private static final BigDecimal SUCCESS_VALUE = BigDecimal.ONE; - - KafkaMonitorTask(TasksExecutionServiceProvider serviceProvider, MonitorContextConfiguration configuration, Map kafkaServer) { + KafkaMonitorTask(TasksExecutionServiceProvider serviceProvider, MonitorContextConfiguration configuration, + Map kafkaServer) { this.configuration = configuration; this.kafkaServer = kafkaServer; this.metricWriteHelper = serviceProvider.getMetricWriteHelper(); @@ -52,99 +51,81 @@ public void onTaskComplete() { public void run() { try { populateAndPrintMetrics(); + logger.info("Completed Kafka Monitoring task for Kafka server: {}", + this.kafkaServer.get(Constants.DISPLAY_NAME)); }catch(Exception e ) { - //@todo: specify server - logger.info("Completed the Kafka Monitoring task"); - + logger.error("Exception occurred while collecting metrics for: {} {}", this.kafkaServer.get(Constants.DISPLAY_NAME)); } } - @SuppressWarnings("unchecked") - //TODO: Change return type - private BigDecimal populateAndPrintMetrics() { - Phaser phaser;//todo:no need of phasers in a non-threaded DomainProcessor task + public void populateAndPrintMetrics() { try{ - phaser = new Phaser(); - //@todo: combine declaration and usage - Map requestMap; - Map connectionMap; - - //TODO: //put in a diff method - requestMap = buildRequestMap(); - jmxAdapter = JMXConnectionAdapter.create(requestMap); - connectionMap = getConnectionParameters(); - Object useDefaultSslConnectionFactory = connectionMap.get("useDefaultSslConnectionFactory"); - Object useSsl = connectionMap.get("useSsl"); - jmxConnection = jmxAdapter.open(Boolean.valueOf(useDefaultSslConnectionFactory.toString()), Boolean.valueOf(useSsl.toString())); - logger.debug("JMX Connection is open"); - - List> mbeansFromConfig = (List>) configuration.getConfigYml().get(Constants.MBEANS); + BigDecimal connectionStatus = openJMXConnection(); + List> mbeansFromConfig = (List>) configuration.getConfigYml() + .get(Constants.MBEANS); for (Map mbeanFromConfig : mbeansFromConfig) { - DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( configuration, jmxAdapter, jmxConnection, mbeanFromConfig, displayName,metricWriteHelper, phaser); + DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor(configuration, jmxAdapter, + jmxConnection, mbeanFromConfig, displayName, metricWriteHelper, connectionStatus); domainMetricsProcessor.populateMetricsForMBean(); - //awaitadvannce -// logger.debug("Registering phaser for " + displayName); } } catch (Exception e) { - logger.error("Error while opening JMX connection {}{}" ,this.kafkaServer.get(Constants.DISPLAY_NAME), e); + logger.error("Error while opening JMX connection: {} {}" ,this.kafkaServer.get(Constants.DISPLAY_NAME), e); } finally { try { jmxAdapter.close(jmxConnection); logger.debug("JMX connection is closed"); } catch (Exception ioe) { - logger.error("Unable to close the connection {} ", ioe); - return ERROR_VALUE; + logger.error("Unable to close the connection: {} ", ioe); } } - return SUCCESS_VALUE; } - @SuppressWarnings("unchecked") - //TODO: add a serviceurl field, explain in yaml + private BigDecimal openJMXConnection() { + try { + Map requestMap = buildRequestMap(); + jmxAdapter = JMXConnectionAdapter.create(requestMap); + Map connectionMap = getConnectionParameters(); + jmxConnection = jmxAdapter.open(YmlUtils.getBoolean(connectionMap.get("useDefaultSslConnectionFactory")), + YmlUtils.getBoolean(this.kafkaServer.get("useSsl"))); + logger.debug("JMX Connection is open to Kafka server: {}", this.kafkaServer.get(Constants.DISPLAY_NAME)); + return BigDecimal.ONE; + } catch (IOException ioe) { + logger.error("Unable to open a JMX Connection Kafka server: {} {} " + , this.kafkaServer.get(Constants.DISPLAY_NAME), ioe); + } + return BigDecimal.ZERO; + } + private Map buildRequestMap() { Map requestMap = new HashMap<>(); - requestMap.put("host", kafkaServer.get(Constants.HOST));//TODO: move keys to constants - requestMap.put("port", kafkaServer.get(Constants.PORT)); - requestMap.put("displayName", kafkaServer.get(Constants.DISPLAY_NAME)); - if(!Strings.isNullOrEmpty(kafkaServer.get(Constants.USERNAME))) { - requestMap.put("username", kafkaServer.get(Constants.USERNAME)); - requestMap.put("password", getPassword(kafkaServer)); - } + requestMap.put(Constants.HOST, this. kafkaServer.get(Constants.HOST)); + requestMap.put(Constants.PORT, this.kafkaServer.get(Constants.PORT)); + requestMap.put(Constants.DISPLAY_NAME, this.kafkaServer.get(Constants.DISPLAY_NAME)); + requestMap.put(Constants.SERVICE_URL, this.kafkaServer.get(Constants.SERVICE_URL)); + requestMap.put(Constants.USERNAME, this.kafkaServer.get(Constants.USERNAME)); + requestMap.put(Constants.PASSWORD, getPassword()); return requestMap; } - @SuppressWarnings("unchecked") - //change un -modified params to final - // use CryptoUtils - private String getPassword(Map server) { - CryptoUtil.getPassword() - String password = server.get(Constants.PASSWORD); - if(Strings.isNullOrEmpty(password)){ - logger.error("Password cannot be null"); - } - String encryptedPassword = server.get(Constants.ENCRYPTED_PASSWORD); - Map configMap = configuration.getConfigYml(); - String encryptionKey = configMap.get(Constants.ENCRYPTION_KEY).toString(); - if(!Strings.isNullOrEmpty(password)){ - return password; - } - if(!Strings.isNullOrEmpty(encryptedPassword) && !Strings.isNullOrEmpty(encryptionKey)){ - Map cryptoMap = Maps.newHashMap(); - cryptoMap.put("password-encrypted", encryptedPassword); - cryptoMap.put("encryption-key", encryptionKey); - logger.debug("Decrypting the ecncrypted password........"); - return CryptoUtil.getPassword(cryptoMap); - } - return ""; + private String getPassword() { + String password = this.kafkaServer.get(Constants.PASSWORD); + if (!Strings.isNullOrEmpty(password)) { return password; } + String encryptionKey = configuration.getConfigYml().get(Constants.ENCRYPTION_KEY).toString(); + String encryptedPassword = this.kafkaServer.get(Constants.ENCRYPTED_PASSWORD); + if (!Strings.isNullOrEmpty(encryptionKey) && !Strings.isNullOrEmpty(encryptedPassword)) { + java.util.Map cryptoMap = Maps.newHashMap(); + cryptoMap.put(TaskInputArgs.ENCRYPTED_PASSWORD, encryptedPassword); + cryptoMap.put(TaskInputArgs.ENCRYPTION_KEY, encryptionKey); + return CryptoUtil.getPassword(cryptoMap); + } + return null; } private Map getConnectionParameters(){ - Map connectionMap = new HashMap<>(); - connectionMap = (Map) configuration.getConfigYml().get("connection"); + Map connectionMap = (Map) configuration.getConfigYml().get(Constants.CONNECTION); return connectionMap; } - } diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index 2a8daf6..1acb86b 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -16,6 +16,7 @@ import com.appdynamics.extensions.metrics.Metric; import com.google.common.base.Strings; import com.google.common.collect.Lists; +import com.google.common.primitives.Booleans; import org.slf4j.LoggerFactory; import javax.management.*; import javax.management.openmbean.CompositeData; @@ -33,62 +34,65 @@ public class DomainMetricsProcessor { private final JMXConnector jmxConnection; private MetricWriteHelper metricWriteHelper; private String metricPrefix; - private Phaser phaser; private Map mbeanFromConfig; private String displayName; + private BigDecimal heartBeat; - public DomainMetricsProcessor(MonitorContextConfiguration configuration, JMXConnectionAdapter jmxAdapter, JMXConnector jmxConnection, Map mbeanFromConfig, String displayName, MetricWriteHelper metricWriteHelper, Phaser phaser) { + public DomainMetricsProcessor(MonitorContextConfiguration configuration, JMXConnectionAdapter jmxAdapter, + JMXConnector jmxConnection, Map mbeanFromConfig, String displayName, MetricWriteHelper metricWriteHelper, + BigDecimal heartBeat) { this.jmxAdapter = jmxAdapter; this.jmxConnection = jmxConnection; this.metricWriteHelper = metricWriteHelper; this.metricPrefix = configuration.getMetricPrefix() + Constants.METRIC_SEPARATOR + displayName; - this.phaser = phaser; - this.phaser.register(); this.mbeanFromConfig = mbeanFromConfig; this.displayName = displayName; + this.heartBeat = heartBeat; } - public void populateMetricsForMBean() { -// phaser.arriveAndAwaitAdvance();//todo: phaser logic try { - //todo:change the names - Map metricProperties = (Map) this.mbeanFromConfig.get(Constants.METRICS); - String mbeanName = (String) this.mbeanFromConfig.get(Constants.OBJECTNAME); + List finalMetricList = new ArrayList<>(); + finalMetricList.add(new Metric("HeartBeat", heartBeat.toString(), + this.displayName + "|HeartBeat", + "AVERAGE", "AVERAGE", "INDIVIDUAL")); + metricWriteHelper.transformAndPrintMetrics(finalMetricList); + String objectName = (String) this.mbeanFromConfig.get(Constants.OBJECTNAME); + List> metricProperties = (List>) this.mbeanFromConfig.get(Constants.METRICS); + logger.debug("Processing mbean {} ", objectName); - logger.debug(String.format("Processing mbean %s from the conf file", mbeanName)); - List finalMetricList = getNodeMetrics(jmxConnection,mbeanName,metricProperties); - //todo: move it one level up - //todo: use metric - finalMetricList.add(new Metric("HeartBeat", String.valueOf(BigInteger.ONE), metricPrefix + "|HeartBeat", "AVG", "AVG", "IND")); - logger.debug("Printing metrics for server {}", mbeanName); - metricWriteHelper.transformAndPrintMetrics(finalMetricList); + for (Map metricPropertiesPerMetric : metricProperties) { + finalMetricList = getNodeMetrics(jmxConnection, objectName, metricPropertiesPerMetric); + logger.debug("Printing metrics for server {}", this.displayName); + metricWriteHelper.transformAndPrintMetrics(finalMetricList); + } + logger.debug("Finished processing mbean {} ", objectName); - } catch (IntrospectionException | IOException | MalformedObjectNameException | InstanceNotFoundException | ReflectionException e) { - logger.error("Kafka Monitor error: " + e.getMessage()); - //todo:avverage, average, individual -// metricWriteHelper.printMetric(metricPrefix + "|HeartBeat", BigDecimal.ZERO, "AVG.AVG.IND"); + } catch (IntrospectionException | IOException | MalformedObjectNameException + | InstanceNotFoundException | ReflectionException e) { + logger.error("Kafka Monitor error: {} " ,e); } -// finally { @todo: remove finally block -//// phaser.arriveAndDeregister(); -// logger.debug("DomainProcessor Phaser arrived for {}", displayName); -// } } - - private List getNodeMetrics(JMXConnector jmxConnection, String objectName, Map metricProperties) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { - List nodeMetrics = Lists.newArrayList(); - Set objectInstances = this.jmxAdapter.queryMBeans(jmxConnection, ObjectName.getInstance(objectName)); - for (ObjectInstance instance : objectInstances) { - List metricNamesDictionary = this.jmxAdapter.getReadableAttributeNames(jmxConnection, instance); - List attributes =this.jmxAdapter.getAttributes(jmxConnection, instance.getObjectName(), metricNamesDictionary.toArray(new String[metricNamesDictionary.size()])); - collect(nodeMetrics, attributes, instance, metricProperties); + private List getNodeMetrics(JMXConnector jmxConnection, String objectName, Map metricProperties) + throws IntrospectionException, ReflectionException, InstanceNotFoundException, + IOException, MalformedObjectNameException { + + List nodeMetrics = Lists.newArrayList(); + Set objectInstances = this.jmxAdapter.queryMBeans(jmxConnection, + ObjectName.getInstance(objectName)); + for (ObjectInstance instance : objectInstances) { + List metricNamesDictionary = this.jmxAdapter.getReadableAttributeNames(jmxConnection, instance); + List attributes =this.jmxAdapter.getAttributes(jmxConnection, + instance.getObjectName(), metricNamesDictionary.toArray(new String[metricNamesDictionary.size()])); + collect(nodeMetrics, attributes, instance, metricProperties); } return nodeMetrics; } - private void collect(List nodeMetrics, List attributes, ObjectInstance instance, Map metricProperties) { + private void collect(List nodeMetrics, List attributes, ObjectInstance instance, + Map metricProperties) { for (Attribute attribute : attributes) { try { if(isCompositeDataObject(attribute)){ @@ -97,13 +101,15 @@ private void collect(List nodeMetrics, List attributes, Objec String key = attribute.getName()+ "."+ str; Object attributeValue = ((CompositeDataSupport) attribute.getValue()).get(str); if(metricProperties.containsKey(key)){ - setMetricDetails(metricPrefix, key, attributeValue.toString(), instance, metricProperties, nodeMetrics); + setMetricDetails(metricPrefix, key, attributeValue.toString(), instance, + (Map)metricProperties.get(key), nodeMetrics); } } } else{ if(metricProperties.containsKey(attribute.getName())) { - setMetricDetails(metricPrefix, attribute.getName(), attribute.getValue(), instance, metricProperties, nodeMetrics); + setMetricDetails(metricPrefix, attribute.getName(), attribute.getValue(), instance, + (Map)metricProperties.get(attribute.getName()), nodeMetrics); } } } catch (Exception e) { @@ -116,10 +122,10 @@ private boolean isCompositeDataObject(Attribute attribute){ return attribute.getValue().getClass().equals(CompositeDataSupport.class); } - private void setMetricDetails (String metricPrefix, String attributeName, Object attributeValue, ObjectInstance instance, Map metricPropertiesMap, List nodeMetrics) { + private void setMetricDetails (String metricPrefix, String attributeName, Object attributeValue, + ObjectInstance instance, Map metricPropertiesMap, + List nodeMetrics) { String metricPath = metricPrefix + Constants.METRIC_SEPARATOR + buildName(instance)+ attributeName; - //@todo:metric properties map should have only metric qualifiers - //@todo: change to metricproperties.get(""); Metric metric = new Metric(attributeName, attributeValue.toString(), metricPath, metricPropertiesMap); nodeMetrics.add(metric); } @@ -144,7 +150,8 @@ private String buildName(ObjectInstance instance) { keyPropertyList.remove("type"); keyPropertyList.remove("name"); for (Map.Entry entry : keyPropertyList.entrySet()) { - sb.append(entry.getKey()).append(Constants.METRIC_SEPARATOR).append(entry.getValue()).append(Constants.METRIC_SEPARATOR); + sb.append(entry.getKey()).append(Constants.METRIC_SEPARATOR).append(entry.getValue()) + .append(Constants.METRIC_SEPARATOR); } return sb.toString(); } diff --git a/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java index d5f92d3..72dba8f 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java +++ b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java @@ -8,8 +8,6 @@ public class Constants { public static final String SERVERS = "servers"; -// public static final String SERVICE_URL = "serviceUrl"; remove missing fields - public static final String HOST = "host"; public static final String PORT = "port"; @@ -30,5 +28,7 @@ public class Constants { public static final String OBJECTNAME = "objectName"; + public static final String SERVICE_URL = "serviceUrl"; + public static final String CONNECTION = "connection"; } diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index e8824bc..c9bc6dc 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -1,15 +1,16 @@ metricPrefix: "Server|Component:<[or]>|Custom Metrics|Kafka" # Add your Kafka Instances below: - #@todo:remove localhost / port + servers: - - host: "localhost" - port: "9999" + - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" + host: "" + port: "" username: "" password: "" encryptedPassword: "" displayName: "Local Kafka Server" - useSsl: true + useSsl: false #Provide the encryption key for the password encryptionKey: "" @@ -45,273 +46,273 @@ numberOfThreads: 10 # For most cases, the mbean configuration does not need to be changed. mbeans: -#@todo: change metric qualifiers to uppercase #@todo: compare actual with output values - objectName: "kafka.server:type=BrokerTopicMetrics,*" metrics: - - Count: "Count" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - - MeanRate: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=DelayedFetchMetrics,*" metrics: - - Count: "Count" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - - MeanRate: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=KafkaRequestHandlerPool,*" metrics: - - Count: "Count" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - - MeanRate: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec" metrics: - - Count: "Count" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - - MeanRate: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=ReplicaManager,name=IsrShrinksPerSec" metrics: - - Count: "Count" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - - MeanRate: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=SessionExpireListener,*" metrics: - - Count: "Count" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - - MeanRate: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.network:type=RequestMetrics,*" metrics: - - Count: "Count" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - - MeanRate: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.controller:type=ControllerStats,*" metrics: - - Count: "Count" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - - MeanRate: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=DelayedOperationPurgatory,*" metrics: - - Value: "Value" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=ReplicaFetcherManager,*" metrics: - - Value: "Value" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=ReplicaManager,name=LeaderCount" metrics: - - Value: "Value" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=ReplicaManager,name=PartitionCount" metrics: - - Value: "Value" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions" metrics: - - Value: "Value" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.network:type=Processor,*" metrics: - - Value: "Value" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.network:type=RequestChannel,*" metrics: - - Value: "Value" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.network:type=SocketServer,*" metrics: - - Value: "Value" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" + - Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=KafkaServer,name=BrokerState" metrics: - - Value: "Value" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - # - objectName: "kafka.server:type=BrokerTopicMetrics,*" - # metrics: - # - Count: "Count" - # multiplier: "" - # delta: "false" - # aggregationType: "AVERAGE" - # timeRollUpType: "AVERAGE" - # clusterRollUpType: "INDIVIDUAL" + - Value: + alias: "Value" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + +#JVM Metrics + - objectName: "java.lang:type=Memory" + metrics: + - HeapMemoryUsage.committed: + alias: "Heap Memory Usage | Committed" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - # - MeanRate: "MeanRate" - # multiplier: "" - # delta: "false" - # aggregationType: "AVERAGE" - # timeRollUpType: "AVERAGE" - # clusterRollUpType: "INDIVIDUAL" - # - objectName: "kafka.server:type=BrokerTopicMetrics,*" - # metrics: - # - Count: "Count" - # multiplier: "" - # delta: "false" - # aggregationType: "AVERAGE" - # timeRollUpType: "AVERAGE" - # clusterRollUpType: "INDIVIDUAL" + - HeapMemoryUsage.max: + alias: "Heap Memory Usage | Max" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - # - MeanRate: "MeanRate" - # multiplier: "" - # delta: "false" - # aggregationType: "AVERAGE" - # timeRollUpType: "AVERAGE" - # clusterRollUpType: "INDIVIDUAL" - # - objectName: "kafka.server:type=BrokerTopicMetrics,*" - # metrics: - # - Count: "Count" - # multiplier: "" - # delta: "false" - # aggregationType: "AVERAGE" - # timeRollUpType: "AVERAGE" - # clusterRollUpType: "INDIVIDUAL" + - HeapMemoryUsage.used: + alias: "Non Heap Memory Usage | Used" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - # - MeanRate: "MeanRate" - # multiplier: "" - # delta: "false" - # aggregationType: "AVERAGE" - # timeRollUpType: "AVERAGE" - # clusterRollUpType: "INDIVIDUAL" - # - objectName: "kafka.server:type=BrokerTopicMetrics,*" - # metrics: - # - Count: "Count" - # multiplier: "" - # delta: "false" - # aggregationType: "AVERAGE" - # timeRollUpType: "AVERAGE" - # clusterRollUpType: "INDIVIDUAL" + - NonHeapMemoryUsage.committed: + alias: "Non Heap Memory Usage | Committed" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - # - MeanRate: "MeanRate" - # multiplier: "" - # delta: "false" - # aggregationType: "AVERAGE" - # timeRollUpType: "AVERAGE" - # clusterRollUpType: "INDIVIDUAL" - # - objectName: "kafka.server:type=BrokerTopicMetrics,*" - # metrics: - # - Count: "Count" - # multiplier: "" - # delta: "false" - # aggregationType: "AVERAGE" - # timeRollUpType: "AVERAGE" - # clusterRollUpType: "INDIVIDUAL" +# - NonHeapMemoryUsage.max: +# alias: " Non Heap Memory Usage | Max" +# multiplier: "" +# delta: "false" +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" - # - MeanRate: "MeanRate" - # multiplier: "" - # delta: "false" - # aggregationType: "AVERAGE" - # timeRollUpType: "AVERAGE" - # clusterRollUpType: "INDIVIDUAL" + - NonHeapMemoryUsage.used: + alias: "Non Heap Memory Usage | Used" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" diff --git a/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java index 98d4467..d6998fa 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java @@ -71,14 +71,14 @@ public void getNodeMetricsForNonCompositeAttributes() throws IOException,Introsp Phaser phaser = new Phaser(); phaser.register(); Map metricProperties = (Map) mBean.get("metrics"); - DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor(contextConfiguration, jmxConnectionAdapter, - jmxConnector, mBean, server.get("displayName"), metricWriteHelper,phaser); +// DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor(contextConfiguration, jmxConnectionAdapter, +// jmxConnector, mBean, server.get("displayName"), metricWriteHelper,phaser); //@todo:call populateAndPrintMetrics(), //@todo: mock metricWriterHelper //@todo: argument capture the list of metrics passed to transformAndPrintMetric - List metrics = domainMetricsProcessor.getNodeMetrics(jmxConnector, mBean.get("objectName").toString(),metricProperties ); - Assert.assertEquals(metrics.get(0).getMetricName(),"Count"); - Assert.assertEquals(metrics.get(1).getMetricName(), "Value"); +// List metrics = domainMetricsProcessor.getNodeMetrics(jmxConnector, mBean.get("objectName").toString(),metricProperties ); +// Assert.assertEquals(metrics.get(0).getMetricName(),"Count"); +// Assert.assertEquals(metrics.get(1).getMetricName(), "Value"); //@todo: test values and properties also } @@ -120,11 +120,12 @@ public void getNodeMetricsForCompositeMetrics() throws MalformedObjectNameExcept Phaser phaser = new Phaser(); phaser.register(); Map metricProperties = (Map) mBean.get("metrics"); - DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor(contextConfiguration, jmxConnectionAdapter, - jmxConnector, mBean, server.get("displayName"), metricWriteHelper,phaser); - List metrics = domainMetricsProcessor.getNodeMetrics(jmxConnector, mBean.get("objectName").toString(),metricProperties ); - Assert.assertEquals(metrics.get(0).getMetricName(),"Count"); - Assert.assertEquals(metrics.get(1).getMetricName(), "Value"); + //commented out for test +// DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor(contextConfiguration, jmxConnectionAdapter, +// jmxConnector, mBean, server.get("displayName"), metricWriteHelper,phaser); +// List metrics = domainMetricsProcessor.getNodeMetrics(jmxConnector, mBean.get("objectName").toString(),metricProperties ); +// Assert.assertEquals(metrics.get(0).getMetricName(),"Count"); +// Assert.assertEquals(metrics.get(1).getMetricName(), "Value"); } From f23f839f29d92da9c6710aa5b1352107fbc21f21 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Thu, 2 Aug 2018 11:20:16 -0700 Subject: [PATCH 20/49] code review changes --- README.md | 388 ++++-------------- pom.xml | 1 - .../kafka/CustomSSLSocketFactory.java | 168 +++++++- .../kafka/JMXConnectionAdapter.java | 63 ++- .../extensions/kafka/KafkaMonitor.java | 49 +-- .../extensions/kafka/KafkaMonitorTask.java | 48 ++- .../kafka/metrics/DomainMetricsProcessor.java | 75 ++-- src/main/resources/conf/config.yml | 46 +-- .../extensions/kafka/KafkaMonitorTest.java | 85 ---- .../metrics/CustomSSLSocketFactoryTest.java | 84 ++-- .../metrics/DomainMetricsProcessorTest.java | 284 +++++++++---- src/test/resources/conf/config.yml | 43 -- .../conf/config_for_composite_metrics.yml | 17 - .../conf/config_for_non_composite_metrics.yml | 30 +- 14 files changed, 679 insertions(+), 702 deletions(-) delete mode 100644 src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java delete mode 100755 src/test/resources/conf/config.yml delete mode 100755 src/test/resources/conf/config_for_composite_metrics.yml diff --git a/README.md b/README.md index f66045f..5db38ca 100755 --- a/README.md +++ b/README.md @@ -1,20 +1,19 @@ AppDynamics Monitoring Extension for use with Kafka ============================== - -An AppDynamics extension to be used with a stand alone machine agent to provide metrics for Apache Kafka +An AppDynamics extension to be used with a stand alone machine agent to provide metrics for Apache Kafka. ## Use Case ## - -Kafka is a distributed, partitioned, replicated commit log service. It provides the functionality of a messaging system, but with a unique design. +Apache Kafka® is a distributed, fault-tolerant streaming platform. It can be used to process streams of data in +real-time. ## Prerequisites ## -This extension extracts the metrics from Kafka using the JMX protocol. Make sure you have configured JMX in Kafka - -To know more about JMX, please follow the below link +1. This extension extracts the metrics from Kafka using the JMX protocol. Make sure you have configured JMX in Kafka + To know more about JMX, please follow the below link + http://docs.oracle.com/javase/6/docs/technotes/guides/management/agent.html - http://docs.oracle.com/javase/6/docs/technotes/guides/management/agent.html +2. Please ensure Kafka is up and running and is accessible from the the machine on which machine agent is installed. ## Troubleshooting steps ## @@ -24,25 +23,25 @@ Before configuring the extension, please make sure to run the below steps to che ``` telnet - - It is the jmxremote.port specified. + - It is the jmxremote.port specified.In the case of Kafka it is usually 9999 - IP address ``` + If telnet works, it confirm the access to the Kafka server. +2. Start jconsole: + Jconsole comes as a utility with installed jdk. In your terminal, please type ```jconsole``` + In the remote connection text-box, please put in the service url : - If telnet works, it confirm the access to the Kafka server. + ```service:jmx:rmi:///jndi/rmi://:9999/jmxrmi``` - -2. Start jconsole. Jconsole comes as a utility with installed jdk. After giving the correct host and port, check if Kafka -mbean shows up. - -3. It is a good idea to match the mbean configuration in the config.yml against the jconsole. JMX is case sensitive so make -sure the config matches exact. +3. In the left pane of the jConsole, the list of mbeans exposed by Kafka is displayed. + It is a good idea to match the mbean configuration in the config.yml against the jconsole. + JMX is case sensitive so make sure the config matches exact. ## Metrics Provided ## -In addition to the metrics exposed by Kafka, we also add a metric called "Metrics Collected" with a value 0 when an error occurs and 1 when the metrics collection is successful. - -Note : By default, a Machine agent or a AppServer agent can send a fixed number of metrics to the controller. To change this limit, please follow the instructions mentioned [here](http://docs.appdynamics.com/display/PRO14S/Metrics+Limits). +Note : By default, a Machine agent or a AppServer agent can send a fixed number of metrics to the controller. +To change this limit, please follow the instructions mentioned [here](http://docs.appdynamics.com/display/PRO14S/Metrics+Limits). For eg. ``` java -Dappdynamics.agent.maxMetrics=2500 -jar machineagent.jar @@ -50,12 +49,14 @@ For eg. ## Installation ## -1. Run "mvn clean install" and find the KafkaMonitor.zip file in the "target" folder. You can also download the KafkaMonitor.zip from [AppDynamics Exchange][]. -2. Unzip as "KafkaMonitor" and copy the "KafkaMonitor" directory to `/monitors` +1. Run "mvn clean install" and find the KafkaMonitor.zip file in the "target" folder. You can also download the + KafkaMonitor.zip from [AppDynamics Exchange][]. +2. Unzip as "KafkaMonitor" and copy the "KafkaMonitor" directory to `/monitors` # Configuration ## -Note : Please make sure to not use tab (\t) while editing yaml files. You may want to validate the yaml file using a [yaml validator](http://yamllint.com/) +Note : Please make sure to not use tab (\t) while editing yaml files. +You may want to validate the yaml file using a [yaml validator](http://yamllint.com/) 1. Configure the Kafka instances by editing the config.yml file in `/monitors/KafkaMonitor/`. 2. Below is the default config.yml which has metrics configured already @@ -69,300 +70,77 @@ metricPrefix: "Server|Component:|Custom Metrics|Kafka" #This will create it in specific Tier/Component. #Please make sure to replace with the appropriate one from your environment. -#To find the in your environment, please follow the screenshot https://docs.appdynamics.com/display/PRO42/Build+a+Monitoring+Extension+Using+Java +#To find the in your environment, please follow the screenshot +https://docs.appdynamics.com/display/PRO42/Build+a+Monitoring+Extension+Using+Java # List of Kafka Instances -servers: - - host: "localhost" - port: "9999" - username: "" - password: "" - encryptedPassword: "" - displayName: "Local Kafka Server" #displayName is a REQUIRED field for level metrics. +# Please provide the service url (or) the host/port of each of the Kafka instances +servers: + - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" + host: "" + port: "" + username: "" + password: "" + encryptedPassword: "" + +# displayName is a REQUIRED field if you have more than 1 Kafka instance being +# otherwise please leave this field empty + + displayName: "Local Kafka Server" +# this field must be set to true if you want to access the metrics securely +#if this is set to true, the connection section below has more parameters that need to be configured. + useSsl: true + +#Provide the encryption key for the encrypted password encryptionKey: "" + +#Configure this section only if useSsl is set to true in any of the servers in the above section connection: - useSsl: true - socketTimeout: 3000 - connectTimeout: 1000 - sslProtocols: ["TLSv1.2"] - sslCertCheckEnabled: false - sslVerifyHostname: false - - sslCipherSuites: "" - sslTrustStorePath: "" - sslTrustStorePassword: "" - sslTrustStoreEncryptedPassword: "" - sslKeyStorePath: "" - sslKeyStorePassword: "" - useDefaultSslConnectionFactory: true - -# number of concurrent tasks. -# This doesn't need to be changed unless many instances are configured + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocol: "TLSv1.2" + sslTrustStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks" + sslTrustStorePassword: "test1234" + sslTrustStoreEncryptedPassword: "" + + #key store details for mutual auth on ssl + sslKeyStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.keystore.jks" + sslKeyStorePassword: "test1234" + sslKeyStoreEncryptedPassword: "" + +# Set the following flag to true if you want to use the certs that are provided by default JDK + useDefaultSslConnectionFactory: false + +# Each Kafka server needs 1 thread each, so please configure this according to the number of servers you are monitoring numberOfThreads: 10 -# The configuration of different metrics from various mbeans of Kafka server -# For most cases, the mbean configuration does not need to be changed. +# The configuration of different metrics from all mbeans exposed by Kafka server mbeans: - - - objectName: "kafka.server:type=BrokerTopicMetrics,*" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - objectName: "kafka.server:type=DelayedFetchMetrics,*" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - objectName: "kafka.server:type=KafkaRequestHandlerPool,*" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - objectName: "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - objectName: "kafka.server:type=ReplicaManager,name=IsrShrinksPerSec" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - objectName: "kafka.server:type=SessionExpireListener,*" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - objectName: "kafka.network:type=RequestMetrics,*" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - objectName: "kafka.controller:type=ControllerStats,*" - metrics: - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - MeanRate: - alias: "MeanRate" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - - - objectName: "kafka.server:type=DelayedOperationPurgatory,*" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - - objectName: "kafka.server:type=KafkaServer,name=BrokerState" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - objectName: "kafka.server:type=ReplicaFetcherManager,*" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - - objectName: "kafka.server:type=ReplicaManager,name=LeaderCount" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - objectName: "kafka.server:type=ReplicaManager,name=PartitionCount" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - objectName: "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - objectName: "kafka.network:type=Processor,*" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - objectName: "kafka.network:type=Processor,*" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - - objectName: "kafka.network:type=RequestChannel,*" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - - - objectName: "kafka.network:type=SocketServer,*" - metrics: - Value: - alias: "Value" - multiplier: "" - delta: "false" - aggregationType: "average" - timeRollUpType: "average" - clusterRollUpType: "individual" - - -#JVM Metrics - - objectName: "java.lang:type=Memory" - metrics: - HeapMemoryUsage.committed: "Heap Memory Usage | Committed" - HeapMemoryUsage.max: "Heap Memory Usage | Max" - HeapMemoryUsage.used: "Heap Memory Usage | Used" - NonHeapMemoryUsage.committed: "Heap Memory Usage | Committed" - NonHeapMemoryUsage.used: "Heap Memory Usage | Used" -``` +# Each "objectName" is fully qualified path of the object of the Kafka server +# For example "kafka.server:type=BrokerTopicMetrics,*" will return all objects nested under ReplicaManager type +# Each of the entries in the "metrics" section is an attribute of the objectName under which it's listed + + - objectName: "kafka.server:type=BrokerTopicMetrics,*" + metrics: + - Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" 3. Configure the path to the config.yml file by editing the in the monitor.xml file in the `/monitors/KafkaMonitor/` directory. Below is the sample For Windows, make sure you enter the right path. diff --git a/pom.xml b/pom.xml index 2b4de81..da2ecd0 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,6 @@ commons-io commons-io 2.4 - provided diff --git a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java index 343cb66..ce1eefc 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java +++ b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java @@ -1,42 +1,176 @@ +/** + * Copyright 2018 AppDynamics, Inc. + * + * 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. + */ + package com.appdynamics.extensions.kafka; -import com.appdynamics.extensions.util.YmlUtils; +import com.appdynamics.extensions.crypto.Decryptor; +import com.appdynamics.extensions.util.PathResolver; +import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.*; import javax.rmi.ssl.SslRMIClientSocketFactory; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; + import java.security.*; import java.security.cert.CertificateException; +import java.util.Map; public class CustomSSLSocketFactory extends SslRMIClientSocketFactory { private static final Logger logger = LoggerFactory.getLogger(CustomSSLSocketFactory.class); - SSLSocketFactory createSocketFactory() throws IOException { - String trustStorePath = ""; - char trustStorePassword[] = "".toCharArray(); + + public SSLSocketFactory createSocketFactory(String encryptionKey, Map connectionMap) throws IOException { + String trustStorePath = resolveTrustStorePath(connectionMap); + char[] trustStorePassword = getTrustStorePassword(encryptionKey, connectionMap); + String keyStorePath = resolveKeyStorePath(connectionMap); + char[] keyStorePassword = getKeyStorePassword(encryptionKey, connectionMap); + try { - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(new FileInputStream(trustStorePath), trustStorePassword); + //initializing keystore + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(new FileInputStream(keyStorePath), keyStorePassword); + KeyManagerFactory keyManagerFactory = KeyManagerFactory + .getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, keyStorePassword); + + //initializing truststore + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(new FileInputStream(trustStorePath), trustStorePassword); TrustManagerFactory trustManagerFactory = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(keyStore); - SSLContext sslContext = SSLContext.getInstance("TLS");//todo: take it from config - sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom()); - SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); - return sslSocketFactory; + trustManagerFactory.init(trustStore); + SSLContext sslContext = SSLContext.getInstance(connectionMap.get("sslProtocol").toString()); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom()); + return sslContext.getSocketFactory(); + + } catch (NoSuchAlgorithmException exception) { + logger.debug("No Such algorithm"); + } catch (CertificateException e) { + logger.error("CommonName in the certificate is not the same as the host name: {}", e); + } catch (KeyStoreException e) { + logger.error(""); + } catch (KeyManagementException e) { + logger.error(""); + } + catch(UnrecoverableKeyException uke){ + + } + return null; + } + + SSLSocketFactory createSocketFactory(Map connectionMap) throws IOException { + String trustStorePath = resolveTrustStorePath(connectionMap); + char[] trustStorePassword = getTrustStorePassword("", connectionMap); + String keyStorePath = resolveKeyStorePath(connectionMap); + char[] keyStorePassword = getKeyStorePassword("",connectionMap); + + try { + //initializing keystore + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(new FileInputStream(keyStorePath), keyStorePassword); + KeyManagerFactory keyManagerFactory = KeyManagerFactory + .getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, keyStorePassword); + + //initializing truststore + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(new FileInputStream(trustStorePath), trustStorePassword); + TrustManagerFactory trustManagerFactory = TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + SSLContext sslContext = SSLContext.getInstance(connectionMap.get("sslProtocol").toString()); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), + new SecureRandom()); + + return sslContext.getSocketFactory(); }catch(NoSuchAlgorithmException exception){ logger.debug("No Such algorithm"); } catch (CertificateException e) { logger.error("CommonName in the certificate is not the same as the host name: {}",e); } catch (KeyStoreException e) { - logger.error(""); + logger.error("KeyStore exception {}", e); } catch (KeyManagementException e) { - logger.error(""); + logger.error("KeyManagement Exception {}" , e); + }catch(UnrecoverableKeyException e){ + logger.error("Unrecoverable Key {}",e); } return null; } -} + + String resolveTrustStorePath(Map connectionMap) { + File installDir = PathResolver.resolveDirectory(CustomSSLSocketFactory.class); + String sslTrustStorePath = (String) connectionMap.get("sslTrustStorePath"); + File file = PathResolver.getFile(sslTrustStorePath, installDir); + logger.debug("The config property [sslTrustStorePath] with value [{}] is resolved to file [{}]" + , sslTrustStorePath, getPath(file)); + if (file == null || !file.exists()) { + logger.debug("The sslTrustStorePath [{}] doesn't exist", getPath(file)); + return null; + } + return getPath(file).toString(); + } + + private static Object getPath(File file) { return file != null ? file.getAbsolutePath() : null; } + + protected static char[] getTrustStorePassword(String encryptionKey, Map connection) { + String sslTrustStorePassword = (String) connection.get("sslTrustStorePassword"); + if (!Strings.isNullOrEmpty(sslTrustStorePassword)) { + return sslTrustStorePassword.toCharArray(); + } else { + String sslTrustStoreEncryptedPassword = (String) connection.get("sslTrustStoreEncryptedPassword"); + if (!Strings.isNullOrEmpty(sslTrustStoreEncryptedPassword) && !Strings.isNullOrEmpty(encryptionKey)) { + return new Decryptor(encryptionKey).decrypt(sslTrustStoreEncryptedPassword).toCharArray(); + } else { + logger.warn("Returning null password for sslTrustStore. Please set the [connection.sslTrustStore] or " + + "[connection.sslTrustStoreEncryptedPassword + encryptionKey]"); + return null; + } + } + } + + String resolveKeyStorePath(Map connectionMap) { + File installDir = PathResolver.resolveDirectory(CustomSSLSocketFactory.class); + String sslTrustStorePath = (String) connectionMap.get("sslKeyStorePath"); + File file = PathResolver.getFile(sslTrustStorePath, installDir); + logger.debug("The config property [sslTrustStorePath] with value [{}] is resolved to file [{}]" + , sslTrustStorePath, getPath(file)); + if (file == null || !file.exists()) { + logger.debug("The sslTrustStorePath [{}] doesn't exist", getPath(file)); + return null; + } + return getPath(file).toString(); + } + + protected static char[] getKeyStorePassword(String encryptionKey, Map connection) { + String sslTrustStorePassword = (String) connection.get("sslKeyStorePassword"); + if (!Strings.isNullOrEmpty(sslTrustStorePassword)) { + return sslTrustStorePassword.toCharArray(); + } else { + String sslTrustStoreEncryptedPassword = (String) connection.get("sslKeyStoreEncryptedPassword"); + if (!Strings.isNullOrEmpty(sslTrustStoreEncryptedPassword) && !Strings.isNullOrEmpty(encryptionKey)) { + return new Decryptor(encryptionKey).decrypt(sslTrustStoreEncryptedPassword).toCharArray(); + } else { + logger.warn("Returning null password for sslKeyStore. Please set the [connection.sslKeyStore] or " + + "[connection.sslKeyStoreEncryptedPassword + encryptionKey]"); + return null; + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index 11a25be..675938f 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -1,15 +1,23 @@ -/* - * Copyright 2018. AppDynamics LLC and its affiliates. - * All Rights Reserved. - * This is unpublished proprietary source code of AppDynamics LLC and its affiliates. - * The copyright notice above does not evidence any actual or intended publication of such source code. +/** + * Copyright 2018 AppDynamics, Inc. * + * 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. */ - package com.appdynamics.extensions.kafka; import com.appdynamics.extensions.kafka.utils.Constants; +import com.appdynamics.extensions.util.YmlUtils; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Lists; @@ -50,14 +58,39 @@ private JMXConnectionAdapter(Map requestMap) throws MalformedURL this.password = requestMap.get(Constants.PASSWORD); } - static JMXConnectionAdapter create(Map requestMap) throws MalformedURLException { + static JMXConnectionAdapter create(final Map requestMap) throws MalformedURLException { return new JMXConnectionAdapter(requestMap); } - //TODO:change according to config params - JMXConnector open(boolean useDefaultSslFactory, boolean useSsl) throws IOException { + JMXConnector open(boolean useSsl, String encryptionKey, Map connectionMap) throws IOException { JMXConnector jmxConnector; final Map env = new HashMap(); + boolean useDefaultSslFactory= YmlUtils.getBoolean(connectionMap.get("useDefaultSslConnectionFactory")); + + if(useSsl) { + if (Preconditions.checkNotNull(useDefaultSslFactory) && useDefaultSslFactory){ + SslRMIClientSocketFactory sslRMIClientSocketFactory = new SslRMIClientSocketFactory(); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, sslRMIClientSocketFactory); + } else if (Preconditions.checkNotNull(useDefaultSslFactory) && !useDefaultSslFactory) { + CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE + ,customSSLSocketFactory.createSocketFactory( encryptionKey, connectionMap)); + } + } + if (!Strings.isNullOrEmpty(this.username)) { + env.put(JMXConnector.CREDENTIALS, new String[]{username, password}); + } + jmxConnector = JMXConnectorFactory.connect(this.serviceUrl,env); + if (jmxConnector == null) { throw new IOException("Unable to connect to Mbean server"); } + return jmxConnector; + } + + + JMXConnector open(boolean useSsl, Map connectionMap) throws IOException { + JMXConnector jmxConnector; + final Map env = new HashMap(); + boolean useDefaultSslFactory= YmlUtils.getBoolean(connectionMap.get("useDefaultSslConnectionFactory")); + if(useSsl) { if (Preconditions.checkNotNull(useDefaultSslFactory) && useDefaultSslFactory){ SslRMIClientSocketFactory sslRMIClientSocketFactory = new SslRMIClientSocketFactory(); @@ -65,7 +98,7 @@ JMXConnector open(boolean useDefaultSslFactory, boolean useSsl) throws IOExcepti } else if (Preconditions.checkNotNull(useDefaultSslFactory) && !useDefaultSslFactory) { CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE - , customSSLSocketFactory.createSocketFactory()); + ,customSSLSocketFactory.createSocketFactory(connectionMap)); } } if (!Strings.isNullOrEmpty(this.username)) { @@ -82,12 +115,13 @@ void close(JMXConnector jmxConnector) throws IOException { } } - public Set queryMBeans(JMXConnector jmxConnection, ObjectName objectName) throws IOException { + public Set queryMBeans(final JMXConnector jmxConnection, final ObjectName objectName) + throws IOException { MBeanServerConnection connection = jmxConnection.getMBeanServerConnection(); return connection.queryMBeans(objectName, null); } - public List getReadableAttributeNames(JMXConnector jmxConnection, ObjectInstance instance) + public List getReadableAttributeNames(final JMXConnector jmxConnection, final ObjectInstance instance) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException { MBeanServerConnection connection = jmxConnection.getMBeanServerConnection(); List attributeNames = Lists.newArrayList(); @@ -100,8 +134,9 @@ public List getReadableAttributeNames(JMXConnector jmxConnection, Object return attributeNames; } - public List getAttributes(JMXConnector jmxConnection, ObjectName objectName, String[] strings) - throws IOException, ReflectionException, InstanceNotFoundException { + public List getAttributes(final JMXConnector jmxConnection, final ObjectName objectName, + final String[] strings) throws IOException, ReflectionException, + InstanceNotFoundException { MBeanServerConnection connection = jmxConnection.getMBeanServerConnection(); AttributeList list = connection.getAttributes(objectName, strings); if (list != null) { diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index eec309a..85cc7c1 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -13,6 +13,8 @@ import com.appdynamics.extensions.TasksExecutionServiceProvider; import com.appdynamics.extensions.kafka.utils.Constants; import com.appdynamics.extensions.util.AssertUtils; +import com.appdynamics.extensions.util.YmlUtils; +import com.google.common.primitives.Booleans; import com.singularity.ee.agent.systemagent.api.exception.TaskExecutionException; import org.apache.log4j.ConsoleAppender; import org.apache.log4j.Level; @@ -25,16 +27,28 @@ public class KafkaMonitor extends ABaseMonitor { -// @Override -// @SuppressWarnings("unchecked") -// protected void initializeMoreStuff(Map args) { -// -// Map servers = (Map) this.getContextConfiguration().getConfigYml().get(Constants.SERVERS); -// if (Boolean.valueOf((servers.get("useSsl")))) { -// System.setProperty("javax.net.ssl.trustStore", connectionMapFromConfig.get("sslTrustStorePath")); -// System.setProperty("javax.net.ssl.trustStorePassword", connectionMapFromConfig.get("sslTrustStorePassword")); -// } -// } + @Override + protected void initializeMoreStuff(Map args) { + + List> servers = (List>) this.getContextConfiguration() + .getConfigYml().get(Constants.SERVERS); + Map connectionMap = (Map) this.getContextConfiguration() + .getConfigYml().get(Constants.CONNECTION); + boolean flag = false; + + for (Map server : servers) { + if(!flag) { + if (YmlUtils.getBoolean((server.get("useSsl")))) { + System.setProperty("javax.net.ssl.trustStore", + connectionMap.get("sslTrustStorePath").toString()); + System.setProperty("javax.net.ssl.trustStorePassword", + connectionMap.get("sslTrustStorePassword").toString()); + flag = true; + break; + } + } + } + } protected String getDefaultMetricPrefix() { return DEFAULT_METRIC_PREFIX; } @@ -48,6 +62,7 @@ protected void doRun(TasksExecutionServiceProvider tasksExecutionServiceProvider for (Map kafkaServer : kafkaServers) { KafkaMonitorTask task = new KafkaMonitorTask(tasksExecutionServiceProvider, this.getContextConfiguration(), kafkaServer); + //todo: do we need an assert for displayname AssertUtils.assertNotNull(kafkaServer.get(Constants.DISPLAY_NAME), "The displayName can not be null"); tasksExecutionServiceProvider.submit(kafkaServer.get(Constants.DISPLAY_NAME), task); @@ -62,19 +77,5 @@ protected int getTaskCount() { } -// @TODO: to be removed before publishing - public static void main(String[] args) throws TaskExecutionException { - ConsoleAppender ca = new ConsoleAppender(); - ca.setWriter(new OutputStreamWriter(System.out)); - ca.setLayout(new PatternLayout("%-5p [%t]: %m%n")); - ca.setThreshold(Level.DEBUG); - org.apache.log4j.Logger.getRootLogger().addAppender(ca); - - KafkaMonitor monitor = new KafkaMonitor(); - Map taskArgs = new HashMap(); - taskArgs.put("config-file", - "/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/main/resources/conf/config.yml"); - monitor.execute(taskArgs, null); - } } \ No newline at end of file diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index cc4d9e0..43e8f85 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -1,9 +1,17 @@ -/* - * Copyright 2018. AppDynamics LLC and its affiliates. - * All Rights Reserved. - * This is unpublished proprietary source code of AppDynamics LLC and its affiliates. - * The copyright notice above does not evidence any actual or intended publication of such source code. +/** + * Copyright 2018 AppDynamics, Inc. * + * 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. */ package com.appdynamics.extensions.kafka; @@ -54,7 +62,8 @@ public void run() { logger.info("Completed Kafka Monitoring task for Kafka server: {}", this.kafkaServer.get(Constants.DISPLAY_NAME)); }catch(Exception e ) { - logger.error("Exception occurred while collecting metrics for: {} {}", this.kafkaServer.get(Constants.DISPLAY_NAME)); + logger.error("Exception occurred while collecting metrics for: {} {}", + this.kafkaServer.get(Constants.DISPLAY_NAME)); } } @@ -85,8 +94,13 @@ private BigDecimal openJMXConnection() { Map requestMap = buildRequestMap(); jmxAdapter = JMXConnectionAdapter.create(requestMap); Map connectionMap = getConnectionParameters(); - jmxConnection = jmxAdapter.open(YmlUtils.getBoolean(connectionMap.get("useDefaultSslConnectionFactory")), - YmlUtils.getBoolean(this.kafkaServer.get("useSsl"))); + if(configuration.getConfigYml().containsKey("encryptionKey") && !Strings.isNullOrEmpty( configuration.getConfigYml().get("encryptionKey").toString()) ) + jmxConnection = jmxAdapter.open(YmlUtils.getBoolean(this.kafkaServer.get("useSsl")), configuration.getConfigYml().get("encryptionKey").toString(), connectionMap); + + else + jmxConnection = jmxAdapter.open(YmlUtils.getBoolean(this.kafkaServer.get("useSsl")), connectionMap); + + logger.debug("JMX Connection is open to Kafka server: {}", this.kafkaServer.get(Constants.DISPLAY_NAME)); return BigDecimal.ONE; } catch (IOException ioe) { @@ -110,13 +124,17 @@ private Map buildRequestMap() { private String getPassword() { String password = this.kafkaServer.get(Constants.PASSWORD); if (!Strings.isNullOrEmpty(password)) { return password; } - String encryptionKey = configuration.getConfigYml().get(Constants.ENCRYPTION_KEY).toString(); - String encryptedPassword = this.kafkaServer.get(Constants.ENCRYPTED_PASSWORD); - if (!Strings.isNullOrEmpty(encryptionKey) && !Strings.isNullOrEmpty(encryptedPassword)) { - java.util.Map cryptoMap = Maps.newHashMap(); - cryptoMap.put(TaskInputArgs.ENCRYPTED_PASSWORD, encryptedPassword); - cryptoMap.put(TaskInputArgs.ENCRYPTION_KEY, encryptionKey); - return CryptoUtil.getPassword(cryptoMap); + + if(configuration.getConfigYml().containsKey(Constants.ENCRYPTION_KEY) && + configuration.getConfigYml().containsKey(Constants.ENCRYPTED_PASSWORD)) { + String encryptionKey = configuration.getConfigYml().get(Constants.ENCRYPTION_KEY).toString(); + String encryptedPassword = this.kafkaServer.get(Constants.ENCRYPTED_PASSWORD); + if (!Strings.isNullOrEmpty(encryptionKey) && !Strings.isNullOrEmpty(encryptedPassword)) { + java.util.Map cryptoMap = Maps.newHashMap(); + cryptoMap.put(TaskInputArgs.ENCRYPTED_PASSWORD, encryptedPassword); + cryptoMap.put(TaskInputArgs.ENCRYPTION_KEY, encryptionKey); + return CryptoUtil.getPassword(cryptoMap); + } } return null; } diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index 1acb86b..82d9280 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -1,9 +1,17 @@ -/* - * Copyright 2018. AppDynamics LLC and its affiliates. - * All Rights Reserved. - * This is unpublished proprietary source code of AppDynamics LLC and its affiliates. - * The copyright notice above does not evidence any actual or intended publication of such source code. +/** + * Copyright 2018 AppDynamics, Inc. * + * 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. */ package com.appdynamics.extensions.kafka.metrics; @@ -16,7 +24,6 @@ import com.appdynamics.extensions.metrics.Metric; import com.google.common.base.Strings; import com.google.common.collect.Lists; -import com.google.common.primitives.Booleans; import org.slf4j.LoggerFactory; import javax.management.*; import javax.management.openmbean.CompositeData; @@ -24,9 +31,7 @@ import javax.management.remote.JMXConnector; import java.io.IOException; import java.math.BigDecimal; -import java.math.BigInteger; import java.util.*; -import java.util.concurrent.Phaser; public class DomainMetricsProcessor { static final org.slf4j.Logger logger = LoggerFactory.getLogger(DomainMetricsProcessor.class); @@ -52,41 +57,38 @@ public DomainMetricsProcessor(MonitorContextConfiguration configuration, JMXConn public void populateMetricsForMBean() { try { - List finalMetricList = new ArrayList<>(); - finalMetricList.add(new Metric("HeartBeat", heartBeat.toString(), - this.displayName + "|HeartBeat", - "AVERAGE", "AVERAGE", "INDIVIDUAL")); - metricWriteHelper.transformAndPrintMetrics(finalMetricList); - String objectName = (String) this.mbeanFromConfig.get(Constants.OBJECTNAME); - List> metricProperties = (List>) this.mbeanFromConfig.get(Constants.METRICS); + List> metricsList = (List>) this.mbeanFromConfig.get(Constants.METRICS); logger.debug("Processing mbean {} ", objectName); - - for (Map metricPropertiesPerMetric : metricProperties) { - finalMetricList = getNodeMetrics(jmxConnection, objectName, metricPropertiesPerMetric); - logger.debug("Printing metrics for server {}", this.displayName); - metricWriteHelper.transformAndPrintMetrics(finalMetricList); - } + List finalMetricList = getNodeMetrics(jmxConnection, objectName, metricsList); + finalMetricList.add(new Metric("HeartBeat", heartBeat.toString(), + this.displayName + "|HeartBeat", "AVERAGE", + "AVERAGE", "INDIVIDUAL")); + logger.debug("Printing metrics for server {}", this.displayName); + metricWriteHelper.transformAndPrintMetrics(finalMetricList); logger.debug("Finished processing mbean {} ", objectName); - } catch (IntrospectionException | IOException | MalformedObjectNameException | InstanceNotFoundException | ReflectionException e) { logger.error("Kafka Monitor error: {} " ,e); } } - private List getNodeMetrics(JMXConnector jmxConnection, String objectName, Map metricProperties) + private List getNodeMetrics(JMXConnector jmxConnection, String objectName, + List> metricProperties) + throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { - - List nodeMetrics = Lists.newArrayList(); - Set objectInstances = this.jmxAdapter.queryMBeans(jmxConnection, - ObjectName.getInstance(objectName)); - for (ObjectInstance instance : objectInstances) { - List metricNamesDictionary = this.jmxAdapter.getReadableAttributeNames(jmxConnection, instance); - List attributes =this.jmxAdapter.getAttributes(jmxConnection, - instance.getObjectName(), metricNamesDictionary.toArray(new String[metricNamesDictionary.size()])); - collect(nodeMetrics, attributes, instance, metricProperties); + List nodeMetrics = Lists.newArrayList(); + Set objectInstances = this.jmxAdapter.queryMBeans(jmxConnection, + ObjectName.getInstance(objectName)); + for (ObjectInstance instance : objectInstances) { + List metricNamesDictionary = this.jmxAdapter.getReadableAttributeNames(jmxConnection, instance); + List attributes =this.jmxAdapter.getAttributes(jmxConnection, + instance.getObjectName(), metricNamesDictionary.toArray( + new String[metricNamesDictionary.size()])); + for(Map metricPropertiesPerMetric : metricProperties) { + collect(nodeMetrics, attributes, instance, metricPropertiesPerMetric); + } } return nodeMetrics; } @@ -102,14 +104,14 @@ private void collect(List nodeMetrics, List attributes, Objec Object attributeValue = ((CompositeDataSupport) attribute.getValue()).get(str); if(metricProperties.containsKey(key)){ setMetricDetails(metricPrefix, key, attributeValue.toString(), instance, - (Map)metricProperties.get(key), nodeMetrics); + (Map)metricProperties.get(key),nodeMetrics); } } } else{ if(metricProperties.containsKey(attribute.getName())) { setMetricDetails(metricPrefix, attribute.getName(), attribute.getValue(), instance, - (Map)metricProperties.get(attribute.getName()), nodeMetrics); + (Map)metricProperties.get(attribute.getName()),nodeMetrics); } } } catch (Exception e) { @@ -123,8 +125,8 @@ private boolean isCompositeDataObject(Attribute attribute){ } private void setMetricDetails (String metricPrefix, String attributeName, Object attributeValue, - ObjectInstance instance, Map metricPropertiesMap, - List nodeMetrics) { + ObjectInstance instance, Map metricPropertiesMap, List nodeMetrics + ) { String metricPath = metricPrefix + Constants.METRIC_SEPARATOR + buildName(instance)+ attributeName; Metric metric = new Metric(attributeName, attributeValue.toString(), metricPath, metricPropertiesMap); nodeMetrics.add(metric); @@ -136,7 +138,6 @@ private String buildName(ObjectInstance instance) { StringBuilder sb = new StringBuilder(); String type = keyPropertyList.get("type"); String name = keyPropertyList.get("name"); - sb.append(objectName.getDomain()); if(!Strings.isNullOrEmpty(type)) { sb.append(Constants.METRIC_SEPARATOR); diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index c9bc6dc..1a3bb84 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -1,7 +1,11 @@ -metricPrefix: "Server|Component:<[or]>|Custom Metrics|Kafka" +#This will populate the metrics in all the tiers, under this path(not recommended) +#metricPrefix: "Custom metrics|Kafka" - # Add your Kafka Instances below: +#The following prefix will populate the metrics under respective tiers +metricPrefix: "Server|Component:|Custom Metrics|Kafka" + +# Add your Kafka Instances below: servers: - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" host: "" @@ -12,41 +16,39 @@ servers: displayName: "Local Kafka Server" useSsl: false - #Provide the encryption key for the password + + #Provide the encryption key for the encrypted password encryptionKey: "" - #Configure this section only if useSSL is true. + + #Configure this section only if useSsl is set to true in any of the servers in the above section + connection: socketTimeout: 3000 connectTimeout: 1000 - sslProtocols: ["TLSv1.2"] - sslCertCheckEnabled: false - sslVerifyHostname: false - sslCipherSuites: "[]" + sslProtocol: "TLSv1.2" sslTrustStorePath: "" sslTrustStorePassword: "" sslTrustStoreEncryptedPassword: "" - #key store details for mutual auth on ssl sslKeyStorePath: "" sslKeyStorePassword: "" sslKeyStoreEncryptedPassword: "" -# Set the following flag to true if you want to use the certs in the JDK -# Set this to false if you want to use your own certs - useDefaultSslConnectionFactory: true +# Set the following flag to true if you want to use the certs that are provided by default JDK + useDefaultSslConnectionFactory: false -# number of concurrent tasks. -# This doesn't need to be changed unless many instances are configured -# @todo: explain this field +# Each Kafka server needs 1 thread each, so please configure this according to the number of servers you are monitoring numberOfThreads: 10 -# The configuration of different metrics from various mbeans of Kafka server -# For most cases, the mbean configuration does not need to be changed. +# The configuration of different metrics from all mbeans exposed by Kafka server mbeans: -#@todo: compare actual with output values +# Each "objectName" is fully qualified path of the object of the Kafka server +# For example "kafka.server:type=ReplicaManager,*" will return all objects nested under ReplicaManager type +# Each of the entries in the "metrics" section is an attribute of the objectName under which it's listed + - objectName: "kafka.server:type=BrokerTopicMetrics,*" metrics: - Count: @@ -301,14 +303,6 @@ mbeans: timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" -# - NonHeapMemoryUsage.max: -# alias: " Non Heap Memory Usage | Max" -# multiplier: "" -# delta: "false" -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" - - NonHeapMemoryUsage.used: alias: "Non Heap Memory Usage | Used" multiplier: "" diff --git a/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java b/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java deleted file mode 100644 index 6a77606..0000000 --- a/src/test/java/com/appdynamics/extensions/kafka/KafkaMonitorTest.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.appdynamics.extensions.kafka; - -import com.appdynamics.extensions.AMonitorJob; -import com.appdynamics.extensions.conf.MonitorContextConfiguration; -import com.appdynamics.extensions.http.Http4ClientBuilder; -import com.appdynamics.extensions.kafka.metrics.CustomSSLSocketFactoryTest; -import com.appdynamics.extensions.util.PathResolver; -import com.google.common.collect.Maps; -import com.singularity.ee.agent.systemagent.api.AManagedMonitor; -import com.singularity.ee.agent.systemagent.api.exception.TaskExecutionException; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.junit.Assert; -import org.junit.Test; -import org.mockito.Mockito; - -import javax.management.remote.JMXConnector; -import javax.management.remote.JMXConnectorFactory; -import javax.management.remote.JMXConnectorServer; -import javax.management.remote.JMXServiceURL; -import javax.management.remote.rmi.RMIConnectorServer; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; -import javax.rmi.ssl.SslRMIClientSocketFactory; -import javax.rmi.ssl.SslRMIServerSocketFactory; -import java.io.FileInputStream; -import java.security.KeyStore; -import java.security.SecureRandom; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class KafkaMonitorTest { - - - public static final String CONFIG_ARG = "config-file"; - MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration("Kafka", "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), Mockito.mock(AMonitorJob.class)); - - - - //@todo:move it to the customsslfactorytest file - @Test(expected = Exception.class) - public void testConfigureSSL() throws Exception { - int port = 8745; - JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); - jmxConnectorServer.start(); - JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:"+port+"/jmxrmi"); - Map env = new HashMap(); -// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE,new CustomSSLSocketFactory()); - JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); - jmxConnectorServer.stop(); - } - - @Test - public void testConfigureSSLWithKeys() throws Exception { - int port = 8745; - JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); - jmxConnectorServer.start(); - JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:"+port+"/jmxrmi"); -// System.setProperty("javax.net.ssl.trustStore", ""); -// System.setProperty("javax.net.ssl.trustStorePassword", ""); - String truststore = ""; - char truststorepass[] = "".toCharArray(); - KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(new FileInputStream(truststore), truststorepass); - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(ks); - SSLContext ctx = SSLContext.getInstance("TLS"); - ctx.init(null, tmf.getTrustManagers(), new SecureRandom()); - - Map env = new HashMap(); - CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); - SslRMIServerSocketFactory ssf = new SslRMIServerSocketFactory(); - env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE,ctx.getSocketFactory()); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory); - JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); - jmxConnectorServer.stop(); - } - - //todo: test for default ssl factory - -} \ No newline at end of file diff --git a/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java index 87d0b88..423b0d6 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java @@ -1,45 +1,83 @@ package com.appdynamics.extensions.kafka.metrics; import com.appdynamics.extensions.kafka.CustomSSLSocketFactory; +import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.management.*; -import javax.management.remote.JMXConnectorServer; -import javax.management.remote.JMXConnectorServerFactory; -import javax.management.remote.JMXServiceURL; +import javax.management.remote.*; import javax.management.remote.rmi.RMIConnectorServer; -import javax.rmi.ssl.SslRMIClientSocketFactory; -import javax.rmi.ssl.SslRMIServerSocketFactory; - import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; -import java.rmi.server.RMIServerSocketFactory; import java.util.HashMap; +import java.util.Map; public class CustomSSLSocketFactoryTest { private static final Logger logger = LoggerFactory.getLogger(CustomSSLSocketFactoryTest.class); public static JMXConnectorServer startSSL(int port) { - - MBeanServer mbs = MBeanServerFactory.createMBeanServer(); + MBeanServer mBeanServer = MBeanServerFactory.createMBeanServer(); HashMap env = new HashMap(); - SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory(); - SslRMIServerSocketFactory ssf = new SslRMIServerSocketFactory(); -// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE,new CustomSSLSocketFactory()); -// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE,ssf); - JMXConnectorServer cs = null; + JMXConnectorServer jmxConnectorServer = null; try { - JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:"+port+"/jmxrmi"); - Registry registry = LocateRegistry.createRegistry(port, csf, null); - cs = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs); - - }catch(Exception ioe){ - logger.debug("could not connect"); + JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); + Registry registry = LocateRegistry.createRegistry(port, null, null); + jmxConnectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mBeanServer); + } catch (Exception ioe) { + logger.debug("Could not connect"); } + return jmxConnectorServer; + } - return cs; - + @Test + public void testConfigureSSL() throws Exception { + int port = 8745; + JMXConnectorServer jmxConnectorServer = startSSL(port); + jmxConnectorServer.start(); + JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); + Map env = new HashMap(); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new CustomSSLSocketFactory()); + JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); + jmxConnectorServer.stop(); } -} +// @Test +// public void testCustomSSLFactoryWithKeys() throws Exception { +// int port = 8745; +// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration +// ("Kafka Monitor", +// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), +// Mockito.mock(AMonitorJob.class)); +// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL.yml"); +// Map config = contextConfiguration.getConfigYml(); +// Map connectionMap = (Map) config.get("connection"); +// Map env = new HashMap(); +// CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); +// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory.createSocketFactory("", +// connectionMap)); +// JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); +// jmxConnectorServer.start(); +// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); +// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); +// jmxConnectorServer.stop(); +// } +// +// @Test +// public void testDefaultSSLFactoryWithKeys() throws Exception { +// int port = 8745; +// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration +// ("Kafka Monitor", +// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), +// Mockito.mock(AMonitorJob.class)); +// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL.yml"); +// Map env = new HashMap(); +// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); +// JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); +// jmxConnectorServer.start(); +// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); +// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); +// +// } + +} \ No newline at end of file diff --git a/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java index d6998fa..3697168 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java @@ -4,131 +4,235 @@ import com.appdynamics.extensions.MetricWriteHelper; import com.appdynamics.extensions.conf.MonitorContextConfiguration; import com.appdynamics.extensions.kafka.JMXConnectionAdapter; -import com.appdynamics.extensions.kafka.metrics.DomainMetricsProcessor; +import com.appdynamics.extensions.kafka.utils.Constants; import com.appdynamics.extensions.metrics.Metric; import com.appdynamics.extensions.util.PathResolver; import com.google.common.collect.Lists; -import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.singularity.ee.agent.systemagent.api.AManagedMonitor; import org.junit.Assert; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import javax.management.*; +import javax.management.openmbean.CompositeDataSupport; +import javax.management.openmbean.CompositeType; +import javax.management.openmbean.OpenDataException; +import javax.management.openmbean.OpenType; import javax.management.remote.JMXConnector; import java.io.IOException; +import java.math.BigDecimal; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.Phaser; - import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class DomainMetricsProcessorTest { - //todo:make access private - JMXConnector jmxConnector = mock(JMXConnector.class); - JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); - MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); - MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration("Kafka", - "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), Mockito.mock(AMonitorJob.class)); -// contextConfiguration.setConfigYml("/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/test/resources/conf/config_for_non_composite_metrics.yml"); - - - - @Test - public void getNodeMetricsForNonCompositeAttributes() throws IOException,IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException { - //todo:give relative path - contextConfiguration.setConfigYml("conf/config.yml"); + @Test + public void getMetricsForNonCompositeObject() throws IOException, + IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException { + JMXConnector jmxConnector = mock(JMXConnector.class); + JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); + MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); + ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); + MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration + ("Kafka Monitor", + "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), + Mockito.mock(AMonitorJob.class)); + contextConfiguration.setConfigYml("src/test/resources/conf/config_for_non_composite_metrics.yml"); Map config = contextConfiguration.getConfigYml(); - List mBeans = (List) config.get("mbeans"); + List mBeans = (List) config.get("mbeans"); Set objectInstances = Sets.newHashSet(); - objectInstances.add(new ObjectInstance("org.apache.kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", "test")); - + objectInstances.add(new ObjectInstance( + "org.apache.kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", "test")); List attributes = Lists.newArrayList(); attributes.add(new Attribute("Count", 100)); - attributes.add(new Attribute("Value", 200 )); - + attributes.add(new Attribute("Mean Rate", 200 )); List metricNames = Lists.newArrayList(); metricNames.add("Count"); - metricNames.add("Value"); - - when(jmxConnectionAdapter.queryMBeans(eq(jmxConnector), Mockito.any(ObjectName.class))).thenReturn(objectInstances); - when(jmxConnectionAdapter.getReadableAttributeNames(eq(jmxConnector), Mockito.any(ObjectInstance.class))).thenReturn(metricNames); - when(jmxConnectionAdapter.getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), Mockito.any(String[] - .class))).thenReturn(attributes); - - Map server = Maps.newHashMap(); - //@todo: pull values from config - server.put("host", "localhost"); - server.put("port", "9999"); - server.put("displayName", "TestServer1"); - - for(Map mBean : mBeans){ - //@todo:remove phaser - Phaser phaser = new Phaser(); - phaser.register(); - Map metricProperties = (Map) mBean.get("metrics"); -// DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor(contextConfiguration, jmxConnectionAdapter, -// jmxConnector, mBean, server.get("displayName"), metricWriteHelper,phaser); - //@todo:call populateAndPrintMetrics(), - //@todo: mock metricWriterHelper - //@todo: argument capture the list of metrics passed to transformAndPrintMetric -// List metrics = domainMetricsProcessor.getNodeMetrics(jmxConnector, mBean.get("objectName").toString(),metricProperties ); -// Assert.assertEquals(metrics.get(0).getMetricName(),"Count"); -// Assert.assertEquals(metrics.get(1).getMetricName(), "Value"); - //@todo: test values and properties also + metricNames.add("Mean Rate"); + doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector), + Mockito.any(ObjectName.class) ); + doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector), + Mockito.any(ObjectInstance.class)); + doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), + Mockito.any(String[].class)); + List> servers = (List>)config.get(Constants.SERVERS); + for (Map mBean : mBeans) { + DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( + contextConfiguration, jmxConnectionAdapter, + jmxConnector, mBean, "server1", metricWriteHelper, BigDecimal.ONE); + domainMetricsProcessor.populateMetricsForMBean(); + verify(metricWriteHelper) + .transformAndPrintMetrics(pathCaptor.capture()); + Metric firstResultMetric = (Metric)pathCaptor.getValue().get(0); + Metric secondResultMetric = (Metric)pathCaptor.getValue().get(1); + Assert.assertEquals(firstResultMetric.getMetricName(),"Count"); + Assert.assertEquals(firstResultMetric.getMetricValue(), "100"); + Assert.assertEquals(firstResultMetric.getAggregationType(), "AVERAGE"); + Assert.assertEquals(firstResultMetric.getClusterRollUpType(), "INDIVIDUAL"); + Assert.assertEquals(firstResultMetric.getTimeRollUpType(), "AVERAGE"); + Assert.assertEquals(secondResultMetric.getMetricName(), "Mean Rate"); + Assert.assertEquals(secondResultMetric.getMetricValue(), "200"); + Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); + Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); + Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "AVERAGE"); } - } - - @Test - //todo: check the composite metrics, check Cassandra Test - public void getNodeMetricsForCompositeMetrics() throws MalformedObjectNameException, ReflectionException, InstanceNotFoundException,IntrospectionException,IOException{ - - - //@todo:change file - contextConfiguration.setConfigYml("/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/test/resources/conf/config_for_non_composite_metrics.yml"); + public void getMetricsForCompositeObject() throws MalformedObjectNameException, ReflectionException, + InstanceNotFoundException,IntrospectionException,IOException,OpenDataException { + + JMXConnector jmxConnector = mock(JMXConnector.class); + JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); + MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); + MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration + ("Kafka Monitor", + "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), + Mockito.mock(AMonitorJob.class)); + contextConfiguration.setConfigYml("src/test/resources/conf/config_for_composite_metrics.yml"); + ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); Map config = contextConfiguration.getConfigYml(); - List mBeans = (List) config.get("mbeans"); + List> mBeans = (List>) config.get("mbeans"); Set objectInstances = Sets.newHashSet(); - objectInstances.add(new ObjectInstance("org.apache.kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", "test")); - + objectInstances.add(new ObjectInstance("java.lang:type=Memory", "test")); List attributes = Lists.newArrayList(); + attributes.add(new Attribute("HeapMemoryUsage", createCompositeDataSupportObject())); attributes.add(new Attribute("Count", 100)); - attributes.add(new Attribute("Value", 200 )); - + attributes.add(new Attribute("Mean Rate", 200 )); List metricNames = Lists.newArrayList(); - metricNames.add("Count"); - metricNames.add("Value"); - - when(jmxConnectionAdapter.queryMBeans(eq(jmxConnector), Mockito.any(ObjectName.class))).thenReturn(objectInstances); - when(jmxConnectionAdapter.getReadableAttributeNames(eq(jmxConnector), Mockito.any(ObjectInstance.class))).thenReturn(metricNames); - when(jmxConnectionAdapter.getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), Mockito.any(String[] - .class))).thenReturn(attributes); - - Map server = Maps.newHashMap(); - server.put("host", "localhost"); - server.put("port", "9999"); - server.put("displayName", "TestServer1"); - - for(Map mBean : mBeans){ - Phaser phaser = new Phaser(); - phaser.register(); - Map metricProperties = (Map) mBean.get("metrics"); - //commented out for test -// DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor(contextConfiguration, jmxConnectionAdapter, -// jmxConnector, mBean, server.get("displayName"), metricWriteHelper,phaser); -// List metrics = domainMetricsProcessor.getNodeMetrics(jmxConnector, mBean.get("objectName").toString(),metricProperties ); -// Assert.assertEquals(metrics.get(0).getMetricName(),"Count"); -// Assert.assertEquals(metrics.get(1).getMetricName(), "Value"); + metricNames.add("metric1"); + metricNames.add("metric2"); + doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector),Mockito.any(ObjectName.class) ); + doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector), Mockito.any(ObjectInstance.class)); + doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), Mockito.any(String[] + .class)); + List> servers = (List>)config.get(Constants.SERVERS); + for (Map mBean : mBeans) { + DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( + contextConfiguration, jmxConnectionAdapter, + jmxConnector, mBean, "server2", metricWriteHelper, BigDecimal.ONE); + domainMetricsProcessor.populateMetricsForMBean(); + verify(metricWriteHelper) + .transformAndPrintMetrics(pathCaptor.capture()); + Metric firstResultMetric = (Metric)pathCaptor.getValue().get(0); + Metric secondResultMetric = (Metric)pathCaptor.getValue().get(1); + Assert.assertEquals(firstResultMetric.getMetricName(),"HeapMemoryUsage.min"); + Assert.assertEquals(firstResultMetric.getMetricValue(), "50"); + Assert.assertEquals(firstResultMetric.getAggregationType(), "AVERAGE"); + Assert.assertEquals(firstResultMetric.getClusterRollUpType(), "INDIVIDUAL"); + Assert.assertEquals(firstResultMetric.getTimeRollUpType(), "AVERAGE"); + Assert.assertEquals(secondResultMetric.getMetricName(),"HeapMemoryUsage.max"); + Assert.assertEquals(secondResultMetric.getMetricValue(), "100"); + Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); + Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); + Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "AVERAGE"); } + } + @Test + public void getMetricsForCompositeAndNonCompositeObject() throws IOException, + IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException, + OpenDataException{ + JMXConnector jmxConnector = mock(JMXConnector.class); + JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); + MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); + ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); + MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration + ("Kafka Monitor", + "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), + Mockito.mock(AMonitorJob.class)); + contextConfiguration.setConfigYml("src/test/resources/conf/config_composite_and_non_composite_metrics.yml"); + Map config = contextConfiguration.getConfigYml(); + List> mBeans = (List>) config.get("mbeans"); + Set objectInstances = Sets.newHashSet(); + objectInstances.add(new ObjectInstance( + "org.apache.kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", "test")); + List attributes = Lists.newArrayList(); + attributes.add(new Attribute(("Count"), 0)); + attributes.add(new Attribute("HeapMemoryUsage", createCompositeDataSupportObject())); + List metricNames = Lists.newArrayList(); + metricNames.add("metric1"); + metricNames.add("metric2"); + doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector) + ,Mockito.any(ObjectName.class) ); + doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector) + , Mockito.any(ObjectInstance.class)); + doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), + Mockito.any(ObjectName.class), Mockito.any(String[] + .class)); + for (Map mBean : mBeans) { + DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( + contextConfiguration, jmxConnectionAdapter, + jmxConnector, mBean, "server1", metricWriteHelper, BigDecimal.ONE); + domainMetricsProcessor.populateMetricsForMBean(); + verify(metricWriteHelper) + .transformAndPrintMetrics(pathCaptor.capture()); + Metric firstResultMetric = (Metric) pathCaptor.getValue().get(0); + Metric secondResultMetric = (Metric) pathCaptor.getValue().get(1); + Assert.assertEquals(firstResultMetric.getMetricName(), "Count"); + Assert.assertEquals(firstResultMetric.getMetricValue(), "0"); + Assert.assertEquals(firstResultMetric.getAggregationType(), "AVERAGE"); + Assert.assertEquals(firstResultMetric.getClusterRollUpType(), "INDIVIDUAL"); + Assert.assertEquals(firstResultMetric.getTimeRollUpType(), "AVERAGE"); + Assert.assertEquals(secondResultMetric.getMetricName(), "HeapMemoryUsage.min"); + Assert.assertEquals(secondResultMetric.getMetricValue(), "50"); + Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); + Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); + Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "SUM"); + } + } - + private CompositeDataSupport createCompositeDataSupportObject () throws OpenDataException { + String typeName = "type"; + String description = "description"; + String[] itemNames = {"min", "max"}; + String[] itemDescriptions = {"maxDesc", "minDesc"}; + OpenType[] itemTypes = new OpenType[]{new OpenType("java.lang.String", "type", + "description") { + @Override + public boolean isValue (Object obj) { + return true; + } + @Override + public boolean equals (Object obj) { + return false; + } + @Override + public int hashCode () { + return 0; + } + @Override + public String toString () { + return "50"; + } + }, new OpenType("java.lang.String", "type", "description") { + @Override + public boolean isValue (Object obj) { + return true; + } + @Override + public boolean equals (Object obj) { + return false; + } + @Override + public int hashCode () { + return 0; + } + @Override + public String toString () { + return "100"; + } + }}; + CompositeType compositeType = new CompositeType(typeName, description, itemNames, + itemDescriptions, itemTypes); + String[] itemNamesForCompositeDataSupport = {"min", "max"}; + Object[] itemValuesForCompositeDataSupport = {new BigDecimal(50), new BigDecimal(100)}; + return new CompositeDataSupport(compositeType, itemNamesForCompositeDataSupport, + itemValuesForCompositeDataSupport); } } + diff --git a/src/test/resources/conf/config.yml b/src/test/resources/conf/config.yml deleted file mode 100755 index ed8b437..0000000 --- a/src/test/resources/conf/config.yml +++ /dev/null @@ -1,43 +0,0 @@ -### ANY CHANGES TO THIS FILE DOES NOT REQUIRE A RESTART ### - - -metricPrefix: Custom Metrics|Kafka -#metricPrefix: Server|Component:|Custom Metrics|Kafka - -# List of Kafka Instances -servers: - - host: "localhost" - port: "9999" - username: "" - password: "" - #encryptedPassword: - #encryptionKey: - useSSl: false - displayName: "Local Kafka Server" - -connection: - useSsl: false - socketTimeout: 3000 - connectTimeout: 1000 - sslProtocols: ["TLSV1.2"] - sslCertCheckEnabled: true - sslVerifyHostname: true - - sslCipherSuites: [] - sslTrustStorePath: "" - sslTrustStorePassword: "" - sslTrustStoreEncryptedPassword: "" - useDefaultSslConnectionFactory: false - -numberOfThreads: 10 - -mbeans: - - - objectName: ["kafka.server:type=BrokerTopicMetrics,*"] - metrics: - Count: - alias: "Count" -# @todo: metric qualifiers - - MeanRate: - alias: "MeanRate" \ No newline at end of file diff --git a/src/test/resources/conf/config_for_composite_metrics.yml b/src/test/resources/conf/config_for_composite_metrics.yml deleted file mode 100755 index f489623..0000000 --- a/src/test/resources/conf/config_for_composite_metrics.yml +++ /dev/null @@ -1,17 +0,0 @@ -connection: - socketTimeout: 3000 - connectTimeout: 1000 - sslProtocols: ["TLSV1.2"] - sslCertCheckEnabled: true - sslVerifyHostname: true - - sslCipherSuites: [] - sslTrustStorePath: "" - sslTrustStorePassword: "" - sslTrustStoreEncryptedPassword: "" - useDefaultSslConnectionFactory: - -- objectName: ["java.lang:type=Memory"] - metrics: - HeapMemoryUsage.committed: "Heap Memory Usage | Committed" - HeapMemoryUsage.max: "Heap Memory Usage | Max" \ No newline at end of file diff --git a/src/test/resources/conf/config_for_non_composite_metrics.yml b/src/test/resources/conf/config_for_non_composite_metrics.yml index 38b64c2..61d2437 100644 --- a/src/test/resources/conf/config_for_non_composite_metrics.yml +++ b/src/test/resources/conf/config_for_non_composite_metrics.yml @@ -1,11 +1,31 @@ -metricPrefix: "Custom Metrics|Kafka|" +metricPrefix: "Custom Metrics|Kafka" + +# List of Kafka Instances +servers: + - host: "localhost" + port: "9999" + username: "" + password: "" + useSsl: false + displayName: "Test Kafka Server1" + mbeans: - objectName: "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec" metrics: + - Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - Mean Rate: + alias: "Mean Rate" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" - Count: - alias: "Min Latency" - Value: - alias: "Max Latency" From ad36d4bac602cc843c3594a6c3a43b77d2be1121 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Thu, 2 Aug 2018 11:22:40 -0700 Subject: [PATCH 21/49] code review changes-config files --- ...ig_composite_and_non_composite_metrics.yml | 29 ++++++++++++++++++ src/test/resources/conf/config_for_SSL.yml | 28 +++++++++++++++++ .../conf/config_for_composite_metrics.yml | 30 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 src/test/resources/conf/config_composite_and_non_composite_metrics.yml create mode 100644 src/test/resources/conf/config_for_SSL.yml create mode 100644 src/test/resources/conf/config_for_composite_metrics.yml diff --git a/src/test/resources/conf/config_composite_and_non_composite_metrics.yml b/src/test/resources/conf/config_composite_and_non_composite_metrics.yml new file mode 100644 index 0000000..1d152e9 --- /dev/null +++ b/src/test/resources/conf/config_composite_and_non_composite_metrics.yml @@ -0,0 +1,29 @@ +metricPrefix: "Custom Metrics|Kafka" + +# List of Kafka Instances +servers: + - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" + username: "" + password: "" + useSsl: false + displayName: "Test Kafka Server1" + +mbeans: + + - objectName: "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec" + metrics: + - Count: + alias: "Count" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - HeapMemoryUsage.min: + alias: "Heap Memory Usage | Min" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "SUM" + clusterRollUpType: "INDIVIDUAL" diff --git a/src/test/resources/conf/config_for_SSL.yml b/src/test/resources/conf/config_for_SSL.yml new file mode 100644 index 0000000..c8266de --- /dev/null +++ b/src/test/resources/conf/config_for_SSL.yml @@ -0,0 +1,28 @@ +metricPrefix: "Server|Component:19|Custom Metrics|Kafka" + +servers: + - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" + host: "" + port: "" + username: "" + password: "" + encryptedPassword: "" + displayName: "Local Kafka Server" + useSsl: true + + #Provide the encryption key for the password +encryptionKey: "" + + #Configure this section only if useSSL is set to true. +connection: + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocol: "TLSv1.2" + sslTrustStorePath: "" + sslTrustStorePassword: "" + sslTrustStoreEncryptedPassword: "" + + #key store details for mutual auth on ssl + sslKeyStorePath: "" + sslKeyStorePassword: "" + sslKeyStoreEncryptedPassword: "" \ No newline at end of file diff --git a/src/test/resources/conf/config_for_composite_metrics.yml b/src/test/resources/conf/config_for_composite_metrics.yml new file mode 100644 index 0000000..57dd14a --- /dev/null +++ b/src/test/resources/conf/config_for_composite_metrics.yml @@ -0,0 +1,30 @@ +metricPrefix: "Custom Metrics|Kafka" + +# List of Kafka Instances +servers: + - host: "localhost" + port: "9999" + username: "" + password: "" + useSsl: false + displayName: "Test Kafka Server" + +mbeans: + + - objectName: "java.lang:type=Memory" + metrics: + - HeapMemoryUsage.min: + alias: "Heap Memory Usage | Min" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - HeapMemoryUsage.max: + alias: "Heap Memory Usage | max" + multiplier: "" + delta: "false" + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" From 3e9eff42510a5506713636145c28f127080689e0 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Thu, 2 Aug 2018 16:15:46 -0700 Subject: [PATCH 22/49] code review 2 comments --- .../kafka/CustomSSLSocketFactory.java | 11 +++++--- .../kafka/JMXConnectionAdapter.java | 7 +++--- .../extensions/kafka/KafkaMonitor.java | 25 +++++++++---------- .../extensions/kafka/KafkaMonitorTask.java | 13 +++++----- .../kafka/metrics/DomainMetricsProcessor.java | 3 ++- src/main/resources/conf/config.yml | 8 +++--- .../metrics/DomainMetricsProcessorTest.java | 2 +- 7 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java index ce1eefc..5445c3a 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java +++ b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java @@ -33,7 +33,8 @@ public class CustomSSLSocketFactory extends SslRMIClientSocketFactory { private static final Logger logger = LoggerFactory.getLogger(CustomSSLSocketFactory.class); - +//todo: access specifiers +//todo: remove redundant method public SSLSocketFactory createSocketFactory(String encryptionKey, Map connectionMap) throws IOException { String trustStorePath = resolveTrustStorePath(connectionMap); char[] trustStorePassword = getTrustStorePassword(encryptionKey, connectionMap); @@ -54,6 +55,7 @@ public SSLSocketFactory createSocketFactory(String encryptionKey, Map TrustManagerFactory trustManagerFactory = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(trustStore); + SSLContext sslContext = SSLContext.getInstance(connectionMap.get("sslProtocol").toString()); sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom()); return sslContext.getSocketFactory(); @@ -73,7 +75,7 @@ public SSLSocketFactory createSocketFactory(String encryptionKey, Map return null; } - + //todo: refactor this code to remove redundant methods SSLSocketFactory createSocketFactory(Map connectionMap) throws IOException { String trustStorePath = resolveTrustStorePath(connectionMap); char[] trustStorePassword = getTrustStorePassword("", connectionMap); @@ -106,13 +108,13 @@ SSLSocketFactory createSocketFactory(Map connectionMap) throws IOExce } catch (KeyStoreException e) { logger.error("KeyStore exception {}", e); } catch (KeyManagementException e) { - logger.error("KeyManagement Exception {}" , e); - }catch(UnrecoverableKeyException e){ + logger.error("KeyManagemena){ logger.error("Unrecoverable Key {}",e); } return null; } + //todo:chk path resolver if certs need to be in socketfactory directory String resolveTrustStorePath(Map connectionMap) { File installDir = PathResolver.resolveDirectory(CustomSSLSocketFactory.class); String sslTrustStorePath = (String) connectionMap.get("sslTrustStorePath"); @@ -157,6 +159,7 @@ String resolveKeyStorePath(Map connectionMap) { return getPath(file).toString(); } + //todo:change access specifiers to private protected static char[] getKeyStorePassword(String encryptionKey, Map connection) { String sslTrustStorePassword = (String) connection.get("sslKeyStorePassword"); if (!Strings.isNullOrEmpty(sslTrustStorePassword)) { diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index 675938f..8f65145 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -66,7 +66,7 @@ JMXConnector open(boolean useSsl, String encryptionKey, Map connectio JMXConnector jmxConnector; final Map env = new HashMap(); boolean useDefaultSslFactory= YmlUtils.getBoolean(connectionMap.get("useDefaultSslConnectionFactory")); - + //todo: pass configurations\ instead of Yml Utils if(useSsl) { if (Preconditions.checkNotNull(useDefaultSslFactory) && useDefaultSslFactory){ SslRMIClientSocketFactory sslRMIClientSocketFactory = new SslRMIClientSocketFactory(); @@ -74,6 +74,7 @@ JMXConnector open(boolean useSsl, String encryptionKey, Map connectio } else if (Preconditions.checkNotNull(useDefaultSslFactory) && !useDefaultSslFactory) { CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE + //todo: handle null ,customSSLSocketFactory.createSocketFactory( encryptionKey, connectionMap)); } } @@ -88,7 +89,7 @@ JMXConnector open(boolean useSsl, String encryptionKey, Map connectio JMXConnector open(boolean useSsl, Map connectionMap) throws IOException { JMXConnector jmxConnector; - final Map env = new HashMap(); + final Map env = new HashMap(); boolean useDefaultSslFactory= YmlUtils.getBoolean(connectionMap.get("useDefaultSslConnectionFactory")); if(useSsl) { @@ -100,7 +101,7 @@ JMXConnector open(boolean useSsl, Map connectionMap) throws IOExcepti env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE ,customSSLSocketFactory.createSocketFactory(connectionMap)); } - } + }//todo: remove redundant calls to same method if (!Strings.isNullOrEmpty(this.username)) { env.put(JMXConnector.CREDENTIALS, new String[]{username, password}); } diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index 85cc7c1..61a0d18 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -35,18 +35,18 @@ protected void initializeMoreStuff(Map args) { Map connectionMap = (Map) this.getContextConfiguration() .getConfigYml().get(Constants.CONNECTION); boolean flag = false; +//todo: take params from connection section +// todo: useDefault ssl is false only then set properties +//todo:move it to onConfigReload + +// if (YmlUtils.getBoolean((server.get("useSsl")))) { +// System.setProperty("javax.net.ssl.trustStore", +// connectionMap.get("sslTrustStorePath").toString()); +// System.setProperty("javax.net.ssl.trustStorePassword", +// connectionMap.get("sslTrustStorePassword").toString()); +// +// } - for (Map server : servers) { - if(!flag) { - if (YmlUtils.getBoolean((server.get("useSsl")))) { - System.setProperty("javax.net.ssl.trustStore", - connectionMap.get("sslTrustStorePath").toString()); - System.setProperty("javax.net.ssl.trustStorePassword", - connectionMap.get("sslTrustStorePassword").toString()); - flag = true; - break; - } - } } } @@ -54,7 +54,7 @@ protected void initializeMoreStuff(Map args) { public String getMonitorName() { return "Kafka Monitor"; - } + } //todo:move to constants protected void doRun(TasksExecutionServiceProvider tasksExecutionServiceProvider) { List> kafkaServers = (List>) @@ -62,7 +62,6 @@ protected void doRun(TasksExecutionServiceProvider tasksExecutionServiceProvider for (Map kafkaServer : kafkaServers) { KafkaMonitorTask task = new KafkaMonitorTask(tasksExecutionServiceProvider, this.getContextConfiguration(), kafkaServer); - //todo: do we need an assert for displayname AssertUtils.assertNotNull(kafkaServer.get(Constants.DISPLAY_NAME), "The displayName can not be null"); tasksExecutionServiceProvider.submit(kafkaServer.get(Constants.DISPLAY_NAME), task); diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index 43e8f85..a062e6a 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -94,12 +94,13 @@ private BigDecimal openJMXConnection() { Map requestMap = buildRequestMap(); jmxAdapter = JMXConnectionAdapter.create(requestMap); Map connectionMap = getConnectionParameters(); - if(configuration.getConfigYml().containsKey("encryptionKey") && !Strings.isNullOrEmpty( configuration.getConfigYml().get("encryptionKey").toString()) ) + if(configuration.getConfigYml().containsKey("encryptionKey") && !Strings.isNullOrEmpty( configuration.getConfigYml().get("encryptionKey").toString()) ) { jmxConnection = jmxAdapter.open(YmlUtils.getBoolean(this.kafkaServer.get("useSsl")), configuration.getConfigYml().get("encryptionKey").toString(), connectionMap); + } - else + else { jmxConnection = jmxAdapter.open(YmlUtils.getBoolean(this.kafkaServer.get("useSsl")), connectionMap); - + } logger.debug("JMX Connection is open to Kafka server: {}", this.kafkaServer.get(Constants.DISPLAY_NAME)); return BigDecimal.ONE; @@ -112,7 +113,7 @@ private BigDecimal openJMXConnection() { private Map buildRequestMap() { Map requestMap = new HashMap<>(); - requestMap.put(Constants.HOST, this. kafkaServer.get(Constants.HOST)); + requestMap.put(Constants.HOST, this.kafkaServer.get(Constants.HOST)); requestMap.put(Constants.PORT, this.kafkaServer.get(Constants.PORT)); requestMap.put(Constants.DISPLAY_NAME, this.kafkaServer.get(Constants.DISPLAY_NAME)); requestMap.put(Constants.SERVICE_URL, this.kafkaServer.get(Constants.SERVICE_URL)); @@ -121,6 +122,7 @@ private Map buildRequestMap() { return requestMap; } + //todo: refactor method to remove redundancy private String getPassword() { String password = this.kafkaServer.get(Constants.PASSWORD); if (!Strings.isNullOrEmpty(password)) { return password; } @@ -140,8 +142,7 @@ private String getPassword() { } private Map getConnectionParameters(){ - Map connectionMap = (Map) configuration.getConfigYml().get(Constants.CONNECTION); - return connectionMap; + return (Map) configuration.getConfigYml().get(Constants.CONNECTION); } } diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index 82d9280..9615f7f 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -34,6 +34,7 @@ import java.util.*; public class DomainMetricsProcessor { + //todo:access modifiers static final org.slf4j.Logger logger = LoggerFactory.getLogger(DomainMetricsProcessor.class); private final JMXConnectionAdapter jmxAdapter; private final JMXConnector jmxConnection; @@ -56,6 +57,7 @@ public DomainMetricsProcessor(MonitorContextConfiguration configuration, JMXConn } public void populateMetricsForMBean() { + //todo:metric apth for heartbeat try { String objectName = (String) this.mbeanFromConfig.get(Constants.OBJECTNAME); List> metricsList = (List>) this.mbeanFromConfig.get(Constants.METRICS); @@ -75,7 +77,6 @@ public void populateMetricsForMBean() { private List getNodeMetrics(JMXConnector jmxConnection, String objectName, List> metricProperties) - throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { List nodeMetrics = Lists.newArrayList(); diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index 1a3bb84..9ac2c28 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -22,7 +22,7 @@ encryptionKey: "" #Configure this section only if useSsl is set to true in any of the servers in the above section - +#todo:refactor this path connection: socketTimeout: 3000 connectTimeout: 1000 @@ -31,13 +31,14 @@ connection: sslTrustStorePassword: "" sslTrustStoreEncryptedPassword: "" +#todo: mutual auth removal #key store details for mutual auth on ssl sslKeyStorePath: "" sslKeyStorePassword: "" sslKeyStoreEncryptedPassword: "" # Set the following flag to true if you want to use the certs that are provided by default JDK - useDefaultSslConnectionFactory: false + useDefaultSslConnectionFactory: true # Each Kafka server needs 1 thread each, so please configure this according to the number of servers you are monitoring numberOfThreads: 10 @@ -48,7 +49,8 @@ mbeans: # Each "objectName" is fully qualified path of the object of the Kafka server # For example "kafka.server:type=ReplicaManager,*" will return all objects nested under ReplicaManager type # Each of the entries in the "metrics" section is an attribute of the objectName under which it's listed - +#todo:change delta to boolean +#TODO: UNITS - objectName: "kafka.server:type=BrokerTopicMetrics,*" metrics: - Count: diff --git a/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java index 3697168..d192c12 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java @@ -104,7 +104,7 @@ public void getMetricsForCompositeObject() throws MalformedObjectNameException, attributes.add(new Attribute("Count", 100)); attributes.add(new Attribute("Mean Rate", 200 )); List metricNames = Lists.newArrayList(); - metricNames.add("metric1"); + metricNames.add("metric1");//todo:remove this if not needed metricNames.add("metric2"); doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector),Mockito.any(ObjectName.class) ); doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector), Mockito.any(ObjectInstance.class)); From a11a8b835bceb343a85847155fc79a4955148372 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Tue, 7 Aug 2018 11:31:16 -0700 Subject: [PATCH 23/49] changes after CR 2 --- pom.xml | 2 +- .../kafka/CustomSSLSocketFactory.java | 124 ++--------------- .../kafka/JMXConnectionAdapter.java | 52 +++---- .../extensions/kafka/KafkaMonitor.java | 53 ++++--- .../extensions/kafka/KafkaMonitorTask.java | 72 ++++++---- .../kafka/metrics/DomainMetricsProcessor.java | 56 ++++---- .../extensions/kafka/utils/Constants.java | 10 ++ src/main/resources/conf/config.yml | 129 +++++++----------- .../metrics/CustomSSLSocketFactoryTest.java | 83 ++++++----- .../metrics/DomainMetricsProcessorTest.java | 62 +++++---- src/test/resources/conf/config_for_SSL.yml | 6 +- 11 files changed, 275 insertions(+), 374 deletions(-) diff --git a/pom.xml b/pom.xml index da2ecd0..50f646a 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.appdynamics.extensions kafka-monitoring-extension - 2.0 + 2.0.0 jar kafka-monitoring-extension http://maven.apache.org diff --git a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java index 5445c3a..01e1776 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java +++ b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java @@ -17,38 +17,25 @@ package com.appdynamics.extensions.kafka; import com.appdynamics.extensions.crypto.Decryptor; -import com.appdynamics.extensions.util.PathResolver; import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.*; import javax.rmi.ssl.SslRMIClientSocketFactory; -import java.io.File; import java.io.FileInputStream; import java.io.IOException; - import java.security.*; import java.security.cert.CertificateException; import java.util.Map; public class CustomSSLSocketFactory extends SslRMIClientSocketFactory { + private static final Logger logger = LoggerFactory.getLogger(CustomSSLSocketFactory.class); -//todo: access specifiers -//todo: remove redundant method - public SSLSocketFactory createSocketFactory(String encryptionKey, Map connectionMap) throws IOException { - String trustStorePath = resolveTrustStorePath(connectionMap); - char[] trustStorePassword = getTrustStorePassword(encryptionKey, connectionMap); - String keyStorePath = resolveKeyStorePath(connectionMap); - char[] keyStorePassword = getKeyStorePassword(encryptionKey, connectionMap); + public SSLSocketFactory createSocketFactory(Map connectionMap) throws IOException { + String trustStorePath = connectionMap.get("sslTrustStorePath").toString(); + char[] trustStorePassword = getTrustStorePassword(connectionMap.get("encryptionKey").toString(), connectionMap); try { - //initializing keystore - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load(new FileInputStream(keyStorePath), keyStorePassword); - KeyManagerFactory keyManagerFactory = KeyManagerFactory - .getInstance(KeyManagerFactory.getDefaultAlgorithm()); - keyManagerFactory.init(keyStore, keyStorePassword); - //initializing truststore KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); trustStore.load(new FileInputStream(trustStorePath), trustStorePassword); @@ -56,81 +43,26 @@ public SSLSocketFactory createSocketFactory(String encryptionKey, Map .getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(trustStore); + //create ssl Context of the protocol specified in config.yml SSLContext sslContext = SSLContext.getInstance(connectionMap.get("sslProtocol").toString()); - sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom()); + //initialize with the trust managers + sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom()); + return sslContext.getSocketFactory(); } catch (NoSuchAlgorithmException exception) { - logger.debug("No Such algorithm"); + logger.error("No Such algorithm", exception); } catch (CertificateException e) { - logger.error("CommonName in the certificate is not the same as the host name: {}", e); + logger.error("CommonName in the certificate is not the same as the host name", e); } catch (KeyStoreException e) { - logger.error(""); + logger.error("Cannot initialize trustStore", e); } catch (KeyManagementException e) { - logger.error(""); - } - catch(UnrecoverableKeyException uke){ - + logger.error("Cannot initialize keyMangers", e); } return null; } - //todo: refactor this code to remove redundant methods - SSLSocketFactory createSocketFactory(Map connectionMap) throws IOException { - String trustStorePath = resolveTrustStorePath(connectionMap); - char[] trustStorePassword = getTrustStorePassword("", connectionMap); - String keyStorePath = resolveKeyStorePath(connectionMap); - char[] keyStorePassword = getKeyStorePassword("",connectionMap); - - try { - //initializing keystore - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load(new FileInputStream(keyStorePath), keyStorePassword); - KeyManagerFactory keyManagerFactory = KeyManagerFactory - .getInstance(KeyManagerFactory.getDefaultAlgorithm()); - keyManagerFactory.init(keyStore, keyStorePassword); - - //initializing truststore - KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); - trustStore.load(new FileInputStream(trustStorePath), trustStorePassword); - TrustManagerFactory trustManagerFactory = TrustManagerFactory - .getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(trustStore); - SSLContext sslContext = SSLContext.getInstance(connectionMap.get("sslProtocol").toString()); - sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), - new SecureRandom()); - - return sslContext.getSocketFactory(); - }catch(NoSuchAlgorithmException exception){ - logger.debug("No Such algorithm"); - } catch (CertificateException e) { - logger.error("CommonName in the certificate is not the same as the host name: {}",e); - } catch (KeyStoreException e) { - logger.error("KeyStore exception {}", e); - } catch (KeyManagementException e) { - logger.error("KeyManagemena){ - logger.error("Unrecoverable Key {}",e); - } - return null; - } - - //todo:chk path resolver if certs need to be in socketfactory directory - String resolveTrustStorePath(Map connectionMap) { - File installDir = PathResolver.resolveDirectory(CustomSSLSocketFactory.class); - String sslTrustStorePath = (String) connectionMap.get("sslTrustStorePath"); - File file = PathResolver.getFile(sslTrustStorePath, installDir); - logger.debug("The config property [sslTrustStorePath] with value [{}] is resolved to file [{}]" - , sslTrustStorePath, getPath(file)); - if (file == null || !file.exists()) { - logger.debug("The sslTrustStorePath [{}] doesn't exist", getPath(file)); - return null; - } - return getPath(file).toString(); - } - - private static Object getPath(File file) { return file != null ? file.getAbsolutePath() : null; } - - protected static char[] getTrustStorePassword(String encryptionKey, Map connection) { + private static char[] getTrustStorePassword(String encryptionKey, Map connection) { String sslTrustStorePassword = (String) connection.get("sslTrustStorePassword"); if (!Strings.isNullOrEmpty(sslTrustStorePassword)) { return sslTrustStorePassword.toCharArray(); @@ -146,34 +78,4 @@ protected static char[] getTrustStorePassword(String encryptionKey, Map connectionMap) { - File installDir = PathResolver.resolveDirectory(CustomSSLSocketFactory.class); - String sslTrustStorePath = (String) connectionMap.get("sslKeyStorePath"); - File file = PathResolver.getFile(sslTrustStorePath, installDir); - logger.debug("The config property [sslTrustStorePath] with value [{}] is resolved to file [{}]" - , sslTrustStorePath, getPath(file)); - if (file == null || !file.exists()) { - logger.debug("The sslTrustStorePath [{}] doesn't exist", getPath(file)); - return null; - } - return getPath(file).toString(); - } - - //todo:change access specifiers to private - protected static char[] getKeyStorePassword(String encryptionKey, Map connection) { - String sslTrustStorePassword = (String) connection.get("sslKeyStorePassword"); - if (!Strings.isNullOrEmpty(sslTrustStorePassword)) { - return sslTrustStorePassword.toCharArray(); - } else { - String sslTrustStoreEncryptedPassword = (String) connection.get("sslKeyStoreEncryptedPassword"); - if (!Strings.isNullOrEmpty(sslTrustStoreEncryptedPassword) && !Strings.isNullOrEmpty(encryptionKey)) { - return new Decryptor(encryptionKey).decrypt(sslTrustStoreEncryptedPassword).toCharArray(); - } else { - logger.warn("Returning null password for sslKeyStore. Please set the [connection.sslKeyStore] or " + - "[connection.sslKeyStoreEncryptedPassword + encryptionKey]"); - return null; - } - } - } - } \ No newline at end of file diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index 8f65145..e3b2f1e 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -17,10 +17,11 @@ import com.appdynamics.extensions.kafka.utils.Constants; -import com.appdynamics.extensions.util.YmlUtils; -import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Lists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import javax.management.Attribute; import javax.management.AttributeList; import javax.management.InstanceNotFoundException; @@ -34,6 +35,7 @@ import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import javax.management.remote.rmi.RMIConnectorServer; +import javax.net.ssl.SSLSocketFactory; import javax.rmi.ssl.SslRMIClientSocketFactory; import java.io.IOException; import java.net.MalformedURLException; @@ -47,6 +49,7 @@ public class JMXConnectionAdapter { private final JMXServiceURL serviceUrl; private final String username; private final String password; + private static final Logger logger = LoggerFactory.getLogger(JMXConnectionAdapter.class); private JMXConnectionAdapter(Map requestMap) throws MalformedURLException { if(!Strings.isNullOrEmpty(requestMap.get(Constants.SERVICE_URL))) @@ -62,46 +65,25 @@ static JMXConnectionAdapter create(final Map requestMap) throws return new JMXConnectionAdapter(requestMap); } - JMXConnector open(boolean useSsl, String encryptionKey, Map connectionMap) throws IOException { - JMXConnector jmxConnector; - final Map env = new HashMap(); - boolean useDefaultSslFactory= YmlUtils.getBoolean(connectionMap.get("useDefaultSslConnectionFactory")); - //todo: pass configurations\ instead of Yml Utils - if(useSsl) { - if (Preconditions.checkNotNull(useDefaultSslFactory) && useDefaultSslFactory){ - SslRMIClientSocketFactory sslRMIClientSocketFactory = new SslRMIClientSocketFactory(); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, sslRMIClientSocketFactory); - } else if (Preconditions.checkNotNull(useDefaultSslFactory) && !useDefaultSslFactory) { - CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE - //todo: handle null - ,customSSLSocketFactory.createSocketFactory( encryptionKey, connectionMap)); - } - } - if (!Strings.isNullOrEmpty(this.username)) { - env.put(JMXConnector.CREDENTIALS, new String[]{username, password}); - } - jmxConnector = JMXConnectorFactory.connect(this.serviceUrl,env); - if (jmxConnector == null) { throw new IOException("Unable to connect to Mbean server"); } - return jmxConnector; - } - + JMXConnector open(Map connectionMap) throws IOException { - JMXConnector open(boolean useSsl, Map connectionMap) throws IOException { JMXConnector jmxConnector; - final Map env = new HashMap(); - boolean useDefaultSslFactory= YmlUtils.getBoolean(connectionMap.get("useDefaultSslConnectionFactory")); + final Map env = new HashMap(); - if(useSsl) { - if (Preconditions.checkNotNull(useDefaultSslFactory) && useDefaultSslFactory){ + if(Boolean.valueOf(connectionMap.get("useSsl").toString())) { + if (!connectionMap.containsKey("sslTrustStorePath")){ //using default jre truststore SslRMIClientSocketFactory sslRMIClientSocketFactory = new SslRMIClientSocketFactory(); env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, sslRMIClientSocketFactory); - } else if (Preconditions.checkNotNull(useDefaultSslFactory) && !useDefaultSslFactory) { + } else{ CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE - ,customSSLSocketFactory.createSocketFactory(connectionMap)); + SSLSocketFactory customSslSocketFactoryObject = customSSLSocketFactory.createSocketFactory(connectionMap); + if(null!= customSslSocketFactoryObject) { + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSslSocketFactoryObject); + } + else + {logger.debug("customSslSocketFactoryObject cannot be null");} } - }//todo: remove redundant calls to same method + } if (!Strings.isNullOrEmpty(this.username)) { env.put(JMXConnector.CREDENTIALS, new String[]{username, password}); } diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index 61a0d18..27aa7c6 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -14,11 +14,14 @@ import com.appdynamics.extensions.kafka.utils.Constants; import com.appdynamics.extensions.util.AssertUtils; import com.appdynamics.extensions.util.YmlUtils; +import com.google.common.base.Strings; import com.google.common.primitives.Booleans; import com.singularity.ee.agent.systemagent.api.exception.TaskExecutionException; import org.apache.log4j.ConsoleAppender; import org.apache.log4j.Level; import org.apache.log4j.PatternLayout; + +import java.io.File; import java.io.OutputStreamWriter; import java.util.HashMap; import java.util.List; @@ -28,33 +31,28 @@ public class KafkaMonitor extends ABaseMonitor { @Override - protected void initializeMoreStuff(Map args) { - - List> servers = (List>) this.getContextConfiguration() - .getConfigYml().get(Constants.SERVERS); - Map connectionMap = (Map) this.getContextConfiguration() - .getConfigYml().get(Constants.CONNECTION); - boolean flag = false; -//todo: take params from connection section -// todo: useDefault ssl is false only then set properties -//todo:move it to onConfigReload - -// if (YmlUtils.getBoolean((server.get("useSsl")))) { -// System.setProperty("javax.net.ssl.trustStore", -// connectionMap.get("sslTrustStorePath").toString()); -// System.setProperty("javax.net.ssl.trustStorePassword", -// connectionMap.get("sslTrustStorePassword").toString()); -// -// } - + protected void onConfigReload(File file) { + //todo: needs restart for system props to reflect + Map configMap = (Map) this.getContextConfiguration() + .getConfigYml(); + //if the config yaml contains the field sslTrustStorePath then the keys are set + // if the field is not present, default jre truststore is used + //field cannot be left blank + if(configMap.containsKey("connection")) { + Map connectionMap = (Map) configMap.get("connection"); + if (connectionMap.containsKey(Constants.TRUST_STORE_PATH) && + !Strings.isNullOrEmpty(connectionMap.get(Constants.TRUST_STORE_PATH).toString())) { + System.setProperty("javax.net.ssl.trustStore", connectionMap.get(Constants.TRUST_STORE_PATH).toString()); + System.setProperty("javax.net.ssl.trustStorePassword", connectionMap.get(Constants.TRUST_STORE_PASSWORD).toString()); + } } } protected String getDefaultMetricPrefix() { return DEFAULT_METRIC_PREFIX; } public String getMonitorName() { - return "Kafka Monitor"; - } //todo:move to constants + return Constants.KAFKA_MONITOR; + } protected void doRun(TasksExecutionServiceProvider tasksExecutionServiceProvider) { List> kafkaServers = (List>) @@ -75,6 +73,19 @@ protected int getTaskCount() { return servers.size(); } + // @TODO: to be removed before publishing + public static void main(String[] args) throws TaskExecutionException { + ConsoleAppender ca = new ConsoleAppender(); + ca.setWriter(new OutputStreamWriter(System.out)); + ca.setLayout(new PatternLayout("%-5p [%t]: %m%n")); + ca.setThreshold(Level.DEBUG); + org.apache.log4j.Logger.getRootLogger().addAppender(ca); + KafkaMonitor monitor = new KafkaMonitor(); + Map taskArgs = new HashMap(); + taskArgs.put("config-file", + "/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/main/resources/conf/config.yml"); + monitor.execute(taskArgs, null); + } } \ No newline at end of file diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index a062e6a..a543b8d 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -24,6 +24,7 @@ import com.appdynamics.extensions.crypto.CryptoUtil; import com.appdynamics.extensions.kafka.metrics.DomainMetricsProcessor; import com.appdynamics.extensions.kafka.utils.Constants; +import com.appdynamics.extensions.metrics.Metric; import com.appdynamics.extensions.util.YmlUtils; import com.google.common.base.Strings; import com.google.common.collect.Maps; @@ -58,9 +59,9 @@ public void onTaskComplete() { public void run() { try { - populateAndPrintMetrics(); - logger.info("Completed Kafka Monitoring task for Kafka server: {}", - this.kafkaServer.get(Constants.DISPLAY_NAME)); + populateAndPrintMetrics(); + logger.info("Completed Kafka Monitoring task for Kafka server: {}", + this.kafkaServer.get(Constants.DISPLAY_NAME)); }catch(Exception e ) { logger.error("Exception occurred while collecting metrics for: {} {}", this.kafkaServer.get(Constants.DISPLAY_NAME)); @@ -72,11 +73,17 @@ public void populateAndPrintMetrics() { BigDecimal connectionStatus = openJMXConnection(); List> mbeansFromConfig = (List>) configuration.getConfigYml() .get(Constants.MBEANS); + + DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor(configuration, jmxAdapter, + jmxConnection,displayName, metricWriteHelper); for (Map mbeanFromConfig : mbeansFromConfig) { - DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor(configuration, jmxAdapter, - jmxConnection, mbeanFromConfig, displayName, metricWriteHelper, connectionStatus); - domainMetricsProcessor.populateMetricsForMBean(); + domainMetricsProcessor.populateMetricsForMBean(mbeanFromConfig); } + metricWriteHelper.printMetric(this.configuration.getMetricPrefix() + + Constants.METRIC_SEPARATOR + this.displayName + Constants.METRIC_SEPARATOR+ "kafka.server" + + Constants.METRIC_SEPARATOR+"HeartBeat", + connectionStatus.toString(), + Constants.AVERAGE, Constants.AVERAGE, Constants.INDIVIDUAL); } catch (Exception e) { logger.error("Error while opening JMX connection: {} {}" ,this.kafkaServer.get(Constants.DISPLAY_NAME), e); } finally { @@ -93,15 +100,17 @@ private BigDecimal openJMXConnection() { try { Map requestMap = buildRequestMap(); jmxAdapter = JMXConnectionAdapter.create(requestMap); - Map connectionMap = getConnectionParameters(); - if(configuration.getConfigYml().containsKey("encryptionKey") && !Strings.isNullOrEmpty( configuration.getConfigYml().get("encryptionKey").toString()) ) { - jmxConnection = jmxAdapter.open(YmlUtils.getBoolean(this.kafkaServer.get("useSsl")), configuration.getConfigYml().get("encryptionKey").toString(), connectionMap); - } + Map connectionMap =(Map) getConnectionParameters(); + connectionMap.put("useSsl", this.kafkaServer.get("useSsl") ); - else { - jmxConnection = jmxAdapter.open(YmlUtils.getBoolean(this.kafkaServer.get("useSsl")), connectionMap); + if(configuration.getConfigYml().containsKey("encryptionKey") && + !Strings.isNullOrEmpty( configuration.getConfigYml().get("encryptionKey").toString()) ) { + connectionMap.put("encryptionKey",configuration.getConfigYml().get("encryptionKey").toString()); } + else { connectionMap.put("encryptionKey" ,""); } + + jmxConnection = jmxAdapter.open(connectionMap); logger.debug("JMX Connection is open to Kafka server: {}", this.kafkaServer.get(Constants.DISPLAY_NAME)); return BigDecimal.ONE; } catch (IOException ioe) { @@ -122,27 +131,32 @@ private Map buildRequestMap() { return requestMap; } - //todo: refactor method to remove redundancy private String getPassword() { - String password = this.kafkaServer.get(Constants.PASSWORD); - if (!Strings.isNullOrEmpty(password)) { return password; } - - if(configuration.getConfigYml().containsKey(Constants.ENCRYPTION_KEY) && - configuration.getConfigYml().containsKey(Constants.ENCRYPTED_PASSWORD)) { - String encryptionKey = configuration.getConfigYml().get(Constants.ENCRYPTION_KEY).toString(); - String encryptedPassword = this.kafkaServer.get(Constants.ENCRYPTED_PASSWORD); - if (!Strings.isNullOrEmpty(encryptionKey) && !Strings.isNullOrEmpty(encryptedPassword)) { - java.util.Map cryptoMap = Maps.newHashMap(); - cryptoMap.put(TaskInputArgs.ENCRYPTED_PASSWORD, encryptedPassword); - cryptoMap.put(TaskInputArgs.ENCRYPTION_KEY, encryptionKey); - return CryptoUtil.getPassword(cryptoMap); - } + String password = this.kafkaServer.get(Constants.PASSWORD); + Map configMap = configuration.getConfigYml(); + + if (!Strings.isNullOrEmpty(password)) { return password; } + if(configMap.containsKey(Constants.ENCRYPTION_KEY) && + configMap.containsKey(Constants.ENCRYPTED_PASSWORD)) { + + String encryptionKey = configMap.get(Constants.ENCRYPTION_KEY).toString(); + String encryptedPassword = this.kafkaServer.get(Constants.ENCRYPTED_PASSWORD); + + if (!Strings.isNullOrEmpty(encryptionKey) && !Strings.isNullOrEmpty(encryptedPassword)) { + java.util.Map cryptoMap = Maps.newHashMap(); + cryptoMap.put(TaskInputArgs.ENCRYPTED_PASSWORD, encryptedPassword); + cryptoMap.put(TaskInputArgs.ENCRYPTION_KEY, encryptionKey); + return CryptoUtil.getPassword(cryptoMap); } - return null; + } + return null; } - private Map getConnectionParameters(){ - return (Map) configuration.getConfigYml().get(Constants.CONNECTION); + private Map getConnectionParameters(){ + if(configuration.getConfigYml().containsKey("connection")) + return (Map) configuration.getConfigYml().get(Constants.CONNECTION); + else + return new HashMap<>(); } } diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index 9615f7f..c64876f 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -23,63 +23,55 @@ import com.appdynamics.extensions.kafka.utils.Constants; import com.appdynamics.extensions.metrics.Metric; import com.google.common.base.Strings; -import com.google.common.collect.Lists; import org.slf4j.LoggerFactory; import javax.management.*; import javax.management.openmbean.CompositeData; import javax.management.openmbean.CompositeDataSupport; import javax.management.remote.JMXConnector; import java.io.IOException; -import java.math.BigDecimal; import java.util.*; public class DomainMetricsProcessor { - //todo:access modifiers + static final org.slf4j.Logger logger = LoggerFactory.getLogger(DomainMetricsProcessor.class); private final JMXConnectionAdapter jmxAdapter; private final JMXConnector jmxConnection; private MetricWriteHelper metricWriteHelper; private String metricPrefix; - private Map mbeanFromConfig; private String displayName; - private BigDecimal heartBeat; + private List nodeMetrics = new ArrayList<>(); public DomainMetricsProcessor(MonitorContextConfiguration configuration, JMXConnectionAdapter jmxAdapter, - JMXConnector jmxConnection, Map mbeanFromConfig, String displayName, MetricWriteHelper metricWriteHelper, - BigDecimal heartBeat) { + JMXConnector jmxConnection, String displayName, MetricWriteHelper metricWriteHelper + ) { this.jmxAdapter = jmxAdapter; this.jmxConnection = jmxConnection; this.metricWriteHelper = metricWriteHelper; this.metricPrefix = configuration.getMetricPrefix() + Constants.METRIC_SEPARATOR + displayName; - this.mbeanFromConfig = mbeanFromConfig; this.displayName = displayName; - this.heartBeat = heartBeat; } - public void populateMetricsForMBean() { - //todo:metric apth for heartbeat + public void populateMetricsForMBean(Map mbeanFromConfig) { + try { - String objectName = (String) this.mbeanFromConfig.get(Constants.OBJECTNAME); - List> metricsList = (List>) this.mbeanFromConfig.get(Constants.METRICS); - logger.debug("Processing mbean {} ", objectName); - List finalMetricList = getNodeMetrics(jmxConnection, objectName, metricsList); - finalMetricList.add(new Metric("HeartBeat", heartBeat.toString(), - this.displayName + "|HeartBeat", "AVERAGE", - "AVERAGE", "INDIVIDUAL")); - logger.debug("Printing metrics for server {}", this.displayName); - metricWriteHelper.transformAndPrintMetrics(finalMetricList); - logger.debug("Finished processing mbean {} ", objectName); + String objectName = (String) mbeanFromConfig.get(Constants.OBJECTNAME); + List> metricsList = (List>) mbeanFromConfig.get(Constants.METRICS); + logger.debug("Processing mbean {} ", objectName); + List finalMetricList = getNodeMetrics(jmxConnection, objectName, metricsList); + logger.debug("Printing metrics for server {}", this.displayName); + metricWriteHelper.transformAndPrintMetrics(finalMetricList); + logger.debug("Finished processing mbean {} ", objectName); } catch (IntrospectionException | IOException | MalformedObjectNameException | InstanceNotFoundException | ReflectionException e) { - logger.error("Kafka Monitor error: {} " ,e); - } + logger.error("Kafka Monitor error " ,e); + } } - private List getNodeMetrics(JMXConnector jmxConnection, String objectName, + private List getNodeMetrics (JMXConnector jmxConnection, String objectName, List> metricProperties) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException, MalformedObjectNameException { - List nodeMetrics = Lists.newArrayList(); + Set objectInstances = this.jmxAdapter.queryMBeans(jmxConnection, ObjectName.getInstance(objectName)); for (ObjectInstance instance : objectInstances) { @@ -88,13 +80,13 @@ private List getNodeMetrics(JMXConnector jmxConnection, String objectNam instance.getObjectName(), metricNamesDictionary.toArray( new String[metricNamesDictionary.size()])); for(Map metricPropertiesPerMetric : metricProperties) { - collect(nodeMetrics, attributes, instance, metricPropertiesPerMetric); + collect(attributes, instance, metricPropertiesPerMetric); } } return nodeMetrics; } - private void collect(List nodeMetrics, List attributes, ObjectInstance instance, + private void collect (List attributes, ObjectInstance instance, Map metricProperties) { for (Attribute attribute : attributes) { try { @@ -105,14 +97,14 @@ private void collect(List nodeMetrics, List attributes, Objec Object attributeValue = ((CompositeDataSupport) attribute.getValue()).get(str); if(metricProperties.containsKey(key)){ setMetricDetails(metricPrefix, key, attributeValue.toString(), instance, - (Map)metricProperties.get(key),nodeMetrics); + (Map)metricProperties.get(key)); } } } else{ if(metricProperties.containsKey(attribute.getName())) { setMetricDetails(metricPrefix, attribute.getName(), attribute.getValue(), instance, - (Map)metricProperties.get(attribute.getName()),nodeMetrics); + (Map)metricProperties.get(attribute.getName())); } } } catch (Exception e) { @@ -121,19 +113,19 @@ private void collect(List nodeMetrics, List attributes, Objec } } - private boolean isCompositeDataObject(Attribute attribute){ + private boolean isCompositeDataObject (Attribute attribute){ return attribute.getValue().getClass().equals(CompositeDataSupport.class); } private void setMetricDetails (String metricPrefix, String attributeName, Object attributeValue, - ObjectInstance instance, Map metricPropertiesMap, List nodeMetrics + ObjectInstance instance, Map metricPropertiesMap ) { String metricPath = metricPrefix + Constants.METRIC_SEPARATOR + buildName(instance)+ attributeName; Metric metric = new Metric(attributeName, attributeValue.toString(), metricPath, metricPropertiesMap); nodeMetrics.add(metric); } - private String buildName(ObjectInstance instance) { + private String buildName (ObjectInstance instance) { ObjectName objectName = instance.getObjectName(); Hashtable keyPropertyList = objectName.getKeyPropertyList(); StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java index 72dba8f..c330ee5 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java +++ b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java @@ -4,6 +4,8 @@ public class Constants { public static final String DEFAULT_METRIC_PREFIX = "Custom Metrics|Kafka"; + public static final String KAFKA_MONITOR = "Kafka Monitor"; + public static final String METRIC_SEPARATOR = "|"; public static final String SERVERS = "servers"; @@ -31,4 +33,12 @@ public class Constants { public static final String SERVICE_URL = "serviceUrl"; public static final String CONNECTION = "connection"; + + public static final String AVERAGE = "AVERAGE"; + + public static final String INDIVIDUAL = "INDIVIDUAL"; + + public static final String TRUST_STORE_PATH = "sslTrustStorePath"; + + public static final String TRUST_STORE_PASSWORD = "sslTrustStorePassword"; } diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index 9ac2c28..6e7f026 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -5,40 +5,37 @@ metricPrefix: "Server|Component:|Custom Metrics|Kafka" -# Add your Kafka Instances below: +# Add your Kafka Instances below servers: - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" host: "" port: "" - username: "" - password: "" + username: "monitorRole" + password: "QED" encryptedPassword: "" displayName: "Local Kafka Server" - useSsl: false + useSsl: true #Provide the encryption key for the encrypted password encryptionKey: "" - #Configure this section only if useSsl is set to true in any of the servers in the above section -#todo:refactor this path -connection: - socketTimeout: 3000 - connectTimeout: 1000 - sslProtocol: "TLSv1.2" - sslTrustStorePath: "" - sslTrustStorePassword: "" - sslTrustStoreEncryptedPassword: "" +# Configure the following connection section ONLY if: +# 1. the config property [useSsl] is set to true in any of the servers in the above section AND +# 2. you want to use your own custom SSL certs +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Please REMOVE the connection section below if: +# 1.You are not using SSL for any of the Kafka servers listed in the server section OR +# 2.You are using SSL for any of the servers([useSsl : true]), but you want to use default truststore of the JRE -#todo: mutual auth removal - #key store details for mutual auth on ssl - sslKeyStorePath: "" - sslKeyStorePassword: "" - sslKeyStoreEncryptedPassword: "" - -# Set the following flag to true if you want to use the certs that are provided by default JDK - useDefaultSslConnectionFactory: true +#connection: +# socketTimeout: 3000 +# connectTimeout: 1000 +# sslProtocol: "TLSv1.2" +# sslTrustStorePath: "path/to/custom/truststore" #defaults to conf/cacerts.jks +# sslTrustStorePassword: "changeit" # defaults to empty +# sslTrustStoreEncryptedPassword: "" # Each Kafka server needs 1 thread each, so please configure this according to the number of servers you are monitoring numberOfThreads: 10 @@ -49,32 +46,14 @@ mbeans: # Each "objectName" is fully qualified path of the object of the Kafka server # For example "kafka.server:type=ReplicaManager,*" will return all objects nested under ReplicaManager type # Each of the entries in the "metrics" section is an attribute of the objectName under which it's listed -#todo:change delta to boolean -#TODO: UNITS - - objectName: "kafka.server:type=BrokerTopicMetrics,*" - metrics: - - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - MeanRate: - alias: "Mean Rate" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.server:type=DelayedFetchMetrics,*" + - objectName: "kafka.server:type=BrokerTopicMetrics,*" metrics: - Count: alias: "Count" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -82,16 +61,17 @@ mbeans: - MeanRate: alias: "Mean Rate" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=KafkaRequestHandlerPool,*" metrics: - Count: alias: "Count" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -99,7 +79,7 @@ mbeans: - MeanRate: alias: "Mean Rate" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -108,7 +88,7 @@ mbeans: - Count: alias: "Count" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -116,7 +96,7 @@ mbeans: - MeanRate: alias: "Mean Rate" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -125,7 +105,7 @@ mbeans: - Count: alias: "Count" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -133,7 +113,7 @@ mbeans: - MeanRate: alias: "Mean Rate" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -142,7 +122,7 @@ mbeans: - Count: alias: "Count" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -150,7 +130,7 @@ mbeans: - MeanRate: alias: "Mean Rate" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -159,7 +139,7 @@ mbeans: - Count: alias: "Count" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -167,7 +147,7 @@ mbeans: - MeanRate: alias: "Mean Rate" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -176,7 +156,7 @@ mbeans: - Count: alias: "Count" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -184,7 +164,7 @@ mbeans: - MeanRate: alias: "Mean Rate" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -193,7 +173,7 @@ mbeans: - Value: alias: "Value" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -202,7 +182,7 @@ mbeans: - Value: alias: "Value" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -211,7 +191,7 @@ mbeans: - Value: alias: "Value" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -220,7 +200,7 @@ mbeans: - Value: alias: "Value" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -229,7 +209,7 @@ mbeans: - Value: alias: "Value" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -238,7 +218,7 @@ mbeans: - Value: alias: "Value" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -247,7 +227,7 @@ mbeans: - Value: alias: "Value" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -256,7 +236,7 @@ mbeans: - Value: alias: "Value" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -265,7 +245,7 @@ mbeans: - Value: alias: "Value" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -274,41 +254,34 @@ mbeans: - objectName: "java.lang:type=Memory" metrics: - HeapMemoryUsage.committed: - alias: "Heap Memory Usage | Committed" + alias: "Heap Memory Usage | Committed (bytes)" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" - HeapMemoryUsage.max: - alias: "Heap Memory Usage | Max" + alias: "Heap Memory Usage | Max (bytes)" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" - HeapMemoryUsage.used: - alias: "Non Heap Memory Usage | Used" + alias: "Non Heap Memory Usage | Used (bytes)" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" - NonHeapMemoryUsage.committed: - alias: "Non Heap Memory Usage | Committed" + alias: "Non Heap Memory Usage | Committed (bytes)" multiplier: "" - delta: "false" + delta: false aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" - - NonHeapMemoryUsage.used: - alias: "Non Heap Memory Usage | Used" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" diff --git a/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java index 423b0d6..db59c32 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java @@ -1,13 +1,19 @@ package com.appdynamics.extensions.kafka.metrics; +import com.appdynamics.extensions.AMonitorJob; +import com.appdynamics.extensions.conf.MonitorContextConfiguration; import com.appdynamics.extensions.kafka.CustomSSLSocketFactory; +import com.appdynamics.extensions.util.PathResolver; +import com.singularity.ee.agent.systemagent.api.AManagedMonitor; import org.junit.Test; +import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.management.*; import javax.management.remote.*; import javax.management.remote.rmi.RMIConnectorServer; +import javax.rmi.ssl.SslRMIClientSocketFactory; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; @@ -41,43 +47,46 @@ public void testConfigureSSL() throws Exception { JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); jmxConnectorServer.stop(); } - -// @Test -// public void testCustomSSLFactoryWithKeys() throws Exception { -// int port = 8745; -// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration -// ("Kafka Monitor", -// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), -// Mockito.mock(AMonitorJob.class)); -// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL.yml"); -// Map config = contextConfiguration.getConfigYml(); -// Map connectionMap = (Map) config.get("connection"); -// Map env = new HashMap(); -// CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); -// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory.createSocketFactory("", -// connectionMap)); -// JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); -// jmxConnectorServer.start(); -// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); -// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); -// jmxConnectorServer.stop(); -// } -// -// @Test -// public void testDefaultSSLFactoryWithKeys() throws Exception { -// int port = 8745; -// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration -// ("Kafka Monitor", -// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), -// Mockito.mock(AMonitorJob.class)); -// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL.yml"); -// Map env = new HashMap(); -// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); -// JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); -// jmxConnectorServer.start(); -// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); -// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); // -// } + @Test + public void testCustomSSLFactoryWithKeys() throws Exception { + int port = 8746; + MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration + ("Kafka Monitor", + "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), + Mockito.mock(AMonitorJob.class)); + contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL.yml"); + Map config = contextConfiguration.getConfigYml(); + Map connectionMap = (Map) config.get("connection"); + connectionMap.put("encryptionKey", ""); + Map env = new HashMap(); + CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory.createSocketFactory(connectionMap)); + JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); + jmxConnectorServer.start(); + JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); + JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); + jmxConnectorServer.stop(); + } + + @Test + public void testDefaultSSLFactoryWithKeys() throws Exception { + int port = 8747; + MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration + ("Kafka Monitor", + "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), + Mockito.mock(AMonitorJob.class)); + contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL.yml"); + Map env = new HashMap(); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); + JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); + jmxConnectorServer.start(); + JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); + JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); + + } + + + } \ No newline at end of file diff --git a/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java index d192c12..e3159a1 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java @@ -4,7 +4,6 @@ import com.appdynamics.extensions.MetricWriteHelper; import com.appdynamics.extensions.conf.MonitorContextConfiguration; import com.appdynamics.extensions.kafka.JMXConnectionAdapter; -import com.appdynamics.extensions.kafka.utils.Constants; import com.appdynamics.extensions.metrics.Metric; import com.appdynamics.extensions.util.PathResolver; import com.google.common.collect.Lists; @@ -30,17 +29,20 @@ public class DomainMetricsProcessorTest { - @Test - public void getMetricsForNonCompositeObject() throws IOException, + @Test + public void whenNonCompositeObjectsThenReturnMetrics() throws IOException, IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException { + JMXConnector jmxConnector = mock(JMXConnector.class); JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); + ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration - ("Kafka Monitor", - "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), - Mockito.mock(AMonitorJob.class)); + ("Kafka Monitor", + "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), + Mockito.mock(AMonitorJob.class)); + contextConfiguration.setConfigYml("src/test/resources/conf/config_for_non_composite_metrics.yml"); Map config = contextConfiguration.getConfigYml(); List mBeans = (List) config.get("mbeans"); @@ -52,19 +54,19 @@ public void getMetricsForNonCompositeObject() throws IOException, attributes.add(new Attribute("Mean Rate", 200 )); List metricNames = Lists.newArrayList(); metricNames.add("Count"); - metricNames.add("Mean Rate"); doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector), Mockito.any(ObjectName.class) ); doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector), Mockito.any(ObjectInstance.class)); doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), Mockito.any(String[].class)); - List> servers = (List>)config.get(Constants.SERVERS); + DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( + contextConfiguration, jmxConnectionAdapter, + jmxConnector, "server1", metricWriteHelper); + for (Map mBean : mBeans) { - DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( - contextConfiguration, jmxConnectionAdapter, - jmxConnector, mBean, "server1", metricWriteHelper, BigDecimal.ONE); - domainMetricsProcessor.populateMetricsForMBean(); + + domainMetricsProcessor.populateMetricsForMBean(mBean); verify(metricWriteHelper) .transformAndPrintMetrics(pathCaptor.capture()); Metric firstResultMetric = (Metric)pathCaptor.getValue().get(0); @@ -83,18 +85,22 @@ public void getMetricsForNonCompositeObject() throws IOException, } @Test - public void getMetricsForCompositeObject() throws MalformedObjectNameException, ReflectionException, + //todo: remove lots of common code + public void whenCompositeObjectsThenReturnMetrics() throws MalformedObjectNameException, ReflectionException, InstanceNotFoundException,IntrospectionException,IOException,OpenDataException { JMXConnector jmxConnector = mock(JMXConnector.class); JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); + + ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration ("Kafka Monitor", "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), Mockito.mock(AMonitorJob.class)); + + contextConfiguration.setConfigYml("src/test/resources/conf/config_for_composite_metrics.yml"); - ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); Map config = contextConfiguration.getConfigYml(); List> mBeans = (List>) config.get("mbeans"); Set objectInstances = Sets.newHashSet(); @@ -104,18 +110,16 @@ public void getMetricsForCompositeObject() throws MalformedObjectNameException, attributes.add(new Attribute("Count", 100)); attributes.add(new Attribute("Mean Rate", 200 )); List metricNames = Lists.newArrayList(); - metricNames.add("metric1");//todo:remove this if not needed - metricNames.add("metric2"); doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector),Mockito.any(ObjectName.class) ); doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector), Mockito.any(ObjectInstance.class)); doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), Mockito.any(String[] .class)); - List> servers = (List>)config.get(Constants.SERVERS); + DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( + contextConfiguration, jmxConnectionAdapter, + jmxConnector, "server2", metricWriteHelper); for (Map mBean : mBeans) { - DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( - contextConfiguration, jmxConnectionAdapter, - jmxConnector, mBean, "server2", metricWriteHelper, BigDecimal.ONE); - domainMetricsProcessor.populateMetricsForMBean(); + + domainMetricsProcessor.populateMetricsForMBean(mBean); verify(metricWriteHelper) .transformAndPrintMetrics(pathCaptor.capture()); Metric firstResultMetric = (Metric)pathCaptor.getValue().get(0); @@ -134,17 +138,20 @@ public void getMetricsForCompositeObject() throws MalformedObjectNameException, } @Test - public void getMetricsForCompositeAndNonCompositeObject() throws IOException, + public void whenCompositeAndNonCompositeObjectsThenReturnMetrics() throws IOException, IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException, OpenDataException{ + JMXConnector jmxConnector = mock(JMXConnector.class); JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); + ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration ("Kafka Monitor", "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), Mockito.mock(AMonitorJob.class)); + contextConfiguration.setConfigYml("src/test/resources/conf/config_composite_and_non_composite_metrics.yml"); Map config = contextConfiguration.getConfigYml(); List> mBeans = (List>) config.get("mbeans"); @@ -164,11 +171,12 @@ public void getMetricsForCompositeAndNonCompositeObject() throws IOException, doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), Mockito.any(String[] .class)); + DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( + contextConfiguration, jmxConnectionAdapter, + jmxConnector,"server1", metricWriteHelper); for (Map mBean : mBeans) { - DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( - contextConfiguration, jmxConnectionAdapter, - jmxConnector, mBean, "server1", metricWriteHelper, BigDecimal.ONE); - domainMetricsProcessor.populateMetricsForMBean(); + + domainMetricsProcessor.populateMetricsForMBean(mBean); verify(metricWriteHelper) .transformAndPrintMetrics(pathCaptor.capture()); Metric firstResultMetric = (Metric) pathCaptor.getValue().get(0); @@ -183,7 +191,7 @@ public void getMetricsForCompositeAndNonCompositeObject() throws IOException, Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "SUM"); - } + } } private CompositeDataSupport createCompositeDataSupportObject () throws OpenDataException { diff --git a/src/test/resources/conf/config_for_SSL.yml b/src/test/resources/conf/config_for_SSL.yml index c8266de..4867e24 100644 --- a/src/test/resources/conf/config_for_SSL.yml +++ b/src/test/resources/conf/config_for_SSL.yml @@ -13,13 +13,13 @@ servers: #Provide the encryption key for the password encryptionKey: "" - #Configure this section only if useSSL is set to true. +# Configure this section only if useSSL is set to true. connection: socketTimeout: 3000 connectTimeout: 1000 sslProtocol: "TLSv1.2" - sslTrustStorePath: "" - sslTrustStorePassword: "" + sslTrustStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks" + sslTrustStorePassword: "test1234" sslTrustStoreEncryptedPassword: "" #key store details for mutual auth on ssl From 263e4462164c53d50566563fc034f3249cd66130 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Tue, 7 Aug 2018 14:11:34 -0700 Subject: [PATCH 24/49] updating config.yml comments --- .../extensions/kafka/KafkaMonitor.java | 17 +---------------- src/main/resources/conf/config.yml | 16 ++++++++++------ .../metrics/CustomSSLSocketFactoryTest.java | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index 27aa7c6..db577c4 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -37,7 +37,7 @@ protected void onConfigReload(File file) { .getConfigYml(); //if the config yaml contains the field sslTrustStorePath then the keys are set // if the field is not present, default jre truststore is used - //field cannot be left blank + //if left blank, defaults to /conf/cacerts if(configMap.containsKey("connection")) { Map connectionMap = (Map) configMap.get("connection"); if (connectionMap.containsKey(Constants.TRUST_STORE_PATH) && @@ -73,19 +73,4 @@ protected int getTaskCount() { return servers.size(); } - // @TODO: to be removed before publishing - public static void main(String[] args) throws TaskExecutionException { - ConsoleAppender ca = new ConsoleAppender(); - ca.setWriter(new OutputStreamWriter(System.out)); - ca.setLayout(new PatternLayout("%-5p [%t]: %m%n")); - ca.setThreshold(Level.DEBUG); - org.apache.log4j.Logger.getRootLogger().addAppender(ca); - - KafkaMonitor monitor = new KafkaMonitor(); - Map taskArgs = new HashMap(); - taskArgs.put("config-file", - "/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/main/resources/conf/config.yml"); - monitor.execute(taskArgs, null); - } - } \ No newline at end of file diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index 6e7f026..f202db2 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -4,17 +4,21 @@ #The following prefix will populate the metrics under respective tiers metricPrefix: "Server|Component:|Custom Metrics|Kafka" +# To know your Tier-ID, Please refer the link +# https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695 + # Add your Kafka Instances below + servers: - - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" + - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" #provide service URL or the host/port pair host: "" port: "" - username: "monitorRole" - password: "QED" + username: "" + password: "" encryptedPassword: "" displayName: "Local Kafka Server" - useSsl: true + useSsl: false # set to true if you're using SSL for this server #Provide the encryption key for the encrypted password @@ -33,8 +37,8 @@ encryptionKey: "" # socketTimeout: 3000 # connectTimeout: 1000 # sslProtocol: "TLSv1.2" -# sslTrustStorePath: "path/to/custom/truststore" #defaults to conf/cacerts.jks -# sslTrustStorePassword: "changeit" # defaults to empty +# sslTrustStorePath: "path/to/your/truststore" #defaults to conf/cacerts.jks +# sslTrustStorePassword: "your/truststore/password" # defaults to empty # sslTrustStoreEncryptedPassword: "" # Each Kafka server needs 1 thread each, so please configure this according to the number of servers you are monitoring diff --git a/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java index db59c32..4e0c04d 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java @@ -1,3 +1,20 @@ +/** + * Copyright 2018 AppDynamics, Inc. + * + * 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. + */ + + package com.appdynamics.extensions.kafka.metrics; import com.appdynamics.extensions.AMonitorJob; From 82bc93f3fcf5ea8be4e5f5a66099f6d911ea9101 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Wed, 8 Aug 2018 11:22:03 -0700 Subject: [PATCH 25/49] readme --- README.md | 227 +++++++++++++++++++----------------------------------- 1 file changed, 80 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index 5db38ca..0b51e92 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ AppDynamics Monitoring Extension for use with Kafka -============================== +=================================================== An AppDynamics extension to be used with a stand alone machine agent to provide metrics for Apache Kafka. @@ -9,34 +9,73 @@ real-time. ## Prerequisites ## -1. This extension extracts the metrics from Kafka using the JMX protocol. Make sure you have configured JMX in Kafka - To know more about JMX, please follow the below link - http://docs.oracle.com/javase/6/docs/technotes/guides/management/agent.html - +1. This extension extracts the metrics from Kafka using the JMX protocol. + Please ensure JMX is enabled 2. Please ensure Kafka is up and running and is accessible from the the machine on which machine agent is installed. +3. In order to use this extension, you do need a Standalone JAVA Machine Agent + (https://docs.appdynamics.com/display/PRO44/Standalone+Machine+Agents) + or SIM Agent (https://docs.appdynamics.com/display/PRO44/Server+Visibility).
+ For more details on downloading these products, please visit https://download.appdynamics.com/.
+ The extension needs to be able to connect to the Kafka instance(s) in order to collect and send metrics.
+ To do this, you will have to either establish a remote connection in between the extension and Kafka, + or have an agent on the same machine running the product in order for the extension to collect and send the metrics. + +## Enabling JMX +Before configuring the extension, please make sure to run the below steps to check if the set up is correct. +1. Test connection to the port + ``` + nc -v localhost 9999 + ``` + + If you get ```Connection to localhost port 9999 [tcp/distinct] succeeded!```,it confirms the access to the Kafka server. +
If not the JMX port needs to be enabled. + +2. JMX configuration: +
To enable JMX monitoring for Kafka broker, port 9999 has to be configured to allow monitoring on that port. +
Edit the Kafka start-up script `/bin/kafka-server-start.sh` to include + + ```export JMX_PORT=${JMX_PORT:-9999}``` + Please note, that the Kafka server needs to be restarted once the JMX port is added. + +3. Start jConsole: +
jConsole comes as a utility with installed Java JDK. + In your terminal, please type ```jconsole```.
+ In the remote connection text-box, please put in the service url : + ```service:jmx:rmi:///jndi/rmi://:9999/jmxrmi``` -## Troubleshooting steps ## -Before configuring the extension, please make sure to run the below steps to check if the set up is correct. + In the left pane of the jConsole, the list of mbeans exposed by Kafka is displayed. + It is a good idea to match the mbean configuration in the config.yml against the jconsole. + JMX is case sensitive so make sure the config matches the names listed. + +## Configuring Kafka Start-up scripts ## -1. Telnet into your Kafka server from the box where the extension is deployed. - ``` - telnet + Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
+ + KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false" + + Also, the changes to `kafka-run-class.sh` has to be made on all the Kafka servers that you wish to monitor. + Please note, that any changes to `kafka-run-class.sh` needs the Kafka server to be restarted for the changes to + take effect. + +## SSL and password authentication ### + +If you need to monitor your Kafka servers securely via SSL , please enable the flags mentioned below. +
Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
+ + KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=true -Djavax.net.ssl.keyStore=/Absolute/path/to/keystore -Djavax.net.ssl.keyStorePassword=password -Dcom.sun.management.jmxremote.registry.ssl=false" + +If you also need username/password authentication, please set the flag `-Dcom.sun.management.jmxremote.authenticate=true` +in the `KAFKA_JMX_OPTS` variable. + +#### Keys #### + +#### Password Settings #### +
To know more on how to set the credentials, please see section below(username/password auth) - - It is the jmxremote.port specified.In the case of Kafka it is usually 9999 - - IP address - ``` - If telnet works, it confirm the access to the Kafka server. -2. Start jconsole: - Jconsole comes as a utility with installed jdk. In your terminal, please type ```jconsole``` - In the remote connection text-box, please put in the service url : - ```service:jmx:rmi:///jndi/rmi://:9999/jmxrmi``` -3. In the left pane of the jConsole, the list of mbeans exposed by Kafka is displayed. - It is a good idea to match the mbean configuration in the config.yml against the jconsole. - JMX is case sensitive so make sure the config matches exact. ## Metrics Provided ## @@ -50,125 +89,20 @@ For eg. ## Installation ## 1. Run "mvn clean install" and find the KafkaMonitor.zip file in the "target" folder. You can also download the - KafkaMonitor.zip from [AppDynamics Exchange][]. -2. Unzip as "KafkaMonitor" and copy the "KafkaMonitor" directory to `/monitors` - -# Configuration ## - -Note : Please make sure to not use tab (\t) while editing yaml files. -You may want to validate the yaml file using a [yaml validator](http://yamllint.com/) - -1. Configure the Kafka instances by editing the config.yml file in `/monitors/KafkaMonitor/`. -2. Below is the default config.yml which has metrics configured already - For eg. - -``` -### ANY CHANGES TO THIS FILE DOES NOT REQUIRE A RESTART ### - -#This will create this metric in all the tiers, under this path -metricPrefix: "Server|Component:|Custom Metrics|Kafka" - -#This will create it in specific Tier/Component. -#Please make sure to replace with the appropriate one from your environment. -#To find the in your environment, please follow the screenshot -https://docs.appdynamics.com/display/PRO42/Build+a+Monitoring+Extension+Using+Java - - -# List of Kafka Instances -# Please provide the service url (or) the host/port of each of the Kafka instances - -servers: - - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" - host: "" - port: "" - username: "" - password: "" - encryptedPassword: "" - -# displayName is a REQUIRED field if you have more than 1 Kafka instance being -# otherwise please leave this field empty - - displayName: "Local Kafka Server" -# this field must be set to true if you want to access the metrics securely -#if this is set to true, the connection section below has more parameters that need to be configured. - useSsl: true - -#Provide the encryption key for the encrypted password -encryptionKey: "" - - -#Configure this section only if useSsl is set to true in any of the servers in the above section -connection: - socketTimeout: 3000 - connectTimeout: 1000 - sslProtocol: "TLSv1.2" - sslTrustStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks" - sslTrustStorePassword: "test1234" - sslTrustStoreEncryptedPassword: "" - - #key store details for mutual auth on ssl - sslKeyStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.keystore.jks" - sslKeyStorePassword: "test1234" - sslKeyStoreEncryptedPassword: "" - -# Set the following flag to true if you want to use the certs that are provided by default JDK - useDefaultSslConnectionFactory: false - -# Each Kafka server needs 1 thread each, so please configure this according to the number of servers you are monitoring -numberOfThreads: 10 - -# The configuration of different metrics from all mbeans exposed by Kafka server -mbeans: - -# Each "objectName" is fully qualified path of the object of the Kafka server -# For example "kafka.server:type=BrokerTopicMetrics,*" will return all objects nested under ReplicaManager type -# Each of the entries in the "metrics" section is an attribute of the objectName under which it's listed - - - objectName: "kafka.server:type=BrokerTopicMetrics,*" - metrics: - - Count: - alias: "Count" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - - MeanRate: - alias: "Mean Rate" - multiplier: "" - delta: "false" - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - -3. Configure the path to the config.yml file by editing the in the monitor.xml file in the `/monitors/KafkaMonitor/` directory. Below is the sample - For Windows, make sure you enter the right path. -``` - - - - .... - -``` + KafkaMonitor.zip from [AppDynamics Exchange][https://www.appdynamics.com/community/exchange/]. +2. Unzip as "KafkaMonitor" and copy the "KafkaMonitor" directory to `/monitors` + Please place the extension in the "monitors" directory of your Machine Agent installation directory. + Do not place the extension in the "extensions" directory of your Machine Agent installation directory. -## Custom Dashboard ## -![](https://github.com/Appdynamics/kafka-monitoring-extension/blob/master/Kafka_CustomDashboard.png?raw=true) +## Configuration ## -## Enable JMX ## -To enable JMX Monitoring for Kafka broker, please follow below instructions: +Config.yml -Edit kafka-run-class.sh and modify KAFKA_JMX_OPTS variable like below (please replace <> with your Kafka Broker hostname) +Please copy all the contents of the config.yml file and go to http://www.yamllint.com/ .
+On reaching the website, paste the contents and press the “Go” button on the bottom left.
+If you get a valid output, that means your formatting is correct and you may move on to the next step. -``` -KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=-Djava.net.preferIPv4Stack=true" -``` -Add below line in kafka-server-start.sh - -``` - export JMX_PORT=${JMX_PORT:-9999} -``` ## Credentials Encryption Please visit [this](https://community.appdynamics.com/t5/Knowledge-Base/How-to-use-Password-Encryption-with-Extensions/ta-p/29397) page to get detailed instructions on password encryption. The steps in this document will guide you through the whole process. @@ -187,10 +121,10 @@ Please provide the following in order for us to assist you better.

 1. Stop the running machine agent
. 2. Delete all existing logs under /logs
. 3. Please enable debug logging by editing the file /conf/logging/log4j.xml. Change the level value of the following elements to debug.
 - ``` - - - ``` + + `` + `` + 4. Start the machine agent and please let it run for 10 mins. Then zip and upload all the logs in the directory /logs/*. 5. Attach the zipped /conf/* directory here. 
6. Attach the zipped /monitors/ directory here
. @@ -198,14 +132,13 @@ Please provide the following in order for us to assist you better.

 For any support related questions, you can also contact help@appdynamics.com. ## Contributing -Always feel free to fork and contribute any changes directly via [GitHub](https://github.com/Appdynamics/activemq-monitoring-extension). +Always feel free to fork and contribute any changes directly via [GitHub](https://github.com/Appdynamics/kafka-monitoring-extension). ## Version -| Name | Version | -|--------------------------|------------| -|Extension Version |2.1.0 | -|Controller Compatibility |4.4 or Later| -|Product Tested On |Kafka 2.11. | -|Last Update |07/18/2018 | -|List of Changes |[Change log](https://github.com/Appdynamics/kafka-monitoring-extension/blob/master/Changelog.md) | +Extension Version: 2.0.0 +
Controller Compatibility: 4.0 or Later +
Tested On: Apache Kafka 2.11 +
Operating System Tested On: Mac OS +
Last updated On: Aug 08, 2018 +
List of Changes to this extension [Change log](https://github.com/Appdynamics/kafka-monitoring-extension/blob/master/Changelog.md) From 003596dfc5ea4dd16e95b59a51ffd6735416dcd6 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Wed, 8 Aug 2018 13:56:56 -0700 Subject: [PATCH 26/49] readme ssl section --- README.md | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 138 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0b51e92..738db5c 100755 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ real-time. ## Prerequisites ## 1. This extension extracts the metrics from Kafka using the JMX protocol. - Please ensure JMX is enabled + Please ensure JMX is enabled. 2. Please ensure Kafka is up and running and is accessible from the the machine on which machine agent is installed. 3. In order to use this extension, you do need a Standalone JAVA Machine Agent - (https://docs.appdynamics.com/display/PRO44/Standalone+Machine+Agents) + (https://docs.appdynamics.com/display/PRO44/Standalone+Machine+Agents). or SIM Agent (https://docs.appdynamics.com/display/PRO44/Server+Visibility).
For more details on downloading these products, please visit https://download.appdynamics.com/.
The extension needs to be able to connect to the Kafka instance(s) in order to collect and send metrics.
@@ -22,9 +22,14 @@ real-time. ## Enabling JMX Before configuring the extension, please make sure to run the below steps to check if the set up is correct. -1. Test connection to the port +1. Test connection to the port from the machine where the extension is installed. ``` - nc -v localhost 9999 + nc -v KafkaHostIP port + + For example, connecting to the localhost on port 9999. + + nc -v localhost 9999 + ``` If you get ```Connection to localhost port 9999 [tcp/distinct] succeeded!```,it confirms the access to the Kafka server. @@ -60,24 +65,148 @@ Before configuring the extension, please make sure to run the below steps to che ## SSL and password authentication ### -If you need to monitor your Kafka servers securely via SSL , please enable the flags mentioned below. -
Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
+ If you need to monitor your Kafka servers securely via SSL, please enable the flags mentioned below. +
Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=true -Djavax.net.ssl.keyStore=/Absolute/path/to/keystore -Djavax.net.ssl.keyStorePassword=password -Dcom.sun.management.jmxremote.registry.ssl=false" -If you also need username/password authentication, please set the flag `-Dcom.sun.management.jmxremote.authenticate=true` -in the `KAFKA_JMX_OPTS` variable. + If you also need username/password authentication, please set the flag`-Dcom.sun.management.jmxremote.authenticate=true` + in the `KAFKA_JMX_OPTS` variable. #### Keys #### +To generate your keystore and truststore please follow the steps: +Creating a keystore is mandatory for SSL. However, creating a truststore is optional. You can create +the truststore `kafka.client.truststore.jks`, or you can use the default truststore that comes with the +JRE. + +In case you want to use the default JRE trust store, please replace the `kafka.client.truststore.jks` with +`jre/lib/security/cacerts` + + ``` + #Step #1 + keytool -keystore kafka.server.keystore.jks -alias localhost -validity 365 -genkey + Step #2 + openssl req -new -x509 -keyout ca-key -out ca-cert -days 365 + keytool -keystore kafka.client.truststore.jks -alias CARoot -import -file ca-cert + keytool -keystore kafka.server.truststore.jks -alias CARoot -import -file ca-cert + +``` + Additional info about creating SSL keys is listed [here](https://docs.confluent.io/current/tutorials/security_tutorial.html#creating-ssl-keys-and-certificates) + + #### Password Settings ####
To know more on how to set the credentials, please see section below(username/password auth) +## Configuring the extension using config.yml +Configure the Redis monitoring extension by editing the config.yml file in `/monitors/KafkaMonitor/` + 1. Configure the "tier" under which the metrics need to be reported. This can be done by changing the value of `` in + metricPrefix: "Server|Component:``|Custom Metrics|Kafka".Please refer [how to find component ID](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) + For example, + ``` + metricPrefix: "Server|Component:19|Custom Metrics|Redis" + ``` + 2. Configure the Kafka servers by specifying either Service URL or of all Kafka servers, + username & password (only if authentication enabled),encryptedPassword(only if password encryption required). + If you are using SSL to securely monitor your Kafka servers, please set useSsl is true. + + For example, + ``` + - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" #provide service URL or the pair + host: "" + port: "" + username: "monitorRole" + password: "QED" + encryptedPassword: "" + displayName: "Local Kafka Server" + useSsl: true # set to true if you're using SSL for this server + + ``` + 3. Configure the encyptionKey for encryptionPasswords(only if password encryption required). + + For example, + #Encryption key for Encrypted password. + encryptionKey: "axcdde43535hdhdgfiniyy576" + + 4. Configure the connection section only if you are using monitoring over SSL for ANY Kafka server(s), AND you want to use your own custom + truststore. + Please remove this section if you are using SSL, but yu want to use the default JRE truststore. + Please also remove this section if you are not using SSL for any of your servers. + + ``` + connection: + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocol: "TLSv1.2" + sslTrustStorePath: "/path/to/truststore/client/kafka.client.truststore.jks" #defaults to conf/cacerts.jks + sslTrustStorePassword: "test1234" # defaults to empty + sslTrustStoreEncryptedPassword: "" + ``` + + 5. Configure the numberOfThreads according to the number of Kafka instances you are monitoring on one extension. + Each server needs one thread. + + For example, + + If number Kafka servers that need to be monitored by one extension is 10, then number of threads is 10 + + numberOfThreads: 30 + + 6. Configure the metrics section. + + For configuring the metrics, the following properties can be used: + + | Metric Property | Default value | Possible values | Description | + | :---------------- | :-------------- | :------------------------------ | :------------------------------------------------------------------------------------------------------------- | + | alias | metric name | Any string | The substitute name to be used in the metric browser instead of metric name. | + | aggregationType | "AVERAGE" | "AVERAGE", "SUM", "OBSERVATION" | [Aggregation qualifier](https://docs.appdynamics.com/display/PRO44/Build+a+Monitoring+Extension+Using+Java) | + | timeRollUpType | "AVERAGE" | "AVERAGE", "SUM", "CURRENT" | [Time roll-up qualifier](https://docs.appdynamics.com/display/PRO44/Build+a+Monitoring+Extension+Using+Java) | + | clusterRollUpType | "INDIVIDUAL" | "INDIVIDUAL", "COLLECTIVE" | [Cluster roll-up qualifier](https://docs.appdynamics.com/display/PRO44/Build+a+Monitoring+Extension+Using+Java)| + | multiplier | 1 | Any number | Value with which the metric needs to be multiplied. | + | convert | null | Any key value map | Set of key value pairs that indicates the value to which the metrics need to be transformed. eg: UP:0, DOWN:1 | + | delta | false | true, false | If enabled, gives the delta values of metrics instead of actual values. | + + + + For example, + objectName: "kafka.server:type=BrokerTopicMetrics,* will fetch metrics of all objects nested under + `BrokerTopicMetrics` + + ``` + - objectName: "kafka.server:type=BrokerTopicMetrics,*" + metrics: + - Count: + alias: "Count" + multiplier: "" + delta: false + aggregationType: "OBSERVATION" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + ``` + **All these metric properties are optional, and the default value shown in the table is applied to the metric(if a property has not been specified) by default.** + + If you need a metric from a specific object under an mBean,
+ `objectName: kafka.server:type=ReplicaManager,name=IsrExpandsPerSec` + will return only those metrics corresponding to the `IsrExpandsPerSec` object. + +## Metrics Provided ## +This extension collects metrics via JMX and can be configured to report any of the metrics that Kafka exposes. It provides metrics on +Kafka server, controller and the network. -## Metrics Provided ## +In addition, it also provides the JVM metrics:HeapMemoryUsage.committed, HeapMemoryUsage.max etc +NonHeapMemoryUsage.committed, NonHeapMemoryUsage.max +There is also a HearBeat metric which denotes whether the connection from the extension to the Kafka server was successful. Note : By default, a Machine agent or a AppServer agent can send a fixed number of metrics to the controller. To change this limit, please follow the instructions mentioned [here](http://docs.appdynamics.com/display/PRO14S/Metrics+Limits). From 07347041a1990d24146628d0778ef3ae0bc76715 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Wed, 8 Aug 2018 13:59:20 -0700 Subject: [PATCH 27/49] readme corrections --- README.md | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 738db5c..f6ec98a 100755 --- a/README.md +++ b/README.md @@ -99,14 +99,18 @@ In case you want to use the default JRE trust store, please replace the `kafka.c
To know more on how to set the credentials, please see section below(username/password auth) ## Configuring the extension using config.yml -Configure the Redis monitoring extension by editing the config.yml file in `/monitors/KafkaMonitor/` +Configure the Kafka monitoring extension by editing the config.yml file in `/monitors/KafkaMonitor/ + +Please copy all the contents of the config.yml file and go to http://www.yamllint.com/ .
+On reaching the website, paste the contents and press the “Go” button on the bottom left.
+If you get a valid output, that means your formatting is correct and you may move on to the next step. 1. Configure the "tier" under which the metrics need to be reported. This can be done by changing the value of `` in metricPrefix: "Server|Component:``|Custom Metrics|Kafka".Please refer [how to find component ID](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) For example, ``` - metricPrefix: "Server|Component:19|Custom Metrics|Redis" + metricPrefix: "Server|Component:19|Custom Metrics|Kafka" ``` 2. Configure the Kafka servers by specifying either Service URL or of all Kafka servers, username & password (only if authentication enabled),encryptedPassword(only if password encryption required). @@ -224,15 +228,6 @@ For eg. Please place the extension in the "monitors" directory of your Machine Agent installation directory. Do not place the extension in the "extensions" directory of your Machine Agent installation directory. -## Configuration ## - -Config.yml - -Please copy all the contents of the config.yml file and go to http://www.yamllint.com/ .
-On reaching the website, paste the contents and press the “Go” button on the bottom left.
-If you get a valid output, that means your formatting is correct and you may move on to the next step. - - ## Credentials Encryption Please visit [this](https://community.appdynamics.com/t5/Knowledge-Base/How-to-use-Password-Encryption-with-Extensions/ta-p/29397) page to get detailed instructions on password encryption. The steps in this document will guide you through the whole process. From 709e92867a78de20839630106a53aec7cf64f39e Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Wed, 8 Aug 2018 14:02:23 -0700 Subject: [PATCH 28/49] readme password section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f6ec98a..1cb588f 100755 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ In case you want to use the default JRE trust store, please replace the `kafka.c #### Password Settings #### -
To know more on how to set the credentials, please see section below(username/password auth) +
To know more on how to set the credentials, please see section "Using Password and Access Files" in [this](https://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html). ## Configuring the extension using config.yml Configure the Kafka monitoring extension by editing the config.yml file in `/monitors/KafkaMonitor/ From c8e29622ca9c7e07730334d92bf57c38e1356cc5 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Fri, 10 Aug 2018 13:36:26 -0700 Subject: [PATCH 29/49] ssl serversocketfactory --- .../kafka/CustomSSLSocketFactory.java | 99 +++- .../kafka/JMXConnectionAdapter.java | 54 ++- .../extensions/kafka/KafkaMonitor.java | 30 +- .../extensions/kafka/KafkaMonitorTask.java | 22 +- .../kafka/metrics/DomainMetricsProcessor.java | 2 +- src/main/resources/conf/config.yml | 455 +++++++++--------- .../metrics/CustomSSLSocketFactoryTest.java | 192 +++++--- .../metrics/DomainMetricsProcessorTest.java | 425 ++++++++-------- src/test/resources/conf/config_for_SSL.yml | 4 - .../resources/conf/config_for_SSL_server.yml | 24 + 10 files changed, 751 insertions(+), 556 deletions(-) create mode 100644 src/test/resources/conf/config_for_SSL_server.yml diff --git a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java index 01e1776..1341114 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java +++ b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java @@ -18,37 +18,55 @@ import com.appdynamics.extensions.crypto.Decryptor; import com.google.common.base.Strings; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLContexts; +import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import javax.crypto.Cipher; +import javax.management.remote.JMXServiceURL; +import javax.net.SocketFactory; import javax.net.ssl.*; import javax.rmi.ssl.SslRMIClientSocketFactory; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.net.Socket; import java.security.*; import java.security.cert.CertificateException; import java.util.Map; +import java.util.StringTokenizer; public class CustomSSLSocketFactory extends SslRMIClientSocketFactory { private static final Logger logger = LoggerFactory.getLogger(CustomSSLSocketFactory.class); + private Map connectionMap; + + public CustomSSLSocketFactory() { + } + + public CustomSSLSocketFactory(Map connectionMap) { + this.connectionMap = connectionMap; + } public SSLSocketFactory createSocketFactory(Map connectionMap) throws IOException { String trustStorePath = connectionMap.get("sslTrustStorePath").toString(); char[] trustStorePassword = getTrustStorePassword(connectionMap.get("encryptionKey").toString(), connectionMap); try { - //initializing truststore - KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); - trustStore.load(new FileInputStream(trustStorePath), trustStorePassword); - TrustManagerFactory trustManagerFactory = TrustManagerFactory - .getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(trustStore); + //initializing truststore + KeyStore trustStore = KeyStore.getInstance("JKS"); + trustStore.load(new FileInputStream(trustStorePath), trustStorePassword); + TrustManagerFactory trustManagerFactory = TrustManagerFactory + .getInstance("SunX509"); + trustManagerFactory.init(trustStore); - //create ssl Context of the protocol specified in config.yml - SSLContext sslContext = SSLContext.getInstance(connectionMap.get("sslProtocol").toString()); - //initialize with the trust managers - sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom()); + //create ssl Context + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + //initialize with the trust managers + sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - return sslContext.getSocketFactory(); + return sslContext.getSocketFactory(); } catch (NoSuchAlgorithmException exception) { logger.error("No Such algorithm", exception); @@ -62,6 +80,65 @@ public SSLSocketFactory createSocketFactory(Map connectionMap) throws return null; } + @Override + public Socket createSocket(String host, int port) throws IOException { + // Retrieve the SSLSocketFactory + final SocketFactory sslSocketFactory = createSocketFactory(connectionMap); + // Create the SSLSocket + final SSLSocket sslSocket = (SSLSocket) + sslSocketFactory.createSocket("127.0.0.1", + 9999); + // Set the SSLSocket Enabled Cipher Suites + final String enabledCipherSuites = + connectionMap.get("sslCipherSuites").toString(); + if (enabledCipherSuites != null) { + StringTokenizer st = new StringTokenizer(enabledCipherSuites, ","); + int tokens = st.countTokens(); + String enabledCipherSuitesList[] = new String[tokens]; + for (int i = 0 ; i < tokens; i++) { + enabledCipherSuitesList[i] = st.nextToken(); + } + try { + sslSocket.setEnabledCipherSuites(enabledCipherSuitesList); + } catch (IllegalArgumentException e) { + throw (IOException) + new IOException(e.getMessage()).initCause(e); + } + } + // Set the SSLSocket Enabled Protocols + + final String enabledProtocols = + connectionMap.get("sslProtocols").toString(); + if (enabledProtocols != null) { + StringTokenizer st = new StringTokenizer(enabledProtocols, ","); + int tokens = st.countTokens(); + String enabledProtocolsList[] = new String[tokens]; + for (int i = 0 ; i < tokens; i++) { + enabledProtocolsList[i] = st.nextToken(); + } + try { + sslSocket.setEnabledProtocols(enabledProtocolsList); + } catch (IllegalArgumentException e) { + throw (IOException) + new IOException(e.getMessage()).initCause(e); + } + } + return sslSocket; + } + + public int hashCode() { + return getClass().hashCode(); + } + + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (obj == null || getClass() != obj.getClass()) { + return false; + } + return true; + } + private static char[] getTrustStorePassword(String encryptionKey, Map connection) { String sslTrustStorePassword = (String) connection.get("sslTrustStorePassword"); if (!Strings.isNullOrEmpty(sslTrustStorePassword)) { diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index e3b2f1e..5770f2c 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -17,10 +17,12 @@ import com.appdynamics.extensions.kafka.utils.Constants; +import com.appdynamics.extensions.kafka.utils.CustomSSLServerSocketFactory; import com.google.common.base.Strings; import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sun.management.jmxremote.ConnectorBootstrap; import javax.management.Attribute; import javax.management.AttributeList; @@ -34,11 +36,29 @@ import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; +import javax.management.remote.rmi.RMIConnector; import javax.management.remote.rmi.RMIConnectorServer; +import javax.naming.Context; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; import javax.rmi.ssl.SslRMIClientSocketFactory; +import javax.rmi.ssl.SslRMIServerSocketFactory; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; import java.net.MalformedURLException; +import java.rmi.NotBoundException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.rmi.server.RMISocketFactory; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -61,34 +81,34 @@ private JMXConnectionAdapter(Map requestMap) throws MalformedURL this.password = requestMap.get(Constants.PASSWORD); } - static JMXConnectionAdapter create(final Map requestMap) throws MalformedURLException { + static JMXConnectionAdapter create( Map requestMap) throws MalformedURLException { return new JMXConnectionAdapter(requestMap); } JMXConnector open(Map connectionMap) throws IOException { JMXConnector jmxConnector; - final Map env = new HashMap(); + final Map env = new HashMap<>(); if(Boolean.valueOf(connectionMap.get("useSsl").toString())) { - if (!connectionMap.containsKey("sslTrustStorePath")){ //using default jre truststore + env.put(Context.SECURITY_PROTOCOL, "ssl"); + if (!connectionMap.containsKey(Constants.TRUST_STORE_PATH)){ //using default jre truststore SslRMIClientSocketFactory sslRMIClientSocketFactory = new SslRMIClientSocketFactory(); env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, sslRMIClientSocketFactory); + env.put("com.sun.jndi.rmi.factory.socket", sslRMIClientSocketFactory); } else{ - CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); - SSLSocketFactory customSslSocketFactoryObject = customSSLSocketFactory.createSocketFactory(connectionMap); - if(null!= customSslSocketFactoryObject) { - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSslSocketFactoryObject); - } - else - {logger.debug("customSslSocketFactoryObject cannot be null");} + + CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(connectionMap); + env.put("com.sun.jndi.rmi.factory.socket", customSSLSocketFactory);//// secure RMI connector as well + env.put("jmx.remote.rmi.client.socket.factory", customSSLSocketFactory); + env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new CustomSSLServerSocketFactory(connectionMap).getServerSocketFactory()); } } - if (!Strings.isNullOrEmpty(this.username)) { - env.put(JMXConnector.CREDENTIALS, new String[]{username, password}); - } + + if (!Strings.isNullOrEmpty(this.username)) {env.put(JMXConnector.CREDENTIALS, new String[]{username, password});} jmxConnector = JMXConnectorFactory.connect(this.serviceUrl,env); if (jmxConnector == null) { throw new IOException("Unable to connect to Mbean server"); } + jmxConnector = JMXConnectorFactory.connect(this.serviceUrl,env); return jmxConnector; } @@ -98,13 +118,13 @@ void close(JMXConnector jmxConnector) throws IOException { } } - public Set queryMBeans(final JMXConnector jmxConnection, final ObjectName objectName) + public Set queryMBeans( JMXConnector jmxConnection, ObjectName objectName) throws IOException { MBeanServerConnection connection = jmxConnection.getMBeanServerConnection(); return connection.queryMBeans(objectName, null); } - public List getReadableAttributeNames(final JMXConnector jmxConnection, final ObjectInstance instance) + public List getReadableAttributeNames( JMXConnector jmxConnection, ObjectInstance instance) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException { MBeanServerConnection connection = jmxConnection.getMBeanServerConnection(); List attributeNames = Lists.newArrayList(); @@ -117,8 +137,8 @@ public List getReadableAttributeNames(final JMXConnector jmxConnection, return attributeNames; } - public List getAttributes(final JMXConnector jmxConnection, final ObjectName objectName, - final String[] strings) throws IOException, ReflectionException, + public List getAttributes( JMXConnector jmxConnection, ObjectName objectName, + String[] strings) throws IOException, ReflectionException, InstanceNotFoundException { MBeanServerConnection connection = jmxConnection.getMBeanServerConnection(); AttributeList list = connection.getAttributes(objectName, strings); diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index db577c4..7dff39b 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -23,6 +23,7 @@ import java.io.File; import java.io.OutputStreamWriter; +import java.rmi.server.UnicastRemoteObject; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -38,16 +39,17 @@ protected void onConfigReload(File file) { //if the config yaml contains the field sslTrustStorePath then the keys are set // if the field is not present, default jre truststore is used //if left blank, defaults to /conf/cacerts - if(configMap.containsKey("connection")) { - Map connectionMap = (Map) configMap.get("connection"); - if (connectionMap.containsKey(Constants.TRUST_STORE_PATH) && - !Strings.isNullOrEmpty(connectionMap.get(Constants.TRUST_STORE_PATH).toString())) { - System.setProperty("javax.net.ssl.trustStore", connectionMap.get(Constants.TRUST_STORE_PATH).toString()); - System.setProperty("javax.net.ssl.trustStorePassword", connectionMap.get(Constants.TRUST_STORE_PASSWORD).toString()); - } + if(configMap.containsKey("connection")) { +// Map connectionMap = (Map) configMap.get("connection"); +// if (connectionMap.containsKey(Constants.TRUST_STORE_PATH) && +// !Strings.isNullOrEmpty(connectionMap.get(Constants.TRUST_STORE_PATH).toString())) { +// System.setProperty("javax.net.ssl.trustStore", connectionMap.get(Constants.TRUST_STORE_PATH).toString()); +// System.setProperty("javax.net.ssl.trustStorePassword", connectionMap.get(Constants.TRUST_STORE_PASSWORD).toString()); +// } } } + protected String getDefaultMetricPrefix() { return DEFAULT_METRIC_PREFIX; } public String getMonitorName() { @@ -73,4 +75,18 @@ protected int getTaskCount() { return servers.size(); } + //TODO: remove before publishing + public static void main(String[] args) throws TaskExecutionException { + ConsoleAppender ca = new ConsoleAppender(); + ca.setWriter(new OutputStreamWriter(System.out)); + ca.setLayout(new PatternLayout("%-5p [%t]: %m%n")); + ca.setThreshold(Level.DEBUG); + org.apache.log4j.Logger.getRootLogger().addAppender(ca); + + KafkaMonitor monitor = new KafkaMonitor(); + Map taskArgs = new HashMap(); + taskArgs.put("config-file", "/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/main/resources/conf/config.yml"); + monitor.execute(taskArgs, null); + } + } \ No newline at end of file diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index a543b8d..ebaa445 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -42,7 +42,6 @@ public class KafkaMonitorTask implements AMonitorTaskRunnable { private Map kafkaServer; private MetricWriteHelper metricWriteHelper; private String displayName; - private JMXConnector jmxConnection; private JMXConnectionAdapter jmxAdapter; KafkaMonitorTask(TasksExecutionServiceProvider serviceProvider, MonitorContextConfiguration configuration, @@ -69,15 +68,17 @@ public void run() { } public void populateAndPrintMetrics() { + JMXConnector jmxConnector = null; try{ - BigDecimal connectionStatus = openJMXConnection(); + BigDecimal connectionStatus = BigDecimal.ONE; + jmxConnector = openJMXConnection(); List> mbeansFromConfig = (List>) configuration.getConfigYml() .get(Constants.MBEANS); DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor(configuration, jmxAdapter, - jmxConnection,displayName, metricWriteHelper); + jmxConnector,displayName, metricWriteHelper); for (Map mbeanFromConfig : mbeansFromConfig) { - domainMetricsProcessor.populateMetricsForMBean(mbeanFromConfig); + domainMetricsProcessor.populateMetricsForMBean(jmxConnector, mbeanFromConfig); } metricWriteHelper.printMetric(this.configuration.getMetricPrefix() + Constants.METRIC_SEPARATOR + this.displayName + Constants.METRIC_SEPARATOR+ "kafka.server" + @@ -88,7 +89,7 @@ public void populateAndPrintMetrics() { logger.error("Error while opening JMX connection: {} {}" ,this.kafkaServer.get(Constants.DISPLAY_NAME), e); } finally { try { - jmxAdapter.close(jmxConnection); + jmxAdapter.close(jmxConnector); logger.debug("JMX connection is closed"); } catch (Exception ioe) { logger.error("Unable to close the connection: {} ", ioe); @@ -96,7 +97,7 @@ public void populateAndPrintMetrics() { } } - private BigDecimal openJMXConnection() { + private JMXConnector openJMXConnection() { try { Map requestMap = buildRequestMap(); jmxAdapter = JMXConnectionAdapter.create(requestMap); @@ -110,14 +111,15 @@ private BigDecimal openJMXConnection() { else { connectionMap.put("encryptionKey" ,""); } - jmxConnection = jmxAdapter.open(connectionMap); + JMXConnector jmxConnection = jmxAdapter.open(connectionMap); + if(jmxConnection == null) {logger.error("Connection object is null");} logger.debug("JMX Connection is open to Kafka server: {}", this.kafkaServer.get(Constants.DISPLAY_NAME)); - return BigDecimal.ONE; + return jmxConnection; } catch (IOException ioe) { logger.error("Unable to open a JMX Connection Kafka server: {} {} " , this.kafkaServer.get(Constants.DISPLAY_NAME), ioe); } - return BigDecimal.ZERO; + return null; } private Map buildRequestMap() { @@ -149,7 +151,7 @@ private String getPassword() { return CryptoUtil.getPassword(cryptoMap); } } - return null; + return ""; } private Map getConnectionParameters(){ diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index c64876f..b91ab98 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -51,7 +51,7 @@ public DomainMetricsProcessor(MonitorContextConfiguration configuration, JMXConn this.displayName = displayName; } - public void populateMetricsForMBean(Map mbeanFromConfig) { + public void populateMetricsForMBean(JMXConnector jmxConnection, Map mbeanFromConfig) { try { String objectName = (String) mbeanFromConfig.get(Constants.OBJECTNAME); diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index f202db2..8bbeff4 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -11,14 +11,14 @@ metricPrefix: "Server|Component:|Custom Metrics|Kafka" # Add your Kafka Instances below servers: - - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" #provide service URL or the host/port pair + - serviceUrl: "service:jmx:rmi:///jndi/rmi://127.0.0.1:9999/jmxrmi" #provide service URL or the host/port pair host: "" port: "" - username: "" - password: "" + username: "" + password: "" encryptedPassword: "" displayName: "Local Kafka Server" - useSsl: false # set to true if you're using SSL for this server + useSsl: true # set to true if you're using SSL for this server #Provide the encryption key for the encrypted password @@ -36,9 +36,10 @@ encryptionKey: "" #connection: # socketTimeout: 3000 # connectTimeout: 1000 -# sslProtocol: "TLSv1.2" -# sslTrustStorePath: "path/to/your/truststore" #defaults to conf/cacerts.jks -# sslTrustStorePassword: "your/truststore/password" # defaults to empty +# sslProtocols: "TLSv1.2" +# sslCipherSuites: "" +# sslTrustStorePath: "" #defaults to conf/cacerts.jks +# sslTrustStorePassword: "" # defaults to empty # sslTrustStoreEncryptedPassword: "" # Each Kafka server needs 1 thread each, so please configure this according to the number of servers you are monitoring @@ -58,42 +59,7 @@ mbeans: alias: "Count" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - - MeanRate: - alias: "Mean Rate" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - - objectName: "kafka.server:type=KafkaRequestHandlerPool,*" - metrics: - - Count: - alias: "Count" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - - MeanRate: - alias: "Mean Rate" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec" - metrics: - - Count: - alias: "Count" - multiplier: "" - delta: false - aggregationType: "AVERAGE" + aggregationType: "OBSERVATION" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" @@ -104,188 +70,223 @@ mbeans: aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.server:type=ReplicaManager,name=IsrShrinksPerSec" - metrics: - - Count: - alias: "Count" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - - MeanRate: - alias: "Mean Rate" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.server:type=SessionExpireListener,*" - metrics: - - Count: - alias: "Count" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - - MeanRate: - alias: "Mean Rate" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.network:type=RequestMetrics,*" - metrics: - - Count: - alias: "Count" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - - MeanRate: - alias: "Mean Rate" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.controller:type=ControllerStats,*" - metrics: - - Count: - alias: "Count" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - - MeanRate: - alias: "Mean Rate" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.server:type=DelayedOperationPurgatory,*" - metrics: - - Value: - alias: "Value" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.server:type=ReplicaFetcherManager,*" - metrics: - - Value: - alias: "Value" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.server:type=ReplicaManager,name=LeaderCount" - metrics: - - Value: - alias: "Value" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.server:type=ReplicaManager,name=PartitionCount" - metrics: - - Value: - alias: "Value" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions" - metrics: - - Value: - alias: "Value" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.network:type=Processor,*" - metrics: - - Value: - alias: "Value" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.network:type=RequestChannel,*" - metrics: - - Value: - alias: "Value" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.network:type=SocketServer,*" - metrics: - - Value: - alias: "Value" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.server:type=KafkaServer,name=BrokerState" - metrics: - - Value: - alias: "Value" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - -#JVM Metrics - - objectName: "java.lang:type=Memory" - metrics: - - HeapMemoryUsage.committed: - alias: "Heap Memory Usage | Committed (bytes)" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - - HeapMemoryUsage.max: - alias: "Heap Memory Usage | Max (bytes)" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - - HeapMemoryUsage.used: - alias: "Non Heap Memory Usage | Used (bytes)" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" - - - NonHeapMemoryUsage.committed: - alias: "Non Heap Memory Usage | Committed (bytes)" - multiplier: "" - delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" - clusterRollUpType: "INDIVIDUAL" +# - objectName: "kafka.server:type=KafkaRequestHandlerPool,*" +# metrics: +# - Count: +# alias: "Count" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# +# - MeanRate: +# alias: "Mean Rate" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# - objectName: "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec" +# metrics: +# - Count: +# alias: "Count" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# +# - MeanRate: +# alias: "Mean Rate" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# - objectName: "kafka.server:type=ReplicaManager,name=IsrShrinksPerSec" +# metrics: +# - Count: +# alias: "Count" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# +# - MeanRate: +# alias: "Mean Rate" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# - objectName: "kafka.server:type=SessionExpireListener,*" +# metrics: +# - Count: +# alias: "Count" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# +# - MeanRate: +# alias: "Mean Rate" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# - objectName: "kafka.network:type=RequestMetrics,*" +# metrics: +# - Count: +# alias: "Count" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# +# - MeanRate: +# alias: "Mean Rate" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# - objectName: "kafka.controller:type=ControllerStats,*" +# metrics: +# - Count: +# alias: "Count" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# +# - MeanRate: +# alias: "Mean Rate" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# - objectName: "kafka.server:type=DelayedOperationPurgatory,*" +# metrics: +# - Value: +# alias: "Value" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# - objectName: "kafka.server:type=ReplicaFetcherManager,*" +# metrics: +# - Value: +# alias: "Value" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# - objectName: "kafka.server:type=ReplicaManager,name=LeaderCount" +# metrics: +# - Value: +# alias: "Value" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# - objectName: "kafka.server:type=ReplicaManager,name=PartitionCount" +# metrics: +# - Value: +# alias: "Value" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# - objectName: "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions" +# metrics: +# - Value: +# alias: "Value" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# - objectName: "kafka.network:type=Processor,*" +# metrics: +# - Value: +# alias: "Value" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# - objectName: "kafka.network:type=RequestChannel,*" +# metrics: +# - Value: +# alias: "Value" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# - objectName: "kafka.network:type=SocketServer,*" +# metrics: +# - Value: +# alias: "Value" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# - objectName: "kafka.server:type=KafkaServer,name=BrokerState" +# metrics: +# - Value: +# alias: "Value" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# +##JVM Metrics +# - objectName: "java.lang:type=Memory" +# metrics: +# - HeapMemoryUsage.committed: +# alias: "Heap Memory Usage | Committed (bytes)" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# +# - HeapMemoryUsage.max: +# alias: "Heap Memory Usage | Max (bytes)" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# +# - HeapMemoryUsage.used: +# alias: "Non Heap Memory Usage | Used (bytes)" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# +# - NonHeapMemoryUsage.committed: +# alias: "Non Heap Memory Usage | Committed (bytes)" +# multiplier: "" +# delta: false +# aggregationType: "AVERAGE" +# timeRollUpType: "AVERAGE" +# clusterRollUpType: "INDIVIDUAL" +# diff --git a/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java index 4e0c04d..b753336 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java @@ -31,79 +31,139 @@ import javax.management.remote.*; import javax.management.remote.rmi.RMIConnectorServer; import javax.rmi.ssl.SslRMIClientSocketFactory; +import javax.rmi.ssl.SslRMIServerSocketFactory; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; +import java.rmi.server.RMIClientSocketFactory; +import java.rmi.server.RMIServerSocketFactory; import java.util.HashMap; import java.util.Map; public class CustomSSLSocketFactoryTest { private static final Logger logger = LoggerFactory.getLogger(CustomSSLSocketFactoryTest.class); - - public static JMXConnectorServer startSSL(int port) { - MBeanServer mBeanServer = MBeanServerFactory.createMBeanServer(); - HashMap env = new HashMap(); - JMXConnectorServer jmxConnectorServer = null; - try { - JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); - Registry registry = LocateRegistry.createRegistry(port, null, null); - jmxConnectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mBeanServer); - } catch (Exception ioe) { - logger.debug("Could not connect"); - } - return jmxConnectorServer; - } - - @Test - public void testConfigureSSL() throws Exception { - int port = 8745; - JMXConnectorServer jmxConnectorServer = startSSL(port); - jmxConnectorServer.start(); - JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); - Map env = new HashMap(); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new CustomSSLSocketFactory()); - JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); - jmxConnectorServer.stop(); - } // - @Test - public void testCustomSSLFactoryWithKeys() throws Exception { - int port = 8746; - MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration - ("Kafka Monitor", - "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), - Mockito.mock(AMonitorJob.class)); - contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL.yml"); - Map config = contextConfiguration.getConfigYml(); - Map connectionMap = (Map) config.get("connection"); - connectionMap.put("encryptionKey", ""); - Map env = new HashMap(); - CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory.createSocketFactory(connectionMap)); - JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); - jmxConnectorServer.start(); - JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); - JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); - jmxConnectorServer.stop(); - } - - @Test - public void testDefaultSSLFactoryWithKeys() throws Exception { - int port = 8747; - MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration - ("Kafka Monitor", - "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), - Mockito.mock(AMonitorJob.class)); - contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL.yml"); - Map env = new HashMap(); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); - JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startSSL(port); - jmxConnectorServer.start(); - JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); - JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); - - } - - - +// public static JMXConnectorServer startDefaultSSLServer(int port) { +// MBeanServer mBeanServer = MBeanServerFactory.createMBeanServer(); +// HashMap env = new HashMap(); +// JMXConnectorServer jmxConnectorServer = null; +// try { +// JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); +// Registry registry = LocateRegistry.createRegistry(port, new SslRMIClientSocketFactory(), null); +// jmxConnectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mBeanServer); +// } catch (Exception ioe) { +// logger.debug("Could not connect"); +// } +// return jmxConnectorServer; +// } +// +// @Test +// public void testDefaultSSLServerConnection() throws Exception { +// int port = 8750; +// JMXConnectorServer jmxConnectorServer = startDefaultSSLServer(port); +// jmxConnectorServer.start(); +// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); +// Map env = new HashMap(); +// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); +// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory()); +// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); +// jmxConnectorServer.stop(); +// } +// +// @Test() +// public void testDefaultSSLFactoryWithIncorrectJRETrustStore() throws Exception { +// int port = 8747; +// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration +// ("Kafka Monitor", +// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), +// Mockito.mock(AMonitorJob.class)); +// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL_with_default_jre_keys.yml"); +// Map env = new HashMap(); +// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); +// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory()); +// JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startDefaultSSLServer(port); +// jmxConnectorServer.start(); +// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); +// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); +// jmxConnectorServer.stop(); +// +// } +////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// public static JMXConnectorServer startCustomSSLServer(int port) { +// MBeanServer mBeanServer = MBeanServerFactory.createMBeanServer(); +// HashMap env = new HashMap(); +// JMXConnectorServer jmxConnectorServer = null; +// try { +// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration +// ("Kafka Monitor", +// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), +// Mockito.mock(AMonitorJob.class)); +// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL_server.yml"); +// JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); +// Map config = contextConfiguration.getConfigYml(); +// Map connectionMap = (Map) config.get("connection"); +// connectionMap.put("encryptionKey", ""); +// Registry registry = LocateRegistry.createRegistry(port, new SslRMIClientSocketFactory(),null); +// jmxConnectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mBeanServer); +// } catch (Exception ioe) { +// logger.debug("Could not connect"); +// } +// return jmxConnectorServer; +// } +// +// @Test +// public void testCustomSSLServerConnection() throws Exception { +// int port = 8745; +// JMXConnectorServer jmxConnectorServer = startCustomSSLServer(port); +// jmxConnectorServer.start(); +// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); +// Map env = new HashMap(); +// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new CustomSSLSocketFactory()); +// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, null); +// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); +// jmxConnectorServer.stop(); +// } +// +// @Test +// public void testCustomSSLFactoryWithCorrectTrustStore() throws Exception { +// int port = 8746; +// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration +// ("Kafka Monitor", +// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), +// Mockito.mock(AMonitorJob.class)); +// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL.yml"); +// Map config = contextConfiguration.getConfigYml(); +// Map connectionMap = (Map) config.get("connection"); +// connectionMap.put("encryptionKey", ""); +// Map env = new HashMap(); +// CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); +// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory.createSocketFactory(connectionMap)); +// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory()); +// JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startCustomSSLServer(port); +// jmxConnectorServer.start(); +// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); +// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); +// jmxConnectorServer.stop(); +// } +// +// +// @Test() +// public void testCustomSSLFactoryWithIncorrectKeys() throws Exception { +// int port = 8749; +// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration +// ("Kafka Monitor", +// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), +// Mockito.mock(AMonitorJob.class)); +// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL_with_incorrect_keys.yml"); +// Map config = contextConfiguration.getConfigYml(); +// Map connectionMap = (Map) config.get("connection"); +// connectionMap.put("encryptionKey", ""); +// Map env = new HashMap(); +// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new CustomSSLSocketFactory().createSocketFactory(connectionMap)); +// JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startCustomSSLServer(port); +// jmxConnectorServer.start(); +// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); +// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); +// jmxConnectorServer.stop(); +// } } \ No newline at end of file diff --git a/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java index e3159a1..c705301 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java @@ -29,218 +29,217 @@ public class DomainMetricsProcessorTest { - @Test - public void whenNonCompositeObjectsThenReturnMetrics() throws IOException, - IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException { - - JMXConnector jmxConnector = mock(JMXConnector.class); - JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); - MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); - - ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); - MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration - ("Kafka Monitor", - "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), - Mockito.mock(AMonitorJob.class)); - - contextConfiguration.setConfigYml("src/test/resources/conf/config_for_non_composite_metrics.yml"); - Map config = contextConfiguration.getConfigYml(); - List mBeans = (List) config.get("mbeans"); - Set objectInstances = Sets.newHashSet(); - objectInstances.add(new ObjectInstance( - "org.apache.kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", "test")); - List attributes = Lists.newArrayList(); - attributes.add(new Attribute("Count", 100)); - attributes.add(new Attribute("Mean Rate", 200 )); - List metricNames = Lists.newArrayList(); - metricNames.add("Count"); - doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector), - Mockito.any(ObjectName.class) ); - doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector), - Mockito.any(ObjectInstance.class)); - doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), - Mockito.any(String[].class)); - DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( - contextConfiguration, jmxConnectionAdapter, - jmxConnector, "server1", metricWriteHelper); - - for (Map mBean : mBeans) { - - domainMetricsProcessor.populateMetricsForMBean(mBean); - verify(metricWriteHelper) - .transformAndPrintMetrics(pathCaptor.capture()); - Metric firstResultMetric = (Metric)pathCaptor.getValue().get(0); - Metric secondResultMetric = (Metric)pathCaptor.getValue().get(1); - Assert.assertEquals(firstResultMetric.getMetricName(),"Count"); - Assert.assertEquals(firstResultMetric.getMetricValue(), "100"); - Assert.assertEquals(firstResultMetric.getAggregationType(), "AVERAGE"); - Assert.assertEquals(firstResultMetric.getClusterRollUpType(), "INDIVIDUAL"); - Assert.assertEquals(firstResultMetric.getTimeRollUpType(), "AVERAGE"); - Assert.assertEquals(secondResultMetric.getMetricName(), "Mean Rate"); - Assert.assertEquals(secondResultMetric.getMetricValue(), "200"); - Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); - Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); - Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "AVERAGE"); - } - } - - @Test - //todo: remove lots of common code - public void whenCompositeObjectsThenReturnMetrics() throws MalformedObjectNameException, ReflectionException, - InstanceNotFoundException,IntrospectionException,IOException,OpenDataException { - - JMXConnector jmxConnector = mock(JMXConnector.class); - JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); - MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); - - ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); - MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration - ("Kafka Monitor", - "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), - Mockito.mock(AMonitorJob.class)); - - - contextConfiguration.setConfigYml("src/test/resources/conf/config_for_composite_metrics.yml"); - Map config = contextConfiguration.getConfigYml(); - List> mBeans = (List>) config.get("mbeans"); - Set objectInstances = Sets.newHashSet(); - objectInstances.add(new ObjectInstance("java.lang:type=Memory", "test")); - List attributes = Lists.newArrayList(); - attributes.add(new Attribute("HeapMemoryUsage", createCompositeDataSupportObject())); - attributes.add(new Attribute("Count", 100)); - attributes.add(new Attribute("Mean Rate", 200 )); - List metricNames = Lists.newArrayList(); - doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector),Mockito.any(ObjectName.class) ); - doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector), Mockito.any(ObjectInstance.class)); - doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), Mockito.any(String[] - .class)); - DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( - contextConfiguration, jmxConnectionAdapter, - jmxConnector, "server2", metricWriteHelper); - for (Map mBean : mBeans) { - - domainMetricsProcessor.populateMetricsForMBean(mBean); - verify(metricWriteHelper) - .transformAndPrintMetrics(pathCaptor.capture()); - Metric firstResultMetric = (Metric)pathCaptor.getValue().get(0); - Metric secondResultMetric = (Metric)pathCaptor.getValue().get(1); - Assert.assertEquals(firstResultMetric.getMetricName(),"HeapMemoryUsage.min"); - Assert.assertEquals(firstResultMetric.getMetricValue(), "50"); - Assert.assertEquals(firstResultMetric.getAggregationType(), "AVERAGE"); - Assert.assertEquals(firstResultMetric.getClusterRollUpType(), "INDIVIDUAL"); - Assert.assertEquals(firstResultMetric.getTimeRollUpType(), "AVERAGE"); - Assert.assertEquals(secondResultMetric.getMetricName(),"HeapMemoryUsage.max"); - Assert.assertEquals(secondResultMetric.getMetricValue(), "100"); - Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); - Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); - Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "AVERAGE"); - } - } - - @Test - public void whenCompositeAndNonCompositeObjectsThenReturnMetrics() throws IOException, - IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException, - OpenDataException{ - - JMXConnector jmxConnector = mock(JMXConnector.class); - JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); - MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); - - ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); - MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration - ("Kafka Monitor", - "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), - Mockito.mock(AMonitorJob.class)); - - contextConfiguration.setConfigYml("src/test/resources/conf/config_composite_and_non_composite_metrics.yml"); - Map config = contextConfiguration.getConfigYml(); - List> mBeans = (List>) config.get("mbeans"); - Set objectInstances = Sets.newHashSet(); - objectInstances.add(new ObjectInstance( - "org.apache.kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", "test")); - List attributes = Lists.newArrayList(); - attributes.add(new Attribute(("Count"), 0)); - attributes.add(new Attribute("HeapMemoryUsage", createCompositeDataSupportObject())); - List metricNames = Lists.newArrayList(); - metricNames.add("metric1"); - metricNames.add("metric2"); - doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector) - ,Mockito.any(ObjectName.class) ); - doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector) - , Mockito.any(ObjectInstance.class)); - doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), - Mockito.any(ObjectName.class), Mockito.any(String[] - .class)); - DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( - contextConfiguration, jmxConnectionAdapter, - jmxConnector,"server1", metricWriteHelper); - for (Map mBean : mBeans) { - - domainMetricsProcessor.populateMetricsForMBean(mBean); - verify(metricWriteHelper) - .transformAndPrintMetrics(pathCaptor.capture()); - Metric firstResultMetric = (Metric) pathCaptor.getValue().get(0); - Metric secondResultMetric = (Metric) pathCaptor.getValue().get(1); - Assert.assertEquals(firstResultMetric.getMetricName(), "Count"); - Assert.assertEquals(firstResultMetric.getMetricValue(), "0"); - Assert.assertEquals(firstResultMetric.getAggregationType(), "AVERAGE"); - Assert.assertEquals(firstResultMetric.getClusterRollUpType(), "INDIVIDUAL"); - Assert.assertEquals(firstResultMetric.getTimeRollUpType(), "AVERAGE"); - Assert.assertEquals(secondResultMetric.getMetricName(), "HeapMemoryUsage.min"); - Assert.assertEquals(secondResultMetric.getMetricValue(), "50"); - Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); - Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); - Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "SUM"); - } - } - - private CompositeDataSupport createCompositeDataSupportObject () throws OpenDataException { - String typeName = "type"; - String description = "description"; - String[] itemNames = {"min", "max"}; - String[] itemDescriptions = {"maxDesc", "minDesc"}; - OpenType[] itemTypes = new OpenType[]{new OpenType("java.lang.String", "type", - "description") { - @Override - public boolean isValue (Object obj) { - return true; - } - @Override - public boolean equals (Object obj) { - return false; - } - @Override - public int hashCode () { - return 0; - } - @Override - public String toString () { - return "50"; - } - }, new OpenType("java.lang.String", "type", "description") { - @Override - public boolean isValue (Object obj) { - return true; - } - @Override - public boolean equals (Object obj) { - return false; - } - @Override - public int hashCode () { - return 0; - } - @Override - public String toString () { - return "100"; - } - }}; - CompositeType compositeType = new CompositeType(typeName, description, itemNames, - itemDescriptions, itemTypes); - String[] itemNamesForCompositeDataSupport = {"min", "max"}; - Object[] itemValuesForCompositeDataSupport = {new BigDecimal(50), new BigDecimal(100)}; - return new CompositeDataSupport(compositeType, itemNamesForCompositeDataSupport, - itemValuesForCompositeDataSupport); - } +// @Test +// public void whenNonCompositeObjectsThenReturnMetrics() throws IOException, +// IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException { +// +// JMXConnector jmxConnector = mock(JMXConnector.class); +// JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); +// MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); +// +// ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); +// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration +// ("Kafka Monitor", +// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), +// Mockito.mock(AMonitorJob.class)); +// +// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_non_composite_metrics.yml"); +// Map config = contextConfiguration.getConfigYml(); +// List mBeans = (List) config.get("mbeans"); +// Set objectInstances = Sets.newHashSet(); +// objectInstances.add(new ObjectInstance( +// "org.apache.kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", "test")); +// List attributes = Lists.newArrayList(); +// attributes.add(new Attribute("Count", 100)); +// attributes.add(new Attribute("Mean Rate", 200 )); +// List metricNames = Lists.newArrayList(); +// metricNames.add("Count"); +// doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector), +// Mockito.any(ObjectName.class) ); +// doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector), +// Mockito.any(ObjectInstance.class)); +// doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), +// Mockito.any(String[].class)); +// DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( +// contextConfiguration, jmxConnectionAdapter, +// jmxConnector, "server1", metricWriteHelper); +// +// for (Map mBean : mBeans) { +// +// domainMetricsProcessor.populateMetricsForMBean(mBean); +// verify(metricWriteHelper) +// .transformAndPrintMetrics(pathCaptor.capture()); +// Metric firstResultMetric = (Metric)pathCaptor.getValue().get(0); +// Metric secondResultMetric = (Metric)pathCaptor.getValue().get(1); +// Assert.assertEquals(firstResultMetric.getMetricName(),"Count"); +// Assert.assertEquals(firstResultMetric.getMetricValue(), "100"); +// Assert.assertEquals(firstResultMetric.getAggregationType(), "AVERAGE"); +// Assert.assertEquals(firstResultMetric.getClusterRollUpType(), "INDIVIDUAL"); +// Assert.assertEquals(firstResultMetric.getTimeRollUpType(), "AVERAGE"); +// Assert.assertEquals(secondResultMetric.getMetricName(), "Mean Rate"); +// Assert.assertEquals(secondResultMetric.getMetricValue(), "200"); +// Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); +// Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); +// Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "AVERAGE"); +// } +// } +// +// @Test +// public void whenCompositeObjectsThenReturnMetrics() throws MalformedObjectNameException, ReflectionException, +// InstanceNotFoundException,IntrospectionException,IOException,OpenDataException { +// +// JMXConnector jmxConnector = mock(JMXConnector.class); +// JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); +// MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); +// +// ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); +// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration +// ("Kafka Monitor", +// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), +// Mockito.mock(AMonitorJob.class)); +// +// +// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_composite_metrics.yml"); +// Map config = contextConfiguration.getConfigYml(); +// List> mBeans = (List>) config.get("mbeans"); +// Set objectInstances = Sets.newHashSet(); +// objectInstances.add(new ObjectInstance("java.lang:type=Memory", "test")); +// List attributes = Lists.newArrayList(); +// attributes.add(new Attribute("HeapMemoryUsage", createCompositeDataSupportObject())); +// attributes.add(new Attribute("Count", 100)); +// attributes.add(new Attribute("Mean Rate", 200 )); +// List metricNames = Lists.newArrayList(); +// doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector),Mockito.any(ObjectName.class) ); +// doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector), Mockito.any(ObjectInstance.class)); +// doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), Mockito.any(String[] +// .class)); +// DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( +// contextConfiguration, jmxConnectionAdapter, +// jmxConnector, "server2", metricWriteHelper); +// for (Map mBean : mBeans) { +// +// domainMetricsProcessor.populateMetricsForMBean(mBean); +// verify(metricWriteHelper) +// .transformAndPrintMetrics(pathCaptor.capture()); +// Metric firstResultMetric = (Metric)pathCaptor.getValue().get(0); +// Metric secondResultMetric = (Metric)pathCaptor.getValue().get(1); +// Assert.assertEquals(firstResultMetric.getMetricName(),"HeapMemoryUsage.min"); +// Assert.assertEquals(firstResultMetric.getMetricValue(), "50"); +// Assert.assertEquals(firstResultMetric.getAggregationType(), "AVERAGE"); +// Assert.assertEquals(firstResultMetric.getClusterRollUpType(), "INDIVIDUAL"); +// Assert.assertEquals(firstResultMetric.getTimeRollUpType(), "AVERAGE"); +// Assert.assertEquals(secondResultMetric.getMetricName(),"HeapMemoryUsage.max"); +// Assert.assertEquals(secondResultMetric.getMetricValue(), "100"); +// Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); +// Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); +// Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "AVERAGE"); +// } +// } +// +// @Test +// public void whenCompositeAndNonCompositeObjectsThenReturnMetrics() throws IOException, +// IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException, +// OpenDataException{ +// +// JMXConnector jmxConnector = mock(JMXConnector.class); +// JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); +// MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); +// +// ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); +// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration +// ("Kafka Monitor", +// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), +// Mockito.mock(AMonitorJob.class)); +// +// contextConfiguration.setConfigYml("src/test/resources/conf/config_composite_and_non_composite_metrics.yml"); +// Map config = contextConfiguration.getConfigYml(); +// List> mBeans = (List>) config.get("mbeans"); +// Set objectInstances = Sets.newHashSet(); +// objectInstances.add(new ObjectInstance( +// "org.apache.kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", "test")); +// List attributes = Lists.newArrayList(); +// attributes.add(new Attribute(("Count"), 0)); +// attributes.add(new Attribute("HeapMemoryUsage", createCompositeDataSupportObject())); +// List metricNames = Lists.newArrayList(); +// metricNames.add("metric1"); +// metricNames.add("metric2"); +// doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector) +// ,Mockito.any(ObjectName.class) ); +// doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector) +// , Mockito.any(ObjectInstance.class)); +// doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), +// Mockito.any(ObjectName.class), Mockito.any(String[] +// .class)); +// DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( +// contextConfiguration, jmxConnectionAdapter, +// jmxConnector,"server1", metricWriteHelper); +// for (Map mBean : mBeans) { +// +// domainMetricsProcessor.populateMetricsForMBean(mBean); +// verify(metricWriteHelper) +// .transformAndPrintMetrics(pathCaptor.capture()); +// Metric firstResultMetric = (Metric) pathCaptor.getValue().get(0); +// Metric secondResultMetric = (Metric) pathCaptor.getValue().get(1); +// Assert.assertEquals(firstResultMetric.getMetricName(), "Count"); +// Assert.assertEquals(firstResultMetric.getMetricValue(), "0"); +// Assert.assertEquals(firstResultMetric.getAggregationType(), "AVERAGE"); +// Assert.assertEquals(firstResultMetric.getClusterRollUpType(), "INDIVIDUAL"); +// Assert.assertEquals(firstResultMetric.getTimeRollUpType(), "AVERAGE"); +// Assert.assertEquals(secondResultMetric.getMetricName(), "HeapMemoryUsage.min"); +// Assert.assertEquals(secondResultMetric.getMetricValue(), "50"); +// Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); +// Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); +// Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "SUM"); +// } +// } +// +// private CompositeDataSupport createCompositeDataSupportObject () throws OpenDataException { +// String typeName = "type"; +// String description = "description"; +// String[] itemNames = {"min", "max"}; +// String[] itemDescriptions = {"maxDesc", "minDesc"}; +// OpenType[] itemTypes = new OpenType[]{new OpenType("java.lang.String", "type", +// "description") { +// @Override +// public boolean isValue (Object obj) { +// return true; +// } +// @Override +// public boolean equals (Object obj) { +// return false; +// } +// @Override +// public int hashCode () { +// return 0; +// } +// @Override +// public String toString () { +// return "50"; +// } +// }, new OpenType("java.lang.String", "type", "description") { +// @Override +// public boolean isValue (Object obj) { +// return true; +// } +// @Override +// public boolean equals (Object obj) { +// return false; +// } +// @Override +// public int hashCode () { +// return 0; +// } +// @Override +// public String toString () { +// return "100"; +// } +// }}; +// CompositeType compositeType = new CompositeType(typeName, description, itemNames, +// itemDescriptions, itemTypes); +// String[] itemNamesForCompositeDataSupport = {"min", "max"}; +// Object[] itemValuesForCompositeDataSupport = {new BigDecimal(50), new BigDecimal(100)}; +// return new CompositeDataSupport(compositeType, itemNamesForCompositeDataSupport, +// itemValuesForCompositeDataSupport); +// } } diff --git a/src/test/resources/conf/config_for_SSL.yml b/src/test/resources/conf/config_for_SSL.yml index 4867e24..1c97743 100644 --- a/src/test/resources/conf/config_for_SSL.yml +++ b/src/test/resources/conf/config_for_SSL.yml @@ -22,7 +22,3 @@ connection: sslTrustStorePassword: "test1234" sslTrustStoreEncryptedPassword: "" - #key store details for mutual auth on ssl - sslKeyStorePath: "" - sslKeyStorePassword: "" - sslKeyStoreEncryptedPassword: "" \ No newline at end of file diff --git a/src/test/resources/conf/config_for_SSL_server.yml b/src/test/resources/conf/config_for_SSL_server.yml new file mode 100644 index 0000000..4029564 --- /dev/null +++ b/src/test/resources/conf/config_for_SSL_server.yml @@ -0,0 +1,24 @@ +metricPrefix: "Server|Component:19|Custom Metrics|Kafka" + +servers: + - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" + host: "" + port: "" + username: "" + password: "" + encryptedPassword: "" + displayName: "Local Kafka Server" + useSsl: true + + #Provide the encryption key for the password +encryptionKey: "" + +# Configure this section only if useSSL is set to true. +connection: + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocol: "TLSv1.2" + sslKeyStorePath: "/Users/vishaka.sekar/AppDynamics/server/kafka.server.truststore.jks" + sslKeyStorePassword: "test1234" + sslTrustStoreEncryptedPassword: "" + From bc8687366f135e092d5ef755b9960591f238ba7f Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Fri, 10 Aug 2018 13:43:35 -0700 Subject: [PATCH 30/49] sslserversocket --- .../utils/CustomSSLServerSocketFactory.java | 80 +++++++++++++++++++ .../config_for_SSL_with_default_jre_keys.yml | 24 ++++++ .../config_for_SSL_with_incorrect_keys.yml | 24 ++++++ 3 files changed, 128 insertions(+) create mode 100644 src/main/java/com/appdynamics/extensions/kafka/utils/CustomSSLServerSocketFactory.java create mode 100644 src/test/resources/conf/config_for_SSL_with_default_jre_keys.yml create mode 100644 src/test/resources/conf/config_for_SSL_with_incorrect_keys.yml diff --git a/src/main/java/com/appdynamics/extensions/kafka/utils/CustomSSLServerSocketFactory.java b/src/main/java/com/appdynamics/extensions/kafka/utils/CustomSSLServerSocketFactory.java new file mode 100644 index 0000000..d1dea9d --- /dev/null +++ b/src/main/java/com/appdynamics/extensions/kafka/utils/CustomSSLServerSocketFactory.java @@ -0,0 +1,80 @@ +package com.appdynamics.extensions.kafka.utils; + +import com.appdynamics.extensions.crypto.Decryptor; +import com.appdynamics.extensions.kafka.CustomSSLSocketFactory; +import com.google.common.base.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; +import javax.rmi.ssl.SslRMIServerSocketFactory; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.Map; + +public class CustomSSLServerSocketFactory { + + private static final Logger logger = LoggerFactory.getLogger(CustomSSLSocketFactory.class); + private Map connectionMap; + + public CustomSSLServerSocketFactory(Map connectionMap) { + this.connectionMap = connectionMap; + } + + public SslRMIServerSocketFactory getServerSocketFactory() throws IOException { + return new SslRMIServerSocketFactory(createSocketFactory(connectionMap),null,null,false); + } + + public SSLContext createSocketFactory(Map connectionMap) throws IOException { + String trustStorePath = connectionMap.get("sslTrustStorePath").toString(); + char[] trustStorePassword = getTrustStorePassword(connectionMap.get("encryptionKey").toString(), connectionMap); + try { + //initializing truststore + KeyStore trustStore = KeyStore.getInstance("JKS"); + trustStore.load(new FileInputStream(trustStorePath), trustStorePassword); + TrustManagerFactory trustManagerFactory = TrustManagerFactory + .getInstance("SunX509"); + trustManagerFactory.init(trustStore); + + //create ssl Context + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + //initialize with the trust managers + sslContext.init(null, trustManagerFactory.getTrustManagers(), null); + + return sslContext; + + } catch (NoSuchAlgorithmException exception) { + logger.error("No Such algorithm", exception); + } catch (CertificateException e) { + logger.error("CommonName in the certificate is not the same as the host name", e); + } catch (KeyStoreException e) { + logger.error("Cannot initialize trustStore", e); + } catch (KeyManagementException e) { + logger.error("Cannot initialize keyMangers", e); + } + return null; + } + private static char[] getTrustStorePassword(String encryptionKey, Map connection) { + String sslTrustStorePassword = (String) connection.get("sslTrustStorePassword"); + if (!Strings.isNullOrEmpty(sslTrustStorePassword)) { + return sslTrustStorePassword.toCharArray(); + } else { + String sslTrustStoreEncryptedPassword = (String) connection.get("sslTrustStoreEncryptedPassword"); + if (!Strings.isNullOrEmpty(sslTrustStoreEncryptedPassword) && !Strings.isNullOrEmpty(encryptionKey)) { + return new Decryptor(encryptionKey).decrypt(sslTrustStoreEncryptedPassword).toCharArray(); + } else { + logger.warn("Returning null password for sslTrustStore. Please set the [connection.sslTrustStore] or " + + "[connection.sslTrustStoreEncryptedPassword + encryptionKey]"); + return null; + } + } + } + +} diff --git a/src/test/resources/conf/config_for_SSL_with_default_jre_keys.yml b/src/test/resources/conf/config_for_SSL_with_default_jre_keys.yml new file mode 100644 index 0000000..d1ba9d9 --- /dev/null +++ b/src/test/resources/conf/config_for_SSL_with_default_jre_keys.yml @@ -0,0 +1,24 @@ +metricPrefix: "Server|Component:19|Custom Metrics|Kafka" + +servers: + - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" + host: "" + port: "" + username: "" + password: "" + encryptedPassword: "" + displayName: "Local Kafka Server" + useSsl: true + + #Provide the encryption key for the password +encryptionKey: "" + +## Configure this section only if useSSL is set to true. +#connection: +# socketTimeout: 3000 +# connectTimeout: 1000 +# sslProtocol: "TLSv1.2" +# sslTrustStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.keystore.jks" +# sslTrustStorePassword: "test1234" +# sslTrustStoreEncryptedPassword: "" + diff --git a/src/test/resources/conf/config_for_SSL_with_incorrect_keys.yml b/src/test/resources/conf/config_for_SSL_with_incorrect_keys.yml new file mode 100644 index 0000000..4ba004b --- /dev/null +++ b/src/test/resources/conf/config_for_SSL_with_incorrect_keys.yml @@ -0,0 +1,24 @@ +metricPrefix: "Server|Component:19|Custom Metrics|Kafka" + +servers: + - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" + host: "" + port: "" + username: "" + password: "" + encryptedPassword: "" + displayName: "Local Kafka Server" + useSsl: true + + #Provide the encryption key for the password +encryptionKey: "" + +# Configure this section only if useSSL is set to true. +connection: + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocol: "TLSv1.2" + sslTrustStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.keystore.jks" + sslTrustStorePassword: "test1234" + sslTrustStoreEncryptedPassword: "" + From ee8c8a76eb74e4cdb7e5f71113eccd4befd89d00 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Thu, 16 Aug 2018 17:06:08 -0700 Subject: [PATCH 31/49] ssl default certs path --- .../kafka/CustomSSLSocketFactory.java | 158 ------ .../kafka/JMXConnectionAdapter.java | 46 +- .../extensions/kafka/KafkaMonitor.java | 26 +- .../extensions/kafka/KafkaMonitorTask.java | 32 +- .../kafka/metrics/DomainMetricsProcessor.java | 6 +- .../utils/CustomSSLServerSocketFactory.java | 80 --- src/main/resources/conf/config.yml | 461 +++++++++--------- .../metrics/CustomSSLSocketFactoryTest.java | 169 ------- .../metrics/DomainMetricsProcessorTest.java | 426 ++++++++-------- .../kafka/metrics/SSLConnectionTest.java | 109 +++++ src/test/resources/conf/config_for_SSL.yml | 24 - .../resources/conf/config_for_SSL_server.yml | 24 - .../config_for_SSL_with_default_jre_keys.yml | 24 - .../config_for_SSL_with_incorrect_keys.yml | 24 - 14 files changed, 582 insertions(+), 1027 deletions(-) delete mode 100644 src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java delete mode 100644 src/main/java/com/appdynamics/extensions/kafka/utils/CustomSSLServerSocketFactory.java delete mode 100644 src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java create mode 100644 src/test/java/com/appdynamics/extensions/kafka/metrics/SSLConnectionTest.java delete mode 100644 src/test/resources/conf/config_for_SSL.yml delete mode 100644 src/test/resources/conf/config_for_SSL_server.yml delete mode 100644 src/test/resources/conf/config_for_SSL_with_default_jre_keys.yml delete mode 100644 src/test/resources/conf/config_for_SSL_with_incorrect_keys.yml diff --git a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java b/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java deleted file mode 100644 index 1341114..0000000 --- a/src/main/java/com/appdynamics/extensions/kafka/CustomSSLSocketFactory.java +++ /dev/null @@ -1,158 +0,0 @@ -/** - * Copyright 2018 AppDynamics, Inc. - * - * 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. - */ - -package com.appdynamics.extensions.kafka; - -import com.appdynamics.extensions.crypto.Decryptor; -import com.google.common.base.Strings; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; -import org.apache.http.conn.ssl.SSLContexts; -import org.apache.http.conn.ssl.TrustSelfSignedStrategy; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.crypto.Cipher; -import javax.management.remote.JMXServiceURL; -import javax.net.SocketFactory; -import javax.net.ssl.*; -import javax.rmi.ssl.SslRMIClientSocketFactory; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.net.Socket; -import java.security.*; -import java.security.cert.CertificateException; -import java.util.Map; -import java.util.StringTokenizer; - -public class CustomSSLSocketFactory extends SslRMIClientSocketFactory { - - private static final Logger logger = LoggerFactory.getLogger(CustomSSLSocketFactory.class); - private Map connectionMap; - - public CustomSSLSocketFactory() { - } - - public CustomSSLSocketFactory(Map connectionMap) { - this.connectionMap = connectionMap; - } - - public SSLSocketFactory createSocketFactory(Map connectionMap) throws IOException { - String trustStorePath = connectionMap.get("sslTrustStorePath").toString(); - char[] trustStorePassword = getTrustStorePassword(connectionMap.get("encryptionKey").toString(), connectionMap); - try { - //initializing truststore - KeyStore trustStore = KeyStore.getInstance("JKS"); - trustStore.load(new FileInputStream(trustStorePath), trustStorePassword); - TrustManagerFactory trustManagerFactory = TrustManagerFactory - .getInstance("SunX509"); - trustManagerFactory.init(trustStore); - - //create ssl Context - SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); - //initialize with the trust managers - sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - - return sslContext.getSocketFactory(); - - } catch (NoSuchAlgorithmException exception) { - logger.error("No Such algorithm", exception); - } catch (CertificateException e) { - logger.error("CommonName in the certificate is not the same as the host name", e); - } catch (KeyStoreException e) { - logger.error("Cannot initialize trustStore", e); - } catch (KeyManagementException e) { - logger.error("Cannot initialize keyMangers", e); - } - return null; - } - - @Override - public Socket createSocket(String host, int port) throws IOException { - // Retrieve the SSLSocketFactory - final SocketFactory sslSocketFactory = createSocketFactory(connectionMap); - // Create the SSLSocket - final SSLSocket sslSocket = (SSLSocket) - sslSocketFactory.createSocket("127.0.0.1", - 9999); - // Set the SSLSocket Enabled Cipher Suites - final String enabledCipherSuites = - connectionMap.get("sslCipherSuites").toString(); - if (enabledCipherSuites != null) { - StringTokenizer st = new StringTokenizer(enabledCipherSuites, ","); - int tokens = st.countTokens(); - String enabledCipherSuitesList[] = new String[tokens]; - for (int i = 0 ; i < tokens; i++) { - enabledCipherSuitesList[i] = st.nextToken(); - } - try { - sslSocket.setEnabledCipherSuites(enabledCipherSuitesList); - } catch (IllegalArgumentException e) { - throw (IOException) - new IOException(e.getMessage()).initCause(e); - } - } - // Set the SSLSocket Enabled Protocols - - final String enabledProtocols = - connectionMap.get("sslProtocols").toString(); - if (enabledProtocols != null) { - StringTokenizer st = new StringTokenizer(enabledProtocols, ","); - int tokens = st.countTokens(); - String enabledProtocolsList[] = new String[tokens]; - for (int i = 0 ; i < tokens; i++) { - enabledProtocolsList[i] = st.nextToken(); - } - try { - sslSocket.setEnabledProtocols(enabledProtocolsList); - } catch (IllegalArgumentException e) { - throw (IOException) - new IOException(e.getMessage()).initCause(e); - } - } - return sslSocket; - } - - public int hashCode() { - return getClass().hashCode(); - } - - public boolean equals(Object obj) { - if (obj == this) { - return true; - } else if (obj == null || getClass() != obj.getClass()) { - return false; - } - return true; - } - - private static char[] getTrustStorePassword(String encryptionKey, Map connection) { - String sslTrustStorePassword = (String) connection.get("sslTrustStorePassword"); - if (!Strings.isNullOrEmpty(sslTrustStorePassword)) { - return sslTrustStorePassword.toCharArray(); - } else { - String sslTrustStoreEncryptedPassword = (String) connection.get("sslTrustStoreEncryptedPassword"); - if (!Strings.isNullOrEmpty(sslTrustStoreEncryptedPassword) && !Strings.isNullOrEmpty(encryptionKey)) { - return new Decryptor(encryptionKey).decrypt(sslTrustStoreEncryptedPassword).toCharArray(); - } else { - logger.warn("Returning null password for sslTrustStore. Please set the [connection.sslTrustStore] or " + - "[connection.sslTrustStoreEncryptedPassword + encryptionKey]"); - return null; - } - } - } - -} \ No newline at end of file diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index 5770f2c..db8f15e 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -17,48 +17,19 @@ import com.appdynamics.extensions.kafka.utils.Constants; -import com.appdynamics.extensions.kafka.utils.CustomSSLServerSocketFactory; import com.google.common.base.Strings; import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sun.management.jmxremote.ConnectorBootstrap; -import javax.management.Attribute; -import javax.management.AttributeList; -import javax.management.InstanceNotFoundException; -import javax.management.IntrospectionException; -import javax.management.MBeanAttributeInfo; -import javax.management.MBeanServerConnection; -import javax.management.ObjectInstance; -import javax.management.ObjectName; -import javax.management.ReflectionException; +import javax.management.*; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; -import javax.management.remote.rmi.RMIConnector; import javax.management.remote.rmi.RMIConnectorServer; -import javax.naming.Context; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; import javax.rmi.ssl.SslRMIClientSocketFactory; -import javax.rmi.ssl.SslRMIServerSocketFactory; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import java.net.InetAddress; import java.net.MalformedURLException; -import java.rmi.NotBoundException; -import java.rmi.registry.LocateRegistry; -import java.rmi.registry.Registry; -import java.rmi.server.RMISocketFactory; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -86,23 +57,12 @@ static JMXConnectionAdapter create( Map requestMap) throws Malfo } JMXConnector open(Map connectionMap) throws IOException { - JMXConnector jmxConnector; final Map env = new HashMap<>(); if(Boolean.valueOf(connectionMap.get("useSsl").toString())) { - env.put(Context.SECURITY_PROTOCOL, "ssl"); - if (!connectionMap.containsKey(Constants.TRUST_STORE_PATH)){ //using default jre truststore - SslRMIClientSocketFactory sslRMIClientSocketFactory = new SslRMIClientSocketFactory(); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, sslRMIClientSocketFactory); - env.put("com.sun.jndi.rmi.factory.socket", sslRMIClientSocketFactory); - } else{ - - CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(connectionMap); - env.put("com.sun.jndi.rmi.factory.socket", customSSLSocketFactory);//// secure RMI connector as well - env.put("jmx.remote.rmi.client.socket.factory", customSSLSocketFactory); - env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new CustomSSLServerSocketFactory(connectionMap).getServerSocketFactory()); - } + SslRMIClientSocketFactory sslRMIClientSocketFactory = new SslRMIClientSocketFactory(); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, sslRMIClientSocketFactory); } if (!Strings.isNullOrEmpty(this.username)) {env.put(JMXConnector.CREDENTIALS, new String[]{username, password});} diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index 7dff39b..f357699 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -12,44 +12,34 @@ import com.appdynamics.extensions.ABaseMonitor; import com.appdynamics.extensions.TasksExecutionServiceProvider; import com.appdynamics.extensions.kafka.utils.Constants; +import com.appdynamics.extensions.kafka.utils.SslUtils; import com.appdynamics.extensions.util.AssertUtils; -import com.appdynamics.extensions.util.YmlUtils; -import com.google.common.base.Strings; -import com.google.common.primitives.Booleans; import com.singularity.ee.agent.systemagent.api.exception.TaskExecutionException; import org.apache.log4j.ConsoleAppender; import org.apache.log4j.Level; import org.apache.log4j.PatternLayout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; import java.io.OutputStreamWriter; -import java.rmi.server.UnicastRemoteObject; import java.util.HashMap; import java.util.List; import java.util.Map; + import static com.appdynamics.extensions.kafka.utils.Constants.DEFAULT_METRIC_PREFIX; public class KafkaMonitor extends ABaseMonitor { + private static final Logger logger = LoggerFactory.getLogger(KafkaMonitor.class); @Override protected void onConfigReload(File file) { - //todo: needs restart for system props to reflect - Map configMap = (Map) this.getContextConfiguration() + Map configMap = this.getContextConfiguration() .getConfigYml(); - //if the config yaml contains the field sslTrustStorePath then the keys are set - // if the field is not present, default jre truststore is used - //if left blank, defaults to /conf/cacerts - if(configMap.containsKey("connection")) { -// Map connectionMap = (Map) configMap.get("connection"); -// if (connectionMap.containsKey(Constants.TRUST_STORE_PATH) && -// !Strings.isNullOrEmpty(connectionMap.get(Constants.TRUST_STORE_PATH).toString())) { -// System.setProperty("javax.net.ssl.trustStore", connectionMap.get(Constants.TRUST_STORE_PATH).toString()); -// System.setProperty("javax.net.ssl.trustStorePassword", connectionMap.get(Constants.TRUST_STORE_PASSWORD).toString()); -// } - } + SslUtils sslUtils = new SslUtils(); + sslUtils.setSslProperties(configMap); } - protected String getDefaultMetricPrefix() { return DEFAULT_METRIC_PREFIX; } public String getMonitorName() { diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index ebaa445..e12d0c6 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -24,11 +24,10 @@ import com.appdynamics.extensions.crypto.CryptoUtil; import com.appdynamics.extensions.kafka.metrics.DomainMetricsProcessor; import com.appdynamics.extensions.kafka.utils.Constants; -import com.appdynamics.extensions.metrics.Metric; -import com.appdynamics.extensions.util.YmlUtils; import com.google.common.base.Strings; import com.google.common.collect.Maps; import org.slf4j.LoggerFactory; + import javax.management.remote.JMXConnector; import java.io.IOException; import java.math.BigDecimal; @@ -43,6 +42,7 @@ public class KafkaMonitorTask implements AMonitorTaskRunnable { private MetricWriteHelper metricWriteHelper; private String displayName; private JMXConnectionAdapter jmxAdapter; + private JMXConnector jmxConnector; KafkaMonitorTask(TasksExecutionServiceProvider serviceProvider, MonitorContextConfiguration configuration, Map kafkaServer) { @@ -68,17 +68,14 @@ public void run() { } public void populateAndPrintMetrics() { - JMXConnector jmxConnector = null; try{ - BigDecimal connectionStatus = BigDecimal.ONE; - jmxConnector = openJMXConnection(); + BigDecimal connectionStatus = openJMXConnection(); List> mbeansFromConfig = (List>) configuration.getConfigYml() .get(Constants.MBEANS); - DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor(configuration, jmxAdapter, jmxConnector,displayName, metricWriteHelper); for (Map mbeanFromConfig : mbeansFromConfig) { - domainMetricsProcessor.populateMetricsForMBean(jmxConnector, mbeanFromConfig); + domainMetricsProcessor.populateMetricsForMBean(mbeanFromConfig); } metricWriteHelper.printMetric(this.configuration.getMetricPrefix() + Constants.METRIC_SEPARATOR + this.displayName + Constants.METRIC_SEPARATOR+ "kafka.server" + @@ -97,24 +94,27 @@ public void populateAndPrintMetrics() { } } - private JMXConnector openJMXConnection() { + private BigDecimal openJMXConnection() { try { Map requestMap = buildRequestMap(); jmxAdapter = JMXConnectionAdapter.create(requestMap); Map connectionMap =(Map) getConnectionParameters(); connectionMap.put("useSsl", this.kafkaServer.get("useSsl") ); + logger.debug("[useSsl] is set [{}] for server [{}]", connectionMap.get("useSsl"), + this.kafkaServer.get(Constants.DISPLAY_NAME)); if(configuration.getConfigYml().containsKey("encryptionKey") && !Strings.isNullOrEmpty( configuration.getConfigYml().get("encryptionKey").toString()) ) { connectionMap.put("encryptionKey",configuration.getConfigYml().get("encryptionKey").toString()); } - else { connectionMap.put("encryptionKey" ,""); } + jmxConnector = jmxAdapter.open(connectionMap); - JMXConnector jmxConnection = jmxAdapter.open(connectionMap); - if(jmxConnection == null) {logger.error("Connection object is null");} - logger.debug("JMX Connection is open to Kafka server: {}", this.kafkaServer.get(Constants.DISPLAY_NAME)); - return jmxConnection; + if(jmxConnector != null) { + logger.debug("JMX Connection is open to Kafka server: {}", this.kafkaServer.get(Constants.DISPLAY_NAME)); + return BigDecimal.ONE; + } + return BigDecimal.ZERO; } catch (IOException ioe) { logger.error("Unable to open a JMX Connection Kafka server: {} {} " , this.kafkaServer.get(Constants.DISPLAY_NAME), ioe); @@ -138,9 +138,7 @@ private String getPassword() { Map configMap = configuration.getConfigYml(); if (!Strings.isNullOrEmpty(password)) { return password; } - if(configMap.containsKey(Constants.ENCRYPTION_KEY) && - configMap.containsKey(Constants.ENCRYPTED_PASSWORD)) { - + if(configMap.containsKey(Constants.ENCRYPTION_KEY)) { String encryptionKey = configMap.get(Constants.ENCRYPTION_KEY).toString(); String encryptedPassword = this.kafkaServer.get(Constants.ENCRYPTED_PASSWORD); @@ -162,5 +160,3 @@ private Map getConnectionParameters(){ } } - - diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index b91ab98..1b5d99a 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -42,8 +42,7 @@ public class DomainMetricsProcessor { private List nodeMetrics = new ArrayList<>(); public DomainMetricsProcessor(MonitorContextConfiguration configuration, JMXConnectionAdapter jmxAdapter, - JMXConnector jmxConnection, String displayName, MetricWriteHelper metricWriteHelper - ) { + JMXConnector jmxConnection, String displayName, MetricWriteHelper metricWriteHelper) { this.jmxAdapter = jmxAdapter; this.jmxConnection = jmxConnection; this.metricWriteHelper = metricWriteHelper; @@ -51,8 +50,7 @@ public DomainMetricsProcessor(MonitorContextConfiguration configuration, JMXConn this.displayName = displayName; } - public void populateMetricsForMBean(JMXConnector jmxConnection, Map mbeanFromConfig) { - + public void populateMetricsForMBean(Map mbeanFromConfig) { try { String objectName = (String) mbeanFromConfig.get(Constants.OBJECTNAME); List> metricsList = (List>) mbeanFromConfig.get(Constants.METRICS); diff --git a/src/main/java/com/appdynamics/extensions/kafka/utils/CustomSSLServerSocketFactory.java b/src/main/java/com/appdynamics/extensions/kafka/utils/CustomSSLServerSocketFactory.java deleted file mode 100644 index d1dea9d..0000000 --- a/src/main/java/com/appdynamics/extensions/kafka/utils/CustomSSLServerSocketFactory.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.appdynamics.extensions.kafka.utils; - -import com.appdynamics.extensions.crypto.Decryptor; -import com.appdynamics.extensions.kafka.CustomSSLSocketFactory; -import com.google.common.base.Strings; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManagerFactory; -import javax.rmi.ssl.SslRMIServerSocketFactory; -import java.io.FileInputStream; -import java.io.IOException; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.util.Map; - -public class CustomSSLServerSocketFactory { - - private static final Logger logger = LoggerFactory.getLogger(CustomSSLSocketFactory.class); - private Map connectionMap; - - public CustomSSLServerSocketFactory(Map connectionMap) { - this.connectionMap = connectionMap; - } - - public SslRMIServerSocketFactory getServerSocketFactory() throws IOException { - return new SslRMIServerSocketFactory(createSocketFactory(connectionMap),null,null,false); - } - - public SSLContext createSocketFactory(Map connectionMap) throws IOException { - String trustStorePath = connectionMap.get("sslTrustStorePath").toString(); - char[] trustStorePassword = getTrustStorePassword(connectionMap.get("encryptionKey").toString(), connectionMap); - try { - //initializing truststore - KeyStore trustStore = KeyStore.getInstance("JKS"); - trustStore.load(new FileInputStream(trustStorePath), trustStorePassword); - TrustManagerFactory trustManagerFactory = TrustManagerFactory - .getInstance("SunX509"); - trustManagerFactory.init(trustStore); - - //create ssl Context - SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); - //initialize with the trust managers - sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - - return sslContext; - - } catch (NoSuchAlgorithmException exception) { - logger.error("No Such algorithm", exception); - } catch (CertificateException e) { - logger.error("CommonName in the certificate is not the same as the host name", e); - } catch (KeyStoreException e) { - logger.error("Cannot initialize trustStore", e); - } catch (KeyManagementException e) { - logger.error("Cannot initialize keyMangers", e); - } - return null; - } - private static char[] getTrustStorePassword(String encryptionKey, Map connection) { - String sslTrustStorePassword = (String) connection.get("sslTrustStorePassword"); - if (!Strings.isNullOrEmpty(sslTrustStorePassword)) { - return sslTrustStorePassword.toCharArray(); - } else { - String sslTrustStoreEncryptedPassword = (String) connection.get("sslTrustStoreEncryptedPassword"); - if (!Strings.isNullOrEmpty(sslTrustStoreEncryptedPassword) && !Strings.isNullOrEmpty(encryptionKey)) { - return new Decryptor(encryptionKey).decrypt(sslTrustStoreEncryptedPassword).toCharArray(); - } else { - logger.warn("Returning null password for sslTrustStore. Please set the [connection.sslTrustStore] or " + - "[connection.sslTrustStoreEncryptedPassword + encryptionKey]"); - return null; - } - } - } - -} diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index 8bbeff4..e0fd040 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -9,9 +9,8 @@ metricPrefix: "Server|Component:|Custom Metrics|Kafka" # Add your Kafka Instances below - servers: - - serviceUrl: "service:jmx:rmi:///jndi/rmi://127.0.0.1:9999/jmxrmi" #provide service URL or the host/port pair + - serviceUrl: "service:jmx:rmi:///jndi/rmi://127.0.0.1:9999/jmxrmi" #provide service URL [OR] provide [host][port] pair host: "" port: "" username: "" @@ -32,17 +31,21 @@ encryptionKey: "" # Please REMOVE the connection section below if: # 1.You are not using SSL for any of the Kafka servers listed in the server section OR # 2.You are using SSL for any of the servers([useSsl : true]), but you want to use default truststore of the JRE +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -#connection: -# socketTimeout: 3000 -# connectTimeout: 1000 -# sslProtocols: "TLSv1.2" -# sslCipherSuites: "" -# sslTrustStorePath: "" #defaults to conf/cacerts.jks -# sslTrustStorePassword: "" # defaults to empty -# sslTrustStoreEncryptedPassword: "" +#If you are using the conenction section, +#any change to the connection section below requires a machine agent restart for the changes to reflect +connection: + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocols: "TLSv1.2" + sslCipherSuites: "" + sslTrustStorePath: "" #if [sslTrustStorePath: ""] empty, it defaults to MA home/conf/cacerts.jks + sslTrustStorePassword: "" # [sslTrustStorePassword: ""] defaults to "" + sslTrustStoreEncryptedPassword: "" #provide encrypted Password if encryption is needed # Each Kafka server needs 1 thread each, so please configure this according to the number of servers you are monitoring +# [numberOfThreads] = numberOfKafkaServers numberOfThreads: 10 # The configuration of different metrics from all mbeans exposed by Kafka server @@ -71,222 +74,222 @@ mbeans: timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" -# - objectName: "kafka.server:type=KafkaRequestHandlerPool,*" -# metrics: -# - Count: -# alias: "Count" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# -# - MeanRate: -# alias: "Mean Rate" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# - objectName: "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec" -# metrics: -# - Count: -# alias: "Count" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# -# - MeanRate: -# alias: "Mean Rate" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# - objectName: "kafka.server:type=ReplicaManager,name=IsrShrinksPerSec" -# metrics: -# - Count: -# alias: "Count" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# -# - MeanRate: -# alias: "Mean Rate" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# - objectName: "kafka.server:type=SessionExpireListener,*" -# metrics: -# - Count: -# alias: "Count" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# -# - MeanRate: -# alias: "Mean Rate" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# - objectName: "kafka.network:type=RequestMetrics,*" -# metrics: -# - Count: -# alias: "Count" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# -# - MeanRate: -# alias: "Mean Rate" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# - objectName: "kafka.controller:type=ControllerStats,*" -# metrics: -# - Count: -# alias: "Count" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# -# - MeanRate: -# alias: "Mean Rate" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# - objectName: "kafka.server:type=DelayedOperationPurgatory,*" -# metrics: -# - Value: -# alias: "Value" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# - objectName: "kafka.server:type=ReplicaFetcherManager,*" -# metrics: -# - Value: -# alias: "Value" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# - objectName: "kafka.server:type=ReplicaManager,name=LeaderCount" -# metrics: -# - Value: -# alias: "Value" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# - objectName: "kafka.server:type=ReplicaManager,name=PartitionCount" -# metrics: -# - Value: -# alias: "Value" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# - objectName: "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions" -# metrics: -# - Value: -# alias: "Value" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# - objectName: "kafka.network:type=Processor,*" -# metrics: -# - Value: -# alias: "Value" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# - objectName: "kafka.network:type=RequestChannel,*" -# metrics: -# - Value: -# alias: "Value" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# - objectName: "kafka.network:type=SocketServer,*" -# metrics: -# - Value: -# alias: "Value" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# - objectName: "kafka.server:type=KafkaServer,name=BrokerState" -# metrics: -# - Value: -# alias: "Value" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# -##JVM Metrics -# - objectName: "java.lang:type=Memory" -# metrics: -# - HeapMemoryUsage.committed: -# alias: "Heap Memory Usage | Committed (bytes)" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# -# - HeapMemoryUsage.max: -# alias: "Heap Memory Usage | Max (bytes)" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# -# - HeapMemoryUsage.used: -# alias: "Non Heap Memory Usage | Used (bytes)" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# -# - NonHeapMemoryUsage.committed: -# alias: "Non Heap Memory Usage | Committed (bytes)" -# multiplier: "" -# delta: false -# aggregationType: "AVERAGE" -# timeRollUpType: "AVERAGE" -# clusterRollUpType: "INDIVIDUAL" -# + - objectName: "kafka.server:type=KafkaRequestHandlerPool,*" + metrics: + - Count: + alias: "Count" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec" + metrics: + - Count: + alias: "Count" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=ReplicaManager,name=IsrShrinksPerSec" + metrics: + - Count: + alias: "Count" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=SessionExpireListener,*" + metrics: + - Count: + alias: "Count" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.network:type=RequestMetrics,*" + metrics: + - Count: + alias: "Count" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.controller:type=ControllerStats,*" + metrics: + - Count: + alias: "Count" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - MeanRate: + alias: "Mean Rate" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=DelayedOperationPurgatory,*" + metrics: + - Value: + alias: "Value" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=ReplicaFetcherManager,*" + metrics: + - Value: + alias: "Value" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=ReplicaManager,name=LeaderCount" + metrics: + - Value: + alias: "Value" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=ReplicaManager,name=PartitionCount" + metrics: + - Value: + alias: "Value" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions" + metrics: + - Value: + alias: "Value" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.network:type=Processor,*" + metrics: + - Value: + alias: "Value" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.network:type=RequestChannel,*" + metrics: + - Value: + alias: "Value" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.network:type=SocketServer,*" + metrics: + - Value: + alias: "Value" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=KafkaServer,name=BrokerState" + metrics: + - Value: + alias: "Value" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + +#JVM Metrics + - objectName: "java.lang:type=Memory" + metrics: + - HeapMemoryUsage.committed: + alias: "Heap Memory Usage | Committed (bytes)" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - HeapMemoryUsage.max: + alias: "Heap Memory Usage | Max (bytes)" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - HeapMemoryUsage.used: + alias: "Non Heap Memory Usage | Used (bytes)" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + + - NonHeapMemoryUsage.committed: + alias: "Non Heap Memory Usage | Committed (bytes)" + multiplier: "" + delta: false + aggregationType: "AVERAGE" + timeRollUpType: "AVERAGE" + clusterRollUpType: "INDIVIDUAL" + diff --git a/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java deleted file mode 100644 index b753336..0000000 --- a/src/test/java/com/appdynamics/extensions/kafka/metrics/CustomSSLSocketFactoryTest.java +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Copyright 2018 AppDynamics, Inc. - * - * 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. - */ - - -package com.appdynamics.extensions.kafka.metrics; - -import com.appdynamics.extensions.AMonitorJob; -import com.appdynamics.extensions.conf.MonitorContextConfiguration; -import com.appdynamics.extensions.kafka.CustomSSLSocketFactory; -import com.appdynamics.extensions.util.PathResolver; -import com.singularity.ee.agent.systemagent.api.AManagedMonitor; -import org.junit.Test; -import org.mockito.Mockito; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.management.*; -import javax.management.remote.*; -import javax.management.remote.rmi.RMIConnectorServer; -import javax.rmi.ssl.SslRMIClientSocketFactory; -import javax.rmi.ssl.SslRMIServerSocketFactory; -import java.rmi.registry.LocateRegistry; -import java.rmi.registry.Registry; -import java.rmi.server.RMIClientSocketFactory; -import java.rmi.server.RMIServerSocketFactory; -import java.util.HashMap; -import java.util.Map; - -public class CustomSSLSocketFactoryTest { - private static final Logger logger = LoggerFactory.getLogger(CustomSSLSocketFactoryTest.class); -// -// public static JMXConnectorServer startDefaultSSLServer(int port) { -// MBeanServer mBeanServer = MBeanServerFactory.createMBeanServer(); -// HashMap env = new HashMap(); -// JMXConnectorServer jmxConnectorServer = null; -// try { -// JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); -// Registry registry = LocateRegistry.createRegistry(port, new SslRMIClientSocketFactory(), null); -// jmxConnectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mBeanServer); -// } catch (Exception ioe) { -// logger.debug("Could not connect"); -// } -// return jmxConnectorServer; -// } -// -// @Test -// public void testDefaultSSLServerConnection() throws Exception { -// int port = 8750; -// JMXConnectorServer jmxConnectorServer = startDefaultSSLServer(port); -// jmxConnectorServer.start(); -// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); -// Map env = new HashMap(); -// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); -// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory()); -// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); -// jmxConnectorServer.stop(); -// } -// -// @Test() -// public void testDefaultSSLFactoryWithIncorrectJRETrustStore() throws Exception { -// int port = 8747; -// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration -// ("Kafka Monitor", -// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), -// Mockito.mock(AMonitorJob.class)); -// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL_with_default_jre_keys.yml"); -// Map env = new HashMap(); -// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); -// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory()); -// JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startDefaultSSLServer(port); -// jmxConnectorServer.start(); -// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); -// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); -// jmxConnectorServer.stop(); -// -// } -////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// public static JMXConnectorServer startCustomSSLServer(int port) { -// MBeanServer mBeanServer = MBeanServerFactory.createMBeanServer(); -// HashMap env = new HashMap(); -// JMXConnectorServer jmxConnectorServer = null; -// try { -// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration -// ("Kafka Monitor", -// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), -// Mockito.mock(AMonitorJob.class)); -// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL_server.yml"); -// JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); -// Map config = contextConfiguration.getConfigYml(); -// Map connectionMap = (Map) config.get("connection"); -// connectionMap.put("encryptionKey", ""); -// Registry registry = LocateRegistry.createRegistry(port, new SslRMIClientSocketFactory(),null); -// jmxConnectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mBeanServer); -// } catch (Exception ioe) { -// logger.debug("Could not connect"); -// } -// return jmxConnectorServer; -// } -// -// @Test -// public void testCustomSSLServerConnection() throws Exception { -// int port = 8745; -// JMXConnectorServer jmxConnectorServer = startCustomSSLServer(port); -// jmxConnectorServer.start(); -// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); -// Map env = new HashMap(); -// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new CustomSSLSocketFactory()); -// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, null); -// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); -// jmxConnectorServer.stop(); -// } -// -// @Test -// public void testCustomSSLFactoryWithCorrectTrustStore() throws Exception { -// int port = 8746; -// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration -// ("Kafka Monitor", -// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), -// Mockito.mock(AMonitorJob.class)); -// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL.yml"); -// Map config = contextConfiguration.getConfigYml(); -// Map connectionMap = (Map) config.get("connection"); -// connectionMap.put("encryptionKey", ""); -// Map env = new HashMap(); -// CustomSSLSocketFactory customSSLSocketFactory = new CustomSSLSocketFactory(); -// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, customSSLSocketFactory.createSocketFactory(connectionMap)); -// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory()); -// JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startCustomSSLServer(port); -// jmxConnectorServer.start(); -// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); -// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); -// jmxConnectorServer.stop(); -// } -// -// -// @Test() -// public void testCustomSSLFactoryWithIncorrectKeys() throws Exception { -// int port = 8749; -// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration -// ("Kafka Monitor", -// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), -// Mockito.mock(AMonitorJob.class)); -// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_SSL_with_incorrect_keys.yml"); -// Map config = contextConfiguration.getConfigYml(); -// Map connectionMap = (Map) config.get("connection"); -// connectionMap.put("encryptionKey", ""); -// Map env = new HashMap(); -// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new CustomSSLSocketFactory().createSocketFactory(connectionMap)); -// JMXConnectorServer jmxConnectorServer = CustomSSLSocketFactoryTest.startCustomSSLServer(port); -// jmxConnectorServer.start(); -// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi"); -// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); -// jmxConnectorServer.stop(); -// } - -} \ No newline at end of file diff --git a/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java index c705301..05657d0 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessorTest.java @@ -13,6 +13,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; + import javax.management.*; import javax.management.openmbean.CompositeDataSupport; import javax.management.openmbean.CompositeType; @@ -24,222 +25,223 @@ import java.util.List; import java.util.Map; import java.util.Set; + import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; public class DomainMetricsProcessorTest { -// @Test -// public void whenNonCompositeObjectsThenReturnMetrics() throws IOException, -// IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException { -// -// JMXConnector jmxConnector = mock(JMXConnector.class); -// JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); -// MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); -// -// ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); -// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration -// ("Kafka Monitor", -// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), -// Mockito.mock(AMonitorJob.class)); -// -// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_non_composite_metrics.yml"); -// Map config = contextConfiguration.getConfigYml(); -// List mBeans = (List) config.get("mbeans"); -// Set objectInstances = Sets.newHashSet(); -// objectInstances.add(new ObjectInstance( -// "org.apache.kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", "test")); -// List attributes = Lists.newArrayList(); -// attributes.add(new Attribute("Count", 100)); -// attributes.add(new Attribute("Mean Rate", 200 )); -// List metricNames = Lists.newArrayList(); -// metricNames.add("Count"); -// doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector), -// Mockito.any(ObjectName.class) ); -// doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector), -// Mockito.any(ObjectInstance.class)); -// doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), -// Mockito.any(String[].class)); -// DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( -// contextConfiguration, jmxConnectionAdapter, -// jmxConnector, "server1", metricWriteHelper); -// -// for (Map mBean : mBeans) { -// -// domainMetricsProcessor.populateMetricsForMBean(mBean); -// verify(metricWriteHelper) -// .transformAndPrintMetrics(pathCaptor.capture()); -// Metric firstResultMetric = (Metric)pathCaptor.getValue().get(0); -// Metric secondResultMetric = (Metric)pathCaptor.getValue().get(1); -// Assert.assertEquals(firstResultMetric.getMetricName(),"Count"); -// Assert.assertEquals(firstResultMetric.getMetricValue(), "100"); -// Assert.assertEquals(firstResultMetric.getAggregationType(), "AVERAGE"); -// Assert.assertEquals(firstResultMetric.getClusterRollUpType(), "INDIVIDUAL"); -// Assert.assertEquals(firstResultMetric.getTimeRollUpType(), "AVERAGE"); -// Assert.assertEquals(secondResultMetric.getMetricName(), "Mean Rate"); -// Assert.assertEquals(secondResultMetric.getMetricValue(), "200"); -// Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); -// Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); -// Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "AVERAGE"); -// } -// } -// -// @Test -// public void whenCompositeObjectsThenReturnMetrics() throws MalformedObjectNameException, ReflectionException, -// InstanceNotFoundException,IntrospectionException,IOException,OpenDataException { -// -// JMXConnector jmxConnector = mock(JMXConnector.class); -// JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); -// MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); -// -// ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); -// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration -// ("Kafka Monitor", -// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), -// Mockito.mock(AMonitorJob.class)); -// -// -// contextConfiguration.setConfigYml("src/test/resources/conf/config_for_composite_metrics.yml"); -// Map config = contextConfiguration.getConfigYml(); -// List> mBeans = (List>) config.get("mbeans"); -// Set objectInstances = Sets.newHashSet(); -// objectInstances.add(new ObjectInstance("java.lang:type=Memory", "test")); -// List attributes = Lists.newArrayList(); -// attributes.add(new Attribute("HeapMemoryUsage", createCompositeDataSupportObject())); -// attributes.add(new Attribute("Count", 100)); -// attributes.add(new Attribute("Mean Rate", 200 )); -// List metricNames = Lists.newArrayList(); -// doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector),Mockito.any(ObjectName.class) ); -// doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector), Mockito.any(ObjectInstance.class)); -// doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), Mockito.any(String[] -// .class)); -// DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( -// contextConfiguration, jmxConnectionAdapter, -// jmxConnector, "server2", metricWriteHelper); -// for (Map mBean : mBeans) { -// -// domainMetricsProcessor.populateMetricsForMBean(mBean); -// verify(metricWriteHelper) -// .transformAndPrintMetrics(pathCaptor.capture()); -// Metric firstResultMetric = (Metric)pathCaptor.getValue().get(0); -// Metric secondResultMetric = (Metric)pathCaptor.getValue().get(1); -// Assert.assertEquals(firstResultMetric.getMetricName(),"HeapMemoryUsage.min"); -// Assert.assertEquals(firstResultMetric.getMetricValue(), "50"); -// Assert.assertEquals(firstResultMetric.getAggregationType(), "AVERAGE"); -// Assert.assertEquals(firstResultMetric.getClusterRollUpType(), "INDIVIDUAL"); -// Assert.assertEquals(firstResultMetric.getTimeRollUpType(), "AVERAGE"); -// Assert.assertEquals(secondResultMetric.getMetricName(),"HeapMemoryUsage.max"); -// Assert.assertEquals(secondResultMetric.getMetricValue(), "100"); -// Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); -// Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); -// Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "AVERAGE"); -// } -// } -// -// @Test -// public void whenCompositeAndNonCompositeObjectsThenReturnMetrics() throws IOException, -// IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException, -// OpenDataException{ -// -// JMXConnector jmxConnector = mock(JMXConnector.class); -// JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); -// MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); -// -// ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); -// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration -// ("Kafka Monitor", -// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), -// Mockito.mock(AMonitorJob.class)); -// -// contextConfiguration.setConfigYml("src/test/resources/conf/config_composite_and_non_composite_metrics.yml"); -// Map config = contextConfiguration.getConfigYml(); -// List> mBeans = (List>) config.get("mbeans"); -// Set objectInstances = Sets.newHashSet(); -// objectInstances.add(new ObjectInstance( -// "org.apache.kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", "test")); -// List attributes = Lists.newArrayList(); -// attributes.add(new Attribute(("Count"), 0)); -// attributes.add(new Attribute("HeapMemoryUsage", createCompositeDataSupportObject())); -// List metricNames = Lists.newArrayList(); -// metricNames.add("metric1"); -// metricNames.add("metric2"); -// doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector) -// ,Mockito.any(ObjectName.class) ); -// doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector) -// , Mockito.any(ObjectInstance.class)); -// doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), -// Mockito.any(ObjectName.class), Mockito.any(String[] -// .class)); -// DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( -// contextConfiguration, jmxConnectionAdapter, -// jmxConnector,"server1", metricWriteHelper); -// for (Map mBean : mBeans) { -// -// domainMetricsProcessor.populateMetricsForMBean(mBean); -// verify(metricWriteHelper) -// .transformAndPrintMetrics(pathCaptor.capture()); -// Metric firstResultMetric = (Metric) pathCaptor.getValue().get(0); -// Metric secondResultMetric = (Metric) pathCaptor.getValue().get(1); -// Assert.assertEquals(firstResultMetric.getMetricName(), "Count"); -// Assert.assertEquals(firstResultMetric.getMetricValue(), "0"); -// Assert.assertEquals(firstResultMetric.getAggregationType(), "AVERAGE"); -// Assert.assertEquals(firstResultMetric.getClusterRollUpType(), "INDIVIDUAL"); -// Assert.assertEquals(firstResultMetric.getTimeRollUpType(), "AVERAGE"); -// Assert.assertEquals(secondResultMetric.getMetricName(), "HeapMemoryUsage.min"); -// Assert.assertEquals(secondResultMetric.getMetricValue(), "50"); -// Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); -// Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); -// Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "SUM"); -// } -// } -// -// private CompositeDataSupport createCompositeDataSupportObject () throws OpenDataException { -// String typeName = "type"; -// String description = "description"; -// String[] itemNames = {"min", "max"}; -// String[] itemDescriptions = {"maxDesc", "minDesc"}; -// OpenType[] itemTypes = new OpenType[]{new OpenType("java.lang.String", "type", -// "description") { -// @Override -// public boolean isValue (Object obj) { -// return true; -// } -// @Override -// public boolean equals (Object obj) { -// return false; -// } -// @Override -// public int hashCode () { -// return 0; -// } -// @Override -// public String toString () { -// return "50"; -// } -// }, new OpenType("java.lang.String", "type", "description") { -// @Override -// public boolean isValue (Object obj) { -// return true; -// } -// @Override -// public boolean equals (Object obj) { -// return false; -// } -// @Override -// public int hashCode () { -// return 0; -// } -// @Override -// public String toString () { -// return "100"; -// } -// }}; -// CompositeType compositeType = new CompositeType(typeName, description, itemNames, -// itemDescriptions, itemTypes); -// String[] itemNamesForCompositeDataSupport = {"min", "max"}; -// Object[] itemValuesForCompositeDataSupport = {new BigDecimal(50), new BigDecimal(100)}; -// return new CompositeDataSupport(compositeType, itemNamesForCompositeDataSupport, -// itemValuesForCompositeDataSupport); -// } + @Test + public void whenNonCompositeObjectsThenReturnMetrics() throws IOException, + IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException { + + JMXConnector jmxConnector = mock(JMXConnector.class); + JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); + MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); + + ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); + MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration + ("Kafka Monitor", + "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), + Mockito.mock(AMonitorJob.class)); + + contextConfiguration.setConfigYml("src/test/resources/conf/config_for_non_composite_metrics.yml"); + Map config = contextConfiguration.getConfigYml(); + List mBeans = (List) config.get("mbeans"); + Set objectInstances = Sets.newHashSet(); + objectInstances.add(new ObjectInstance( + "org.apache.kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", "test")); + List attributes = Lists.newArrayList(); + attributes.add(new Attribute("Count", 100)); + attributes.add(new Attribute("Mean Rate", 200 )); + List metricNames = Lists.newArrayList(); + metricNames.add("Count"); + doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector), + Mockito.any(ObjectName.class) ); + doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector), + Mockito.any(ObjectInstance.class)); + doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), + Mockito.any(String[].class)); + DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( + contextConfiguration, jmxConnectionAdapter, + jmxConnector, "server1", metricWriteHelper); + + for (Map mBean : mBeans) { + + domainMetricsProcessor.populateMetricsForMBean(mBean); + verify(metricWriteHelper) + .transformAndPrintMetrics(pathCaptor.capture()); + Metric firstResultMetric = (Metric)pathCaptor.getValue().get(0); + Metric secondResultMetric = (Metric)pathCaptor.getValue().get(1); + Assert.assertEquals(firstResultMetric.getMetricName(),"Count"); + Assert.assertEquals(firstResultMetric.getMetricValue(), "100"); + Assert.assertEquals(firstResultMetric.getAggregationType(), "AVERAGE"); + Assert.assertEquals(firstResultMetric.getClusterRollUpType(), "INDIVIDUAL"); + Assert.assertEquals(firstResultMetric.getTimeRollUpType(), "AVERAGE"); + Assert.assertEquals(secondResultMetric.getMetricName(), "Mean Rate"); + Assert.assertEquals(secondResultMetric.getMetricValue(), "200"); + Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); + Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); + Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "AVERAGE"); + } + } + + @Test + public void whenCompositeObjectsThenReturnMetrics() throws MalformedObjectNameException, ReflectionException, + InstanceNotFoundException,IntrospectionException,IOException,OpenDataException { + + JMXConnector jmxConnector = mock(JMXConnector.class); + JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); + MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); + + ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); + MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration + ("Kafka Monitor", + "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), + Mockito.mock(AMonitorJob.class)); + + + contextConfiguration.setConfigYml("src/test/resources/conf/config_for_composite_metrics.yml"); + Map config = contextConfiguration.getConfigYml(); + List> mBeans = (List>) config.get("mbeans"); + Set objectInstances = Sets.newHashSet(); + objectInstances.add(new ObjectInstance("java.lang:type=Memory", "test")); + List attributes = Lists.newArrayList(); + attributes.add(new Attribute("HeapMemoryUsage", createCompositeDataSupportObject())); + attributes.add(new Attribute("Count", 100)); + attributes.add(new Attribute("Mean Rate", 200 )); + List metricNames = Lists.newArrayList(); + doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector),Mockito.any(ObjectName.class) ); + doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector), Mockito.any(ObjectInstance.class)); + doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), Mockito.any(ObjectName.class), Mockito.any(String[] + .class)); + DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( + contextConfiguration, jmxConnectionAdapter, + jmxConnector, "server2", metricWriteHelper); + for (Map mBean : mBeans) { + + domainMetricsProcessor.populateMetricsForMBean(mBean); + verify(metricWriteHelper) + .transformAndPrintMetrics(pathCaptor.capture()); + Metric firstResultMetric = (Metric)pathCaptor.getValue().get(0); + Metric secondResultMetric = (Metric)pathCaptor.getValue().get(1); + Assert.assertEquals(firstResultMetric.getMetricName(),"HeapMemoryUsage.min"); + Assert.assertEquals(firstResultMetric.getMetricValue(), "50"); + Assert.assertEquals(firstResultMetric.getAggregationType(), "AVERAGE"); + Assert.assertEquals(firstResultMetric.getClusterRollUpType(), "INDIVIDUAL"); + Assert.assertEquals(firstResultMetric.getTimeRollUpType(), "AVERAGE"); + Assert.assertEquals(secondResultMetric.getMetricName(),"HeapMemoryUsage.max"); + Assert.assertEquals(secondResultMetric.getMetricValue(), "100"); + Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); + Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); + Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "AVERAGE"); + } + } + + @Test + public void whenCompositeAndNonCompositeObjectsThenReturnMetrics() throws IOException, + IntrospectionException,ReflectionException, InstanceNotFoundException,MalformedObjectNameException, + OpenDataException{ + + JMXConnector jmxConnector = mock(JMXConnector.class); + JMXConnectionAdapter jmxConnectionAdapter = mock(JMXConnectionAdapter.class); + MetricWriteHelper metricWriteHelper = mock(MetricWriteHelper.class); + + ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(List.class); + MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration + ("Kafka Monitor", + "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), + Mockito.mock(AMonitorJob.class)); + + contextConfiguration.setConfigYml("src/test/resources/conf/config_composite_and_non_composite_metrics.yml"); + Map config = contextConfiguration.getConfigYml(); + List> mBeans = (List>) config.get("mbeans"); + Set objectInstances = Sets.newHashSet(); + objectInstances.add(new ObjectInstance( + "org.apache.kafka.server:type=ReplicaManager,name=IsrExpandsPerSec", "test")); + List attributes = Lists.newArrayList(); + attributes.add(new Attribute(("Count"), 0)); + attributes.add(new Attribute("HeapMemoryUsage", createCompositeDataSupportObject())); + List metricNames = Lists.newArrayList(); + metricNames.add("metric1"); + metricNames.add("metric2"); + doReturn(objectInstances).when(jmxConnectionAdapter).queryMBeans(eq(jmxConnector) + ,Mockito.any(ObjectName.class) ); + doReturn(metricNames).when(jmxConnectionAdapter).getReadableAttributeNames(eq(jmxConnector) + , Mockito.any(ObjectInstance.class)); + doReturn(attributes).when(jmxConnectionAdapter).getAttributes(eq(jmxConnector), + Mockito.any(ObjectName.class), Mockito.any(String[] + .class)); + DomainMetricsProcessor domainMetricsProcessor = new DomainMetricsProcessor( + contextConfiguration, jmxConnectionAdapter, + jmxConnector,"server1", metricWriteHelper); + for (Map mBean : mBeans) { + + domainMetricsProcessor.populateMetricsForMBean(mBean); + verify(metricWriteHelper) + .transformAndPrintMetrics(pathCaptor.capture()); + Metric firstResultMetric = (Metric) pathCaptor.getValue().get(0); + Metric secondResultMetric = (Metric) pathCaptor.getValue().get(1); + Assert.assertEquals(firstResultMetric.getMetricName(), "Count"); + Assert.assertEquals(firstResultMetric.getMetricValue(), "0"); + Assert.assertEquals(firstResultMetric.getAggregationType(), "AVERAGE"); + Assert.assertEquals(firstResultMetric.getClusterRollUpType(), "INDIVIDUAL"); + Assert.assertEquals(firstResultMetric.getTimeRollUpType(), "AVERAGE"); + Assert.assertEquals(secondResultMetric.getMetricName(), "HeapMemoryUsage.min"); + Assert.assertEquals(secondResultMetric.getMetricValue(), "50"); + Assert.assertEquals(secondResultMetric.getAggregationType(), "AVERAGE"); + Assert.assertEquals(secondResultMetric.getClusterRollUpType(), "INDIVIDUAL"); + Assert.assertEquals(secondResultMetric.getTimeRollUpType(), "SUM"); + } + } + + private CompositeDataSupport createCompositeDataSupportObject () throws OpenDataException { + String typeName = "type"; + String description = "description"; + String[] itemNames = {"min", "max"}; + String[] itemDescriptions = {"maxDesc", "minDesc"}; + OpenType[] itemTypes = new OpenType[]{new OpenType("java.lang.String", "type", + "description") { + @Override + public boolean isValue (Object obj) { + return true; + } + @Override + public boolean equals (Object obj) { + return false; + } + @Override + public int hashCode () { + return 0; + } + @Override + public String toString () { + return "50"; + } + }, new OpenType("java.lang.String", "type", "description") { + @Override + public boolean isValue (Object obj) { + return true; + } + @Override + public boolean equals (Object obj) { + return false; + } + @Override + public int hashCode () { + return 0; + } + @Override + public String toString () { + return "100"; + } + }}; + CompositeType compositeType = new CompositeType(typeName, description, itemNames, + itemDescriptions, itemTypes); + String[] itemNamesForCompositeDataSupport = {"min", "max"}; + Object[] itemValuesForCompositeDataSupport = {new BigDecimal(50), new BigDecimal(100)}; + return new CompositeDataSupport(compositeType, itemNamesForCompositeDataSupport, + itemValuesForCompositeDataSupport); + } } diff --git a/src/test/java/com/appdynamics/extensions/kafka/metrics/SSLConnectionTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/SSLConnectionTest.java new file mode 100644 index 0000000..cd3a485 --- /dev/null +++ b/src/test/java/com/appdynamics/extensions/kafka/metrics/SSLConnectionTest.java @@ -0,0 +1,109 @@ +/** + * Copyright 2018 AppDynamics, Inc. + * + * 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. + */ + + +package com.appdynamics.extensions.kafka.metrics; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXConnectorServer; +import javax.management.remote.JMXServiceURL; +import javax.management.remote.rmi.RMIConnectorServer; +import javax.rmi.ssl.SslRMIClientSocketFactory; +import javax.rmi.ssl.SslRMIServerSocketFactory; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +public class SSLConnectionTest { +// +// @Before +// public void setUpConnectionWithoutSSL(){ +// +// Properties props = new Properties(); +// props.setProperty("com.sun.management.jmxremote.authenticate", "false"); +// props.setProperty("com.sun.management.jmxremote.ssl", "false"); +// props.setProperty("com.sun.management.jmxremote.registry.ssl", "false"); +// JMXConnectorServer server = sun.management.jmxremote.ConnectorBootstrap +// .startRemoteConnectorServer("9990", props); +// } +// +// @Test +// public void whenNotUsingSslThenTestServerConnection() throws Exception { +// +// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:9990/jmxrmi"); +// Map env = new HashMap(); +// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); +// Assert.assertNotNull(jmxConnector); +// } + +// @Before +// public void setUpConnectionWithSslAndCorrectKeys(){ +// System.setProperty("javax.net.ssl.trustStore", "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks"); +// System.setProperty("javax.net.ssl.trustStorePassword", "test1234"); +// System.setProperty("javax.net.ssl.keyStore", "/Users/vishaka.sekar/AppDynamics/server/kafka.server.keystore.jks"); +// System.setProperty("javax.net.ssl.keyStorePassword", "test1234"); +// System.setProperty("java.rmi.server.hostname", "10.0.0.106"); +// System.setProperty("com.sun.management.jmxremote.port", "6789"); +// Properties connectionProperties = new Properties(); +// connectionProperties.setProperty("com.sun.management.jmxremote.authenticate", "false"); +// connectionProperties.setProperty("com.sun.management.jmxremote.ssl", "true"); +// connectionProperties.setProperty("com.sun.management.jmxremote.registry.ssl", "true"); +// +// JMXConnectorServer server = sun.management.jmxremote.ConnectorBootstrap +// .startRemoteConnectorServer("6789", connectionProperties); +// } +// +// @Test() +// public void whenUsingSslAndCorrectKeysThenTestServerConnection() throws Exception { +// int port = 6789; +// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://10.0.0.106:6789/jmxrmi"); +// Map env = new HashMap(); +// env.put("com.sun.jndi.rmi.factory.socket",new SslRMIClientSocketFactory()); +// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); +// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory()); +// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); +// } + + @Before + public void setUpConnectionWithIncorrectKeys(){ + System.setProperty("javax.net.ssl.trustStore", "/Users/vishaka.sekar/kafka.test.keystore.jks"); + System.setProperty("javax.net.ssl.trustStorePassword", "test1234"); + System.setProperty("javax.net.ssl.keyStore", "/Users/vishaka.sekar/AppDynamics/server/kafka.server.keystore.jks"); + System.setProperty("javax.net.ssl.keyStorePassword", "test1234"); + Properties props = new Properties(); + props.setProperty("com.sun.management.jmxremote.authenticate", "false"); + props.setProperty("com.sun.management.jmxremote.ssl", "true"); + props.setProperty("com.sun.management.jmxremote.registry.ssl", "false"); + JMXConnectorServer server = sun.management.jmxremote.ConnectorBootstrap + .startRemoteConnectorServer("6789", props); + } + @Test(expected = Exception.class) + public void testSSLServerConnectionWithIncorrectTrustStore() throws Exception { + int port = 6789; + JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:6789/jmxrmi"); + Map env = new HashMap(); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); + env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory()); + JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); + } +} \ No newline at end of file diff --git a/src/test/resources/conf/config_for_SSL.yml b/src/test/resources/conf/config_for_SSL.yml deleted file mode 100644 index 1c97743..0000000 --- a/src/test/resources/conf/config_for_SSL.yml +++ /dev/null @@ -1,24 +0,0 @@ -metricPrefix: "Server|Component:19|Custom Metrics|Kafka" - -servers: - - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" - host: "" - port: "" - username: "" - password: "" - encryptedPassword: "" - displayName: "Local Kafka Server" - useSsl: true - - #Provide the encryption key for the password -encryptionKey: "" - -# Configure this section only if useSSL is set to true. -connection: - socketTimeout: 3000 - connectTimeout: 1000 - sslProtocol: "TLSv1.2" - sslTrustStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks" - sslTrustStorePassword: "test1234" - sslTrustStoreEncryptedPassword: "" - diff --git a/src/test/resources/conf/config_for_SSL_server.yml b/src/test/resources/conf/config_for_SSL_server.yml deleted file mode 100644 index 4029564..0000000 --- a/src/test/resources/conf/config_for_SSL_server.yml +++ /dev/null @@ -1,24 +0,0 @@ -metricPrefix: "Server|Component:19|Custom Metrics|Kafka" - -servers: - - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" - host: "" - port: "" - username: "" - password: "" - encryptedPassword: "" - displayName: "Local Kafka Server" - useSsl: true - - #Provide the encryption key for the password -encryptionKey: "" - -# Configure this section only if useSSL is set to true. -connection: - socketTimeout: 3000 - connectTimeout: 1000 - sslProtocol: "TLSv1.2" - sslKeyStorePath: "/Users/vishaka.sekar/AppDynamics/server/kafka.server.truststore.jks" - sslKeyStorePassword: "test1234" - sslTrustStoreEncryptedPassword: "" - diff --git a/src/test/resources/conf/config_for_SSL_with_default_jre_keys.yml b/src/test/resources/conf/config_for_SSL_with_default_jre_keys.yml deleted file mode 100644 index d1ba9d9..0000000 --- a/src/test/resources/conf/config_for_SSL_with_default_jre_keys.yml +++ /dev/null @@ -1,24 +0,0 @@ -metricPrefix: "Server|Component:19|Custom Metrics|Kafka" - -servers: - - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" - host: "" - port: "" - username: "" - password: "" - encryptedPassword: "" - displayName: "Local Kafka Server" - useSsl: true - - #Provide the encryption key for the password -encryptionKey: "" - -## Configure this section only if useSSL is set to true. -#connection: -# socketTimeout: 3000 -# connectTimeout: 1000 -# sslProtocol: "TLSv1.2" -# sslTrustStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.keystore.jks" -# sslTrustStorePassword: "test1234" -# sslTrustStoreEncryptedPassword: "" - diff --git a/src/test/resources/conf/config_for_SSL_with_incorrect_keys.yml b/src/test/resources/conf/config_for_SSL_with_incorrect_keys.yml deleted file mode 100644 index 4ba004b..0000000 --- a/src/test/resources/conf/config_for_SSL_with_incorrect_keys.yml +++ /dev/null @@ -1,24 +0,0 @@ -metricPrefix: "Server|Component:19|Custom Metrics|Kafka" - -servers: - - serviceUrl: "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi" - host: "" - port: "" - username: "" - password: "" - encryptedPassword: "" - displayName: "Local Kafka Server" - useSsl: true - - #Provide the encryption key for the password -encryptionKey: "" - -# Configure this section only if useSSL is set to true. -connection: - socketTimeout: 3000 - connectTimeout: 1000 - sslProtocol: "TLSv1.2" - sslTrustStorePath: "/Users/vishaka.sekar/AppDynamics/client/kafka.client.keystore.jks" - sslTrustStorePassword: "test1234" - sslTrustStoreEncryptedPassword: "" - From 69a7c71b9d07ecced46a17f3469930ab4c5face5 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Thu, 16 Aug 2018 17:10:08 -0700 Subject: [PATCH 32/49] setting ssl props --- .../extensions/kafka/utils/SslUtils.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java diff --git a/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java b/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java new file mode 100644 index 0000000..b17efb6 --- /dev/null +++ b/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java @@ -0,0 +1,73 @@ +package com.appdynamics.extensions.kafka.utils; + +import com.appdynamics.extensions.crypto.Decryptor; +import com.appdynamics.extensions.kafka.KafkaMonitor; +import com.appdynamics.extensions.util.PathResolver; +import com.google.common.base.Strings; +import com.singularity.ee.agent.systemagent.api.AManagedMonitor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.Map; + +public class SslUtils { + private static final Logger logger = LoggerFactory.getLogger(SslUtils.class); + + public void setSslProperties(Map configMap){ + + //if the config yaml contains the field sslTrustStorePath then the keys are set + // if the field is not present, it defaults to /conf/ + // any change in config.yml parameters requires a MA restart + if(configMap.containsKey(Constants.CONNECTION)) { + Map connectionMap = (Map) configMap.get(Constants.CONNECTION); + if (connectionMap.containsKey(Constants.TRUST_STORE_PATH) && + !Strings.isNullOrEmpty(connectionMap.get(Constants.TRUST_STORE_PATH).toString())) { + String sslTrustStorePath = connectionMap.get(Constants.TRUST_STORE_PATH).toString(); + File customSslTrustStoreFile = new File(sslTrustStorePath); + if(customSslTrustStoreFile == null || !customSslTrustStoreFile.exists() ) { + logger.debug("The file [{}] doesn't exist", customSslTrustStoreFile.getAbsolutePath()); + } + else { + logger.debug("Using custom SSL truststore [{}]", sslTrustStorePath); + System.setProperty("javax.net.ssl.trustStore", customSslTrustStoreFile.getAbsolutePath()); + } + } + else if (connectionMap.containsKey(Constants.TRUST_STORE_PATH) && + Strings.isNullOrEmpty(connectionMap.get(Constants.TRUST_STORE_PATH).toString())){ + //getting path of machine agent home + File installDir = PathResolver.resolveDirectory(AManagedMonitor.class); + File defaultTrustStoreFile = PathResolver.getFile("conf/cacerts.jks", installDir); + if (defaultTrustStoreFile == null || !defaultTrustStoreFile.exists()) { + logger.debug("The file [{}] doesn't exist", installDir+"/conf/cacerts.jks"); + } + else { + logger.debug("Using Machine Agent truststore {}", installDir+"conf/cacerts.jks"); + System.setProperty("javax.net.ssl.trustStore", defaultTrustStoreFile.getAbsolutePath()); + } + } + System.setProperty("javax.net.ssl.trustStorePassword", getSslTrustStorePassword(connectionMap, configMap)); + } + } + + private String getSslTrustStorePassword(Map connectionMap, Map config) { + String password = (String) connectionMap.get("sslTrustStorePassword"); + if (!Strings.isNullOrEmpty(password)) { + return password; + } else { + String encrypted = (String) connectionMap.get("sslTrustStoreEncryptedPassword"); + if (!Strings.isNullOrEmpty(encrypted)) { + String encryptionKey = (String) config.get("encryptionKey"); + if (!Strings.isNullOrEmpty(encryptionKey)) { + return new Decryptor(encryptionKey).decrypt(encrypted); + } else { + logger.error("Cannot decrypt the password. Encryption key not set"); + throw new RuntimeException("Cannot decrypt [encryptedPassword], since [encryptionKey] is not set"); + } + } else { + logger.warn("No password set, using empty string"); + return ""; + } + } + } +} From eae1457ffb2744297c0e3dd361ea1ad00f3e93b2 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Mon, 20 Aug 2018 12:51:03 -0700 Subject: [PATCH 33/49] tests and readme --- README.md | 176 +++++++++--------- pom.xml | 18 ++ .../extensions/kafka/utils/SslUtils.java | 3 +- src/main/resources/conf/config.yml | 10 +- .../kafka/metrics/SSLConnectionTest.java | 109 ----------- .../extensions/kafka/utils/SSLUtilsTest.java | 168 +++++++++++++++++ .../conf/config_ssl_default_keys.yml | 27 +++ .../conf/config_ssl_incorrect_keys.yml | 27 +++ 8 files changed, 339 insertions(+), 199 deletions(-) delete mode 100644 src/test/java/com/appdynamics/extensions/kafka/metrics/SSLConnectionTest.java create mode 100644 src/test/java/com/appdynamics/extensions/kafka/utils/SSLUtilsTest.java create mode 100644 src/test/resources/conf/config_ssl_default_keys.yml create mode 100644 src/test/resources/conf/config_ssl_incorrect_keys.yml diff --git a/README.md b/README.md index 1cb588f..b19d263 100755 --- a/README.md +++ b/README.md @@ -1,79 +1,80 @@ -AppDynamics Monitoring Extension for use with Kafka +Kafka Monitoring Extension for AppDynamics =================================================== -An AppDynamics extension to be used with a stand alone machine agent to provide metrics for Apache Kafka. - ## Use Case ## Apache Kafka® is a distributed, fault-tolerant streaming platform. It can be used to process streams of data in -real-time. +real-time.The Kafka Monitoring extension can be used with a stand alone machine agent to provide metrics for multiple +Apache Kafka . + ## Prerequisites ## 1. This extension extracts the metrics from Kafka using the JMX protocol. - Please ensure JMX is enabled. + Please ensure JMX is enabled. 2. Please ensure Kafka is up and running and is accessible from the the machine on which machine agent is installed. -3. In order to use this extension, you do need a Standalone JAVA Machine Agent - (https://docs.appdynamics.com/display/PRO44/Standalone+Machine+Agents). - or SIM Agent (https://docs.appdynamics.com/display/PRO44/Server+Visibility).
- For more details on downloading these products, please visit https://download.appdynamics.com/.
- The extension needs to be able to connect to the Kafka instance(s) in order to collect and send metrics.
- To do this, you will have to either establish a remote connection in between the extension and Kafka, - or have an agent on the same machine running the product in order for the extension to collect and send the metrics. - -## Enabling JMX -Before configuring the extension, please make sure to run the below steps to check if the set up is correct. -1. Test connection to the port from the machine where the extension is installed. - ``` - nc -v KafkaHostIP port +3. In order to use this extension, you do need a [Standalone JAVA Machine Agent](https://docs.appdynamics.com/display/PRO44/Standalone+Machine+Agents). + or [SIM Agent](https://docs.appdynamics.com/display/PRO44/Server+Visibility).
+ For more details on downloading these products, please visit [Downloads](https://download.appdynamics.com/).
+ +## Installation ## + + Unzip as "KafkaMonitor" and copy the "KafkaMonitor" directory to `/monitors` + Please place the extension in the `monitors` directory of your Machine Agent installation directory. + +## Configuration + +##### 1. Configuring ports + Test connection to the Kafka host/port from the machine where the extension is installed. + ``` nc -v KafkaServerIP port ``` For example, connecting to the localhost on port 9999. - - nc -v localhost 9999 - - ``` - - If you get ```Connection to localhost port 9999 [tcp/distinct] succeeded!```,it confirms the access to the Kafka server. -
If not the JMX port needs to be enabled. - -2. JMX configuration: -
To enable JMX monitoring for Kafka broker, port 9999 has to be configured to allow monitoring on that port. -
Edit the Kafka start-up script `/bin/kafka-server-start.sh` to include + ``` nc -v localhost 9999 ```. +
If you get ```Connection to localhost port 9999 [tcp/distinct] succeeded!```,it confirms the access to the Kafka server. - ```export JMX_PORT=${JMX_PORT:-9999}``` - Please note, that the Kafka server needs to be restarted once the JMX port is added. +##### 2. Enabling JMX -3. Start jConsole: -
jConsole comes as a utility with installed Java JDK. - In your terminal, please type ```jconsole```.
- In the remote connection text-box, please put in the service url : + To enable JMX monitoring for Kafka broker, port 9999 has to be configured to allow monitoring on that port. +
Edit the Kafka start-up script `/bin/kafka-server-start.sh` to include - ```service:jmx:rmi:///jndi/rmi://:9999/jmxrmi``` + export JMX_PORT=${JMX_PORT:-9999} + + Please note, that the Kafka server needs to be restarted once the JMX port is added. - In the left pane of the jConsole, the list of mbeans exposed by Kafka is displayed. - It is a good idea to match the mbean configuration in the config.yml against the jconsole. - JMX is case sensitive so make sure the config matches the names listed. - -## Configuring Kafka Start-up scripts ## +##### 3. Configuring Kafka Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false" - Also, the changes to `kafka-run-class.sh` has to be made on all the Kafka servers that you wish to monitor. - Please note, that any changes to `kafka-run-class.sh` needs the Kafka server to be restarted for the changes to + - Also, the changes to `kafka-run-class.sh` has to be made on all the Kafka servers that you wish to monitor. + - Please note, that any changes to `kafka-run-class.sh` needs the Kafka server to be restarted for the changes to take effect. -## SSL and password authentication ### +##### 4. Setting up SSL and Password Authentication in Kafka### - If you need to monitor your Kafka servers securely via SSL, please enable the flags mentioned below. -
Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
+ If you need to monitor your Kafka servers securely via SSL, please follow the following steps: + 1. Enable the flags mentioned below: +
Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
- KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=true -Djavax.net.ssl.keyStore=/Absolute/path/to/keystore -Djavax.net.ssl.keyStorePassword=password -Dcom.sun.management.jmxremote.registry.ssl=false" + `KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=true -Djavax.net.ssl.keyStore=/Absolute/path/to/keystore -Djavax.net.ssl.keyStorePassword=password -Dcom.sun.management.jmxremote.registry.ssl=false" + ` - If you also need username/password authentication, please set the flag`-Dcom.sun.management.jmxremote.authenticate=true` - in the `KAFKA_JMX_OPTS` variable. + 2. The extension also needs to be configured to use SSL. In the config.yml of the Kafka Extension, uncomment the `connection` section. +
+ ``` + connection: + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocol: "TLSv1.2" + sslTrustStorePath: "/path/to/truststore/client/kafka.client.truststore.jks" #defaults to conf/cacerts.jks + sslTrustStorePassword: "test1234" # defaults to empty + sslTrustStoreEncryptedPassword: "" + ``` + + 3. If you need username/password authentication, please set the flag
`-Dcom.sun.management.jmxremote.authenticate=true` + in the `KAFKA_JMX_OPTS` variable.Please refer to section [here](#passwordsettings) for further steps. -#### Keys #### +##### Generating SSL Keys #### To generate your keystore and truststore please follow the steps: Creating a keystore is mandatory for SSL. However, creating a truststore is optional. You can create @@ -95,26 +96,28 @@ In case you want to use the default JRE trust store, please replace the `kafka.c Additional info about creating SSL keys is listed [here](https://docs.confluent.io/current/tutorials/security_tutorial.html#creating-ssl-keys-and-certificates) -#### Password Settings #### -
To know more on how to set the credentials, please see section "Using Password and Access Files" in [this](https://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html). +######
Password Settings +If you need password authentication, you need to set-up the password in the JVM of the Kafka server +To know more on how to set the credentials, please see section `Using Password and Access Files` in [this](https://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html). -## Configuring the extension using config.yml +###### Config.yml Configure the Kafka monitoring extension by editing the config.yml file in `/monitors/KafkaMonitor/ -Please copy all the contents of the config.yml file and go to http://www.yamllint.com/ .
-On reaching the website, paste the contents and press the “Go” button on the bottom left.
-If you get a valid output, that means your formatting is correct and you may move on to the next step. - - 1. Configure the "tier" under which the metrics need to be reported. This can be done by changing the value of `` in - metricPrefix: "Server|Component:``|Custom Metrics|Kafka".Please refer [how to find component ID](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) - + 1. Configure the "tier" under which the metrics need to be reported. This can be done by changing the value of `` in +
`metricPrefix: "Server|Component:|Custom Metrics|Kafka"`.
+ Please refer + [How to find Component ID](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) of your tiers. + For example, ``` metricPrefix: "Server|Component:19|Custom Metrics|Kafka" ``` - 2. Configure the Kafka servers by specifying either Service URL or of all Kafka servers, - username & password (only if authentication enabled),encryptedPassword(only if password encryption required). - If you are using SSL to securely monitor your Kafka servers, please set useSsl is true. + 2. Configure the Kafka servers by specifying either `serviceUrl` or `` of all Kafka servers. + - Here, `host` is the IP address + of the Kafka server to be monitored, and `port` is the JMX port of the Kafka server. + - Please provide `username` & `password` (only if authentication enabled) + - `encryptedPassword`(only if password encryption required). + - If you are using SSL to securely monitor your Kafka servers, please set `useSsl` as `true`. For example, ``` @@ -156,7 +159,7 @@ If you get a valid output, that means your formatting is correct and you may mov If number Kafka servers that need to be monitored by one extension is 10, then number of threads is 10 - numberOfThreads: 30 + numberOfThreads: 10 6. Configure the metrics section. @@ -203,8 +206,12 @@ If you get a valid output, that means your formatting is correct and you may mov `objectName: kafka.server:type=ReplicaManager,name=IsrExpandsPerSec` will return only those metrics corresponding to the `IsrExpandsPerSec` object. -## Metrics Provided ## - + ###### Validating config.yml: + Please copy all the contents of the config.yml file and go to [YamlLint](http://www.yamllint.com/).
+ On reaching the website, paste the contents and press the “Go” button on the bottom left.
+ If you get a valid output, that means your formatting is correct and you may move on to the next step. + +## Metrics This extension collects metrics via JMX and can be configured to report any of the metrics that Kafka exposes. It provides metrics on Kafka server, controller and the network. @@ -219,23 +226,19 @@ For eg. java -Dappdynamics.agent.maxMetrics=2500 -jar machineagent.jar ``` -## Installation ## - -1. Run "mvn clean install" and find the KafkaMonitor.zip file in the "target" folder. You can also download the - KafkaMonitor.zip from [AppDynamics Exchange][https://www.appdynamics.com/community/exchange/]. - -2. Unzip as "KafkaMonitor" and copy the "KafkaMonitor" directory to `/monitors` - Please place the extension in the "monitors" directory of your Machine Agent installation directory. - Do not place the extension in the "extensions" directory of your Machine Agent installation directory. - ## Credentials Encryption -Please visit [this](https://community.appdynamics.com/t5/Knowledge-Base/How-to-use-Password-Encryption-with-Extensions/ta-p/29397) page to get detailed instructions on password encryption. The steps in this document will guide you through the whole process. +Please visit [this](https://community.appdynamics.com/t5/Knowledge-Base/How-to-use-Password-Encryption-with-Extensions/ta-p/29397) +page to get detailed instructions on password encryption. The steps in this document will guide you through the whole process. ## Extensions Workbench -Workbench is an inbuilt feature provided with each extension in order to assist you to fine tune the extension setup before you actually deploy it on the controller. Please review the following [document](https://community.appdynamics.com/t5/Knowledge-Base/How-to-use-the-Extensions-WorkBench/ta-p/30130) for how to use the Extensions WorkBench +Workbench is an inbuilt feature provided with each extension in order to assist you to fine tune the extension setup +before you actually deploy it on the controller. Please review the following +[document](https://community.appdynamics.com/t5/Knowledge-Base/How-to-use-the-Extensions-WorkBench/ta-p/30130) for +how to use the Extensions WorkBench ## Troubleshooting -Please follow the steps listed in the [extensions troubleshooting document](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) in order to troubleshoot your issue. These are a set of common issues that customers might have faced during the installation of the extension. If these don't solve your issue, please follow the last step on the troubleshooting-document to contact the support team. +Please follow the steps listed in the [extensions troubleshooting document](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) in order to troubleshoot your issue. +These are a set of common issues that customers might have faced during the installation of the extension. If these don't solve your issue, please follow the last step on the troubleshooting-document to contact the support team. ## Support Tickets If after going through the Troubleshooting Document you have not been able to get your extension working, please file a ticket and add the following information. @@ -259,10 +262,13 @@ For any support related questions, you can also contact help@appdynamics.com. Always feel free to fork and contribute any changes directly via [GitHub](https://github.com/Appdynamics/kafka-monitoring-extension). ## Version - -Extension Version: 2.0.0 -
Controller Compatibility: 4.0 or Later -
Tested On: Apache Kafka 2.11 -
Operating System Tested On: Mac OS -
Last updated On: Aug 08, 2018 -
List of Changes to this extension [Change log](https://github.com/Appdynamics/kafka-monitoring-extension/blob/master/Changelog.md) + |______Name___________________|_______Version_____| + | Extension Version: | 2.0.0 | + | Controller Compatibility: | 4.0 or Later | + | Tested On: | Apache Kafka 2.11 | + | Operating System Tested On: | Mac OS | + | Last updated On: | Aug 08, 2018 | + | List of changes | [Change log](https://github.com/Appdynamics/kafka-monitoring-extension/blob/master/Changelog.md) + | to this extension | | + | ____________________________|___________________| + diff --git a/pom.xml b/pom.xml index 50f646a..81a1d95 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,24 @@ 4.11 test + + org.mockito + mockito-all + 1.9.5 + test + + + org.mockito + mockito-all + 1.9.5 + test + + + org.mockito + mockito-all + 1.9.5 + test + ${project.artifactId} diff --git a/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java b/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java index b17efb6..327fe9f 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java +++ b/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java @@ -36,7 +36,8 @@ public void setSslProperties(Map configMap){ else if (connectionMap.containsKey(Constants.TRUST_STORE_PATH) && Strings.isNullOrEmpty(connectionMap.get(Constants.TRUST_STORE_PATH).toString())){ //getting path of machine agent home - File installDir = PathResolver.resolveDirectory(AManagedMonitor.class); + // File installDir = PathResolver.resolveDirectory(AManagedMonitor.class); + File installDir = new File("/Users/vishaka.sekar/AppDynamics/machineagent/");//for test only todo:remove before publishing File defaultTrustStoreFile = PathResolver.getFile("conf/cacerts.jks", installDir); if (defaultTrustStoreFile == null || !defaultTrustStoreFile.exists()) { logger.debug("The file [{}] doesn't exist", installDir+"/conf/cacerts.jks"); diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index e0fd040..4989dfe 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -28,12 +28,14 @@ encryptionKey: "" # 1. the config property [useSsl] is set to true in any of the servers in the above section AND # 2. you want to use your own custom SSL certs # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Please REMOVE the connection section below if: +# Please REMOVE/COMMENT OUT the connection section below if: # 1.You are not using SSL for any of the Kafka servers listed in the server section OR # 2.You are using SSL for any of the servers([useSsl : true]), but you want to use default truststore of the JRE + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -#If you are using the conenction section, +#If you are using the connection section, #any change to the connection section below requires a machine agent restart for the changes to reflect connection: socketTimeout: 3000 @@ -41,7 +43,7 @@ connection: sslProtocols: "TLSv1.2" sslCipherSuites: "" sslTrustStorePath: "" #if [sslTrustStorePath: ""] empty, it defaults to MA home/conf/cacerts.jks - sslTrustStorePassword: "" # [sslTrustStorePassword: ""] defaults to "" + sslTrustStorePassword: "test1234" # [sslTrustStorePassword: ""] defaults to "" sslTrustStoreEncryptedPassword: "" #provide encrypted Password if encryption is needed # Each Kafka server needs 1 thread each, so please configure this according to the number of servers you are monitoring @@ -62,7 +64,7 @@ mbeans: alias: "Count" multiplier: "" delta: false - aggregationType: "OBSERVATION" + aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" diff --git a/src/test/java/com/appdynamics/extensions/kafka/metrics/SSLConnectionTest.java b/src/test/java/com/appdynamics/extensions/kafka/metrics/SSLConnectionTest.java deleted file mode 100644 index cd3a485..0000000 --- a/src/test/java/com/appdynamics/extensions/kafka/metrics/SSLConnectionTest.java +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright 2018 AppDynamics, Inc. - * - * 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. - */ - - -package com.appdynamics.extensions.kafka.metrics; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.management.remote.JMXConnector; -import javax.management.remote.JMXConnectorFactory; -import javax.management.remote.JMXConnectorServer; -import javax.management.remote.JMXServiceURL; -import javax.management.remote.rmi.RMIConnectorServer; -import javax.rmi.ssl.SslRMIClientSocketFactory; -import javax.rmi.ssl.SslRMIServerSocketFactory; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; - -public class SSLConnectionTest { -// -// @Before -// public void setUpConnectionWithoutSSL(){ -// -// Properties props = new Properties(); -// props.setProperty("com.sun.management.jmxremote.authenticate", "false"); -// props.setProperty("com.sun.management.jmxremote.ssl", "false"); -// props.setProperty("com.sun.management.jmxremote.registry.ssl", "false"); -// JMXConnectorServer server = sun.management.jmxremote.ConnectorBootstrap -// .startRemoteConnectorServer("9990", props); -// } -// -// @Test -// public void whenNotUsingSslThenTestServerConnection() throws Exception { -// -// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:9990/jmxrmi"); -// Map env = new HashMap(); -// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); -// Assert.assertNotNull(jmxConnector); -// } - -// @Before -// public void setUpConnectionWithSslAndCorrectKeys(){ -// System.setProperty("javax.net.ssl.trustStore", "/Users/vishaka.sekar/AppDynamics/client/kafka.client.truststore.jks"); -// System.setProperty("javax.net.ssl.trustStorePassword", "test1234"); -// System.setProperty("javax.net.ssl.keyStore", "/Users/vishaka.sekar/AppDynamics/server/kafka.server.keystore.jks"); -// System.setProperty("javax.net.ssl.keyStorePassword", "test1234"); -// System.setProperty("java.rmi.server.hostname", "10.0.0.106"); -// System.setProperty("com.sun.management.jmxremote.port", "6789"); -// Properties connectionProperties = new Properties(); -// connectionProperties.setProperty("com.sun.management.jmxremote.authenticate", "false"); -// connectionProperties.setProperty("com.sun.management.jmxremote.ssl", "true"); -// connectionProperties.setProperty("com.sun.management.jmxremote.registry.ssl", "true"); -// -// JMXConnectorServer server = sun.management.jmxremote.ConnectorBootstrap -// .startRemoteConnectorServer("6789", connectionProperties); -// } -// -// @Test() -// public void whenUsingSslAndCorrectKeysThenTestServerConnection() throws Exception { -// int port = 6789; -// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://10.0.0.106:6789/jmxrmi"); -// Map env = new HashMap(); -// env.put("com.sun.jndi.rmi.factory.socket",new SslRMIClientSocketFactory()); -// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); -// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory()); -// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); -// } - - @Before - public void setUpConnectionWithIncorrectKeys(){ - System.setProperty("javax.net.ssl.trustStore", "/Users/vishaka.sekar/kafka.test.keystore.jks"); - System.setProperty("javax.net.ssl.trustStorePassword", "test1234"); - System.setProperty("javax.net.ssl.keyStore", "/Users/vishaka.sekar/AppDynamics/server/kafka.server.keystore.jks"); - System.setProperty("javax.net.ssl.keyStorePassword", "test1234"); - Properties props = new Properties(); - props.setProperty("com.sun.management.jmxremote.authenticate", "false"); - props.setProperty("com.sun.management.jmxremote.ssl", "true"); - props.setProperty("com.sun.management.jmxremote.registry.ssl", "false"); - JMXConnectorServer server = sun.management.jmxremote.ConnectorBootstrap - .startRemoteConnectorServer("6789", props); - } - @Test(expected = Exception.class) - public void testSSLServerConnectionWithIncorrectTrustStore() throws Exception { - int port = 6789; - JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:6789/jmxrmi"); - Map env = new HashMap(); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); - env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory()); - JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); - } -} \ No newline at end of file diff --git a/src/test/java/com/appdynamics/extensions/kafka/utils/SSLUtilsTest.java b/src/test/java/com/appdynamics/extensions/kafka/utils/SSLUtilsTest.java new file mode 100644 index 0000000..5e0e0f0 --- /dev/null +++ b/src/test/java/com/appdynamics/extensions/kafka/utils/SSLUtilsTest.java @@ -0,0 +1,168 @@ +/** + * Copyright 2018 AppDynamics, Inc. + * + * 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. + */ + + +package com.appdynamics.extensions.kafka.utils; + +import com.appdynamics.extensions.AMonitorJob; +import com.appdynamics.extensions.conf.MonitorContextConfiguration; +import com.appdynamics.extensions.util.PathResolver; +import com.singularity.ee.agent.systemagent.api.AManagedMonitor; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXConnectorServer; +import javax.management.remote.JMXServiceURL; +import javax.management.remote.rmi.RMIConnectorServer; +import javax.rmi.ssl.SslRMIClientSocketFactory; +import javax.rmi.ssl.SslRMIServerSocketFactory; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +public class SSLUtilsTest { +// +// @Before +// public void setUpConnectionWithoutSSL(){ +// +// Properties props = new Properties(); +// props.setProperty("com.sun.management.jmxremote.authenticate", "false"); +// props.setProperty("com.sun.management.jmxremote.ssl", "false"); +// props.setProperty("com.sun.management.jmxremote.registry.ssl", "false"); +// JMXConnectorServer server = sun.management.jmxremote.ConnectorBootstrap +// .startRemoteConnectorServer("9990", props); +// } +// +// @Test +// public void whenNotUsingSslThenTestServerConnection() throws Exception { +// +// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:9990/jmxrmi"); +// Map env = new HashMap(); +// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); +// Assert.assertNotNull(jmxConnector); +// } + +// @Before +// public void setUpConnectionWithSslAndCorrectKeys(){ +// System.setProperty("javax.net.ssl.keyStore", "/Users/vishaka.sekar/AppDynamics/server/kafka.server.keystore.jks"); +// System.setProperty("javax.net.ssl.keyStorePassword", "test1234"); +// System.setProperty("java.rmi.server.hostname", "10.0.0.106"); +// System.setProperty("com.sun.management.jmxremote.port", "6789"); +// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration +// ("Kafka Monitor", +// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), +// Mockito.mock(AMonitorJob.class)); +// contextConfiguration.setConfigYml("src/test/resources/conf/config_ssl_correct_keys.yml"); +// Map configMap = contextConfiguration.getConfigYml(); +// SslUtils sslUtils = new SslUtils(); +// sslUtils.setSslProperties(configMap); +// Properties connectionProperties = new Properties(); +// connectionProperties.setProperty("com.sun.management.jmxremote.authenticate", "false"); +// connectionProperties.setProperty("com.sun.management.jmxremote.ssl", "true"); +// connectionProperties.setProperty("com.sun.management.jmxremote.registry.ssl", "false"); +// +// JMXConnectorServer server = sun.management.jmxremote.ConnectorBootstrap +// .startRemoteConnectorServer("6789", connectionProperties); +// } +// +// @Test() +// public void whenUsingSslAndCorrectKeysThenTestServerConnection() throws Exception { +// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://10.0.0.106:6789/jmxrmi"); +// Map env = new HashMap(); +// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); +// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory()); +// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); +// Assert.assertNotNull(jmxConnector); +// } + +// @Before +// public void setUpConnectionWithIncorrectKeys(){ +// System.setProperty("javax.net.ssl.keyStore", "/Users/vishaka.sekar/AppDynamics/server/kafka.server.keystore.jks"); +// System.setProperty("javax.net.ssl.keyStorePassword", "test1234"); +// MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration +// ("Kafka Monitor", +// "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), +// Mockito.mock(AMonitorJob.class)); +// contextConfiguration.setConfigYml("src/test/resources/conf/config_ssl_incorrect_keys.yml"); +// Map configMap = contextConfiguration.getConfigYml(); +// SslUtils sslUtils = new SslUtils(); +// sslUtils.setSslProperties(configMap); +// Properties props = new Properties(); +// props.setProperty("com.sun.management.jmxremote.authenticate", "false"); +// props.setProperty("com.sun.management.jmxremote.ssl", "true"); +// JMXConnectorServer server = sun.management.jmxremote.ConnectorBootstrap +// .startRemoteConnectorServer("6789", props); +// } +// +// +// @Test() +// public void testSSLServerConnectionWithIncorrectTrustStore() { +// int port = 6789; +// try { +// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:6789/jmxrmi"); +// Map env = new HashMap(); +// env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); +// env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory()); +// JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); +// } catch (MalformedURLException e) { +// +// } catch (IOException e) { +// Assert.assertEquals( e.getCause().toString(), +// "javax.net.ssl.SSLException: java.lang.RuntimeException: " + +// "Unexpected error: java.security.InvalidAlgorithmParameterException: " + +// "the trustAnchors parameter must be non-empty"); +// } +// } + + @Before + public void setUpConnectionWithSslAndDefaultKeys(){ + System.setProperty("javax.net.ssl.keyStore", "/Users/vishaka.sekar/AppDynamics/server/kafka.server.keystore.jks"); + System.setProperty("javax.net.ssl.keyStorePassword", "test1234"); + System.setProperty("java.rmi.server.hostname", "127.0.0.1"); + System.setProperty("com.sun.management.jmxremote.port", "6789"); + MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration + ("Kafka Monitor", + "Custom Metrics|Kafka|", PathResolver.resolveDirectory(AManagedMonitor.class), + Mockito.mock(AMonitorJob.class)); + contextConfiguration.setConfigYml("src/test/resources/conf/config_ssl_default_keys.yml"); + Map configMap = contextConfiguration.getConfigYml(); + SslUtils sslUtils = new SslUtils(); + sslUtils.setSslProperties(configMap); + Properties connectionProperties = new Properties(); + connectionProperties.setProperty("com.sun.management.jmxremote.authenticate", "false"); + connectionProperties.setProperty("com.sun.management.jmxremote.ssl", "true"); + connectionProperties.setProperty("com.sun.management.jmxremote.registry.ssl", "false"); + + JMXConnectorServer server = sun.management.jmxremote.ConnectorBootstrap + .startRemoteConnectorServer("6789", connectionProperties); + } + + @Test() + public void whenUsingSslAndCorrectKeysThenTestServerConnection() throws Exception { + JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:6789/jmxrmi"); + Map env = new HashMap(); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); + env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory()); + JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); + Assert.assertNotNull(jmxConnector); + } + + +} \ No newline at end of file diff --git a/src/test/resources/conf/config_ssl_default_keys.yml b/src/test/resources/conf/config_ssl_default_keys.yml new file mode 100644 index 0000000..57da731 --- /dev/null +++ b/src/test/resources/conf/config_ssl_default_keys.yml @@ -0,0 +1,27 @@ +#This will populate the metrics in all the tiers, under this path(not recommended) +#metricPrefix: "Custom metrics|Kafka" + +#The following prefix will populate the metrics under respective tiers +metricPrefix: "Server|Component:|Custom Metrics|Kafka" + +# Configure the following connection section ONLY if: +# 1. the config property [useSsl] is set to true in any of the servers in the above section AND +# 2. you want to use your own custom SSL certs +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Please REMOVE/COMMENT OUT the connection section below if: +# 1.You are not using SSL for any of the Kafka servers listed in the server section OR +# 2.You are using SSL for any of the servers([useSsl : true]), but you want to use default truststore of the JRE + + +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#If you are using the connection section, +#any change to the connection section below requires a machine agent restart for the changes to reflect +connection: + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocols: "TLSv1.2" + sslCipherSuites: "" + sslTrustStorePath: "" #if [sslTrustStorePath: ""] empty, it defaults to MA home/conf/cacerts.jks + sslTrustStorePassword: "changeit" # [sslTrustStorePassword: ""] defaults to "" + sslTrustStoreEncryptedPassword: "" #provide encrypted Password if encryption is needed diff --git a/src/test/resources/conf/config_ssl_incorrect_keys.yml b/src/test/resources/conf/config_ssl_incorrect_keys.yml new file mode 100644 index 0000000..c787d89 --- /dev/null +++ b/src/test/resources/conf/config_ssl_incorrect_keys.yml @@ -0,0 +1,27 @@ +#This will populate the metrics in all the tiers, under this path(not recommended) +#metricPrefix: "Custom metrics|Kafka" + +#The following prefix will populate the metrics under respective tiers +metricPrefix: "Server|Component:|Custom Metrics|Kafka" + +# Configure the following connection section ONLY if: +# 1. the config property [useSsl] is set to true in any of the servers in the above section AND +# 2. you want to use your own custom SSL certs +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Please REMOVE/COMMENT OUT the connection section below if: +# 1.You are not using SSL for any of the Kafka servers listed in the server section OR +# 2.You are using SSL for any of the servers([useSsl : true]), but you want to use default truststore of the JRE + + +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#If you are using the connection section, +#any change to the connection section below requires a machine agent restart for the changes to reflect +connection: + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocols: "TLSv1.2" + sslCipherSuites: "" + sslTrustStorePath: "/Users/vishaka.sekar/AppDynamics/server/kafka.server.keystore.jks" #if [sslTrustStorePath: ""] empty, it defaults to MA home/conf/cacerts.jks + sslTrustStorePassword: "test1234" # [sslTrustStorePassword: ""] defaults to "" + sslTrustStoreEncryptedPassword: "" #provide encrypted Password if encryption is needed From dd9e0f77aa1d6dec4c499a6495643ffebd985d34 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Tue, 21 Aug 2018 15:36:31 -0700 Subject: [PATCH 34/49] updating readme, moving vars to Constants --- README.md | 199 +++++++++--------- .../kafka/JMXConnectionAdapter.java | 11 +- .../extensions/kafka/KafkaMonitor.java | 4 +- .../extensions/kafka/KafkaMonitorTask.java | 14 +- .../kafka/metrics/DomainMetricsProcessor.java | 2 +- .../extensions/kafka/utils/Constants.java | 4 + .../extensions/kafka/utils/SslUtils.java | 62 +++--- src/main/resources/conf/config.yml | 16 +- .../extensions/kafka/utils/SSLUtilsTest.java | 1 - 9 files changed, 161 insertions(+), 152 deletions(-) diff --git a/README.md b/README.md index b19d263..4566374 100755 --- a/README.md +++ b/README.md @@ -4,103 +4,117 @@ Kafka Monitoring Extension for AppDynamics ## Use Case ## Apache Kafka® is a distributed, fault-tolerant streaming platform. It can be used to process streams of data in real-time.The Kafka Monitoring extension can be used with a stand alone machine agent to provide metrics for multiple -Apache Kafka . - +Apache Kafka. ## Prerequisites ## -1. This extension extracts the metrics from Kafka using the JMX protocol. - Please ensure JMX is enabled. -2. Please ensure Kafka is up and running and is accessible from the the machine on which machine agent is installed. -3. In order to use this extension, you do need a [Standalone JAVA Machine Agent](https://docs.appdynamics.com/display/PRO44/Standalone+Machine+Agents). - or [SIM Agent](https://docs.appdynamics.com/display/PRO44/Server+Visibility).
- For more details on downloading these products, please visit [Downloads](https://download.appdynamics.com/).
- +- In order to use this extension, you do need a [Standalone JAVA Machine Agent](https://docs.appdynamics.com/display/PRO44/Standalone+Machine+Agents). + or [SIM Agent](https://docs.appdynamics.com/display/PRO44/Server+Visibility).For more details on downloading these products, please visit [Downloads](https://download.appdynamics.com/).
+- You also need a [Kafka](#https://kafka.apache.org/quickstart) server installed. + ## Installation ## - Unzip as "KafkaMonitor" and copy the "KafkaMonitor" directory to `/monitors` - Please place the extension in the `monitors` directory of your Machine Agent installation directory. +Unzip as "KafkaMonitor" and copy the "KafkaMonitor" directory to `/monitors` +Please place the extension in the `monitors` directory of your Machine Agent installation directory. ## Configuration ##### 1. Configuring ports - Test connection to the Kafka host/port from the machine where the extension is installed. + - According to [Oracle's explanation](#https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8035404),JMX opens 3 + different ports:one is the JMX connector port (the one in config.yml),one for the RMIRegistry and, + the third one is an ephemeral port is RMI registry of the local only server. + + We can explicitly configure the first two ports in the Kakfa start-up scripts to avoid it picking random ports(here port 9999 and 9998 are used). + The third one, however, is an ephemeral port(that's how JMX works). + + - Test connection to the Kafka host and ports 9999 and 9998 from the machine where the extension is installed.
``` nc -v KafkaServerIP port ``` - For example, connecting to the localhost on port 9999. - ``` nc -v localhost 9999 ```. -
If you get ```Connection to localhost port 9999 [tcp/distinct] succeeded!```,it confirms the access to the Kafka server. + + #For example, to test connection to the localhost on port 9999, use + nc -v localhost 9999 + + If you get ```Connection to localhost port 9999 [tcp/distinct] succeeded!```,it confirms the access to the Kafka server. ##### 2. Enabling JMX - To enable JMX monitoring for Kafka broker, port 9999 has to be configured to allow monitoring on that port. -
Edit the Kafka start-up script `/bin/kafka-server-start.sh` to include + - To enable JMX monitoring for Kafka broker, a JMX_PORT has to be configured to allow monitoring on that port. +
Edit the Kafka start-up script `/bin/kafka-server-start.sh` to include: - export JMX_PORT=${JMX_PORT:-9999} + #For example, to use port 9999 as the JMX_PORT, use + export JMX_PORT=${JMX_PORT:-9999} - Please note, that the Kafka server needs to be restarted once the JMX port is added. + - Please note, that the Kafka server needs to be restarted once the JMX port is added. -##### 3. Configuring Kafka +##### 3. Configuring Kafka for non-SSL monitoring + + This section outlines the configuration of the Kafka start-up scripts if monitoring is not done over SSL. + If SSL is being used please skip to [Setting up SSL in Kafka](#sslsettings). + - To enable monitoring, some flags need to be set in the Kafka start-up scripts. Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
- KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false" + `KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.rmi.port=9998 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"` - Also, the changes to `kafka-run-class.sh` has to be made on all the Kafka servers that you wish to monitor. - Please note, that any changes to `kafka-run-class.sh` needs the Kafka server to be restarted for the changes to - take effect. + take effect. -##### 4. Setting up SSL and Password Authentication in Kafka### +##### 4. Monitoring over SSL If you need to monitor your Kafka servers securely via SSL, please follow the following steps: - 1. Enable the flags mentioned below: -
Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
- - `KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=true -Djavax.net.ssl.keyStore=/Absolute/path/to/keystore -Djavax.net.ssl.keyStorePassword=password -Dcom.sun.management.jmxremote.registry.ssl=false" - ` + ##### 1. Generating SSL Keys + - Providing a keystore and truststore is mandatory for SSL. The keystore is used by the Kafka Server, the truststore is + is used by the Kafka Monitoring Extension to trust the server. + - The extension supports a custom truststore, and if no truststore is specified,it defaults to the Machine Agent + truststore at `/conf/cacerts.jks`. + - You can create your truststore or choose to use the Machine Agent truststore at `/conf/cacerts.jks`. + - Keytool is a utility that comes with the JDK. Please use the following commands to generate a keystore, and import + the certificates into the truststore. + - If you choose to use the Machine Agent truststore `cacerts.jks`, please follow the steps 1,2 and 3b below to import the certs into `cacerts.jks`. + + #Step #1 + keytool -keystore kafka.server.keystore.jks -alias localhost -validity 365 -genkey + + Step #2 + openssl req -new -x509 -keyout ca-key -out ca-cert -days 365 + + #Step #3a: if you are creating your own truststore + keytool -keystore kafka.client.truststore.jks -alias CARoot -import -file ca-cert + + # Step #3b: or if you are using Machine Agent truststore + keytool -keystore /path/to/MachineAgentHome/conf/cacerts.jks -alias CARoot -import -file ca-cert + + - Additional info about creating SSL keys is listed [here].(https://docs.confluent.io/current/tutorials/security_tutorial.html#creating-ssl-keys-and-certificates) + - 2. The extension also needs to be configured to use SSL. In the config.yml of the Kafka Extension, uncomment the `connection` section. -
- ``` - connection: - socketTimeout: 3000 - connectTimeout: 1000 - sslProtocol: "TLSv1.2" - sslTrustStorePath: "/path/to/truststore/client/kafka.client.truststore.jks" #defaults to conf/cacerts.jks - sslTrustStorePassword: "test1234" # defaults to empty - sslTrustStoreEncryptedPassword: "" - ``` - - 3. If you need username/password authentication, please set the flag
`-Dcom.sun.management.jmxremote.authenticate=true` - in the `KAFKA_JMX_OPTS` variable.Please refer to section [here](#passwordsettings) for further steps. - -##### Generating SSL Keys #### - -To generate your keystore and truststore please follow the steps: -Creating a keystore is mandatory for SSL. However, creating a truststore is optional. You can create -the truststore `kafka.client.truststore.jks`, or you can use the default truststore that comes with the -JRE. - -In case you want to use the default JRE trust store, please replace the `kafka.client.truststore.jks` with -`jre/lib/security/cacerts` - - ``` - #Step #1 - keytool -keystore kafka.server.keystore.jks -alias localhost -validity 365 -genkey - Step #2 - openssl req -new -x509 -keyout ca-key -out ca-cert -days 365 - keytool -keystore kafka.client.truststore.jks -alias CARoot -import -file ca-cert - keytool -keystore kafka.server.truststore.jks -alias CARoot -import -file ca-cert + ##### 2. Configuring Kafka for monitoring over SSL #### + Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
-``` - Additional info about creating SSL keys is listed [here](https://docs.confluent.io/current/tutorials/security_tutorial.html#creating-ssl-keys-and-certificates) + `KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.rmi.port=9998 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=true -Djavax.net.ssl.keyStore=/Absolute/path/to/keystore -Djavax.net.ssl.keyStorePassword=password -Dcom.sun.management.jmxremote.registry.ssl=false"` + + ##### 3. Configuring the extension to use SSL #### + 1. The extension also needs to be configured to use SSL. In the config.yml of the Kafka Extension, uncomment the `connection` section.
+ + connection: + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocol: "TLSv1.2" + sslTrustStorePath: "/path/to/truststore/client/kafka.client.truststore.jks" #defaults to conf/cacerts.jks + sslTrustStorePassword: "test1234" # defaults to empty + sslTrustStoreEncryptedPassword: "" + +
Please note that any changes to the `connection`section of the config.yml, needs the Machine Agent to + be restarted for the changes to take effect. + 2. If you need username/password authentication, please set the flag
`-Dcom.sun.management.jmxremote.authenticate=true` + in the `KAFKA_JMX_OPTS` variable.Please refer to [Password Settings](#passwordsettings) for further steps. -###### Password Settings +##### 5. Password Settings If you need password authentication, you need to set-up the password in the JVM of the Kafka server -To know more on how to set the credentials, please see section `Using Password and Access Files` in [this](https://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html). +To know more on how to set the credentials, please see section `Using Password and Access Files` in [this link](https://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html). -###### Config.yml +##### 6. Config.yml Configure the Kafka monitoring extension by editing the config.yml file in `/monitors/KafkaMonitor/ 1. Configure the "tier" under which the metrics need to be reported. This can be done by changing the value of `` in @@ -113,9 +127,9 @@ Configure the Kafka monitoring extension by editing the config.yml file in `either `serviceUrl` or `` of all Kafka servers. - - Here, `host` is the IP address - of the Kafka server to be monitored, and `port` is the JMX port of the Kafka server. - - Please provide `username` & `password` (only if authentication enabled) + - Here, `host` is the IP address. + of the Kafka server to be monitored, and `port` is the JMX port of the Kafka server. + - Please provide `username` & `password` (only if authentication enabled). - `encryptedPassword`(only if password encryption required). - If you are using SSL to securely monitor your Kafka servers, please set `useSsl` as `true`. @@ -137,10 +151,9 @@ Configure the Kafka monitoring extension by editing the config.yml file in `ANY Kafka server(s), AND you want to use your own custom - truststore. - Please remove this section if you are using SSL, but yu want to use the default JRE truststore. - Please also remove this section if you are not using SSL for any of your servers. + 4. Configure the connection section only if you are using monitoring over SSL for ANY of Kafka server(s). + - Please remove this section if you are not using SSL for any of your servers. + - If you are using the Machine Agent Truststore, please leave the `sslTrustStorePath` as `""`. ``` connection: @@ -161,9 +174,8 @@ Configure the Kafka monitoring extension by editing the config.yml file in ` + For configuring the metrics, the following properties can be used: | Metric Property | Default value | Possible values | Description | | :---------------- | :-------------- | :------------------------------ | :------------------------------------------------------------------------------------------------------------- | @@ -175,13 +187,11 @@ Configure the Kafka monitoring extension by editing the config.yml file in ` `objectName: kafka.server:type=ReplicaManager,name=IsrExpandsPerSec` will return only those metrics corresponding to the `IsrExpandsPerSec` object. - ###### Validating config.yml: - Please copy all the contents of the config.yml file and go to [YamlLint](http://www.yamllint.com/).
- On reaching the website, paste the contents and press the “Go” button on the bottom left.
- If you get a valid output, that means your formatting is correct and you may move on to the next step. + ##### 7. Validating config.yml: +Please copy all the contents of the config.yml file and go to [YamlLint](http://www.yamllint.com/).
+On reaching the website, paste the contents and press the “Go” button on the bottom left.
+If you get a valid output, that means your formatting is correct and you may move on to the next step. -## Metrics +##### 8. Metrics This extension collects metrics via JMX and can be configured to report any of the metrics that Kafka exposes. It provides metrics on Kafka server, controller and the network. @@ -237,6 +247,7 @@ before you actually deploy it on the controller. Please review the following how to use the Extensions WorkBench ## Troubleshooting + Please follow the steps listed in the [extensions troubleshooting document](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) in order to troubleshoot your issue. These are a set of common issues that customers might have faced during the installation of the extension. If these don't solve your issue, please follow the last step on the troubleshooting-document to contact the support team. @@ -254,7 +265,7 @@ Please provide the following in order for us to assist you better.

 4. Start the machine agent and please let it run for 10 mins. Then zip and upload all the logs in the directory /logs/*. 5. Attach the zipped /conf/* directory here. -
6. Attach the zipped /monitors/ directory here
. +6. Attach the zipped /monitors/ directory here
. For any support related questions, you can also contact help@appdynamics.com. @@ -262,13 +273,11 @@ For any support related questions, you can also contact help@appdynamics.com. Always feel free to fork and contribute any changes directly via [GitHub](https://github.com/Appdynamics/kafka-monitoring-extension). ## Version - |______Name___________________|_______Version_____| - | Extension Version: | 2.0.0 | - | Controller Compatibility: | 4.0 or Later | - | Tested On: | Apache Kafka 2.11 | - | Operating System Tested On: | Mac OS | - | Last updated On: | Aug 08, 2018 | - | List of changes | [Change log](https://github.com/Appdynamics/kafka-monitoring-extension/blob/master/Changelog.md) - | to this extension | | - | ____________________________|___________________| - +| Name | Version | +| :---------------------------| :---------------------------| +| Extension Version: | 2.0.0 | +| Controller Compatibility: | 4.0 or Later | +| Tested On: | Apache Kafka 2.11 | +| Operating System Tested On: | Mac OS | +| Last updated On: | Aug 21, 2018 | +| List of changes to this extension| [Change log](#https://github.com/Appdynamics/kafka-monitoring-extension/blob/master/Changelog.md) diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index db8f15e..c3e01fb 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -15,12 +15,10 @@ */ package com.appdynamics.extensions.kafka; - import com.appdynamics.extensions.kafka.utils.Constants; import com.google.common.base.Strings; import com.google.common.collect.Lists; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.sun.tools.internal.jxc.ap.Const; import javax.management.*; import javax.management.remote.JMXConnector; @@ -40,14 +38,13 @@ public class JMXConnectionAdapter { private final JMXServiceURL serviceUrl; private final String username; private final String password; - private static final Logger logger = LoggerFactory.getLogger(JMXConnectionAdapter.class); private JMXConnectionAdapter(Map requestMap) throws MalformedURLException { if(!Strings.isNullOrEmpty(requestMap.get(Constants.SERVICE_URL))) this.serviceUrl = new JMXServiceURL(requestMap.get(Constants.SERVICE_URL)); else - this.serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + - requestMap.get(Constants.HOST) + ":" + requestMap.get(Constants.PORT) + "/jmxrmi"); + { this.serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + + requestMap.get(Constants.HOST) + ":" + requestMap.get(Constants.PORT) + "/jmxrmi");} this.username = requestMap.get(Constants.USERNAME); this.password = requestMap.get(Constants.PASSWORD); } @@ -60,7 +57,7 @@ JMXConnector open(Map connectionMap) throws IOException { JMXConnector jmxConnector; final Map env = new HashMap<>(); - if(Boolean.valueOf(connectionMap.get("useSsl").toString())) { + if(Boolean.valueOf(connectionMap.get(Constants.USE_SSL).toString())) { SslRMIClientSocketFactory sslRMIClientSocketFactory = new SslRMIClientSocketFactory(); env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, sslRMIClientSocketFactory); } diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index f357699..1f70b06 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -30,12 +30,10 @@ import static com.appdynamics.extensions.kafka.utils.Constants.DEFAULT_METRIC_PREFIX; public class KafkaMonitor extends ABaseMonitor { - private static final Logger logger = LoggerFactory.getLogger(KafkaMonitor.class); @Override protected void onConfigReload(File file) { - Map configMap = this.getContextConfiguration() - .getConfigYml(); + Map configMap = this.getContextConfiguration().getConfigYml(); SslUtils sslUtils = new SslUtils(); sslUtils.setSslProperties(configMap); } diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java index e12d0c6..8a14582 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitorTask.java @@ -99,15 +99,15 @@ private BigDecimal openJMXConnection() { Map requestMap = buildRequestMap(); jmxAdapter = JMXConnectionAdapter.create(requestMap); Map connectionMap =(Map) getConnectionParameters(); - connectionMap.put("useSsl", this.kafkaServer.get("useSsl") ); - logger.debug("[useSsl] is set [{}] for server [{}]", connectionMap.get("useSsl"), + connectionMap.put(Constants.USE_SSL, this.kafkaServer.get(Constants.USE_SSL) ); + logger.debug("[useSsl] is set [{}] for server [{}]", connectionMap.get(Constants.USE_SSL), this.kafkaServer.get(Constants.DISPLAY_NAME)); - if(configuration.getConfigYml().containsKey("encryptionKey") && - !Strings.isNullOrEmpty( configuration.getConfigYml().get("encryptionKey").toString()) ) { - connectionMap.put("encryptionKey",configuration.getConfigYml().get("encryptionKey").toString()); + if(configuration.getConfigYml().containsKey(Constants.ENCRYPTION_KEY) && + !Strings.isNullOrEmpty( configuration.getConfigYml().get(Constants.ENCRYPTION_KEY).toString()) ) { + connectionMap.put(Constants.ENCRYPTION_KEY,configuration.getConfigYml().get(Constants.ENCRYPTION_KEY).toString()); } - else { connectionMap.put("encryptionKey" ,""); } + else { connectionMap.put(Constants.ENCRYPTION_KEY ,""); } jmxConnector = jmxAdapter.open(connectionMap); if(jmxConnector != null) { @@ -153,7 +153,7 @@ private String getPassword() { } private Map getConnectionParameters(){ - if(configuration.getConfigYml().containsKey("connection")) + if(configuration.getConfigYml().containsKey(Constants.CONNECTION)) return (Map) configuration.getConfigYml().get(Constants.CONNECTION); else return new HashMap<>(); diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index 1b5d99a..213c977 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -24,6 +24,7 @@ import com.appdynamics.extensions.metrics.Metric; import com.google.common.base.Strings; import org.slf4j.LoggerFactory; + import javax.management.*; import javax.management.openmbean.CompositeData; import javax.management.openmbean.CompositeDataSupport; @@ -147,5 +148,4 @@ private String buildName (ObjectInstance instance) { } return sb.toString(); } - } diff --git a/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java index c330ee5..7585291 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java +++ b/src/main/java/com/appdynamics/extensions/kafka/utils/Constants.java @@ -41,4 +41,8 @@ public class Constants { public static final String TRUST_STORE_PATH = "sslTrustStorePath"; public static final String TRUST_STORE_PASSWORD = "sslTrustStorePassword"; + + public static final String TRUST_STORE_ENCRYPTED_PASSWORD = "sslTrustStoreEncryptedPassword"; + + public static final String USE_SSL = "useSsl"; } diff --git a/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java b/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java index 327fe9f..da36fb3 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java +++ b/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java @@ -1,10 +1,8 @@ package com.appdynamics.extensions.kafka.utils; import com.appdynamics.extensions.crypto.Decryptor; -import com.appdynamics.extensions.kafka.KafkaMonitor; import com.appdynamics.extensions.util.PathResolver; import com.google.common.base.Strings; -import com.singularity.ee.agent.systemagent.api.AManagedMonitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,49 +14,57 @@ public class SslUtils { public void setSslProperties(Map configMap){ - //if the config yaml contains the field sslTrustStorePath then the keys are set - // if the field is not present, it defaults to /conf/ - // any change in config.yml parameters requires a MA restart + // if connection section is present in the config.yml --> means SSL is required for atleast 1 server + // setting SSL params only if connection is present in config.yml + // else skipping this step if(configMap.containsKey(Constants.CONNECTION)) { Map connectionMap = (Map) configMap.get(Constants.CONNECTION); + + //if sslTrustStorePath is not empty -->then use custom truststore if (connectionMap.containsKey(Constants.TRUST_STORE_PATH) && !Strings.isNullOrEmpty(connectionMap.get(Constants.TRUST_STORE_PATH).toString())) { - String sslTrustStorePath = connectionMap.get(Constants.TRUST_STORE_PATH).toString(); - File customSslTrustStoreFile = new File(sslTrustStorePath); - if(customSslTrustStoreFile == null || !customSslTrustStoreFile.exists() ) { - logger.debug("The file [{}] doesn't exist", customSslTrustStoreFile.getAbsolutePath()); - } - else { - logger.debug("Using custom SSL truststore [{}]", sslTrustStorePath); - System.setProperty("javax.net.ssl.trustStore", customSslTrustStoreFile.getAbsolutePath()); - } + + String sslTrustStorePath = connectionMap.get(Constants.TRUST_STORE_PATH).toString(); + File customSslTrustStoreFile = new File(sslTrustStorePath); + if(customSslTrustStoreFile == null || !customSslTrustStoreFile.exists() ) { + logger.debug("The file [{}] doesn't exist", customSslTrustStoreFile.getAbsolutePath()); + } + else { + logger.debug("Using custom SSL truststore [{}]", sslTrustStorePath); + System.setProperty("javax.net.ssl.trustStore", customSslTrustStoreFile.getAbsolutePath()); + } } + + //if sslTrustStorePath is empty ---> then use MA certs at /conf/cacerts.jks else if (connectionMap.containsKey(Constants.TRUST_STORE_PATH) && Strings.isNullOrEmpty(connectionMap.get(Constants.TRUST_STORE_PATH).toString())){ - //getting path of machine agent home - // File installDir = PathResolver.resolveDirectory(AManagedMonitor.class); - File installDir = new File("/Users/vishaka.sekar/AppDynamics/machineagent/");//for test only todo:remove before publishing - File defaultTrustStoreFile = PathResolver.getFile("conf/cacerts.jks", installDir); - if (defaultTrustStoreFile == null || !defaultTrustStoreFile.exists()) { - logger.debug("The file [{}] doesn't exist", installDir+"/conf/cacerts.jks"); - } - else { - logger.debug("Using Machine Agent truststore {}", installDir+"conf/cacerts.jks"); - System.setProperty("javax.net.ssl.trustStore", defaultTrustStoreFile.getAbsolutePath()); - } + //getting path of machine agent home + // File installDir = PathResolver.resolveDirectory(AManagedMonitor.class); + File installDir = new File("/Users/vishaka.sekar/AppDynamics/machineagent");//for test only todo:remove before publishing + File defaultTrustStoreFile = PathResolver.getFile("/conf/cacerts.jks", installDir); + if (defaultTrustStoreFile == null || !defaultTrustStoreFile.exists()) { + logger.debug("The file [{}] doesn't exist", installDir+"/conf/cacerts.jks"); + } + + else { + logger.debug("Using Machine Agent truststore {}", installDir+"/conf/cacerts.jks"); + System.setProperty("javax.net.ssl.trustStore", defaultTrustStoreFile.getAbsolutePath()); + } } + //in both cases, sslTrustStorePassword has to be specified in config.yml System.setProperty("javax.net.ssl.trustStorePassword", getSslTrustStorePassword(connectionMap, configMap)); + } } private String getSslTrustStorePassword(Map connectionMap, Map config) { - String password = (String) connectionMap.get("sslTrustStorePassword"); + String password = (String) connectionMap.get(Constants.TRUST_STORE_PASSWORD); if (!Strings.isNullOrEmpty(password)) { return password; } else { - String encrypted = (String) connectionMap.get("sslTrustStoreEncryptedPassword"); + String encrypted = (String) connectionMap.get(Constants.TRUST_STORE_ENCRYPTED_PASSWORD); if (!Strings.isNullOrEmpty(encrypted)) { - String encryptionKey = (String) config.get("encryptionKey"); + String encryptionKey = (String) config.get(Constants.ENCRYPTION_KEY); if (!Strings.isNullOrEmpty(encryptionKey)) { return new Decryptor(encryptionKey).decrypt(encrypted); } else { diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index 4989dfe..202c8ef 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -10,7 +10,7 @@ metricPrefix: "Server|Component:|Custom Metrics|Kafka" # Add your Kafka Instances below servers: - - serviceUrl: "service:jmx:rmi:///jndi/rmi://127.0.0.1:9999/jmxrmi" #provide service URL [OR] provide [host][port] pair + - serviceUrl: "service:jmx:rmi:///jndi/rmi://10.0.66.179:9999/jmxrmi" #provide jmx service URL [OR] provide [host][port] pair host: "" port: "" username: "" @@ -25,14 +25,10 @@ encryptionKey: "" # Configure the following connection section ONLY if: -# 1. the config property [useSsl] is set to true in any of the servers in the above section AND -# 2. you want to use your own custom SSL certs +# the config property [useSsl] is set to true in any of the servers in the 'servers' section # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Please REMOVE/COMMENT OUT the connection section below if: -# 1.You are not using SSL for any of the Kafka servers listed in the server section OR -# 2.You are using SSL for any of the servers([useSsl : true]), but you want to use default truststore of the JRE - - +# SSL is not being used for ANY of the Kafka servers listed in the 'servers' section #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #If you are using the connection section, @@ -42,12 +38,12 @@ connection: connectTimeout: 1000 sslProtocols: "TLSv1.2" sslCipherSuites: "" - sslTrustStorePath: "" #if [sslTrustStorePath: ""] empty, it defaults to MA home/conf/cacerts.jks - sslTrustStorePassword: "test1234" # [sslTrustStorePassword: ""] defaults to "" + sslTrustStorePath: "" #if sslTrustStorePath: "" empty, it defaults to /conf/cacerts.jks + sslTrustStorePassword: "changeit" # [sslTrustStorePassword: ""] defaults to "" sslTrustStoreEncryptedPassword: "" #provide encrypted Password if encryption is needed # Each Kafka server needs 1 thread each, so please configure this according to the number of servers you are monitoring -# [numberOfThreads] = numberOfKafkaServers +# [numberOfThreads] = Number_of_Kafka_Servers_Monitored numberOfThreads: 10 # The configuration of different metrics from all mbeans exposed by Kafka server diff --git a/src/test/java/com/appdynamics/extensions/kafka/utils/SSLUtilsTest.java b/src/test/java/com/appdynamics/extensions/kafka/utils/SSLUtilsTest.java index 5e0e0f0..dd2ea06 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/utils/SSLUtilsTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/utils/SSLUtilsTest.java @@ -159,7 +159,6 @@ public void whenUsingSslAndCorrectKeysThenTestServerConnection() throws Exceptio JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:6789/jmxrmi"); Map env = new HashMap(); env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); - env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory()); JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); Assert.assertNotNull(jmxConnector); } From 65b224928985d7c693cba5e3d014e41bdde471d5 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Tue, 21 Aug 2018 15:46:31 -0700 Subject: [PATCH 35/49] updating readme --- README.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4566374..03d96b5 100755 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Apache Kafka. - In order to use this extension, you do need a [Standalone JAVA Machine Agent](https://docs.appdynamics.com/display/PRO44/Standalone+Machine+Agents). or [SIM Agent](https://docs.appdynamics.com/display/PRO44/Server+Visibility).For more details on downloading these products, please visit [Downloads](https://download.appdynamics.com/).
-- You also need a [Kafka](#https://kafka.apache.org/quickstart) server installed. +- You also need a [Kafka](https://kafka.apache.org/quickstart) server installed. ## Installation ## @@ -21,7 +21,7 @@ Please place the extension in the `monitors` directory of your Machine Ag ## Configuration ##### 1. Configuring ports - - According to [Oracle's explanation](#https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8035404),JMX opens 3 + - According to [Oracle's explanation](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8035404),JMX opens 3 different ports:one is the JMX connector port (the one in config.yml),one for the RMIRegistry and, the third one is an ephemeral port is RMI registry of the local only server. @@ -85,7 +85,7 @@ Please place the extension in the `monitors` directory of your Machine Ag # Step #3b: or if you are using Machine Agent truststore keytool -keystore /path/to/MachineAgentHome/conf/cacerts.jks -alias CARoot -import -file ca-cert - - Additional info about creating SSL keys is listed [here].(https://docs.confluent.io/current/tutorials/security_tutorial.html#creating-ssl-keys-and-certificates) + - Additional info about creating SSL keys is listed [here](https://docs.confluent.io/current/tutorials/security_tutorial.html#creating-ssl-keys-and-certificates). ##### 2. Configuring Kafka for monitoring over SSL #### @@ -115,12 +115,10 @@ If you need password authentication, you need to set-up the password in the JVM To know more on how to set the credentials, please see section `Using Password and Access Files` in [this link](https://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html). ##### 6. Config.yml -Configure the Kafka monitoring extension by editing the config.yml file in `/monitors/KafkaMonitor/ +Configure the Kafka monitoring extension by editing the config.yml file in `/monitors/KafkaMonitor/` 1. Configure the "tier" under which the metrics need to be reported. This can be done by changing the value of `` in -
`metricPrefix: "Server|Component:|Custom Metrics|Kafka"`.
- Please refer - [How to find Component ID](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) of your tiers. + `metricPrefix: "Server|Component:|Custom Metrics|Kafka"`.
Please refer this [link](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) to find Component-ID of your tiers. For example, ``` @@ -280,4 +278,4 @@ Always feel free to fork and contribute any changes directly via [GitHub](https: | Tested On: | Apache Kafka 2.11 | | Operating System Tested On: | Mac OS | | Last updated On: | Aug 21, 2018 | -| List of changes to this extension| [Change log](#https://github.com/Appdynamics/kafka-monitoring-extension/blob/master/Changelog.md) +| List of changes to this extension| [Change log](https://github.com/Appdynamics/kafka-monitoring-extension/blob/master/Changelog.md) From 3bbec09109c38c7a9f082ce8528ca1e84185ef16 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Thu, 23 Aug 2018 11:01:47 -0700 Subject: [PATCH 36/49] CR 3 changes --- README.md | 5 +- .../kafka/JMXConnectionAdapter.java | 2 +- .../extensions/kafka/KafkaMonitor.java | 23 ------- .../kafka/metrics/DomainMetricsProcessor.java | 2 +- .../extensions/kafka/utils/SslUtils.java | 69 +++++++++++-------- src/main/resources/conf/config.yml | 2 +- .../extensions/kafka/utils/SSLUtilsTest.java | 24 ++++--- .../conf/config_ssl_incorrect_keys.yml | 2 +- 8 files changed, 61 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 03d96b5..aefe168 100755 --- a/README.md +++ b/README.md @@ -76,13 +76,13 @@ Please place the extension in the `monitors` directory of your Machine Ag #Step #1 keytool -keystore kafka.server.keystore.jks -alias localhost -validity 365 -genkey - Step #2 + #Step #2 openssl req -new -x509 -keyout ca-key -out ca-cert -days 365 #Step #3a: if you are creating your own truststore keytool -keystore kafka.client.truststore.jks -alias CARoot -import -file ca-cert - # Step #3b: or if you are using Machine Agent truststore + #Step #3b: or if you are using Machine Agent truststore keytool -keystore /path/to/MachineAgentHome/conf/cacerts.jks -alias CARoot -import -file ca-cert - Additional info about creating SSL keys is listed [here](https://docs.confluent.io/current/tutorials/security_tutorial.html#creating-ssl-keys-and-certificates). @@ -245,7 +245,6 @@ before you actually deploy it on the controller. Please review the following how to use the Extensions WorkBench ## Troubleshooting - Please follow the steps listed in the [extensions troubleshooting document](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) in order to troubleshoot your issue. These are a set of common issues that customers might have faced during the installation of the extension. If these don't solve your issue, please follow the last step on the troubleshooting-document to contact the support team. diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index c3e01fb..32bd8ab 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -18,7 +18,7 @@ import com.appdynamics.extensions.kafka.utils.Constants; import com.google.common.base.Strings; import com.google.common.collect.Lists; -import com.sun.tools.internal.jxc.ap.Const; + import javax.management.*; import javax.management.remote.JMXConnector; diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index 1f70b06..7f8bf5c 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -14,16 +14,8 @@ import com.appdynamics.extensions.kafka.utils.Constants; import com.appdynamics.extensions.kafka.utils.SslUtils; import com.appdynamics.extensions.util.AssertUtils; -import com.singularity.ee.agent.systemagent.api.exception.TaskExecutionException; -import org.apache.log4j.ConsoleAppender; -import org.apache.log4j.Level; -import org.apache.log4j.PatternLayout; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.File; -import java.io.OutputStreamWriter; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -62,19 +54,4 @@ protected int getTaskCount() { AssertUtils.assertNotNull(servers, "The 'servers' section in config.yml is not initialised"); return servers.size(); } - - //TODO: remove before publishing - public static void main(String[] args) throws TaskExecutionException { - ConsoleAppender ca = new ConsoleAppender(); - ca.setWriter(new OutputStreamWriter(System.out)); - ca.setLayout(new PatternLayout("%-5p [%t]: %m%n")); - ca.setThreshold(Level.DEBUG); - org.apache.log4j.Logger.getRootLogger().addAppender(ca); - - KafkaMonitor monitor = new KafkaMonitor(); - Map taskArgs = new HashMap(); - taskArgs.put("config-file", "/Users/vishaka.sekar/AppDynamics/kafka-monitoring-extension/src/main/resources/conf/config.yml"); - monitor.execute(taskArgs, null); - } - } \ No newline at end of file diff --git a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java index 213c977..508b7c8 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/metrics/DomainMetricsProcessor.java @@ -95,7 +95,7 @@ private void collect (List attributes, ObjectInstance instance, String key = attribute.getName()+ "."+ str; Object attributeValue = ((CompositeDataSupport) attribute.getValue()).get(str); if(metricProperties.containsKey(key)){ - setMetricDetails(metricPrefix, key, attributeValue.toString(), instance, + setMetricDetails(metricPrefix, key, attributeValue, instance, (Map)metricProperties.get(key)); } } diff --git a/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java b/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java index da36fb3..d9d0112 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java +++ b/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java @@ -1,59 +1,72 @@ package com.appdynamics.extensions.kafka.utils; import com.appdynamics.extensions.crypto.Decryptor; +import com.appdynamics.extensions.util.AssertUtils; import com.appdynamics.extensions.util.PathResolver; import com.google.common.base.Strings; +import com.singularity.ee.agent.systemagent.api.AManagedMonitor; +import com.sun.tools.javac.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import java.io.File; import java.util.Map; public class SslUtils { private static final Logger logger = LoggerFactory.getLogger(SslUtils.class); - public void setSslProperties(Map configMap){ + /** + *This method executes every time config file is changed. + * sets SSL params only if [connection] is present in config.yml + * if [connection] section is present in the config.yml --> means SSL is required for atleast 1 server + * if [sslTrustStorePath] is empty in the config.yml ---> then use MA certs at /conf/cacerts.jks + * if [sslTrustStorePath] is not empty -->then use custom truststore path specified in the config.yml + * [sslTrustStorePath] cannot be null + * in both cases, sslTrustStorePassword has to be specified in config.yml + */ + public void setSslProperties(Map configMap) { - // if connection section is present in the config.yml --> means SSL is required for atleast 1 server - // setting SSL params only if connection is present in config.yml - // else skipping this step - if(configMap.containsKey(Constants.CONNECTION)) { + if (configMap.containsKey(Constants.CONNECTION)) { Map connectionMap = (Map) configMap.get(Constants.CONNECTION); - //if sslTrustStorePath is not empty -->then use custom truststore - if (connectionMap.containsKey(Constants.TRUST_STORE_PATH) && - !Strings.isNullOrEmpty(connectionMap.get(Constants.TRUST_STORE_PATH).toString())) { + if (connectionMap.containsKey(Constants.TRUST_STORE_PATH)){ + Assert.checkNonNull(connectionMap.get(Constants.TRUST_STORE_PATH), "[sslTrustStorePath] cannot be null"); + if(!(connectionMap.get(Constants.TRUST_STORE_PATH).toString()).isEmpty()) { String sslTrustStorePath = connectionMap.get(Constants.TRUST_STORE_PATH).toString(); File customSslTrustStoreFile = new File(sslTrustStorePath); - if(customSslTrustStoreFile == null || !customSslTrustStoreFile.exists() ) { + if (customSslTrustStoreFile == null || !customSslTrustStoreFile.exists()) { logger.debug("The file [{}] doesn't exist", customSslTrustStoreFile.getAbsolutePath()); - } - else { - logger.debug("Using custom SSL truststore [{}]", sslTrustStorePath); + } else { + + logger.debug("Using custom SSL truststore [{}] ", sslTrustStorePath); + logger.debug("Setting SystemProperty [javax.net.ssl.trustStore] ", customSslTrustStoreFile.getAbsolutePath()); System.setProperty("javax.net.ssl.trustStore", customSslTrustStoreFile.getAbsolutePath()); } - } + } - //if sslTrustStorePath is empty ---> then use MA certs at /conf/cacerts.jks - else if (connectionMap.containsKey(Constants.TRUST_STORE_PATH) && - Strings.isNullOrEmpty(connectionMap.get(Constants.TRUST_STORE_PATH).toString())){ - //getting path of machine agent home - // File installDir = PathResolver.resolveDirectory(AManagedMonitor.class); - File installDir = new File("/Users/vishaka.sekar/AppDynamics/machineagent");//for test only todo:remove before publishing + else if ((connectionMap.get(Constants.TRUST_STORE_PATH).toString()).isEmpty()) { + File installDir = PathResolver.resolveDirectory(AManagedMonitor.class); + //while debugging please comment out the above line + //and add + //File installDir = new File("/path/to/machineagent-home"); + //for example, + //File installDir = new File("/Users/vishaka.sekar/AppDynamics/machineagent"); File defaultTrustStoreFile = PathResolver.getFile("/conf/cacerts.jks", installDir); - if (defaultTrustStoreFile == null || !defaultTrustStoreFile.exists()) { - logger.debug("The file [{}] doesn't exist", installDir+"/conf/cacerts.jks"); - } - - else { - logger.debug("Using Machine Agent truststore {}", installDir+"/conf/cacerts.jks"); + if (defaultTrustStoreFile == null || !defaultTrustStoreFile.exists()) { + logger.debug("The file [{}] doesn't exist", installDir + "/conf/cacerts.jks"); + } else { + logger.debug("Using Machine Agent truststore {}", installDir + "/conf/cacerts.jks"); + logger.debug("Setting SystemProperty [javax.net.ssl.trustStore] ",defaultTrustStoreFile.getAbsolutePath()); System.setProperty("javax.net.ssl.trustStore", defaultTrustStoreFile.getAbsolutePath()); } - } - //in both cases, sslTrustStorePassword has to be specified in config.yml + } System.setProperty("javax.net.ssl.trustStorePassword", getSslTrustStorePassword(connectionMap, configMap)); + } + } + + else if(!configMap.containsKey(Constants.CONNECTION)){ + logger.debug("[connection] section is not present in the config.yml"); } } diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index 202c8ef..8ed740b 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -162,7 +162,7 @@ mbeans: - Count: alias: "Count" multiplier: "" - delta: false + delta: true aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" diff --git a/src/test/java/com/appdynamics/extensions/kafka/utils/SSLUtilsTest.java b/src/test/java/com/appdynamics/extensions/kafka/utils/SSLUtilsTest.java index dd2ea06..14d609f 100644 --- a/src/test/java/com/appdynamics/extensions/kafka/utils/SSLUtilsTest.java +++ b/src/test/java/com/appdynamics/extensions/kafka/utils/SSLUtilsTest.java @@ -33,12 +33,14 @@ import javax.management.remote.rmi.RMIConnectorServer; import javax.rmi.ssl.SslRMIClientSocketFactory; import javax.rmi.ssl.SslRMIServerSocketFactory; +import java.io.IOException; +import java.net.MalformedURLException; import java.util.HashMap; import java.util.Map; import java.util.Properties; public class SSLUtilsTest { -// + // @Before // public void setUpConnectionWithoutSSL(){ // @@ -57,13 +59,14 @@ public class SSLUtilsTest { // Map env = new HashMap(); // JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); // Assert.assertNotNull(jmxConnector); +// // } // @Before // public void setUpConnectionWithSslAndCorrectKeys(){ -// System.setProperty("javax.net.ssl.keyStore", "/Users/vishaka.sekar/AppDynamics/server/kafka.server.keystore.jks"); +// System.setProperty("javax.net.ssl.keyStore", "src/test/resources/keystore/kafka.server.keystore.jks"); // System.setProperty("javax.net.ssl.keyStorePassword", "test1234"); -// System.setProperty("java.rmi.server.hostname", "10.0.0.106"); +// System.setProperty("java.rmi.server.hostname", "127.0.0.1"); // System.setProperty("com.sun.management.jmxremote.port", "6789"); // MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration // ("Kafka Monitor", @@ -82,19 +85,19 @@ public class SSLUtilsTest { // .startRemoteConnectorServer("6789", connectionProperties); // } // -// @Test() +// @Test // public void whenUsingSslAndCorrectKeysThenTestServerConnection() throws Exception { -// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://10.0.0.106:6789/jmxrmi"); +// JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:6789/jmxrmi"); // Map env = new HashMap(); // env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); // env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory()); // JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, env); // Assert.assertNotNull(jmxConnector); // } - +// // @Before // public void setUpConnectionWithIncorrectKeys(){ -// System.setProperty("javax.net.ssl.keyStore", "/Users/vishaka.sekar/AppDynamics/server/kafka.server.keystore.jks"); +// System.setProperty("javax.net.ssl.keyStore", "src/test/resources/keystore/kafka.server.keystore.jks"); // System.setProperty("javax.net.ssl.keyStorePassword", "test1234"); // MonitorContextConfiguration contextConfiguration = new MonitorContextConfiguration // ("Kafka Monitor", @@ -112,7 +115,7 @@ public class SSLUtilsTest { // } // // -// @Test() +// @Test // public void testSSLServerConnectionWithIncorrectTrustStore() { // int port = 6789; // try { @@ -131,9 +134,10 @@ public class SSLUtilsTest { // } // } + @Before public void setUpConnectionWithSslAndDefaultKeys(){ - System.setProperty("javax.net.ssl.keyStore", "/Users/vishaka.sekar/AppDynamics/server/kafka.server.keystore.jks"); + System.setProperty("javax.net.ssl.keyStore", "src/test/resources/keystore/kafka.server.keystore.jks"); System.setProperty("javax.net.ssl.keyStorePassword", "test1234"); System.setProperty("java.rmi.server.hostname", "127.0.0.1"); System.setProperty("com.sun.management.jmxremote.port", "6789"); @@ -154,7 +158,7 @@ public void setUpConnectionWithSslAndDefaultKeys(){ .startRemoteConnectorServer("6789", connectionProperties); } - @Test() + @Test public void whenUsingSslAndCorrectKeysThenTestServerConnection() throws Exception { JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:6789/jmxrmi"); Map env = new HashMap(); diff --git a/src/test/resources/conf/config_ssl_incorrect_keys.yml b/src/test/resources/conf/config_ssl_incorrect_keys.yml index c787d89..0584ec9 100644 --- a/src/test/resources/conf/config_ssl_incorrect_keys.yml +++ b/src/test/resources/conf/config_ssl_incorrect_keys.yml @@ -22,6 +22,6 @@ connection: connectTimeout: 1000 sslProtocols: "TLSv1.2" sslCipherSuites: "" - sslTrustStorePath: "/Users/vishaka.sekar/AppDynamics/server/kafka.server.keystore.jks" #if [sslTrustStorePath: ""] empty, it defaults to MA home/conf/cacerts.jks + sslTrustStorePath: "src/test/resources/keystore/kafka.server.keystore.jks" #if [sslTrustStorePath: ""] empty, it defaults to MA home/conf/cacerts.jks sslTrustStorePassword: "test1234" # [sslTrustStorePassword: ""] defaults to "" sslTrustStoreEncryptedPassword: "" #provide encrypted Password if encryption is needed From f0e503e37f8a9ecf2a4282b69de75be4a009470b Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Thu, 23 Aug 2018 11:04:12 -0700 Subject: [PATCH 37/49] sample keys for test --- CHANGELOG.md | 10 ++++++ .../conf/config_ssl_correct_keys.yml | 34 ++++++++++++++++++ .../keystore/kafka.client.truststore.jks | Bin 0 -> 3795 bytes .../keystore/kafka.server.keystore.jks | Bin 0 -> 3656 bytes 4 files changed, 44 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 src/test/resources/conf/config_ssl_correct_keys.yml create mode 100644 src/test/resources/keystore/kafka.client.truststore.jks create mode 100644 src/test/resources/keystore/kafka.server.keystore.jks diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f5031a2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# AppDynamics Kafka Monitoring Extension CHANGELOG + +## 2.0.0 - Aug 8, 2018 +1. Moved to 2.0 framework. +2. Added support for SSL +3. Added support for composite metrics + + + + diff --git a/src/test/resources/conf/config_ssl_correct_keys.yml b/src/test/resources/conf/config_ssl_correct_keys.yml new file mode 100644 index 0000000..ead9f0b --- /dev/null +++ b/src/test/resources/conf/config_ssl_correct_keys.yml @@ -0,0 +1,34 @@ +#This will populate the metrics in all the tiers, under this path(not recommended) +#metricPrefix: "Custom metrics|Kafka" + +#The following prefix will populate the metrics under respective tiers +metricPrefix: "Server|Component:|Custom Metrics|Kafka" + +# To know your Tier-ID, Please refer the link +# https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695 + + #Provide the encryption key for the encrypted password +encryptionKey: "" + + +# Configure the following connection section ONLY if: +# 1. the config property [useSsl] is set to true in any of the servers in the above section AND +# 2. you want to use your own custom SSL certs +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Please REMOVE/COMMENT OUT the connection section below if: +# 1.You are not using SSL for any of the Kafka servers listed in the server section OR +# 2.You are using SSL for any of the servers([useSsl : true]), but you want to use default truststore of the JRE + + +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#If you are using the connection section, +#any change to the connection section below requires a machine agent restart for the changes to reflect +connection: + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocols: "TLSv1.2" + sslCipherSuites: "" + sslTrustStorePath: "src/test/resources/keystore/kafka.client.truststore.jks" #if [sslTrustStorePath: ""] empty, it defaults to MA home/conf/cacerts.jks + sslTrustStorePassword: "test1234" # [sslTrustStorePassword: ""] defaults to "" + sslTrustStoreEncryptedPassword: "" #provide encrypted Password if encryption is needed diff --git a/src/test/resources/keystore/kafka.client.truststore.jks b/src/test/resources/keystore/kafka.client.truststore.jks new file mode 100644 index 0000000000000000000000000000000000000000..3a472719e13e7f95b28c1e48f4813a9f72ec9d0c GIT binary patch literal 3795 zcmeH}S5#Bk8plr(5`r|5jtDWp0MZUk2z96;0SP9N8WjZrX-daMkr1j>VGyJ`5kfD9 zCPhUV7~=?B3sssl=?IDpMi}HSXEC$xUH8SeI}c}_^9G6VwDquQ~~8aSo_!8*YkKZ^*) z_}yfN5y;0lSj(R@lW1xIYxKC1saLqI>G(zGbqLbFzvAIB1a1|Jtk@{K|HY6YBYpq0 zBX+c)`W>>bjObR%eF*Uz&f!fvOxP);h5n-%(BbL3P9>D38Lfi-?{FrZ!gSWT<+_g^ zTpjF#^|-?#YMR|c4wU2|S@sBLC+4XOe1^;67Y+eTc}qS-$$Wg+pZ2JpKd@VzycLSDYDF_ll4p_iH5A5$ z#-1scwtT=6dsro*THHy|knq0!m+JDktyF7JLmRR7Ay*Z4iQ_+NaqTN)n^ws7mD=O5z1qeCx@Q0uJ+ z)OyC}WqXM=8Dl+_S54#A6FyMiU&cJ_Ew{w46h^E(k7&F)?@UP4es?CIP$56}Qc9X$ zR(p#f1tCOIGH5URoF=!DokWX%_PcSE;2O- zTm85;?%y~cb82D8Ja+Lg!-S&G)TdE%pK@un{Z~FNGg5qm(_L+mjHCWBUgH;)w_~j? zHrOp1W)^K_T5^pE1h~p`@^DTk6dvLbx0s*93bFRjPo7bh3oPtp_||lW$Yc`>uAmc? zp$nlEVlDdiG2uKC{^`ekd+VFDYa3NQn4;N6^PJTd%sl_Z_w+StT`D&%fn#n(tp8NPf` z-OyHX``S@iANh`DN>!e10d?WxkTSp6jj~168emm(!zVQ;Oe)?GlMYKu$2PP`Tixbq z8nn$@*K;n-TE@XcNiqjpfZh)R)iV6ix9#@$VEwm1RKF5}j+~GAlMpxvlHu|onT2oH zAg#S0n3jP*x$Y+f{{0Q2p`yOuAYUVB^-VZ`l@#xON%8KN6z_gX?FA0-z*oz`cwuKy zc#sS;+;yWmf^}Dg-I^?r-5|3kl9Ukj!TV)%56hS0uJxR>Zdh0RC11N*L+IUnN2a*m zm*a?8BkR%*AyrHh+w9{iQG5>VghO;MPi0eGNNm7Kio0V4R|+5fI8x?kr7dZm1G*PV z(@oUa3b)-oO>H-^BdEw+4YtCAHnl%hJ_jUpn+cQ#S@AsQ zU-M0s@;;@XppDeL(}A0sNEDH!hK-%QV>F7rw8N6B_qs?@w9gfK1ZdvKERGKTG&VZB<@z+JD|$uco@(>-^+&C*-ATJO z0tAX3ZBb^pE2jdW)LS+zoN7Z_YQgP}?SdWY-s$CJcM^j^i!o#HNZ zOI9&Va18DU?o5J9H;UfYYGG%ekT-VA#LT|z_8&ed;!-9TyJ6w>!?0eL5|)@9g{@nv zQ3_3+yPNs4h=g}oV4A$z5Z=(&U22KNNIhPY91l{K^(u*76gy$I8UFd)Xd{85?15Gp zoyLz;LwV#l#i2vsEfvXa>8ha{d01KWVDpX1{J{xWvCip8_RhG*w@XF>6Dxwlqq|;( z178av;XzIFvu;_eJ#+4mq~3Hf3VsBUD}8#El(c7(X|_7`{E%oZTc%z3 zw??oxZzO>mx<##o`Wo9M{$G+)RQVl39?^v<+;ZNID=SX%PCaGF?50$!TQ3BIqnMq7 zS4rYkW7jghRs-HQx{f~yvfK=oiAyVAp124`LGn{6yQSti`iuf6J#r}LnE#%Gg2Ey^Qiyd75No&3zDAc41P z$lu>jRrdO6L=O08KmE%@{?Ekm&&2V6IC127%0@8k&5b4Hmt0U<4xwvLJD7g}p4!gT literal 0 HcmV?d00001 diff --git a/src/test/resources/keystore/kafka.server.keystore.jks b/src/test/resources/keystore/kafka.server.keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..f4476423e87d8a1a4ad24db2824a3fa4c9a6dc78 GIT binary patch literal 3656 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 literal 0 HcmV?d00001 From 8b871cfed4c96a4006b2110eea4aaafdfb44c9c2 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Thu, 23 Aug 2018 15:46:46 -0700 Subject: [PATCH 38/49] updating metric qualifiers after dicussion --- src/main/resources/conf/config.yml | 74 +++++++++++++++--------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index 8ed740b..f6e70b1 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -2,15 +2,15 @@ #metricPrefix: "Custom metrics|Kafka" #The following prefix will populate the metrics under respective tiers -metricPrefix: "Server|Component:|Custom Metrics|Kafka" +metricPrefix: "Server|Component:|Custom Metrics|Kafka" -# To know your Tier-ID, Please refer the link +# To know your Component-ID, Please refer the link # https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695 # Add your Kafka Instances below servers: - - serviceUrl: "service:jmx:rmi:///jndi/rmi://10.0.66.179:9999/jmxrmi" #provide jmx service URL [OR] provide [host][port] pair + - serviceUrl: "service:jmx:rmi:///jndi/rmi://127.0.0.1:9999/jmxrmi" #provide jmx service URL [OR] provide [host][port] pair host: "" port: "" username: "" @@ -38,7 +38,7 @@ connection: connectTimeout: 1000 sslProtocols: "TLSv1.2" sslCipherSuites: "" - sslTrustStorePath: "" #if sslTrustStorePath: "" empty, it defaults to /conf/cacerts.jks + sslTrustStorePath: "" #if [sslTrustStorePath]: "" empty, it defaults to /conf/cacerts.jks sslTrustStorePassword: "changeit" # [sslTrustStorePassword: ""] defaults to "" sslTrustStoreEncryptedPassword: "" #provide encrypted Password if encryption is needed @@ -60,8 +60,8 @@ mbeans: alias: "Count" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + aggregationType: "OBSERVATION" + timeRollUpType: "CURRENT" clusterRollUpType: "INDIVIDUAL" - MeanRate: @@ -78,8 +78,8 @@ mbeans: alias: "Count" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + aggregationType: "OBSERVATION" + timeRollUpType: "CURRENT" clusterRollUpType: "INDIVIDUAL" - MeanRate: @@ -95,8 +95,8 @@ mbeans: alias: "Count" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + aggregationType: "OBSERVATION" + timeRollUpType: "CURRENT" clusterRollUpType: "INDIVIDUAL" - MeanRate: @@ -112,8 +112,8 @@ mbeans: alias: "Count" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + aggregationType: "OBSERVATION" + timeRollUpType: "CURRENT" clusterRollUpType: "INDIVIDUAL" - MeanRate: @@ -129,8 +129,8 @@ mbeans: alias: "Count" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + aggregationType: "OBSERVATION" + timeRollUpType: "CURRENT" clusterRollUpType: "INDIVIDUAL" - MeanRate: @@ -146,8 +146,8 @@ mbeans: alias: "Count" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + aggregationType: "OBSERVATION" + timeRollUpType: "CURRENT" clusterRollUpType: "INDIVIDUAL" - MeanRate: @@ -162,9 +162,9 @@ mbeans: - Count: alias: "Count" multiplier: "" - delta: true - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + delta: false + aggregationType: "OBSERVATION" + timeRollUpType: "CURRENT" clusterRollUpType: "INDIVIDUAL" - MeanRate: @@ -180,8 +180,8 @@ mbeans: alias: "Value" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + aggregationType: "OBSERVATION" + timeRollUpType: "SUM" clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=ReplicaFetcherManager,*" metrics: @@ -189,8 +189,8 @@ mbeans: alias: "Value" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + aggregationType: "OBSERVATION" + timeRollUpType: "SUM" clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=ReplicaManager,name=LeaderCount" metrics: @@ -198,8 +198,8 @@ mbeans: alias: "Value" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + aggregationType: "OBSERVATION" + timeRollUpType: "SUM" clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=ReplicaManager,name=PartitionCount" metrics: @@ -207,8 +207,8 @@ mbeans: alias: "Value" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + aggregationType: "OBSERVATION" + timeRollUpType: "SUM" clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions" metrics: @@ -216,8 +216,8 @@ mbeans: alias: "Value" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + aggregationType: "OBSERVATION" + timeRollUpType: "SUM" clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.network:type=Processor,*" metrics: @@ -225,8 +225,8 @@ mbeans: alias: "Value" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + aggregationType: "OBSERVATION" + timeRollUpType: "SUM" clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.network:type=RequestChannel,*" metrics: @@ -234,8 +234,8 @@ mbeans: alias: "Value" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + aggregationType: "OBSERVATION" + timeRollUpType: "SUM" clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.network:type=SocketServer,*" metrics: @@ -243,8 +243,8 @@ mbeans: alias: "Value" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + aggregationType: "OBSERVATION" + timeRollUpType: "SUM" clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=KafkaServer,name=BrokerState" metrics: @@ -252,8 +252,8 @@ mbeans: alias: "Value" multiplier: "" delta: false - aggregationType: "AVERAGE" - timeRollUpType: "AVERAGE" + aggregationType: "OBSERVATION" + timeRollUpType: "SUM" clusterRollUpType: "INDIVIDUAL" #JVM Metrics From 5b990b25a03389f47fffbfe2a980d01b2b437d12 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Thu, 23 Aug 2018 15:58:09 -0700 Subject: [PATCH 39/49] fixing formatting in readme --- README.md | 43 ++++++++----------------------------------- 1 file changed, 8 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index aefe168..2259961 100755 --- a/README.md +++ b/README.md @@ -7,61 +7,46 @@ real-time.The Kafka Monitoring extension can be used with a stand alone machine Apache Kafka. ## Prerequisites ## - - In order to use this extension, you do need a [Standalone JAVA Machine Agent](https://docs.appdynamics.com/display/PRO44/Standalone+Machine+Agents). or [SIM Agent](https://docs.appdynamics.com/display/PRO44/Server+Visibility).For more details on downloading these products, please visit [Downloads](https://download.appdynamics.com/).
- You also need a [Kafka](https://kafka.apache.org/quickstart) server installed. ## Installation ## - Unzip as "KafkaMonitor" and copy the "KafkaMonitor" directory to `/monitors` Please place the extension in the `monitors` directory of your Machine Agent installation directory. ## Configuration - ##### 1. Configuring ports - According to [Oracle's explanation](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8035404),JMX opens 3 different ports:one is the JMX connector port (the one in config.yml),one for the RMIRegistry and, the third one is an ephemeral port is RMI registry of the local only server. - We can explicitly configure the first two ports in the Kakfa start-up scripts to avoid it picking random ports(here port 9999 and 9998 are used). The third one, however, is an ephemeral port(that's how JMX works). - - Test connection to the Kafka host and ports 9999 and 9998 from the machine where the extension is installed.
``` nc -v KafkaServerIP port ``` #For example, to test connection to the localhost on port 9999, use nc -v localhost 9999 - If you get ```Connection to localhost port 9999 [tcp/distinct] succeeded!```,it confirms the access to the Kafka server. - ##### 2. Enabling JMX - - To enable JMX monitoring for Kafka broker, a JMX_PORT has to be configured to allow monitoring on that port.
Edit the Kafka start-up script `/bin/kafka-server-start.sh` to include: - #For example, to use port 9999 as the JMX_PORT, use export JMX_PORT=${JMX_PORT:-9999} - - Please note, that the Kafka server needs to be restarted once the JMX port is added. - ##### 3. Configuring Kafka for non-SSL monitoring This section outlines the configuration of the Kafka start-up scripts if monitoring is not done over SSL. If SSL is being used please skip to [Setting up SSL in Kafka](#sslsettings). - - To enable monitoring, some flags need to be set in the Kafka start-up scripts. Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
`KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.rmi.port=9998 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"` - - Also, the changes to `kafka-run-class.sh` has to be made on all the Kafka servers that you wish to monitor. - Please note, that any changes to `kafka-run-class.sh` needs the Kafka server to be restarted for the changes to take effect. - ##### 4. Monitoring over SSL - If you need to monitor your Kafka servers securely via SSL, please follow the following steps: ##### 1. Generating SSL Keys - Providing a keystore and truststore is mandatory for SSL. The keystore is used by the Kafka Server, the truststore is @@ -86,16 +71,13 @@ Please place the extension in the `monitors` directory of your Machine Ag keytool -keystore /path/to/MachineAgentHome/conf/cacerts.jks -alias CARoot -import -file ca-cert - Additional info about creating SSL keys is listed [here](https://docs.confluent.io/current/tutorials/security_tutorial.html#creating-ssl-keys-and-certificates). - - + ##### 2. Configuring Kafka for monitoring over SSL #### Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
`KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.rmi.port=9998 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=true -Djavax.net.ssl.keyStore=/Absolute/path/to/keystore -Djavax.net.ssl.keyStorePassword=password -Dcom.sun.management.jmxremote.registry.ssl=false"` - ##### 3. Configuring the extension to use SSL #### 1. The extension also needs to be configured to use SSL. In the config.yml of the Kafka Extension, uncomment the `connection` section.
- connection: socketTimeout: 3000 connectTimeout: 1000 @@ -119,7 +101,6 @@ Configure the Kafka monitoring extension by editing the config.yml file in `` in `metricPrefix: "Server|Component:|Custom Metrics|Kafka"`.
Please refer this [link](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) to find Component-ID of your tiers. - For example, ``` metricPrefix: "Server|Component:19|Custom Metrics|Kafka" @@ -130,7 +111,6 @@ Configure the Kafka monitoring extension by editing the config.yml file in ` pair @@ -144,7 +124,6 @@ Configure the Kafka monitoring extension by editing the config.yml file in `ANY of Kafka server(s). - Please remove this section if you are not using SSL for any of your servers. - If you are using the Machine Agent Truststore, please leave the `sslTrustStorePath` as `""`. - ``` connection: socketTimeout: 3000 @@ -161,17 +139,13 @@ Configure the Kafka monitoring extension by editing the config.yml file in `conf/cacerts.jks sslTrustStorePassword: "test1234" # defaults to empty sslTrustStoreEncryptedPassword: "" - ``` - + ``` 5. Configure the numberOfThreads according to the number of Kafka instances you are monitoring on one extension. Each server needs one thread. For example, - If number Kafka servers that need to be monitored by one extension is 10, then number of threads is 10 - - numberOfThreads: 10 - + numberOfThreads: 10 6. Configure the metrics section.
For configuring the metrics, the following properties can be used: @@ -184,7 +158,6 @@ Configure the Kafka monitoring extension by editing the config.yml file in ` +`HeapMemoryUsage.committed, HeapMemoryUsage.max, NonHeapMemoryUsage.committed, NonHeapMemoryUsage.max` +- There is also a `HeartBeat` metric under`kafka.server` which denotes whether the connection from the extension + to the Kafka server was successful(1 = Successful , 0 = Unsuccessful). Note : By default, a Machine agent or a AppServer agent can send a fixed number of metrics to the controller. To change this limit, please follow the instructions mentioned [here](http://docs.appdynamics.com/display/PRO14S/Metrics+Limits). From 6ef7c73cb6e285b398eae9c41b33630112c51b9e Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Fri, 24 Aug 2018 12:21:14 -0700 Subject: [PATCH 40/49] doc review changes --- README.md | 155 +++++++++++++++++++++++------------------------------- 1 file changed, 67 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 2259961..d068d8b 100755 --- a/README.md +++ b/README.md @@ -9,55 +9,55 @@ Apache Kafka. ## Prerequisites ## - In order to use this extension, you do need a [Standalone JAVA Machine Agent](https://docs.appdynamics.com/display/PRO44/Standalone+Machine+Agents). or [SIM Agent](https://docs.appdynamics.com/display/PRO44/Server+Visibility).For more details on downloading these products, please visit [Downloads](https://download.appdynamics.com/).
- - You also need a [Kafka](https://kafka.apache.org/quickstart) server installed. - +- The extension needs to be able to connect to Kafka in order to collect and send metrics. + To do this, you will have to either establish a remote connection in between the extension and the product, + or have an agent on the same machine running the product in order for the extension to collect and send the metrics. ## Installation ## Unzip as "KafkaMonitor" and copy the "KafkaMonitor" directory to `/monitors` Please place the extension in the `monitors` directory of your Machine Agent installation directory. - ## Configuration ##### 1. Configuring ports - According to [Oracle's explanation](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8035404),JMX opens 3 - different ports:one is the JMX connector port (the one in config.yml),one for the RMIRegistry and, - the third one is an ephemeral port is RMI registry of the local only server. - We can explicitly configure the first two ports in the Kakfa start-up scripts to avoid it picking random ports(here port 9999 and 9998 are used). - The third one, however, is an ephemeral port(that's how JMX works). - - Test connection to the Kafka host and ports 9999 and 9998 from the machine where the extension is installed.
- ``` nc -v KafkaServerIP port ``` - - #For example, to test connection to the localhost on port 9999, use - nc -v localhost 9999 - If you get ```Connection to localhost port 9999 [tcp/distinct] succeeded!```,it confirms the access to the Kafka server. + different ports: + - One is the JMX connector port(the one in config.yml), one for the RMIRegistry and, + the third one is an ephemeral port is RMI registry of the local only server. + - We can explicitly configure the first two ports in the Kakfa start-up scripts to avoid it picking random ports. + - Here port 9999 is used as JMX Connector port 9998 is used as the JMX/RMI port. + - The third one, however, is an ephemeral port(that's how JMX works). + - Test connection to the Kafka host and ports 9999 and 9998 from the machine where the extension is installed.
+ + For example, to test connection to the localhost on port 9999, use + nc -v localhost 9999 + + - If you get ```Connection to localhost port 9999 [tcp/distinct] succeeded!```, it confirms the access to the Kafka server. ##### 2. Enabling JMX - To enable JMX monitoring for Kafka broker, a JMX_PORT has to be configured to allow monitoring on that port. -
Edit the Kafka start-up script `/bin/kafka-server-start.sh` to include: - #For example, to use port 9999 as the JMX_PORT, use - export JMX_PORT=${JMX_PORT:-9999} +
Edit the Kafka start-up script `/bin/kafka-server-start.sh` to include:
+ `export JMX_PORT=${JMX_PORT:-9999}`
+ This configures port 9999 as the JMX port of Kafka. - Please note, that the Kafka server needs to be restarted once the JMX port is added. ##### 3. Configuring Kafka for non-SSL monitoring - - This section outlines the configuration of the Kafka start-up scripts if monitoring is not done over SSL. - If SSL is being used please skip to [Setting up SSL in Kafka](#sslsettings). + This section outlines the configuration of the Kafka start-up scripts if monitoring is not done over SSL.If SSL is being used please skip to [Setting up SSL in Kafka](#sslsettings). - To enable monitoring, some flags need to be set in the Kafka start-up scripts. Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
- `KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.rmi.port=9998 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"` - Also, the changes to `kafka-run-class.sh` has to be made on all the Kafka servers that you wish to monitor. - - Please note, that any changes to `kafka-run-class.sh` needs the Kafka server to be restarted for the changes to - take effect. + - Please note, that any changes to `kafka-run-class.sh` needs the Kafka server to be restarted for the changes to take effect. ##### 4. Monitoring over SSL If you need to monitor your Kafka servers securely via SSL, please follow the following steps: - ##### 1. Generating SSL Keys - - Providing a keystore and truststore is mandatory for SSL. The keystore is used by the Kafka Server, the truststore is - is used by the Kafka Monitoring Extension to trust the server. - - The extension supports a custom truststore, and if no truststore is specified,it defaults to the Machine Agent - truststore at `/conf/cacerts.jks`. - - You can create your truststore or choose to use the Machine Agent truststore at `/conf/cacerts.jks`. + +##### i. Generating SSL Keys + - Providing a Keystore and Truststore is mandatory for using SSL. The Keystore is used by the Kafka server, the Truststore is + is used by the Kafka Monitoring Extension to trust the server. + - The extension supports a custom Truststore, and if no Truststore is specified, the extension defaults to the Machine Agent + Truststore at `/conf/cacerts.jks`. + - You can create your Truststore or choose to use the Machine Agent Truststore at `/conf/cacerts.jks`. - Keytool is a utility that comes with the JDK. Please use the following commands to generate a keystore, and import - the certificates into the truststore. - - If you choose to use the Machine Agent truststore `cacerts.jks`, please follow the steps 1,2 and 3b below to import the certs into `cacerts.jks`. - + the certificates into the Truststore. + - If you choose to use the custom Truststore, please follow steps 1, 2 and 3a listed below. + - If you choose to use the Machine Agent Truststore `cacerts.jks`, please follow the steps 1, 2 and 3b listed below to import the certs into `cacerts.jks`. + #Step #1 keytool -keystore kafka.server.keystore.jks -alias localhost -validity 365 -genkey @@ -69,36 +69,34 @@ Please place the extension in the `monitors` directory of your Machine Ag #Step #3b: or if you are using Machine Agent truststore keytool -keystore /path/to/MachineAgentHome/conf/cacerts.jks -alias CARoot -import -file ca-cert - - Additional info about creating SSL keys is listed [here](https://docs.confluent.io/current/tutorials/security_tutorial.html#creating-ssl-keys-and-certificates). - - ##### 2. Configuring Kafka for monitoring over SSL #### - Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
+##### ii. Configuring Kafka for monitoring over SSL #### + Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable, as listed below:
+ ``` + KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.rmi.port=9998 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=true -Djavax.net.ssl.keyStore=/Absolute/path/to/keystore -Djavax.net.ssl.keyStorePassword=password -Dcom.sun.management.jmxremote.registry.ssl=false" + ``` - `KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.rmi.port=9998 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=true -Djavax.net.ssl.keyStore=/Absolute/path/to/keystore -Djavax.net.ssl.keyStorePassword=password -Dcom.sun.management.jmxremote.registry.ssl=false"` - ##### 3. Configuring the extension to use SSL #### + +##### iii. Configuring the Extension to use SSL #### 1. The extension also needs to be configured to use SSL. In the config.yml of the Kafka Extension, uncomment the `connection` section.
- connection: - socketTimeout: 3000 - connectTimeout: 1000 - sslProtocol: "TLSv1.2" - sslTrustStorePath: "/path/to/truststore/client/kafka.client.truststore.jks" #defaults to conf/cacerts.jks - sslTrustStorePassword: "test1234" # defaults to empty - sslTrustStoreEncryptedPassword: "" - -
Please note that any changes to the `connection`section of the config.yml, needs the Machine Agent to + ``` + connection: + socketTimeout: 3000 + connectTimeout: 1000 + sslProtocol: "TLSv1.2" + sslTrustStorePath: "/path/to/truststore/client/kafka.client.truststore.jks" #defaults to conf/cacerts.jks + sslTrustStorePassword: "test1234" # defaults to empty + sslTrustStoreEncryptedPassword: "" + ``` + - Please note that any changes to the `connection` section of the config.yml, needs the Machine Agent to be restarted for the changes to take effect. 2. If you need username/password authentication, please set the flag
`-Dcom.sun.management.jmxremote.authenticate=true` in the `KAFKA_JMX_OPTS` variable.Please refer to [Password Settings](#passwordsettings) for further steps. - - ##### 5. Password Settings If you need password authentication, you need to set-up the password in the JVM of the Kafka server To know more on how to set the credentials, please see section `Using Password and Access Files` in [this link](https://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html). - ##### 6. Config.yml Configure the Kafka monitoring extension by editing the config.yml file in `/monitors/KafkaMonitor/` - 1. Configure the "tier" under which the metrics need to be reported. This can be done by changing the value of `` in `metricPrefix: "Server|Component:|Custom Metrics|Kafka"`.
Please refer this [link](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) to find Component-ID of your tiers. For example, @@ -121,13 +119,13 @@ Configure the Kafka monitoring extension by editing the config.yml file in `ANY
of Kafka server(s). - Please remove this section if you are not using SSL for any of your servers. - If you are using the Machine Agent Truststore, please leave the `sslTrustStorePath` as `""`. @@ -138,17 +136,15 @@ Configure the Kafka monitoring extension by editing the config.yml file in `conf/cacerts.jks sslTrustStorePassword: "test1234" # defaults to empty - sslTrustStoreEncryptedPassword: "" - ``` + sslTrustStoreEncryptedPassword: ""``` 5. Configure the numberOfThreads according to the number of Kafka instances you are monitoring on one extension. Each server needs one thread. - For example, If number Kafka servers that need to be monitored by one extension is 10, then number of threads is 10 numberOfThreads: 10 6. Configure the metrics section.
For configuring the metrics, the following properties can be used: - + | Metric Property | Default value | Possible values | Description | | :---------------- | :-------------- | :------------------------------ | :------------------------------------------------------------------------------------------------------------- | | alias | metric name | Any string | The substitute name to be used in the metric browser instead of metric name. | @@ -159,10 +155,9 @@ Configure the Kafka monitoring extension by editing the config.yml file in ` - `objectName: kafka.server:type=ReplicaManager,name=IsrExpandsPerSec` - will return only those metrics corresponding to the `IsrExpandsPerSec` object. - - ##### 7. Validating config.yml: -Please copy all the contents of the config.yml file and go to [YamlLint](http://www.yamllint.com/).
-On reaching the website, paste the contents and press the “Go” button on the bottom left.
-If you get a valid output, that means your formatting is correct and you may move on to the next step. - + **All these metric properties are optional, and the default value shown in the table is applied to the metric(if a property has not been specified) by default.** + If you need a metric from a specific object under an mBean, `objectName: kafka.server:type=ReplicaManager,name=IsrExpandsPerSec` will return only those metrics corresponding to the `IsrExpandsPerSec` object. +##### 7. Validating config.yml: +- Please copy all the contents of the config.yml file and go to [YamlLint](http://www.yamllint.com/). +- On reaching the website, paste the contents and press the “Go” button on the bottom left.
+- If you get a valid output, that means your formatting is correct and you may move on to the next step. ##### 8. Metrics - This extension collects metrics via JMX and can be configured to report any of the metrics that Kafka exposes. It provides metrics on Kafka server, controller and the network. - In addition, it also provides the JVM metrics:
`HeapMemoryUsage.committed, HeapMemoryUsage.max, NonHeapMemoryUsage.committed, NonHeapMemoryUsage.max` - There is also a `HeartBeat` metric under`kafka.server` which denotes whether the connection from the extension - to the Kafka server was successful(1 = Successful , 0 = Unsuccessful). - -Note : By default, a Machine agent or a AppServer agent can send a fixed number of metrics to the controller. -To change this limit, please follow the instructions mentioned [here](http://docs.appdynamics.com/display/PRO14S/Metrics+Limits). -For eg. -``` + to the Kafka server was successful(1 = Successful, 0 = Unsuccessful). +- By default, a Machine agent or a AppServer agent can send a fixed number of metrics to the controller. + To change this limit, please follow the instructions mentioned [here](http://docs.appdynamics.com/display/PRO14S/Metrics+Limits). + For eg. + ``` java -Dappdynamics.agent.maxMetrics=2500 -jar machineagent.jar -``` - + ``` ## Credentials Encryption Please visit [this](https://community.appdynamics.com/t5/Knowledge-Base/How-to-use-Password-Encryption-with-Extensions/ta-p/29397) page to get detailed instructions on password encryption. The steps in this document will guide you through the whole process. - ## Extensions Workbench Workbench is an inbuilt feature provided with each extension in order to assist you to fine tune the extension setup before you actually deploy it on the controller. Please review the following [document](https://community.appdynamics.com/t5/Knowledge-Base/How-to-use-the-Extensions-WorkBench/ta-p/30130) for how to use the Extensions WorkBench - ## Troubleshooting Please follow the steps listed in the [extensions troubleshooting document](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) in order to troubleshoot your issue. These are a set of common issues that customers might have faced during the installation of the extension. If these don't solve your issue, please follow the last step on the troubleshooting-document to contact the support team. - ## Support Tickets If after going through the Troubleshooting Document you have not been able to get your extension working, please file a ticket and add the following information. - Please provide the following in order for us to assist you better.

 - 1. Stop the running machine agent
. 2. Delete all existing logs under /logs
. 3. Please enable debug logging by editing the file /conf/logging/log4j.xml. Change the level value of the following elements to debug.
 @@ -236,12 +218,9 @@ Please provide the following in order for us to assist you better.

 4. Start the machine agent and please let it run for 10 mins. Then zip and upload all the logs in the directory /logs/*. 5. Attach the zipped /conf/* directory here. 6. Attach the zipped /monitors/ directory here
. - For any support related questions, you can also contact help@appdynamics.com. - ## Contributing Always feel free to fork and contribute any changes directly via [GitHub](https://github.com/Appdynamics/kafka-monitoring-extension). - ## Version | Name | Version | | :---------------------------| :---------------------------| From 836f4fc9f0ab43083408124744bcf48280915a8a Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Fri, 24 Aug 2018 13:51:15 -0700 Subject: [PATCH 41/49] doc review changes --- README.md | 93 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index d068d8b..85ac5d5 100755 --- a/README.md +++ b/README.md @@ -14,26 +14,30 @@ Apache Kafka. To do this, you will have to either establish a remote connection in between the extension and the product, or have an agent on the same machine running the product in order for the extension to collect and send the metrics. ## Installation ## -Unzip as "KafkaMonitor" and copy the "KafkaMonitor" directory to `/monitors` -Please place the extension in the `monitors` directory of your Machine Agent installation directory. + - To build from source, clone this repository and run 'mvn clean install'. This will produce a KafkaMonitor-VERSION.zip in the target directory Alternatively, download the latest release archive from [GitHub](#https://github.com/Appdynamics/kafka-monitoring-extension) + - Unzip the file KafkaMonitor-[version].zip into /monitors/ + - In the newly created directory "KafkaMonitor", edit the config.yml to configure the parameters (See Configuration section below) + - Restart the Machine Agent + - In the AppDynamics Metric Browser, look for: Server||Custom Metrics|Kafka ## Configuration ##### 1. Configuring ports - - According to [Oracle's explanation](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8035404),JMX opens 3 - different ports: - - One is the JMX connector port(the one in config.yml), one for the RMIRegistry and, - the third one is an ephemeral port is RMI registry of the local only server. - - We can explicitly configure the first two ports in the Kakfa start-up scripts to avoid it picking random ports. - - Here port 9999 is used as JMX Connector port 9998 is used as the JMX/RMI port. - - The third one, however, is an ephemeral port(that's how JMX works). - - Test connection to the Kafka host and ports 9999 and 9998 from the machine where the extension is installed.
+ - According to [Oracle's explanation](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8035404),JMX opens 3 + different ports: + - One is the JMX connector port(the one in config.yml), + - one for the RMIRegistry and, + - the third one is an ephemeral port is RMI registry of the local only server. + - We can explicitly configure the first two ports in the Kakfa start-up scripts to avoid it picking random ports. + - Here port 9999 is used as JMX Connector port 9998 is used as the JMX/RMI port. + - The third one, however, is an ephemeral port(that's how JMX works). + - Test connection to the Kafka host and ports 9999 and 9998 from the machine where the extension is installed. For example, to test connection to the localhost on port 9999, use - nc -v localhost 9999 + nc -v localhost 9999. - - If you get ```Connection to localhost port 9999 [tcp/distinct] succeeded!```, it confirms the access to the Kafka server. + - If you get ```Connection to localhost port 9999 [tcp/distinct] succeeded!```, it confirms the access to the Kafka server. ##### 2. Enabling JMX - To enable JMX monitoring for Kafka broker, a JMX_PORT has to be configured to allow monitoring on that port. -
Edit the Kafka start-up script `/bin/kafka-server-start.sh` to include:
+
Edit the Kafka start-up script `/bin/kafka-server-start.sh` to include:
`export JMX_PORT=${JMX_PORT:-9999}`
This configures port 9999 as the JMX port of Kafka. - Please note, that the Kafka server needs to be restarted once the JMX port is added. @@ -46,11 +50,10 @@ Please place the extension in the `monitors` directory of your Machine Ag - Please note, that any changes to `kafka-run-class.sh` needs the Kafka server to be restarted for the changes to take effect. ##### 4. Monitoring over SSL If you need to monitor your Kafka servers securely via SSL, please follow the following steps: - -##### i. Generating SSL Keys - - Providing a Keystore and Truststore is mandatory for using SSL. The Keystore is used by the Kafka server, the Truststore is - is used by the Kafka Monitoring Extension to trust the server. - - The extension supports a custom Truststore, and if no Truststore is specified, the extension defaults to the Machine Agent +##### 4.1. Generating SSL Keys + - Providing a Keystore and Truststore is mandatory for using SSL. The Keystore is used by the Kafka server, the Truststore is + is used by the Kafka Monitoring Extension to trust the server. + - The extension supports a custom Truststore, and if no Truststore is specified, the extension defaults to the Machine Agent Truststore at `/conf/cacerts.jks`. - You can create your Truststore or choose to use the Machine Agent Truststore at `/conf/cacerts.jks`. - Keytool is a utility that comes with the JDK. Please use the following commands to generate a keystore, and import @@ -70,15 +73,16 @@ Please place the extension in the `monitors` directory of your Machine Ag #Step #3b: or if you are using Machine Agent truststore keytool -keystore /path/to/MachineAgentHome/conf/cacerts.jks -alias CARoot -import -file ca-cert - Additional info about creating SSL keys is listed [here](https://docs.confluent.io/current/tutorials/security_tutorial.html#creating-ssl-keys-and-certificates). -##### ii. Configuring Kafka for monitoring over SSL #### +##### 4.2. Configuring Kafka for monitoring over SSL #### Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable, as listed below:
+ ``` KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.rmi.port=9998 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=true -Djavax.net.ssl.keyStore=/Absolute/path/to/keystore -Djavax.net.ssl.keyStorePassword=password -Dcom.sun.management.jmxremote.registry.ssl=false" ``` -##### iii. Configuring the Extension to use SSL #### - 1. The extension also needs to be configured to use SSL. In the config.yml of the Kafka Extension, uncomment the `connection` section.
+##### 4.3. Configuring the Extension to use SSL #### + - The extension also needs to be configured to use SSL. In the config.yml of the Kafka Extension, uncomment the `connection` section.
``` connection: socketTimeout: 3000 @@ -90,20 +94,20 @@ Please place the extension in the `monitors` directory of your Machine Ag ``` - Please note that any changes to the `connection` section of the config.yml, needs the Machine Agent to be restarted for the changes to take effect. - 2. If you need username/password authentication, please set the flag
`-Dcom.sun.management.jmxremote.authenticate=true` + - If you need username/password authentication, please set the flag
`-Dcom.sun.management.jmxremote.authenticate=true` in the `KAFKA_JMX_OPTS` variable.Please refer to [Password Settings](#passwordsettings) for further steps. ##### 5. Password Settings If you need password authentication, you need to set-up the password in the JVM of the Kafka server To know more on how to set the credentials, please see section `Using Password and Access Files` in [this link](https://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html). ##### 6. Config.yml Configure the Kafka monitoring extension by editing the config.yml file in `/monitors/KafkaMonitor/` - 1. Configure the "tier" under which the metrics need to be reported. This can be done by changing the value of `` in + - Configure the "tier" under which the metrics need to be reported. This can be done by changing the value of `` in `metricPrefix: "Server|Component:|Custom Metrics|Kafka"`.
Please refer this [link](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) to find Component-ID of your tiers. For example, ``` metricPrefix: "Server|Component:19|Custom Metrics|Kafka" ``` - 2. Configure the Kafka servers by specifying either `serviceUrl` or `` of all Kafka servers. + - Configure the Kafka servers by specifying either `serviceUrl` or `` of all Kafka servers. - Here, `host` is the IP address. of the Kafka server to be monitored, and `port` is the JMX port of the Kafka server. - Please provide `username` & `password` (only if authentication enabled). @@ -120,13 +124,14 @@ Configure the Kafka monitoring extension by editing the config.yml file in `ANY of Kafka server(s). + + - Configure the connection section only if you are using monitoring over SSL for ANY of Kafka server(s). - Please remove this section if you are not using SSL for any of your servers. - If you are using the Machine Agent Truststore, please leave the `sslTrustStorePath` as `""`. ``` @@ -136,13 +141,15 @@ Configure the Kafka monitoring extension by editing the config.yml file in `conf/cacerts.jks sslTrustStorePassword: "test1234" # defaults to empty - sslTrustStoreEncryptedPassword: ""``` - 5. Configure the numberOfThreads according to the number of Kafka instances you are monitoring on one extension. - Each server needs one thread. - For example, - If number Kafka servers that need to be monitored by one extension is 10, then number of threads is 10 - numberOfThreads: 10 - 6. Configure the metrics section.
+ sslTrustStoreEncryptedPassword: "" + ``` + + - Configure the numberOfThreads according to the number of Kafka instances you are monitoring on one extension.Each server needs one thread + ``` For example, + If number Kafka servers that need to be monitored by one extension is 10, then number of threads is 10 + numberOfThreads: 10 + ``` + - Configure the metrics section.
For configuring the metrics, the following properties can be used: | Metric Property | Default value | Possible values | Description | @@ -154,9 +161,8 @@ Configure the Kafka monitoring extension by editing the config.yml file in ` @@ -212,9 +218,10 @@ Please provide the following in order for us to assist you better.

 2. Delete all existing logs under /logs
. 3. Please enable debug logging by editing the file /conf/logging/log4j.xml. Change the level value of the following elements to debug.
 - `` - `` - + ``` + + + ``` 4. Start the machine agent and please let it run for 10 mins. Then zip and upload all the logs in the directory /logs/*. 5. Attach the zipped /conf/* directory here. 6. Attach the zipped /monitors/ directory here
. From 8e6fca311e9b4907aa5078ce4067307b7b5b2c7e Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Mon, 27 Aug 2018 12:07:30 -0700 Subject: [PATCH 42/49] readme --- README.md | 87 +++++++------------ .../kafka/JMXConnectionAdapter.java | 1 - .../extensions/kafka/KafkaMonitor.java | 4 +- .../extensions/kafka/utils/SslUtils.java | 17 ++-- src/main/resources/conf/config.yml | 71 +++------------ 5 files changed, 53 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index 85ac5d5..50b7547 100755 --- a/README.md +++ b/README.md @@ -5,36 +5,33 @@ Kafka Monitoring Extension for AppDynamics Apache Kafka® is a distributed, fault-tolerant streaming platform. It can be used to process streams of data in real-time.The Kafka Monitoring extension can be used with a stand alone machine agent to provide metrics for multiple Apache Kafka. - ## Prerequisites ## - In order to use this extension, you do need a [Standalone JAVA Machine Agent](https://docs.appdynamics.com/display/PRO44/Standalone+Machine+Agents). - or [SIM Agent](https://docs.appdynamics.com/display/PRO44/Server+Visibility).For more details on downloading these products, please visit [Downloads](https://download.appdynamics.com/).
-- You also need a [Kafka](https://kafka.apache.org/quickstart) server installed. +or [SIM Agent](https://docs.appdynamics.com/display/PRO44/Server+Visibility).For more details on downloading these products, please visit [Downloads](https://download.appdynamics.com/).
+- The extension also needs a `[Kafka](https://kafka.apache.org/quickstart) server installed. - The extension needs to be able to connect to Kafka in order to collect and send metrics. To do this, you will have to either establish a remote connection in between the extension and the product, or have an agent on the same machine running the product in order for the extension to collect and send the metrics. ## Installation ## - - To build from source, clone this repository and run 'mvn clean install'. This will produce a KafkaMonitor-VERSION.zip in the target directory Alternatively, download the latest release archive from [GitHub](#https://github.com/Appdynamics/kafka-monitoring-extension) - - Unzip the file KafkaMonitor-[version].zip into /monitors/ - - In the newly created directory "KafkaMonitor", edit the config.yml to configure the parameters (See Configuration section below) - - Restart the Machine Agent - - In the AppDynamics Metric Browser, look for: Server||Custom Metrics|Kafka +- To build from source, clone this repository and run 'mvn clean install'. This will produce a KafkaMonitor-VERSION.zip in the target directory Alternatively, download the latest release archive from [GitHub](#https://github.com/Appdynamics/kafka-monitoring-extension) +- Unzip the file KafkaMonitor-[version].zip into /monitors/ +- In the newly created directory "KafkaMonitor", edit the config.yml to configure the parameters (See Configuration section below) +- Restart the Machine Agent +- In the AppDynamics Metric Browser, look for: Server||Custom Metrics|Kafka ## Configuration ##### 1. Configuring ports - - According to [Oracle's explanation](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8035404),JMX opens 3 - different ports: - - One is the JMX connector port(the one in config.yml), - - one for the RMIRegistry and, - - the third one is an ephemeral port is RMI registry of the local only server. - - We can explicitly configure the first two ports in the Kakfa start-up scripts to avoid it picking random ports. - - Here port 9999 is used as JMX Connector port 9998 is used as the JMX/RMI port. - - The third one, however, is an ephemeral port(that's how JMX works). - - Test connection to the Kafka host and ports 9999 and 9998 from the machine where the extension is installed. +- According to [Oracle's explanation](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8035404),JMX opens 3 different ports: + - One is the JMX connector port(the one in config.yml), + - one for the RMIRegistry and, + - the third one is an ephemeral port is RMI registry of the local only server. +- We can explicitly configure the first two ports in the Kakfa start-up scripts to avoid it picking random ports. +- Here port 9999 is used as JMX Connector port 9998 is used as the JMX/RMI port. +- The third one, however, is an ephemeral port(that's how JMX works). +- Test connection to the Kafka host and ports 9999 and 9998 from the machine where the extension is installed. For example, to test connection to the localhost on port 9999, use nc -v localhost 9999. - - - If you get ```Connection to localhost port 9999 [tcp/distinct] succeeded!```, it confirms the access to the Kafka server. +- If the message ```Connection to localhost port 9999 [tcp/distinct] succeeded!```is displayed, it confirms the access to the Kafka server. ##### 2. Enabling JMX - To enable JMX monitoring for Kafka broker, a JMX_PORT has to be configured to allow monitoring on that port.
Edit the Kafka start-up script `/bin/kafka-server-start.sh` to include:
@@ -46,20 +43,17 @@ Apache Kafka. - To enable monitoring, some flags need to be set in the Kafka start-up scripts. Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
`KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.rmi.port=9998 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"` - - Also, the changes to `kafka-run-class.sh` has to be made on all the Kafka servers that you wish to monitor. + - Also, the changes to `kafka-run-class.sh` has to be made on all the Kafka servers that are being monitored. - Please note, that any changes to `kafka-run-class.sh` needs the Kafka server to be restarted for the changes to take effect. ##### 4. Monitoring over SSL If you need to monitor your Kafka servers securely via SSL, please follow the following steps: ##### 4.1. Generating SSL Keys - - Providing a Keystore and Truststore is mandatory for using SSL. The Keystore is used by the Kafka server, the Truststore is - is used by the Kafka Monitoring Extension to trust the server. - - The extension supports a custom Truststore, and if no Truststore is specified, the extension defaults to the Machine Agent - Truststore at `/conf/cacerts.jks`. + - Providing a Keystore and Truststore is mandatory for using SSL. The Keystore is used by the Kafka server, the Truststore is used by the Kafka Monitoring Extension to trust the server. + - The extension supports a custom Truststore, and if no Truststore is specified, the extension defaults to the Machine Agent Truststore at `/conf/cacerts.jks`. - You can create your Truststore or choose to use the Machine Agent Truststore at `/conf/cacerts.jks`. - - Keytool is a utility that comes with the JDK. Please use the following commands to generate a keystore, and import - the certificates into the Truststore. - - If you choose to use the custom Truststore, please follow steps 1, 2 and 3a listed below. - - If you choose to use the Machine Agent Truststore `cacerts.jks`, please follow the steps 1, 2 and 3b listed below to import the certs into `cacerts.jks`. + - Keytool is a utility that comes with the JDK. Please use the following commands to generate a keystore, and import the certificates into the Truststore. + - To use the custom Truststore, please follow steps 1, 2 and 3a listed below. + - To to use the Machine Agent Truststore `cacerts.jks`, please follow the steps 1, 2 and 3b listed below to import the certs into `cacerts.jks`. #Step #1 keytool -keystore kafka.server.keystore.jks -alias localhost -validity 365 -genkey @@ -75,12 +69,9 @@ Apache Kafka. - Additional info about creating SSL keys is listed [here](https://docs.confluent.io/current/tutorials/security_tutorial.html#creating-ssl-keys-and-certificates). ##### 4.2. Configuring Kafka for monitoring over SSL #### Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable, as listed below:
- ``` KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.rmi.port=9998 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=true -Djavax.net.ssl.keyStore=/Absolute/path/to/keystore -Djavax.net.ssl.keyStorePassword=password -Dcom.sun.management.jmxremote.registry.ssl=false" ``` - - ##### 4.3. Configuring the Extension to use SSL #### - The extension also needs to be configured to use SSL. In the config.yml of the Kafka Extension, uncomment the `connection` section.
``` @@ -97,7 +88,7 @@ Apache Kafka. - If you need username/password authentication, please set the flag
`-Dcom.sun.management.jmxremote.authenticate=true` in the `KAFKA_JMX_OPTS` variable.Please refer to [Password Settings](#passwordsettings) for further steps. ##### 5. Password Settings -If you need password authentication, you need to set-up the password in the JVM of the Kafka server +If you need password authentication, the password needs to be set in the JVM of the Kafka server. To know more on how to set the credentials, please see section `Using Password and Access Files` in [this link](https://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html). ##### 6. Config.yml Configure the Kafka monitoring extension by editing the config.yml file in `/monitors/KafkaMonitor/` @@ -112,7 +103,7 @@ Configure the Kafka monitoring extension by editing the config.yml file in ` pair @@ -126,13 +117,12 @@ Configure the Kafka monitoring extension by editing the config.yml file in `ANY of Kafka server(s). - - Please remove this section if you are not using SSL for any of your servers. + - Please remove this section if SSL is not required to connect to any of your servers. - If you are using the Machine Agent Truststore, please leave the `sslTrustStorePath` as `""`. ``` connection: @@ -144,13 +134,13 @@ Configure the Kafka monitoring extension by editing the config.yml file in ` - For configuring the metrics, the following properties can be used: + For configuring the metrics, the following properties can be used: | Metric Property | Default value | Possible values | Description | | :---------------- | :-------------- | :------------------------------ | :------------------------------------------------------------------------------------------------------------- | @@ -181,33 +171,23 @@ Configure the Kafka monitoring extension by editing the config.yml file in ` - If you get a valid output, that means your formatting is correct and you may move on to the next step. ##### 8. Metrics -- This extension collects metrics via JMX and can be configured to report any of the metrics that Kafka exposes. It provides metrics on -Kafka server, controller and the network. +- This extension collects metrics via JMX and can be configured to report any of the metrics that Kafka exposes. It provides metrics on Kafka server, controller and the network. - In addition, it also provides the JVM metrics:
`HeapMemoryUsage.committed, HeapMemoryUsage.max, NonHeapMemoryUsage.committed, NonHeapMemoryUsage.max` -- There is also a `HeartBeat` metric under`kafka.server` which denotes whether the connection from the extension - to the Kafka server was successful(1 = Successful, 0 = Unsuccessful). +- There is also a `HeartBeat` metric under`kafka.server` which denotes whether the connection from the extension to the Kafka server was successful(1 = Successful, 0 = Unsuccessful). - By default, a Machine agent or a AppServer agent can send a fixed number of metrics to the controller. To change this limit, please follow the instructions mentioned [here](http://docs.appdynamics.com/display/PRO14S/Metrics+Limits). - For eg. - ``` - java -Dappdynamics.agent.maxMetrics=2500 -jar machineagent.jar - ``` ## Credentials Encryption -Please visit [this](https://community.appdynamics.com/t5/Knowledge-Base/How-to-use-Password-Encryption-with-Extensions/ta-p/29397) -page to get detailed instructions on password encryption. The steps in this document will guide you through the whole process. +Please visit [this](https://community.appdynamics.com/t5/Knowledge-Base/How-to-use-Password-Encryption-with-Extensions/ta-p/29397) page to get detailed instructions on password encryption. The steps in this document will guide you through the whole process. ## Extensions Workbench -Workbench is an inbuilt feature provided with each extension in order to assist you to fine tune the extension setup -before you actually deploy it on the controller. Please review the following -[document](https://community.appdynamics.com/t5/Knowledge-Base/How-to-use-the-Extensions-WorkBench/ta-p/30130) for -how to use the Extensions WorkBench +Workbench is an inbuilt feature provided with each extension in order to assist you to fine tune the extension setup before you actually deploy it on the controller. Please review the following +[document](https://community.appdynamics.com/t5/Knowledge-Base/How-to-use-the-Extensions-WorkBench/ta-p/30130) for how to use the Extensions WorkBench ## Troubleshooting Please follow the steps listed in the [extensions troubleshooting document](https://community.appdynamics.com/t5/Knowledge-Base/How-to-troubleshoot-missing-custom-metrics-or-extensions-metrics/ta-p/28695) in order to troubleshoot your issue. These are a set of common issues that customers might have faced during the installation of the extension. If these don't solve your issue, please follow the last step on the troubleshooting-document to contact the support team. @@ -217,7 +197,6 @@ Please provide the following in order for us to assist you better.

 1. Stop the running machine agent
. 2. Delete all existing logs under /logs
. 3. Please enable debug logging by editing the file /conf/logging/log4j.xml. Change the level value of the following elements to debug.
 - ``` diff --git a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java index 32bd8ab..3602aa6 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java +++ b/src/main/java/com/appdynamics/extensions/kafka/JMXConnectionAdapter.java @@ -19,7 +19,6 @@ import com.google.common.base.Strings; import com.google.common.collect.Lists; - import javax.management.*; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; diff --git a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java index 7f8bf5c..5637b08 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java +++ b/src/main/java/com/appdynamics/extensions/kafka/KafkaMonitor.java @@ -14,6 +14,7 @@ import com.appdynamics.extensions.kafka.utils.Constants; import com.appdynamics.extensions.kafka.utils.SslUtils; import com.appdynamics.extensions.util.AssertUtils; +import org.slf4j.LoggerFactory; import java.io.File; import java.util.List; @@ -22,7 +23,7 @@ import static com.appdynamics.extensions.kafka.utils.Constants.DEFAULT_METRIC_PREFIX; public class KafkaMonitor extends ABaseMonitor { - + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(KafkaMonitor.class); @Override protected void onConfigReload(File file) { Map configMap = this.getContextConfiguration().getConfigYml(); @@ -54,4 +55,5 @@ protected int getTaskCount() { AssertUtils.assertNotNull(servers, "The 'servers' section in config.yml is not initialised"); return servers.size(); } + } \ No newline at end of file diff --git a/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java b/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java index d9d0112..f27b178 100644 --- a/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java +++ b/src/main/java/com/appdynamics/extensions/kafka/utils/SslUtils.java @@ -1,11 +1,11 @@ package com.appdynamics.extensions.kafka.utils; import com.appdynamics.extensions.crypto.Decryptor; -import com.appdynamics.extensions.util.AssertUtils; + import com.appdynamics.extensions.util.PathResolver; +import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.singularity.ee.agent.systemagent.api.AManagedMonitor; -import com.sun.tools.javac.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; @@ -27,11 +27,9 @@ public void setSslProperties(Map configMap) { if (configMap.containsKey(Constants.CONNECTION)) { Map connectionMap = (Map) configMap.get(Constants.CONNECTION); - if (connectionMap.containsKey(Constants.TRUST_STORE_PATH)){ - Assert.checkNonNull(connectionMap.get(Constants.TRUST_STORE_PATH), "[sslTrustStorePath] cannot be null"); + Preconditions.checkNotNull(connectionMap.get(Constants.TRUST_STORE_PATH), "[sslTrustStorePath] cannot be null"); if(!(connectionMap.get(Constants.TRUST_STORE_PATH).toString()).isEmpty()) { - String sslTrustStorePath = connectionMap.get(Constants.TRUST_STORE_PATH).toString(); File customSslTrustStoreFile = new File(sslTrustStorePath); if (customSslTrustStoreFile == null || !customSslTrustStoreFile.exists()) { @@ -39,24 +37,19 @@ public void setSslProperties(Map configMap) { } else { logger.debug("Using custom SSL truststore [{}] ", sslTrustStorePath); - logger.debug("Setting SystemProperty [javax.net.ssl.trustStore] ", customSslTrustStoreFile.getAbsolutePath()); + logger.debug("Setting SystemProperty [javax.net.ssl.trustStore] {} ", customSslTrustStoreFile.getAbsolutePath()); System.setProperty("javax.net.ssl.trustStore", customSslTrustStoreFile.getAbsolutePath()); } } else if ((connectionMap.get(Constants.TRUST_STORE_PATH).toString()).isEmpty()) { File installDir = PathResolver.resolveDirectory(AManagedMonitor.class); - //while debugging please comment out the above line - //and add - //File installDir = new File("/path/to/machineagent-home"); - //for example, - //File installDir = new File("/Users/vishaka.sekar/AppDynamics/machineagent"); File defaultTrustStoreFile = PathResolver.getFile("/conf/cacerts.jks", installDir); if (defaultTrustStoreFile == null || !defaultTrustStoreFile.exists()) { logger.debug("The file [{}] doesn't exist", installDir + "/conf/cacerts.jks"); } else { logger.debug("Using Machine Agent truststore {}", installDir + "/conf/cacerts.jks"); - logger.debug("Setting SystemProperty [javax.net.ssl.trustStore] ",defaultTrustStoreFile.getAbsolutePath()); + logger.debug("Setting SystemProperty [javax.net.ssl.trustStore] {}",defaultTrustStoreFile.getAbsolutePath()); System.setProperty("javax.net.ssl.trustStore", defaultTrustStoreFile.getAbsolutePath()); } } diff --git a/src/main/resources/conf/config.yml b/src/main/resources/conf/config.yml index f6e70b1..755a0ab 100755 --- a/src/main/resources/conf/config.yml +++ b/src/main/resources/conf/config.yml @@ -56,14 +56,6 @@ mbeans: - objectName: "kafka.server:type=BrokerTopicMetrics,*" metrics: - - Count: - alias: "Count" - multiplier: "" - delta: false - aggregationType: "OBSERVATION" - timeRollUpType: "CURRENT" - clusterRollUpType: "INDIVIDUAL" - - MeanRate: alias: "Mean Rate" multiplier: "" @@ -72,15 +64,8 @@ mbeans: timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" - - objectName: "kafka.server:type=KafkaRequestHandlerPool,*" + - objectName: "kafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercent" metrics: - - Count: - alias: "Count" - multiplier: "" - delta: false - aggregationType: "OBSERVATION" - timeRollUpType: "CURRENT" - clusterRollUpType: "INDIVIDUAL" - MeanRate: alias: "Mean Rate" @@ -91,13 +76,6 @@ mbeans: clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=ReplicaManager,name=IsrExpandsPerSec" metrics: - - Count: - alias: "Count" - multiplier: "" - delta: false - aggregationType: "OBSERVATION" - timeRollUpType: "CURRENT" - clusterRollUpType: "INDIVIDUAL" - MeanRate: alias: "Mean Rate" @@ -108,13 +86,6 @@ mbeans: clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=ReplicaManager,name=IsrShrinksPerSec" metrics: - - Count: - alias: "Count" - multiplier: "" - delta: false - aggregationType: "OBSERVATION" - timeRollUpType: "CURRENT" - clusterRollUpType: "INDIVIDUAL" - MeanRate: alias: "Mean Rate" @@ -125,13 +96,6 @@ mbeans: clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=SessionExpireListener,*" metrics: - - Count: - alias: "Count" - multiplier: "" - delta: false - aggregationType: "OBSERVATION" - timeRollUpType: "CURRENT" - clusterRollUpType: "INDIVIDUAL" - MeanRate: alias: "Mean Rate" @@ -142,13 +106,7 @@ mbeans: clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.network:type=RequestMetrics,*" metrics: - - Count: - alias: "Count" - multiplier: "" - delta: false - aggregationType: "OBSERVATION" - timeRollUpType: "CURRENT" - clusterRollUpType: "INDIVIDUAL" + - MeanRate: alias: "Mean Rate" @@ -159,13 +117,6 @@ mbeans: clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.controller:type=ControllerStats,*" metrics: - - Count: - alias: "Count" - multiplier: "" - delta: false - aggregationType: "OBSERVATION" - timeRollUpType: "CURRENT" - clusterRollUpType: "INDIVIDUAL" - MeanRate: alias: "Mean Rate" @@ -174,6 +125,7 @@ mbeans: aggregationType: "AVERAGE" timeRollUpType: "AVERAGE" clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=DelayedOperationPurgatory,*" metrics: - Value: @@ -181,7 +133,7 @@ mbeans: multiplier: "" delta: false aggregationType: "OBSERVATION" - timeRollUpType: "SUM" + timeRollUpType: "CURRENT" clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=ReplicaFetcherManager,*" metrics: @@ -190,8 +142,9 @@ mbeans: multiplier: "" delta: false aggregationType: "OBSERVATION" - timeRollUpType: "SUM" + timeRollUpType: "CURRENT" clusterRollUpType: "INDIVIDUAL" + - objectName: "kafka.server:type=ReplicaManager,name=LeaderCount" metrics: - Value: @@ -199,7 +152,7 @@ mbeans: multiplier: "" delta: false aggregationType: "OBSERVATION" - timeRollUpType: "SUM" + timeRollUpType: "CURRENT" clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=ReplicaManager,name=PartitionCount" metrics: @@ -208,7 +161,7 @@ mbeans: multiplier: "" delta: false aggregationType: "OBSERVATION" - timeRollUpType: "SUM" + timeRollUpType: "CURRENT" clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions" metrics: @@ -217,7 +170,7 @@ mbeans: multiplier: "" delta: false aggregationType: "OBSERVATION" - timeRollUpType: "SUM" + timeRollUpType: "CURRENT" clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.network:type=Processor,*" metrics: @@ -226,7 +179,7 @@ mbeans: multiplier: "" delta: false aggregationType: "OBSERVATION" - timeRollUpType: "SUM" + timeRollUpType: "CURRENT" clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.network:type=RequestChannel,*" metrics: @@ -235,7 +188,7 @@ mbeans: multiplier: "" delta: false aggregationType: "OBSERVATION" - timeRollUpType: "SUM" + timeRollUpType: "CURRENT" clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.network:type=SocketServer,*" metrics: @@ -244,7 +197,7 @@ mbeans: multiplier: "" delta: false aggregationType: "OBSERVATION" - timeRollUpType: "SUM" + timeRollUpType: "CURRENT" clusterRollUpType: "INDIVIDUAL" - objectName: "kafka.server:type=KafkaServer,name=BrokerState" metrics: From 0c0f5d78f6c36df14c8ade113d18a07128b26b48 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Mon, 27 Aug 2018 13:25:27 -0700 Subject: [PATCH 43/49] readmepart3 --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 50b7547..37f69ad 100755 --- a/README.md +++ b/README.md @@ -8,22 +8,22 @@ Apache Kafka. ## Prerequisites ## - In order to use this extension, you do need a [Standalone JAVA Machine Agent](https://docs.appdynamics.com/display/PRO44/Standalone+Machine+Agents). or [SIM Agent](https://docs.appdynamics.com/display/PRO44/Server+Visibility).For more details on downloading these products, please visit [Downloads](https://download.appdynamics.com/).
-- The extension also needs a `[Kafka](https://kafka.apache.org/quickstart) server installed. +- The extension also needs a [Kafka](https://kafka.apache.org/quickstart) server installed. - The extension needs to be able to connect to Kafka in order to collect and send metrics. To do this, you will have to either establish a remote connection in between the extension and the product, or have an agent on the same machine running the product in order for the extension to collect and send the metrics. ## Installation ## - To build from source, clone this repository and run 'mvn clean install'. This will produce a KafkaMonitor-VERSION.zip in the target directory Alternatively, download the latest release archive from [GitHub](#https://github.com/Appdynamics/kafka-monitoring-extension) -- Unzip the file KafkaMonitor-[version].zip into /monitors/ -- In the newly created directory "KafkaMonitor", edit the config.yml to configure the parameters (See Configuration section below) +- Unzip the file KafkaMonitor-[version].zip into /monitors/ +- In the newly created directory "KafkaMonitor", edit the config.yml to configure the parameters (See Configuration section below) - Restart the Machine Agent -- In the AppDynamics Metric Browser, look for: Server||Custom Metrics|Kafka +- In the AppDynamics Metric Browser, look for: Server||Custom Metrics|Kafka ## Configuration ##### 1. Configuring ports -- According to [Oracle's explanation](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8035404),JMX opens 3 different ports: - - One is the JMX connector port(the one in config.yml), - - one for the RMIRegistry and, - - the third one is an ephemeral port is RMI registry of the local only server. +- According to [Oracle's explanation](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8035404), JMX opens 3 different ports: + - One is the JMX connector port(the one in config.yml) + - One for the RMIRegistry + - The third one is an ephemeral port is RMI registry of the local only server - We can explicitly configure the first two ports in the Kakfa start-up scripts to avoid it picking random ports. - Here port 9999 is used as JMX Connector port 9998 is used as the JMX/RMI port. - The third one, however, is an ephemeral port(that's how JMX works). @@ -37,14 +37,14 @@ or [SIM Agent](https://docs.appdynamics.com/display/PRO44/Server+Visibility).For
Edit the Kafka start-up script `/bin/kafka-server-start.sh` to include:
`export JMX_PORT=${JMX_PORT:-9999}`
This configures port 9999 as the JMX port of Kafka. - - Please note, that the Kafka server needs to be restarted once the JMX port is added. + - Please note that the Kafka server needs to be restarted once the JMX port is added. ##### 3. Configuring Kafka for non-SSL monitoring This section outlines the configuration of the Kafka start-up scripts if monitoring is not done over SSL.If SSL is being used please skip to [Setting up SSL in Kafka](#sslsettings). - To enable monitoring, some flags need to be set in the Kafka start-up scripts. Edit `/bin/kafka-run-class.sh` and modify `KAFKA_JMX_OPTS` variable like below
`KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.rmi.port=9998 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"` - Also, the changes to `kafka-run-class.sh` has to be made on all the Kafka servers that are being monitored. - - Please note, that any changes to `kafka-run-class.sh` needs the Kafka server to be restarted for the changes to take effect. + - Please note that any changes to `kafka-run-class.sh` needs the Kafka server to be restarted for the changes to take effect. ##### 4. Monitoring over SSL If you need to monitor your Kafka servers securely via SSL, please follow the following steps: ##### 4.1. Generating SSL Keys From b3db0644a721337530faa0eab681fe41243c3234 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Mon, 27 Aug 2018 13:36:49 -0700 Subject: [PATCH 44/49] readme-fixed tags --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 37f69ad..6cd6f4b 100755 --- a/README.md +++ b/README.md @@ -14,10 +14,10 @@ or [SIM Agent](https://docs.appdynamics.com/display/PRO44/Server+Visibility).For or have an agent on the same machine running the product in order for the extension to collect and send the metrics. ## Installation ## - To build from source, clone this repository and run 'mvn clean install'. This will produce a KafkaMonitor-VERSION.zip in the target directory Alternatively, download the latest release archive from [GitHub](#https://github.com/Appdynamics/kafka-monitoring-extension) -- Unzip the file KafkaMonitor-[version].zip into /monitors/ +- Unzip the file KafkaMonitor-\[version\].zip into /monitors/ - In the newly created directory "KafkaMonitor", edit the config.yml to configure the parameters (See Configuration section below) - Restart the Machine Agent -- In the AppDynamics Metric Browser, look for: Server||Custom Metrics|Kafka +- In the AppDynamics Metric Browser, look for: Server|\|Custom Metrics|Kafka ## Configuration ##### 1. Configuring ports - According to [Oracle's explanation](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8035404), JMX opens 3 different ports: From bd9103eb769fa5aa1cced241da1b2bb7c5ee4a6a Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Mon, 27 Aug 2018 13:38:43 -0700 Subject: [PATCH 45/49] readme-fixed date --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6cd6f4b..58a282f 100755 --- a/README.md +++ b/README.md @@ -214,5 +214,5 @@ Always feel free to fork and contribute any changes directly via [GitHub](https: | Controller Compatibility: | 4.0 or Later | | Tested On: | Apache Kafka 2.11 | | Operating System Tested On: | Mac OS | -| Last updated On: | Aug 21, 2018 | +| Last updated On: | Aug 27, 2018 | | List of changes to this extension| [Change log](https://github.com/Appdynamics/kafka-monitoring-extension/blob/master/Changelog.md) From d19800fddc63b831f01d730d03451c8af9a69e21 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Mon, 27 Aug 2018 17:00:27 -0700 Subject: [PATCH 46/49] readme-updated metric path --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 58a282f..8825f1f 100755 --- a/README.md +++ b/README.md @@ -17,8 +17,7 @@ or [SIM Agent](https://docs.appdynamics.com/display/PRO44/Server+Visibility).For - Unzip the file KafkaMonitor-\[version\].zip into /monitors/ - In the newly created directory "KafkaMonitor", edit the config.yml to configure the parameters (See Configuration section below) - Restart the Machine Agent -- In the AppDynamics Metric Browser, look for: Server|\|Custom Metrics|Kafka -## Configuration +- In the AppDynamics Metric Browser, look for: In the AppDynamics Metric Browser, look for: Application Infrastructure Performance|\|Custom Metrics|Log Monitor. If SIM is enabled, look for the metric browser under the Servers tab. ##### 1. Configuring ports - According to [Oracle's explanation](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8035404), JMX opens 3 different ports: - One is the JMX connector port(the one in config.yml) From d236c82ce5eaf036ef9c2a4ddbab48c5e8bb9b58 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Mon, 27 Aug 2018 17:02:44 -0700 Subject: [PATCH 47/49] readme-updated table structure --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8825f1f..bddff4a 100755 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ Configure the Kafka monitoring extension by editing the config.yml file in ` Date: Mon, 27 Aug 2018 17:38:41 -0700 Subject: [PATCH 48/49] readme-sim path updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bddff4a..21e0b1b 100755 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ or [SIM Agent](https://docs.appdynamics.com/display/PRO44/Server+Visibility).For - Unzip the file KafkaMonitor-\[version\].zip into /monitors/ - In the newly created directory "KafkaMonitor", edit the config.yml to configure the parameters (See Configuration section below) - Restart the Machine Agent -- In the AppDynamics Metric Browser, look for: In the AppDynamics Metric Browser, look for: Application Infrastructure Performance|\|Custom Metrics|Log Monitor. If SIM is enabled, look for the metric browser under the Servers tab. +- In the AppDynamics Metric Browser, look for: In the AppDynamics Metric Browser, look for: Application Infrastructure Performance|\|Custom Metrics|Kafka. If SIM is enabled, look for the metric browser under the Servers tab: Application Infrastructure Performance|Root|Custom Metrics|Kafka. ##### 1. Configuring ports - According to [Oracle's explanation](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8035404), JMX opens 3 different ports: - One is the JMX connector port(the one in config.yml) From f6bacf0eef1c221f4404bacefa3b46e124e2ca72 Mon Sep 17 00:00:00 2001 From: VishakaSekar Date: Mon, 27 Aug 2018 17:42:17 -0700 Subject: [PATCH 49/49] readme-sim path updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 21e0b1b..f452b47 100755 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ or [SIM Agent](https://docs.appdynamics.com/display/PRO44/Server+Visibility).For - Unzip the file KafkaMonitor-\[version\].zip into /monitors/ - In the newly created directory "KafkaMonitor", edit the config.yml to configure the parameters (See Configuration section below) - Restart the Machine Agent -- In the AppDynamics Metric Browser, look for: In the AppDynamics Metric Browser, look for: Application Infrastructure Performance|\|Custom Metrics|Kafka. If SIM is enabled, look for the metric browser under the Servers tab: Application Infrastructure Performance|Root|Custom Metrics|Kafka. +- In the AppDynamics Metric Browser, look for: In the AppDynamics Metric Browser, look for: Application Infrastructure Performance|\|Custom Metrics|Kafka. If SIM is enabled, look for the Metric Browser for the following metric path under the Servers tab: Application Infrastructure Performance|Root|Custom Metrics|Kafka. ##### 1. Configuring ports - According to [Oracle's explanation](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8035404), JMX opens 3 different ports: - One is the JMX connector port(the one in config.yml)