├── .gitignore ├── .travis.settings.xml ├── .travis.yml ├── LICENSE ├── README.md ├── codecov.yml ├── docs ├── Elasticsearch_jmeter_metrics_template.md ├── Kibana_saved_objects.json ├── configuration.JPG └── logstash.conf ├── intellij-java-google-style.xml ├── plugins-repo.json ├── pom.xml └── src ├── main ├── java │ └── io │ │ └── github │ │ └── rahulsinghai │ │ └── jmeter │ │ └── backendlistener │ │ ├── kafka │ │ ├── KafkaBackendClient.java │ │ └── KafkaMetricPublisher.java │ │ └── model │ │ └── MetricsRow.java └── resources │ └── log4j2.xml └── test └── java └── io └── github └── rahulsinghai └── jmeter └── backendlistener ├── kafka ├── TestKafkaBackendClient.java └── TestKafkaMetricPublisher.java └── model └── TestMetricsRow.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | logs/ 7 | *.lck 8 | 9 | # BlueJ files 10 | *.ctxt 11 | 12 | # Mobile Tools for Java (J2ME) 13 | .mtj.tmp/ 14 | 15 | # Package Files # 16 | *.jar 17 | *.war 18 | *.nar 19 | *.ear 20 | *.zip 21 | *.tar.gz 22 | *.rar 23 | 24 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 25 | hs_err_pid* 26 | 27 | # Maven 28 | target/ 29 | pom.xml.tag 30 | pom.xml.releaseBackup 31 | pom.xml.versionsBackup 32 | pom.xml.next 33 | release.properties 34 | dependency-reduced-pom.xml 35 | buildNumber.properties 36 | .mvn/timing.properties 37 | .project 38 | .classpath 39 | .settings/ 40 | .settings/* 41 | .settings 42 | 43 | # IntelliJ 44 | **.idea 45 | *.iml 46 | *.sh 47 | bin/ 48 | /bin/ 49 | lib/ 50 | out/ 51 | certs/ 52 | *.jks 53 | *.p12 54 | -------------------------------------------------------------------------------- /.travis.settings.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | ossrh 9 | ${env.SONATYPE_USERNAME} 10 | ${env.SONATYPE_PASSWORD} 11 | 12 | 13 | 14 | 15 | 16 | ossrh 17 | 18 | true 19 | 20 | 21 | gpg 22 | ${env.GPG_KEYNAME} 23 | ${env.GPG_PASSPHRASE} 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Faster builds https://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure 2 | sudo: false 3 | dist: trusty 4 | 5 | language: java 6 | 7 | jdk: 8 | - oraclejdk8 9 | 10 | # For GUI based tests using xvfb 11 | #before_script: 12 | # - export DISPLAY=:99.0 13 | # - sh -e /etc/init.d/xvfb start 14 | # - sleep 3 15 | 16 | before_install: 17 | - echo "Download Maven ${CUSTOM_MVN_VERION}...."; 18 | - wget https://archive.apache.org/dist/maven/maven-3/${CUSTOM_MVN_VERION}/binaries/apache-maven-${CUSTOM_MVN_VERION}-bin.tar.gz 19 | - tar xzvf apache-maven-${CUSTOM_MVN_VERION}-bin.tar.gz 20 | - export M2_HOME=`pwd`/apache-maven-${CUSTOM_MVN_VERION} 21 | - export PATH=$M2_HOME/bin:$PATH 22 | - mvn -v 23 | 24 | script: "mvn test verify" 25 | 26 | # cache the build tool's caches 27 | cache: 28 | directories: 29 | - "$HOME/.m2" 30 | 31 | # After the build, run the script that will collect JaCoCo statistics 32 | # It uses the third-party service https://codecov.io/ 33 | after_success: 34 | - bash <(curl -s https://codecov.io/bash) 35 | # - if [ "$TRAVIS_BRANCH" != "master" ]; then openssl aes-256-cbc -pass pass:$ENC_PASSWORD 36 | # -in .travis/sign.asc.enc -out .travis/sign.asc -d; gpg -q --fast-import .travis/sign.asc; 37 | # mvn deploy --settings .travis.settings.xml -DskipTests=true; chmod +x .travis/upload-build.sh; .travis/upload-build.sh; fi 38 | 39 | # Send a notification to your email id, if the assembly has dropped 40 | notifications: 41 | email: 42 | recipients: 43 | - singrahu@gmail.com 44 | on_success: change # options: [always|never|change] default: always 45 | on_failure: always # options: [always|never|change] default: always 46 | on_start: never # options: [always|never|change] default: always 47 | webhooks: 48 | urls: 49 | - https://webhooks.gitter.im/e/3030952ce142f06ee5b3 50 | on_success: change # options: [always|never|change] default: always 51 | on_failure: always # options: [always|never|change] default: always 52 | on_start: never # options: [always|never|change] default: always 53 | 54 | env: 55 | global: 56 | - CUSTOM_MVN_VERION="3.6.1" 57 | # CodeCov.io API token 58 | - secure: cud+2qClMMkbZYcyXCKsZyYXHQQIQO+vDhaeenouy9RrZO+LjleoHgPDlBjEF0DVj7IvjyjjpLn+f2QDMRIufejU7K49Etj9ktY0iKsqFZ4kuK3o+5qtmrht6JDJs4EiLFJZ+QYdY1KTHS87cnQuYt7xmG5AlQYAOWhOkYGwSA50z2+iVo81ZdG+C92zh7cHRhS3mnH5R76spmX82Gtjs/mxZJc1WR5je4O99fKWDiNPIDIK4EXLX+LeMwFu62iz0pV/VvtahMfv1L7bq9h4xgKMlweFGP0zsluQgbmfPZLW2wCU+MyOJt63KsJlG2UpPge4+X1upDQ+Xj+ii8gUzP9AbxeMn8OZKjsg7xDOsuXxchsmEhTfGoIT1QaMfZJOU7aSActPg8XmLAfFPKO65HrviEnrp6xitc9RfdUtu5GdJNHJD43QNEregPt6mjaSPqMN0eDV/T0HMU91FBqfEoAWMEhz5NAH4KxP7Lu2QSnoEyeBV39Ve1FM1r949cdPKEEUa3XyZvshoiK55hHlCs1VTgm9rEvAV4/Jcbf4CwbKvdyinWEb6/hirgxZHT8zyvOWTa9QIP4lEFBeMALLtxtIb/XTYOyx2AA4sZsR/mfyCnjqa/+j5B/iuD3uBGN685bjocZ1Ta4TYKnoXU7pA5tQzz3mY8Vyjz5q/N/5eK8= 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jmeter-backend-listener-kafka 2 | 3 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/2574897d4d0646b4a2f2a34c0b86fc35)](https://app.codacy.com/app/rahulsinghai/jmeter-backend-listener-kafka?utm_source=github.com&utm_medium=referral&utm_content=rahulsinghai/jmeter-backend-listener-kafka&utm_campaign=Badge_Grade_Dashboard) 4 | [![codecov](https://codecov.io/gh/rahulsinghai/jmeter-backend-listener-kafka/branch/master/graph/badge.svg)](https://codecov.io/gh/rahulsinghai/jmeter-backend-listener-kafka) 5 | [![Build Status](https://travis-ci.org/rahulsinghai/jmeter-backend-listener-kafka.svg?branch=master)](https://travis-ci.org/rahulsinghai/jmeter-backend-listener-kafka) 6 | 7 | A JMeter plug-in that enables you to send test results to a Kafka server. 8 | 9 | ## Overview 10 | 11 | ### Description 12 | 13 | JMeter Backend Listener Kafka is a JMeter plugin enabling you to send test results to a Kafka server. 14 | It is inspired from JMeter [ElasticSearch](https://github.com/delirius325/jmeter-elasticsearch-backend-listener) backend listener plug-in. 15 | 16 | ### Features 17 | 18 | - Filters 19 | - Only send the samples you want, by using Filters! Simply type them as follows in the appropriate field: `filter1;filter2;filter3` or `sampleLabel_must_contain_this`. 20 | 21 | - Specific fields `field1;field2;field3` 22 | - Specify fields that you want to send to Kafka (possible fields below): 23 | - AllThreads 24 | - BodySize 25 | - Bytes 26 | - SentBytes 27 | - ConnectTime 28 | - ContentType 29 | - DataType 30 | - ErrorCount 31 | - GrpThreads 32 | - IdleTime 33 | - Latency 34 | - ResponseTime 35 | - SampleCount 36 | - SampleLabel 37 | - ThreadName 38 | - URL 39 | - ResponseCode 40 | - TestStartTime 41 | - SampleStartTime 42 | - SampleEndTime 43 | - Timestamp 44 | - InjectorHostname 45 | 46 | - Verbose, semi-verbose, error only, and quiet mode: 47 | - **debug** : Send request/response information of all samplers (headers, body, etc.) 48 | - **info** : Sends all samplers to the Kafka server, but only sends the headers, body info for the failed samplers. 49 | - **quiet** : Only sends the response time, bytes, and other metrics 50 | - **error** : Only sends the failing samplers to the Kafka server (Along with their headers and body information). 51 | 52 | - Use Logstash/NiFi or any other tool to consume data from Kafka topic and then ingest it into a Database of your liking. 53 | 54 | ### Maven dependency 55 | 56 | ```xml 57 | 58 | io.github.rahulsinghai 59 | jmeter.backendlistener.kafka 60 | 1.0.1 61 | 62 | ``` 63 | 64 | ### Installing JMeter 65 | 66 | - SSH to a Unix machine with X-11 Forwarding enabled, and then set DISPLAY variable: 67 | 68 | ```bash 69 | export DISPLAY=Your_terminal_IP:0.0 70 | ``` 71 | 72 | - Download [JMeter](https://jmeter.apache.org/download_jmeter.cgi) binary and extract it: 73 | 74 | ```bash 75 | mkdir -P /home/jmeter 76 | cd /home/jmeter 77 | curl -O -k http://mirror.vorboss.net/apache//jmeter/binaries/apache-jmeter-5.1.1.tgz 78 | tar -zxvf apache-jmeter-5.1.1.tgz 79 | ln -s apache-jmeter-5.1.1 ./current 80 | export JMETER_HOME=/data/elastic/jmeter/current 81 | ``` 82 | 83 | - Download and install [Plugin Manager](https://jmeter-plugins.org/wiki/PluginsManager/) to `lib/ext` folder: 84 | 85 | ```bash 86 | curl -O -k http://search.maven.org/remotecontent?filepath=kg/apc/jmeter-plugins-manager/1.3/jmeter-plugins-manager-1.3.jar 87 | mv jmeter-plugins-manager-1.3.jar apache-jmeter-5.1.1/lib/ext/ 88 | ``` 89 | 90 | Detailed instructions on installing Plug-ins Manager are available at this [blog](https://octoperf.com/blog/2018/04/04/jmeter-plugins-install/). 91 | 92 | - Start JMeter: 93 | 94 | ```bash 95 | cd $JMETER_HOME 96 | JVM_ARGS="-Dhttps.proxyHost=myproxy.com -Dhttps.proxyPort=8080 -Dhttp.proxyUser=user -Dhttp.proxyPass=***" ./bin/jmeter.sh 97 | ``` 98 | 99 | ### Packaging and testing your newly added code 100 | 101 | - Build the artefact: Execute below mvn command. Make sure JAVA_HOME is set properly 102 | 103 | ```bash 104 | mvn clean package 105 | ``` 106 | 107 | - Move the resulting JAR to your `JMETER_HOME/lib/ext`. 108 | 109 | ```bash 110 | mv target/jmeter.backendlistener.kafka-1.0.0-SNAPSHOT.jar $JMETER_HOME/lib/ext/ 111 | ``` 112 | 113 | - Restart JMeter 114 | 115 | - Go to Options > Plugins Manager 116 | 117 | - You will find Kafka Backend listener plug-in mentioned in the Installed plug-ins tab. 118 | 119 | ### Configuring jmeter-backend-listener-kafka plug-in 120 | 121 | - In your **Test Pan**, right click on **Thread Group** > Add > Listener > Backend Listener 122 | - Choose `io.github.rahulsinghai.jmeter.backendlistener.kafka.KafkaBackendClient` as `Backend Listener Implementation`. 123 | - Specify parameters as shown in image below (**bootstrap.servers** and **kafka.topic** are mandatory ones): 124 | 125 | ![Screenshot of configuration](docs/configuration.JPG "Screenshot of configuration") 126 | 127 | ### Running your JMeter test plan 128 | 129 | You can run the test plan in GUI mode or in CLI mode using command like below: 130 | 131 | ```bash 132 | bin/jmeter -H [HTTP proxy server] -P [HTTP proxy port] -N "localhost|127.0.0.1|*.singhaiuklimited.com" -n -t test_kafkaserver.jmx -l test_kafkaserver_result.jtl 133 | ``` 134 | 135 | ## Screenshots 136 | 137 | ### Sample Grafana dashboard 138 | 139 | ![Sample Grafana dashboard](https://image.ibb.co/jW6LNx/Screen_Shot_2018_03_21_at_10_21_18_AM.png "Sample Grafana Dashboard") 140 | 141 | ### For more info 142 | 143 | For more information, here's a little [documentation](https://github.com/rahulsinghai/jmeter-backend-listener-kafka/wiki). 144 | 145 | ## Contributing 146 | 147 | Feel free to contribute by branching and making pull requests, or simply by suggesting ideas through the "Issues" tab. 148 | 149 | ### Code Styling 150 | 151 | - Please find instructions [here](https://github.com/HPI-Information-Systems/Metanome/wiki/Installing-the-google-styleguide-settings-in-intellij-and-eclipse) on how to configure your IntelliJ or Eclipse to format the source code according to Google style. 152 | Once configured in IntelliJ, format code as normal with `Ctrl + Alt + L`. 153 | 154 | Adding the XML file alone and auto-formatting the whole document could replace imports with wildcard imports, which isn't always what we want. 155 | 156 | - To stop this from happening, Go to `File` → `Settings` → `Editor` → `Code Style` → `Java` and select the `Imports` tab. 157 | - Set `Class Count to use import with '*'` and `Names count to us static import with '*'` to a higher value; anything over `999` should be fine. 158 | 159 | You can now reformat code throughout your project without imports being changed to Wildcard imports. 160 | 161 | - You also need to use `maven-git-code-format` plugin in `pom.xml` to auto format the code according to Google code style before any Git commit. 162 | 163 | ### [Markdown formatting](https://github.com/remarkjs/remark/tree/master/packages/remark-cli) 164 | 165 | Use **remark-cli** to format markdown files. 166 | It ensures a single style is used: list items use one type of bullet (_, -, +), emphasis (_ or \_) and importance (\_\_ or \*\*) use a standard marker, table fences are aligned, and more. 167 | 168 | - Install `remark-cli` and `remark-preset-lint-recommended` 169 | 170 | ```bash 171 | npm install remark-cli -g 172 | npm install remark-preset-lint-recommended -g 173 | 174 | # Add a table of contents to `README.md` 175 | remark README.md --use toc --output 176 | 177 | # Lint markdown files in the current directory 178 | # according to the markdown style guide. 179 | remark README.md --use remark-preset-lint-recommended -o 180 | 181 | # Rewrite all applicable files 182 | remark . -o 183 | ``` 184 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | notify: 3 | gitter: 4 | default: 5 | url: "https://webhooks.gitter.im/e/26065428feb26ee58616" 6 | threshold: 1% 7 | range: "50...100" -------------------------------------------------------------------------------- /docs/Elasticsearch_jmeter_metrics_template.md: -------------------------------------------------------------------------------- 1 | # Elasticsearch index template 2 | 3 | Below are instructions to create an Elasticsearch template for the index that will be used to store JMeter metrics: 4 | 5 | ```text 6 | HEAD _template/jmeter_metrics_template 7 | GET /_template/jmeter_metrics_template 8 | GET _template/*jmeter_metrics* 9 | DELETE /_template/jmeter_metrics_template 10 | 11 | PUT _template/jmeter_metrics_template 12 | { 13 | "order": 1, 14 | "index_patterns": "jmeter_metrics-*", 15 | "settings": { 16 | "index": { 17 | "codec": "best_compression", 18 | "mapping": { 19 | "total_fields": { 20 | "limit": "256" 21 | } 22 | }, 23 | "refresh_interval": "1s", 24 | "number_of_replicas": "2", 25 | "number_of_shards": "1" 26 | } 27 | }, 28 | "mappings": { 29 | "logs": { 30 | "dynamic_templates": [ 31 | { 32 | "strings_as_keywords": { 33 | "match_mapping_type": "string", 34 | "mapping": { 35 | "type": "keyword" 36 | } 37 | } 38 | } 39 | ], 40 | "properties": { 41 | "@timestamp": { 42 | "type": "date", 43 | "format": "dateOptionalTime" 44 | }, 45 | "@version": { 46 | "type": "keyword", 47 | "ignore_above": 256 48 | }, 49 | "AllThreads": { 50 | "type": "integer" 51 | }, 52 | "AssertionResults": { 53 | "properties": { 54 | "failure": { 55 | "type": "boolean" 56 | }, 57 | "failureMessage": { 58 | "type": "text", 59 | "index": false 60 | }, 61 | "name": { 62 | "type": "text", 63 | "index": false 64 | } 65 | } 66 | }, 67 | "BodySize": { 68 | "type": "long" 69 | }, 70 | "BuildNumber": { 71 | "type": "integer" 72 | }, 73 | "Bytes": { 74 | "type": "long" 75 | }, 76 | "ConnectTime": { 77 | "type": "long" 78 | }, 79 | "ContentType": { 80 | "type": "text", 81 | "fields": { 82 | "keyword": { 83 | "type": "keyword", 84 | "ignore_above": 256 85 | } 86 | } 87 | }, 88 | "DataType": { 89 | "type": "keyword", 90 | "ignore_above": 256 91 | }, 92 | "ElapsedTime": { 93 | "type": "date", 94 | "format": "dateOptionalTime" 95 | }, 96 | "ElapsedTimeComparison": { 97 | "type": "date", 98 | "format": "dateOptionalTime" 99 | }, 100 | "ErrorCount": { 101 | "type": "integer" 102 | }, 103 | "FailureMessage": { 104 | "type": "text", 105 | "index": false 106 | }, 107 | "GrpThreads": { 108 | "type": "integer" 109 | }, 110 | "IdleTime": { 111 | "type": "long" 112 | }, 113 | "InjectorHostname": { 114 | "type": "text", 115 | "index": false 116 | }, 117 | "Latency": { 118 | "type": "long" 119 | }, 120 | "RequestBody": { 121 | "type": "text", 122 | "index": false 123 | }, 124 | "RequestHeaders": { 125 | "type": "text", 126 | "index": false 127 | }, 128 | "ResponseBody": { 129 | "type": "text", 130 | "index": false 131 | }, 132 | "ResponseCode": { 133 | "type": "keyword", 134 | "ignore_above": 256 135 | }, 136 | "ResponseHeaders": { 137 | "type": "text", 138 | "index": false 139 | }, 140 | "ResponseMessage": { 141 | "type": "text", 142 | "index": false 143 | }, 144 | "ResponseTime": { 145 | "type": "long" 146 | }, 147 | "SampleCount": { 148 | "type": "integer" 149 | }, 150 | "SampleEndTime": { 151 | "type": "date", 152 | "format": "dateOptionalTime" 153 | }, 154 | "SampleLabel": { 155 | "type": "keyword", 156 | "ignore_above": 256 157 | }, 158 | "SampleStartTime": { 159 | "type": "date", 160 | "format": "dateOptionalTime" 161 | }, 162 | "SentBytes": { 163 | "type": "long" 164 | }, 165 | "Success": { 166 | "type": "boolean" 167 | }, 168 | "TestElement": { 169 | "properties": { 170 | "name": { 171 | "type": "text", 172 | "index": false 173 | } 174 | } 175 | }, 176 | "TestStartTime": { 177 | "type": "long" 178 | }, 179 | "ThreadName": { 180 | "type": "keyword", 181 | "ignore_above": 256 182 | }, 183 | "Timestamp": { 184 | "type": "date", 185 | "format": "dateOptionalTime" 186 | }, 187 | "URL": { 188 | "type": "keyword", 189 | "ignore_above": 256 190 | } 191 | } 192 | } 193 | }, 194 | "aliases": { 195 | "jmeter_metrics": {} 196 | } 197 | } 198 | 199 | DELETE jmeter_metrics-2019-06-28 200 | PUT jmeter_metrics-2019-06-28 201 | GET jmeter_metrics-2019-06-28/_mapping 202 | GET jmeter_metrics-2019-06-28/_aliases 203 | GET jmeter_metrics-2019-06-28/_settings 204 | 205 | GET _cat/indices/jmeter_metrics-*?v&s=index 206 | GET jmeter_metrics/_search 207 | { 208 | "query": { 209 | "match_all": {} 210 | } 211 | } 212 | ``` 213 | -------------------------------------------------------------------------------- /docs/Kibana_saved_objects.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "JMeter_Metrics_Synthesis_Report_visualization", 4 | "_type": "visualization", 5 | "_source": { 6 | "title": "JMeter Metrics Synthesis Report", 7 | "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{\"customLabel\":\"# of samples\"},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"customLabel\":\"Transaction\",\"field\":\"SampleLabel\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"size\":10},\"schema\":\"bucket\",\"type\":\"terms\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"customLabel\":\"Avg. response time (msec)\",\"field\":\"ResponseTime\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"enabled\":true,\"id\":\"4\",\"params\":{\"customLabel\":\"# of errors\",\"field\":\"ErrorCount\"},\"schema\":\"metric\",\"type\":\"sum\"},{\"enabled\":true,\"id\":\"5\",\"params\":{\"customLabel\":\"Avg. Bytes\",\"field\":\"Bytes\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"enabled\":true,\"id\":\"6\",\"params\":{\"customBucket\":{\"enabled\":true,\"id\":\"6-bucket\",\"params\":{\"customInterval\":\"2h\",\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"s\",\"min_doc_count\":1},\"schema\":{\"aggFilter\":[],\"deprecate\":false,\"editor\":false,\"group\":\"none\",\"max\":null,\"min\":0,\"name\":\"bucketAgg\",\"params\":[],\"title\":\"Bucket Agg\"},\"type\":\"date_histogram\"},\"customLabel\":\"Sent bytes per second\",\"customMetric\":{\"enabled\":true,\"id\":\"6-metric\",\"params\":{\"field\":\"SentBytes\"},\"schema\":{\"aggFilter\":[\"!top_hits\",\"!percentiles\",\"!percentile_ranks\",\"!median\",\"!std_dev\",\"!sum_bucket\",\"!avg_bucket\",\"!min_bucket\",\"!max_bucket\",\"!derivative\",\"!moving_avg\",\"!serial_diff\",\"!cumulative_sum\"],\"deprecate\":false,\"editor\":false,\"group\":\"none\",\"max\":null,\"min\":0,\"name\":\"metricAgg\",\"params\":[],\"title\":\"Metric Agg\"},\"type\":\"sum\"}},\"schema\":\"metric\",\"type\":\"avg_bucket\"},{\"enabled\":true,\"id\":\"7\",\"params\":{\"customBucket\":{\"enabled\":true,\"id\":\"7-bucket\",\"params\":{\"customInterval\":\"2h\",\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"m\",\"min_doc_count\":1},\"schema\":{\"aggFilter\":[],\"deprecate\":false,\"editor\":false,\"group\":\"none\",\"max\":null,\"min\":0,\"name\":\"bucketAgg\",\"params\":[],\"title\":\"Bucket Agg\"},\"type\":\"date_histogram\"},\"customLabel\":\"Samples per minute\",\"customMetric\":{\"enabled\":true,\"id\":\"7-metric\",\"params\":{\"field\":\"SampleCount\"},\"schema\":{\"aggFilter\":[\"!top_hits\",\"!percentiles\",\"!percentile_ranks\",\"!median\",\"!std_dev\",\"!sum_bucket\",\"!avg_bucket\",\"!min_bucket\",\"!max_bucket\",\"!derivative\",\"!moving_avg\",\"!serial_diff\",\"!cumulative_sum\"],\"deprecate\":false,\"editor\":false,\"group\":\"none\",\"max\":null,\"min\":0,\"name\":\"metricAgg\",\"params\":[],\"title\":\"Metric Agg\"},\"type\":\"sum\"}},\"schema\":\"metric\",\"type\":\"avg_bucket\"},{\"enabled\":true,\"id\":\"8\",\"params\":{\"customLabel\":\"Response time\",\"field\":\"ResponseTime\",\"percents\":[90]},\"schema\":\"metric\",\"type\":\"percentiles\"}],\"params\":{\"perPage\":10,\"showMeticsAtAllLevels\":false,\"showPartialRows\":false,\"showTotal\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"totalFunc\":\"sum\"},\"title\":\"JMeter Metrics Synthesis Report\",\"type\":\"table\"}", 8 | "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", 9 | "description": "", 10 | "version": 1, 11 | "kibanaSavedObjectMeta": { 12 | "searchSourceJSON": "{\"index\":\"jmeter_metrics\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" 13 | } 14 | } 15 | }, 16 | { 17 | "_id": "JMeter_Metrics_Number_of_active_threads_visualization", 18 | "_type": "visualization", 19 | "_source": { 20 | "title": "JMeter Metrics Number of active threads", 21 | "visState": "{\"title\":\"JMeter Metrics Number of active threads\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"AllThreads\",\"customLabel\":\"Number of active threads\"}}]}", 22 | "uiStateJSON": "{}", 23 | "description": "", 24 | "version": 1, 25 | "kibanaSavedObjectMeta": { 26 | "searchSourceJSON": "{\"index\":\"jmeter_metrics\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" 27 | } 28 | } 29 | }, 30 | { 31 | "_id": "JMeter_Metrics_Transactions_per_minute_visualization", 32 | "_type": "visualization", 33 | "_source": { 34 | "title": "JMeter Metrics Transactions per minute", 35 | "visState": "{\"title\":\"JMeter Metrics Transactions per minute\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":true,\"style\":{\"color\":\"#eee\"},\"valueAxis\":\"ValueAxis-1\"},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"# of transactions\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"histogram\",\"mode\":\"normal\",\"data\":{\"label\":\"# of transactions\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"sum\",\"schema\":\"metric\",\"params\":{\"field\":\"SampleCount\",\"customLabel\":\"# of transactions\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"Timestamp\",\"interval\":\"m\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{},\"customLabel\":\"Transactions per minute\"}}]}", 36 | "uiStateJSON": "{\"vis\":{\"legendOpen\":false}}", 37 | "description": "", 38 | "version": 1, 39 | "kibanaSavedObjectMeta": { 40 | "searchSourceJSON": "{\"index\":\"jmeter_metrics\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" 41 | } 42 | } 43 | }, 44 | { 45 | "_id": "JMeter_Metrics_90th_percentile_of_response_time_visualization", 46 | "_type": "visualization", 47 | "_source": { 48 | "title": "JMeter Metrics 90th percentile of response time", 49 | "visState": "{\"title\":\"JMeter Metrics 90th percentile of response time\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"terms\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"percentile\",\"percentiles\":[{\"value\":\"90\",\"percentile\":\"\",\"shade\":0.2,\"id\":\"e47183a0-9cbc-11e9-b82c-4141549369ad\",\"mode\":\"line\"}],\"field\":\"ResponseTime\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"ms,s,3\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"label\":\"90th percentile of response time\",\"terms_field\":\"SampleLabel\",\"value_template\":\"{{value}} sec\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"jmeter_metrics\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1},\"aggs\":[]}", 50 | "uiStateJSON": "{}", 51 | "description": "", 52 | "version": 1, 53 | "kibanaSavedObjectMeta": { 54 | "searchSourceJSON": "{}" 55 | } 56 | } 57 | }, 58 | { 59 | "_id": "JMeter_Metrics_Number_of_threads_Vs_Response_time_visualization", 60 | "_type": "visualization", 61 | "_source": { 62 | "title": "JMeter Metrics Number of threads Vs Response time", 63 | "visState": "{\"title\":\"JMeter Metrics Number of threads Vs Response time\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"avg\",\"field\":\"AllThreads\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":\"0\",\"stacked\":\"none\",\"label\":\"Average Number of Threads\"},{\"id\":\"2fd83430-9cb6-11e9-b82c-4141549369ad\",\"color\":\"rgba(252,196,0,1)\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"2fd83431-9cb6-11e9-b82c-4141549369ad\",\"type\":\"avg\",\"field\":\"ResponseTime\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":\"0\",\"stacked\":\"none\",\"label\":\"Average Response Time\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"jmeter_metrics\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1},\"aggs\":[]}", 64 | "uiStateJSON": "{}", 65 | "description": "", 66 | "version": 1, 67 | "kibanaSavedObjectMeta": { 68 | "searchSourceJSON": "{}" 69 | } 70 | } 71 | }, 72 | { 73 | "_id": "JMeter_Metrics_Number_of_errors_visualization", 74 | "_type": "visualization", 75 | "_source": { 76 | "title": "JMeter Metrics Number of errors", 77 | "visState": "{\"title\":\"JMeter Metrics Number of errors\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"terms\",\"metrics\":[{\"value\":\"99\",\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"sum\",\"field\":\"ErrorCount\",\"percentiles\":[{\"id\":\"7ace2b20-9cbb-11e9-b82c-4141549369ad\",\"mode\":\"line\",\"shade\":0.2,\"value\":50}]}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":\"0\",\"stacked\":\"none\",\"terms_field\":\"SampleLabel\",\"label\":\"# of errors\",\"terms_order_by\":\"61ca57f2-469d-11e7-af02-69e470af7417\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"jmeter_metrics\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1},\"aggs\":[]}", 78 | "uiStateJSON": "{}", 79 | "description": "", 80 | "version": 1, 81 | "kibanaSavedObjectMeta": { 82 | "searchSourceJSON": "{}" 83 | } 84 | } 85 | }, 86 | { 87 | "_id": "JMeter_Metrics_Response_time_visualization", 88 | "_type": "visualization", 89 | "_source": { 90 | "title": "JMeter Metrics Response time", 91 | "visState": "{\"title\":\"JMeter Metrics Response time\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"terms\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"avg\",\"field\":\"ResponseTime\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"ms,s,3\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":\"0\",\"stacked\":\"none\",\"label\":\"Response Time\",\"terms_field\":\"SampleLabel\",\"terms_order_by\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"value_template\":\"{{value}} sec\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"jmeter_metrics\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1},\"aggs\":[]}", 92 | "uiStateJSON": "{}", 93 | "description": "", 94 | "version": 1, 95 | "kibanaSavedObjectMeta": { 96 | "searchSourceJSON": "{}" 97 | } 98 | } 99 | }, 100 | { 101 | "_id": "JMeter_Metrics_Sent_KB_per_minute_per_transaction_visualization", 102 | "_type": "visualization", 103 | "_source": { 104 | "title": "JMeter Metrics Sent KB per minute per transaction", 105 | "visState": "{\"title\":\"JMeter Metrics Sent KB per minute per transaction\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"terms\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"sum\",\"field\":\"SentBytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"bytes\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":\"0\",\"stacked\":\"none\",\"label\":\"Sent KB/minute per transaction\",\"terms_field\":\"SampleLabel\",\"terms_order_by\":\"61ca57f2-469d-11e7-af02-69e470af7417\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"jmeter_metrics\",\"interval\":\"1m\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1},\"aggs\":[]}", 106 | "uiStateJSON": "{}", 107 | "description": "", 108 | "version": 1, 109 | "kibanaSavedObjectMeta": { 110 | "searchSourceJSON": "{}" 111 | } 112 | } 113 | }, 114 | { 115 | "_id": "JMeter_Metrics_Hits_per_minute_visualization", 116 | "_type": "visualization", 117 | "_source": { 118 | "title": "JMeter Metrics Hits per minute", 119 | "visState": "{\"title\":\"JMeter Metrics Hits per minute\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"terms\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"count\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":\"0\",\"stacked\":\"none\",\"terms_field\":\"SampleLabel\",\"label\":\"Hits per minute\",\"terms_order_by\":\"61ca57f2-469d-11e7-af02-69e470af7417\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"jmeter_metrics\",\"interval\":\"1m\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1},\"aggs\":[]}", 120 | "uiStateJSON": "{}", 121 | "description": "", 122 | "version": 1, 123 | "kibanaSavedObjectMeta": { 124 | "searchSourceJSON": "{}" 125 | } 126 | } 127 | }, 128 | { 129 | "_id": "JMeter_Metrics_Controls_visualization", 130 | "_type": "visualization", 131 | "_source": { 132 | "title": "JMeter Metrics Controls", 133 | "visState": "{\"title\":\"JMeter Metrics Controls\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"fieldName\":\"SampleLabel\",\"id\":\"1542568300307\",\"indexPattern\":\"jmeter_metrics\",\"label\":\"Transaction\",\"options\":{\"multiselect\":true,\"order\":\"desc\",\"size\":5,\"type\":\"terms\"},\"type\":\"list\"},{\"id\":\"1542568419863\",\"indexPattern\":\"jmeter_metrics\",\"fieldName\":\"ThreadName\",\"label\":\"ThreadName\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"size\":5,\"order\":\"desc\"},\"parent\":\"\"}],\"updateFiltersOnChange\":true,\"useTimeFilter\":true,\"pinFilters\":false},\"aggs\":[]}", 134 | "uiStateJSON": "{}", 135 | "description": "", 136 | "version": 1, 137 | "kibanaSavedObjectMeta": { 138 | "searchSourceJSON": "{}" 139 | } 140 | } 141 | }, 142 | { 143 | "_id": "JMeter_Metrics_Intensity_visualization", 144 | "_type": "visualization", 145 | "_source": { 146 | "title": "JMeter Metrics Intensity", 147 | "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{\"customBucket\":{\"enabled\":true,\"id\":\"1-bucket\",\"params\":{\"customInterval\":\"2h\",\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"m\",\"min_doc_count\":1},\"schema\":{\"aggFilter\":[],\"deprecate\":false,\"editor\":false,\"group\":\"none\",\"max\":null,\"min\":0,\"name\":\"bucketAgg\",\"params\":[],\"title\":\"Bucket Agg\"},\"type\":\"date_histogram\"},\"customLabel\":\"Transactions per minute\",\"customMetric\":{\"enabled\":true,\"id\":\"1-metric\",\"params\":{\"field\":\"SampleCount\"},\"schema\":{\"aggFilter\":[\"!top_hits\",\"!percentiles\",\"!percentile_ranks\",\"!median\",\"!std_dev\",\"!sum_bucket\",\"!avg_bucket\",\"!min_bucket\",\"!max_bucket\",\"!derivative\",\"!moving_avg\",\"!serial_diff\",\"!cumulative_sum\"],\"deprecate\":false,\"editor\":false,\"group\":\"none\",\"max\":null,\"min\":0,\"name\":\"metricAgg\",\"params\":[],\"title\":\"Metric Agg\"},\"type\":\"sum\"}},\"schema\":\"metric\",\"type\":\"avg_bucket\"}],\"params\":{\"addLegend\":false,\"addTooltip\":true,\"metric\":{\"colorSchema\":\"Green to Red\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"invertColors\":false,\"labels\":{\"show\":true},\"metricColorMode\":\"None\",\"percentageMode\":false,\"style\":{\"bgColor\":false,\"bgFill\":\"#000\",\"fontSize\":60,\"labelColor\":false,\"subText\":\"\"},\"useRanges\":false},\"type\":\"metric\"},\"title\":\"JMeter Metrics Intensity\",\"type\":\"metric\"}", 148 | "uiStateJSON": "{}", 149 | "description": "", 150 | "version": 1, 151 | "kibanaSavedObjectMeta": { 152 | "searchSourceJSON": "{\"index\":\"jmeter_metrics\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" 153 | } 154 | } 155 | }, 156 | { 157 | "_id": "JMeter_Metrics_Statistics_Dashboard", 158 | "_type": "dashboard", 159 | "_source": { 160 | "title": "JMeter Metrics Statistics Dashboard", 161 | "hits": 0, 162 | "description": "", 163 | "panelsJSON": "[{\"panelIndex\":\"6\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":16,\"i\":\"6\"},\"embeddableConfig\":{\"vis\":{\"legendOpen\":false}},\"id\":\"JMeter_Metrics_Transactions_per_minute_visualization\",\"type\":\"visualization\",\"version\":\"6.4.0\"},{\"panelIndex\":\"8\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":5,\"i\":\"8\"},\"id\":\"JMeter_Metrics_Controls_visualization\",\"type\":\"visualization\",\"version\":\"6.3.1\"},{\"panelIndex\":\"9\",\"gridData\":{\"x\":0,\"y\":16,\"w\":24,\"h\":15,\"i\":\"9\"},\"embeddableConfig\":{},\"id\":\"JMeter_Metrics_90th_percentile_of_response_time_visualization\",\"type\":\"visualization\",\"version\":\"6.3.1\"},{\"panelIndex\":\"10\",\"gridData\":{\"x\":24,\"y\":31,\"w\":24,\"h\":15,\"i\":\"10\"},\"embeddableConfig\":{},\"id\":\"JMeter_Metrics_Hits_per_minute_visualization\",\"type\":\"visualization\",\"version\":\"6.3.1\"},{\"panelIndex\":\"11\",\"gridData\":{\"x\":0,\"y\":31,\"w\":24,\"h\":15,\"i\":\"11\"},\"embeddableConfig\":{},\"id\":\"JMeter_Metrics_Number_of_errors_visualization\",\"type\":\"visualization\",\"version\":\"6.3.1\"},{\"panelIndex\":\"12\",\"gridData\":{\"x\":24,\"y\":16,\"w\":24,\"h\":15,\"i\":\"12\"},\"embeddableConfig\":{},\"id\":\"JMeter_Metrics_Number_of_threads_Vs_Response_time_visualization\",\"type\":\"visualization\",\"version\":\"6.3.1\"},{\"panelIndex\":\"13\",\"gridData\":{\"x\":0,\"y\":46,\"w\":24,\"h\":15,\"i\":\"13\"},\"embeddableConfig\":{},\"id\":\"JMeter_Metrics_Response_time_visualization\",\"type\":\"visualization\",\"version\":\"6.3.1\"},{\"panelIndex\":\"14\",\"gridData\":{\"x\":24,\"y\":46,\"w\":24,\"h\":15,\"i\":\"14\"},\"embeddableConfig\":{},\"id\":\"JMeter_Metrics_Sent_KB_per_minute_per_transaction_visualization\",\"type\":\"visualization\",\"version\":\"6.3.1\"},{\"panelIndex\":\"15\",\"gridData\":{\"x\":0,\"y\":61,\"w\":48,\"h\":24,\"i\":\"15\"},\"embeddableConfig\":{},\"id\":\"JMeter_Metrics_Synthesis_Report_visualization\",\"type\":\"visualization\",\"version\":\"6.3.1\"},{\"panelIndex\":\"16\",\"gridData\":{\"x\":12,\"y\":5,\"w\":12,\"h\":11,\"i\":\"16\"},\"embeddableConfig\":{},\"id\":\"JMeter_Metrics_Number_of_active_threads_visualization\",\"type\":\"visualization\",\"version\":\"6.3.1\"},{\"panelIndex\":\"17\",\"gridData\":{\"x\":0,\"y\":5,\"w\":12,\"h\":11,\"i\":\"17\"},\"version\":\"6.3.1\",\"type\":\"visualization\",\"id\":\"JMeter_Metrics_Intensity_visualization\",\"embeddableConfig\":{}}]", 164 | "optionsJSON": "{\"darkTheme\":true,\"hidePanelTitles\":false,\"useMargins\":false}", 165 | "version": 1, 166 | "timeRestore": true, 167 | "timeTo": "now", 168 | "timeFrom": "now-4h", 169 | "refreshInterval": { 170 | "display": "Off", 171 | "pause": false, 172 | "value": 0 173 | }, 174 | "kibanaSavedObjectMeta": { 175 | "searchSourceJSON": "{\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" 176 | } 177 | } 178 | } 179 | ] 180 | -------------------------------------------------------------------------------- /docs/configuration.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeranalyticsltd/jmeter-backend-listener-kafka/25db7c584c602553f9009db319b80f977dd2d064/docs/configuration.JPG -------------------------------------------------------------------------------- /docs/logstash.conf: -------------------------------------------------------------------------------- 1 | input { 2 | kafka { 3 | bootstrap_servers => "localhost:9092" 4 | topics => [ "JMETER_METRICS" ] 5 | codec => "json" 6 | group_id => "GRP_JMETER_METRICS_LOGSTASH_01" 7 | auto_offset_reset => "latest" 8 | } 9 | } 10 | 11 | filter { 12 | json { 13 | source => "message" 14 | } 15 | 16 | # Logstash has strange implementation for json filter above. 17 | # If it sees a field named @timestamp, then it expects it to be a seconds based value (UNIX rather than UNIX_MS) and tries to parse on its own to reuse as event's @timestamp. 18 | # In our case, @timestamp is milli-seconds based long, hence it fails to parse and causes a warning with _timestampparsefailure tag. 19 | 20 | if "_timestampparsefailure" in [tags] { 21 | date { 22 | match => [ "_@timestamp", "UNIX_MS" ] 23 | remove_tag => "_timestampparsefailure" 24 | remove_field => "_@timestamp" 25 | } 26 | } 27 | 28 | date{ 29 | match => [ "ElapsedTime", "ISO8601", "yyyy-MM-dd HH:mm:ss" ] 30 | target => ElapsedTime 31 | } 32 | 33 | date{ 34 | match => [ "Timestamp", "ISO8601" ] 35 | target => Timestamp 36 | } 37 | 38 | date{ 39 | match => [ "SampleStartTime", "ISO8601" ] 40 | target => SampleStartTime 41 | } 42 | 43 | date{ 44 | match => [ "SampleEndTime", "ISO8601" ] 45 | target => SampleEndTime 46 | } 47 | 48 | ruby { 49 | code => "if (event.get('tags') != nil) && event.get('tags').length == 0 then event.remove('tags') end" 50 | } 51 | } 52 | 53 | output { 54 | #stdout { codec => rubydebug } 55 | 56 | elasticsearch { 57 | index => "jmeter_metrics-%{+yyyy-MM-dd}" 58 | hosts => ["localhost:9201"] 59 | user => "elastic" 60 | password => "changeme" 61 | document_type => "logs" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /intellij-java-google-style.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 21 | 28 | 599 | -------------------------------------------------------------------------------- /plugins-repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "jmeter.backendlistener.kafka", 3 | "name": "Kafka backend listener", 4 | "description": "Apache JMeter plugin for sending sample results to a Kafka server. ", 5 | "helpUrl": "https://github.com/rahulsinghai/jmeter-backend-listener-kafka/issues", 6 | "screenshotUrl": "https://github.com/rahulsinghai/jmeter-backend-listener-kafka/blob/master/docs/configuration.JPG", 7 | "vendor": "rahulsinghai", 8 | "markerClass": "io.github.rahulsinghai.jmeter.backendlistener.kafka.KafkaBackendClient", 9 | "versions": { 10 | "1.0.0": { 11 | "changes": "Initial version.", 12 | "downloadUrl": "https://search.maven.org/remotecontent?filepath=io/github/rahulsinghai/jmeter.backendlistener.kafka/1.0.0/jmeter.backendlistener.kafka-1.0.0.jar", 13 | "depends": [ 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.github.rahulsinghai 6 | jmeter.backendlistener.kafka 7 | 1.0.2-SNAPSHOT 8 | jar 9 | 10 | ${project.artifactId} 11 | JMeter Backend Listener Kafka is a JMeter plugin enabling you to send test results to a Kafka server. It is meant as an alternative live-monitoring tool to the built-in "InfluxDB" backend listener of JMeter. 12 | https://github.com/rahulsinghai/jmeter-backend-listener-kafka 13 | 2019 14 | 15 | 16 | Rahul Singhai 17 | https://www.cse.iitb.ac.in/~rahuls_05 18 | 19 | 20 | 21 | 22 | The Apache Software License, Version 2.0 23 | http://www.apache.org/licenses/LICENSE-2.0.txt 24 | repo 25 | 26 | 27 | 28 | 29 | GitHub 30 | https://github.com/rahulsinghai/jmeter-backend-listener-kafka/issues 31 | 32 | 33 | 34 | 35 | rahulsinghai 36 | Rahul Singhai 37 | singrahu@gmail.com 38 | http://www.github.com/rahulsinghai 39 | Europe/London 40 | 41 | project owner 42 | developer 43 | 44 | 45 | 46 | 47 | 48 | scm:git:git://github.com/rahulsinghai/jmeter-backend-listener-kafka.git 49 | scm:git:ssh://git@github.com/rahulsinghai/jmeter-backend-listener-kafka.git 50 | https://github.com/rahulsinghai/jmeter-backend-listener-kafka/tree/master 51 | HEAD 52 | 53 | 54 | 55 | UTF-8 56 | UTF-8 57 | 1.8 58 | 1.8 59 | 3.8.1 60 | 3.0.0-M1 61 | 3.1.1 62 | 3.0.0-M2 63 | 1.6 64 | 1.24 65 | 0.8.4 66 | 3.1.0 67 | 1.6.8 68 | 2.5.3 69 | 3.2.1 70 | 3.1.0 71 | 3.0.0-M3 72 | 28.0-jre 73 | 2.8.5 74 | 5.4.2 75 | 3.8.1 76 | 5.1.1 77 | 2.3.0 78 | 79 | 80 | 81 | 82 | com.google.code.gson 83 | gson 84 | ${gson.version} 85 | 86 | 87 | com.google.guava 88 | guava 89 | ${guava.version} 90 | 91 | 92 | org.apache.jmeter 93 | ApacheJMeter_config 94 | ${org.apache.jmeter.version} 95 | provided 96 | 97 | 98 | org.apache.jmeter 99 | ApacheJMeter_core 100 | ${org.apache.jmeter.version} 101 | provided 102 | 103 | 104 | org.apache.jmeter 105 | ApacheJMeter_components 106 | ${org.apache.jmeter.version} 107 | provided 108 | 109 | 110 | org.apache.jmeter 111 | jorphan 112 | ${org.apache.jmeter.version} 113 | provided 114 | 115 | 116 | org.apache.commons 117 | commons-lang3 118 | ${org.apache.commons} 119 | provided 120 | 121 | 122 | org.apache.kafka 123 | kafka-clients 124 | ${org.apache.kafka} 125 | 126 | 127 | org.junit.jupiter 128 | junit-jupiter-engine 129 | ${junit.version} 130 | test 131 | 132 | 133 | org.junit.jupiter 134 | junit-jupiter 135 | ${junit.version} 136 | test 137 | 138 | 139 | org.jacoco 140 | org.jacoco.agent 141 | ${maven.jacoco.plugin.version} 142 | test 143 | runtime 144 | 145 | 146 | com.fasterxml.jackson.core 147 | jackson-databind 148 | [2.9.9,) 149 | 150 | 151 | 152 | 153 | 154 | ossrh 155 | Sonatype Nexus Snapshots 156 | https://oss.sonatype.org/content/repositories/snapshots 157 | 158 | 159 | ossrh 160 | Nexus Release Repository 161 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 162 | 163 | https://oss.sonatype.org/content/groups/public/io/github/rahulsinghai/jmeter.backendlistener.kafka/ 164 | 165 | 166 | 178 | 179 | 180 | release-sign-artifacts 181 | 182 | 183 | 184 | performRelease 185 | true 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | org.apache.maven.plugins 194 | maven-source-plugin 195 | ${maven.source.plugin.version} 196 | 197 | 198 | attach-sources 199 | 200 | jar-no-fork 201 | 202 | 203 | 204 | 205 | 206 | 207 | org.apache.maven.plugins 208 | maven-javadoc-plugin 209 | ${maven.javadoc.plugin.version} 210 | 211 | 212 | attach-javadocs 213 | 214 | jar 215 | 216 | 217 | 218 | 219 | 220 | 221 | org.apache.maven.plugins 222 | maven-gpg-plugin 223 | ${maven.gpg.plugin.version} 224 | 225 | 226 | sign-artifacts 227 | verify 228 | 229 | sign 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | org.apache.maven.plugins 243 | maven-release-plugin 244 | ${maven.release.plugin.version} 245 | 246 | v@{project.version} 247 | true 248 | release-sign-artifacts 249 | deploy 250 | 251 | 252 | 253 | 254 | org.sonatype.plugins 255 | nexus-staging-maven-plugin 256 | ${maven.nexus.staging.plugin.version} 257 | true 258 | 259 | ossrh 260 | https://oss.sonatype.org/ 261 | true 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | com.cosium.code 271 | maven-git-code-format 272 | ${maven.git.code.format.plugin.version} 273 | 274 | 275 | 276 | install-formatter-hook 277 | 278 | install-hooks 279 | 280 | 281 | 282 | 283 | validate-code-format 284 | 285 | validate-code-format 286 | 287 | 288 | 289 | 290 | 291 | 292 | maven-enforcer-plugin 293 | ${maven.enforcer.plugin.version} 294 | 295 | 296 | 297 | enforce 298 | 299 | 300 | 301 | 302 | [3.5.4,) 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | org.apache.maven.plugins 313 | maven-surefire-plugin 314 | ${maven.surefire.plugin.version} 315 | 316 | methods 317 | 10 318 | 2 319 | true 320 | 321 | 322 | 323 | 324 | org.apache.maven.plugins 325 | maven-compiler-plugin 326 | ${maven.compiler.plugin.version} 327 | 328 | 1.8 329 | 1.8 330 | 331 | 332 | 333 | 334 | org.apache.maven.plugins 335 | maven-dependency-plugin 336 | ${maven.dependency.plugin.version} 337 | 338 | 339 | copy-dependencies 340 | package 341 | 342 | copy-dependencies 343 | 344 | 345 | compile 346 | provided 347 | ${project.build.directory}/dependencies 348 | false 349 | false 350 | true 351 | 352 | 353 | 354 | 355 | 356 | 357 | org.apache.maven.plugins 358 | maven-deploy-plugin 359 | ${maven.deploy.plugin.version} 360 | 361 | 362 | default-deploy 363 | deploy 364 | 365 | deploy 366 | 367 | 368 | 369 | 370 | 371 | 372 | org.apache.maven.plugins 373 | maven-shade-plugin 374 | ${maven.shade.plugin.version} 375 | 376 | 377 | 378 | package 379 | 380 | shade 381 | 382 | 383 | 384 | 385 | org.apache.jmeter:* 386 | commons-codec:* 387 | commons-logging:* 388 | org.apache.httpcomponents:* 389 | net.java.dev.jna:* 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | org.jacoco 399 | jacoco-maven-plugin 400 | ${maven.jacoco.plugin.version} 401 | 402 | 406 | 407 | pre-unit-test 408 | 409 | prepare-agent 410 | 411 | 412 | 413 | 417 | 418 | post-unit-test 419 | test 420 | 421 | report 422 | 423 | 424 | 425 | 426 | 427 | jacoco-check 428 | 429 | check 430 | 431 | 432 | 433 | 434 | PACKAGE 435 | 436 | 437 | LINE 438 | COVEREDRATIO 439 | 0.25 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | -------------------------------------------------------------------------------- /src/main/java/io/github/rahulsinghai/jmeter/backendlistener/kafka/KafkaBackendClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Rahul Singhai. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.rahulsinghai.jmeter.backendlistener.kafka; 18 | 19 | import com.google.common.base.Strings; 20 | import com.google.gson.Gson; 21 | import io.github.rahulsinghai.jmeter.backendlistener.model.MetricsRow; 22 | import java.util.*; 23 | import java.util.regex.Matcher; 24 | import java.util.regex.Pattern; 25 | import org.apache.jmeter.config.Arguments; 26 | import org.apache.jmeter.samplers.SampleResult; 27 | import org.apache.jmeter.util.JMeterUtils; 28 | import org.apache.jmeter.visualizers.backend.AbstractBackendListenerClient; 29 | import org.apache.jmeter.visualizers.backend.BackendListenerContext; 30 | import org.apache.kafka.clients.producer.KafkaProducer; 31 | import org.apache.kafka.clients.producer.ProducerConfig; 32 | import org.apache.kafka.common.serialization.LongSerializer; 33 | import org.apache.kafka.common.serialization.StringSerializer; 34 | import org.slf4j.Logger; 35 | import org.slf4j.LoggerFactory; 36 | 37 | /** 38 | * A {@link org.apache.jmeter.visualizers.backend.Backend Backend} which produces Kafka messages. 39 | * 40 | * @author rahulsinghai 41 | * @since 20190624 42 | */ 43 | public class KafkaBackendClient extends AbstractBackendListenerClient { 44 | 45 | private static final Logger logger = LoggerFactory.getLogger(KafkaBackendClient.class); 46 | 47 | private static final String BUILD_NUMBER = "BuildNumber"; 48 | 49 | /** Parameter for setting the Kafka topic name. */ 50 | private static final String KAFKA_TOPIC = "kafka.topic"; 51 | 52 | private static final String KAFKA_FIELDS = "kafka.fields"; 53 | private static final String KAFKA_TIMESTAMP = "kafka.timestamp"; 54 | private static final String KAFKA_SAMPLE_FILTER = "kafka.sample.filter"; 55 | private static final String KAFKA_TEST_MODE = "kafka.test.mode"; 56 | private static final String KAFKA_PARSE_REQ_HEADERS = "kafka.parse.all.req.headers"; 57 | private static final String KAFKA_PARSE_RES_HEADERS = "kafka.parse.all.res.headers"; 58 | 59 | /** Parameter for setting the Kafka security protocol; "true" or "false". */ 60 | private static final String KAFKA_SSL_ENABLED = "kafka.ssl.enabled"; 61 | 62 | /** The password of the private key in the key store file. This is optional for client. */ 63 | private static final String KAFKA_SSL_KEY_PASSWORD = "kafka.ssl.key.password"; 64 | 65 | /** 66 | * The location of the key store file (include path information). This is optional for client and 67 | * can be used for two-way authentication for client. 68 | */ 69 | private static final String KAFKA_SSL_KEYSTORE_LOCATION = "kafka.ssl.keystore.location"; 70 | 71 | /** 72 | * The store password for the Kafka SSL key store file. This is optional for client and only 73 | * needed if kafka.ssl.keystore.location is configured. 74 | */ 75 | private static final String KAFKA_SSL_KEYSTORE_PASSWORD = "kafka.ssl.keystore.password"; 76 | 77 | /** 78 | * Parameter for setting the Kafka ssl truststore file location(include path information); for 79 | * example, "client.truststore.jks". 80 | */ 81 | private static final String KAFKA_SSL_TRUSTSTORE_LOCATION = "kafka.ssl.truststore.location"; 82 | 83 | /** 84 | * The password for the Kafka SSL trust store file. If a password is not set access to the 85 | * truststore is still available, but integrity checking is disabled. 86 | */ 87 | private static final String KAFKA_SSL_TRUSTSTORE_PASSWORD = "kafka.ssl.truststore.password"; 88 | 89 | /** The list of protocols enabled for SSL connections. */ 90 | private static final String KAFKA_SSL_ENABLED_PROTOCOLS = "kafka.ssl.enabled.protocols"; 91 | 92 | /** The file format of the key store file. This is optional for client. */ 93 | private static final String KAFKA_SSL_KEYSTORE_TYPE = "kafka.ssl.keystore.type"; 94 | 95 | /** 96 | * The SSL protocol used to generate the SSLContext. Default setting is TLS, which is fine for 97 | * most cases. Allowed values in recent JVMs are TLS, TLSv1.1 and TLSv1.2. SSL, SSLv2 and SSLv3 98 | * may be supported in older JVMs, but their usage is discouraged due to known security 99 | * vulnerabilities. 100 | */ 101 | private static final String KAFKA_SSL_PROTOCOL = "kafka.ssl.protocol"; 102 | 103 | /** 104 | * The name of the security provider used for SSL connections. Default value is the default 105 | * security provider of the JVM. 106 | */ 107 | private static final String KAFKA_SSL_PROVIDER = "kafka.ssl.provider"; 108 | 109 | /** The file format of the trust store file. */ 110 | private static final String KAFKA_SSL_TRUSTSTORE_TYPE = "kafka.ssl.truststore.type"; 111 | 112 | /** 113 | * The number of acknowledgments the producer requires the leader to have received before 114 | * considering a request complete. 115 | * 116 | * 121 | */ 122 | private static final String KAFKA_ACKS_CONFIG = "kafka.acks"; 123 | 124 | /** 125 | * A list of host/port pairs to use for establishing the initial connection to the Kafka cluster. 126 | * The client will make use of all servers irrespective of which servers are specified here for 127 | * bootstrapping—this list only impacts the initial hosts used to discover the full set of 128 | * servers. This list should be in the form host1:port1,host2:port2,.... Since these servers are 129 | * just used for the initial connection to discover the full cluster membership (which may change 130 | * dynamically), this list need not contain the full set of servers (you may want more than one, 131 | * though, in case a server is down). 132 | */ 133 | private static final String KAFKA_BOOTSTRAP_SERVERS_CONFIG = "kafka.bootstrap.servers"; 134 | 135 | /** 136 | * Optional compression type for all data generated by the producer. The default is none (i.e. no 137 | * compression). Valid values are none, gzip, snappy, 138 | * lz4, or zstd. Compression is of full batches of data, so the efficacy 139 | * of batching will also impact the compression ratio (more batching means better compression). 140 | */ 141 | private static final String KAFKA_COMPRESSION_TYPE_CONFIG = "kafka.compression.type"; 142 | 143 | /** 144 | * The producer will attempt to batch records together into fewer requests whenever multiple 145 | * records are being sent to the same partition. This helps performance on both the client and the 146 | * server. This configuration controls the default batch size in bytes. 147 | * 148 | *

No attempt will be made to batch records larger than this size. 149 | * 150 | *

Requests sent to brokers will contain multiple batches, one for each partition with data 151 | * available to be sent. 152 | * 153 | *

A small batch size will make batching less common and may reduce throughput (a batch size of 154 | * zero will disable batching entirely). A very large batch size may use memory a bit more 155 | * wastefully as we will always allocate a buffer of the specified batch size in anticipation of 156 | * additional records. 157 | */ 158 | private static final String KAFKA_BATCH_SIZE_CONFIG = "kafka.batch.size"; 159 | 160 | /** 161 | * An id string to pass to the server when making requests. The purpose of this is to be able to 162 | * track the source of requests beyond just ip/port by allowing a logical application name to be 163 | * included in server-side request logging. 164 | */ 165 | private static final String KAFKA_CLIENT_ID_CONFIG = "kafka.client.id"; 166 | 167 | /** Close idle connections after the number of milliseconds specified by this config. */ 168 | private static final String KAFKA_CONNECTIONS_MAX_IDLE_MS_CONFIG = 169 | "kafka.connections.max.idle.ms"; 170 | 171 | private static final Map DEFAULT_ARGS = new LinkedHashMap<>(); 172 | 173 | static { 174 | DEFAULT_ARGS.put(KAFKA_ACKS_CONFIG, "1"); 175 | DEFAULT_ARGS.put(KAFKA_BOOTSTRAP_SERVERS_CONFIG, null); 176 | DEFAULT_ARGS.put(KAFKA_TOPIC, null); 177 | DEFAULT_ARGS.put(KAFKA_SAMPLE_FILTER, null); 178 | DEFAULT_ARGS.put(KAFKA_FIELDS, null); 179 | DEFAULT_ARGS.put(KAFKA_TEST_MODE, "info"); 180 | DEFAULT_ARGS.put(KAFKA_PARSE_REQ_HEADERS, "false"); 181 | DEFAULT_ARGS.put(KAFKA_PARSE_RES_HEADERS, "false"); 182 | DEFAULT_ARGS.put(KAFKA_TIMESTAMP, "yyyy-MM-dd'T'HH:mm:ss.SSSZZ"); 183 | DEFAULT_ARGS.put(KAFKA_COMPRESSION_TYPE_CONFIG, null); 184 | DEFAULT_ARGS.put(KAFKA_SSL_ENABLED, "false"); 185 | DEFAULT_ARGS.put(KAFKA_SSL_KEY_PASSWORD, null); 186 | DEFAULT_ARGS.put(KAFKA_SSL_KEYSTORE_LOCATION, null); 187 | DEFAULT_ARGS.put(KAFKA_SSL_KEYSTORE_PASSWORD, null); 188 | DEFAULT_ARGS.put(KAFKA_SSL_TRUSTSTORE_LOCATION, null); 189 | DEFAULT_ARGS.put(KAFKA_SSL_TRUSTSTORE_PASSWORD, null); 190 | DEFAULT_ARGS.put(KAFKA_SSL_ENABLED_PROTOCOLS, "TLSv1.2,TLSv1.1,TLSv1"); 191 | DEFAULT_ARGS.put(KAFKA_SSL_KEYSTORE_TYPE, "JKS"); 192 | DEFAULT_ARGS.put(KAFKA_SSL_PROTOCOL, "TLS"); 193 | DEFAULT_ARGS.put(KAFKA_SSL_PROVIDER, null); 194 | DEFAULT_ARGS.put(KAFKA_SSL_TRUSTSTORE_TYPE, "JKS"); 195 | DEFAULT_ARGS.put(KAFKA_BATCH_SIZE_CONFIG, Integer.toString(16384)); 196 | DEFAULT_ARGS.put(KAFKA_CLIENT_ID_CONFIG, "JMeterKafkaBackendListener"); 197 | DEFAULT_ARGS.put(KAFKA_CONNECTIONS_MAX_IDLE_MS_CONFIG, Long.toString(180000L)); 198 | } 199 | 200 | private KafkaMetricPublisher publisher; 201 | private Set modes; 202 | private Set filters; 203 | private Set fields; 204 | private int buildNumber; 205 | 206 | @Override 207 | public Arguments getDefaultParameters() { 208 | Arguments arguments = new Arguments(); 209 | DEFAULT_ARGS.forEach(arguments::addArgument); 210 | return arguments; 211 | } 212 | 213 | @Override 214 | public void setupTest(BackendListenerContext context) throws Exception { 215 | this.filters = new HashSet<>(); 216 | this.fields = new HashSet<>(); 217 | this.modes = new HashSet<>(Arrays.asList("info", "debug", "error", "quiet")); 218 | this.buildNumber = 219 | (JMeterUtils.getProperty(KafkaBackendClient.BUILD_NUMBER) != null 220 | && !JMeterUtils.getProperty(KafkaBackendClient.BUILD_NUMBER).trim().equals("")) 221 | ? Integer.parseInt(JMeterUtils.getProperty(KafkaBackendClient.BUILD_NUMBER)) 222 | : 0; 223 | 224 | Properties props = new Properties(); 225 | props.put( 226 | ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, 227 | context.getParameter(KAFKA_BOOTSTRAP_SERVERS_CONFIG)); 228 | props.put(ProducerConfig.CLIENT_ID_CONFIG, context.getParameter(KAFKA_CLIENT_ID_CONFIG)); 229 | props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, LongSerializer.class.getName()); 230 | props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 231 | props.put(ProducerConfig.ACKS_CONFIG, context.getParameter(KAFKA_ACKS_CONFIG)); 232 | 233 | String compressionType = context.getParameter(KAFKA_COMPRESSION_TYPE_CONFIG); 234 | if (!Strings.isNullOrEmpty(compressionType)) { 235 | props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, compressionType); 236 | } 237 | 238 | // check if kafka security protocol is SSL or PLAINTEXT (default) 239 | if (context.getParameter(KAFKA_SSL_ENABLED).equals("true")) { 240 | logger.debug("Setting up SSL properties..."); 241 | props.put(KAFKA_SSL_KEY_PASSWORD, context.getParameter(KAFKA_SSL_KEY_PASSWORD)); 242 | props.put(KAFKA_SSL_KEYSTORE_LOCATION, context.getParameter(KAFKA_SSL_KEYSTORE_LOCATION)); 243 | props.put(KAFKA_SSL_KEYSTORE_PASSWORD, context.getParameter(KAFKA_SSL_KEYSTORE_PASSWORD)); 244 | props.put(KAFKA_SSL_TRUSTSTORE_LOCATION, context.getParameter(KAFKA_SSL_TRUSTSTORE_LOCATION)); 245 | props.put(KAFKA_SSL_TRUSTSTORE_PASSWORD, context.getParameter(KAFKA_SSL_TRUSTSTORE_PASSWORD)); 246 | props.put(KAFKA_SSL_ENABLED_PROTOCOLS, context.getParameter(KAFKA_SSL_ENABLED_PROTOCOLS)); 247 | props.put(KAFKA_SSL_KEYSTORE_TYPE, context.getParameter(KAFKA_SSL_KEYSTORE_TYPE)); 248 | props.put(KAFKA_SSL_PROTOCOL, context.getParameter(KAFKA_SSL_PROTOCOL)); 249 | props.put(KAFKA_SSL_PROVIDER, context.getParameter(KAFKA_SSL_PROVIDER)); 250 | props.put(KAFKA_SSL_TRUSTSTORE_TYPE, context.getParameter(KAFKA_SSL_TRUSTSTORE_TYPE)); 251 | } 252 | props.put( 253 | ProducerConfig.BATCH_SIZE_CONFIG, 254 | Integer.parseInt(context.getParameter(KAFKA_BATCH_SIZE_CONFIG))); 255 | props.put( 256 | ProducerConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG, 257 | Long.parseLong(context.getParameter(KAFKA_CONNECTIONS_MAX_IDLE_MS_CONFIG))); 258 | 259 | convertParameterToSet(context, KAFKA_SAMPLE_FILTER, this.filters); 260 | convertParameterToSet(context, KAFKA_FIELDS, this.fields); 261 | 262 | KafkaProducer producer = new KafkaProducer<>(props); 263 | this.publisher = new KafkaMetricPublisher(producer, context.getParameter(KAFKA_TOPIC)); 264 | 265 | checkTestMode(context.getParameter(KAFKA_TEST_MODE)); 266 | super.setupTest(context); 267 | } 268 | 269 | /** Method that converts a semicolon separated list contained in a parameter into a string set */ 270 | private void convertParameterToSet( 271 | BackendListenerContext context, String parameter, Set set) { 272 | String[] array = 273 | (context.getParameter(parameter).contains(";")) 274 | ? context.getParameter(parameter).split(";") 275 | : new String[] {context.getParameter(parameter)}; 276 | if (array.length > 0 && !array[0].trim().equals("")) { 277 | for (String entry : array) { 278 | set.add(entry.toLowerCase().trim()); 279 | if (logger.isDebugEnabled()) { 280 | logger.debug("Parsed from " + parameter + ": " + entry.toLowerCase().trim()); 281 | } 282 | } 283 | } 284 | } 285 | 286 | @Override 287 | public void handleSampleResults(List results, BackendListenerContext context) { 288 | for (SampleResult sr : results) { 289 | MetricsRow row = 290 | new MetricsRow( 291 | sr, 292 | context.getParameter(KAFKA_TEST_MODE), 293 | context.getParameter(KAFKA_TIMESTAMP), 294 | this.buildNumber, 295 | context.getBooleanParameter(KAFKA_PARSE_REQ_HEADERS, false), 296 | context.getBooleanParameter(KAFKA_PARSE_RES_HEADERS, false), 297 | fields); 298 | 299 | if (validateSample(context, sr)) { 300 | try { 301 | // Prefix to skip from adding service specific parameters to the metrics row 302 | String servicePrefixName = "kafka."; 303 | this.publisher.addToList(new Gson().toJson(row.getRowAsMap(context, servicePrefixName))); 304 | } catch (Exception e) { 305 | logger.error( 306 | "The Kafka Backend Listener was unable to add sampler to the list of samplers to send... More info in JMeter's console."); 307 | e.printStackTrace(); 308 | } 309 | } 310 | } 311 | 312 | try { 313 | this.publisher.publishMetrics(); 314 | } catch (Exception e) { 315 | logger.error("Error occurred while publishing to Kafka topic.", e); 316 | } finally { 317 | this.publisher.clearList(); 318 | } 319 | } 320 | 321 | @Override 322 | public void teardownTest(BackendListenerContext context) throws Exception { 323 | if (this.publisher.getListSize() > 0) { 324 | this.publisher.publishMetrics(); 325 | } 326 | this.publisher.closeProducer(); 327 | super.teardownTest(context); 328 | } 329 | 330 | /** 331 | * This method checks if the test mode is valid 332 | * 333 | * @param mode The test mode as String 334 | */ 335 | private void checkTestMode(String mode) { 336 | if (!this.modes.contains(mode)) { 337 | logger.warn( 338 | "The parameter \"kafka.test.mode\" isn't set properly. Three modes are allowed: debug ,info, and quiet."); 339 | logger.warn( 340 | " -- \"debug\": sends request and response details to Kafka. Info only sends the details if the response has an error."); 341 | logger.warn(" -- \"info\": should be used in production"); 342 | logger.warn(" -- \"error\": should be used if you."); 343 | logger.warn(" -- \"quiet\": should be used if you don't care to have the details."); 344 | } 345 | } 346 | 347 | /** 348 | * This method will validate the current sample to see if it is part of the filters or not. 349 | * 350 | * @param context The Backend Listener's context 351 | * @param sr The current SampleResult 352 | * @return true or false depending on whether or not the sample is valid 353 | */ 354 | private boolean validateSample(BackendListenerContext context, SampleResult sr) { 355 | boolean valid = true; 356 | String sampleLabel = sr.getSampleLabel().toLowerCase().trim(); 357 | 358 | if (this.filters.size() > 0) { 359 | for (String filter : filters) { 360 | Pattern pattern = Pattern.compile(filter); 361 | Matcher matcher = pattern.matcher(sampleLabel); 362 | 363 | if (sampleLabel.contains(filter) || matcher.find()) { 364 | valid = true; 365 | break; 366 | } else { 367 | valid = false; 368 | } 369 | } 370 | } 371 | 372 | // if sample is successful but test mode is "error" only 373 | if (sr.isSuccessful() 374 | && context.getParameter(KAFKA_TEST_MODE).trim().equalsIgnoreCase("error") 375 | && valid) { 376 | valid = false; 377 | } 378 | 379 | return valid; 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /src/main/java/io/github/rahulsinghai/jmeter/backendlistener/kafka/KafkaMetricPublisher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Rahul Singhai. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.rahulsinghai.jmeter.backendlistener.kafka; 18 | 19 | import java.util.LinkedList; 20 | import java.util.List; 21 | import org.apache.kafka.clients.producer.KafkaProducer; 22 | import org.apache.kafka.clients.producer.ProducerRecord; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | /** 27 | * A wrapper around Kafka Producer to publish messages. 28 | * 29 | * @author rahulsinghai 30 | * @since 20190624 31 | */ 32 | class KafkaMetricPublisher { 33 | 34 | private static final Logger logger = LoggerFactory.getLogger(KafkaMetricPublisher.class); 35 | 36 | private KafkaProducer producer; 37 | private String topic; 38 | private List metricList; 39 | 40 | KafkaMetricPublisher(KafkaProducer producer, String topic) { 41 | this.producer = producer; 42 | this.topic = topic; 43 | this.metricList = new LinkedList<>(); 44 | } 45 | 46 | /** 47 | * This method returns the current size of the JSON documents list 48 | * 49 | * @return integer representing the size of the JSON documents list 50 | */ 51 | public int getListSize() { 52 | return this.metricList.size(); 53 | } 54 | 55 | /** This method closes the producer */ 56 | public void closeProducer() { 57 | this.producer.flush(); 58 | this.producer.close(); 59 | } 60 | 61 | /** This method clears the JSON documents list */ 62 | public void clearList() { 63 | this.metricList.clear(); 64 | } 65 | 66 | /** 67 | * This method adds a metric to the list (metricList). 68 | * 69 | * @param metric String parameter representing a JSON document for Kafka 70 | */ 71 | public void addToList(String metric) { 72 | this.metricList.add(metric); 73 | } 74 | 75 | /** This method publishes the documents present in the list (metricList). */ 76 | public void publishMetrics() { 77 | 78 | long time = System.currentTimeMillis(); 79 | for (int i = 0; i < this.metricList.size(); i++) { 80 | final ProducerRecord record = 81 | new ProducerRecord<>(this.topic, i + time, metricList.get(i)); 82 | producer.send( 83 | record, 84 | (metadata, exception) -> { 85 | long elapsedTime = System.currentTimeMillis() - time; 86 | if (metadata != null) { 87 | if (logger.isDebugEnabled()) { 88 | logger.debug( 89 | "Record sent with (key=%s value=%s) " 90 | + "meta(partition=%d, offset=%d) time=%d\n", 91 | record.key(), 92 | record.value(), 93 | metadata.partition(), 94 | metadata.offset(), 95 | elapsedTime); 96 | } 97 | } else { 98 | if (logger.isErrorEnabled()) { 99 | logger.error("Exception: " + exception); 100 | logger.error( 101 | "Kafka Backend Listener was unable to publish to the Kafka topic {}.", 102 | this.topic); 103 | } 104 | } 105 | }); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/io/github/rahulsinghai/jmeter/backendlistener/model/MetricsRow.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Rahul Singhai. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.rahulsinghai.jmeter.backendlistener.model; 18 | 19 | import java.net.InetAddress; 20 | import java.net.UnknownHostException; 21 | import java.text.ParseException; 22 | import java.text.SimpleDateFormat; 23 | import java.time.LocalDateTime; 24 | import java.time.format.DateTimeFormatter; 25 | import java.util.Calendar; 26 | import java.util.Date; 27 | import java.util.HashMap; 28 | import java.util.Iterator; 29 | import java.util.LinkedList; 30 | import java.util.Map; 31 | import java.util.Set; 32 | import org.apache.jmeter.assertions.AssertionResult; 33 | import org.apache.jmeter.samplers.SampleResult; 34 | import org.apache.jmeter.threads.JMeterContextService; 35 | import org.apache.jmeter.visualizers.backend.BackendListenerContext; 36 | import org.slf4j.Logger; 37 | import org.slf4j.LoggerFactory; 38 | 39 | public class MetricsRow { 40 | 41 | private static final Logger logger = LoggerFactory.getLogger(MetricsRow.class); 42 | private SampleResult sampleResult; 43 | private String kafkaTestMode; 44 | private String kafkaTimestamp; 45 | private int ciBuildNumber; 46 | private HashMap metricsMap; 47 | private Set fields; 48 | private boolean allReqHeaders; 49 | private boolean allResHeaders; 50 | 51 | public MetricsRow( 52 | SampleResult sr, 53 | String testMode, 54 | String timeStamp, 55 | int buildNumber, 56 | boolean parseReqHeaders, 57 | boolean parseResHeaders, 58 | Set fields) { 59 | this.sampleResult = sr; 60 | this.kafkaTestMode = testMode.trim(); 61 | this.kafkaTimestamp = timeStamp.trim(); 62 | this.ciBuildNumber = buildNumber; 63 | this.metricsMap = new HashMap<>(); 64 | this.allReqHeaders = parseReqHeaders; 65 | this.allResHeaders = parseResHeaders; 66 | this.fields = fields; 67 | } 68 | 69 | /** 70 | * This method returns the current row as a Map(String, Object) for the provided sampleResult 71 | * 72 | * @param context BackendListenerContext 73 | * @param servicePrefixName Prefix string denoting the service name. This will allow to skip 74 | * adding all service specific parameters to the metrics row. 75 | * @return A Map(String, Object) comprising all the metrics as key value objects 76 | * @throws UnknownHostException If unable to determine injector host name. 77 | */ 78 | public Map getRowAsMap(BackendListenerContext context, String servicePrefixName) 79 | throws UnknownHostException { 80 | SimpleDateFormat sdf = new SimpleDateFormat(this.kafkaTimestamp); 81 | 82 | // add all the default SampleResult parameters 83 | addFilteredMetricToMetricsMap("AllThreads", this.sampleResult.getAllThreads()); 84 | addFilteredMetricToMetricsMap("BodySize", this.sampleResult.getBodySizeAsLong()); 85 | addFilteredMetricToMetricsMap("Bytes", this.sampleResult.getBytesAsLong()); 86 | addFilteredMetricToMetricsMap("SentBytes", this.sampleResult.getSentBytes()); 87 | addFilteredMetricToMetricsMap("ConnectTime", this.sampleResult.getConnectTime()); 88 | addFilteredMetricToMetricsMap("ContentType", this.sampleResult.getContentType()); 89 | addFilteredMetricToMetricsMap("DataType", this.sampleResult.getDataType()); 90 | addFilteredMetricToMetricsMap("ErrorCount", this.sampleResult.getErrorCount()); 91 | addFilteredMetricToMetricsMap("GrpThreads", this.sampleResult.getGroupThreads()); 92 | addFilteredMetricToMetricsMap("IdleTime", this.sampleResult.getIdleTime()); 93 | addFilteredMetricToMetricsMap("Latency", this.sampleResult.getLatency()); 94 | addFilteredMetricToMetricsMap("ResponseTime", this.sampleResult.getTime()); 95 | addFilteredMetricToMetricsMap("SampleCount", this.sampleResult.getSampleCount()); 96 | addFilteredMetricToMetricsMap("SampleLabel", this.sampleResult.getSampleLabel()); 97 | addFilteredMetricToMetricsMap("ThreadName", this.sampleResult.getThreadName()); 98 | addFilteredMetricToMetricsMap("URL", this.sampleResult.getURL()); 99 | addFilteredMetricToMetricsMap("ResponseCode", this.sampleResult.getResponseCode()); 100 | addFilteredMetricToMetricsMap("TestStartTime", JMeterContextService.getTestStartTime()); 101 | addFilteredMetricToMetricsMap( 102 | "SampleStartTime", sdf.format(new Date(this.sampleResult.getStartTime()))); 103 | addFilteredMetricToMetricsMap( 104 | "SampleEndTime", sdf.format(new Date(this.sampleResult.getEndTime()))); 105 | addFilteredMetricToMetricsMap( 106 | "Timestamp", sdf.format(new Date(this.sampleResult.getTimeStamp()))); 107 | addFilteredMetricToMetricsMap("InjectorHostname", InetAddress.getLocalHost().getHostName()); 108 | 109 | // Add the details according to the mode that is set 110 | switch (this.kafkaTestMode) { 111 | case "debug": 112 | case "error": 113 | addDetails(); 114 | break; 115 | case "info": 116 | if (!this.sampleResult.isSuccessful()) { 117 | addDetails(); 118 | } 119 | break; 120 | default: 121 | break; 122 | } 123 | 124 | addAssertions(); 125 | addElapsedTime(sdf); 126 | addCustomFields(context, servicePrefixName); 127 | parseHeadersAsJsonProps(this.allReqHeaders, this.allResHeaders); 128 | 129 | return this.metricsMap; 130 | } 131 | 132 | /** This method adds all the assertions for the current sampleResult */ 133 | private void addAssertions() { 134 | AssertionResult[] assertionResults = this.sampleResult.getAssertionResults(); 135 | if (assertionResults != null) { 136 | @SuppressWarnings("unchecked") 137 | HashMap[] assertionArray = new HashMap[assertionResults.length]; 138 | int i = 0; 139 | StringBuilder failureMessageStringBuilder = new StringBuilder(); 140 | boolean isFailure = false; 141 | for (AssertionResult assertionResult : assertionResults) { 142 | HashMap assertionMap = new HashMap<>(); 143 | boolean failure = assertionResult.isFailure() || assertionResult.isError(); 144 | isFailure = isFailure || assertionResult.isFailure() || assertionResult.isError(); 145 | assertionMap.put("failure", failure); 146 | assertionMap.put("failureMessage", assertionResult.getFailureMessage()); 147 | failureMessageStringBuilder.append(assertionResult.getFailureMessage()); 148 | failureMessageStringBuilder.append("\n"); 149 | assertionMap.put("name", assertionResult.getName()); 150 | assertionArray[i] = assertionMap; 151 | i++; 152 | } 153 | addFilteredMetricToMetricsMap("AssertionResults", assertionArray); 154 | addFilteredMetricToMetricsMap("FailureMessage", failureMessageStringBuilder.toString()); 155 | addFilteredMetricToMetricsMap("Success", !isFailure); 156 | } 157 | } 158 | 159 | /** 160 | * This method adds the ElapsedTime as a key:value pair in the metricsMap object. Also, depending 161 | * on whether or not the tests were launched from a CI tool (i.e Jenkins), it will add a 162 | * hard-coded version of the ElapsedTime for results comparison purposes 163 | * 164 | * @param sdf SimpleDateFormat 165 | */ 166 | private void addElapsedTime(SimpleDateFormat sdf) { 167 | Date elapsedTime; 168 | 169 | if (this.ciBuildNumber != 0) { 170 | elapsedTime = getElapsedTime(true); 171 | addFilteredMetricToMetricsMap("BuildNumber", this.ciBuildNumber); 172 | 173 | if (elapsedTime != null) { 174 | addFilteredMetricToMetricsMap("ElapsedTimeComparison", sdf.format(elapsedTime)); 175 | } 176 | } 177 | 178 | elapsedTime = getElapsedTime(false); 179 | if (elapsedTime != null) { 180 | addFilteredMetricToMetricsMap("ElapsedTime", sdf.format(elapsedTime)); 181 | } 182 | } 183 | 184 | /** 185 | * Methods that add all custom fields added by the user in the Backend Listener's GUI panel 186 | * 187 | * @param context BackendListenerContext 188 | */ 189 | private void addCustomFields(BackendListenerContext context, String servicePrefixName) { 190 | Iterator pluginParameters = context.getParameterNamesIterator(); 191 | while (pluginParameters.hasNext()) { 192 | String parameterName = pluginParameters.next(); 193 | 194 | if (!parameterName.startsWith(servicePrefixName) 195 | && !context.getParameter(parameterName).trim().equals("")) { 196 | String parameter = context.getParameter(parameterName).trim(); 197 | 198 | try { 199 | addFilteredMetricToMetricsMap(parameterName, Long.parseLong(parameter)); 200 | } catch (Exception e) { 201 | if (logger.isDebugEnabled()) { 202 | logger.debug("Cannot convert custom field to number"); 203 | } 204 | addFilteredMetricToMetricsMap(parameterName, context.getParameter(parameterName).trim()); 205 | } 206 | } 207 | } 208 | } 209 | 210 | /** Method that adds the request and response's body/headers */ 211 | private void addDetails() { 212 | addFilteredMetricToMetricsMap("RequestHeaders", this.sampleResult.getRequestHeaders()); 213 | addFilteredMetricToMetricsMap("RequestBody", this.sampleResult.getSamplerData()); 214 | addFilteredMetricToMetricsMap("ResponseHeaders", this.sampleResult.getResponseHeaders()); 215 | addFilteredMetricToMetricsMap("ResponseBody", this.sampleResult.getResponseDataAsString()); 216 | addFilteredMetricToMetricsMap("ResponseMessage", this.sampleResult.getResponseMessage()); 217 | } 218 | 219 | /** 220 | * This method will parse the headers and look for custom variables passed through as header. It 221 | * can also separate all headers into different Kafka document properties by passing "true". This 222 | * is a work-around the native behaviour of JMeter where variables are not accessible within the 223 | * backend listener. 224 | * 225 | * @param allReqHeaders boolean to determine if the user wants to separate ALL request headers 226 | * into different JSON properties. 227 | * @param allResHeaders boolean to determine if the user wants to separate ALL response headers 228 | * into different JSON properties. 229 | *

NOTE: This will be fixed as soon as a patch comes in for JMeter to change the behaviour. 230 | */ 231 | private void parseHeadersAsJsonProps(boolean allReqHeaders, boolean allResHeaders) { 232 | LinkedList headersArrayList = new LinkedList<>(); 233 | 234 | if (allReqHeaders) { 235 | headersArrayList.add(this.sampleResult.getRequestHeaders().split("\n")); 236 | } 237 | 238 | if (allResHeaders) { 239 | headersArrayList.add(this.sampleResult.getResponseHeaders().split("\n")); 240 | } 241 | 242 | for (String[] lines : headersArrayList) { 243 | for (String line : lines) { 244 | String[] header = line.split(":", 2); 245 | 246 | // if not all req headers and header contains special X-tag 247 | if (header.length > 1) { 248 | if (!this.allReqHeaders && header[0].startsWith("X-kafka-backend")) { 249 | this.metricsMap.put(header[0].replaceAll("kafka-", "").trim(), header[1].trim()); 250 | } else { 251 | this.metricsMap.put(header[0].replaceAll("kafka-", "").trim(), header[1].trim()); 252 | } 253 | } 254 | } 255 | } 256 | } 257 | 258 | /** 259 | * Adds a given key-value pair to metricsMap if the key is contained in the field filter or in 260 | * case of empty field filter 261 | */ 262 | private void addFilteredMetricToMetricsMap(String key, Object value) { 263 | if (this.fields.size() == 0 || this.fields.contains(key.toLowerCase())) { 264 | this.metricsMap.put(key, value); 265 | } 266 | } 267 | 268 | /** 269 | * This method is meant to return the elapsed time in a human readable format. The purpose of this 270 | * is mostly for build comparison in Kibana. By doing this, the user is able to set the X-axis of 271 | * his graph to this date and split the series by build numbers. It allows him to overlap test 272 | * results and see if there is regression or not. 273 | * 274 | * @param forBuildComparison boolean to determine if there is CI (continuous integration) or not 275 | * @return The elapsed time in YYYY-MM-dd HH:mm:ss format 276 | */ 277 | private Date getElapsedTime(boolean forBuildComparison) { 278 | String sElapsed; 279 | // Calculate the elapsed time (Starting from midnight on a random day - enables us to compare of 280 | // two loads over their duration) 281 | long start = JMeterContextService.getTestStartTime(); 282 | long end = System.currentTimeMillis(); 283 | long elapsed = (end - start); 284 | long minutes = (elapsed / 1000) / 60; 285 | long seconds = (elapsed / 1000) % 60; 286 | 287 | Calendar cal = Calendar.getInstance(); 288 | cal.set( 289 | Calendar.HOUR_OF_DAY, 290 | 0); // If there is more than an hour of data, the number of minutes/seconds will increment 291 | // this 292 | cal.set(Calendar.MINUTE, (int) minutes); 293 | cal.set(Calendar.SECOND, (int) seconds); 294 | 295 | if (forBuildComparison) { 296 | sElapsed = 297 | String.format( 298 | "2019-07-01 %02d:%02d:%02d", 299 | cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND)); 300 | } else { 301 | sElapsed = 302 | String.format( 303 | "%s %02d:%02d:%02d", 304 | DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()), 305 | cal.get(Calendar.HOUR_OF_DAY), 306 | cal.get(Calendar.MINUTE), 307 | cal.get(Calendar.SECOND)); 308 | } 309 | 310 | SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 311 | try { 312 | return formatter.parse(sElapsed); 313 | } catch (ParseException e) { 314 | logger.error("Unexpected error occurred computing elapsed date", e); 315 | return null; 316 | } 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/test/java/io/github/rahulsinghai/jmeter/backendlistener/kafka/TestKafkaBackendClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Rahul Singhai. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.rahulsinghai.jmeter.backendlistener.kafka; 18 | 19 | import static org.junit.Assert.assertNotNull; 20 | 21 | import org.apache.jmeter.config.Arguments; 22 | import org.junit.jupiter.api.BeforeAll; 23 | import org.junit.jupiter.api.Test; 24 | 25 | public class TestKafkaBackendClient { 26 | 27 | private static KafkaBackendClient client; 28 | 29 | @BeforeAll 30 | public static void setUp() { 31 | client = new KafkaBackendClient(); 32 | } 33 | 34 | @Test 35 | public void testGetDefaultParameters() { 36 | Arguments args = client.getDefaultParameters(); 37 | assertNotNull(args); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/io/github/rahulsinghai/jmeter/backendlistener/kafka/TestKafkaMetricPublisher.java: -------------------------------------------------------------------------------- 1 | package io.github.rahulsinghai.jmeter.backendlistener.kafka; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class TestKafkaMetricPublisher { 8 | 9 | @Test 10 | public void testMetricList() { 11 | KafkaMetricPublisher pub = new KafkaMetricPublisher(null, null); 12 | assertEquals(pub.getListSize(), 0); 13 | pub.addToList("metric1"); 14 | assertEquals(pub.getListSize(), 1); 15 | pub.clearList(); 16 | assertEquals(pub.getListSize(), 0); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/io/github/rahulsinghai/jmeter/backendlistener/model/TestMetricsRow.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Rahul Singhai. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.rahulsinghai.jmeter.backendlistener.model; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | import static org.junit.Assert.assertNotNull; 21 | 22 | import java.net.UnknownHostException; 23 | import java.util.HashSet; 24 | import java.util.Map; 25 | import org.apache.jmeter.assertions.AssertionResult; 26 | import org.apache.jmeter.config.Arguments; 27 | import org.apache.jmeter.samplers.SampleResult; 28 | import org.apache.jmeter.visualizers.backend.BackendListenerContext; 29 | import org.junit.jupiter.api.AfterAll; 30 | import org.junit.jupiter.api.BeforeAll; 31 | import org.junit.jupiter.api.Test; 32 | 33 | public class TestMetricsRow { 34 | 35 | private static BackendListenerContext context; 36 | 37 | @BeforeAll 38 | public static void setUp() { 39 | final Arguments arguments = new Arguments(); 40 | arguments.addArgument("customArg1", Boolean.toString(false)); 41 | arguments.addArgument("customArg2", "Test project"); 42 | arguments.addArgument("customArg3", "1"); 43 | context = new BackendListenerContext(arguments); 44 | } 45 | 46 | @AfterAll 47 | public static void tearDown() { 48 | context = null; 49 | } 50 | 51 | @Test 52 | public void testGetMetricInfo() throws UnknownHostException { 53 | SampleResult sampleResult = new SampleResult(); 54 | sampleResult.sampleStart(); 55 | try { 56 | Thread.sleep(110); 57 | } catch (InterruptedException e) { 58 | e.printStackTrace(); 59 | } 60 | sampleResult.setBytes(100L); 61 | sampleResult.setSampleLabel("Test Sample"); 62 | sampleResult.setEncodingAndType("text/html"); 63 | sampleResult.setSuccessful(false); 64 | 65 | AssertionResult assertResult = new AssertionResult("assertion1"); 66 | assertResult.setResultForNull(); 67 | sampleResult.addAssertionResult(assertResult); 68 | sampleResult.sampleEnd(); 69 | 70 | String servicePrefixName = "kafka."; 71 | MetricsRow metricsRow = 72 | new MetricsRow( 73 | sampleResult, "info", "yyyy-MM-dd'T'HH:mm:ss.SSSZZ", 0, false, false, new HashSet<>()); 74 | Map mapMetric = metricsRow.getRowAsMap(context, servicePrefixName); 75 | assertNotNull(mapMetric); 76 | assertNotNull(mapMetric.get("SampleLabel")); 77 | assertEquals(mapMetric.get("SampleLabel").toString(), "Test Sample"); 78 | } 79 | 80 | @Test 81 | public void testGetMetricError() throws UnknownHostException { 82 | SampleResult sampleResult = new SampleResult(); 83 | sampleResult.sampleStart(); 84 | try { 85 | Thread.sleep(110); 86 | } catch (InterruptedException e) { 87 | e.printStackTrace(); 88 | } 89 | sampleResult.setBytes(100L); 90 | sampleResult.setSampleLabel("Test Sample"); 91 | sampleResult.setEncodingAndType("text/html"); 92 | sampleResult.setSuccessful(true); 93 | sampleResult.setResponseHeaders("X-kafka-backend:true\\nresponse-header:test"); 94 | sampleResult.sampleEnd(); 95 | 96 | String servicePrefixName = "kafka."; 97 | MetricsRow metricsRow = 98 | new MetricsRow( 99 | sampleResult, "error", "yyyy-MM-dd'T'HH:mm:ss.SSSZZ", 1, false, true, new HashSet<>()); 100 | Map mapMetric = metricsRow.getRowAsMap(context, servicePrefixName); 101 | assertNotNull(mapMetric); 102 | assertNotNull(mapMetric.get("SampleLabel")); 103 | assertEquals(mapMetric.get("SampleLabel").toString(), "Test Sample"); 104 | } 105 | } 106 | --------------------------------------------------------------------------------