├── .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 | [](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 | [](https://codecov.io/gh/rahulsinghai/jmeter-backend-listener-kafka)
5 | [](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 | 
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 | 
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 |
Simply add a \"Backend Listener\" and change the implementation
Change the listener's configuration
To use filters, simply separate their name by a semicolon. (f1;f2;f3)
",
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 | *
117 | *
acks=0
118 | *
acks=1
119 | *
acks=all
120 | *
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 |
--------------------------------------------------------------------------------