Skip to content

Commit 08cf5d3

Browse files
Refactored plugin, segmented in different parts, less confusing
1 parent 81df714 commit 08cf5d3

File tree

5 files changed

+424
-337
lines changed

5 files changed

+424
-337
lines changed
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package io.github.delirius325.jmeter.backendlistener.elasticsearch;
2+
3+
import org.apache.jmeter.assertions.AssertionResult;
4+
import org.apache.jmeter.samplers.SampleResult;
5+
import org.apache.jmeter.threads.JMeterContextService;
6+
import org.apache.jmeter.visualizers.backend.BackendListenerContext;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
10+
import java.net.InetAddress;
11+
import java.text.ParseException;
12+
import java.text.SimpleDateFormat;
13+
import java.time.LocalDateTime;
14+
import java.time.format.DateTimeFormatter;
15+
import java.util.*;
16+
17+
public class ElasticSearchMetric {
18+
private static final Logger logger = LoggerFactory.getLogger(ElasticsearchBackendClient.class);
19+
20+
private SampleResult sampleResult;
21+
private String esTestMode;
22+
private String esTimestamp;
23+
private int ciBuildNumber;
24+
private HashMap<String, Object> json;
25+
26+
public ElasticSearchMetric(SampleResult sr, String testMode, String timeStamp, int buildNumber) {
27+
this.sampleResult = sr;
28+
this.esTestMode = testMode;
29+
this.esTimestamp = timeStamp;
30+
this.ciBuildNumber = buildNumber;
31+
this.json = new HashMap<>();
32+
}
33+
34+
/**
35+
* This method returns the current metric as JSON (Map<String, Object>) for the provided sampleResult
36+
* @param context BackendListenerContext
37+
* @return a JSON Object as Map<String, Object>
38+
*/
39+
public Map<String, Object> getMetric(BackendListenerContext context) throws Exception {
40+
SimpleDateFormat sdf = new SimpleDateFormat(this.esTimestamp);
41+
42+
//add all the default SampleResult parameters
43+
this.json.put("AllThreads", this.sampleResult.getAllThreads());
44+
this.json.put("BodySize", this.sampleResult.getBodySizeAsLong());
45+
this.json.put("Bytes", this.sampleResult.getBytesAsLong());
46+
this.json.put("SentBytes", this.sampleResult.getSentBytes());
47+
this.json.put("ConnectTime", this.sampleResult.getConnectTime());
48+
this.json.put("ContentType", this.sampleResult.getContentType());
49+
this.json.put("DataType", this.sampleResult.getDataType());
50+
this.json.put("ErrorCount", this.sampleResult.getErrorCount());
51+
this.json.put("GrpThreads", this.sampleResult.getGroupThreads());
52+
this.json.put("IdleTime", this.sampleResult.getIdleTime());
53+
this.json.put("Latency", this.sampleResult.getLatency());
54+
this.json.put("ResponseTime", this.sampleResult.getTime());
55+
this.json.put("SampleCount", this.sampleResult.getSampleCount());
56+
this.json.put("SampleLabel", this.sampleResult.getSampleLabel());
57+
this.json.put("ThreadName", this.sampleResult.getThreadName());
58+
this.json.put("URL", this.sampleResult.getURL());
59+
this.json.put("ResponseCode", this.sampleResult.getResponseCode());
60+
this.json.put("StartTime", sdf.format(new Date(this.sampleResult.getStartTime())));
61+
this.json.put("EndTime", sdf.format(new Date(this.sampleResult.getEndTime())));
62+
this.json.put("Timestamp", sdf.format(new Date(this.sampleResult.getTimeStamp())));
63+
this.json.put("InjectorHostname", InetAddress.getLocalHost().getHostName());
64+
65+
// Add the details according to the mode that is set
66+
if(this.esTestMode == "debug" || this.esTestMode == "error") {
67+
addDetails();
68+
} else if (this.esTestMode == "info") {
69+
if(!this.sampleResult.isSuccessful()) {
70+
addDetails();
71+
}
72+
} else if (this.esTestMode != "quiet") {
73+
logger.warn("The parameter \"es.test.mode\" isn't set properly. Three modes are allowed: debug ,info, and quiet.");
74+
logger.warn(" -- \"debug\": sends request and response details to ElasticSearch. Info only sends the details if the response has an error.");
75+
logger.warn(" -- \"info\": should be used in production");
76+
logger.warn(" -- \"quiet\": should be used if you don't care to have the details.");
77+
}
78+
79+
addAssertions();
80+
addElapsedTime(sdf);
81+
addCustomFields(context);
82+
83+
return this.json;
84+
}
85+
86+
/**
87+
* This method adds all the assertions for the current sampleResult
88+
*
89+
*/
90+
private void addAssertions() {
91+
AssertionResult[] assertionResults = this.sampleResult.getAssertionResults();
92+
if(assertionResults != null) {
93+
Map<String, Object>[] assertionArray = new HashMap[assertionResults.length];
94+
Integer i = 0;
95+
for(AssertionResult assertionResult : assertionResults) {
96+
HashMap<String, Object> assertionMap = new HashMap<>();
97+
boolean failure = assertionResult.isFailure() || assertionResult.isError();
98+
assertionMap.put("failure", failure);
99+
assertionMap.put("failureMessage", assertionResult.getFailureMessage());
100+
assertionMap.put("name", assertionResult.getName());
101+
assertionArray[i] = assertionMap;
102+
i++;
103+
}
104+
this.json.put("AssertionResults", assertionArray);
105+
}
106+
}
107+
108+
/**
109+
* This method adds the ElapsedTime as a key:value pair in the JSON object. Also,
110+
* depending on whether or not the tests were launched from a CI tool (i.e Jenkins),
111+
* it will add a hard-coded version of the ElapsedTime for results comparison purposes
112+
*
113+
* @param sdf SimpleDateFormat
114+
*/
115+
private void addElapsedTime(SimpleDateFormat sdf) {
116+
Date elapsedTime;
117+
118+
if(this.ciBuildNumber != 0) {
119+
elapsedTime = getElapsedTime(true);
120+
this.json.put("BuildNumber", this.ciBuildNumber);
121+
122+
if(elapsedTime != null)
123+
this.json.put("ElapsedTimeComparison", sdf.format(elapsedTime));
124+
}
125+
126+
elapsedTime = getElapsedTime(false);
127+
if(elapsedTime != null)
128+
this.json.put("ElapsedTime", sdf.format(elapsedTime));
129+
}
130+
131+
/**
132+
* Methods that add all custom fields added by the user in the Backend Listener's GUI panel
133+
*
134+
* @param context BackendListenerContext
135+
*/
136+
private void addCustomFields(BackendListenerContext context) {
137+
Iterator<String> pluginParameters = context.getParameterNamesIterator();
138+
while(pluginParameters.hasNext()) {
139+
String parameterName = pluginParameters.next();
140+
141+
if(!parameterName.contains("es.") && !context.getParameter(parameterName).trim().equals("")) {
142+
this.json.put(parameterName, context.getParameter(parameterName).trim());
143+
}
144+
}
145+
}
146+
147+
148+
/**
149+
* Method that adds the request and response's body/headers
150+
*
151+
*/
152+
private void addDetails() {
153+
this.json.put("RequestHeaders", this.sampleResult.getRequestHeaders());
154+
this.json.put("RequestBody", this.sampleResult.getSamplerData());
155+
this.json.put("ResponseHeaders", this.sampleResult.getResponseHeaders());
156+
this.json.put("ResponseBody", this.sampleResult.getResponseDataAsString());
157+
this.json.put("ResponseMessage", this.sampleResult.getResponseMessage());
158+
}
159+
160+
/**
161+
* This method is meant to return the elapsed time a human readable format. The purpose of this is
162+
* mostly for build comparison in Kibana. By doing this, the user is able to set the X-axis of his graph
163+
* to this date and split the series by build numbers. It allows him to overlap test results and see if
164+
* there is regression or not.
165+
*
166+
* @param forBuildComparison boolean to determine if there is CI (continuous integration) or not
167+
* @return The elapsed time in YYYY-MM-dd HH:mm:ss format
168+
*/
169+
protected Date getElapsedTime(boolean forBuildComparison) {
170+
String sElapsed;
171+
//Calculate the elapsed time (Starting from midnight on a random day - enables us to compare of two loads over their duration)
172+
long start = JMeterContextService.getTestStartTime();
173+
long end = System.currentTimeMillis();
174+
long elapsed = (end - start);
175+
long minutes = (elapsed / 1000) / 60;
176+
long seconds = (elapsed / 1000) % 60;
177+
178+
Calendar cal = Calendar.getInstance();
179+
cal.set(Calendar.HOUR_OF_DAY, 0); //If there is more than an hour of data, the number of minutes/seconds will increment this
180+
cal.set(Calendar.MINUTE, (int) minutes);
181+
cal.set(Calendar.SECOND, (int) seconds);
182+
183+
if(forBuildComparison) {
184+
sElapsed = String.format("2017-01-01 %02d:%02d:%02d",
185+
cal.get(Calendar.HOUR_OF_DAY),
186+
cal.get(Calendar.MINUTE),
187+
cal.get(Calendar.SECOND));
188+
} else {
189+
sElapsed = String.format("%s %02d:%02d:%02d",
190+
DateTimeFormatter.ofPattern("yyyy-mm-dd").format(LocalDateTime.now()),
191+
cal.get(Calendar.HOUR_OF_DAY),
192+
cal.get(Calendar.MINUTE),
193+
cal.get(Calendar.SECOND));
194+
}
195+
196+
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
197+
try {
198+
return formatter.parse(sElapsed);
199+
} catch (ParseException e) {
200+
logger.error("Unexpected error occured computing elapsed date", e);
201+
return null;
202+
}
203+
}
204+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.github.delirius325.jmeter.backendlistener.elasticsearch;
2+
3+
import org.apache.http.HttpEntity;
4+
import org.apache.http.HttpStatus;
5+
import org.apache.http.entity.ContentType;
6+
import org.apache.http.nio.entity.NStringEntity;
7+
import org.apache.jmeter.visualizers.backend.BackendListenerContext;
8+
import org.elasticsearch.client.Response;
9+
import org.elasticsearch.client.RestClient;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
13+
import java.io.IOException;
14+
15+
import java.util.*;
16+
17+
public class ElasticSearchMetricSender {
18+
private static final Logger logger = LoggerFactory.getLogger(ElasticsearchBackendClient.class);
19+
20+
private RestClient client;
21+
private String esIndex;
22+
private List<String> metricList;
23+
24+
public ElasticSearchMetricSender(RestClient cli, String index) {
25+
this.client = cli;
26+
this.esIndex = index;
27+
this.metricList = new LinkedList<String>();
28+
}
29+
30+
public int getListSize() {
31+
return this.metricList.size();
32+
}
33+
34+
public void clearList() {
35+
this.metricList.clear();
36+
}
37+
38+
public void addToList(String metric) {
39+
this.metricList.add(metric);
40+
}
41+
42+
public void createIndex() {
43+
try {
44+
this.client.performRequest("PUT", "/"+ this.esIndex);
45+
} catch (Exception e) {
46+
logger.info("Index already exists!");
47+
}
48+
}
49+
50+
public void sendRequest() throws IOException {
51+
String actionMetaData = String.format("{ \"index\" : { \"_index\" : \"%s\", \"_type\" : \"%s\" } }%n", this.esIndex, "SampleResult");
52+
53+
StringBuilder bulkRequestBody = new StringBuilder();
54+
for (String metric : this.metricList) {
55+
bulkRequestBody.append(actionMetaData);
56+
bulkRequestBody.append(metric);
57+
bulkRequestBody.append("\n");
58+
}
59+
60+
HttpEntity entity = new NStringEntity(bulkRequestBody.toString(), ContentType.APPLICATION_JSON);
61+
try {
62+
Response response = this.client.performRequest("POST", "/"+ this.esIndex +"/SampleResult/_bulk", Collections.emptyMap(), entity);
63+
if(response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
64+
if(logger.isErrorEnabled()) {
65+
logger.error("ElasticSearch Backend Listener failed to write results for index {}", this.esIndex);
66+
}
67+
}
68+
} catch (Exception e) {
69+
if(logger.isErrorEnabled()) {
70+
logger.error("ElasticSearch Backend Listener was unable to perform request to the ElasticSearch engine. Request reached timeout.");
71+
}
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)