diff --git a/README.md b/README.md index bd9bf63..bece3d9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ [![Codacy Badge](https://api.codacy.com/project/badge/Grade/8a2f2a06171248acb6411a2d870558c8)](https://app.codacy.com/app/antho325/jmeter-elasticsearch-backend-listener?utm_source=github.com&utm_medium=referral&utm_content=delirius325/jmeter-elasticsearch-backend-listener&utm_campaign=Badge_Grade_Dashboard) [![Build Status](https://travis-ci.org/delirius325/jmeter-elasticsearch-backend-listener.svg?branch=master)](https://travis-ci.org/delirius325/jmeter-elasticsearch-backend-listener) - # Overview ### Description JMeter ElasticSearch Backend Listener is a JMeter plugin enabling you to send test results to an ElasticSearch engine. It is meant as an alternative live-monitoring tool to the built-in "InfluxDB" backend listener of JMeter. @@ -11,12 +10,13 @@ JMeter ElasticSearch Backend Listener is a JMeter plugin enabling you to send te * ElasticSearch low-level REST client * Using the low-level client makes the plugin compatible with any ElasticSearch version * X-Pack Authentication! - * Just supply your crendentials in the specified fields! + * Just supply your credentials in the specified fields! * Bulk requests * By making bulk requests, there are practically no impacts on the performance of the tests themselves. * Filters * Only send the samples you want by using Filters! Simply type them as follows in the field ``es.sample.filter`` : ``filter1;filter2;filter3`` or ``sampleLabel_must_contain_this``. * You can also choose to exclude certain samplers; `!!exclude_this;filter1;filter2` + * SYS_DYNAMIC_ES_PER_SAMPLE_FILTER is a System property that can be set before the ElasticsearchBackendClient#handleSampleResults(...) is called. It will override the es.sample.filter value set in the backendlistener configuration. * Specific fields ```field1;field2;field3` * Specify fields that you want to send to ElasticSearch (possible fields below) * AllThreads @@ -46,7 +46,7 @@ JMeter ElasticSearch Backend Listener is a JMeter plugin enabling you to send te * __info__ : Sends all samplers to the ElasticSearch engine, but only sends the headers, body info for the failed samplers. * __quiet__ : Only sends the response time, bytes, and other metrics * __error__ : Only sends the failing samplers to the ElasticSearch engine (Along with their headers and body information). -* Use either Kibana or Grafana to vizualize your results! +* Use either Kibana or Grafana to visualize your results! * [Click here to get a sample Grafana dashboard!](https://github.com/delirius325/jmeter-elasticsearch-backend-listener/wiki/JMeter-Generic-Dashboard) - All you need to do is import it into Grafana and change the data source! * Continuous Integration support - [Build comparison!](https://github.com/delirius325/jmeter-elasticsearch-backend-listener/wiki/Continuous-Integration---Build-Comparison) * Send JMeter variables to ElasticSearch! [Refer to this for more info!](https://github.com/delirius325/jmeter-elasticsearch-backend-listener/wiki/Sending-JMeter-variables) diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..308def6 --- /dev/null +++ b/install.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " + exit 1 +fi + +declare BENCHMARK_PROJECT_DIR=$1 + + +declare VERSION=$(xmllint --xpath "/*[local-name()='project']/*[local-name()='version']/text()" ./pom.xml) +echo "Building JMeter Elasticsearch Backend Listener plugin version: $VERSION" +declare TEMP_LOGS="$(mktemp)" +mvn package >${TEMP_LOGS} 2>&1 +if [[ $? -ne 0 ]]; then + echo "Maven build failed. See details below:" + cat ${TEMP_LOGS} + rm ${TEMP_LOGS} + exit 1 +fi +rm ${TEMP_LOGS} + +echo "Copying JMeter Elasticsearch Backend Listener plugin to JMeter lib/ext directory and benchmark resources" +cp ./target/jmeter.backendlistener.elasticsearch-$VERSION.jar $HOME/.sdkman/candidates/jmeter/current/lib/ext +cp ./target/jmeter.backendlistener.elasticsearch-$VERSION.jar $BENCHMARK_PROJECT_DIR/benchmarks/tools/benchmark-deployment/src/main/resources + +echo Checking installation... +if [[ ! -f "$HOME/.sdkman/candidates/jmeter/current/lib/ext/jmeter.backendlistener.elasticsearch-$VERSION.jar" ]]; then + echo "Error: JMeter Elasticsearch Backend Listener plugin not found in JMeter lib/ext directory" + exit 1 +fi + +ls -l $HOME/.sdkman/candidates/jmeter/current/lib/ext/jmeter.backendlistener.elasticsearch-$VERSION.jar + +if [[ ! -f "$BENCHMARK_PROJECT_DIR/benchmarks/tools/benchmark-deployment/src/main/resources/jmeter.backendlistener.elasticsearch-$VERSION.jar" ]]; then + echo "Error: JMeter Elasticsearch Backend Listener plugin not found in benchmark resources" + exit 1 +fi + +ls -l $BENCHMARK_PROJECT_DIR/benchmarks/tools/benchmark-deployment/src/main/resources/jmeter.backendlistener.elasticsearch-$VERSION.jar + +echo "JMeter Elasticsearch Backend Listener plugin installed successfully in $HOME/.sdkman/candidates/jmeter/current/lib/ext and in $BENCHMARK_PROJECT_DIR/benchmarks/tools/benchmark-deployment/src/main/resources" \ No newline at end of file diff --git a/src/main/java/io/github/delirius325/jmeter/backendlistener/elasticsearch/ElasticsearchBackendClient.java b/src/main/java/io/github/delirius325/jmeter/backendlistener/elasticsearch/ElasticsearchBackendClient.java index c458a35..bb3142b 100644 --- a/src/main/java/io/github/delirius325/jmeter/backendlistener/elasticsearch/ElasticsearchBackendClient.java +++ b/src/main/java/io/github/delirius325/jmeter/backendlistener/elasticsearch/ElasticsearchBackendClient.java @@ -1,9 +1,10 @@ - package io.github.delirius325.jmeter.backendlistener.elasticsearch; - -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +package io.github.delirius325.jmeter.backendlistener.elasticsearch; +import com.amazonaws.auth.AWS4Signer; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.http.AWSRequestSigningApacheInterceptor; +import com.google.gson.Gson; import org.apache.commons.io.FilenameUtils; import org.apache.http.HttpHost; import org.apache.http.HttpRequestInterceptor; @@ -20,11 +21,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.amazonaws.auth.AWS4Signer; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.http.AWSRequestSigningApacheInterceptor; -import com.google.gson.Gson; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class ElasticsearchBackendClient extends AbstractBackendListenerClient { @@ -56,6 +61,7 @@ public class ElasticsearchBackendClient extends AbstractBackendListenerClient { private static final Logger logger = LoggerFactory.getLogger(ElasticsearchBackendClient.class); private static final AWSCredentialsProvider credentialsProvider = new DefaultAWSCredentialsProviderChain(); private static final Map DEFAULT_ARGS = new LinkedHashMap<>(); + static { DEFAULT_ARGS.put(ES_SCHEME, "http"); DEFAULT_ARGS.put(ES_HOST, null); @@ -71,7 +77,7 @@ public class ElasticsearchBackendClient extends AbstractBackendListenerClient { DEFAULT_ARGS.put(ES_AUTH_PWD, ""); DEFAULT_ARGS.put(ES_PARSE_REQ_HEADERS, "false"); DEFAULT_ARGS.put(ES_PARSE_RES_HEADERS, "false"); - DEFAULT_ARGS.put(ES_AWS_ENDPOINT, ""); + DEFAULT_ARGS.put(ES_AWS_ENDPOINT, ""); DEFAULT_ARGS.put(ES_AWS_REGION, ""); DEFAULT_ARGS.put(ES_SSL_TRUSTSTORE_PATH, ""); DEFAULT_ARGS.put(ES_SSL_TRUSTSTORE_PW, ""); @@ -79,6 +85,7 @@ public class ElasticsearchBackendClient extends AbstractBackendListenerClient { DEFAULT_ARGS.put(ES_SSL_KEYSTORE_PW, ""); DEFAULT_ARGS.put(ES_SSL_VERIFICATION_MODE, "full"); } + private ElasticSearchMetricSender sender; private Set modes; private Set filters; @@ -107,7 +114,7 @@ public void setupTest(BackendListenerContext context) throws Exception { this.timeoutMs = Integer.parseInt((context.getParameter(ES_TIMEOUT_MS))); this.buildNumber = (JMeterUtils.getProperty(ElasticsearchBackendClient.BUILD_NUMBER) != null && !JMeterUtils.getProperty(ElasticsearchBackendClient.BUILD_NUMBER).trim().equals("")) - ? Integer.parseInt(JMeterUtils.getProperty(ElasticsearchBackendClient.BUILD_NUMBER)) : 0; + ? Integer.parseInt(JMeterUtils.getProperty(ElasticsearchBackendClient.BUILD_NUMBER)) : 0; setSSLConfiguration(context); @@ -123,8 +130,7 @@ public void setupTest(BackendListenerContext context) throws Exception { contextBuilder.loadTrustMaterial(TRUST_ALL_STRATEGY); httpAsyncClientBuilder.setSSLContext(contextBuilder.build()); httpAsyncClientBuilder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE); - } - catch (Exception e) { + } catch (Exception e) { // NOTE: purposedly ignored as this strategy does not use any custom algorithm // or certificate } @@ -165,19 +171,38 @@ public void onFailure(Node node) { } } + /** + * Method that converts a semicolon separated list contained in a System property {@link System#getProperty(String)} into a string set + * + * @param propertyName + * @param set + */ + private void convertSystemPropertyToSet(String propertyName, Set set) { + String value = System.getProperty(propertyName); + + convertValueToSet(propertyName, set, value); + } + /** * Method that converts a semicolon separated list contained in a parameter into a string set + * * @param context * @param parameter * @param set */ private void convertParameterToSet(BackendListenerContext context, String parameter, Set set) { - String[] array = (context.getParameter(parameter).contains(";")) ? context.getParameter(parameter).split(";") - : new String[] { context.getParameter(parameter) }; + String value = context.getParameter(parameter); + + convertValueToSet(parameter, set, value); + } + + private static void convertValueToSet(String parameter, Set set, String value) { + String[] array = (value.contains(";")) ? value.split(";") + : new String[]{value}; if (array.length > 0 && !array[0].trim().equals("")) { for (String entry : array) { set.add(entry.toLowerCase().trim()); - if(logger.isDebugEnabled()) + if (logger.isDebugEnabled()) logger.debug("Parsed from " + parameter + ": " + entry.toLowerCase().trim()); } } @@ -185,6 +210,7 @@ private void convertParameterToSet(BackendListenerContext context, String parame /** * Method that sets the SSL configuration to be able to send requests to a secured endpoint + * * @param context */ private void setSSLConfiguration(BackendListenerContext context) { @@ -228,7 +254,47 @@ private void setSSLConfiguration(BackendListenerContext context) { @Override public void handleSampleResults(List results, BackendListenerContext context) { + if (results.isEmpty()) { + logger.warn("There are no sampler results to handle!"); + return; + } + + List paramNames = new ArrayList<>(); + context.getParameterNamesIterator().forEachRemaining(paramNames::add); + paramNames.stream().sorted().forEach(name -> logger.info("Parameter: {} = {}", name, context.getParameter(name))); + + if (System.getProperty("SYS_DYNAMIC_ES_PER_SAMPLE_FILTER") != null) { + // Override filters from ES_SAMPLE_FILTER if exists + this.filters.clear(); + logger.info("============> Property SYS_DYNAMIC_ES_PER_SAMPLE_FILTER filters: {}", System.getProperty("SYS_DYNAMIC_ES_PER_SAMPLE_FILTER")); + convertSystemPropertyToSet("SYS_DYNAMIC_ES_PER_SAMPLE_FILTER", this.filters); + logger.info("============> Property SYS_DYNAMIC_ES_PER_SAMPLE_FILTER filters: {}", this.filters); + } + + final String filtersAsString = (this.filters.isEmpty()) ? "no filters" : String.join(", ", this.filters); for (SampleResult sr : results) { + String respString = sr.getResponseDataAsString(); + if (sr.getBytesAsLong() == 0 || respString == null) { + logger.error("SampleResult has no response data. Details: label={}, success={}, responseCode={}, responseMessage={}, " + + "startTime={}, endTime={}, threadName={}, bytes={}, dataType={}, samplerData={}, requestHeaders={}, responseHeaders={}", + sr.getSampleLabel(), + sr.isSuccessful(), + sr.getResponseCode(), + sr.getResponseMessage(), + sr.getStartTime(), + sr.getEndTime(), + sr.getThreadName(), + sr.getBytesAsLong(), + sr.getDataType(), + sr.getSamplerData(), + sr.getRequestHeaders(), + sr.getResponseHeaders()); + throw new IllegalStateException("SampleResult has no response data for label: " + sr.getSampleLabel()); + } + + int endIndex = Math.min(100, respString.length()); + respString = respString.substring(0, endIndex); + logger.info("Handle sampler result ({}) {}: {}", endIndex, sr.getSampleLabel(), respString); ElasticSearchMetric metric = new ElasticSearchMetric(sr, context.getParameter(ES_TEST_MODE), context.getParameter(ES_TIMESTAMP), this.buildNumber, context.getBooleanParameter(ES_PARSE_REQ_HEADERS, false), @@ -242,6 +308,9 @@ public void handleSampleResults(List results, BackendListenerConte "The ElasticSearch Backend Listener was unable to add sampler to the list of samplers to send... More info in JMeter's console."); e.printStackTrace(); } + logger.info("Sampler result processed {} with filters {} (ES_SAMPLE_FILTER {})", sr.getSampleLabel(), filtersAsString, context.getParameter("es.sample.filter")); + } else { + logger.info("Sampler result skipped {} with filters {} (ES_SAMPLE_FILTER {})", sr.getSampleLabel(), filtersAsString, context.getParameter(ES_SAMPLE_FILTER)); } } @@ -249,7 +318,7 @@ public void handleSampleResults(List results, BackendListenerConte try { this.sender.sendRequest(this.esVersion); } catch (Exception e) { - logger.error("Error occured while sending bulk request.", e); + logger.error("Error occurred while sending bulk request.", e); } finally { this.sender.clearList(); } @@ -267,9 +336,8 @@ public void teardownTest(BackendListenerContext context) throws Exception { /** * This method checks if the test mode is valid - * - * @param mode - * The test mode as String + * + * @param mode The test mode as String */ private void checkTestMode(String mode) { if (!this.modes.contains(mode)) { @@ -285,26 +353,28 @@ private void checkTestMode(String mode) { /** * This method will validate the current sample to see if it is part of the filters or not. - * - * @param context - * The Backend Listener's context - * @param sr - * The current SampleResult - * @return true or false depending on whether or not the sample is valid + * + * @param context The Backend Listener's context + * @param sr The current SampleResult + * @return true or false depending on whether the sample is valid */ private boolean validateSample(BackendListenerContext context, SampleResult sr) { boolean valid = true; String sampleLabel = sr.getSampleLabel().toLowerCase().trim(); - if (this.filters.size() > 0) { + if (!this.filters.isEmpty()) { for (String filter : filters) { - Pattern pattern = Pattern.compile(filter); + Pattern pattern = Pattern.compile(String.format(".*%s.*", filter)); Matcher matcher = pattern.matcher(sampleLabel); - if (!sampleLabel.startsWith("!!") && (sampleLabel.contains(filter) || matcher.find())) { - valid = true; + if (matcher.find()) { + // README.md#Features*Filters + // You can also choose to exclude certain samplers; `!!exclude_this;filter1;filter2` + valid = !sampleLabel.startsWith("!!"); break; } else { + logger.warn(String.format("Filter out sample label %s, it may start with '!!' or it may not match the filter %s. " + + "Check the %s property value in the JMX file. Leave it empty to increase validation", sampleLabel, filter, ES_SAMPLE_FILTER)); valid = false; } }