Skip to content

Commit 7f9dac1

Browse files
committed
Initial commit.
0 parents  commit 7f9dac1

File tree

11 files changed

+504
-0
lines changed

11 files changed

+504
-0
lines changed

LICENSE.md

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
The MIT License (MIT)
2+
Copyright (c) 2016 Eugene Morozov
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in all
12+
copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
SOFTWARE.

README.md

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Anomaly Detection in Performance Test Data
2+
3+
## Overview
4+
5+
Example of Spring Boot, Cucumber, JMeter and R integration that demonstrates
6+
anomaly detection in performance test data.
7+
8+
This demo was shown at DataScienceLab Data Science in FinTech meetup
9+
(http://www.meetup.com/data-science-lab/events/227113953).
10+
11+
## Configuration
12+
13+
Configuration is hardcoded to demo the integration, update JMeter and Rscript
14+
paths in `src/test/java/com/emorozov/datasciencelab/DataScienceLabDemoTestPerformanceAnomaliesSteps.java`
15+
before you start.
16+
17+
## R Integration
18+
19+
Once R is installed, install the packages manually prior to running Cucumeber files.
20+
This is a workaround for package installation failing from Java Runtime.exec().
21+
22+
```
23+
install.packages('dplyr')
24+
install.packages('tidyr')
25+
install.packages('mvoutlier')
26+
install.packages('jsonlite')
27+
```
28+
29+
## R Specifics
30+
31+
Since sample is very small (only 50 runs taking about 10 seconds), CPU metrics
32+
tend to introduce significant number of outliers.
33+
34+
In the current implementation cpu metrics are excluded in the
35+
`performance-anomalies.R` script.
36+
37+
## Todo
38+
39+
1. Create more generic Cucumber step definitions, so that mvoutlier
40+
parameters can be passed into the script from feature files.
41+
1. Make sure that dependencies are checked in R script and installed
42+
automatically.
43+
1. Refactor config, so paths are not hardcoded.

pom.xml

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<groupId>com.example</groupId>
7+
<artifactId>datasciencelab-demo</artifactId>
8+
<version>0.0.1-SNAPSHOT</version>
9+
10+
<parent>
11+
<groupId>org.springframework.boot</groupId>
12+
<artifactId>spring-boot-starter-parent</artifactId>
13+
<version>1.3.1.RELEASE</version>
14+
</parent>
15+
16+
<properties>
17+
<maven.compiler.plugin.version>3.3</maven.compiler.plugin.version>
18+
<lombok.version>1.16.6</lombok.version>
19+
<junit.version>4.12</junit.version>
20+
<hamcrest.version>1.3</hamcrest.version>
21+
<jmeter.version>2.13</jmeter.version>
22+
<cucumber.java.version>1.2.4</cucumber.java.version>
23+
</properties>
24+
25+
<build>
26+
<plugins>
27+
<plugin>
28+
<groupId>org.apache.maven.plugins</groupId>
29+
<artifactId>maven-compiler-plugin</artifactId>
30+
<version>${maven.compiler.plugin.version}</version>
31+
<configuration>
32+
<source>1.8</source>
33+
<target>1.8</target>
34+
</configuration>
35+
</plugin>
36+
</plugins>
37+
</build>
38+
39+
<dependencies>
40+
<dependency>
41+
<groupId>org.springframework.boot</groupId>
42+
<artifactId>spring-boot-starter-web</artifactId>
43+
</dependency>
44+
<dependency>
45+
<groupId>org.projectlombok</groupId>
46+
<artifactId>lombok</artifactId>
47+
<version>${lombok.version}</version>
48+
<scope>provided</scope>
49+
</dependency>
50+
<dependency>
51+
<groupId>org.apache.jmeter</groupId>
52+
<artifactId>ApacheJMeter_monitors</artifactId>
53+
<version>${jmeter.version}</version>
54+
<scope>test</scope>
55+
<exclusions>
56+
<exclusion>
57+
<artifactId>commons-math3</artifactId>
58+
<groupId>commons-math3</groupId>
59+
</exclusion>
60+
<exclusion>
61+
<artifactId>commons-pool2</artifactId>
62+
<groupId>commons-pool2</groupId>
63+
</exclusion>
64+
</exclusions>
65+
</dependency>
66+
<dependency>
67+
<groupId>org.apache.jmeter</groupId>
68+
<artifactId>ApacheJMeter_java</artifactId>
69+
<version>${jmeter.version}</version>
70+
<scope>test</scope>
71+
<exclusions>
72+
<exclusion>
73+
<artifactId>commons-math3</artifactId>
74+
<groupId>commons-math3</groupId>
75+
</exclusion>
76+
<exclusion>
77+
<artifactId>commons-pool2</artifactId>
78+
<groupId>commons-pool2</groupId>
79+
</exclusion>
80+
</exclusions>
81+
</dependency>
82+
<dependency>
83+
<groupId>junit</groupId>
84+
<artifactId>junit</artifactId>
85+
<version>${junit.version}</version>
86+
<scope>test</scope>
87+
</dependency>
88+
<dependency>
89+
<groupId>org.hamcrest</groupId>
90+
<artifactId>hamcrest-all</artifactId>
91+
<version>${hamcrest.version}</version>
92+
</dependency>
93+
<dependency>
94+
<groupId>info.cukes</groupId>
95+
<artifactId>cucumber-java</artifactId>
96+
<version>${cucumber.java.version}</version>
97+
<scope>test</scope>
98+
</dependency>
99+
<dependency>
100+
<groupId>info.cukes</groupId>
101+
<artifactId>cucumber-junit</artifactId>
102+
<version>${cucumber.java.version}</version>
103+
<scope>test</scope>
104+
</dependency>
105+
</dependencies>
106+
107+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.emorozov.datasciencelab;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
5+
import org.springframework.web.bind.annotation.RequestMapping;
6+
import org.springframework.web.bind.annotation.RestController;
7+
8+
import java.util.Random;
9+
10+
@RestController
11+
@EnableAutoConfiguration
12+
public class DataScienceLabDemo {
13+
14+
public static final Random random = new Random();
15+
16+
@RequestMapping("/users")
17+
public String users() throws Exception {
18+
Thread.sleep((long) (random.nextGaussian() * 20) + 100);
19+
// Comment out additional sleep statement below to see the tests passing
20+
if (random.nextInt(100) > 80) {
21+
Thread.sleep(200);
22+
}
23+
return "Return some users.";
24+
}
25+
26+
@RequestMapping("/trades")
27+
public String trades() throws Exception {
28+
Thread.sleep((long) (random.nextGaussian() * 20) + 100);
29+
return "Return some trades.";
30+
}
31+
32+
public static void main(String[] args) throws Exception {
33+
SpringApplication.run(DataScienceLabDemo.class, args);
34+
}
35+
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.emorozov.datasciencelab;
2+
3+
import org.junit.runner.RunWith;
4+
import cucumber.api.junit.Cucumber;
5+
6+
@RunWith(Cucumber.class)
7+
public class DataScienceLabDemoTest {
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package com.emorozov.datasciencelab;
2+
3+
import com.emorozov.datasciencelab.sampler.CpuSampler;
4+
import com.fasterxml.jackson.annotation.JsonCreator;
5+
import com.fasterxml.jackson.annotation.JsonProperty;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import cucumber.api.java.Before;
8+
import cucumber.api.java.en.Given;
9+
import cucumber.api.java.en.Then;
10+
import cucumber.api.java.en.When;
11+
import lombok.AllArgsConstructor;
12+
import lombok.Data;
13+
import org.apache.jmeter.control.LoopController;
14+
import org.apache.jmeter.engine.StandardJMeterEngine;
15+
import org.apache.jmeter.protocol.http.sampler.HTTPSampler;
16+
import org.apache.jmeter.reporters.ResultCollector;
17+
import org.apache.jmeter.testelement.TestPlan;
18+
import org.apache.jmeter.threads.ThreadGroup;
19+
import org.apache.jmeter.util.JMeterUtils;
20+
import org.apache.jorphan.collections.HashTree;
21+
22+
import java.io.File;
23+
import java.nio.file.Files;
24+
import java.nio.file.Paths;
25+
import java.util.List;
26+
import java.util.stream.Stream;
27+
28+
import static org.hamcrest.MatcherAssert.assertThat;
29+
import static org.hamcrest.number.OrderingComparison.lessThan;
30+
31+
public class DataScienceLabDemoTestPerformanceAnomaliesSteps {
32+
33+
@Data
34+
public class Endpoint {
35+
private String domain;
36+
private int port;
37+
private String path;
38+
private String method;
39+
}
40+
41+
@Data
42+
@AllArgsConstructor
43+
private static class RInput {
44+
private String metrics;
45+
private Integer samplerCount;
46+
private Double criticalQiantile;
47+
}
48+
49+
@Data
50+
private static class ROutput {
51+
private Integer outliers;
52+
@JsonCreator
53+
public ROutput(@JsonProperty("outliers") Integer outliers) {
54+
this.outliers = outliers;
55+
}
56+
}
57+
58+
private int samplersCount = 0;
59+
60+
@Before
61+
public void clearTemporaryFiles() throws Exception {
62+
Files.deleteIfExists(Paths.get("metrics.csv"));
63+
Files.deleteIfExists(Paths.get("input.json"));
64+
Files.deleteIfExists(Paths.get("output.json"));
65+
}
66+
67+
@Given("^endpoints are up and running$")
68+
public void systemEndpointsAreUpAndRunning(final List<Endpoint> endpoints) throws Exception {
69+
// TODO Actually validate that endpoints are up and running
70+
}
71+
72+
@When("^(.+) requests are submitted for endpoints$")
73+
public void requestsAreSubmitted(final Integer loops,
74+
final List<Endpoint> endpoints) {
75+
76+
// Set status vars - number of endpoints and a CPU sampler
77+
this.samplersCount = endpoints.size() + 1;
78+
79+
// Create JMeter engine
80+
StandardJMeterEngine jmeter = new StandardJMeterEngine();
81+
82+
// Init JMeter engine
83+
// TODO Refactor config
84+
JMeterUtils.setJMeterHome("JMETER_HOME");
85+
JMeterUtils.loadJMeterProperties("JMETER_HOME/bin/jmeter.properties");
86+
JMeterUtils.initLocale();
87+
88+
// Build samplers, use path for name of the sampler
89+
Stream<HTTPSampler> httpSamplers = endpoints.stream().map(endpoint -> {
90+
HTTPSampler httpSampler = new HTTPSampler();
91+
httpSampler.setName(endpoint.getPath());
92+
httpSampler.setDomain(endpoint.getDomain());
93+
httpSampler.setPort(endpoint.getPort());
94+
httpSampler.setPath(endpoint.getPath());
95+
httpSampler.setMethod(endpoint.getMethod());
96+
return httpSampler;
97+
});
98+
99+
// Build a CPU sampler
100+
CpuSampler cpuSampler = new CpuSampler();
101+
cpuSampler.setName("cpu");
102+
103+
// Loop Controller
104+
LoopController loopController = new LoopController();
105+
loopController.setName("Default Loop Controller");
106+
loopController.setLoops(loops);
107+
loopController.setFirst(true);
108+
loopController.initialize();
109+
110+
// Thread Group
111+
org.apache.jmeter.threads.ThreadGroup threadGroup = new ThreadGroup();
112+
threadGroup.setName("Default Thread Group");
113+
threadGroup.setNumThreads(1);
114+
threadGroup.setRampUp(1);
115+
threadGroup.setSamplerController(loopController);
116+
117+
// Create results collector
118+
ResultCollector logger = new ResultCollector();
119+
logger.setName("Default Results Collector");
120+
logger.setFilename("metrics.csv");
121+
122+
// Create a test structure
123+
HashTree testPlanTree = new HashTree();
124+
125+
// Add Test Plan
126+
TestPlan testPlan = new TestPlan("Sample R integration test plan");
127+
testPlanTree.add(testPlan);
128+
129+
// Hang off Thread Group off Test Plan
130+
HashTree threadGroupHashTree = testPlanTree.add(testPlan, threadGroup);
131+
132+
// Add samplers
133+
threadGroupHashTree.add(httpSamplers.toArray());
134+
threadGroupHashTree.add(cpuSampler);
135+
136+
// Add logger
137+
testPlanTree.add(testPlan, logger);
138+
139+
// Run Test Plan
140+
jmeter.configure(testPlanTree);
141+
jmeter.run();
142+
threadGroup.waitThreadsStopped();
143+
}
144+
145+
@Then("^metrics outliers are below (.+) percent with critical quantile (.+)$")
146+
public void outliersAcrossInputAreBelowThreshold(final Integer outliersThreshold,
147+
final Double criticalQuantile)
148+
throws Exception {
149+
150+
// Get the object mapper
151+
ObjectMapper mapper = new ObjectMapper();
152+
153+
// Create inputs for analytics
154+
RInput input = new RInput("metrics.csv", this.samplersCount, criticalQuantile);
155+
156+
mapper.writeValue(new File("input.json"), input);
157+
158+
// Run analytics
159+
// TODO Refactor config
160+
Runtime rt = Runtime.getRuntime();
161+
Process pr = rt.exec("Rscript ./src/test/r/performance-anomalies.R");
162+
pr.waitFor();
163+
164+
// Get the outputs
165+
ROutput output = mapper.readValue(new File("output.json"), ROutput.class);
166+
167+
// Assert
168+
assertThat(output.getOutliers(), lessThan(outliersThreshold));
169+
}
170+
}

0 commit comments

Comments
 (0)