Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#14, #19 - adding tags to a measurement point using an generic argument "optionalTags" #29

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,71 @@
# JMeter-InfluxDB-Writer
Plugin for JMeter that allows to write load test data on-the-fly to influxDB.

## Installation ##
After you have installed JMeter on your machine put a plugin JAR-file (for example *JMeter-InfluxDB-Writer-plugin-1.2.jar*)
into the *[JMETER_HOME]/lib/ext* directory and then start JMeter.

The plugin can bee seen when you add a Backend-Listener element to your test plan. In the JMeter's GUI you see
a drop-down list named Backend Listener implementation. One of the entries must be
**rocks.nt.apm.jmeter.JMeterInfluxDBBackendListenerClient**.

## Configuration of the Listener ##
It is recommended that you add only one Backend-Listener element for your whole test plan. Thus you get a single point
of service for all your samplers. After you select from the implementation drop-down list the plugin you have
to configure it.

Please note, when the plugin runs it creates automatically (on the very first run) three tables filled later on with
statistic performance data:
* requestsRaw - detailed information about performance of the application under test (its version, error count, sampler/request names, hardware hostname etc.)
* testStartEnd - keeps time stamps about start and end of specific test runs associated with run-Ids (see below _runId_).
* virtualUsers - keeps time stamps about virtual users (also called threads or parallel clients) in specific test runs associated with run-Ids.

The plugin has the following attributes to be filled out in:
#### testName ####
You may put here the name of you test plan.

#### nodeName ####
For example, here you may place name of the host/machine where the test runs, thus having a specific information
about the hardware and therefore additional performance data about the software.

#### runId ###
Descriptor/Id for the test run

#### optionalTags ####
This attribute is empty per default. But you may specify here additional tags for the InfluxDB-table _requestsRaw_
to have more descriptors (or querying possibilities) for your statistic data. This is a text with key-value pairs
delimited by _coma, colon or semicolon_. Whereas key is a tag to be recorded in the InfluxDB-database and value is
its value for the current measurement.

For ex. "appVersion=4.1.11;testdataVersion=3.0" means that the InfluxDB-table _requestsRaw_ gets two tags "appVersion"
and "testdataVersion" and every measurement gets for these two tags values "4.1.11" and "3.0" correspondingly.

#### influxDBHost ####
Host where InfluxDB runs.

#### influxDBPort ####
Port of the InfluxDB to be used.

#### influxDBUser ####
Database user.

#### influxDBPassword ####
Database user's password.

#### influxDBDatabase ####
Name of the database to be filled with the performance data. Note that it must be created before the test starts.
Otherwise an error would be reported.

#### retentionPolicy ####
Descriptor about how long the statistical data should be stored and when its compression starts. Read more in
the documentation of the InfluxDB project.

#### samplersList ####
A regex-string describing which samplers should be caught by the given Backend Listener.

#### useRegexForSamplerList ####
A flag (_true_ or _false_) indicating that the attribute **samplersList** (see above) is a regex or a simple text string.

#### recordSubSamples ####
A flag (_true_ or _false_) indicating whether sub-samplers should be caught by the given Backend Listener thus making +
the collected performance data more granular.
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
package rocks.nt.apm.jmeter;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.threads.JMeterContextService;
Expand All @@ -21,17 +12,20 @@
import org.influxdb.InfluxDBFactory;
import org.influxdb.dto.Point;
import org.influxdb.dto.Point.Builder;

import rocks.nt.apm.jmeter.config.influxdb.InfluxDBConfig;
import rocks.nt.apm.jmeter.config.influxdb.RequestMeasurement;
import rocks.nt.apm.jmeter.config.influxdb.TestStartEndMeasurement;
import rocks.nt.apm.jmeter.config.influxdb.VirtualUsersMeasurement;

import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
* Backend listener that writes JMeter metrics to influxDB directly.
*
* @author Alexander Wert
*
* @author Alexander Wert
*/
public class JMeterInfluxDBBackendListenerClient extends AbstractBackendListenerClient implements Runnable {
/**
Expand All @@ -45,6 +39,7 @@ public class JMeterInfluxDBBackendListenerClient extends AbstractBackendListener
private static final String KEY_USE_REGEX_FOR_SAMPLER_LIST = "useRegexForSamplerList";
private static final String KEY_TEST_NAME = "testName";
private static final String KEY_RUN_ID = "runId";
private static final String KEY_TAGS_LISTING = "optionalTags";
private static final String KEY_NODE_NAME = "nodeName";
private static final String KEY_SAMPLERS_LIST = "samplersList";
private static final String KEY_RECORD_SUB_SAMPLES = "recordSubSamples";
Expand All @@ -65,17 +60,23 @@ public class JMeterInfluxDBBackendListenerClient extends AbstractBackendListener
*/
private String testName;

/**
* A unique identifier for a single execution (aka 'run') of a load test.
* In a CI/CD automated performance test, a Jenkins or Bamboo build id would be a good value for this.
*/
private String runId;
/**
* A unique identifier for a single execution (aka 'run') of a load test.
* In a CI/CD automated performance test, a Jenkins or Bamboo build id would be a good value for this.
*/
private String runId;

/**
* Name of the name
* Name of the node. It could be for ex. a name or IP-address of a specific server where the tests run.
*/
private String nodeName;

/**
* Contains an optional list of tags to be recorded in the InfluxDB. Each tag entry is a pair [TAG_NAME]=[TAG_VALUE].
* Delimiter is comma (,) colon (:) or semicolon (;).
*/
private String tagsListing;

/**
* List of samplers to record.
*/
Expand Down Expand Up @@ -107,7 +108,7 @@ public class JMeterInfluxDBBackendListenerClient extends AbstractBackendListener
private Random randomNumberGenerator;

/**
* Indicates whether to record Subsamples
* Indicates whether to record Sub-samples
*/
private boolean recordSubSamples;

Expand All @@ -118,29 +119,31 @@ public void handleSampleResults(List<SampleResult> sampleResults, BackendListene
// Gather all the listeners
List<SampleResult> allSampleResults = new ArrayList<SampleResult>();
for (SampleResult sampleResult : sampleResults) {
allSampleResults.add(sampleResult);
allSampleResults.add(sampleResult);

if(recordSubSamples) {
if (recordSubSamples) {
for (SampleResult subResult : sampleResult.getSubResults()) {
allSampleResults.add(subResult);
}
}
}
}

for(SampleResult sampleResult: allSampleResults) {
getUserMetrics().add(sampleResult);
for (SampleResult sampleResult : allSampleResults) {
getUserMetrics().add(sampleResult);

if ((null != regexForSamplerList && sampleResult.getSampleLabel().matches(regexForSamplerList)) || samplersToFilter.contains(sampleResult.getSampleLabel())) {
Point point = Point.measurement(RequestMeasurement.MEASUREMENT_NAME).time(
Map<String, String> tags = processOptionalTags(tagsListing);
Builder pointBuilder = Point.measurement(RequestMeasurement.MEASUREMENT_NAME).time(
System.currentTimeMillis() * ONE_MS_IN_NANOSECONDS + getUniqueNumberForTheSamplerThread(), TimeUnit.NANOSECONDS)
.tag(RequestMeasurement.Tags.REQUEST_NAME, sampleResult.getSampleLabel())
.addField(RequestMeasurement.Fields.ERROR_COUNT, sampleResult.getErrorCount())
.addField(RequestMeasurement.Fields.ERROR_COUNT, sampleResult.getErrorCount())
.addField(RequestMeasurement.Fields.THREAD_NAME, sampleResult.getThreadName())
.tag(RequestMeasurement.Tags.RUN_ID, runId)
.tag(RequestMeasurement.Tags.TEST_NAME, testName)
.addField(RequestMeasurement.Fields.NODE_NAME, nodeName)
.addField(RequestMeasurement.Fields.RESPONSE_TIME, sampleResult.getTime()).build();
influxDB.write(influxDBConfig.getInfluxDatabase(), influxDBConfig.getInfluxRetentionPolicy(), point);
.addField(RequestMeasurement.Fields.RESPONSE_TIME, sampleResult.getTime());
addOptionalTagsToPoint(pointBuilder, tags);
influxDB.write(influxDBConfig.getInfluxDatabase(), influxDBConfig.getInfluxRetentionPolicy(), pointBuilder.build());
}
}
}
Expand All @@ -151,6 +154,7 @@ public Arguments getDefaultParameters() {
arguments.addArgument(KEY_TEST_NAME, "Test");
arguments.addArgument(KEY_NODE_NAME, "Test-Node");
arguments.addArgument(KEY_RUN_ID, "R001");
arguments.addArgument(KEY_TAGS_LISTING, "");
arguments.addArgument(InfluxDBConfig.KEY_INFLUX_DB_HOST, "localhost");
arguments.addArgument(InfluxDBConfig.KEY_INFLUX_DB_PORT, Integer.toString(InfluxDBConfig.DEFAULT_PORT));
arguments.addArgument(InfluxDBConfig.KEY_INFLUX_DB_USER, "");
Expand All @@ -166,10 +170,10 @@ public Arguments getDefaultParameters() {
@Override
public void setupTest(BackendListenerContext context) throws Exception {
testName = context.getParameter(KEY_TEST_NAME, "Test");
runId = context.getParameter(KEY_RUN_ID,"R001"); //Will be used to compare performance of R001, R002, etc of 'Test'
runId = context.getParameter(KEY_RUN_ID, "R001"); //Will be used to compare performance of R001, R002, etc of 'Test'
randomNumberGenerator = new Random();
nodeName = context.getParameter(KEY_NODE_NAME, "Test-Node");

tagsListing = context.getParameter(KEY_TAGS_LISTING, "");

setupInfluxClient(context);
influxDB.write(
Expand All @@ -179,8 +183,7 @@ public void setupTest(BackendListenerContext context) throws Exception {
.tag(TestStartEndMeasurement.Tags.TYPE, TestStartEndMeasurement.Values.STARTED)
.tag(TestStartEndMeasurement.Tags.NODE_NAME, nodeName)
.tag(TestStartEndMeasurement.Tags.TEST_NAME, testName)
.addField(TestStartEndMeasurement.Fields.PLACEHOLDER, "1")
.build());
.addField(TestStartEndMeasurement.Fields.PLACEHOLDER, "1").build());

parseSamplers(context);
scheduler = Executors.newScheduledThreadPool(1);
Expand All @@ -196,7 +199,7 @@ public void teardownTest(BackendListenerContext context) throws Exception {
LOGGER.info("Shutting down influxDB scheduler...");
scheduler.shutdown();

addVirtualUsersMetrics(0,0,0,0,JMeterContextService.getThreadCounts().finishedThreads);
addVirtualUsersMetrics(0, 0, 0, 0, JMeterContextService.getThreadCounts().finishedThreads);
influxDB.write(
influxDBConfig.getInfluxDatabase(),
influxDBConfig.getInfluxRetentionPolicy(),
Expand All @@ -205,7 +208,7 @@ public void teardownTest(BackendListenerContext context) throws Exception {
.tag(TestStartEndMeasurement.Tags.NODE_NAME, nodeName)
.tag(TestStartEndMeasurement.Tags.RUN_ID, runId)
.tag(TestStartEndMeasurement.Tags.TEST_NAME, testName)
.addField(TestStartEndMeasurement.Fields.PLACEHOLDER,"1")
.addField(TestStartEndMeasurement.Fields.PLACEHOLDER, "1")
.build());

influxDB.disableBatch();
Expand Down Expand Up @@ -234,9 +237,8 @@ public void run() {

/**
* Setup influxDB client.
*
* @param context
* {@link BackendListenerContext}.
*
* @param context {@link BackendListenerContext}.
*/
private void setupInfluxClient(BackendListenerContext context) {
influxDBConfig = new InfluxDBConfig(context);
Expand All @@ -247,9 +249,8 @@ private void setupInfluxClient(BackendListenerContext context) {

/**
* Parses list of samplers.
*
* @param context
* {@link BackendListenerContext}.
*
* @param context {@link BackendListenerContext}.
*/
private void parseSamplers(BackendListenerContext context) {
samplersList = context.getParameter(KEY_SAMPLERS_LIST, "");
Expand Down Expand Up @@ -298,4 +299,45 @@ private void createDatabaseIfNotExistent() {
private int getUniqueNumberForTheSamplerThread() {
return randomNumberGenerator.nextInt(ONE_MS_IN_NANOSECONDS);
}

/**
* Splits a passed string to key-value pairs whereas a delimited by coma, colon or semicolon.
* Whereas key is a tag to be recorded in the InfluxDB-database and value is its value.
* For ex. "appVersion=4.1.11;testdataVersion=3.0" means that the InfluxDB gets two tags "appVersion" and "testdataVersion"
* with values.
*
* @param listOfOptionalTags
* @return a map object of [tag]-[value] pairs.
*/
private Map<String, String> processOptionalTags(String listOfOptionalTags) {
final String tagPairsDelimiterRegex = ",|;|:"; //"\\-|\\+"
final String keyValueDelimiterRegex = "=";
Map<String, String> result = new HashMap<>();
if (listOfOptionalTags == null)
return result;

String[] tags = listOfOptionalTags.split(tagPairsDelimiterRegex);
for (int i = 0; i < tags.length; i++) {
String[] singleTag = tags[i].split(keyValueDelimiterRegex);
if (singleTag.length == 2) {
result.put(singleTag[0].trim(), singleTag[1].trim());
}
}
return result;
}

/**
* If the argument "optionalTags" set in the JMeter-GUI contains some valid tag data then this data gets added
* to the current measurement point.
*
* @param pointBuilder a measurement point object.
* @param tags a map of [tag]-[value] pairs.
*/
private void addOptionalTagsToPoint(Builder pointBuilder, Map<String, String> tags) {
Iterator it = tags.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> pair = (Map.Entry) it.next();
pointBuilder.tag(pair.getKey(), pair.getValue());
}
}
}