├── .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 | [![Build and publish](https://github.com/adrianmo/jmeter-backend-azure/actions/workflows/maven.yml/badge.svg)](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 | ![Screenshot of configuration](docs/configuration.png "Screenshot of JMeter configuration") 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 | ![Request duration](docs/requestduration.png "Screenshot of test requests duration") 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 | ![Live Metrics Stream](docs/livemetrics.png "Screenshot of live metrics stream") 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 | --------------------------------------------------------------------------------