├── .github
└── workflows
│ └── maven.yml
├── .gitignore
├── LICENSE.txt
├── README.md
├── docs
├── configuration.png
├── livemetrics.png
└── requestduration.png
├── pom.xml
└── src
├── main
└── java
│ └── io
│ └── github
│ └── adrianmo
│ └── jmeter
│ └── backendlistener
│ └── azure
│ ├── AzureBackendClient.java
│ └── DataLoggingOption.java
└── test
└── java
└── io
└── github
└── adrianmo
└── jmeter
└── backendlistener
└── azure
└── TestAzureBackendClient.java
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | name: Build and publish
2 |
3 | on:
4 | workflow_dispatch:
5 | release:
6 | types: [released]
7 | pull_request:
8 | branches:
9 | - master
10 | push:
11 | branches:
12 | - master
13 |
14 | jobs:
15 | build:
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v3
21 |
22 | - name: Set up JDK 1.8
23 | uses: actions/setup-java@v2
24 | with:
25 | java-version: 8
26 | distribution: zulu
27 | java-package: jdk
28 |
29 | - name: Maven version
30 | run: mvn -v
31 |
32 | - name: Test
33 | run: mvn test
34 |
35 | - name: Set project version (snapshot)
36 | run: |
37 | commit_sha=$(git rev-parse --short ${{ github.sha }})
38 | mvn versions:set -DgenerateBackupPoms=false -DnewVersion=${commit_sha}-SNAPSHOT
39 |
40 | - name: Build
41 | run: mvn package
42 |
43 | - name: Attach JAR as artifact
44 | uses: actions/upload-artifact@v3
45 | with:
46 | name: jmeter-backendlistener-azure
47 | path: |
48 | target/*.jar
49 | !target/*-javadoc.jar
50 | !target/*-sources.jar
51 | !target/original-*.jar
52 |
53 | publish:
54 | needs: build
55 | runs-on: ubuntu-latest
56 | if: github.event_name == 'release'
57 | steps:
58 | - name: Checkout
59 | uses: actions/checkout@v3
60 |
61 | - name: Set up JDK 1.8
62 | uses: actions/setup-java@v2
63 | with:
64 | java-version: 8
65 | distribution: zulu
66 | java-package: jdk
67 |
68 | - name: Set project version (release)
69 | run: |
70 | mvn versions:set -DgenerateBackupPoms=false -DnewVersion=${{ github.event.release.tag_name }}
71 |
72 | - name: Build
73 | run: mvn package
74 |
75 | - name: Upload JAR to release
76 | uses: svenstaro/upload-release-action@v2
77 | with:
78 | repo_token: ${{ secrets.GITHUB_TOKEN }}
79 | file: target/jmeter.backendlistener.azure-${{ github.event.release.tag_name }}.jar
80 | asset_name: jmeter.backendlistener.azure-${{ github.event.release.tag_name }}.jar
81 | tag: ${{ github.event.release.tag_name }}
82 |
83 | - name: Publish to Maven Central
84 | uses: samuelmeuli/action-maven-publish@v1
85 | with:
86 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
87 | gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }}
88 | nexus_username: ${{ secrets.OSSRH_USERNAME }}
89 | nexus_password: ${{ secrets.OSSRH_TOKEN }}
90 | server_id: ossrh
91 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled class file
2 | *.class
3 |
4 | # Log file
5 | *.log
6 |
7 | # BlueJ files
8 | *.ctxt
9 |
10 | # Mobile Tools for Java (J2ME)
11 | .mtj.tmp/
12 |
13 | # Package Files #
14 | *.jar
15 | *.war
16 | *.nar
17 | *.ear
18 | *.zip
19 | *.tar.gz
20 | *.rar
21 |
22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
23 | hs_err_pid*
24 |
25 | # IDE
26 | .idea/
27 | *.iml
28 | .vscode/
29 |
30 | target/
31 | dependency-reduced-pom.xml
32 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | jmeter-backend-azure
2 | Copyright (c) Microsoft Corporation
3 | All rights reserved.
4 |
5 | MIT License
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this
7 | software and associated documentation files (the ""Software""), to deal in the Software
8 | without restriction, including without limitation the rights to use, copy, modify, merge,
9 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit
10 | persons to whom the Software is furnished to do so, subject to the following conditions:
11 | The above copyright notice and this permission notice shall be included in all copies or
12 | substantial portions of the Software.
13 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
14 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
15 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
16 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
17 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
18 | DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # jmeter-backend-azure
2 |
3 | [](https://github.com/adrianmo/jmeter-backend-azure/actions/workflows/maven.yml)
4 |
5 | A JMeter plug-in that enables you to send test results to Azure Application Insights.
6 |
7 | ## Overview
8 |
9 | ### Description
10 |
11 | JMeter Backend Azure is a JMeter plugin enabling you to send test results to an Azure Application Insights.
12 |
13 | The following test results metrics are exposed by the plugin.
14 |
15 | - TestStartTime
16 | - SampleStartTime
17 | - SampleEndTime
18 | - ResponseCode
19 | - Duration
20 | - URL
21 | - SampleLabel
22 | - SampleCount
23 | - ErrorCount
24 | - Bytes
25 | - SentBytes
26 | - ConnectTime
27 | - IdleTime
28 | - ThreadName
29 | - GrpThreads
30 | - AllThreads
31 | - (Optional) aih.{ResponseHeader}
32 | - (Optional) ResponseData
33 | - (Optional) SampleData
34 |
35 | ### Plugin installation
36 |
37 | Once you have built or downloaded the plugin JAR file from the [releases](https://github.com/adrianmo/jmeter-backend-azure/releases) section,
38 | move the JAR to your `$JMETER_HOME/lib/ext`.
39 |
40 | ```bash
41 | mv target/jmeter.backendlistener.azure-VERSION.jar $JMETER_HOME/lib/ext/
42 | ```
43 |
44 | Then, restart JMeter and the plugin should be loaded.
45 |
46 | ### JMeter configuration
47 |
48 | To make JMeter send test result metrics to Azure Application Insights, in your **Test Pan**, right click on
49 | **Thread Group** > Add > Listener > Backend Listener, and choose `io.github.adrianmo.jmeter.backendlistener.azure.AzureBackendClient` as `Backend Listener Implementation`.
50 | Then, in the Parameters table, configure the following attributes.
51 |
52 | | Attribute | Description | Required |
53 | |---|---|---|
54 | | *connectionString* | The [Connection String](https://docs.microsoft.com/en-us/azure/azure-monitor/app/sdk-connection-string?tabs=java) of your Application Insights instance | Yes |
55 | | *testName* | Name of the test. This value is used to differentiate metrics across test runs or plans in Application Insights and allow you to filter them. | Yes |
56 | | *liveMetrics* | Boolean to indicate whether or not real-time metrics are enabled and available in the [Live Metrics Stream](https://docs.microsoft.com/en-us/azure/azure-monitor/app/live-stream). Defaults to `true`. | No |
57 | | *samplersList* | Optional list of samplers separated by a semi-colon (`;`) that the listener will collect and send metrics to Application Insights. If the list is empty, the listener will not filter samplers and send metrics from all of them. Defaults to an empty string. | No |
58 | | *useRegexForSamplerList* | If set to `true` the `samplersList` will be evaluated as a regex to filter samplers. Defaults to `false`. | No |
59 | | *responseHeaders* | Optional list of response headers separated by a semi-colon (`;`) that the listener will collect and send values to Application Insights. | No |
60 | | *logResponseData* | This value indicates whether or not the response data should be captured. Options are `Always`, `OnFailure`, or `Never`. The response data will be captured as a string into the _ResponseData_ property. Defaults to `OnFailure`. | No |
61 | | *logSampleData* | Boolean to indicate whether or not the sample data should be captured. Options are `Always`, `OnFailure`, or `Never`. The sample data will be captured as a string into the _SampleData_ property. Defaults to `OnFailure`. | No |
62 | | *instrumentationKey* | The Instrumentation Key of your Application Insights instance.
⚠️ **Deprecated**: use *connectionString* instead. | No |
63 |
64 | *Example of configuration:*
65 |
66 | 
67 |
68 | #### Custom properties
69 |
70 | You can add custom data to your metrics by adding properties starting with `ai.`, for example, you might want to provide information related to your environment with the property `ai.environment` and value `staging`.
71 |
72 | ### Visualization
73 |
74 | Test result metrics are available in the **requests** dimension of your Application Insights instance.
75 | In the image you can see an example of how you can visualize the duration of the requests made during your test run.
76 |
77 | 
78 |
79 | Additionally, if you enabled `liveMetrics` in the configuration, you can watch your test performance in real-time in the Live Metrics Stream blade.
80 |
81 | 
82 |
83 | ## Contributing
84 |
85 | Feel free to contribute by forking and making pull requests, or simply by suggesting ideas through the
86 | [Issues](https://github.com/adrianmo/jmeter-backend-azure/issues) section.
87 |
88 | ### Build
89 |
90 | You can make changes to the plugin and build your own JAR file to test changes. To build the artifact,
91 | execute below Maven command. Make sure `JAVA_HOME` is set properly.
92 |
93 | ```bash
94 | mvn clean package
95 | ```
96 |
97 | ---
98 |
99 | This plugin is inspired in the [Elasticsearch](https://github.com/delirius325/jmeter-elasticsearch-backend-listener) and [Kafka](https://github.com/rahulsinghai/jmeter-backend-listener-kafka) backend listener plugins.
100 |
--------------------------------------------------------------------------------
/docs/configuration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianmo/jmeter-backend-azure/37f2d8439776d49d5da21010f1e05d591d284158/docs/configuration.png
--------------------------------------------------------------------------------
/docs/livemetrics.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianmo/jmeter-backend-azure/37f2d8439776d49d5da21010f1e05d591d284158/docs/livemetrics.png
--------------------------------------------------------------------------------
/docs/requestduration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianmo/jmeter-backend-azure/37f2d8439776d49d5da21010f1e05d591d284158/docs/requestduration.png
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 | io.github.adrianmo
6 | jmeter.backendlistener.azure
7 | 0.0.1-SNAPSHOT
8 | jar
9 | ${project.artifactId}
10 | A JMeter plug-in that enables you to send test results to Azure Monitor.
11 | https://github.com/adrianmo/jmeter-backend-azure
12 |
13 |
14 |
15 | The MIT License
16 | https://raw.githubusercontent.com/adrianmo/jmeter-backend-azure/master/LICENSE.txt
17 | repo
18 |
19 |
20 |
21 |
22 |
23 | adrianmo
24 | Adrian Moreno
25 | adrian.moreno@microsoft.com
26 | Microsoft
27 | http://www.microsoft.com/
28 |
29 | architect
30 | developer
31 |
32 | +1
33 |
34 |
35 | https://avatars.githubusercontent.com/u/3786750
36 |
37 |
38 |
39 |
40 |
41 | scm:git:git@github.com:adrianmo/jmeter-backend-azure.git
42 | scm:git:ssh://github.com/adrianmo/jmeter-backend-azure.git
43 | https://github.com/adrianmo/jmeter-backend-azure
44 |
45 |
46 |
47 | UTF-8
48 | 3.12.0
49 | 5.4.1
50 | 2.6.3
51 | 4.13.2
52 | 5.9.2
53 | 1.9.2
54 |
55 |
56 |
57 |
58 | org.apache.jmeter
59 | ApacheJMeter_config
60 | ${org.apache.jmeter.version}
61 | provided
62 |
63 |
64 | org.apache.jmeter
65 | ApacheJMeter_core
66 | ${org.apache.jmeter.version}
67 | provided
68 |
69 |
70 | org.apache.jmeter
71 | ApacheJMeter_components
72 | ${org.apache.jmeter.version}
73 | provided
74 |
75 |
76 | org.apache.jmeter
77 | jorphan
78 | ${org.apache.jmeter.version}
79 | provided
80 |
81 |
82 | org.apache.commons
83 | commons-lang3
84 | ${org.apache.commons}
85 | provided
86 |
87 |
88 | com.microsoft.azure
89 | applicationinsights-core
90 |
91 |
92 | ${com.microsoft.azure.version}
93 |
94 |
95 | org.junit.jupiter
96 | junit-jupiter-engine
97 | ${junit.jupiter.version}
98 | test
99 |
100 |
101 | junit
102 | junit
103 | ${junit.version}
104 | test
105 |
106 |
107 | org.junit.platform
108 | junit-platform-runner
109 | ${junit.platform.version}
110 | test
111 |
112 |
113 | org.mockito
114 | mockito-all
115 | 1.10.19
116 | test
117 |
118 |
119 | org.junit.vintage
120 | junit-vintage-engine
121 | 5.9.2
122 | test
123 |
124 |
125 |
126 |
127 | ossrh
128 | https://s01.oss.sonatype.org/content/repositories/snapshots
129 |
130 |
131 | ossrh
132 | https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/
133 |
134 |
135 |
136 |
137 |
138 | org.apache.maven.plugins
139 | maven-gpg-plugin
140 | 1.6
141 |
142 |
143 | sign-artifacts
144 | verify
145 |
146 | sign
147 |
148 |
149 |
150 |
151 | --pinentry-mode
152 | loopback
153 |
154 |
155 |
156 |
157 |
158 |
159 | org.sonatype.plugins
160 | nexus-staging-maven-plugin
161 | 1.6.7
162 | true
163 |
164 | ossrh
165 | https://s01.oss.sonatype.org/
166 | true
167 |
168 |
169 |
170 | org.apache.maven.plugins
171 | maven-source-plugin
172 | 3.2.0
173 |
174 |
175 | attach-sources
176 |
177 | jar-no-fork
178 |
179 |
180 |
181 |
182 |
183 | org.apache.maven.plugins
184 | maven-javadoc-plugin
185 | 3.1.1
186 |
187 |
188 | attach-javadocs
189 |
190 | jar
191 |
192 |
193 |
194 |
195 |
196 | org.apache.maven.plugins
197 | maven-surefire-plugin
198 | 2.22.2
199 |
200 |
201 | org.junit.platform
202 | junit-platform-surefire-provider
203 | 1.3.2
204 |
205 |
206 | org.junit.jupiter
207 | junit-jupiter-engine
208 | 5.9.2
209 |
210 |
211 |
212 |
213 | org.apache.maven.plugins
214 | maven-compiler-plugin
215 | 3.11.0
216 |
217 | 1.8
218 | 1.8
219 |
220 |
221 |
222 | org.apache.maven.plugins
223 | maven-dependency-plugin
224 | 3.5.0
225 |
226 |
227 | copy-dependencies
228 | package
229 |
230 | copy-dependencies
231 |
232 |
233 | compile
234 | provided
235 | ${project.build.directory}/dependencies
236 | false
237 | false
238 | true
239 |
240 |
241 |
242 |
243 |
244 | org.apache.maven.plugins
245 | maven-shade-plugin
246 | 3.4.1
247 |
248 |
249 |
250 | package
251 |
252 | shade
253 |
254 |
255 |
256 |
257 | *:*
258 |
259 | META-INF/*.SF
260 | META-INF/*.DSA
261 | META-INF/*.RSA
262 |
263 |
264 |
265 |
266 |
267 | org.apache.jmeter:*
268 | commons-codec:*
269 | commons-logging:*
270 | org.apache.httpcomponents:*
271 | net.java.dev.jna:*
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
--------------------------------------------------------------------------------
/src/main/java/io/github/adrianmo/jmeter/backendlistener/azure/AzureBackendClient.java:
--------------------------------------------------------------------------------
1 | package io.github.adrianmo.jmeter.backendlistener.azure;
2 |
3 | import com.microsoft.applicationinsights.TelemetryClient;
4 | import com.microsoft.applicationinsights.TelemetryConfiguration;
5 | import com.microsoft.applicationinsights.internal.quickpulse.QuickPulse;
6 | import com.microsoft.applicationinsights.internal.util.MapUtil;
7 | import com.microsoft.applicationinsights.telemetry.Duration;
8 | import com.microsoft.applicationinsights.telemetry.RequestTelemetry;
9 |
10 | import org.apache.jmeter.config.Arguments;
11 | import org.apache.jmeter.samplers.SampleResult;
12 | import org.apache.jmeter.threads.JMeterContextService;
13 | import org.apache.jmeter.visualizers.backend.AbstractBackendListenerClient;
14 | import org.apache.jmeter.visualizers.backend.BackendListenerContext;
15 | import org.slf4j.Logger;
16 | import org.slf4j.LoggerFactory;
17 |
18 | import java.util.Date;
19 | import java.util.HashMap;
20 | import java.util.HashSet;
21 | import java.util.Iterator;
22 | import java.util.List;
23 | import java.util.Map;
24 | import java.util.Set;
25 | import java.util.regex.Pattern;
26 | import java.util.regex.Matcher;
27 |
28 | public class AzureBackendClient extends AbstractBackendListenerClient {
29 |
30 | /**
31 | * Logger.
32 | */
33 | private static final Logger log = LoggerFactory.getLogger(AzureBackendClient.class);
34 |
35 | /**
36 | * Argument keys.
37 | */
38 | private static final String KEY_TEST_NAME = "testName";
39 | private static final String KEY_INSTRUMENTATION_KEY = "instrumentationKey";
40 | private static final String KEY_CONNECTION_STRING = "connectionString";
41 | private static final String KEY_LIVE_METRICS = "liveMetrics";
42 | private static final String KEY_SAMPLERS_LIST = "samplersList";
43 | private static final String KEY_USE_REGEX_FOR_SAMPLER_LIST = "useRegexForSamplerList";
44 | private static final String KEY_CUSTOM_PROPERTIES_PREFIX = "ai.";
45 | private static final String KEY_HEADERS_PREFIX = "aih.";
46 | private static final String KEY_RESPONSE_HEADERS = "responseHeaders";
47 | private static final String KEY_LOG_RESPONSE_DATA = "logResponseData";
48 | private static final String KEY_LOG_SAMPLE_DATA = "logSampleData";
49 |
50 | /**
51 | * Default argument values.
52 | */
53 | private static final String DEFAULT_TEST_NAME = "jmeter";
54 | private static final String DEFAULT_CONNECTION_STRING = "";
55 | private static final boolean DEFAULT_LIVE_METRICS = true;
56 | private static final String DEFAULT_SAMPLERS_LIST = "";
57 | private static final boolean DEFAULT_USE_REGEX_FOR_SAMPLER_LIST = false;
58 | private static final DataLoggingOption DEFAULT_LOG_RESPONSE_DATA = DataLoggingOption.OnFailure;
59 | private static final DataLoggingOption DEFAULT_LOG_SAMPLE_DATA = DataLoggingOption.OnFailure;
60 |
61 | /**
62 | * Separator for samplers list.
63 | */
64 | private static final String SEPARATOR = ";";
65 |
66 | /**
67 | * Truncated length of the request and response data.
68 | */
69 | private static final int MAX_DATA_LENGTH = 1024;
70 |
71 | /**
72 | * Application Insights telemetry client.
73 | */
74 | private TelemetryClient telemetryClient;
75 |
76 | /**
77 | * Name of the test.
78 | */
79 | private String testName;
80 |
81 | /**
82 | * Custom properties.
83 | */
84 | private Map customProperties = new HashMap();
85 |
86 | /**
87 | * Recording response headers.
88 | */
89 | private String[] responseHeaders = {};
90 |
91 | /**
92 | * Whether to send metrics to the Live Metrics Stream.
93 | */
94 | private boolean liveMetrics;
95 |
96 | /**
97 | * List of samplers to record.
98 | */
99 | private String samplersList = "";
100 |
101 | /**
102 | * Regex if samplers are defined through regular expression.
103 | */
104 | private Boolean useRegexForSamplerList;
105 |
106 | /**
107 | * Set of samplers to record.
108 | */
109 | private Set samplersToFilter;
110 |
111 | /**
112 | * Whether to log the response data to the backend
113 | */
114 | private DataLoggingOption logResponseData;
115 |
116 | /**
117 | * Whether to log the sample data to the backend
118 | */
119 | private DataLoggingOption logSampleData;
120 |
121 | public AzureBackendClient() {
122 | super();
123 | }
124 |
125 | @Override
126 | public Arguments getDefaultParameters() {
127 | Arguments arguments = new Arguments();
128 | arguments.addArgument(KEY_TEST_NAME, DEFAULT_TEST_NAME);
129 | arguments.addArgument(KEY_CONNECTION_STRING, DEFAULT_CONNECTION_STRING);
130 | arguments.addArgument(KEY_LIVE_METRICS, Boolean.toString(DEFAULT_LIVE_METRICS));
131 | arguments.addArgument(KEY_SAMPLERS_LIST, DEFAULT_SAMPLERS_LIST);
132 | arguments.addArgument(KEY_USE_REGEX_FOR_SAMPLER_LIST, Boolean.toString(DEFAULT_USE_REGEX_FOR_SAMPLER_LIST));
133 | arguments.addArgument(KEY_LOG_RESPONSE_DATA, DEFAULT_LOG_RESPONSE_DATA.getValue());
134 | arguments.addArgument(KEY_LOG_SAMPLE_DATA, DEFAULT_LOG_SAMPLE_DATA.getValue());
135 |
136 | return arguments;
137 | }
138 |
139 | @Override
140 | public void setupTest(BackendListenerContext context) throws Exception {
141 | testName = context.getParameter(KEY_TEST_NAME, DEFAULT_TEST_NAME);
142 | liveMetrics = context.getBooleanParameter(KEY_LIVE_METRICS, DEFAULT_LIVE_METRICS);
143 | samplersList = context.getParameter(KEY_SAMPLERS_LIST, DEFAULT_SAMPLERS_LIST).trim();
144 | useRegexForSamplerList = context.getBooleanParameter(KEY_USE_REGEX_FOR_SAMPLER_LIST,
145 | DEFAULT_USE_REGEX_FOR_SAMPLER_LIST);
146 | logResponseData = DataLoggingOption
147 | .fromString(context.getParameter(KEY_LOG_RESPONSE_DATA, DEFAULT_LOG_RESPONSE_DATA.getValue()));
148 | logSampleData = DataLoggingOption
149 | .fromString(context.getParameter(KEY_LOG_SAMPLE_DATA, DEFAULT_LOG_SAMPLE_DATA.getValue()));
150 |
151 | Iterator iterator = context.getParameterNamesIterator();
152 | while (iterator.hasNext()) {
153 | String paramName = iterator.next();
154 | if (paramName.startsWith(KEY_CUSTOM_PROPERTIES_PREFIX)) {
155 | customProperties.put(paramName, context.getParameter(paramName));
156 | } else if (paramName.equals(KEY_RESPONSE_HEADERS)) {
157 | responseHeaders = context.getParameter(KEY_RESPONSE_HEADERS).trim().toLowerCase()
158 | .split("\\s*".concat(SEPARATOR).concat("\\s*"));
159 | }
160 | }
161 |
162 | TelemetryConfiguration config = TelemetryConfiguration.createDefault();
163 | String instrumentationKey = context.getParameter(KEY_INSTRUMENTATION_KEY);
164 | if (instrumentationKey != null) {
165 | log.warn("'instrumentationKey' is deprecated, use 'connectionString' instead");
166 | config.setInstrumentationKey(instrumentationKey);
167 | }
168 |
169 | String connectionString = context.getParameter(KEY_CONNECTION_STRING);
170 | if (connectionString != null) {
171 | config.setConnectionString(connectionString);
172 | }
173 |
174 | telemetryClient = new TelemetryClient(config);
175 | if (liveMetrics) {
176 | QuickPulse.INSTANCE.initialize(config);
177 | }
178 |
179 | samplersToFilter = new HashSet();
180 | if (!useRegexForSamplerList) {
181 | String[] samplers = samplersList.split(SEPARATOR);
182 | samplersToFilter = new HashSet();
183 | for (String samplerName : samplers) {
184 | samplersToFilter.add(samplerName);
185 | }
186 | }
187 | }
188 |
189 | private void trackRequest(String name, SampleResult sr) {
190 | Map properties = new HashMap();
191 | properties.putAll(customProperties);
192 | properties.put("Bytes", Long.toString(sr.getBytesAsLong()));
193 | properties.put("SentBytes", Long.toString(sr.getSentBytes()));
194 | properties.put("ConnectTime", Long.toString(sr.getConnectTime()));
195 | properties.put("ErrorCount", Integer.toString(sr.getErrorCount()));
196 | properties.put("IdleTime", Double.toString(sr.getIdleTime()));
197 | properties.put("Latency", Double.toString(sr.getLatency()));
198 | properties.put("BodySize", Long.toString(sr.getBodySizeAsLong()));
199 | properties.put("TestStartTime", Long.toString(JMeterContextService.getTestStartTime()));
200 | properties.put("SampleStartTime", Long.toString(sr.getStartTime()));
201 | properties.put("SampleEndTime", Long.toString(sr.getEndTime()));
202 | properties.put("SampleLabel", sr.getSampleLabel());
203 | properties.put("ThreadName", sr.getThreadName());
204 | properties.put("URL", sr.getUrlAsString());
205 | properties.put("ResponseCode", sr.getResponseCode());
206 | properties.put("GrpThreads", Integer.toString(sr.getGroupThreads()));
207 | properties.put("AllThreads", Integer.toString(sr.getAllThreads()));
208 | properties.put("SampleCount", Integer.toString(sr.getSampleCount()));
209 |
210 | for (String header : responseHeaders) {
211 | Pattern pattern = Pattern.compile("^".concat(header).concat(":(.*)$"),
212 | Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
213 | Matcher matcher = pattern.matcher(sr.getResponseHeaders());
214 | if (matcher.find()) {
215 | properties.put(KEY_HEADERS_PREFIX.concat(header), matcher.group(1).trim());
216 | }
217 | }
218 |
219 | Date timestamp = new Date(sr.getTimeStamp());
220 | Duration duration = new Duration(sr.getTime());
221 | RequestTelemetry req = new RequestTelemetry(name, timestamp, duration, sr.getResponseCode(),
222 | sr.isSuccessful());
223 | req.getContext().getOperation().setName(name);
224 |
225 | if (sr.getURL() != null) {
226 | req.setUrl(sr.getURL());
227 | }
228 |
229 | if (sr.getSamplerData() != null && ((logSampleData == DataLoggingOption.Always) ||
230 | (logSampleData == DataLoggingOption.OnFailure && !sr.isSuccessful()))) {
231 |
232 | if (sr.getDataType() == SampleResult.TEXT) {
233 | String samplerData;
234 | if (sr.getSamplerData().length() > MAX_DATA_LENGTH) {
235 | log.warn("Sample data is too long, truncating it to {} characters", MAX_DATA_LENGTH);
236 | samplerData = sr.getSamplerData().substring(0, MAX_DATA_LENGTH) + "...[TRUNCATED]";
237 | } else {
238 | samplerData = sr.getSamplerData();
239 | }
240 | properties.put("SampleData", samplerData);
241 | } else {
242 | log.warn("Sample data is in binary format, cannot log it");
243 | properties.put("SampleData", "[BINARY DATA]");
244 | }
245 | }
246 |
247 | if (logResponseData == DataLoggingOption.Always ||
248 | (logResponseData == DataLoggingOption.OnFailure && !sr.isSuccessful())) {
249 | String responseData;
250 | if (sr.getResponseDataAsString().length() > MAX_DATA_LENGTH) {
251 | log.warn("Response data is too long, truncating it to {} characters", MAX_DATA_LENGTH);
252 | responseData = sr.getResponseDataAsString().substring(0, MAX_DATA_LENGTH) + "...[TRUNCATED]";
253 | } else {
254 | responseData = sr.getResponseDataAsString();
255 | }
256 | properties.put("ResponseData", responseData);
257 | }
258 |
259 | MapUtil.copy(properties, req.getProperties());
260 | telemetryClient.trackRequest(req);
261 | }
262 |
263 | @Override
264 | public void handleSampleResults(List results, BackendListenerContext context) {
265 |
266 | boolean samplersToFilterMatch;
267 | for (SampleResult sr : results) {
268 |
269 | samplersToFilterMatch = samplersList.isEmpty() ||
270 | (useRegexForSamplerList && sr.getSampleLabel().matches(samplersList)) ||
271 | (!useRegexForSamplerList && samplersToFilter.contains(sr.getSampleLabel()));
272 |
273 | if (samplersToFilterMatch) {
274 | trackRequest(testName, sr);
275 | }
276 | }
277 | }
278 |
279 | @Override
280 | public void teardownTest(BackendListenerContext context) throws Exception {
281 | samplersToFilter.clear();
282 | telemetryClient.flush();
283 | super.teardownTest(context);
284 | }
285 | }
286 |
--------------------------------------------------------------------------------
/src/main/java/io/github/adrianmo/jmeter/backendlistener/azure/DataLoggingOption.java:
--------------------------------------------------------------------------------
1 | package io.github.adrianmo.jmeter.backendlistener.azure;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 |
6 | public enum DataLoggingOption {
7 | Always("Always"),
8 | OnFailure("OnFailure"),
9 | Never("Never");
10 |
11 | private final String value;
12 | private static final Logger log = LoggerFactory.getLogger(AzureBackendClient.class);
13 |
14 | DataLoggingOption(String value) {
15 | this.value = value;
16 | }
17 |
18 | public String getValue() {
19 | return value;
20 | }
21 |
22 | public static DataLoggingOption fromString(String value) {
23 | for (DataLoggingOption option : DataLoggingOption.values()) {
24 | if (option.value.equalsIgnoreCase(value)) {
25 | return option;
26 | }
27 | }
28 |
29 | // Conditions to provide backwards compatibility
30 | if (value == "true") {
31 | log.warn("Logging value 'true' is deprecated, replacing with 'Always'");
32 | return DataLoggingOption.Always;
33 | } else if (value == "false") {
34 | log.warn("Logging value 'false' is deprecated, replacing with 'Never'");
35 | return DataLoggingOption.Never;
36 | } else if (value != "") {
37 | log.warn("Logging value '{}' is not valid, defaulting to 'OnFailure'", value);
38 | }
39 |
40 | return OnFailure;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/test/java/io/github/adrianmo/jmeter/backendlistener/azure/TestAzureBackendClient.java:
--------------------------------------------------------------------------------
1 | package io.github.adrianmo.jmeter.backendlistener.azure;
2 |
3 | import com.microsoft.applicationinsights.TelemetryClient;
4 | import com.microsoft.applicationinsights.telemetry.RequestTelemetry;
5 |
6 | import org.apache.commons.lang3.RandomStringUtils;
7 | import org.apache.jmeter.config.Arguments;
8 | import org.apache.jmeter.samplers.SampleResult;
9 | import org.apache.jmeter.visualizers.backend.BackendListenerContext;
10 | import org.junit.Before;
11 | import org.junit.Test;
12 | import org.junit.runner.RunWith;
13 | import org.mockito.ArgumentCaptor;
14 | import org.mockito.InjectMocks;
15 | import org.mockito.Mock;
16 | import org.mockito.internal.util.reflection.Whitebox;
17 | import org.mockito.runners.MockitoJUnitRunner;
18 |
19 | import java.util.ArrayList;
20 | import java.util.HashSet;
21 | import java.util.List;
22 |
23 | import static junit.framework.TestCase.fail;
24 | import static org.junit.Assert.*;
25 | import static org.mockito.Mockito.*;
26 |
27 | @RunWith(MockitoJUnitRunner.class)
28 | public class TestAzureBackendClient {
29 |
30 | @Mock
31 | private TelemetryClient telemetryClient;
32 |
33 | @InjectMocks
34 | private final AzureBackendClient client = new AzureBackendClient();
35 |
36 | private BackendListenerContext context;
37 |
38 | @Before
39 | public void setUp() {
40 | Arguments args = new Arguments();
41 | args.addArgument("testName", "test-1");
42 | context = new BackendListenerContext(args);
43 | Whitebox.setInternalState(client, "testName", "test-1");
44 | Whitebox.setInternalState(client, "samplersToFilter", new HashSet<>());
45 | Whitebox.setInternalState(client, "logResponseData", DataLoggingOption.OnFailure);
46 | Whitebox.setInternalState(client, "logSampleData", DataLoggingOption.OnFailure);
47 | }
48 |
49 | @Test
50 | public void testGetDefaultParameters() {
51 | Arguments args = client.getDefaultParameters();
52 | assertNotNull(args);
53 | }
54 |
55 | @Test
56 | public void testHandleSampleResults() {
57 | doNothing().when(telemetryClient).trackRequest(any());
58 |
59 | SampleResult sr = new SampleResult();
60 | List list = new ArrayList();
61 | list.add(sr);
62 |
63 | try {
64 | client.handleSampleResults(list, context);
65 | } catch (Exception e) {
66 | fail(e.toString());
67 | }
68 | }
69 |
70 | @Test
71 | public void testDoNotLogDataOnSuccess() {
72 | doNothing().when(telemetryClient).trackRequest(any(RequestTelemetry.class));
73 | Whitebox.setInternalState(client, "logResponseData", DataLoggingOption.OnFailure);
74 | Whitebox.setInternalState(client, "logSampleData", DataLoggingOption.OnFailure);
75 |
76 | SampleResult sr = new SampleResult();
77 | sr.setSampleLabel("test-1");
78 | sr.setSuccessful(true);
79 | sr.setResponseCode("200");
80 | sr.setResponseMessage("OK");
81 | sr.setResponseData("Test response data".getBytes());
82 | sr.setDataType(SampleResult.TEXT);
83 | sr.setSampleCount(1);
84 | sr.setSamplerData("Test sampler data");
85 | List list = new ArrayList();
86 | list.add(sr);
87 |
88 | client.handleSampleResults(list, context);
89 |
90 | ArgumentCaptor argument = ArgumentCaptor.forClass(RequestTelemetry.class);
91 | verify(telemetryClient).trackRequest(argument.capture());
92 | assertFalse(argument.getValue().getProperties().containsKey("SampleData"));
93 | assertFalse(argument.getValue().getProperties().containsKey("ResponseData"));
94 | }
95 |
96 | @Test
97 | public void testDoLogDataOnSuccess() {
98 | doNothing().when(telemetryClient).trackRequest(any(RequestTelemetry.class));
99 | Whitebox.setInternalState(client, "logResponseData", DataLoggingOption.Always);
100 | Whitebox.setInternalState(client, "logSampleData", DataLoggingOption.Always);
101 |
102 | SampleResult sr = new SampleResult();
103 | sr.setSampleLabel("test-1");
104 | sr.setSuccessful(true);
105 | sr.setResponseCode("200");
106 | sr.setResponseMessage("OK");
107 | sr.setResponseData("Test response data".getBytes());
108 | sr.setDataType(SampleResult.TEXT);
109 | sr.setSampleCount(1);
110 | sr.setSamplerData("Test sampler data");
111 | List list = new ArrayList();
112 | list.add(sr);
113 |
114 | client.handleSampleResults(list, context);
115 |
116 | ArgumentCaptor argument = ArgumentCaptor.forClass(RequestTelemetry.class);
117 | verify(telemetryClient).trackRequest(argument.capture());
118 | assertEquals(argument.getValue().getProperties().get("SampleData"), "Test sampler data");
119 | assertEquals(argument.getValue().getProperties().get("ResponseData"), "Test response data");
120 | }
121 |
122 | @Test
123 | public void testDoLogDataOnFailure() {
124 | doNothing().when(telemetryClient).trackRequest(any(RequestTelemetry.class));
125 | Whitebox.setInternalState(client, "logResponseData", DataLoggingOption.OnFailure);
126 | Whitebox.setInternalState(client, "logSampleData", DataLoggingOption.OnFailure);
127 |
128 | SampleResult sr = new SampleResult();
129 | sr.setSampleLabel("test-1");
130 | sr.setSuccessful(false);
131 | sr.setResponseCode("200");
132 | sr.setResponseMessage("OK");
133 | sr.setErrorCount(1);
134 | sr.setResponseData("Test response data".getBytes());
135 | sr.setDataType(SampleResult.TEXT);
136 | sr.setSampleCount(1);
137 | sr.setSamplerData("Test sampler data");
138 | List list = new ArrayList();
139 | list.add(sr);
140 |
141 | client.handleSampleResults(list, context);
142 |
143 | ArgumentCaptor argument = ArgumentCaptor.forClass(RequestTelemetry.class);
144 | verify(telemetryClient).trackRequest(argument.capture());
145 | assertEquals(argument.getValue().getProperties().get("SampleData"), "Test sampler data");
146 | assertEquals(argument.getValue().getProperties().get("ResponseData"), "Test response data");
147 | }
148 |
149 | @Test
150 | public void testDoNotLogDataOnFailure() {
151 | doNothing().when(telemetryClient).trackRequest(any(RequestTelemetry.class));
152 | Whitebox.setInternalState(client, "logResponseData", DataLoggingOption.Never);
153 | Whitebox.setInternalState(client, "logSampleData", DataLoggingOption.Never);
154 |
155 | SampleResult sr = new SampleResult();
156 | sr.setSampleLabel("test-1");
157 | sr.setSuccessful(false);
158 | sr.setResponseCode("200");
159 | sr.setResponseMessage("OK");
160 | sr.setErrorCount(1);
161 | sr.setResponseData("Test response data".getBytes());
162 | sr.setDataType(SampleResult.TEXT);
163 | sr.setSampleCount(1);
164 | sr.setSamplerData("Test sampler data");
165 | List list = new ArrayList();
166 | list.add(sr);
167 |
168 | client.handleSampleResults(list, context);
169 |
170 | ArgumentCaptor argument = ArgumentCaptor.forClass(RequestTelemetry.class);
171 | verify(telemetryClient).trackRequest(argument.capture());
172 | assertFalse(argument.getValue().getProperties().containsKey("SampleData"));
173 | assertFalse(argument.getValue().getProperties().containsKey("ResponseData"));
174 | }
175 |
176 | @Test
177 | public void testTruncateData() {
178 | doNothing().when(telemetryClient).trackRequest(any(RequestTelemetry.class));
179 | Whitebox.setInternalState(client, "logResponseData", DataLoggingOption.OnFailure);
180 | Whitebox.setInternalState(client, "logSampleData", DataLoggingOption.OnFailure);
181 |
182 | SampleResult sr = new SampleResult();
183 | sr.setSampleLabel("test-1");
184 | sr.setSuccessful(false);
185 | sr.setResponseCode("200");
186 | sr.setResponseMessage("OK");
187 | sr.setErrorCount(1);
188 | sr.setResponseData(RandomStringUtils.randomAlphanumeric(2048).getBytes());
189 | sr.setDataType(SampleResult.TEXT);
190 | sr.setSampleCount(1);
191 | sr.setSamplerData(RandomStringUtils.randomAlphanumeric(2048));
192 | List list = new ArrayList();
193 | list.add(sr);
194 |
195 | client.handleSampleResults(list, context);
196 |
197 | ArgumentCaptor argument = ArgumentCaptor.forClass(RequestTelemetry.class);
198 | verify(telemetryClient).trackRequest(argument.capture());
199 | assertTrue(argument.getValue().getProperties().get("SampleData").endsWith("[TRUNCATED]"));
200 | assertTrue(argument.getValue().getProperties().get("ResponseData").endsWith("[TRUNCATED]"));
201 | }
202 |
203 | @Test
204 | public void testDoNotLogBinaryData() {
205 | doNothing().when(telemetryClient).trackRequest(any(RequestTelemetry.class));
206 | Whitebox.setInternalState(client, "logResponseData", DataLoggingOption.OnFailure);
207 | Whitebox.setInternalState(client, "logSampleData", DataLoggingOption.OnFailure);
208 |
209 | SampleResult sr = new SampleResult();
210 | sr.setSampleLabel("test-1");
211 | sr.setSuccessful(false);
212 | sr.setResponseCode("200");
213 | sr.setResponseMessage("OK");
214 | sr.setErrorCount(1);
215 | sr.setResponseData(RandomStringUtils.randomAlphanumeric(2048).getBytes());
216 | sr.setDataType(SampleResult.BINARY);
217 | sr.setSampleCount(1);
218 | sr.setSamplerData(RandomStringUtils.randomAlphanumeric(2048));
219 | List list = new ArrayList();
220 | list.add(sr);
221 |
222 | client.handleSampleResults(list, context);
223 |
224 | ArgumentCaptor argument = ArgumentCaptor.forClass(RequestTelemetry.class);
225 | verify(telemetryClient).trackRequest(argument.capture());
226 | assertEquals(argument.getValue().getProperties().get("SampleData"), "[BINARY DATA]");
227 | }
228 | }
229 |
--------------------------------------------------------------------------------