Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bash

if [[ $# -ne 1 ]]; then
echo "Usage: $0 <BENCHMARK_PROJECT_DIR>"
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"
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -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<String, String> DEFAULT_ARGS = new LinkedHashMap<>();

static {
DEFAULT_ARGS.put(ES_SCHEME, "http");
DEFAULT_ARGS.put(ES_HOST, null);
Expand All @@ -71,14 +77,15 @@ 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, "");
DEFAULT_ARGS.put(ES_SSL_KEYSTORE_PATH, "");
DEFAULT_ARGS.put(ES_SSL_KEYSTORE_PW, "");
DEFAULT_ARGS.put(ES_SSL_VERIFICATION_MODE, "full");
}

private ElasticSearchMetricSender sender;
private Set<String> modes;
private Set<String> filters;
Expand Down Expand Up @@ -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);

Expand All @@ -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
}
Expand Down Expand Up @@ -165,26 +171,46 @@ 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<String> 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<String> 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<String> 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());
}
}
}

/**
* Method that sets the SSL configuration to be able to send requests to a secured endpoint
*
* @param context
*/
private void setSSLConfiguration(BackendListenerContext context) {
Expand Down Expand Up @@ -228,7 +254,47 @@ private void setSSLConfiguration(BackendListenerContext context) {

@Override
public void handleSampleResults(List<SampleResult> results, BackendListenerContext context) {
if (results.isEmpty()) {
logger.warn("There are no sampler results to handle!");
return;
}

List<String> 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),
Expand All @@ -242,14 +308,17 @@ public void handleSampleResults(List<SampleResult> 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));
}
}

if (this.sender.getListSize() >= this.bulkSize) {
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();
}
Expand All @@ -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)) {
Expand All @@ -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;
}
}
Expand Down